Skip to content

Commit

Permalink
cjs auto-detection on by default (#1163)
Browse files Browse the repository at this point in the history
  • Loading branch information
FredKSchott authored Oct 1, 2020
1 parent bed0968 commit 00feade
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 34 deletions.
5 changes: 2 additions & 3 deletions docs/docs/05-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,8 @@ $ snowpack build --clean
- Alias an installed package name. This applies to imports within your application and within your installed dependency graph.
- Example: `"alias": {"react": "preact/compat", "react-dom": "preact/compat"}`
- **`installOptions.namedExports`** | `string[]`
- Legacy Common.js (CJS) packages should only be imported by the default import (Example: `import reactTable from 'react-table'`)
- But, some packages use named exports in their documentation, which can cause confusion for users. (Example: `import {useTable} from 'react-table'`)
- You can enable "fake/synthetic" named exports for Common.js package by adding the package name under this configuration.
- *NOTE(v2.13.0): Snowpack now automatically supports named exports for most Common.js packages. This configuration remains for any package that Snowpack can't handle automatically. In most cases, this should no longer be needed.*
- Import CJS packages using named exports (Example: `import {useTable} from 'react-table'`).
- Example: `"namedExports": ["react-table"]`
- **`installOptions.rollup`** | `Object`
- Snowpack uses Rollup internally to install your packages. This `rollup` config option gives you deeper control over the internal rollup configuration that we use.
Expand Down
12 changes: 2 additions & 10 deletions docs/docs/09-troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,7 @@ If you see this error message, that means that you've imported a file path not a

### Uncaught SyntaxError: The requested module '/web_modules/XXXXXX.js' does not provide an export named 'YYYYYY'

Snowpack follow's Node.js's CJS-ESM interoperability strategy, where Common.js packages are always exported to the default export (`import react`) and do not support named exports (`import * as react`). Many packages, however, document these named exports in their READMEs and assume that your bundler will support it. We automatically add support for named exports to a small number of very popular packages (like React) that use this sort of documentation.
This is usually seen when importing a named export from a package written in the older Common.js format. Snowpack will try to automatically scan and detect these named exports for legacy Common.js packages, but this is not always possible.

**To solve this issue:** Add the failing package to `installOptions.namedExports` and Snowpack will create those named exports for you automatically (note: you may need to re-run Snowpack with the `--reload` flag to apply this update).
**To solve this issue:** See our documentation on the ["namedExports"](#install-options) configuration option to resolve manually

```json
// snowpack.config.json
{
"installOptions": {
"namedExports": ["someModule"]
}
}
```
1 change: 1 addition & 0 deletions esinstall/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@rollup/plugin-json": "^4.0.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-replace": "^2.3.3",
"cjs-module-lexer": "^0.3.3",
"es-module-lexer": "^0.3.24",
"is-builtin-module": "^3.0.0",
"kleur": "^4.1.1",
Expand Down
74 changes: 56 additions & 18 deletions esinstall/src/rollup-plugins/rollup-plugin-wrap-install-targets.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import * as colors from 'kleur/colors';
import path from 'path';
import fs from 'fs';
import {Plugin} from 'rollup';
import {InstallTarget, AbstractLogger} from '../types';
import {getWebDependencyName} from '../util.js';

function autoDetectExports(fileLoc: string): string[] {
return Object.keys(require(fileLoc)).filter((imp) => imp !== 'default');
}
import parse from 'cjs-module-lexer';

/**
* rollup-plugin-wrap-install-targets
Expand All @@ -27,12 +25,49 @@ export function rollupPluginWrapInstallTargets(
logger: AbstractLogger,
): Plugin {
const installTargetSummaries: {[loc: string]: InstallTarget} = {};
const cjsScannedNamedExports = new Map<string, string[]>();

function isAutoDetect(normalizedFileLoc: string) {
return autoDetectPackageExports.some((p) =>
normalizedFileLoc.includes(`node_modules/${p}${p.endsWith('.js') ? '' : '/'}`),
);
/**
* Runtime analysis: High Fidelity, but not always successful.
* `require()` the CJS file inside of Node.js to load the package and detect it's runtime exports.
*/
function cjsAutoDetectExportsRuntime(normalizedFileLoc: string): string[] | undefined {
try {
const mod = require(normalizedFileLoc);
// skip analysis for non-object modules, these can only be the default export.
if (!mod || (mod.constructor !== Object)) {
return;
}
// Collect and filter all properties of the object as named exports.
return Object.keys(mod).filter((imp) => imp !== 'default');
} catch (err) {
logger.debug(
`✘ Runtime CJS auto-detection for ${colors.bold(
normalizedFileLoc,
)} unsuccessful. Falling back to static analysis. ${err.message}`,
);
}
}

/**
* Attempt #2: Static analysis: Lower Fidelity, but safe to run on anything.
* Get the exports that we scanned originally using static analysis. This is meant to run on
* any file (not only CJS) but it will only return an array if CJS exports were found.
*/
function cjsAutoDetectExportsStatic(filename: string): string[] | undefined {
const fileContents = fs.readFileSync(filename, 'utf-8');
try {
const {exports} = parse(fileContents);
// TODO: Also follow & deeply parse dependency "reexports" returned by the lexer.
if (exports.length > 0) {
return exports;
}
} catch (err) {
// Safe to ignore, this is usually due to the file not being CJS.
logger.debug(`cjsAutoDetectExportsStatic error: ${err.message}`);
}
}

return {
name: 'snowpack:wrap-install-targets',
// Mark some inputs for tree-shaking.
Expand All @@ -51,7 +86,14 @@ export function rollupPluginWrapInstallTargets(
}, {} as any);
installTargetSummaries[val] = installTargetSummary;
const normalizedFileLoc = val.split(path.win32.sep).join(path.posix.sep);
if (isAutoDetect(normalizedFileLoc)) {
const isExplicitAutoDetect = autoDetectPackageExports.some((p) =>
normalizedFileLoc.includes(`node_modules/${p}${p.endsWith('.js') ? '' : '/'}`),
);
const cjsExports = isExplicitAutoDetect
? cjsAutoDetectExportsRuntime(val)
: cjsAutoDetectExportsStatic(val);
if (cjsExports) {
cjsScannedNamedExports.set(normalizedFileLoc, cjsExports);
input[key] = `snowpack-wrap:${val}`;
}
if (isTreeshake && !installTargetSummary.all) {
Expand All @@ -72,15 +114,11 @@ export function rollupPluginWrapInstallTargets(
const fileLoc = id.substring('snowpack-wrap:'.length);
// Reduce all install targets into a single "summarized" install target.
const installTargetSummary = installTargetSummaries[fileLoc];
let uniqueNamedImports = Array.from(new Set(installTargetSummary.named));
let uniqueNamedExports = Array.from(new Set(installTargetSummary.named));
const normalizedFileLoc = fileLoc.split(path.win32.sep).join(path.posix.sep);
if ((!isTreeshake || installTargetSummary.namespace) && isAutoDetect(normalizedFileLoc)) {
try {
uniqueNamedImports = autoDetectExports(fileLoc);
} catch (err) {
logger.error(`✘ Could not auto-detect exports for ${colors.bold(fileLoc)}
${err.message}`);
}
const scannedNamedExports = cjsScannedNamedExports.get(normalizedFileLoc);
if (scannedNamedExports && (!isTreeshake || installTargetSummary.namespace)) {
uniqueNamedExports = scannedNamedExports || [];
installTargetSummary.default = true;
}
const result = `
Expand All @@ -90,7 +128,7 @@ ${err.message}`);
? `import __pika_web_default_export_for_treeshaking__ from '${normalizedFileLoc}'; export default __pika_web_default_export_for_treeshaking__;`
: ''
}
${`export {${uniqueNamedImports.join(',')}} from '${normalizedFileLoc}';`}
${`export {${uniqueNamedExports.join(',')}} from '${normalizedFileLoc}';`}
`;
return result;
},
Expand Down
58 changes: 55 additions & 3 deletions test/esinstall/__snapshots__/install.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`snowpack install auto-named-exports: allFiles 1`] = `
Array [
"cjs-named-export-pkg-02.js",
"import-map.json",
]
`;

exports[`snowpack install auto-named-exports: cjs-named-export-pkg-02.js 1`] = `
"var export1 = 1;
var export2 = 'foo';
var export3 = function foo() {};
var export4 = () => {};
var export5 = null;
var entrypoint = {
export1: export1,
export2: export2,
export3: export3,
export4: export4,
export5: export5
};
export default entrypoint;
export { entrypoint as __moduleExports, export1, export2, export3, export4, export5 };"
`;

exports[`snowpack install auto-named-exports: import-map.json 1`] = `
"{
\\"imports\\": {
\\"cjs-named-export-pkg-02\\": \\"./cjs-named-export-pkg-02.js\\"
}
}"
`;

exports[`snowpack install auto-named-exports: output 1`] = `
"[snowpack] ! installing dependencies…
[snowpack] ✔ install complete
[snowpack]
⦿ web_modules/ size gzip brotli
└─ cjs-named-export-pkg-02.js XXXX KB XXXX KB XXXX KB"
`;

exports[`snowpack install config-alias: allFiles 1`] = `
Array [
"import-map.json",
Expand Down Expand Up @@ -22238,7 +22278,11 @@ var httpVueLoader = createCommonjsModule(function (module, exports) {
return httpVueLoader;
});
});
export default httpVueLoader;"
var _baseURI = httpVueLoader._baseURI;
export default httpVueLoader;
var name = httpVueLoader.name;
var template = httpVueLoader.template;
export { httpVueLoader as __moduleExports, _baseURI, name, template };"
`;

exports[`snowpack install include: import-map.json 1`] = `
Expand Down Expand Up @@ -37962,7 +38006,11 @@ var httpVueLoader = createCommonjsModule(function (module, exports) {
return httpVueLoader;
});
});
export default httpVueLoader;"
var _baseURI = httpVueLoader._baseURI;
export default httpVueLoader;
var name = httpVueLoader.name;
var template = httpVueLoader.template;
export { httpVueLoader as __moduleExports, _baseURI, name, template };"
`;

exports[`snowpack install include-ts: import-map.json 1`] = `
Expand Down Expand Up @@ -51901,7 +51949,11 @@ var httpVueLoader = createCommonjsModule(function (module, exports) {
return httpVueLoader;
});
});
export default httpVueLoader;"
var _baseURI = httpVueLoader._baseURI;
export default httpVueLoader;
var name = httpVueLoader.name;
var template = httpVueLoader.template;
export { httpVueLoader as __moduleExports, _baseURI, name, template };"
`;

exports[`snowpack install include-with-package-name: import-map.json 1`] = `
Expand Down
20 changes: 20 additions & 0 deletions test/esinstall/auto-named-exports/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"private": true,
"version": "1.0.1",
"name": "@snowpack/test-auto-named-exports",
"description": "Handle automatic named export detection for cjs packages",
"scripts": {
"testinstall": "snowpack"
},
"snowpack": {
"install": [
"cjs-named-export-pkg-02"
]
},
"dependencies": {
"cjs-named-export-pkg-02": "file:./packages/cjs-named-export-pkg-02"
},
"devDependencies": {
"snowpack": "^2.12.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
exports.export1 = 1;
exports.export2 = 'foo';
exports.export3 = function foo() {}
exports.export4 = () => {}
exports.export5 = null;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "cjs-named-export-pkg-02",
"version": "1.2.3",
"main": "entrypoint.js"
}
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4603,6 +4603,14 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"

cjs-module-lexer@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.3.3.tgz#40b863443a2ad8aa8d70b10afd65b7aba93a3a08"
integrity sha512-gfhBYtBojuFFdenh8qN1IAlfuMVYPT2m1d9oYV27PgQbuNn79ixoBE3a4SkJds6iaIKYmnVT/UAifV13og2DaQ==

"cjs-named-export-pkg-02@file:./test/esinstall/auto-named-exports/packages/cjs-named-export-pkg-02":
version "1.2.3"

"cjs-named-export-pkg@file:./test/esinstall/config-named-exports/packages/cjs-named-export-pkg":
version "1.2.3"

Expand Down

1 comment on commit 00feade

@vercel
Copy link

@vercel vercel bot commented on 00feade Oct 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.