Skip to content

Commit

Permalink
feat(commonjs): support dynamic require (#206)
Browse files Browse the repository at this point in the history
* 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 <lukas.taegert-atkinson@tngtech.com>
  • Loading branch information
danielgindi and lukastaegert authored Mar 26, 2020
1 parent 1bee4e9 commit cbc341d
Show file tree
Hide file tree
Showing 83 changed files with 3,900 additions and 748 deletions.
27 changes: 27 additions & 0 deletions packages/commonjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,33 @@ Then call `rollup` either via the [CLI](https://www.rollupjs.org/guide/en/#comma

## Options

### `dynamicRequireTargets`

Type: `String|Array[String]`<br>
Default: `[]`

Some modules contain dynamic `require` calls, or require modules that contain circular dependencies, which are not handled well by static imports.
Including those modules as `dynamicRequireTargets` will simulate a CommonJS (NodeJS-like) environment for them with support for dynamic and circular dependencies.

_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/"` -> `"/"`._

Example:

```js
commonjs({
dynamicRequireTargets: [
// include using a glob pattern (either a string or an array of strings)
'node_modules/logform/*.js',

// exclude files that are known to not be required dynamically, this allows for better optimizations
'!node_modules/logform/index.js',
'!node_modules/logform/format.js',
'!node_modules/logform/levels.js',
'!node_modules/logform/browser.js'
]
});
```

### `exclude`

Type: `String` | `Array[...String]`<br>
Expand Down
2 changes: 2 additions & 0 deletions packages/commonjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
},
"dependencies": {
"@rollup/pluginutils": "^3.0.8",
"commondir": "^1.0.1",
"estree-walker": "^1.0.1",
"glob": "^7.1.2",
"is-reference": "^1.1.2",
"magic-string": "^0.25.2",
"resolve": "^1.11.0"
Expand Down
27 changes: 27 additions & 0 deletions packages/commonjs/src/dynamic-require-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { statSync } from 'fs';
import glob from 'glob';
import { resolve } from 'path';
import { normalizePathSlashes } from './transform';

export default function getDynamicRequirePaths(patterns) {
const dynamicRequireModuleSet = new Set();
for (const pattern of (!patterns || Array.isArray(patterns)) ? patterns || [] : [patterns]) {
const isNegated = pattern.startsWith('!');
const modifySet = Set.prototype[isNegated ? 'delete' : 'add'].bind(
dynamicRequireModuleSet
);
for (const path of glob.sync(isNegated ? pattern.substr(1) : pattern)) {
modifySet(normalizePathSlashes(resolve(path)));
}
}
const dynamicRequireModuleDirPaths = Array.from(dynamicRequireModuleSet.values()).filter(path => {
try {
if (statSync(path).isDirectory())
return true;
} catch (ignored) {
// Nothing to do here
}
return false;
});
return { dynamicRequireModuleSet, dynamicRequireModuleDirPaths };
}
138 changes: 133 additions & 5 deletions packages/commonjs/src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@ export const EXTERNAL_SUFFIX = '?commonjs-external';
export const getExternalProxyId = (id) => `\0${id}${EXTERNAL_SUFFIX}`;
export const getIdFromExternalProxyId = (proxyId) => proxyId.slice(1, -EXTERNAL_SUFFIX.length);

export const VIRTUAL_PATH_BASE = '/$$rollup_base$$';
export const getVirtualPathForDynamicRequirePath = (path, commonDir) => {
if (path.startsWith(commonDir))
return VIRTUAL_PATH_BASE + path.slice(commonDir.length);
return path;
};

export const DYNAMIC_REGISTER_PREFIX = '\0commonjs-dynamic-register:';
export const DYNAMIC_JSON_PREFIX = '\0commonjs-dynamic-json:';
export const DYNAMIC_PACKAGES_ID = '\0commonjs-dynamic-packages';

export const HELPERS_ID = '\0commonjsHelpers.js';

// `x['default']` is used instead of `x.default` for backward compatibility with ES3 browsers.
// Minifiers like uglify will usually transpile it back if compatibility with ES3 is not enabled.
export const HELPERS = `
export var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
export function commonjsRequire () {
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
}
export function unwrapExports (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
Expand All @@ -27,4 +34,125 @@ export function createCommonjsModule(fn, module) {
export function getCjsExportFromNamespace (n) {
return n && n['default'] || n;
}`;
}
`;

export const HELPER_NON_DYNAMIC = `
export function commonjsRequire () {
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs');
}
`;

export const HELPERS_DYNAMIC = `
export function commonjsRegister (path, loader) {
DYNAMIC_REQUIRE_LOADERS[path] = loader;
}
const DYNAMIC_REQUIRE_LOADERS = Object.create(null);
const DYNAMIC_REQUIRE_CACHE = Object.create(null);
const DEFAULT_PARENT_MODULE = {
id: '<' + 'rollup>', exports: {}, parent: undefined, filename: null, loaded: false, children: [], paths: []
};
const CHECKED_EXTENSIONS = ['', '.js', '.json'];
function normalize (path) {
path = path.replace(/\\\\/g, '/');
const parts = path.split('/');
const slashed = parts[0] === '';
for (let i = 1; i < parts.length; i++) {
if (parts[i] === '.' || parts[i] === '') {
parts.splice(i--, 1);
}
}
for (let i = 1; i < parts.length; i++) {
if (parts[i] !== '..') continue;
if (i > 0 && parts[i - 1] !== '..' && parts[i - 1] !== '.') {
parts.splice(--i, 2);
i--;
}
}
path = parts.join('/');
if (slashed && path[0] !== '/')
path = '/' + path;
else if (path.length === 0)
path = '.';
return path;
}
function join () {
if (arguments.length === 0)
return '.';
let joined;
for (let i = 0; i < arguments.length; ++i) {
let arg = arguments[i];
if (arg.length > 0) {
if (joined === undefined)
joined = arg;
else
joined += '/' + arg;
}
}
if (joined === undefined)
return '.';
return joined;
}
function isPossibleNodeModulesPath (modulePath) {
let c0 = modulePath[0];
if (c0 === '/' || c0 === '\\\\') return false;
let c1 = modulePath[1], c2 = modulePath[2];
if ((c0 === '.' && (!c1 || c1 === '/' || c1 === '\\\\')) ||
(c0 === '.' && c1 === '.' && (!c2 || c2 === '/' || c2 === '\\\\'))) return false;
if (c1 === ':' && (c2 === '/' || c2 === '\\\\'))
return false;
return true;
}
export function commonjsRequire (path, originalModuleDir) {
const shouldTryNodeModules = isPossibleNodeModulesPath(path);
path = normalize(path);
let relPath;
while (true) {
if (!shouldTryNodeModules) {
relPath = originalModuleDir ? normalize(originalModuleDir + '/' + path) : path;
} else if (originalModuleDir) {
relPath = normalize(originalModuleDir + '/node_modules/' + path);
} else {
relPath = normalize(join('node_modules', path));
}
for (let extensionIndex = 0; extensionIndex < CHECKED_EXTENSIONS.length; extensionIndex++) {
const resolvedPath = relPath + CHECKED_EXTENSIONS[extensionIndex];
let cachedModule = DYNAMIC_REQUIRE_CACHE[resolvedPath];
if (cachedModule) return cachedModule.exports;
const loader = DYNAMIC_REQUIRE_LOADERS[resolvedPath];
if (loader) {
DYNAMIC_REQUIRE_CACHE[resolvedPath] = cachedModule = {
id: resolvedPath,
filename: resolvedPath,
exports: {},
parent: DEFAULT_PARENT_MODULE,
loaded: false,
children: [],
paths: []
};
try {
loader.call(commonjsGlobal, cachedModule, cachedModule.exports);
} catch (error) {
delete DYNAMIC_REQUIRE_CACHE[resolvedPath];
throw error;
}
cachedModule.loaded = true;
return cachedModule.exports;
};
}
if (!shouldTryNodeModules) break;
const nextDir = normalize(originalModuleDir + '/..');
if (nextDir === originalModuleDir) break;
originalModuleDir = nextDir;
}
return require(path);
}
commonjsRequire.cache = DYNAMIC_REQUIRE_CACHE;
`;
Loading

0 comments on commit cbc341d

Please sign in to comment.