Skip to content

Commit

Permalink
Add proper handling of ES modules externals
Browse files Browse the repository at this point in the history
Adding support for pure ESM packages ([see](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)). This is achieved by using proper external type (`commonjs` vs. `module`) in webpack's external config, so that proper code is generated to import ES modules in the server bundle. A few important facts:
* Node.js requirement had to be bumped to >= 12.17, in order to use the dynamic `import()`
* The `server-main.js` cjs entry becomes an "async module", so it returns a Promise that need to be awaited when using `require()`
* Yarn PnP does not support ESM yet (see yarnpkg/berry#638); we'll bundle esm dependencies on the server to workaround this when project is using PnP (the `process.versions.pnp` check)

Co-authored-by: Ryan Tsao <ryan.j.tsao@gmail.com>
  • Loading branch information
2 people authored and fusionjs-sync-bot[bot] committed Sep 16, 2021
1 parent be2f809 commit aff9322
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 29 deletions.
File renamed without changes.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions fusion-cli-tests-fixtures/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"name": "fusion-cli-tests-fixtures",
"private": true,
"workspaces": [
"fixtures/node_modules/fixture-macro-pkg",
"fixtures/node_modules/fixture-es2017-pkg"
"fixtures/node_modules/*"
]
}
1 change: 1 addition & 0 deletions fusion-cli-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"eslint-import-resolver-node": "^0.3.4",
"fixture-es2017-pkg": "0.0.0-monorepo",
"fixture-macro-pkg": "0.0.0-monorepo",
"fixture-pure-esm-pkg": "0.0.0-monorepo",
"flow-bin": "^0.109.0",
"fusion-core": "0.0.0-monorepo",
"fusion-plugin-apollo": "0.0.0-monorepo",
Expand Down
2 changes: 1 addition & 1 deletion fusion-cli-tests/test/e2e/empty/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test('generates error if missing default export', async () => {
// $FlowFixMe
t.fail('did not error');
} catch (e) {
t.ok(e.stderr.includes(' is not a function'));
t.ok(e.stderr.includes('App should export a function'));
} finally {
proc.kill('SIGKILL');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @noflow
import App from 'fusion-core';
import pureEsm from 'fixture-pure-esm-pkg';

export default async function () {
return new App('element', () => {
return pureEsm;
});
};
46 changes: 46 additions & 0 deletions fusion-cli-tests/test/e2e/pure-esm-package-import/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @flow
/* eslint-env node */

const t = require('assert');
const path = require('path');
const puppeteer = require('puppeteer');

const dev = require('../setup.js');
const {start, cmd} = require('../utils.js');

const dir = path.resolve(__dirname, './fixture');

test('`fusion dev` with pure esm package import', async () => {
const app = dev(dir);
const {browser, url} = await app.setup();

const page = await browser.newPage();
await page.goto(`${url}/`, {waitUntil: 'load'});

const content = await page.content();
t.ok(content.includes('FIXTURE_PURE_ESM_PACKAGE_CONTENT'));

app.teardown();
}, 15000);

test('`fusion build` with pure esm package import', async () => {
await cmd(`build --dir=${dir} --production`);
const {proc, port} = await start(`--dir=${dir}`, {
env: {
...process.env,
NODE_ENV: 'production',
},
});

const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.goto(`http://localhost:${port}/`, {waitUntil: 'load'});

const content = await page.content();
t.ok(content.includes('FIXTURE_PURE_ESM_PACKAGE_CONTENT'));

browser.close();
proc.kill('SIGKILL');
}, 25000);

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

22 changes: 18 additions & 4 deletions fusion-cli/build/get-webpack-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ const path = require('path');
const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const ChunkIdPrefixPlugin = require('./plugins/chunk-id-prefix-plugin.js');
const resolveFrom = require('../lib/resolve-from');
const resolveFrom = require('../lib/resolve-from.js');
const isEsModule = require('../lib/is-es-module.js');
const LoaderContextProviderPlugin = require('./plugins/loader-context-provider-plugin.js');
const ChildCompilationPlugin = require('./plugins/child-compilation-plugin.js');
const NodeSourcePlugin = require('./plugins/node-source-plugin.js');
Expand Down Expand Up @@ -322,7 +323,7 @@ function getWebpackConfig(opts /*: WebpackConfigOpts */) {
/(\/\.yarn(\/[^/]+)*\/cache\/[^/]+\.zip|\/\.yarn\/(?:\$\$virtual|__virtual__)(?!(\/[^/]+){2}\/.+$))/,
},
name: runtime,
target,
target: target === 'node' ? 'node12.17' : target,
entry: {
main: [
runtime === 'client' &&
Expand Down Expand Up @@ -604,14 +605,27 @@ function getWebpackConfig(opts /*: WebpackConfigOpts */) {
// if module is missing, skip rewriting to absolute path
return callback(null, request);
}

const isEsm = isEsModule(absolutePath);
if (isEsm) {
if (typeof process.versions.pnp !== 'undefined') {
// Yarn PnP does not support es modules yet,
// have to bundle this dependency on the server
// @see: https://github.com/yarnpkg/berry/issues/638
return callback();
}
}

const moduleType = isEsm ? 'module' : 'commonjs';

if (experimentalBundleTest) {
const bundle = experimentalBundleTest(
absolutePath,
'browser-only'
);
if (bundle === 'browser-only') {
// don't bundle on the server
return callback(null, 'commonjs ' + absolutePath);
return callback(null, `${moduleType} ${absolutePath}`);
} else if (bundle === 'universal') {
// bundle on the server
return callback();
Expand All @@ -621,7 +635,7 @@ function getWebpackConfig(opts /*: WebpackConfigOpts */) {
);
}
}
return callback(null, 'commonjs ' + absolutePath);
return callback(null, `${moduleType} ${absolutePath}`);
}
// bundle everything else (local files, __*)
return callback();
Expand Down
2 changes: 1 addition & 1 deletion fusion-cli/commands/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ exports.run = async function (
if (env) {
const entry = getEntry(env);
// $FlowFixMe
const {start} = require(entry);
const {start} = await require(entry);
return start({dir, port: port || process.env.PORT_HTTP || 3000}); // handle server bootstrap errors (e.g. port already in use)
} else {
throw new Error(`App can't start. JS isn't compiled`); // handle compilation errors
Expand Down
16 changes: 8 additions & 8 deletions fusion-cli/entries/server-entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,20 @@ import ContextPlugin from '../plugins/context-plugin';
import ServerErrorPlugin from '../plugins/server-error-plugin';
import {SSRBodyTemplate} from '../plugins/ssr-plugin';
import stripRoutePrefix from '../lib/strip-prefix.js';
// $FlowFixMe
import main from '__FUSION_ENTRY_PATH__'; // eslint-disable-line import/no-unresolved

let prefix = process.env.ROUTE_PREFIX;
let AssetsPlugin;

// $FlowFixMe
const main = require('__FUSION_ENTRY_PATH__'); // eslint-disable-line import/no-unresolved, import/no-extraneous-dependencies

let server = null;
const state = {serve: null};
const initialize = main
? main.default || main
: () => {
throw new Error('App should export a function');
};
const initialize =
typeof main === 'function'
? main
: () => {
throw new Error('App should export a function');
};

export async function start({port, dir = '.'} /*: any */) {
AssetsPlugin = AssetsFactory(dir);
Expand Down
86 changes: 86 additions & 0 deletions fusion-cli/lib/is-es-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// @flow
/* eslint-env node */
const path = require('path');
const fs = require('fs');

function tryReadFile(filepath) {
try {
return fs.readFileSync(filepath, 'utf-8');
} catch {
return false;
}
}

// Adopted from Node.js internal lib
// @see: https://github.com/nodejs/node/blob/da0ede1ad55a502a25b4139f58aab3fb1ee3bf3f/lib/internal/modules/cjs/loader.js#L288-L336
const packageJsonCache = new Map();
function readPackage(requestPath) {
const jsonPath = path.resolve(requestPath, 'package.json');

if (packageJsonCache.has(jsonPath)) {
return packageJsonCache.get(jsonPath);
}

const json = tryReadFile(jsonPath);
if (json === false) {
return false;
}

try {
const parsed = JSON.parse(json);
const filtered = {
name: parsed.name,
main: parsed.main,
exports: parsed.exports,
imports: parsed.imports,
type: parsed.type,
};
packageJsonCache.set(jsonPath, filtered);
return filtered;
} catch (e) {
e.path = jsonPath;
e.message = 'Error parsing ' + jsonPath + ': ' + e.message;
throw e;
}
}

function readPackageScope(checkPath) {
const rootSeparatorIndex = checkPath.indexOf(path.sep);
let separatorIndex;
do {
separatorIndex = checkPath.lastIndexOf(path.sep);
checkPath = checkPath.slice(0, separatorIndex);
if (checkPath.endsWith(path.sep + 'node_modules')) return false;
const pjson = readPackage(checkPath + path.sep);
if (pjson)
return {
data: pjson,
path: checkPath,
};
} while (separatorIndex > rootSeparatorIndex);
return false;
}

// @see: https://nodejs.org/api/packages.html#packages_determining_module_system
function isEsModule(modulePath /*: string*/) {
if (modulePath.endsWith('.cjs')) {
return false;
}

if (modulePath.endsWith('.mjs')) {
return true;
}

// @see: https://github.com/nodejs/node/blob/da0ede1ad55a502a25b4139f58aab3fb1ee3bf3f/lib/internal/modules/cjs/loader.js#L1120-L1123
if (modulePath.endsWith('.js')) {
const pkg = readPackageScope(modulePath);

if (pkg && pkg.data && pkg.data.type === 'module') {
return true;
}
}

return false;
}

module.exports = isEsModule;
2 changes: 1 addition & 1 deletion fusion-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"request-promise": "^4.2.6"
},
"engines": {
"node": ">=8.9.4",
"node": ">=12.17",
"npm": ">=5.0.0",
"yarn": ">=1.0.0"
},
Expand Down

0 comments on commit aff9322

Please sign in to comment.