Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ESM detection support #160

Merged
merged 1 commit into from Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -152,7 +152,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 @@ -198,7 +199,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 @@ -235,6 +236,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