Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite node module handling (npm plugin) #874

Draft
wants to merge 41 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f3fbb1b
Add foundation for a new npm plugin
marvinhagemeister Sep 11, 2021
fde2f51
Acorn: Add missing exportDefaultDeclaration
marvinhagemeister Sep 11, 2021
5908aed
Add support for commonjs default exports
marvinhagemeister Sep 11, 2021
4682b9e
Add support for legacy sub packages
marvinhagemeister Sep 12, 2021
f1dee81
Add support for auto installing npm dependencies
marvinhagemeister Sep 12, 2021
46bb3f2
Add basic support for "exports" field
marvinhagemeister Sep 12, 2021
bd516a1
Fix unable to resolve scoped packages
marvinhagemeister Sep 12, 2021
7624518
Watcher: Ignore `.cache/` folder
marvinhagemeister Sep 12, 2021
34c4f08
Cleanup npm-plugin logging
marvinhagemeister Sep 12, 2021
4f153e8
Experiment with npm autoInstall
marvinhagemeister Sep 12, 2021
7b52e2c
Fix incorrect npm auto-install cache directory
marvinhagemeister Sep 12, 2021
da3bb3b
Fix commonjs rewriting non "module.exports" assginments
marvinhagemeister Sep 12, 2021
ad65d49
Add support for commonjs proxy modules
marvinhagemeister Sep 12, 2021
2139c51
Fix duplicate download requeusts
marvinhagemeister Sep 12, 2021
ac47f47
Drop if-statement if it's unreachable
marvinhagemeister Sep 12, 2021
cea027b
Remove single top level IIFE in CommonJS bundles
marvinhagemeister Sep 12, 2021
121c772
Switch to a non-string based transpiler for commonjs
marvinhagemeister Sep 16, 2021
560c362
Fix CommonJS file not being detected
marvinhagemeister Sep 16, 2021
86ce5cb
Include json in npm bundles
marvinhagemeister Sep 19, 2021
a0aa34a
Improve npm module bundling
marvinhagemeister Sep 19, 2021
9b1d76e
NPM: Bring back disk cache
marvinhagemeister Sep 19, 2021
31419e6
Update to zecorn 0.8.1
marvinhagemeister Sep 19, 2021
d5b5a2a
Use `writeFile` helper
marvinhagemeister Sep 19, 2021
7aa7999
Remove `setCwd` hack
marvinhagemeister Sep 19, 2021
1862bbf
Fix unable to load json in commonjs package
marvinhagemeister Sep 16, 2021
65104d9
NPM: Remove file dependencies on old plugin
marvinhagemeister Sep 29, 2021
b8dd4ac
NPM: Support loading assets from node modules
marvinhagemeister Sep 29, 2021
944d74e
NPM: Add back size warning plugin
marvinhagemeister Sep 29, 2021
4a13701
NPM: Remove old npm plugin
marvinhagemeister Sep 29, 2021
c2adc8a
Bring back etag caching for npm packages
marvinhagemeister Sep 29, 2021
b791f9b
Upgrade zecorn to fix codegen issues
marvinhagemeister Oct 4, 2021
300d7fc
Allow loading assets from auto-installed packages
marvinhagemeister Oct 6, 2021
755826e
NPM: Use already extracted package if available
marvinhagemeister Oct 6, 2021
ee489af
Support auto installing versioned packages
marvinhagemeister Oct 6, 2021
151def0
Fix incorrect CLI argument casing
marvinhagemeister Oct 6, 2021
a9e22b7
Specify custom npm registry via `--registry`
marvinhagemeister Oct 6, 2021
efe3a9e
Add changeset
marvinhagemeister Oct 6, 2021
bedc4da
NPM: Default to `index.js` if no entry point is found
marvinhagemeister Oct 6, 2021
52950ec
Update zecorn to 0.9.5
marvinhagemeister Oct 6, 2021
bbe841c
Reduce test CLI noise
marvinhagemeister Oct 6, 2021
3043967
WIP
marvinhagemeister Oct 7, 2021
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
WIP
  • Loading branch information
