| 1 | # ES Module Lexer
|
|---|
| 2 |
|
|---|
| 3 | [![Build Status][actions-image]][actions-url]
|
|---|
| 4 |
|
|---|
| 5 | A JS module syntax lexer used in [es-module-shims](https://github.com/guybedford/es-module-shims).
|
|---|
| 6 |
|
|---|
| 7 | Outputs the list of exports and locations of import specifiers, including dynamic import and import meta handling.
|
|---|
| 8 |
|
|---|
| 9 | Supports new syntax features including import attributes and source phase imports.
|
|---|
| 10 |
|
|---|
| 11 | A very small single JS file (4KiB gzipped) that includes inlined Web Assembly for very fast source analysis of ECMAScript module syntax only.
|
|---|
| 12 |
|
|---|
| 13 | For an example of the performance, Angular 1 (720KiB) is fully parsed in 5ms, in comparison to the fastest JS parser, Acorn which takes over 100ms.
|
|---|
| 14 |
|
|---|
| 15 | _Comprehensively handles the JS language grammar while remaining small and fast. - ~10ms per MB of JS cold and ~5ms per MB of JS warm, [see benchmarks](#benchmarks) for more info._
|
|---|
| 16 |
|
|---|
| 17 | > [Built with](https://github.com/guybedford/es-module-lexer/blob/main/chompfile.toml) [Chomp](https://chompbuild.com/)
|
|---|
| 18 |
|
|---|
| 19 | ### Usage
|
|---|
| 20 |
|
|---|
| 21 | ```
|
|---|
| 22 | npm install es-module-lexer
|
|---|
| 23 | ```
|
|---|
| 24 |
|
|---|
| 25 | See [src/lexer.ts](src/lexer.ts) for the type definitions.
|
|---|
| 26 |
|
|---|
| 27 | For use in CommonJS:
|
|---|
| 28 |
|
|---|
| 29 | ```js
|
|---|
| 30 | const { init, parse } = require('es-module-lexer');
|
|---|
| 31 |
|
|---|
| 32 | (async () => {
|
|---|
| 33 | // either await init, or call parse asynchronously
|
|---|
| 34 | // this is necessary for the Web Assembly boot
|
|---|
| 35 | await init;
|
|---|
| 36 |
|
|---|
| 37 | const source = 'export var p = 5';
|
|---|
| 38 | const [imports, exports] = parse(source);
|
|---|
| 39 |
|
|---|
| 40 | // Returns "p"
|
|---|
| 41 | source.slice(exports[0].s, exports[0].e);
|
|---|
| 42 | // Returns "p"
|
|---|
| 43 | source.slice(exports[0].ls, exports[0].le);
|
|---|
| 44 | })();
|
|---|
| 45 | ```
|
|---|
| 46 |
|
|---|
| 47 | An ES module version is also available:
|
|---|
| 48 |
|
|---|
| 49 | ```js
|
|---|
| 50 | import { init, parse } from 'es-module-lexer';
|
|---|
| 51 |
|
|---|
| 52 | (async () => {
|
|---|
| 53 | await init;
|
|---|
| 54 |
|
|---|
| 55 | const source = `
|
|---|
| 56 | import { name } from 'mod\\u1011';
|
|---|
| 57 | import json from './json.json' with { type: 'json' }
|
|---|
| 58 | export var p = 5;
|
|---|
| 59 | export function q () {
|
|---|
| 60 |
|
|---|
| 61 | };
|
|---|
| 62 | export { x as 'external name' } from 'external';
|
|---|
| 63 |
|
|---|
| 64 | // Comments provided to demonstrate edge cases
|
|---|
| 65 | import /*comment!*/ ( 'asdf', { with: { type: 'json' }});
|
|---|
| 66 | import /*comment!*/.meta.asdf;
|
|---|
| 67 |
|
|---|
| 68 | // Source phase imports:
|
|---|
| 69 | import source mod from './mod.wasm';
|
|---|
| 70 | import.source('./mod.wasm');
|
|---|
| 71 | `;
|
|---|
| 72 |
|
|---|
| 73 | const [imports, exports] = parse(source, 'optional-sourcename');
|
|---|
| 74 |
|
|---|
| 75 | // Returns "modထ"
|
|---|
| 76 | imports[0].n
|
|---|
| 77 | // Returns "mod\u1011"
|
|---|
| 78 | source.slice(imports[0].s, imports[0].e);
|
|---|
| 79 | // "s" = start
|
|---|
| 80 | // "e" = end
|
|---|
| 81 |
|
|---|
| 82 | // Returns "import { name } from 'mod'"
|
|---|
| 83 | source.slice(imports[0].ss, imports[0].se);
|
|---|
| 84 | // "ss" = statement start
|
|---|
| 85 | // "se" = statement end
|
|---|
| 86 |
|
|---|
| 87 | // Returns "{ type: 'json' }"
|
|---|
| 88 | source.slice(imports[1].a, imports[1].se);
|
|---|
| 89 | // "a" = attribute start, -1 for no import attributes
|
|---|
| 90 |
|
|---|
| 91 | // Parsed import attributes are available in `at`
|
|---|
| 92 | // Returns [['type', 'json']]
|
|---|
| 93 | imports[1].at;
|
|---|
| 94 | // Returns 'json'
|
|---|
| 95 | imports[1].at[0][1];
|
|---|
| 96 |
|
|---|
| 97 | // Returns null (no attributes)
|
|---|
| 98 | imports[0].at;
|
|---|
| 99 |
|
|---|
| 100 | // Returns "external"
|
|---|
| 101 | source.slice(imports[2].s, imports[2].e);
|
|---|
| 102 |
|
|---|
| 103 | // Returns "p"
|
|---|
| 104 | source.slice(exports[0].s, exports[0].e);
|
|---|
| 105 | // Returns "p"
|
|---|
| 106 | source.slice(exports[0].ls, exports[0].le);
|
|---|
| 107 | // Returns "q"
|
|---|
| 108 | source.slice(exports[1].s, exports[1].e);
|
|---|
| 109 | // Returns "q"
|
|---|
| 110 | source.slice(exports[1].ls, exports[1].le);
|
|---|
| 111 | // Returns "'external name'"
|
|---|
| 112 | source.slice(exports[2].s, exports[2].e);
|
|---|
| 113 | // Returns -1
|
|---|
| 114 | exports[2].ls;
|
|---|
| 115 | // Returns -1
|
|---|
| 116 | exports[2].le;
|
|---|
| 117 |
|
|---|
| 118 | // Import type is provided by `t` value
|
|---|
| 119 | // (1 for static, 2, for dynamic)
|
|---|
| 120 | // Returns true
|
|---|
| 121 | imports[2].t == 2;
|
|---|
| 122 |
|
|---|
| 123 | // Returns "asdf" (only for string literal dynamic imports)
|
|---|
| 124 | imports[2].n
|
|---|
| 125 | // Returns "import /*comment!*/ ( 'asdf', { with: { type: 'json' } })"
|
|---|
| 126 | source.slice(imports[3].ss, imports[3].se);
|
|---|
| 127 | // Returns "'asdf'"
|
|---|
| 128 | source.slice(imports[3].s, imports[3].e);
|
|---|
| 129 | // Returns "( 'asdf', { with: { type: 'json' } })"
|
|---|
| 130 | source.slice(imports[3].d, imports[3].se);
|
|---|
| 131 | // Returns "{ with: { type: 'json' } }"
|
|---|
| 132 | source.slice(imports[3].a, imports[3].se - 1);
|
|---|
| 133 |
|
|---|
| 134 | // For non-string dynamic import expressions:
|
|---|
| 135 | // - n will be undefined
|
|---|
| 136 | // - a is currently -1 even if there is an import attribute
|
|---|
| 137 | // - e is currently the character before the closing )
|
|---|
| 138 |
|
|---|
| 139 | // For nested dynamic imports, the se value of the outer import is -1 as end tracking does not
|
|---|
| 140 | // currently support nested dynamic immports
|
|---|
| 141 |
|
|---|
| 142 | // import.meta is indicated by imports[3].d === -2
|
|---|
| 143 | // Returns true
|
|---|
| 144 | imports[4].d === -2;
|
|---|
| 145 | // Returns "import /*comment!*/.meta"
|
|---|
| 146 | source.slice(imports[4].s, imports[4].e);
|
|---|
| 147 | // ss and se are the same for import meta
|
|---|
| 148 |
|
|---|
| 149 | // Returns "'./mod.wasm'"
|
|---|
| 150 | source.slice(imports[5].s, imports[5].e);
|
|---|
| 151 |
|
|---|
| 152 | // Import type 4 and 5 for static and dynamic source phase
|
|---|
| 153 | imports[5].t === 4;
|
|---|
| 154 | imports[6].t === 5;
|
|---|
| 155 | })();
|
|---|
| 156 | ```
|
|---|
| 157 |
|
|---|
| 158 | ### CSP asm.js Build
|
|---|
| 159 |
|
|---|
| 160 | The default version of the library uses Wasm and (safe) eval usage for performance and a minimal footprint.
|
|---|
| 161 |
|
|---|
| 162 | Neither of these represent security escalation possibilities since there are no execution string injection vectors, but that can still violate existing CSP policies for applications.
|
|---|
| 163 |
|
|---|
| 164 | For a version that works with CSP eval disabled, use the `es-module-lexer/js` build:
|
|---|
| 165 |
|
|---|
| 166 | ```js
|
|---|
| 167 | import { parse } from 'es-module-lexer/js';
|
|---|
| 168 | ```
|
|---|
| 169 |
|
|---|
| 170 | Instead of Web Assembly, this uses an asm.js build which is almost as fast as the Wasm version ([see benchmarks below](#benchmarks)).
|
|---|
| 171 |
|
|---|
| 172 | ### Import Attributes
|
|---|
| 173 |
|
|---|
| 174 | The `a` field provides the index of the start of the `{` attributes bracket, or -1 for no attributes.
|
|---|
| 175 |
|
|---|
| 176 | The list of attribute key and value pairs are provided on the `at` field:
|
|---|
| 177 |
|
|---|
| 178 | ```js
|
|---|
| 179 | const [imports] = parse(`
|
|---|
| 180 | import json from './foo.json' with { type: 'json' };
|
|---|
| 181 | import './foo.css' with { type: 'css' };
|
|---|
| 182 | import pkg from 'pkg' with { type: 'json', integrity: 'sha384-...' };
|
|---|
| 183 | `);
|
|---|
| 184 |
|
|---|
| 185 | // Returns [['type', 'json']]
|
|---|
| 186 | imports[0].at;
|
|---|
| 187 |
|
|---|
| 188 | // Returns [['type', 'css']]
|
|---|
| 189 | imports[1].at;
|
|---|
| 190 |
|
|---|
| 191 | // Multiple attributes
|
|---|
| 192 | // Returns [['type', 'json'], ['integrity', 'sha384-...']]
|
|---|
| 193 | imports[2].at;
|
|---|
| 194 | ```
|
|---|
| 195 |
|
|---|
| 196 | The `at` field is an array of `[key, value]` tuples, or `null` if there are no attributes.
|
|---|
| 197 |
|
|---|
| 198 | Both keys and values support escape sequences:
|
|---|
| 199 |
|
|---|
| 200 | ```js
|
|---|
| 201 | const [imports] = parse(`
|
|---|
| 202 | import foo from './foo.js' with { "custom-key": "value" };
|
|---|
| 203 | import bar from './bar.js' with { "key\\nwith\\nnewlines": "value\\twith\\ttabs" };
|
|---|
| 204 | `);
|
|---|
| 205 |
|
|---|
| 206 | // Quoted keys are unquoted
|
|---|
| 207 | // Returns [['custom-key', 'value']]
|
|---|
| 208 | imports[0].at;
|
|---|
| 209 |
|
|---|
| 210 | // Escape sequences are processed
|
|---|
| 211 | // Returns [['key\nwith\nnewlines', 'value\twith\ttabs']]
|
|---|
| 212 | imports[1].at;
|
|---|
| 213 | ```
|
|---|
| 214 |
|
|---|
| 215 | ### Escape Sequences
|
|---|
| 216 |
|
|---|
| 217 | To handle escape sequences in specifier strings, the `.n` field of imported specifiers will be provided where possible.
|
|---|
| 218 |
|
|---|
| 219 | For dynamic import expressions, this field will be empty if not a valid JS string.
|
|---|
| 220 |
|
|---|
| 221 | ### Facade Detection
|
|---|
| 222 |
|
|---|
| 223 | Facade modules that only use import / export syntax can be detected via the third return value:
|
|---|
| 224 |
|
|---|
| 225 | ```js
|
|---|
| 226 | const [,, facade] = parse(`
|
|---|
| 227 | export * from 'external';
|
|---|
| 228 | import * as ns from 'external2';
|
|---|
| 229 | export { a as b } from 'external3';
|
|---|
| 230 | export { ns };
|
|---|
| 231 | `);
|
|---|
| 232 | facade === true;
|
|---|
| 233 | ```
|
|---|
| 234 |
|
|---|
| 235 | ### ESM Detection
|
|---|
| 236 |
|
|---|
| 237 | Modules that uses ESM syntaxes can be detected via the fourth return value:
|
|---|
| 238 |
|
|---|
| 239 | ```js
|
|---|
| 240 | const [,,, hasModuleSyntax] = parse(`
|
|---|
| 241 | export {}
|
|---|
| 242 | `);
|
|---|
| 243 | hasModuleSyntax === true;
|
|---|
| 244 | ```
|
|---|
| 245 |
|
|---|
| 246 | Dynamic imports are ignored since they can be used in Non-ESM files.
|
|---|
| 247 |
|
|---|
| 248 | ```js
|
|---|
| 249 | const [,,, hasModuleSyntax] = parse(`
|
|---|
| 250 | import('./foo.js')
|
|---|
| 251 | `);
|
|---|
| 252 | hasModuleSyntax === false;
|
|---|
| 253 | ```
|
|---|
| 254 |
|
|---|
| 255 | ### Environment Support
|
|---|
| 256 |
|
|---|
| 257 | Node.js 10+, and [all browsers with Web Assembly support](https://caniuse.com/#feat=wasm).
|
|---|
| 258 |
|
|---|
| 259 | ### Grammar Support
|
|---|
| 260 |
|
|---|
| 261 | * Token state parses all line comments, block comments, strings, template strings, blocks, parens and punctuators.
|
|---|
| 262 | * Division operator / regex token ambiguity is handled via backtracking checks against punctuator prefixes, including closing brace or paren backtracking.
|
|---|
| 263 | * Always correctly parses valid JS source, but may parse invalid JS source without errors.
|
|---|
| 264 |
|
|---|
| 265 | ### Limitations
|
|---|
| 266 |
|
|---|
| 267 | The lexing approach is designed to deal with the full language grammar including RegEx / division operator ambiguity through backtracking and paren / brace tracking.
|
|---|
| 268 |
|
|---|
| 269 | The only limitation to the reduced parser is that the "exports" list may not correctly gather all export identifiers in the following edge cases:
|
|---|
| 270 |
|
|---|
| 271 | ```js
|
|---|
| 272 | // Only "a" is detected as an export, "q" isn't
|
|---|
| 273 | export var a = 'asdf', q = z;
|
|---|
| 274 |
|
|---|
| 275 | // "b" is not detected as an export
|
|---|
| 276 | export var { a: b } = asdf;
|
|---|
| 277 | ```
|
|---|
| 278 |
|
|---|
| 279 | The above cases are handled gracefully in that the lexer will keep going fine, it will just not properly detect the export names above.
|
|---|
| 280 |
|
|---|
| 281 | ### Benchmarks
|
|---|
| 282 |
|
|---|
| 283 | Benchmarks can be run with `npm run bench`.
|
|---|
| 284 |
|
|---|
| 285 | Current results for a high spec machine:
|
|---|
| 286 |
|
|---|
| 287 | #### Wasm Build
|
|---|
| 288 |
|
|---|
| 289 | ```
|
|---|
| 290 | Module load time
|
|---|
| 291 | > 5ms
|
|---|
| 292 | Cold Run, All Samples
|
|---|
| 293 | test/samples/*.js (3123 KiB)
|
|---|
| 294 | > 18ms
|
|---|
| 295 |
|
|---|
| 296 | Warm Runs (average of 25 runs)
|
|---|
| 297 | test/samples/angular.js (739 KiB)
|
|---|
| 298 | > 3ms
|
|---|
| 299 | test/samples/angular.min.js (188 KiB)
|
|---|
| 300 | > 1ms
|
|---|
| 301 | test/samples/d3.js (508 KiB)
|
|---|
| 302 | > 3ms
|
|---|
| 303 | test/samples/d3.min.js (274 KiB)
|
|---|
| 304 | > 2ms
|
|---|
| 305 | test/samples/magic-string.js (35 KiB)
|
|---|
| 306 | > 0ms
|
|---|
| 307 | test/samples/magic-string.min.js (20 KiB)
|
|---|
| 308 | > 0ms
|
|---|
| 309 | test/samples/rollup.js (929 KiB)
|
|---|
| 310 | > 4.32ms
|
|---|
| 311 | test/samples/rollup.min.js (429 KiB)
|
|---|
| 312 | > 2.16ms
|
|---|
| 313 |
|
|---|
| 314 | Warm Runs, All Samples (average of 25 runs)
|
|---|
| 315 | test/samples/*.js (3123 KiB)
|
|---|
| 316 | > 14.16ms
|
|---|
| 317 | ```
|
|---|
| 318 |
|
|---|
| 319 | #### JS Build (asm.js)
|
|---|
| 320 |
|
|---|
| 321 | ```
|
|---|
| 322 | Module load time
|
|---|
| 323 | > 2ms
|
|---|
| 324 | Cold Run, All Samples
|
|---|
| 325 | test/samples/*.js (3123 KiB)
|
|---|
| 326 | > 34ms
|
|---|
| 327 |
|
|---|
| 328 | Warm Runs (average of 25 runs)
|
|---|
| 329 | test/samples/angular.js (739 KiB)
|
|---|
| 330 | > 3ms
|
|---|
| 331 | test/samples/angular.min.js (188 KiB)
|
|---|
| 332 | > 1ms
|
|---|
| 333 | test/samples/d3.js (508 KiB)
|
|---|
| 334 | > 3ms
|
|---|
| 335 | test/samples/d3.min.js (274 KiB)
|
|---|
| 336 | > 2ms
|
|---|
| 337 | test/samples/magic-string.js (35 KiB)
|
|---|
| 338 | > 0ms
|
|---|
| 339 | test/samples/magic-string.min.js (20 KiB)
|
|---|
| 340 | > 0ms
|
|---|
| 341 | test/samples/rollup.js (929 KiB)
|
|---|
| 342 | > 5ms
|
|---|
| 343 | test/samples/rollup.min.js (429 KiB)
|
|---|
| 344 | > 3.04ms
|
|---|
| 345 |
|
|---|
| 346 | Warm Runs, All Samples (average of 25 runs)
|
|---|
| 347 | test/samples/*.js (3123 KiB)
|
|---|
| 348 | > 17.12ms
|
|---|
| 349 | ```
|
|---|
| 350 |
|
|---|
| 351 | ### Building
|
|---|
| 352 |
|
|---|
| 353 | This project uses [Chomp](https://chompbuild.com) for building.
|
|---|
| 354 |
|
|---|
| 355 | With Chomp installed, download the WASI SDK 12.0 from https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-12.
|
|---|
| 356 |
|
|---|
| 357 | - [Linux](https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-linux.tar.gz)
|
|---|
| 358 | - [Windows (MinGW)](https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-mingw.tar.gz)
|
|---|
| 359 | - [macOS](https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-macos.tar.gz)
|
|---|
| 360 |
|
|---|
| 361 | Locate the WASI-SDK as a sibling folder, or customize the path via the `WASI_PATH` environment variable.
|
|---|
| 362 |
|
|---|
| 363 | Emscripten emsdk is also assumed to be a sibling folder or via the `EMSDK_PATH` environment variable.
|
|---|
| 364 |
|
|---|
| 365 | Example setup:
|
|---|
| 366 |
|
|---|
| 367 | ```
|
|---|
| 368 | git clone https://github.com:guybedford/es-module-lexer
|
|---|
| 369 | git clone https://github.com/emscripten-core/emsdk
|
|---|
| 370 | cd emsdk
|
|---|
| 371 | git checkout 1.40.1-fastcomp
|
|---|
| 372 | ./emsdk install 1.40.1-fastcomp
|
|---|
| 373 | cd ..
|
|---|
| 374 | wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-12/wasi-sdk-12.0-linux.tar.gz
|
|---|
| 375 | gunzip wasi-sdk-12.0-linux.tar.gz
|
|---|
| 376 | tar -xf wasi-sdk-12.0-linux.tar
|
|---|
| 377 | mv wasi-sdk-12.0-linux.tar wasi-sdk-12.0
|
|---|
| 378 | cargo install chompbuild
|
|---|
| 379 | cd es-module-lexer
|
|---|
| 380 | chomp test
|
|---|
| 381 | ```
|
|---|
| 382 |
|
|---|
| 383 | For the `asm.js` build, git clone `emsdk` from is assumed to be a sibling folder as well.
|
|---|
| 384 |
|
|---|
| 385 | ### License
|
|---|
| 386 |
|
|---|
| 387 | MIT
|
|---|
| 388 |
|
|---|
| 389 | [actions-image]: https://github.com/guybedford/es-module-lexer/actions/workflows/build.yml/badge.svg
|
|---|
| 390 | [actions-url]: https://github.com/guybedford/es-module-lexer/actions/workflows/build.yml
|
|---|