├── .github ├── dependabot.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── BENEFITS.md ├── DRAWBACKS.md ├── LICENSE ├── PROPOSAL.md ├── README.md ├── WHY_NEST_IMPORTS.md ├── examples ├── babel-parser-example │ ├── README.md │ ├── index.js │ ├── module.js │ ├── package-lock.json │ └── package.json ├── custom-cache-example │ ├── README.md │ ├── index.js │ ├── module.js │ └── package.json └── top-level-example │ ├── README.md │ ├── index.js │ ├── module.js │ └── package.json ├── lib ├── assignment-visitor.js ├── compiler.js ├── empty.js ├── fast-object.js ├── fast-path.js ├── import-export-visitor.js ├── options.js ├── parsers │ ├── acorn-extensions │ │ ├── export.js │ │ ├── import.js │ │ ├── index.js │ │ └── tolerance.js │ ├── acorn.js │ ├── babel.js │ ├── babylon.js │ ├── default.js │ └── top-level.js ├── runtime │ ├── entry.js │ ├── index.js │ └── utils.js ├── transform.js ├── utils.js └── visitor.js ├── node ├── caching-compiler.js ├── compile-hook.js ├── data.js ├── fs.js ├── index.js ├── pkg-info.js ├── repl-hook.js ├── utils.js ├── version.js └── wrapper.js ├── package-lock.json ├── package.json ├── plugins └── babel.js ├── repl └── index.js └── test ├── all-files.js ├── babel-plugin-tests.js ├── cjs ├── bridge.js ├── export-default-property.js ├── module-exports-esModule.js ├── module-exports-function.js ├── module-exports-null.js └── module-exports-object.js ├── compiler-tests.js ├── compiler ├── lines.js └── strict.js ├── default-expression.js ├── default-function.js ├── export-tests.js ├── export ├── all-lodash.js ├── all-multiple.js ├── all-mutual-a.js ├── all-mutual-b.js ├── all.js ├── array-pattern-holes.js ├── class.js ├── common.js ├── cycle-a.js ├── cycle-b.js ├── declarations.js ├── declarations │ ├── basic.js │ └── block.js ├── def.js ├── default-expression.js ├── default-function.js ├── default │ ├── anon-class.js │ ├── anon-function.js │ ├── array.js │ ├── from.js │ ├── function.js │ ├── identifier.js │ ├── named-class.js │ ├── number.js │ └── object.js ├── destructuring.js ├── extensions.js ├── from-ordering-a.js ├── from-ordering-b.js ├── from-ordering-c.js ├── from-ordering-common.js ├── from-ordering.js ├── from.js ├── later.js ├── names.js ├── nested.js ├── renamed.js ├── sealed.js ├── shadowed.js ├── some.js └── swap-later.js ├── file-extension-tests.js ├── file-extension └── a.mjs ├── hoisting-tests.js ├── hoisting ├── a.js └── b.js ├── import-tests.js ├── import ├── cycle-a.js ├── cycle-b.js ├── extensions.js └── name.js ├── misc-tests.js ├── misc ├── abc.js ├── export-all-with-default.js ├── export-all.js ├── export-default.js ├── live.js ├── order-a.js ├── order-b.js ├── order-c.js ├── order-tracker.js └── risky-exports.js ├── node_modules ├── disabled │ └── index.js └── enabled │ ├── enabled.js │ └── package.json ├── output-tests.js ├── output ├── anon-class │ ├── actual.js │ └── expected.js ├── declarations-basic │ ├── actual.js │ └── expected.js ├── default-expression │ ├── actual.js │ └── expected.js ├── default-function │ ├── actual.js │ └── expected.js ├── eval │ ├── actual.js │ └── expected.js ├── export-all-multiple │ ├── actual.js │ └── expected.js ├── export-all │ ├── actual.js │ └── expected.js ├── export-multi-namespace │ ├── actual.js │ └── expected.js ├── export-some │ ├── actual.js │ └── expected.js ├── lines │ ├── actual.js │ └── expected.js ├── live │ ├── actual.js │ └── expected.js ├── mixed-keys │ ├── actual.js │ └── expected.js ├── name │ ├── actual.js │ └── expected.js ├── nested │ ├── actual.js │ └── expected.js ├── only-nested │ ├── actual.js │ └── expected.js └── strict │ ├── actual.js │ └── expected.js ├── package.json ├── repl-tests.js ├── run.js ├── run.sh ├── setter-tests.js ├── setter ├── child.js ├── cycle │ ├── cjs.js │ └── esm.js ├── eval.js ├── fake-const.js ├── grandchild.js └── parent.js ├── tests.js ├── transform-tests.js └── undeclared-export.js /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "npm" # See documentation for possible values 7 | directory: "/" # Location of package manifests 8 | schedule: 9 | interval: "daily" 10 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x, 15.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | test/.cache 4 | test/**/*.gz 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /examples 2 | /node_modules 3 | /packages 4 | /test 5 | -------------------------------------------------------------------------------- /BENEFITS.md: -------------------------------------------------------------------------------- 1 | Benefits 2 | --- 3 | * Dead-simple installation and setup: `npm install --save reify`, `require("reify")` 4 | * Works with every version of Node. 5 | * Put `import` and `export` statements anywhere you like. 6 | * They're just statements. 7 | * If you have a philosophical preference for restricting them to the top-level, there are plenty of ways to enforce that. 8 | * This library doesn't limit you either way. 9 | * Imported symbols are just normal variables. 10 | * Debuggers and dev tools display the symbols as you wrote them, unmangled by a transpiler. 11 | * Not having to rename variables references leads to much faster compilation. 12 | * Variables are confined to the scope where they were imported. 13 | * `import` statements work in the Node REPL. 14 | * Any existing module or package can be imported. 15 | * Babel's `interopRequire{Default,Wildcard}` logic is captured by `Module.prototype.getExportByName`. 16 | * Does not interfere with existing `require.extensions` hooks. 17 | * Much simpler transform implementation as compared with other ECMASCript module compilers. 18 | * `import` statements become calls to `module.importSync(id, setters)` 19 | * `export` statements become assignments to the `exports` object. 20 | * The compiler doesn't have to parse the entire file to find `import` and `export` statements. 21 | * Line numbers are strictly preserved. 22 | * No hoisting of `import` statements. 23 | * All `import` and `export` statements are compiled in-place. 24 | * No need for `Object.defineProperty`-style getters to support `export * from "..."`. 25 | * The `module.importSync(id, setters)` API is human-writable, too, if you want to implement custom update logic. 26 | * Generated code is statically analyzable, since the `exports` object is not exposed. 27 | -------------------------------------------------------------------------------- /DRAWBACKS.md: -------------------------------------------------------------------------------- 1 | Drawbacks 2 | --- 3 | 4 | * Only works with CommonJS module systems that are very similar to Node's module system. 5 | * Adds some overhead to module evaluation. 6 | * ~~Affects every yet-to-be required module in the Node process.~~
7 | _Only modules in packages that explicitly depend on `reify`._ 8 | * Not a general solution for other kinds of JavaScript compilation. 9 | * Not a solution for asynchronous module loading, yet. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ben Newman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PROPOSAL.md: -------------------------------------------------------------------------------- 1 | # ECMAScript Proposal: Nested `import` declarations 2 | 3 | **Stage:** 0 4 | 5 | **Champion:** Ben Newman (Meteor Development Group) 6 | 7 | **Specification:** Work in progress; see attached [commits](https://github.com/tc39/ecma262/pull/646/commits). 8 | 9 | 10 | ## Summary 11 | 12 | This proposal argues for relaxing the current restriction that `import` declarations may appear only at the top level of a module. 13 | 14 | Specifically, this proposal would allow `import` declarations that are 15 | 16 | 1. **nestable** within functions and blocks, enabling multiple new and worthwhile `import` idioms; 17 | 1. **hoisted** to the beginning of the enclosing scope; that is, _declarative_ rather than _imperative_; 18 | 1. **lexically scoped** to the enclosing block; 19 | 1. **synchronously evaluated**, with clear and manageable consequences for runtime module fetching; and 20 | 1. **backwards compatible** with the semantics of existing top-level `import` declarations. 21 | 22 | At this time, no changes are proposed regarding the placement or semantics of `export` declarations. In the opinion of the author, keeping all `export` declarations at the top level remains important for many of the static properties of the ECMAScript module system. 23 | 24 | 25 | ## Motivating examples 26 | 27 | ### Isolated scopes 28 | 29 | Consider writing a simple BDD-style unit test that involves importing a symbol called `check` from three different modules: 30 | 31 | ```js 32 | describe("fancy feature #5", () => { 33 | import { strictEqual } from "assert"; 34 | 35 | it("should work on the client", () => { 36 | import { check } from "./client.js"; 37 | strictEqual(check(), "client ok"); 38 | }); 39 | 40 | it("should work on the server", () => { 41 | import { check } from "./server.js"; 42 | strictEqual(check(), "server ok"); 43 | }); 44 | 45 | it("should work on both client and server", () => { 46 | import { check } from "./both.js"; 47 | strictEqual(check(), "both ok"); 48 | }); 49 | }); 50 | ``` 51 | 52 | If `import` declarations could not appear in nested scopes, you would have to write this code differently: 53 | 54 | ```js 55 | import { strictEqual } from "assert"; 56 | import { check as checkClient } from "./client.js"; 57 | import { check as checkServer } from "./server.js"; 58 | import { check as checkBoth } from "./both.js"; 59 | 60 | describe("fancy feature #5", () => { 61 | it("should work on the client", () => { 62 | strictEqual(checkClient(), "client ok"); 63 | }); 64 | 65 | it("should work on the client", () => { 66 | strictEqual(checkServer(), "server ok"); 67 | }); 68 | 69 | it("should work on both client and server", () => { 70 | strictEqual(checkBoth(), "both ok"); 71 | }); 72 | }); 73 | ``` 74 | 75 | This manual renaming is annoying, and potentially surprising to developers coming from other languages (or even Node), so there needs to be a good reason why it's necessary in JavaScript. Conversely, if it isn't necessary, because there exists a reasonable implementation of nested `import` declarations, then it becomes harder to claim we have a good reason for the top-level restriction. 76 | 77 | In the second version of this code, since the `import` declarations are evaluated before the tests are defined, any exceptions thrown by importing the modules will prevent your tests from running at all! Compare this behavior to that of the original code, where each test captures any and all exceptions resulting from its own particular `import` declaration and `strictEqual(check(), ...)` call. 78 | 79 | ### Lazy evaluation 80 | 81 | As the previous example suggests, putting all your `import` declarations at the top level of your modules means you pay the performance cost of evaluating all your modules at startup time, even if they are not used until much later—or in some cases never used at all. 82 | 83 | If you have the ability to nest `import` declarations in the immediate scope where the imported symbols will be used, then you can take full advantage of your application's specific needs, and there is nothing to stop you from front-loading your imports if that makes sense for your application. 84 | 85 | Eager evaluation of the entire dependency tree is fine for long-running applications like servers, but not so great for short-lived, multi-purpose utilities like command-line tools, or client-side applications that must evaluate modules while the user waits. For example, the [WebTorrent Desktop app](https://webtorrent.io/) was able to reduce startup time dramatically [by deferring `require` calls](https://mobile.twitter.com/WebTorrentApp/status/737890973733244928). This optimization would not have been possible if they could only use `import` declarations at the top level. 86 | 87 | ### Colocation of `import` declarations with consuming code 88 | 89 | When you delete code that contains a nested `import` declaration, you don't have to scroll up to the top of the file and search through a laundry list of other `import` declarations, then search through the rest of the file for any other references to the imported symbols. The scope of the nested `import` declaration is obvious, so it's easy to tell when it's safe to delete. 90 | 91 | ### Optimistic imports 92 | 93 | Perhaps you would like to use a module if it is available, but it's hardly the end of the world if it's not: 94 | 95 | ```js 96 | try { 97 | import esc from "enhanced-super-console"; 98 | console = esc; 99 | } catch (e) { 100 | // That's ok, we'll just stick to the usual implementations of 101 | // console.log, .error, .trace, etc., or stub them out. 102 | } 103 | ``` 104 | 105 | Without the ability to nest `import` declarations inside `try`-`catch` blocks, there would be no way to achieve this progressive enhancement of the `console` object. 106 | 107 | 108 | ### Dead code elimination 109 | 110 | Nested `import` declarations make certain static optimizations significantly more effective; for example, dead code elimination via constant propagation. 111 | 112 | A sophisticated bundling tool might eliminate unreachable code based on the known values of certain constants: 113 | 114 | ```js 115 | if (__USER_ROLE__ === "admin") { 116 | import { setUpAdminTools } from "../admin/tools"; 117 | setUpAdminTools(user.id); 118 | } 119 | ``` 120 | 121 | Ideally this code would disappear completely from your public JavaScript assets (whenever `__USER_ROLE__ !== "admin"`), along with the `../admin/tools` module, assuming it is not used elsewhere by non-admin code. 122 | 123 | > Aside: when `__USER_ROLE__ === "admin"`, note that the body of the condition must remain a block statement, so that `setUpAdminTools` remains scoped to that block, rather than becoming visible to surrounding code. In other words, nested `import`s are still important even after dead code elimination has taken place. 124 | 125 | Without the ability to nest `import` declarations inside conditional blocks, dead code elimination becomes the responsibility of the imported module. You would likely have to wrap the entire body of the `../admin/tools` module with a conditional block, and even then the empty `../admin/tools` module would still be included in the public bundle, which might constitute a leak of sensitive information. 126 | 127 | Worse still, as long as `import` declarations are restricted to the top level, the `import`s of `../admin/tools` cannot be included in the conditional block, making any kind of dead module elimination impossible. 128 | 129 | 130 | ## Semantic details 131 | 132 | ### Backwards compatibility 133 | 134 | At this stage of the ECMAScript specification, any backwards-incompatible change to the existing semantics of top-level `import` declarations would require extraordinary motivation. 135 | 136 | This proposal makes no such suggestion. In fact, the viability of this proposal very much hinges on the technical possibility of allowing nested `import` declarations without breaking existing code. 137 | 138 | For example (while I do not consider the precise evaluation order of modules to be a matter of backwards compatibility), I see no reason why a program that uses only top-level `import` declarations should necessarily evaluate its modules in a different order, just because nested `import` declarations are allowed. 139 | 140 | In other words, I am proposing a strict expansion of the possible use cases for `import` declarations. 141 | 142 | 143 | ### Nesting 144 | 145 | In terms of the grammar, I propose that 146 | 147 |     _ModuleItem_ :
148 |         _ImportDeclaration_
149 |         _ExportDeclaration_
150 |         _StatementListItem_ 151 | 152 |     _StatementListItem_ :
153 |         _Statement_
154 |         _Declaration_ 155 | 156 | be modified as follows: 157 | 158 |     _ModuleItem_ :
159 |         _ExportDeclaration_
160 |         _StatementListItem_ 161 | 162 |     _StatementListItem_ :
163 |         _Statement_
164 |         _Declaration_
165 |         _ImportDeclaration_ 166 | 167 | This modification has a few subtleties worth highlighting: 168 | 169 | * It is important that _ImportDeclaration_ not be producible by a _Statement_ or a _Declaration_, else an _ExportDeclaration_ could export an _ImportDeclaration_, which is presumably undesirable. 170 | 171 | * This grammar would not allow `import` declarations of the following form: 172 | 173 | ```js 174 | if (condition) import "./sometimes"; 175 | ``` 176 | though it would allow 177 | ```js 178 | if (condition) { 179 | import "./sometimes"; 180 | } 181 | ``` 182 | Though potentially surprising, this seems reasonable on the grounds that 183 | ```js 184 | if (condition) let foo = bar; 185 | ``` 186 | is also currently illegal. However, if we want to allow braceless nested `import` declarations, it should be possible to modify the grammar for _IfStatement_ et al., so that the body/consequent/alternate can be either a _Statement_ or an _ImportDeclaration_. 187 | 188 | * It is not immediately obvious from this new grammar that an _ImportDeclaration_ may only appear in a _Module_. I believe the specification should enforce this restriction, but I am currently unsure how best to do so. As I understand it, there are three possibilities: 189 | 190 | * Thread [Import] subscripts throughout the grammar, similar to 191 | [Yield,Return]. 192 | * Verbally forbid _ImportDeclaration_ from appearing unless the goal symbol 193 | is _Module_. 194 | * Rely on runtime errors. 195 | 196 | 197 | ### Declarative hoisting 198 | 199 | TC39 has [previously discussed](https://github.com/tc39/ecma262/issues/368) at length whether `import` declarations should be *declarative* or *imperative*. In short, declarative `import` declarations take effect before any other code in the scope where the declaration appears, whereas imperative declarations may be interleaved with other declarations and statements. 200 | 201 | I believe nested `import` declarations allow an elegant synthesis of these two possible semantic choices. 202 | 203 | For selfish reasons, I was initially skeptical of declarative `import` declarations, because the imperative semantics are easier to implement with a [transpiler](https://github.com/benjamn/reify#how-it-works). Declarative `import` declarations require "hoisting" code to the beginning of the enclosing block, whereas imperative declarations can simply be rewritten in place. 204 | 205 | However, as I began to investigate the consequences of hoisting, I too became convinced that relying on the interleaving of `import` declarations with other statements is almost always a source of bugs, because you can only rarely know with confidence whether a particular `import` declaration is the first evaluator of the imported module. 206 | 207 | With that said, there are occasionally scenarios where it's frustrating that you can't just put a `debugger` statement before an `import` declaration and step into the imported module, wrap an `import` declaration with timing code, or carefully manage the order of exports between two mutually dependent modules. 208 | 209 | For those few scenarios, nested `import` declarations provide a convenient way of achieving imperative behavior: simply wrap the `import` in a `{...}` block statement or a function: 210 | 211 | ```js 212 | import { a, b } from "./ab"; 213 | 214 | // Execution might hit this debugger statement before the "./xy" module is 215 | // imported, if it has not been imported before. 216 | debugger; 217 | { 218 | import { x, y } from "./xy"; 219 | } 220 | 221 | console.log("x", getX()); 222 | 223 | // Imperatively import { x } from "./xy", and return it. 224 | function getX() { 225 | import { x } from "./xy"; 226 | return x; 227 | } 228 | 229 | // If you care about the latest live value of x, return a closure. 230 | function getXFn() { 231 | import { x } from "./xy"; 232 | return () => x; 233 | } 234 | 235 | const getX2 = getXFn(); 236 | 237 | setTimeout(() => { 238 | console.log("current x:", getX2()); 239 | }, delay); 240 | ``` 241 | 242 | Even if you find this syntax clunky, and even if you don't end up using it in production code, you have to admit it's useful when you need it in development. 243 | 244 | In other words, **nested `import` declarations clear the way for embracing declarative `import` semantics by default**, because nested `import` declarations provide a graceful escape hatch in rare cases when you think you need imperative `import` semantics. 245 | 246 | 247 | ### Lexical scoping 248 | 249 | If `import` declarations are hoisted to the beginning of the enclosing block, it seems natural that the imported symbols would have visibility similar to `const`- or `let`-declared variables, rather than `var` declarations. The goal of hoisting is to make imported symbols reliably usable throughout the enclosing block, so exposing the symbols outside that block would undermine the benefits of hoisting. 250 | 251 | I consider this point relatively uncontroversial, and perhaps even already implied by the specification, since a module is effectively a block scope within which top-level imported symbols are confined (i.e., they do not leak into the global environment). 252 | 253 | If you need to use an imported value outside the block where it was imported, you would need to assign it to a variable in the outer scope. If you are worried about the symbol changing values due to live binding, then you can create a closure in the scope of the `import` declaration that refers to the current value of the imported symbol, assign the closure function to some outer variable, and call that function to access the latest value of the imported symbol. 254 | 255 | 256 | ### Synchronous evaluation 257 | 258 | Given that the [WHATWG Loader Standard](https://github.com/whatwg/loader) has adopted an asynchronous (`Promise`-based) API for module loading, it is tempting to imagine a desugaring from `import` declarations to something like `await` expressions, e.g. 259 | 260 | ```js 261 | import { a, b } from "./c"; 262 | ``` 263 | 264 | might be interpreted as 265 | 266 | ```js 267 | const { a, b } = await loader.import("./c"); 268 | ``` 269 | 270 | This desugaring story has a number of fundamental flaws that lead me to believe nested `import` declarations should not be explained through desugaring, and that the `loader.import` API should continue to serve the important role of enabling _explicit_ asynchronous module loading. 271 | 272 | Problems with desugaring to asynchronous forms: 273 | 274 | * JavaScript has a strong precedent against any kind of implicit asynchronicity, or non-cooperative preemption, which is why we have `yield` and `await` instead of full coroutines or threads. Asynchronous module loading should be done explicitly using the `Promise`-based Loader API (or `