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

Support reexports via require spreads #18

Merged
merged 1 commit into from
Oct 21, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,13 @@ EXPORTS_LITERAL_COMPUTED_ASSIGN: EXPORTS_IDENTIFIER COMMENT_SPACE `[` COMMENT_SP

EXPORTS_LITERAL_PROP: (IDENTIFIER (COMMENT_SPACE `:` COMMENT_SPACE IDENTIFIER)?) | (IDENTIFIER_STRING COMMENT_SPACE `:` COMMENT_SPACE IDENTIFIER)

EXPORTS_SPREAD: `...` COMMENT_SPACE (IDENTIFIER | REQUIRE)

EXPORTS_MEMBER: EXPORTS_DOT_ASSIGN | EXPORTS_LITERAL_COMPUTED_ASSIGN

EXPORTS_DEFINE: `Object` COMMENT_SPACE `.` COMMENT_SPACE `defineProperty COMMENT_SPACE `(` EXPORTS_IDENTIFIER COMMENT_SPACE `,` COMMENT_SPACE IDENTIFIER_STRING

EXPORTS_LITERAL: MODULE_EXPORTS COMMENT_SPACE `=` COMMENT_SPACE `{` COMMENT_SPACE (EXPORTS_LITERAL_PROP COMMENT_SPACE `,` COMMENT_SPACE)+ `}`
EXPORTS_LITERAL: MODULE_EXPORTS COMMENT_SPACE `=` COMMENT_SPACE `{` COMMENT_SPACE (EXPORTS_LITERAL_PROP | EXPORTS_SPREAD) COMMENT_SPACE `,` COMMENT_SPACE)+ `}`

REQUIRE: `require` COMMENT_SPACE `(` COMMENT_SPACE STRING_LITERAL COMMENT_SPACE `)`

Expand All @@ -112,7 +114,9 @@ EXPORT_STAR_LIB: `Object.keys(` IDENTIFIER$1 `).forEach(function (` IDENTIFIER$2
```

* The returned export names are the matched `IDENTIFIER` and `IDENTIFIER_STRING` slots for all `EXPORTS_MEMBER`, `EXPORTS_DEFINE` and `EXPORTS_LITERAL` matches.
* The reexport specifiers are taken to be the `STRING_LITERAL` slot of the last `MODULE_EXPORTS_ASSIGN` as well as all _top-level_ `EXPORT_STAR` `REQUIRE` matches and `EXPORTS_ASSIGN` matches whose `IDENTIFIER` also matches the first `IDENTIFIER` in `EXPORT_STAR_LIB`.
* The reexport specifiers are taken to be the the combination of:
1. The `REQUIRE` matches of the last matched of either `MODULE_EXPORTS_ASSIGN` or `EXPORTS_LITERAL`.
2. All _top-level_ `EXPORT_STAR` `REQUIRE` matches and `EXPORTS_ASSIGN` matches whose `IDENTIFIER` also matches the first `IDENTIFIER` in `EXPORT_STAR_LIB`.

### Parsing Examples

Expand Down Expand Up @@ -164,16 +168,18 @@ Simple object definitions are supported:
module.exports = {
a,
'b': b,
c: c
c: c,
...d
};
```

Object properties that are not identifiers or string expressions will bail out of the object detection:
Object properties that are not identifiers or string expressions will bail out of the object detection, while spreads are ignored:

```js
// DETECTS EXPORTS: a, b
module.exports = {
a,
...d,
b: require('c'),
c: "not detected since require('c') above bails the object detection"
}
Expand All @@ -192,7 +198,18 @@ module.exports = require('a');
if (false) module.exports = require('c');
```

This is to avoid overclassification in Webpack bundles with externals which include `module.exports = require('external')` in their source for every external dependency.
This is to avoid over-classification in Webpack bundles with externals which include `module.exports = require('external')` in their source for every external dependency.

In exports object assignment, any spread of `require()` are detected as multiple separate reexports:

```js
// DETECTS REEXPORTS: a, b
module.exports = require('ignored');
module.exports = {
...require('a'),
...require('b')
};
```

#### Transpiler Re-exports

Expand Down
7 changes: 6 additions & 1 deletion include-wasm/cjs-module-lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,14 @@ void _addReexport (const uint16_t* start, const uint16_t* end) {
reexport->end = end;
reexport->next = NULL;
}
void _clearReexports () {
reexport_write_head = NULL;
first_reexport = NULL;
}
void (*addExport)(const uint16_t*, const uint16_t*) = &_addExport;
void (*addReexport)(const uint16_t*, const uint16_t*) = &_addReexport;
bool parseCJS (uint16_t* source, uint32_t sourceLen, void (*addExport)(const uint16_t* start, const uint16_t* end), void (*addReexport)(const uint16_t* start, const uint16_t* end));
void (*clearReexports)() = &_clearReexports;
bool parseCJS (uint16_t* source, uint32_t sourceLen, void (*addExport)(const uint16_t* start, const uint16_t* end), void (*addReexport)(const uint16_t* start, const uint16_t* end), void (*clearReexports)());

enum RequireType {
Import,
Expand Down
2 changes: 1 addition & 1 deletion include/cjs-module-lexer.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ typedef struct StarExportBinding StarExportBinding;

void bail (uint32_t err);

bool parseCJS (uint16_t* source, uint32_t sourceLen, void (*addExport)(const uint16_t*, const uint16_t*), void (*addReexport)(const uint16_t*, const uint16_t*));
bool parseCJS (uint16_t* source, uint32_t sourceLen, void (*addExport)(const uint16_t*, const uint16_t*), void (*addReexport)(const uint16_t*, const uint16_t*), void (*clearReexports)());

enum RequireType {
Import,
Expand Down
23 changes: 16 additions & 7 deletions lexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ let openTokenDepth,
nextBraceIsClass,
starExportMap,
lastStarExportSpecifier,
lastExportsAssignSpecifier,
_exports,
reexports;

Expand All @@ -26,7 +25,6 @@ function resetState () {
nextBraceIsClass = false;
starExportMap = Object.create(null);
lastStarExportSpecifier = null;
lastExportsAssignSpecifier = null;

_exports = new Set();
reexports = new Set();
Expand All @@ -49,8 +47,6 @@ module.exports = function parseCJS (source, name = '@') {
e.loc = pos;
throw e;
}
if (lastExportsAssignSpecifier)
reexports.add(lastExportsAssignSpecifier);
const result = { exports: [..._exports], reexports: [...reexports] };
resetState();
return result;
Expand Down Expand Up @@ -677,6 +673,8 @@ function tryParseExportsDotAssign (assign) {
// module.exports =
case 61/*=*/: {
if (assign) {
if (reexports.size)
reexports = new Set();
pos++;
ch = commentWhitespace();
// { ... }
Expand All @@ -696,9 +694,9 @@ function tryParseExportsDotAssign (assign) {

function tryParseRequire (requireType) {
// require('...')
const revertPos = pos;
if (source.startsWith('equire', pos + 1)) {
pos += 7;
const revertPos = pos - 1;
let ch = commentWhitespace();
if (ch === 40/*(*/) {
pos++;
Expand All @@ -711,7 +709,7 @@ function tryParseRequire (requireType) {
if (ch === 41/*)*/) {
switch (requireType) {
case ExportAssign:
lastExportsAssignSpecifier = source.slice(reexportStart, reexportEnd);
reexports.add(source.slice(reexportStart, reexportEnd));
return true;
case ExportStar:
reexports.add(source.slice(reexportStart, reexportEnd));
Expand All @@ -729,7 +727,7 @@ function tryParseRequire (requireType) {
if (ch === 41/*)*/) {
switch (requireType) {
case ExportAssign:
lastExportsAssignSpecifier = source.slice(reexportStart, reexportEnd);
reexports.add(source.slice(reexportStart, reexportEnd));
return true;
case ExportStar:
reexports.add(source.slice(reexportStart, reexportEnd));
Expand Down Expand Up @@ -766,6 +764,17 @@ function tryParseLiteralExports () {
}
addExport(source.slice(startPos, endPos));
}
else if (ch === 46/*.*/ && source.startsWith('..', pos + 1)) {
pos += 3;
if (source.charCodeAt(pos) === 114/*r*/ && tryParseRequire(ExportAssign)) {
pos++;
}
else if (!identifier()) {
pos = revertPos;
return;
}
ch = commentWhitespace();
}
else if (ch === 39/*'*/ || ch === 34/*"*/) {
const startPos = ++pos;
if (identifier() && source.charCodeAt(pos) === ch) {
Expand Down
30 changes: 17 additions & 13 deletions src/lexer.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ uint16_t* end;
uint16_t* templateStack;
uint16_t** openTokenPosStack;
StarExportBinding* starExportStack;
uint16_t* reexportAssignStart;
uint16_t* reexportAssignEnd;
bool nextBraceIsClass;

uint16_t* lastReexportStart;
Expand All @@ -43,9 +41,10 @@ const StarExportBinding* STAR_EXPORT_STACK_END = &starExportStack_[MAX_STAR_EXPO

void (*addExport)(const uint16_t*, const uint16_t*);
void (*addReexport)(const uint16_t*, const uint16_t*);
void (*clearReexports)();

// Note: parsing is based on the _assumption_ that the source is already valid
bool parseCJS (uint16_t* _source, uint32_t _sourceLen, void (*_addExport)(const uint16_t*, const uint16_t*), void (*_addReexport)(const uint16_t*, const uint16_t*)) {
bool parseCJS (uint16_t* _source, uint32_t _sourceLen, void (*_addExport)(const uint16_t*, const uint16_t*), void (*_addReexport)(const uint16_t*, const uint16_t*), void (*_clearReexports)()) {
source = _source;
sourceLen = _sourceLen;
if (_addExport)
Expand All @@ -64,8 +63,6 @@ bool parseCJS (uint16_t* _source, uint32_t _sourceLen, void (*_addExport)(const
openTokenPosStack = &openTokenPosStack_[0];
starExportStack = &starExportStack_[0];
nextBraceIsClass = false;
reexportAssignStart = NULL;
reexportAssignEnd = NULL;

pos = (uint16_t*)(source - 1);
uint16_t ch = '\0';
Expand Down Expand Up @@ -221,9 +218,6 @@ bool parseCJS (uint16_t* _source, uint32_t _sourceLen, void (*_addExport)(const
if (templateDepth != UINT16_MAX || openTokenDepth || has_error)
return false;

if (reexportAssignStart)
addReexport(reexportAssignStart, reexportAssignEnd);

// success
return true;
}
Expand Down Expand Up @@ -700,6 +694,7 @@ void tryParseExportsDotAssign (bool assign) {
// module.exports =
case '=': {
if (assign) {
clearReexports();
pos++;
ch = commentWhitespace();
// { ... }
Expand All @@ -718,10 +713,10 @@ void tryParseExportsDotAssign (bool assign) {
}

bool tryParseRequire (enum RequireType requireType) {
uint16_t* revertPos = pos;
// require('...')
if (str_eq6(pos + 1, 'e', 'q', 'u', 'i', 'r', 'e')) {
pos += 7;
uint16_t* revertPos = pos - 1;
uint16_t ch = commentWhitespace();
if (ch == '(') {
pos++;
Expand All @@ -737,8 +732,7 @@ bool tryParseRequire (enum RequireType requireType) {
addReexport(reexportStart, reexportEnd);
return true;
case ExportAssign:
reexportAssignStart = reexportStart;
reexportAssignEnd = reexportEnd;
addReexport(reexportStart, reexportEnd);
return true;
default:
starExportStack->specifier_start = reexportStart;
Expand All @@ -757,8 +751,7 @@ bool tryParseRequire (enum RequireType requireType) {
addReexport(reexportStart, reexportEnd);
return true;
case ExportAssign:
reexportAssignStart = reexportStart;
reexportAssignEnd = reexportEnd;
addReexport(reexportStart, reexportEnd);
return true;
default:
starExportStack->specifier_start = reexportStart;
Expand Down Expand Up @@ -812,6 +805,17 @@ void tryParseLiteralExports () {
}
}
}
else if (ch == '.' && str_eq2(pos + 1, '.', '.')) {
pos += 3;
if (*pos == 'r' && tryParseRequire(ExportAssign)) {
pos++;
}
else if (!identifier(*pos)) {
pos = revertPos;
return;
}
ch = commentWhitespace();
}
else {
pos = revertPos;
return;
Expand Down
19 changes: 19 additions & 0 deletions test/_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,25 @@ suite('Lexer', () => {
assert.equal(exports.length, 0);
});

test('module exports reexport spread', () => {
const { exports, reexports } = parse(`
module.exports = {
...a,
...b,
...require('dep1'),
c: d,
...require('dep2'),
name
};
`);
assert.equal(exports.length, 2);
assert.equal(exports[0], 'c');
assert.equal(exports[1], 'name');
assert.equal(reexports.length, 2);
assert.equal(reexports[0], 'dep1');
assert.equal(reexports[1], 'dep2');
});

test('Regexp case', () => {
parse(`
class Number {
Expand Down