Skip to content

Commit

Permalink
major: output ESM for .mjs or "type": "module" builds (#720)
Browse files Browse the repository at this point in the history
* Revert "increase jest watcher timeout"

This reverts commit 6a3991d.

* --esm flag for emitting es modules

* add fixture

* disable v8cache for esm

* use node14 target

* output esm automatically for mjs / type module

* mjs for mjs

* update unit tests

* update cli tests

* update asset relocator loader

* fixup filename handling

* fixup resolve ref

* fixup fs ref

* update unit tests

* fixup esm check

* fixup external type

* fixup webpack externals

* webpack@5.44

* update fixtures

* add fixtures update script

* unify package boundary check

* Remove newline

* fixup: pr feedback

* fixup: include has type module module

Co-authored-by: Steven <steven@ceriously.com>
  • Loading branch information
guybedford and styfle committed Jul 16, 2021
1 parent 51b5fc1 commit 8481ca9
Show file tree
Hide file tree
Showing 42 changed files with 201 additions and 214 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -28,7 +28,7 @@
"@sentry/node": "^4.3.0",
"@slack/web-api": "^5.13.0",
"@tensorflow/tfjs-node": "^0.3.0",
"@vercel/webpack-asset-relocator-loader": "1.5.0",
"@vercel/webpack-asset-relocator-loader": "1.6.0",
"analytics-node": "^3.3.0",
"apollo-server-express": "^2.2.2",
"arg": "^4.1.0",
Expand Down Expand Up @@ -110,7 +110,7 @@
"vue": "^2.5.17",
"vue-server-renderer": "^2.5.17",
"web-vitals": "^0.2.4",
"webpack": "5.43.0",
"webpack": "5.44.0",
"when": "^3.7.8"
},
"resolutions": {
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -38,6 +38,8 @@ Eg:
$ ncc build input.js -o dist
```

If building an `.mjs` or `.js` module inside a `"type": "module"` [package boundary](https://nodejs.org/dist/latest-v16.x/docs/api/packages.html#packages_package_json_and_file_extensions), an ES module output will be created automatically.

Outputs the Node.js compact build of `input.js` into `dist/index.js`.

> Note: If the input file is using a `.cjs` extension, then so will the corresponding output file.
Expand Down
5 changes: 4 additions & 1 deletion src/cli.js
Expand Up @@ -6,6 +6,7 @@ const shebangRegEx = require("./utils/shebang");
const rimraf = require("rimraf");
const crypto = require("crypto");
const { writeFileSync, unlink, existsSync, symlinkSync } = require("fs");
const { hasTypeModule } = require('./utils/has-type-module');
const mkdirp = require("mkdirp");
const { version: nccVersion } = require('../package.json');

Expand Down Expand Up @@ -218,6 +219,7 @@ async function runCmd (argv, stdout, stderr) {
);
if (existsSync(outDir))
rimraf.sync(outDir);
mkdirp.sync(outDir);
run = true;

// fallthrough
Expand All @@ -228,7 +230,8 @@ async function runCmd (argv, stdout, stderr) {
let startTime = Date.now();
let ps;
const buildFile = eval("require.resolve")(resolve(args._[1] || "."));
const ext = buildFile.endsWith('.cjs') ? '.cjs' : '.js';
const esm = buildFile.endsWith('.mjs') || !buildFile.endsWith('.cjs') && hasTypeModule(buildFile);
const ext = buildFile.endsWith('.cjs') ? '.cjs' : esm && (buildFile.endsWith('.mjs') || !hasTypeModule(buildFile)) ? '.mjs' : '.js';
const ncc = require("./index.js")(
buildFile,
{
Expand Down
48 changes: 34 additions & 14 deletions src/index.js
Expand Up @@ -12,6 +12,7 @@ const shebangRegEx = require('./utils/shebang');
const nccCacheDir = require("./utils/ncc-cache-dir");
const LicenseWebpackPlugin = require('license-webpack-plugin').LicenseWebpackPlugin;
const { version: nccVersion } = require('../package.json');
const { hasTypeModule } = require('./utils/has-type-module');

// support glob graceful-fs
fs.gracefulify(require("fs"));
Expand All @@ -36,8 +37,9 @@ function ncc (
{
cache,
customEmit = undefined,
esm = entry.endsWith('.mjs') || !entry.endsWith('.cjs') && hasTypeModule(entry),
externals = [],
filename = 'index' + (entry.endsWith('.cjs') ? '.cjs' : '.js'),
filename = 'index' + (!esm && entry.endsWith('.cjs') ? '.cjs' : esm && (entry.endsWith('.mjs') || !hasTypeModule(entry)) ? '.mjs' : '.js'),
minify = false,
sourceMap = false,
sourceMapRegister = true,
Expand All @@ -55,6 +57,10 @@ function ncc (
production = true,
} = {}
) {
// v8 cache not supported for ES modules
if (esm)
v8cache = false;

const cjsDeps = () => ({
mainFields: ["main"],
extensions: SUPPORTED_EXTENSIONS,
Expand All @@ -74,7 +80,7 @@ function ncc (

if (!quiet) {
console.log(`ncc: Version ${nccVersion}`);
console.log(`ncc: Compiling file ${filename}`);
console.log(`ncc: Compiling file ${filename} into ${esm ? 'ESM' : 'CJS'}`);
}

if (target && !target.startsWith('es')) {
Expand Down Expand Up @@ -233,7 +239,8 @@ function ncc (
},
amd: false,
experiments: {
topLevelAwait: true
topLevelAwait: true,
outputModule: esm
},
optimization: {
nodeEnv: false,
Expand All @@ -247,7 +254,7 @@ function ncc (
},
devtool: sourceMap ? "cheap-module-source-map" : false,
mode: "production",
target: target ? ["node", target] : "node",
target: target ? ["node14", target] : "node14",
stats: {
logging: 'error'
},
Expand All @@ -258,8 +265,9 @@ function ncc (
path: "/",
// Webpack only emits sourcemaps for files ending in .js
filename: ext === '.cjs' ? filename + '.js' : filename,
libraryTarget: "commonjs2",
strictModuleExceptionHandling: true
libraryTarget: esm ? 'module' : 'commonjs2',
strictModuleExceptionHandling: true,
module: esm
},
resolve: {
extensions: SUPPORTED_EXTENSIONS,
Expand All @@ -286,9 +294,9 @@ function ncc (
},
// https://github.com/vercel/ncc/pull/29#pullrequestreview-177152175
node: false,
externals ({ context, request }, callback) {
externals ({ context, request, dependencyType }, callback) {
const external = externalMap.get(request);
if (external) return callback(null, `commonjs ${external}`);
if (external) return callback(null, `${dependencyType === 'esm' && esm ? 'module' : 'node-commonjs'} ${external}`);
return callback();
},
module: {
Expand Down Expand Up @@ -465,12 +473,13 @@ function ncc (
let result;
try {
result = await terser.minify(code, {
module: esm,
compress: false,
mangle: {
keep_classnames: true,
keep_fnames: true
},
sourceMap: sourceMap ? {
sourceMap: map ? {
content: map,
filename,
url: `${filename}.map`
Expand All @@ -483,10 +492,10 @@ function ncc (

({ code, map } = {
code: result.code,
map: sourceMap ? JSON.parse(result.map) : undefined
map: map ? JSON.parse(result.map) : undefined
});
}
catch {
catch (e) {
console.log('An error occurred while minifying. The result will not be minified.');
}
}
Expand All @@ -511,9 +520,20 @@ function ncc (
`if (cachedData) process.on('exit', () => { try { writeFileSync(basename + '.cache', script.createCachedData()); } catch(e) {} });\n`;
}

if (sourceMap && sourceMapRegister) {
code = `require('./sourcemap-register${ext}');` + code;
assets[`sourcemap-register${ext}`] = { source: fs.readFileSync(`${__dirname}/sourcemap-register.js.cache.js`), permissions: defaultPermissions };
if (map && sourceMapRegister) {
const registerExt = esm ? '.cjs' : ext;
code = (esm ? `import './sourcemap-register${registerExt}';` : `require('./sourcemap-register${registerExt}');`) + code;
assets[`sourcemap-register${registerExt}`] = { source: fs.readFileSync(`${__dirname}/sourcemap-register.js.cache.js`), permissions: defaultPermissions };
}

if (esm && !filename.endsWith('.mjs')) {
// always output a "type": "module" package JSON for esm builds
const baseDir = dirname(filename);
const pjsonPath = (baseDir === '.' ? '' : baseDir) + 'package.json';
if (assets[pjsonPath])
assets[pjsonPath].source = JSON.stringify(Object.assign(JSON.parse(pjsonPath.source.toString()), { type: 'module' }));
else
assets[pjsonPath] = { source: JSON.stringify({ type: 'module' }, null, 2) + '\n', permissions: defaultPermissions };
}

if (shebangMatch) {
Expand Down
15 changes: 15 additions & 0 deletions src/utils/has-type-module.js
@@ -0,0 +1,15 @@
const { resolve } = require('path');
const { readFileSync } = require('fs');

exports.hasTypeModule = function hasTypeModule (path) {
while (path !== (path = resolve(path, '..'))) {
try {
return JSON.parse(readFileSync(eval('resolve')(path, 'package.json')).toString()).type === 'module';
}
catch (e) {
if (e.code === 'ENOENT')
continue;
throw e;
}
}
}
9 changes: 8 additions & 1 deletion test/cli.js
Expand Up @@ -73,7 +73,7 @@
{
args: ["build", "-o", "tmp", "test/fixtures/test.mjs"],
expect (code, stdout, stderr) {
return stdout.toString().indexOf('tmp/index.js') !== -1;
return stdout.toString().indexOf('tmp/index.mjs') !== -1;
}
},
{
Expand Down Expand Up @@ -103,5 +103,12 @@
expect (code, stdout) {
return code === 0 && stdout.indexOf('ncc built-in') !== -1;
},
},
{
args: ["build", "-o", "tmp", "test/fixtures/module.cjs"],
expect (code, stdout) {
const fs = require('fs');
return code === 0 && fs.readFileSync('tmp/index.js', 'utf8').toString().indexOf('export {') === -1;
}
}
]
3 changes: 3 additions & 0 deletions test/fixtures/module.cjs
@@ -0,0 +1,3 @@
require('./no-dep.js');

exports.aRealExport = true;
4 changes: 3 additions & 1 deletion test/unit/bundle-subasset/output-coverage.js
Expand Up @@ -84,7 +84,9 @@ module.exports = require("path");
/******/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
Expand Down
4 changes: 3 additions & 1 deletion test/unit/bundle-subasset/output.js
Expand Up @@ -84,7 +84,9 @@ module.exports = require("path");
/******/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
Expand Down
4 changes: 3 additions & 1 deletion test/unit/bundle-subasset2/output-coverage.js
Expand Up @@ -75,7 +75,9 @@
/******/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
/******/
/************************************************************************/
var __webpack_exports__ = {};
// ESM COMPAT FLAG
__nccwpck_require__.r(__webpack_exports__);
Expand Down
4 changes: 3 additions & 1 deletion test/unit/bundle-subasset2/output.js
Expand Up @@ -75,7 +75,9 @@
/******/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
/******/
/************************************************************************/
var __webpack_exports__ = {};
// ESM COMPAT FLAG
__nccwpck_require__.r(__webpack_exports__);
Expand Down
4 changes: 3 additions & 1 deletion test/unit/custom-emit/output-coverage.js
Expand Up @@ -44,7 +44,9 @@ module.exports = require("fs");
/************************************************************************/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
Expand Down
4 changes: 3 additions & 1 deletion test/unit/custom-emit/output.js
Expand Up @@ -44,7 +44,9 @@ module.exports = require("fs");
/************************************************************************/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
Expand Down
32 changes: 6 additions & 26 deletions test/unit/exports-nomodule/output-coverage.js
@@ -1,34 +1,14 @@
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ // The require scope
/******/ var __nccwpck_require__ = {};
/******/
/******/ "use strict";
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/) ? 1 : 0, -1) + "/";
/******/
/************************************************************************/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __nccwpck_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
var __webpack_exports__ = {};
// ESM COMPAT FLAG
__nccwpck_require__.r(__webpack_exports__);

;// CONCATENATED MODULE: ./test/unit/exports-nomodule/node.js
var x = 'x';

;// CONCATENATED MODULE: ./test/unit/exports-nomodule/input.js

console.log(x);

module.exports = __webpack_exports__;
/******/ })()
;
console.log(x);
32 changes: 6 additions & 26 deletions test/unit/exports-nomodule/output.js
@@ -1,34 +1,14 @@
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ // The require scope
/******/ var __nccwpck_require__ = {};
/******/
/******/ "use strict";
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/) ? 1 : 0, -1) + "/";
/******/
/************************************************************************/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __nccwpck_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/";/************************************************************************/
var __webpack_exports__ = {};
// ESM COMPAT FLAG
__nccwpck_require__.r(__webpack_exports__);

;// CONCATENATED MODULE: ./test/unit/exports-nomodule/node.js
var x = 'x';

;// CONCATENATED MODULE: ./test/unit/exports-nomodule/input.js

console.log(x);

module.exports = __webpack_exports__;
/******/ })()
;
console.log(x);

0 comments on commit 8481ca9

Please sign in to comment.