Skip to content

Commit

Permalink
feat: ESM detection support (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red committed Nov 9, 2023
1 parent 39d8ae3 commit d41c1c8
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 25 deletions.
12 changes: 11 additions & 1 deletion README.md
Expand Up @@ -165,6 +165,17 @@ const [,, facade] = parse(`
facade === true;
```

### ESM Detection

Modules that uses ESM syntaxes can be detected via the fourth return value:

```js
const [,,, hasModuleSyntax] = parse(`
export {}
`);
hasModuleSyntax === true;
```

### Environment Support

Node.js 10+, and [all browsers with Web Assembly support](https://caniuse.com/#feat=wasm).
Expand Down Expand Up @@ -301,4 +312,3 @@ MIT

[actions-image]: https://github.com/guybedford/es-module-lexer/actions/workflows/build.yml/badge.svg
[actions-url]: https://github.com/guybedford/es-module-lexer/actions/workflows/build.yml

4 changes: 2 additions & 2 deletions chompfile.toml
Expand Up @@ -96,7 +96,7 @@ deps = ['src/lexer.h', 'src/lexer.c']
run = """
${{ WASI_PATH }}/bin/clang src/lexer.c --sysroot=${{ WASI_PATH }}/share/wasi-sysroot -o lib/lexer.wasm -nostartfiles \
"-Wl,-z,stack-size=13312,--no-entry,--compress-relocations,--strip-all,\
--export=parse,--export=sa,--export=e,--export=ri,--export=re,--export=is,--export=ie,--export=ss,--export=ip,--export=se,--export=ai,--export=id,--export=es,--export=ee,--export=els,--export=ele,--export=f,--export=__heap_base" \
--export=parse,--export=sa,--export=e,--export=ri,--export=re,--export=is,--export=ie,--export=ss,--export=ip,--export=se,--export=ai,--export=id,--export=es,--export=ee,--export=els,--export=ele,--export=f,--export=ms,--export=__heap_base" \
-Wno-logical-op-parentheses -Wno-parentheses \
-Oz
"""
Expand All @@ -110,7 +110,7 @@ run = """
${{ EMSDK_PATH }}/emsdk activate 1.40.1-fastcomp
${{ EMSDK_PATH }}/fastcomp/emscripten/emcc ./src/lexer.c -o lib/lexer.emcc.js -s WASM=0 -Oz --closure 1 \
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_f','_setSource']" \
-s EXPORTED_FUNCTIONS="['_parse','_sa','_e','_ri','_re','_is','_ie','_ss','_ip','_se','_ai','_id','_es','_ee','_els','_ele','_f','_ms','_setSource']" \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 -s SINGLE_FILE=1 -s TOTAL_STACK=4997968 -s --separate-asm -Wno-logical-op-parentheses -Wno-parentheses
# rm lib/lexer.emcc.js
Expand Down
8 changes: 4 additions & 4 deletions lib/lexer.asm.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lib/lexer.emcc.asm.js

Large diffs are not rendered by default.

Binary file modified lib/lexer.wasm
Binary file not shown.
4 changes: 2 additions & 2 deletions src/lexer.asm.js
Expand Up @@ -61,12 +61,12 @@ export function parse (_source, _name = '@') {
});
}

return [imports, exports, !!asm.f()];
return [imports, exports, !!asm.f(), !!asm.ms()];
}

/*
* Ported from Acorn
*
*
* MIT License
* Copyright (C) 2012-2020 by various contributors (see AUTHORS)
Expand Down
2 changes: 2 additions & 0 deletions src/lexer.c
Expand Up @@ -38,6 +38,7 @@ bool parse () {
Import* dynamicImportStack_[512];

facade = true;
hasModuleSyntax = false;
dynamicImportStackDepth = 0;
openTokenDepth = 0;
lastTokenPos = (char16_t*)EMPTY_CHAR;
Expand Down Expand Up @@ -405,6 +406,7 @@ void tryParseExportStatement () {
if (pos > end)
return syntaxError();
}
hasModuleSyntax = true; // to handle "export {}"
pos++;
ch = commentWhitespace(true);
}
Expand Down
8 changes: 7 additions & 1 deletion src/lexer.h
Expand Up @@ -64,6 +64,7 @@ void* analysis_base;
void* analysis_head;

bool facade;
bool hasModuleSyntax;
bool lastSlashWasDivision;
uint16_t openTokenDepth;
char16_t* lastTokenPos;
Expand Down Expand Up @@ -114,14 +115,15 @@ void addImport (const char16_t* statement_start, const char16_t* start, const ch
import->statement_end = end;
else if (dynamic == STANDARD_IMPORT)
import->statement_end = end + 1;
else
else
import->statement_end = 0;
import->start = start;
import->end = end;
import->assert_index = 0;
import->dynamic = dynamic;
import->safe = dynamic == STANDARD_IMPORT;
import->next = NULL;
hasModuleSyntax = true;
}

void addExport (const char16_t* start, const char16_t* end, const char16_t* local_start, const char16_t* local_end) {
Expand All @@ -137,6 +139,7 @@ void addExport (const char16_t* start, const char16_t* end, const char16_t* loca
export->local_start = local_start;
export->local_end = local_end;
export->next = NULL;
hasModuleSyntax = true;
}

// getErr
Expand Down Expand Up @@ -216,6 +219,9 @@ bool re () {
bool f () {
return facade;
}
bool ms () {
return hasModuleSyntax;
}

bool parse ();

Expand Down
7 changes: 5 additions & 2 deletions src/lexer.ts
Expand Up @@ -156,7 +156,8 @@ const isLE = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
export function parse (source: string, name = '@'): readonly [
imports: ReadonlyArray<ImportSpecifier>,
exports: ReadonlyArray<ExportSpecifier>,
facade: boolean
facade: boolean,
hasModuleSyntax: boolean
] {
if (!wasm)
// actually returns a promise if init hasn't resolved (not type safe).
Expand Down Expand Up @@ -202,7 +203,7 @@ export function parse (source: string, name = '@'): readonly [
catch (e) {}
}

return [imports, exports, !!wasm.f()];
return [imports, exports, !!wasm.f(), !!wasm.ms()];
}

function copyBE (src: string, outBuf16: Uint16Array) {
Expand Down Expand Up @@ -239,6 +240,8 @@ let wasm: {
es(): number;
/** facade */
f(): boolean;
/** hasModuleSyntax */
ms(): boolean;
/** getImportDynamic */
id(): number;
/** getImportEnd */
Expand Down
51 changes: 40 additions & 11 deletions test/_unit.cjs
Expand Up @@ -689,7 +689,7 @@ function x() {
const [imports, exports] = parse(source);
assert.strictEqual(exports.length, 0);
assert.strictEqual(imports.length, 10);

assert.strictEqual(imports[0].n, './mod0.js');
assert.strictEqual(imports[1].n, './mod1.js');
assert.strictEqual(imports[2].n, './mod2.js');
Expand All @@ -700,7 +700,7 @@ function x() {
assert.strictEqual(imports[7].n, './mod7.js');
assert.strictEqual(imports[8].n, './mod8.js');
});

if (!js)
test('non-identifier-string as (singleQuote)', () => {
const source = `
Expand All @@ -716,7 +716,7 @@ function x() {
const [imports, exports] = parse(source);
assert.strictEqual(exports.length, 0);
assert.strictEqual(imports.length, 9);

assert.strictEqual(imports[0].n, './mod0.js');
assert.strictEqual(imports[1].n, './mod1.js');
assert.strictEqual(imports[2].n, './mod2.js');
Expand All @@ -727,7 +727,7 @@ function x() {
assert.strictEqual(imports[7].n, './mod7.js');
assert.strictEqual(imports[8].n, './mod8.js');
});

if (!js)
test('with-backslash-keywords as (doubleQuote)', () => {
const source = String.raw`
Expand All @@ -738,13 +738,13 @@ function x() {
const [imports, exports] = parse(source);
assert.strictEqual(exports.length, 0);
assert.strictEqual(imports.length, 4);

assert.strictEqual(imports[0].n, './mod0.js');
assert.strictEqual(imports[1].n, './mod1.js');
assert.strictEqual(imports[2].n, './mod2.js');
assert.strictEqual(imports[3].n, './mod3.js');
});

if (!js)
test('with-backslash-keywords as (singleQuote)', () => {
const source = String.raw`
Expand All @@ -755,13 +755,13 @@ function x() {
const [imports, exports] = parse(source);
assert.strictEqual(exports.length, 0);
assert.strictEqual(imports.length, 4);

assert.strictEqual(imports[0].n, './mod0.js');
assert.strictEqual(imports[1].n, './mod1.js');
assert.strictEqual(imports[2].n, './mod2.js');
assert.strictEqual(imports[3].n, './mod3.js');
});

if (!js)
test('with-emoji as', () => {
const source = `
Expand All @@ -770,7 +770,7 @@ function x() {
const [imports, exports] = parse(source);
assert.strictEqual(exports.length, 0);
assert.strictEqual(imports.length, 2);

assert.strictEqual(imports[0].n, './mod0.js');
assert.strictEqual(imports[1].n, './mod1.js');
});
Expand All @@ -782,7 +782,7 @@ function x() {
const [imports, exports] = parse(source);
assert.strictEqual(exports.length, 0);
assert.strictEqual(imports.length, 1);

assert.strictEqual(imports[0].n, 'mod0');
});

Expand Down Expand Up @@ -832,7 +832,7 @@ function x() {
export { " notidentifier " as foo8 } from './mod8.js';`;
const [imports, exports] = parse(source);
assert.strictEqual(imports.length, 9);

assert.strictEqual(exports.length, 9);
assertExportIs(source, exports[0], { n: 'foo0', ln: undefined });
assertExportIs(source, exports[1], { n: 'foo1', ln: undefined });
Expand Down Expand Up @@ -1353,6 +1353,35 @@ function x() {
assertExportIs(source, exports[10], { n: 'default', ln: undefined });
assertExportIs(source, exports[11], { n: 'default', ln: undefined });
});

test('hasModuleSyntax import1', () => {
const [,,, hasModuleSyntax] = parse('import foo from "./foo"')
assert.strictEqual(hasModuleSyntax, true)
})
test('hasModuleSyntax import2', () => {
const [,,, hasModuleSyntax] = parse('const foo = "import"')
assert.strictEqual(hasModuleSyntax, false)
})
test('hasModuleSyntax import3', () => {
const [,,, hasModuleSyntax] = parse('import("./foo")')
assert.strictEqual(hasModuleSyntax, true)
})
test('hasModuleSyntax import4', () => {
const [,,, hasModuleSyntax] = parse('import.meta.url')
assert.strictEqual(hasModuleSyntax, true)
})
test('hasModuleSyntax export1', () => {
const [,,, hasModuleSyntax] = parse('export const foo = "foo"')
assert.strictEqual(hasModuleSyntax, true)
})
test('hasModuleSyntax export2', () => {
const [,,, hasModuleSyntax] = parse('export {}')
assert.strictEqual(hasModuleSyntax, true)
})
test('hasModuleSyntax export3', () => {
const [,,, hasModuleSyntax] = parse('export * from "./foo"')
assert.strictEqual(hasModuleSyntax, true)
})
});

suite('Invalid syntax', () => {
Expand Down

0 comments on commit d41c1c8

Please sign in to comment.