Skip to content

Commit cbc341d

Browse files
feat(commonjs): support dynamic require (#206)
* Implemented support for dynamic requires (transferred PR) Moved from rollup/rollup-plugin-commonjs#331 * Only add dynamic loader code when dynamic feature is enabled * test(commonjs): update snapshots for easier diffing * Automatically remove user paths * test(commonjs): Prepare tests to support code-splitting * test(commonjs): Try to add a code-splitting test * Fixed code-splitting support * Cleanup: avoid importing commonjs-proxy when we only need to register * Fixed test * Updated pnpm-lock * Updated snapshots * Satisfy linter Co-authored-by: Lukas Taegert-Atkinson <[email protected]>
1 parent 1bee4e9 commit cbc341d

File tree

83 files changed

+3900
-748
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+3900
-748
lines changed

packages/commonjs/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#comma
4444

4545
## Options
4646

47+
### `dynamicRequireTargets`
48+
49+
Type: `String|Array[String]`<br>
50+
Default: `[]`
51+
52+
Some modules contain dynamic `require` calls, or require modules that contain circular dependencies, which are not handled well by static imports.
53+
Including those modules as `dynamicRequireTargets` will simulate a CommonJS (NodeJS-like) environment for them with support for dynamic and circular dependencies.
54+
55+
_Note: In extreme cases, this feature may result in some paths being rendered as absolute in the final bundle. The plugin tries to avoid exposing paths from the local machine, but if you are `dynamicRequirePaths` with paths that are far away from your project's folder, that may require replacing strings like `"/Users/John/Desktop/foo-project/"` -> `"/"`._
56+
57+
Example:
58+
59+
```js
60+
commonjs({
61+
dynamicRequireTargets: [
62+
// include using a glob pattern (either a string or an array of strings)
63+
'node_modules/logform/*.js',
64+
65+
// exclude files that are known to not be required dynamically, this allows for better optimizations
66+
'!node_modules/logform/index.js',
67+
'!node_modules/logform/format.js',
68+
'!node_modules/logform/levels.js',
69+
'!node_modules/logform/browser.js'
70+
]
71+
});
72+
```
73+
4774
### `exclude`
4875

4976
Type: `String` | `Array[...String]`<br>

packages/commonjs/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050
},
5151
"dependencies": {
5252
"@rollup/pluginutils": "^3.0.8",
53+
"commondir": "^1.0.1",
5354
"estree-walker": "^1.0.1",
55+
"glob": "^7.1.2",
5456
"is-reference": "^1.1.2",
5557
"magic-string": "^0.25.2",
5658
"resolve": "^1.11.0"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { statSync } from 'fs';
2+
import glob from 'glob';
3+
import { resolve } from 'path';
4+
import { normalizePathSlashes } from './transform';
5+
6+
export default function getDynamicRequirePaths(patterns) {
7+
const dynamicRequireModuleSet = new Set();
8+
for (const pattern of (!patterns || Array.isArray(patterns)) ? patterns || [] : [patterns]) {
9+
const isNegated = pattern.startsWith('!');
10+
const modifySet = Set.prototype[isNegated ? 'delete' : 'add'].bind(
11+
dynamicRequireModuleSet
12+
);
13+
for (const path of glob.sync(isNegated ? pattern.substr(1) : pattern)) {
14+
modifySet(normalizePathSlashes(resolve(path)));
15+
}
16+
}
17+
const dynamicRequireModuleDirPaths = Array.from(dynamicRequireModuleSet.values()).filter(path => {
18+
try {
19+
if (statSync(path).isDirectory())
20+
return true;
21+
} catch (ignored) {
22+
// Nothing to do here
23+
}
24+
return false;
25+
});
26+
return { dynamicRequireModuleSet, dynamicRequireModuleDirPaths };
27+
}

packages/commonjs/src/helpers.js

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@ export const EXTERNAL_SUFFIX = '?commonjs-external';
66
export const getExternalProxyId = (id) => `\0${id}${EXTERNAL_SUFFIX}`;
77
export const getIdFromExternalProxyId = (proxyId) => proxyId.slice(1, -EXTERNAL_SUFFIX.length);
88

9+
export const VIRTUAL_PATH_BASE = '/$$rollup_base$$';
10+
export const getVirtualPathForDynamicRequirePath = (path, commonDir) => {
11+
if (path.startsWith(commonDir))
12+
return VIRTUAL_PATH_BASE + path.slice(commonDir.length);
13+
return path;
14+
};
15+
16+
export const DYNAMIC_REGISTER_PREFIX = '\0commonjs-dynamic-register:';
17+
export const DYNAMIC_JSON_PREFIX = '\0commonjs-dynamic-json:';
18+
export const DYNAMIC_PACKAGES_ID = '\0commonjs-dynamic-packages';
19+
920
export const HELPERS_ID = '\0commonjsHelpers.js';
1021

1122
// `x['default']` is used instead of `x.default` for backward compatibility with ES3 browsers.
1223
// Minifiers like uglify will usually transpile it back if compatibility with ES3 is not enabled.
1324
export const HELPERS = `
1425
export var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
1526
16-
export function commonjsRequire () {
17-
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
18-
}
19-
2027
export function unwrapExports (x) {
2128
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
2229
}
@@ -27,4 +34,125 @@ export function createCommonjsModule(fn, module) {
2734
2835
export function getCjsExportFromNamespace (n) {
2936
return n && n['default'] || n;
30-
}`;
37+
}
38+
`;
39+
40+
export const HELPER_NON_DYNAMIC = `
41+
export function commonjsRequire () {
42+
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
43+
}
44+
`;
45+
46+
export const HELPERS_DYNAMIC = `
47+
export function commonjsRegister (path, loader) {
48+
DYNAMIC_REQUIRE_LOADERS[path] = loader;
49+
}
50+
51+
const DYNAMIC_REQUIRE_LOADERS = Object.create(null);
52+
const DYNAMIC_REQUIRE_CACHE = Object.create(null);
53+
const DEFAULT_PARENT_MODULE = {
54+
id: '<' + 'rollup>', exports: {}, parent: undefined, filename: null, loaded: false, children: [], paths: []
55+
};
56+
const CHECKED_EXTENSIONS = ['', '.js', '.json'];
57+
58+
function normalize (path) {
59+
path = path.replace(/\\\\/g, '/');
60+
const parts = path.split('/');
61+
const slashed = parts[0] === '';
62+
for (let i = 1; i < parts.length; i++) {
63+
if (parts[i] === '.' || parts[i] === '') {
64+
parts.splice(i--, 1);
65+
}
66+
}
67+
for (let i = 1; i < parts.length; i++) {
68+
if (parts[i] !== '..') continue;
69+
if (i > 0 && parts[i - 1] !== '..' && parts[i - 1] !== '.') {
70+
parts.splice(--i, 2);
71+
i--;
72+
}
73+
}
74+
path = parts.join('/');
75+
if (slashed && path[0] !== '/')
76+
path = '/' + path;
77+
else if (path.length === 0)
78+
path = '.';
79+
return path;
80+
}
81+
82+
function join () {
83+
if (arguments.length === 0)
84+
return '.';
85+
let joined;
86+
for (let i = 0; i < arguments.length; ++i) {
87+
let arg = arguments[i];
88+
if (arg.length > 0) {
89+
if (joined === undefined)
90+
joined = arg;
91+
else
92+
joined += '/' + arg;
93+
}
94+
}
95+
if (joined === undefined)
96+
return '.';
97+
98+
return joined;
99+
}
100+
101+
function isPossibleNodeModulesPath (modulePath) {
102+
let c0 = modulePath[0];
103+
if (c0 === '/' || c0 === '\\\\') return false;
104+
let c1 = modulePath[1], c2 = modulePath[2];
105+
if ((c0 === '.' && (!c1 || c1 === '/' || c1 === '\\\\')) ||
106+
(c0 === '.' && c1 === '.' && (!c2 || c2 === '/' || c2 === '\\\\'))) return false;
107+
if (c1 === ':' && (c2 === '/' || c2 === '\\\\'))
108+
return false;
109+
return true;
110+
}
111+
112+
export function commonjsRequire (path, originalModuleDir) {
113+
const shouldTryNodeModules = isPossibleNodeModulesPath(path);
114+
path = normalize(path);
115+
let relPath;
116+
while (true) {
117+
if (!shouldTryNodeModules) {
118+
relPath = originalModuleDir ? normalize(originalModuleDir + '/' + path) : path;
119+
} else if (originalModuleDir) {
120+
relPath = normalize(originalModuleDir + '/node_modules/' + path);
121+
} else {
122+
relPath = normalize(join('node_modules', path));
123+
}
124+
for (let extensionIndex = 0; extensionIndex < CHECKED_EXTENSIONS.length; extensionIndex++) {
125+
const resolvedPath = relPath + CHECKED_EXTENSIONS[extensionIndex];
126+
let cachedModule = DYNAMIC_REQUIRE_CACHE[resolvedPath];
127+
if (cachedModule) return cachedModule.exports;
128+
const loader = DYNAMIC_REQUIRE_LOADERS[resolvedPath];
129+
if (loader) {
130+
DYNAMIC_REQUIRE_CACHE[resolvedPath] = cachedModule = {
131+
id: resolvedPath,
132+
filename: resolvedPath,
133+
exports: {},
134+
parent: DEFAULT_PARENT_MODULE,
135+
loaded: false,
136+
children: [],
137+
paths: []
138+
};
139+
try {
140+
loader.call(commonjsGlobal, cachedModule, cachedModule.exports);
141+
} catch (error) {
142+
delete DYNAMIC_REQUIRE_CACHE[resolvedPath];
143+
throw error;
144+
}
145+
cachedModule.loaded = true;
146+
return cachedModule.exports;
147+
};
148+
}
149+
if (!shouldTryNodeModules) break;
150+
const nextDir = normalize(originalModuleDir + '/..');
151+
if (nextDir === originalModuleDir) break;
152+
originalModuleDir = nextDir;
153+
}
154+
return require(path);
155+
}
156+
157+
commonjsRequire.cache = DYNAMIC_REQUIRE_CACHE;
158+
`;

0 commit comments

Comments
 (0)