marvinhagemeister committed Oct 7, 2021
commit 30439672574d21b0287987c8cc47bedc92d5972e
4 changes: 2 additions & 2 deletions packages/wmr/src/lib/npm-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export function npmEtagCache() {
const url = new URL(req.url, 'https://localhost');
let id = path.posix.normalize(url.pathname);

if (!id.startsWith('/@npm/@id/')) {
if (!id.startsWith('/@npm/')) {
return next();
}

id = id.slice('/@npm/@id/'.length);
id = id.slice('/@npm/'.length);
if (!isValidPackageName(id)) {
return next();
}
Expand Down
25 changes: 24 additions & 1 deletion packages/wmr/src/lib/plugins.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'path';
import htmPlugin from '../plugins/htm-plugin.js';
import sucrasePlugin from '../plugins/sucrase-plugin.js';
import wmrPlugin from '../plugins/wmr/plugin.js';
Expand Down Expand Up @@ -28,6 +29,7 @@ import { lessPlugin } from '../plugins/less-plugin.js';
import { workerPlugin } from '../plugins/worker-plugin.js';
import { npmPlugin } from '../plugins/npm-plugin/index.js';
import tsConfigPathsPlugin from '../plugins/tsconfig-paths-plugin.js';
import { getNpmPlugins } from '../plugins/npm-plugin/npm-bundle.js';

/**
* @param {import("wmr").Options & { isIIFEWorker?: boolean}} options
Expand All @@ -51,6 +53,14 @@ export function getPlugins(options) {
registry
} = options;

const npmCacheDir = path.join(cwd, '.cache', '@npm');

/**
* Map of package name to folder on disk
* @type {Map<string, string>}
*/
const resolutionCache = new Map();

// Plugins are pre-sorted
let split = plugins.findIndex(p => p.enforce === 'post');
if (split === -1) split = plugins.length;
Expand Down Expand Up @@ -102,7 +112,20 @@ export function getPlugins(options) {
// Only transpile CommonJS in node_modules and explicit .cjs files:
include: /(^npm\/|[/\\]node_modules[/\\]|\.cjs$)/
}),
npmPlugin({ cwd, autoInstall, production, registryUrl: registry }),

...(production
? getNpmPlugins({
autoInstall,
production,
cacheDir: npmCacheDir,
cwd,
registryUrl: registry,
resolutionCache,
browserReplacement: new Map()
})
: []),
!production &&
npmPlugin({ cwd, cacheDir: npmCacheDir, autoInstall, production, registryUrl: registry, resolutionCache, alias }),
resolveExtensionsPlugin({
extensions: ['.ts', '.tsx', '.js', '.cjs'],
index: true
Expand Down
34 changes: 21 additions & 13 deletions packages/wmr/src/plugins/npm-plugin/commonjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,28 @@ export function commonjsPlugin({ production }) {

if (!hasCjsKeywords && hasEsmKeywords) return;

const result = transform(code, {
parse: this.parse,
plugins: [
replace({ 'process.env.NODE_ENV': 'development', __DEV__: !!production }),
optimize(),
commonjsToEsm()
]
});
let result;
try {
result = transform(code, {
parse: this.parse,
plugins: [
replace({ 'process.env.NODE_ENV': 'development', __DEV__: !!production }),
optimize(),
commonjsToEsm()
]
});

return {
code: result.code,
// FIXME: Sourcemap
map: null
};
if (code !== result.code) {
console.log('CJS', id, result.code);
return {
code: result.code,
map: result.map
};
}
} catch (err) {
console.log('ERR', code);
throw err;
}
}
};
}
20 changes: 8 additions & 12 deletions packages/wmr/src/plugins/npm-plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ const log = debug('npm', 196);
* @param {boolean} options.autoInstall
* @param {boolean} options.production
* @param {string} options.registryUrl
* @param {string} options.cacheDir
* @param {Record<string, string>} options.alias
* @param {Map<string, string>} options.resolutionCache
* @returns {import('rollup').Plugin}
*/
export function npmPlugin({ cwd, autoInstall, production, registryUrl }) {
export function npmPlugin({ cwd, cacheDir, autoInstall, production, registryUrl, resolutionCache, alias }) {
const PREFIX = '\0npm:';

const cacheDir = path.join(cwd, '.cache', '@npm');

/** @type {Map<string, { code: string, map: any }>} */
const chunkCache = new Map();

Expand All @@ -38,10 +39,11 @@ export function npmPlugin({ cwd, autoInstall, production, registryUrl }) {
* @param {object} options
* @param {string} options.packageName
* @param {string} options.diskCacheDir
* @param {Record<string, string>} options.alias
* @param {Map<string, string>} options.resolutionCache
* @returns {Promise<{ code: string, map: any }>}
*/
async function bundleNpmPackage(id, { packageName, diskCacheDir, resolutionCache }) {
async function bundleNpmPackage(id, { packageName, diskCacheDir, resolutionCache, alias }) {
const deferred = new Deferred();
pending.set(id, deferred);

Expand All @@ -53,7 +55,7 @@ export function npmPlugin({ cwd, autoInstall, production, registryUrl }) {
}

log(kl.dim(`bundle: `) + kl.cyan(id));
let result = await npmBundle(id, { autoInstall, production, cacheDir, cwd, resolutionCache, registryUrl });
let result = await npmBundle(id, { autoInstall, production, cacheDir, cwd, resolutionCache, registryUrl, alias });

await Promise.all(
result.output.map(async chunkOrAsset => {
Expand Down Expand Up @@ -88,12 +90,6 @@ export function npmPlugin({ cwd, autoInstall, production, registryUrl }) {
return chunk;
}

/**
* Map of package name to folder on disk
* @type {Map<string, string>}
*/
const resolutionCache = new Map();

return {
name: 'npm-plugin',
async resolveId(id) {
Expand All @@ -114,7 +110,7 @@ export function npmPlugin({ cwd, autoInstall, production, registryUrl }) {
log(kl.dim(`asset ${id}, wait for bundling `) + kl.cyan(name));
const diskCacheDir = path.join(cacheDir, escapeFilename(name));
if (!deferred) {
await bundleNpmPackage(name, { packageName: name, diskCacheDir, resolutionCache });
await bundleNpmPackage(name, { packageName: name, diskCacheDir, resolutionCache, alias });
} else {
await deferred;
}
Expand Down
63 changes: 54 additions & 9 deletions packages/wmr/src/plugins/npm-plugin/npm-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { npmAutoInstall } from './npm-auto-install.js';
import jsonPlugin from '../json-plugin.js';
import sizeWarningPlugin from './size-warning-plugin.js';
import { onWarn } from '../../lib/output-utils.js';
import aliasPlugin from '../aliases-plugin.js';

/** @type {import('rollup').WarningHandlerWithDefault} */
function customWarn(warning) {
Expand All @@ -22,6 +23,41 @@ function customWarn(warning) {
onWarn(warning);
}

/**
* @param {object} options
* @param {boolean} options.autoInstall
* @param {boolean} options.production
* @param {string} options.cacheDir
* @param {string} options.cwd
* @param {string} options.registryUrl
* @param {string} [options.requestId]
* @param {Map<string, string>} options.resolutionCache
* @param {Map<string, string>} options.browserReplacement
* @returns {import('rollup').Plugin[]}
*/
export function getNpmPlugins({
autoInstall,
production,
cacheDir,
cwd,
resolutionCache,
registryUrl,
browserReplacement,
requestId
}) {
// @ts-ignore
return [
browserFieldPlugin({ browserReplacement }),
!production && requestId && npmExternalDeps({ requestId }),
!process.env.DISABLE_LOCAL_NPM && npmLocalPackage({ root: cwd }),
autoInstall && npmAutoInstall({ cacheDir, registryUrl }),
npmLoad({ browserReplacement, resolutionCache, production }),
commonjsPlugin({ production }),
subPackageLegacy(),
sizeWarningPlugin()
].filter(Boolean);
}

/**
* @param {string} requestId
* @param {object} options
Expand All @@ -30,29 +66,38 @@ function customWarn(warning) {
* @param {string} options.cacheDir
* @param {string} options.cwd
* @param {string} options.registryUrl
* @param {Record<string, string>} options.alias
* @param {Map<string, string>} options.resolutionCache
*/
export async function npmBundle(requestId, { autoInstall, production, cacheDir, cwd, resolutionCache, registryUrl }) {
export async function npmBundle(
requestId,
{ autoInstall, production, cacheDir, cwd, resolutionCache, registryUrl, alias }
) {
const meta = getPackageInfo(requestId);
const pkgName = meta.name;

/** @type {Map<string, string>} */
const browserReplacement = new Map();

console.log('REQUEST', requestId);

const bundle = await rollup.rollup({
input: requestId,
external: [...builtinModules],
onwarn: customWarn,
plugins: [
browserFieldPlugin({ browserReplacement }),
npmExternalDeps({ requestId }),
!process.env.DISABLE_LOCAL_NPM && npmLocalPackage({ root: cwd }),
autoInstall && npmAutoInstall({ cacheDir, registryUrl }),
npmLoad({ browserReplacement, resolutionCache }),
aliasPlugin({ alias }),
jsonPlugin({ root: cwd }),
commonjsPlugin({ production }),
subPackageLegacy({ rootId: requestId }),
sizeWarningPlugin()
...getNpmPlugins({
requestId,
autoInstall,
production,
cacheDir,
cwd,
resolutionCache,
registryUrl,
browserReplacement
})
]
});

Expand Down
7 changes: 4 additions & 3 deletions packages/wmr/src/plugins/npm-plugin/npm-load.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ const log = debug('npm-load');
* @param {object} options
* @param {Map<string, string>} options.browserReplacement
* @param {Map<string, string>} options.resolutionCache
* @param {boolean} options.production
* @returns {import('rollup').Plugin}
*/
export function npmLoad({ browserReplacement, resolutionCache }) {
export function npmLoad({ browserReplacement, resolutionCache, production }) {
return {
name: 'npm-load',
async resolveId(id, importer) {
Expand Down Expand Up @@ -82,7 +83,7 @@ export function npmLoad({ browserReplacement, resolutionCache }) {
const subPkg = await readJson(path.join(modDir, pathname, 'package.json'));
entry = path.join(modDir, pathname, subPkg.module || subPkg.main || 'index.js');
} catch (err) {
entry = pathname;
entry = path.join(modDir, pathname);
}
}
}
Expand All @@ -93,7 +94,7 @@ export function npmLoad({ browserReplacement, resolutionCache }) {

// Some packages use non-js entry files, but rollup only supports js.
// So we expect other plugins to handle assets.
if (!/\.(?:[tj]sx?|[cm]js|[mc]ts)/.test(path.extname(entry))) {
if (!production && !/\.(?:[tj]sx?|[cm]js|[mc]ts)/.test(path.extname(entry))) {
return {
code: '',
map: null,
Expand Down
4 changes: 1 addition & 3 deletions packages/wmr/src/plugins/npm-plugin/sub-package-legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import { isDirectory } from '../../lib/fs-utils.js';
/**
* Legacy way of defining package entry points before the
* "export" field in `package.json` was a thing.
* @param {object} options
* @param {string} options.rootId
* @returns {import('rollup').Plugin}
*/
export function subPackageLegacy({ rootId }) {
export function subPackageLegacy() {
return {
name: 'legacy-sub-package',
async resolveId(id, importer) {
Expand Down
9 changes: 4 additions & 5 deletions packages/wmr/src/plugins/npm-plugin/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,11 @@ export function isValidPackageName(id) {
!/node_modules|favicon\.ico/.test(id) &&
// Must not be a built-in node module
!builtins.has(id) &&
// Must be lowercase
id.toLowerCase() === id &&
// Package name must be lowercase and contain path segment
// if scoped
/^(?:@[^/A-Z]+\/[^/A-Z]+|[^/A-Z]+)/.test(id) &&
// Must not contain special characters
!/[~'!()*;,?:&=+$]/.test(id) &&
// Must contain a second path segment if scoped
((id[0] === '@' && id.indexOf('/') > 0) || true);
!/[~'!()*;,?:&=+$]/.test(id);

return isValid;
}
Expand Down
16 changes: 10 additions & 6 deletions packages/wmr/src/wmr-middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export default function wmrMiddleware(options) {

// Workaround for transform forcing extensionless ids to be
// non-js
let hasIdPrefix = false;
let isVirtual = false;

let file = '';
let id = path;
Expand All @@ -257,14 +257,18 @@ export default function wmrMiddleware(options) {
// Path for virtual modules that refer to an unprefixed id.
if (path.startsWith('/@id/')) {
// Virtual paths have no exact file match, so we don't set `file`
hasIdPrefix = true;
isVirtual = true;
id = path.slice('/@id/'.length);

// Add back leading slash if it was part of the virtual id.
// Example: `/@windicss/windi.css`
if (req.path.startsWith('/@id//')) {
id = '/' + id;
}
} else if (path.startsWith('/@npm/')) {
// Virtual paths have no exact file match, so we don't set `file`
id = path.slice('/@npm/'.length);
isVirtual = true;
} else if (path.startsWith('/@alias/')) {
id = posix.normalize(path.slice('/@alias/'.length));

Expand All @@ -279,7 +283,7 @@ export default function wmrMiddleware(options) {

if (path.startsWith('/@id/')) {
// Virtual paths have no exact file match, so we don't set `file`
hasIdPrefix = true;
isVirtual = true;
path = path.slice('/@id'.length);
}

Expand All @@ -296,7 +300,7 @@ export default function wmrMiddleware(options) {
// Normalize the cacheKey so it matches what will be in the WRITE_CACHE, where we store in native paths
cacheKey = cacheKey.split(posix.sep).join(sep);

if (!hasIdPrefix) {
if (!isVirtual) {
id = `./${id}`;
}

Expand Down Expand Up @@ -331,7 +335,7 @@ export default function wmrMiddleware(options) {
} else if (queryParams.has('asset')) {
cacheKey += '?asset';
transform = TRANSFORMS.asset;
} else if (prefix || hasIdPrefix || isModule || /\.([mc]js|[tj]sx?)$/.test(file) || STYLE_REG.test(file)) {
} else if (prefix || isVirtual || isModule || /\.([mc]js|[tj]sx?)$/.test(file) || STYLE_REG.test(file)) {
transform = TRANSFORMS.js;
} else if (file.startsWith(root + sep) && (await isFile(file))) {
// Ignore dotfiles
Expand Down Expand Up @@ -591,7 +595,7 @@ export const TRANSFORMS = {
spec = relative(root, spec).split(sep).join(posix.sep);
}
// Retain bare specifiers when serializing to url
else if (!/^\.?\.\//.test(spec)) {
else if (!/^\.?\.\//.test(spec) && prefix !== 'npm') {
spec = `@id/${spec}`;
}

Expand Down
Loading