diff --git a/.changeset/strong-meals-hammer.md b/.changeset/strong-meals-hammer.md new file mode 100644 index 000000000..ca5731ea2 --- /dev/null +++ b/.changeset/strong-meals-hammer.md @@ -0,0 +1,14 @@ +--- +'wmr': major +--- + +**tl;dr:** Auto-installation of npm packages is not enabled by default anymore and has to be opt-in to via `--autoInstall` on the CLI. + +The npm integration in WMR was rewritten from the ground up to support the following new features: + +- Reduce amount of requests by prebundling npm packages +- Resolve the `browser` field in `package.json` +- Resolve the `exports` field in `package.json` +- Improve CommonJS handling by attempting to convert it to ESM +- Ensure reproducible builds by moving auto installing of npm packages behind a CLI flag (`--autoInstall`) +- Allow specifying the url of the npm registry to fetch from when `--autoInstall` is active. This can be done via `--registry URL_TO_MY_REGISTRY` diff --git a/packages/wmr/package.json b/packages/wmr/package.json index f761b6e10..8be453cbf 100644 --- a/packages/wmr/package.json +++ b/packages/wmr/package.json @@ -95,7 +95,8 @@ "totalist": "^1.1.0", "tsconfig-paths": "^3.11.0", "utf-8-validate": "^5.0.2", - "ws": "^7.3.1" + "ws": "^7.3.1", + "zecorn": "^0.9.5" }, "optionalDependencies": { "fsevents": "^2.1.3" diff --git a/packages/wmr/src/build.js b/packages/wmr/src/build.js index 73e911a08..eae8b6f68 100644 --- a/packages/wmr/src/build.js +++ b/packages/wmr/src/build.js @@ -6,7 +6,6 @@ import { bundleProd } from './bundler.js'; import { bundleStats } from './lib/output-utils.js'; import { prerender } from './lib/prerender.js'; import { normalizeOptions } from './lib/normalize-options.js'; -import { setCwd } from './plugins/npm-plugin/registry.js'; /** * @param {Parameters[0] & { prerender?: boolean }} options @@ -14,9 +13,6 @@ import { setCwd } from './plugins/npm-plugin/registry.js'; export default async function build(options) { options.out = options.out || 'dist'; - // @todo remove this hack once registry.js is instantiable - setCwd(options.cwd); - options = await normalizeOptions(options, 'build'); // Clears out the output folder without deleting it -- useful diff --git a/packages/wmr/src/cli.js b/packages/wmr/src/cli.js index 6a1110426..3c6669dca 100644 --- a/packages/wmr/src/cli.js +++ b/packages/wmr/src/cli.js @@ -34,6 +34,8 @@ prog .option('--sourcemap', 'Enable Source Maps') .option('--visualize', 'Launch interactive bundle visualizer') .option('--minify', 'Enable minification of generated code (default: true)', true) + .option('--autoInstall', 'Fetch missing npm packages from npm registry automatically (default: false') + .option('--registry', 'NPM registry url to fetch if "--autoInstall" is set (default: https://registry.npmjs.org)') .action(opts => { opts.minify = bool(opts.minify); run(build(opts)); @@ -60,6 +62,8 @@ prog .option('--compress', 'Enable compression (default: enabled)') .option('--profile', 'Generate build statistics') .option('--reload', 'Switch off hmr and reload on file saves') + .option('--autoInstall', 'Fetch missing npm packages from npm registry automatically (default: false') + .option('--registry', 'NPM registry url to fetch if "--autoInstall" is set (default: https://registry.npmjs.org)') .action(opts => { opts.optimize = !/false|0/.test(opts.compress); opts.compress = bool(opts.compress); diff --git a/packages/wmr/src/lib/acorn-traverse.js b/packages/wmr/src/lib/acorn-traverse.js index 09598ed31..fa990f080 100644 --- a/packages/wmr/src/lib/acorn-traverse.js +++ b/packages/wmr/src/lib/acorn-traverse.js @@ -834,6 +834,8 @@ const TYPES = { importSpecifier: (local, imported) => ({ type: 'ImportSpecifier', local, imported }), importDefaultSpecifier: local => ({ type: 'ImportDefaultSpecifier', local }), importNamespaceSpecifier: local => ({ type: 'ImportNamespaceSpecifier', local }), + exportDefaultDeclaration: declaration => ({ type: 'ExportDefaultDeclaration', declaration }), + exportAllDeclaration: (source, exported = null) => ({ type: 'ExportAllDeclaration', source, exported }), assignmentExpression: (operator, left, right) => ({ type: 'AssignmentExpression', operator, left, right }), variableDeclaration: (kind, declarations) => ({ type: 'VariableDeclaration', kind, declarations }), variableDeclarator: (id, init) => ({ type: 'VariableDeclarator', id, init }), diff --git a/packages/wmr/src/lib/fs-utils.js b/packages/wmr/src/lib/fs-utils.js index bcf814ca8..e4fe1f9c0 100644 --- a/packages/wmr/src/lib/fs-utils.js +++ b/packages/wmr/src/lib/fs-utils.js @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import path from 'path'; /** * Implementation of fs.rm() for Node 12+ @@ -47,3 +48,27 @@ export function hasCustomPrefix(id) { export function pathToUrl(p) { return p.replace(/\\/g, '/'); } + +/** + * Read a file as JSON + * @param {string} file + */ +export async function readJson(file) { + const raw = await fs.readFile(file, 'utf-8'); + return JSON.parse(raw); +} + +/** + * Write file and create directories automatically if necessary + * @param {string} file + * @param {Buffer | string} data + * @param {BufferEncoding} [encoding] + */ +export async function writeFile(file, data, encoding) { + await fs.mkdir(path.dirname(file), { recursive: true }); + if (encoding) { + await fs.writeFile(file, data, encoding); + } else { + await fs.writeFile(file, data); + } +} diff --git a/packages/wmr/src/lib/normalize-options.js b/packages/wmr/src/lib/normalize-options.js index bbbe464ea..65657abf1 100644 --- a/packages/wmr/src/lib/normalize-options.js +++ b/packages/wmr/src/lib/normalize-options.js @@ -25,6 +25,7 @@ export async function normalizeOptions(options, mode, configWatchFiles = []) { options.features = { preact: true }; options.alias = options.alias || options.aliases || {}; options.customRoutes = options.customRoutes || []; + options.registry = options.registry || 'https://registry.npmjs.org'; // `wmr` / `wmr start` is a development command. // `wmr build` / `wmr serve` are production commands. diff --git a/packages/wmr/src/lib/npm-middleware.js b/packages/wmr/src/lib/npm-middleware.js index 8b5326366..02f780169 100644 --- a/packages/wmr/src/lib/npm-middleware.js +++ b/packages/wmr/src/lib/npm-middleware.js @@ -1,184 +1,36 @@ -import * as rollup from 'rollup'; -import commonjs from '@rollup/plugin-commonjs'; -import json from '@rollup/plugin-json'; -// import unpkgPlugin from '../plugins/unpkg-plugin.js'; -import npmPlugin, { normalizeSpecifier } from '../plugins/npm-plugin/index.js'; -import { resolvePackageVersion, loadPackageFile } from '../plugins/npm-plugin/registry.js'; -import { getCachedBundle, setCachedBundle, sendCachedBundle, enqueueCompress } from './npm-middleware-cache.js'; -import processGlobalPlugin from '../plugins/process-global-plugin.js'; -import aliasPlugin from '../plugins/aliases-plugin.js'; -import { getMimeType } from './mimetypes.js'; -import nodeBuiltinsPlugin from '../plugins/node-builtins-plugin.js'; -import * as kl from 'kolorist'; -import { hasDebugFlag, onWarn } from './output-utils.js'; +import path from 'path'; +import { getPackageInfo, isValidPackageName } from '../plugins/npm-plugin/utils.js'; -/** - * Serve a "proxy module" that uses the WMR runtime to load CSS. - * @param {ReturnType} meta - * @param {import('http').ServerResponse} res - * @param {boolean} [isModule] - */ -async function handleAsset(meta, res, isModule) { - let code = ''; - let type = null; - - if (isModule) { - type = 'application/javascript;charset=utf-8'; - const specifier = JSON.stringify('/@npm/' + meta.specifier + '?asset'); - code = `import{style}from '/_wmr.js';\nstyle(${specifier});`; - } else { - type = getMimeType(meta.path); - code = await loadPackageFile(meta); - } - res.writeHead(200, { - 'content-type': type || 'text/plain', - 'content-length': Buffer.byteLength(code) - }); - res.end(code); -} - -/** - * @param {object} [options] - * @param {'npm'|'unpkg'} [options.source = 'npm'] How to fetch package files - * @param {Record} [options.alias] - * @param {boolean} [options.optimize = true] Progressively minify and compress dependency bundles? - * @param {string} [options.cwd] Virtual cwd - * @returns {import('polka').Middleware} - */ -export default function npmMiddleware({ source = 'npm', alias, optimize, cwd } = {}) { +export function npmEtagCache() { return async (req, res, next) => { const url = new URL(req.url, 'https://localhost'); - // @ts-ignore - const mod = url.pathname.replace(/^\//, ''); + let id = path.posix.normalize(url.pathname); - const meta = normalizeSpecifier(mod); + if (!id.startsWith('/@npm/')) { + return next(); + } - try { - await resolvePackageVersion(meta); - } catch (e) { - return next(e); + id = id.slice('/@npm/'.length); + if (!isValidPackageName(id)) { + return next(); } + const { name, version, pathname } = getPackageInfo(id); + try { - // The package name + path + version is a strong ETag since versions are immutable - const etag = Buffer.from(`${meta.specifier}${meta.version}`).toString('base64'); + // The package name + version + pathname is a strong ETag since versions are immutablew + const etag = Buffer.from(`${name}${version}${pathname}`).toString('base64'); const ifNoneMatch = String(req.headers['if-none-match']).replace(/-(gz|br)$/g, ''); + if (ifNoneMatch === etag) { return res.writeHead(304).end(); } - res.setHeader('etag', etag); - - // CSS files and proxy modules don't use Rollup. - if (/\.((css|s[ac]ss|less)|wasm|txt|json)$/.test(meta.path)) { - return handleAsset(meta, res, url.searchParams.has('module')); - } - - res.setHeader('content-type', 'application/javascript;charset=utf-8'); - if (hasDebugFlag()) { - // eslint-disable-next-line no-console - console.log(` ${kl.dim('middleware:') + kl.bold(kl.magenta('npm'))} ${JSON.stringify(meta.specifier)}`); - } - // serve from memory and disk caches: - const cached = await getCachedBundle(etag, meta, cwd); - if (cached) return sendCachedBundle(req, res, cached); - - // const start = Date.now(); - const code = await bundleNpmModule(mod, { source, alias, cwd }); - // console.log(`Bundle dep: ${mod}: ${Date.now() - start}ms`); - - // send it! - res.writeHead(200, { 'content-length': Buffer.byteLength(code) }).end(code); - // store the bundle in memory and disk caches - setCachedBundle(etag, code, meta, cwd); - - // this is a new bundle, we'll compress it with terser and brotli shortly - if (optimize !== false) { - enqueueCompress(etag); - } - } catch (e) { - console.error(`Error bundling ${mod}: `, e); - next(e); + res.setHeader('etag', etag); + } catch (err) { + next(err); } - }; -} - -let npmCache; - -/** - * Bundle am npm module entry path into a single file - * @param {string} mod The module to bundle, including subpackage/path - * @param {object} options - * @param {'npm'|'unpkg'} [options.source] - * @param {Record} [options.alias] - * @param {string} [options.cwd] - */ -async function bundleNpmModule(mod, { source, alias, cwd }) { - let npmProviderPlugin; - if (source === 'unpkg') { - throw Error('unpkg plugin is disabled'); - // npmProviderPlugin = unpkgPlugin({ - // publicPath: '/@npm', - // perPackage: true - // }); - } else { - npmProviderPlugin = npmPlugin({ - publicPath: '/@npm' - }); - } - - const bundle = await rollup.rollup({ - input: mod, - onwarn: onWarn, - // input: '\0entry', - cache: npmCache, - shimMissingExports: true, - treeshake: false, - // inlineDynamicImports: true, - // shimMissingExports: true, - preserveEntrySignatures: 'allow-extension', - plugins: [ - nodeBuiltinsPlugin({}), - aliasPlugin({ alias, cwd }), - npmProviderPlugin, - processGlobalPlugin({ - sourcemap: false, - NODE_ENV: 'development' - }), - commonjs({ - extensions: ['.js', '.cjs', ''], - sourceMap: false, - transformMixedEsModules: true - }), - json(), - { - name: 'no-builtins', - load(s) { - if (s === 'fs' || s === 'path') { - return 'export default {};'; - } - } - }, - { - name: 'never-disk', - load(s) { - throw Error('local access not allowed'); - } - } - ] - }); - - npmCache = bundle.cache; - - const { output } = await bundle.generate({ - format: 'es', - indent: false, - // entryFileNames: '[name].js', - // chunkFileNames: '[name].js', - // Don't transform paths at all: - paths: String - }); - - return output[0].code; + next(); + }; } diff --git a/packages/wmr/src/lib/plugins.js b/packages/wmr/src/lib/plugins.js index 89c43e110..6714a30f4 100644 --- a/packages/wmr/src/lib/plugins.js +++ b/packages/wmr/src/lib/plugins.js @@ -1,9 +1,9 @@ +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'; import wmrStylesPlugin from '../plugins/wmr/styles/styles-plugin.js'; import sassPlugin from '../plugins/sass-plugin.js'; -import npmPlugin from '../plugins/npm-plugin/index.js'; import publicPathPlugin from '../plugins/public-path-plugin.js'; import minifyCssPlugin from '../plugins/minify-css-plugin.js'; import htmlEntriesPlugin from '../plugins/html-entries-plugin.js'; @@ -27,7 +27,9 @@ import { prefreshPlugin } from '../plugins/preact/prefresh.js'; import { absolutePathPlugin } from '../plugins/absolute-path-plugin.js'; 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 @@ -38,6 +40,7 @@ export function getPlugins(options) { plugins, publicPath, alias, + cwd, root, env, minify, @@ -45,9 +48,19 @@ export function getPlugins(options) { isIIFEWorker = false, sourcemap, features, - visualize + visualize, + autoInstall, + registry } = options; + const npmCacheDir = path.join(cwd, '.cache', '@npm'); + + /** + * Map of package name to folder on disk + * @type {Map} + */ + const resolutionCache = new Map(); + // Plugins are pre-sorted let split = plugins.findIndex(p => p.enforce === 'post'); if (split === -1) split = plugins.length; @@ -99,7 +112,20 @@ export function getPlugins(options) { // Only transpile CommonJS in node_modules and explicit .cjs files: include: /(^npm\/|[/\\]node_modules[/\\]|\.cjs$)/ }), - (production || isIIFEWorker) && npmPlugin({ external: false }), + + ...(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 diff --git a/packages/wmr/src/plugins/npm-plugin/browser-field.js b/packages/wmr/src/plugins/npm-plugin/browser-field.js new file mode 100644 index 000000000..2f1d518ba --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/browser-field.js @@ -0,0 +1,24 @@ +import path from 'path'; +import { isValidPackageName } from './utils.js'; + +/** + * @param {object} options + * @param {Map} options.browserReplacement + * @returns {import('rollup').Plugin} + */ +export function browserFieldPlugin({ browserReplacement }) { + return { + name: 'browser-field', + async resolveId(id, importer) { + let spec = path.posix.normalize(id); + const replace = browserReplacement.get(spec); + if (replace) { + if (importer && isValidPackageName(importer)) { + const info = this.getModuleInfo(importer); + importer = path.join(info?.meta.wmr.modDir, importer); + } + return this.resolve(replace, importer, { skipSelf: true }); + } + } + }; +} diff --git a/packages/wmr/src/plugins/npm-plugin/commonjs.js b/packages/wmr/src/plugins/npm-plugin/commonjs.js new file mode 100644 index 000000000..4715999c7 --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/commonjs.js @@ -0,0 +1,47 @@ +import { transform, replace, optimize, commonjsToEsm } from 'zecorn'; + +const CJS_KEYWORDS = /\b(module\.exports|exports|require)\b/; + +export const ESM_KEYWORDS = + /(\bimport\s*(\{.*?\}\s*from|\s[\w$]+\s+from)?\s*['"]|[\s;]export(\s+(default|const|var|let|function|class)[^\w$]|\s*\{))/; + +/** + * Attempt to convert CommonJS modules to ESM + * @param {object} options + * @param {boolean} options.production + * @returns {import('rollup').Plugin} + */ +export function commonjsPlugin({ production }) { + return { + name: 'commonjs', + async transform(code, id) { + const hasCjsKeywords = CJS_KEYWORDS.test(code); + const hasEsmKeywords = ESM_KEYWORDS.test(code); + + if (!hasCjsKeywords && hasEsmKeywords) return; + + let result; + try { + result = transform(code, { + parse: this.parse, + plugins: [ + replace({ 'process.env.NODE_ENV': 'development', __DEV__: !!production }), + optimize(), + commonjsToEsm() + ] + }); + + 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; + } + } + }; +} diff --git a/packages/wmr/src/plugins/npm-plugin/index.js b/packages/wmr/src/plugins/npm-plugin/index.js index b6c1d7265..3c666fe80 100644 --- a/packages/wmr/src/plugins/npm-plugin/index.js +++ b/packages/wmr/src/plugins/npm-plugin/index.js @@ -1,167 +1,177 @@ -import { posix, sep } from 'path'; +import { promises as fs } from 'fs'; +import path from 'path'; import * as kl from 'kolorist'; -import { memo } from './utils.js'; -import { resolvePackageVersion, loadPackageFile, getPackageVersionFromDeps } from './registry.js'; -import { resolveModule } from './resolve.js'; -import { debug, formatPath } from '../../lib/output-utils.js'; +import { isFile, writeFile } from '../../lib/fs-utils.js'; +import { debug } from '../../lib/output-utils.js'; +import { npmBundle } from './npm-bundle.js'; +import { Deferred, escapeFilename, findInstalledPackage, getPackageInfo, isValidPackageName } from './utils.js'; + +const log = debug('npm', 196); /** - * @param {Object} options - * @param {string} [options.publicPath] URL path prefix to use for npm module scripts - * @param {string} [options.prefix] Import prefix to use internally for representing npm modules - * @param {boolean} [options.external] If `false`, resolved npm dependencies will be inlined by Rollup. + * @param {object} options + * @param {string} options.cwd + * @param {boolean} options.autoInstall + * @param {boolean} options.production + * @param {string} options.registryUrl + * @param {string} options.cacheDir + * @param {Record} options.alias + * @param {Map} options.resolutionCache * @returns {import('rollup').Plugin} */ -export default function npmPlugin({ publicPath = '/@npm', prefix = 'npm/', external = true } = {}) { - const log = debug('npm:plugin'); - - return { - name: 'npm-plugin', - async resolveId(id, importer) { - if (id[0] === '\0' || /^[\w-]+:/.test(id)) return; - - if (importer) { - if (importer[0] === '\0') importer = ''; +export function npmPlugin({ cwd, cacheDir, autoInstall, production, registryUrl, resolutionCache, alias }) { + const PREFIX = '\0npm:'; + + /** @type {Map} */ + const chunkCache = new Map(); + + /** @type {Map} */ + const entryToChunk = new Map(); + + /** @type {Map} */ + const pending = new Map(); + + const assetDeferredId = '::asset'; + + /** + * Bundle an npm package and update build caches + * @param {string} id + * @param {object} options + * @param {string} options.packageName + * @param {string} options.diskCacheDir + * @param {Record} options.alias + * @param {Map} options.resolutionCache + * @returns {Promise<{ code: string, map: any }>} + */ + async function bundleNpmPackage(id, { packageName, diskCacheDir, resolutionCache, alias }) { + const deferred = new Deferred(); + pending.set(id, deferred); + + // Also add package name itself so that assets can wait on it + let assetDeferred; + if (id !== packageName) { + assetDeferred = new Deferred(); + pending.set(packageName + assetDeferredId, assetDeferred); + } - // replace windows paths - importer = importer - .replace(/^[A-Z]:/, '') - .split(sep) - .join('/'); - } + log(kl.dim(`bundle: `) + kl.cyan(id)); + let result = await npmBundle(id, { autoInstall, production, cacheDir, cwd, resolutionCache, registryUrl, alias }); - if (id.startsWith(publicPath)) return { id, external }; + await Promise.all( + result.output.map(async chunkOrAsset => { + if (chunkOrAsset.type === 'chunk') { + const { isEntry, fileName, code, map } = chunkOrAsset; + if (isEntry) { + entryToChunk.set(id, fileName); + } - if (id.startsWith(prefix)) id = id.substring(prefix.length); - else if (/^(?:\0|[a-z]+:|\/)/.test(id)) return; + const hasExt = path.extname(fileName); + const diskCachePath = path.join(diskCacheDir, hasExt ? fileName : fileName + '.js'); + await writeFile(diskCachePath, code); - if (importer && importer.startsWith(prefix)) importer = importer.substring(prefix.length); + chunkCache.set(fileName, { code, map: map || null }); + } + }) + ); - // let module, path, version; - /** @type {ReturnType} */ - let meta; + const entryReq = entryToChunk.get(id); + let chunkId = entryReq ? entryReq : id; - const importerMeta = importer && !isDiskPath(importer) && normalizeSpecifier(importer); + const chunk = chunkCache.get(chunkId); + if (!chunk) { + throw new Error(`Compiled chunk for package "${chunkId}" not found.`); + } - let isExternal = false; - let isEntry = false; + deferred.resolve(chunk); + if (assetDeferred) { + assetDeferred.resolve(chunk); + } - // A relative import from within a module (resolve based on importer): - if (isDiskPath(id)) { - // not an npm module - if (!importerMeta) return; + return chunk; + } - meta = Object.assign({}, importerMeta); - meta.path = posix.join(posix.dirname(meta.path || ''), id); - } else { - // An absolute, self or bare import - meta = normalizeSpecifier(id); + return { + name: 'npm-plugin', + async resolveId(id) { + if (!isValidPackageName(id)) return; + + // Assets require special handling as other plugins need to deal with + // non-js files during their `load()` method. To work around this + // limitation in rollup's plugin system we'll pretend that we'd requested + // the bare package instead of the asset and bundle that during the + // resolution step. When that's done we can ensure that the file + // exists on disk and can resolve to that. That way the "style" plugin + // can load it like usual in their `load()` method. + const { name, pathname } = getPackageInfo(id); + const isAsset = pathname && !/\.[tj]sx?$/.test(path.basename(pathname)) && path.extname(pathname) !== ''; + + if (isAsset) { + let deferred = pending.get(name + assetDeferredId); + 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, alias }); + } else { + await deferred; + } - // not imported by an npm module, or imported by a _different_ module - if (!importerMeta || meta.specifier !== importerMeta.specifier) { - isEntry = true; + // Check if the package is local + const modDir = resolutionCache.get(name) || (await findInstalledPackage(cwd, name)); + if (modDir) { + const resolved = path.join(modDir, pathname); + log(kl.dim(`asset found locally at `) + kl.cyan(resolved)); + return resolved; } - if (external && importerMeta && meta.specifier !== importerMeta.specifier) { - isExternal = true; + // Check bundle cache in case the package was auto-installed + const cachePath = path.join(cacheDir, pathname); + if (await isFile(cachePath)) { + return cachePath; } - } - // use package.json version range from importer: - if (!meta.version && importerMeta) { - try { - const importerPkg = JSON.parse( - await loadPackageFile({ - module: importerMeta.module, - version: importerMeta.version, - path: 'package.json' - }) - ); - const contextualVersion = getPackageVersionFromDeps(importerPkg, meta.module); - if (contextualVersion) { - meta.version = contextualVersion; - } - } catch (e) {} + throw new Error(`Could not resolve asset ${id}`); } - meta.version = meta.version || ''; - - // Resolve @latest --> @10.4.1 - await resolvePackageVersion(meta); - - // Versions that resolve to the root are removed - // (see "Option 3" in wmr-middleware.jsL247) - let emitVersion = true; - if ((await resolvePackageVersion({ module: meta.module, version: '' })).version === meta.version) { - emitVersion = false; - // meta.version = ''; + return PREFIX + id; + }, + async load(id) { + if (!id.startsWith(PREFIX)) return; + id = id.slice(PREFIX.length); + + // Return from cache if possible + const cached = chunkCache.get(id); + if (cached) { + log(kl.dim(`load: `) + kl.cyan(id) + ` [memory]`); + return cached; } - // Mark everything except self-imports as external: (eg: "preact/hooks" importing "preact") - // Note: if `external=false` here, we're building a combined bundle and want to merge npm deps. - if (isExternal) { - const versionTag = emitVersion && meta.version ? '@' + meta.version : ''; - id = `${meta.module}${versionTag}${meta.path ? '/' + meta.path : ''}`; - - return { id: `${publicPath}/${id}`, external: 'absolute' }; + // Check disk cache next + const meta = getPackageInfo(id); + const diskCacheDir = path.join(cacheDir, escapeFilename(meta.name)); + const basename = meta.pathname + ? path.extname(meta.pathname) + ? meta.pathname + : meta.pathname + '.js' + : path.basename(id) + '.js'; + + const diskPath = path.join(diskCacheDir, basename); + if (await isFile(diskPath)) { + log(kl.dim(`load: `) + kl.cyan(id) + ` [disk]`); + return await fs.readFile(diskPath, 'utf-8'); } - // Compute the final path - const { module, version, path } = meta; - const readFile = (path = '') => loadPackageFile({ module, version, path }); - const hasFile = path => - readFile(path) - .then(() => true) - .catch(() => false); - - // If external=true, we're bundling a single module, so "no importer" means an entry - // If external=false, we're bundling a whole app, so "different importer" means an entry - const isInternalImport = external ? !!importer : !isEntry; - let resolvedPath = await resolveModule(path, { - readFile, - hasFile, - module, - internal: isInternalImport - }); - resolvedPath = resolvedPath.replace(/^\//, ''); - - let res; - // CSS files are not handled by this plugin. - if (/\.css$/.test(id) && (await hasFile(resolvedPath))) { - res = `./node_modules/${meta.module}/${resolvedPath}`; - log(`${kl.cyan(formatPath(id))} -> ${kl.dim(formatPath(res))}`); - return res; + // Prevent duplicate bundling requeusts + let deferred = pending.get(id); + if (deferred) { + return deferred.promise; } - res = `${prefix}${meta.module}${meta.version ? '@' + meta.version : ''}/${resolvedPath}`; - log(`${kl.cyan(formatPath(id))} -> ${kl.dim(formatPath(res))}`); - return res; - }, - load(id) { - // only load modules this plugin resolved - if (!id.startsWith(prefix)) return; - id = id.substring(prefix.length); + const chunk = await bundleNpmPackage(id, { packageName: meta.name, diskCacheDir, resolutionCache }); - const spec = normalizeSpecifier(id); - return loadPackageFile(spec); + return { + code: chunk.code, + map: chunk.map + }; } }; } - -const PACKAGE_SPECIFIER = /^((?:@[\w.-]{1,200}\/)?[\w.-]{1,200})(?:@([a-z0-9^.~>=<-]{1,50}))?(?:\/(.*))?$/i; - -export const normalizeSpecifier = memo(spec => { - let [, module = '', version = '', path = ''] = spec.match(PACKAGE_SPECIFIER) || []; - if (!module) throw Error(`Invalid specifier: ${spec}`); - version = (version || '').toLowerCase(); - module = module.toLowerCase(); - const specifier = module + (path ? '/' + path : ''); - return { module, version, path, specifier }; -}); - -/** @param {string} filename */ -function isDiskPath(filename) { - // only check for windows paths if we're on windows - if (sep === '\\' && /^(([A-Z]+:)?\\|\.\.?(\\|$))/.test(filename)) return true; - return /^(file:\/\/)?([A-Z]:)?(\/|\.\.?(\/|$))/.test(filename); -} diff --git a/packages/wmr/src/plugins/npm-plugin/npm-auto-install.js b/packages/wmr/src/plugins/npm-plugin/npm-auto-install.js new file mode 100644 index 000000000..725f60958 --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/npm-auto-install.js @@ -0,0 +1,217 @@ +import fs from 'fs'; +import https from 'https'; +import fetch from 'node-fetch'; +import path from 'path'; +import tar from 'tar-stream'; +import zlib from 'zlib'; +import * as kl from 'kolorist'; +import { isDirectory, writeFile } from '../../lib/fs-utils.js'; +import { debug } from '../../lib/output-utils.js'; +import { + Deferred, + escapeFilename, + getPackageInfo, + isValidPackageName, + friendlyNetworkError, + streamToString +} from './utils.js'; + +const log = debug('npm-auto-install'); + +/** + * Fetch package from npm registry + * @param {string} url + */ +async function fetchNpmPkgInfo(url) { + try { + return fetch(url, { + headers: { + accept: 'application/vnd.npm.install-v1+json' + } + }).then(r => r.json()); + } catch (err) { + if (err.code === 'ENOTFOUND') { + } + throw friendlyNetworkError(err, `npm registry lookup failed for "${url}"`); + } +} + +/** + * Stream a file to disk + * @param {string} url + * @param {string} filePath + * @returns + */ +async function streamToDisk(url, filePath) { + await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); + + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + file.on('finish', () => { + file.close(); + resolve(null); + }); + + https + .get(url, res => res.pipe(file)) + .on('error', err => { + // Delete file if there was an error + fs.unlink(filePath, () => null); + reject(err); + }); + }); +} + +/** + * @param {import('stream').Readable} bodyStream + * @param {(name: string, stream: import('stream').PassThrough) => Promise} onFile + * @param {{ include?: RegExp, exclude?: RegExp}} options + */ +async function parseTarball(bodyStream, onFile, { include, exclude }) { + return new Promise((resolve, reject) => { + const extract = tar.extract(); + + extract.on('entry', (header, stream, next) => { + let { type, name } = header; + name = name.replace(/^package\//, ''); + + if (type !== 'file' || (include && !include.test(name)) || (exclude && exclude.test(name))) { + stream.resume(); + return next(); + } + + onFile(name, stream).then(next); + }); + + extract.on('finish', resolve); + extract.on('error', reject); + + bodyStream.pipe(zlib.createGunzip()).pipe(extract); + }); +} + +/** + * Automatically fetch missing npm packages from specified npm + * registry. Note that this should only ever be enabled for + * prototyping. + * @param {object} options + * @param {string} options.cacheDir + * @param {string} options.registryUrl + * @returns {import('rollup').Plugin} + */ +export function npmAutoInstall({ cacheDir, registryUrl }) { + /** Files that should always be ignored when storing packages */ + const FILES_EXCLUDE = /([._-]test\.|__tests?|\/tests?\/|\/node_modules\/|\.d\.ts$)/i; + + const USELESS_PROPERTIES = [ + 'jest', + 'eslintConfig', + 'eslintIgnore', + 'prettier', + 'babel', + 'scripts', + 'devDependencies', + 'peerDependencies', + 'files', + 'keywords', + 'husky', + 'lint-staged' + ]; + + /** @type {Map} */ + const pending = new Map(); + + return { + name: 'npm-auto-install', + async resolveId(id) { + if (!isValidPackageName(id)) return; + + const meta = getPackageInfo(id); + const deferredKey = `${meta.name}@${meta.version || 'latest'}`; + + // Subscribe to existing resolve hook if a request for the + // same package is already in progress; + let deferred = pending.get(deferredKey); + if (deferred) { + log(`waiting on... ${id}`); + return deferred.promise.then(r => ({ ...r, id })); + } + + deferred = new Deferred(); + pending.set(deferredKey, deferred); + + const downloadDir = path.join(cacheDir, '_download'); + + try { + const pkg = await fetchNpmPkgInfo(`${registryUrl}/${meta.name}`); + + let version = meta.version || 'latest'; + // Resolve npm tags to an actual version + const distTags = pkg['dist-tags']; + version = distTags[version] || version; + + let info = pkg.versions[version]; + const { tarball } = info.dist; + + const safeName = escapeFilename(meta.name); + const extractPath = path.join(downloadDir, `${safeName}@${version}`); + + // Check if the extracted package is already present from a + // previous run. + if (await isDirectory(extractPath)) { + log(kl.dim(`resolved from cache: `) + kl.cyan(extractPath)); + } else { + log(kl.dim(`downloading... `) + kl.cyan(id)); + // Download tarball to disk + const tarPath = path.join(downloadDir, `${safeName}-${version}.tgz`); + await streamToDisk(tarball, tarPath); + + // TODO: Check tarball integrity? + + // Extract tar file + log(kl.dim(`extracting... `) + kl.cyan(tarPath)); + + await parseTarball( + fs.createReadStream(tarPath), + async (name, stream) => { + // TODO: Support binary formats + let data = await streamToString(stream); + + if (name.endsWith('package.json')) { + try { + const json = JSON.parse(data); + for (const prop of USELESS_PROPERTIES) { + if (prop in json) { + delete json[prop]; + } + } + data = JSON.stringify(json, null, 2); + } catch (err) { + console.warn(`Invalid package.json`); + } + } + + await writeFile(path.join(extractPath, name), data); + }, + { exclude: FILES_EXCLUDE } + ); + + // Delete tarball after extraction was successful + await fs.promises.unlink(tarPath); + } + + const out = { + id, + meta: { wmr: { modDir: extractPath } } + }; + + log(`resolving... ${meta.name}, request: ${id}`); + deferred.resolve(out); + return out; + } catch (err) { + deferred.reject(err); + throw err; + } + } + }; +} diff --git a/packages/wmr/src/plugins/npm-plugin/npm-bundle.js b/packages/wmr/src/plugins/npm-plugin/npm-bundle.js new file mode 100644 index 000000000..94faa16ab --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/npm-bundle.js @@ -0,0 +1,110 @@ +import * as rollup from 'rollup'; +import { builtinModules } from 'module'; +import { browserFieldPlugin } from './browser-field.js'; +import { commonjsPlugin } from './commonjs.js'; +import { subPackageLegacy } from './sub-package-legacy.js'; +import { npmExternalDeps } from './npm-external-deps.js'; +import { npmLocalPackage } from './npm-local-package.js'; +import { npmLoad } from './npm-load.js'; +import { getPackageInfo } from './utils.js'; +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) { + // Ignore empty bundle warning which happens for CSS-only npm packages. + if (typeof warning === 'object' && warning.code === 'EMPTY_BUNDLE') { + return; + } + + 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} options.resolutionCache + * @param {Map} 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 + * @param {boolean} options.autoInstall + * @param {boolean} options.production + * @param {string} options.cacheDir + * @param {string} options.cwd + * @param {string} options.registryUrl + * @param {Record} options.alias + * @param {Map} options.resolutionCache + */ +export async function npmBundle( + requestId, + { autoInstall, production, cacheDir, cwd, resolutionCache, registryUrl, alias } +) { + const meta = getPackageInfo(requestId); + const pkgName = meta.name; + + /** @type {Map} */ + const browserReplacement = new Map(); + + console.log('REQUEST', requestId); + + const bundle = await rollup.rollup({ + input: requestId, + external: [...builtinModules], + onwarn: customWarn, + plugins: [ + aliasPlugin({ alias }), + jsonPlugin({ root: cwd }), + ...getNpmPlugins({ + requestId, + autoInstall, + production, + cacheDir, + cwd, + resolutionCache, + registryUrl, + browserReplacement + }) + ] + }); + + const result = await bundle.generate({ + chunkFileNames: `${pkgName}-[hash]`, + format: 'esm' + }); + + return result; +} diff --git a/packages/wmr/src/plugins/npm-plugin/npm-external-deps.js b/packages/wmr/src/plugins/npm-plugin/npm-external-deps.js new file mode 100644 index 000000000..315d4e915 --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/npm-external-deps.js @@ -0,0 +1,23 @@ +import { isValidPackageName } from './utils.js'; + +/** + * Detect if an id is an npm package and mark it as external + * @param {object} options + * @param {string} options.requestId + * @returns {import('rollup').Plugin} + */ +export function npmExternalDeps({ requestId }) { + return { + name: 'npm-detect', + async resolveId(id) { + if (!isValidPackageName(id)) return; + + if (id !== requestId) { + return { + id, + external: true + }; + } + } + }; +} diff --git a/packages/wmr/src/plugins/npm-plugin/npm-load.js b/packages/wmr/src/plugins/npm-plugin/npm-load.js new file mode 100644 index 000000000..508f92cce --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/npm-load.js @@ -0,0 +1,129 @@ +import path from 'path'; +import { promises as fs } from 'fs'; +import * as kl from 'kolorist'; +import { getPackageInfo, isValidPackageName, resolvePackageExport } from './utils.js'; +import { readJson } from '../../lib/fs-utils.js'; +import { debug } from '../../lib/output-utils.js'; + +const log = debug('npm-load'); + +/** + * @param {object} options + * @param {Map} options.browserReplacement + * @param {Map} options.resolutionCache + * @param {boolean} options.production + * @returns {import('rollup').Plugin} + */ +export function npmLoad({ browserReplacement, resolutionCache, production }) { + return { + name: 'npm-load', + async resolveId(id, importer) { + if (importer && isValidPackageName(importer)) { + const info = this.getModuleInfo(importer); + return this.resolve(id, info?.meta.wmr.entry, { skipSelf: true }); + } + }, + async load(id) { + if (!isValidPackageName(id)) return; + + const info = this.getModuleInfo(id); + + const { modDir } = info?.meta?.wmr; + const pkg = await readJson(path.join(modDir, 'package.json')); + const { pathname, name } = getPackageInfo(id); + + if (typeof pkg.browser === 'object') { + for (let [spec, replacement] of Object.entries(pkg.browser)) { + // Formats: + // "foo" -> Can be `foo` or `/foo.js` + // "foo/bar" -> Can be `foo/bar` or `/foo/bar.js` + // "./foo/bar" -> `/foo/bar.js` + // "../foo" -> INVALID + // "." -> INVALID + spec = path.posix.normalize(spec); + const idPrefix = replacement.startsWith('./') ? './' : ''; + replacement = idPrefix + path.posix.normalize(replacement); + + // Check for invalid paths + if (spec.startsWith('..') || spec === '.' || replacement.startsWith('..') || replacement === '.') { + continue; + } + + // Add bare entry as is, in case it refers to a package + if (!spec.startsWith('./')) { + browserReplacement.set(spec, replacement); + } + + browserReplacement.set(path.join(modDir, spec), replacement); + } + } + + let entry = ''; + // Package exports + if (pkg.exports) { + const found = resolvePackageExport(pkg, pathname); + if (!found) { + throw new Error(`Unable to resolve entry in module "${pkg.name}"`); + } + + entry = path.join(modDir, found); + } else if (!pathname) { + entry = path.join(modDir, pkg.module || pkg.main || 'index.js'); + } else { + // Special case: Deep import may itself be a replaced path + const replaced = browserReplacement.get(pathname); + if (replaced) { + const resolved = await this.resolve(`./${replaced}`, path.join(modDir, pkg.name), { skipSelf: true }); + + entry = resolved ? resolved.id : path.join(modDir, replaced); + } else { + // Check if the package is a legacy sub-package. This + // was used before the "export" field became a thing. + try { + const subPkg = await readJson(path.join(modDir, pathname, 'package.json')); + entry = path.join(modDir, pathname, subPkg.module || subPkg.main || 'index.js'); + } catch (err) { + entry = path.join(modDir, pathname); + } + } + } + + log(`loaded ${kl.cyan(id)} ${kl.dim(`from ${entry}`)}`); + + resolutionCache.set(id, modDir); + + // Some packages use non-js entry files, but rollup only supports js. + // So we expect other plugins to handle assets. + if (!production && !/\.(?:[tj]sx?|[cm]js|[mc]ts)/.test(path.extname(entry))) { + return { + code: '', + map: null, + moduleSideEffect: false, + meta: { + wmr: { + entry, + modName: name, + modDir + } + } + }; + } + + const code = await fs.readFile(entry, 'utf-8'); + + return { + code, + // FIXME: Load existing sourcemap if any + map: null, + moduleSideEffect: false, + meta: { + wmr: { + entry, + modName: name, + modDir + } + } + }; + } + }; +} diff --git a/packages/wmr/src/plugins/npm-plugin/npm-local-package.js b/packages/wmr/src/plugins/npm-plugin/npm-local-package.js new file mode 100644 index 000000000..b3fdd0a42 --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/npm-local-package.js @@ -0,0 +1,26 @@ +import { findInstalledPackage, getPackageInfo, isValidPackageName } from './utils.js'; + +/** + * Resolve an npm package from disk + * @param {object} options + * @param {string} options.root + * @returns {import('rollup').Plugin} + */ +export function npmLocalPackage({ root }) { + return { + name: 'npm-local', + async resolveId(id) { + if (!isValidPackageName(id)) return; + + const info = getPackageInfo(id); + + const modDir = await findInstalledPackage(root, info.name); + if (modDir) { + return { + id, + meta: { wmr: { modDir } } + }; + } + } + }; +} diff --git a/packages/wmr/src/plugins/npm-plugin/package-strip-plugin.js b/packages/wmr/src/plugins/npm-plugin/package-strip-plugin.js deleted file mode 100644 index 409d2682b..000000000 --- a/packages/wmr/src/plugins/npm-plugin/package-strip-plugin.js +++ /dev/null @@ -1,40 +0,0 @@ -const USELESS_PROPERTIES = [ - 'jest', - 'eslintConfig', - 'eslintIgnore', - 'prettier', - 'babel', - 'scripts', - 'devDependencies', - 'peerDependencies', - // 'repository', // tbh this is useful - 'files', - 'keywords', - 'husky', - 'lint-staged' -]; - -/** - * Remove pointless properties from package.json files before writing to disk - * @returns {import("./registry").Plugin} - */ -export default function stripPackageProperties() { - return { - name: 'strip-package-properties', - transform(contents, filename) { - if (!/(^|\/)package\.json$/.test(filename)) return; - let pkg; - try { - pkg = JSON.parse(contents); - } catch (e) { - console.warn(`Invalid package.json`); - } - for (const prop of USELESS_PROPERTIES) { - if (prop in pkg) { - delete pkg[prop]; - } - } - return JSON.stringify(pkg, null, 2); - } - }; -} diff --git a/packages/wmr/src/plugins/npm-plugin/registry.js b/packages/wmr/src/plugins/npm-plugin/registry.js deleted file mode 100644 index 5aa165949..000000000 --- a/packages/wmr/src/plugins/npm-plugin/registry.js +++ /dev/null @@ -1,407 +0,0 @@ -import { resolve, dirname } from 'path'; -import { promises as fs } from 'fs'; -import tar from 'tar-stream'; -import zlib from 'zlib'; -import semverMaxSatisfying from 'semver/ranges/max-satisfying.js'; -import { getJson, getStream, memo, streamToString, friendlyNetworkError } from './utils.js'; -import stripPackageJsonProperties from './package-strip-plugin.js'; -import sizeWarningPlugin from './size-warning-plugin.js'; - -// @TODO: this whole module should be instantiable - -/** - * @typedef Meta - * @type {{ name: string, versions: Record, modified: string, 'dist-tags': Record }} - */ - -/** - * @typedef Package - * @type {{ name: string, version: string, dependencies: [], devDependencies: [], dist: { tarball: string, integrity, shasum, fileCount, unpackedSize } }} - */ - -/** - * @typedef Module - * @type {{ module: string, version: string, path?: string }} - */ - -/** - * @typedef PackageJson - * @type {{ dependencies?:Record, devDependencies?: Record, peerDependencies?: Record, resolutions?: Record }} - */ - -/** Files that should be included when storing packages */ -const FILES_INCLUDE = /\.(js|mjs|cjs|json|tsx?|css|wasm)$/i; - -/** Files that should always be ignored when storing packages */ -const FILES_EXCLUDE = /([._-]test\.|__tests?|\/tests?\/|\/node_modules\/)/i; - -let NODE_MODULES = './node_modules'; - -/** @todo this is terrible and should be removed once this module is instantiable */ -export function setCwd(cwd) { - NODE_MODULES = resolve(cwd || '.', './node_modules'); -} - -/** - * @typedef Plugin - * @type {{ name?: string, transform?(contents: string, filename: string): string|void }} - */ - -/** @type {Array} */ -const plugins = [stripPackageJsonProperties(), sizeWarningPlugin()]; - -/** The registry to fetch package metadata & tarballs from */ -const API = 'https://registry.npmjs.org'; - -/** How long to cache npm dist-tag version lookups before requerying the registry */ -const DIST_TAG_TTL = 60000; - -/** @type {Map} */ -const DIST_TAG_CACHE = new Map(); - -async function readPackageJson(filename) { - try { - return JSON.parse(await fs.readFile(filename, 'utf-8')); - } catch (e) {} -} - -/** @type {PackageJson} */ -let appPackageJson; - -/** @param {PackageJson} pkg */ -export function getPackageVersionFromDeps(pkg, name) { - return ( - (pkg.dependencies && pkg.dependencies[name]) || - (pkg.devDependencies && pkg.devDependencies[name]) || - (pkg.peerDependencies && pkg.peerDependencies[name]) - ); -} - -function getPackageVersionFromResolutions(pkg, name) { - if (pkg.resolutions) { - for (const pattern in pkg.resolutions) { - const escaped = pattern - .replace(/([.\\^$[]{}()?!])/g, '$1') - .replace(/\*\*/g, '.+') - .replace(/\*/g, '[^/]+'); - const reg = new RegExp('^' + escaped + '$', 'gi'); - if (reg.test(name)) { - // console.log(`using resolution: ${pattern} (${escaped})`); - return pkg.resolutions[pattern]; - } - } - } -} - -/** - * Resolve a (possible) dist-tag version - * @template {Module} T - * @param {T} info - */ -export async function resolvePackageVersion(info) { - // use the locally installed/cached version if available - const key = info.module + '@' + info.version; - const cached = DIST_TAG_CACHE.get(key); - if (cached) { - if (Date.now() - cached.time <= DIST_TAG_TTL) { - info.version = cached.version; - return info; - } - } - - // If not specified, use any version constraints from the project's package.json: - if (!info.version) { - if (!appPackageJson) { - appPackageJson = (await readPackageJson(resolve(NODE_MODULES, '..', 'package.json'))) || {}; - } - const resolvedVersion = - getPackageVersionFromDeps(appPackageJson, info.module) || - getPackageVersionFromResolutions(appPackageJson, info.module); - info.version = resolvedVersion || 'latest'; - } - - const pkg = await readPackageJson(resolve(NODE_MODULES, info.module, 'package.json')); - if (pkg) { - DIST_TAG_CACHE.set(key, { time: Date.now(), version: pkg.version }); - info.version = pkg.version; - return info; - } - - const r = await manuallyResolvePackageVersion(info); - DIST_TAG_CACHE.set(key, { time: Date.now(), version: r.version }); - return r; -} - -/** - * Resolve a dist-tag version by fetching fresh metadata from the registry - * @template {Module} T - * @param {T} info - */ -async function manuallyResolvePackageVersion(info) { - const { module, version } = info; - const meta = await getPackageMeta(module); - const exactVersion = resolveVersion(meta, version); - if (!exactVersion) { - throw Error(`Unknown package version: ${module}@${version}`); - } - info.version = exactVersion; - return info; -} - -/** - * Get the highest matching semver version for a package - * @param {Meta} meta - * @param {string} version - */ -function resolveVersion(meta, version) { - const distTags = meta['dist-tags']; - if (distTags.hasOwnProperty(version)) { - return distTags[version]; - } - - if (meta.versions.hasOwnProperty(version)) { - return version; - } - - const versions = Object.keys(meta.versions); - return semverMaxSatisfying(versions, version); -} - -// "corgi" requests -const SLIM_REQ = { - headers: { - accept: 'application/vnd.npm.install-v1+json' - } -}; - -/** - * Fetch the npm metadata for a module - * @returns {Promise} - */ -const getPackageMeta = memo(async module => { - try { - return await getJson(`${API}/${module}`, SLIM_REQ); - } catch (e) { - throw friendlyNetworkError(e, `npm registry lookup failed for "${module}"`); - } -}); - -/** - * Get a map of files from an npm package - * @param {Module} info - */ -export async function loadPackageFiles({ module, version }) { - const meta = await getPackageMeta(module); - const exactVersion = resolveVersion(meta, version); - if (!exactVersion) { - throw Error(`Unknown package version: ${module}@${version}`); - } - - const info = meta.versions[exactVersion]; - return await getTarFiles(info.dist.tarball, module, version); -} - -/** - * Cache file contents of package files for quick access. - * Example: - * `my-module@1.0.0 :: /index.js` -> `console.log("hello world")` - * @type {Map} - */ -const DISK_CACHE = new Map(); - -/** - * Read a single file from an npm package - * @param {Module} info - */ -export async function loadPackageFile({ module, version, path = '' }) { - path = path.replace(/^\.?\//g, ''); - - // console.log('loadPackageFile: ', module, version, path); - // first, check if this file is sitting in the in-memory cache: - const files = tarFiles.get(module + '/' + version); - if (files) { - const inMemory = files.get(path); - // console.log(`${path} using in-memory strategy ${inMemory ? 'HIT' : 'MISS'}`); - if (inMemory) return inMemory; - return whenFile({ module, version, path }); - } - - // otherwise, check if it's available in node_modules: - const cacheKey = `${module}@${version} :: \n${path}`; - let file = DISK_CACHE.get(cacheKey); - if (file != null) { - return file; - } - - try { - const localPath = resolve(NODE_MODULES, module, path); - const contents = await fs.readFile(localPath, 'utf-8'); - DISK_CACHE.set(cacheKey, contents); - return contents; - } catch (e) { - const packageExists = await fs.stat(resolve(NODE_MODULES, module)).catch(() => null); - // console.log( - // `${path} not found, there is ${packageExists ? 'a' : 'no'} package at ${resolve(NODE_MODULES, module)}:\n${ - // e.message - // }` - // ); - if (packageExists) { - // the package has been streamed to disk, but it doesn't contain this file. - throw Error(`File not found ${e.message}`); - } - } - - // console.log(`${module}/${path} using tar stream strategy`); - // trigger package fetch, and resolve as soon as the file passes through the tar stream: - loadPackageFiles({ module, version }); - return whenFile({ module, version, path }); - - // OLD: get all files, then return the requested one - // const files = await loadPackageFiles({ module, version }); - // if (!files.has(path)) { - // throw Error(`Package ${module} does not contain file ${path}`); - // } - // return files.get(path); -} - -/** @type {Map>} */ -const tarFiles = new Map(); - -/** @type {Map>} */ -const whenFiles = new Map(); - -/** - * Get a promise that resolves once a package file as early as possible - * @param {Module} info - * @returns {Promise} - */ -function whenFile({ module, version, path = '' }) { - // const f = module + '@' + version + ' :: ' + path; - const packageSpecifier = module + '/' + version; - let files = tarFiles.get(packageSpecifier); - let whens = whenFiles.get(packageSpecifier); - if (files) { - const cached = files.get(path); - if (cached != null) { - // console.log(`when(${f}): already available`); - return Promise.resolve(cached); - } - // we already have a completed files listing and this file wasn't in it. - if (!whens) { - // console.log(`when(${f}): confirmed missing`); - return Promise.reject('no such file'); - } - } - // console.log(`when(${f}): pending (${files ? 'has' : 'no'} files, ${whens ? 'has' : 'no'} whens)`); - // whenFile() should never be called prior to getTarFiles() to avoid races. - if (!whens) { - whens = new Set(); - whenFiles.set(packageSpecifier, whens); - } - return new Promise((resolve, reject) => { - // @ts-ignore - whens.add({ path, resolve, reject }); - }); -} - -const getTarFiles = memo(async (tarballUrl, packageName, version) => { - const packageSpecifier = packageName + '/' + version; - // @TODO: pull from cacache - // https://github.com/npm/pacote/blob/c8ce18728512b4c64fb0a793b99b638fcc2adc31/lib/util/cache-dir.js#L11 - // const cacheDir = os.homedir()+'/.npm/_cacache' - // await cacache.get(cacheDir, "sri-hash") - - // the existence of an entry in whenFiles indicates that we have an in-flight request - let whens = whenFiles.get(packageSpecifier); - if (!whens) whenFiles.set(packageSpecifier, (whens = new Set())); - - // const start = Date.now(); - - // we should never reach here if there's an existing entry in tarFiles - // if (tarFiles.get(packageSpecifier)) throw Error('this should never happen'); - /** @type {Map} */ - const files = new Map(); - tarFiles.set(packageSpecifier, files); - - // console.log('streaming tarball for ' + packageName + '@' + version + ': ' + tarballUrl); - let tarStream; - try { - tarStream = await getStream(tarballUrl); - } catch (e) { - throw friendlyNetworkError(e, `npm download failed for "${packageName}"`); - } - - // console.log('getting files for ' + packageName); - await parseTarball(tarStream, async (name, stream) => { - // write the file to node_modules - // createWriteStream(resolve(NODE_MODULES, packageName, name)); - - let data = await streamToString(stream); - - for (const plugin of plugins) { - if (!plugin.transform) continue; - // try { - const out = plugin.transform(data, name); - if (out) data = out; - // } catch (e) {} - } - - // console.log(`file ${name} in package ${packageName}: `); - - writeNpmFile(packageName, name, data); - - files.set(name, data); - Array.from(whens).forEach(when => { - if (when.path === name) { - when.resolve(data); - whens.delete(when); - } - }); - }); - - // console.log('got files for ' + packageName, Array.from(whens)); - - // reject any remaining pending resolutions - const remaining = Array.from(whens); - whenFiles.delete(packageSpecifier); - remaining.forEach(when => { - when.reject(Error(`Package ${packageName} does not contain file ${when.path}`)); - }); - - // console.log(`Streamed ${packageName} in ${Date.now() - start}ms`); - - return files; -}); - -/** Asynchronously write a file to node_modules */ -export async function writeNpmFile(packageName, filename, data) { - await fs.mkdir(resolve(NODE_MODULES, packageName, dirname(filename)), { recursive: true }); - await fs.writeFile(resolve(NODE_MODULES, packageName, filename), data); -} - -/** - * @param {import('stream').Readable} bodyStream - * @param {(name: string, stream: import('stream').PassThrough) => Promise} onFile - */ -function parseTarball(bodyStream, onFile) { - return new Promise((resolve, reject) => { - const extract = tar.extract(); - - extract.on('entry', (header, stream, next) => { - let { type, name } = header; - name = name.replace(/^package\//, ''); - - if (type !== 'file' || !FILES_INCLUDE.test(name) || FILES_EXCLUDE.test(name)) { - stream.resume(); - return next(); - } - - onFile(name, stream).then(next); - }); - - extract.on('finish', resolve); - extract.on('error', reject); - - bodyStream.pipe(zlib.createGunzip()).pipe(extract); - }); -} diff --git a/packages/wmr/src/plugins/npm-plugin/resolve.js b/packages/wmr/src/plugins/npm-plugin/resolve.js deleted file mode 100644 index 760ee88bc..000000000 --- a/packages/wmr/src/plugins/npm-plugin/resolve.js +++ /dev/null @@ -1,79 +0,0 @@ -import { resolve as _resolveExports, legacy as _resolveLegacyEntry } from 'resolve.exports'; - -function resolveExports(pkg, key) { - return _resolveExports(pkg, key, { - browser: true, - conditions: [process.env.NODE_ENV === 'production' ? 'production' : 'development', 'esmodules', 'module'] - }); -} - -function resolveLegacyEntry(pkg, path) { - const entry = - _resolveLegacyEntry(pkg, { - browser: true, - fields: ['esmodules', 'modern', 'module', 'jsnext:main', 'browser', 'main'] - }) || 'index.js'; - return '/' + entry.replace(/^\.?\//, ''); -} - -/** - * @param {string} path - * @param {object} context - * @param {(f: string) => Promise} context.readFile Reads a file within the package directory - * @param {(f: string) => Promise} context.hasFile Checks for the existence of a file within the package directory - * @param {string} [context.module] The module/package name - * @param {boolean} [context.internal = false] Resolve `path` as an internal specifier - obeys Export Map, but falls back to direct resolution. - */ -export async function resolveModule(path, { readFile, hasFile, module, internal }) { - let pkg; - try { - pkg = JSON.parse(await readFile('package.json')); - } catch (e) { - throw Error(`Invalid package.json for ${module}: ${e.message}`); - } - - // Many early adopters of Export Maps use invalid specifiers, - // relying on CommonJS semantics like extensionless imports. - // To address this, we resolve extensionless internal imports. - const isExportMappedSpecifier = pkg.exports && internal; - - // Package Export Maps - if (!internal && pkg.exports) { - // will normalize entry & will throw error if no match - const mapped = resolveExports(pkg, path || '.'); - // An entry ending in `/` remaps to a directory, but is not considered resolved. - if (mapped.endsWith('/')) path = mapped; - else return mapped.replace(/^\./, ''); - } - - // path is a bare import of a package, use its legacy exports (module/main): - if (!path) { - path = resolveLegacyEntry(pkg, path || '.'); - } - - // fallback: implement basic commonjs-style resolution - if (/\.([mc]js|[tj]sx?)$/i.test(path)) { - return path; - } - - // path is a directory, check for package.json: - // (this is skipped ) - if (!isExportMappedSpecifier) { - try { - const subPkg = JSON.parse(await readFile(path + '/package.json')); - path += resolveLegacyEntry(subPkg, '.'); - } catch (e) {} - } - - // extensionless paths: - if (await hasFile(path + '.js')) { - return path + '.js'; - } - - // fall back to implicit directory /index.js: - if (!isExportMappedSpecifier && (await hasFile(path + '/index.js'))) { - return path + '/index.js'; - } - - return path; -} diff --git a/packages/wmr/src/plugins/npm-plugin/size-warning-plugin.js b/packages/wmr/src/plugins/npm-plugin/size-warning-plugin.js index 0cde401fb..ff14224ff 100644 --- a/packages/wmr/src/plugins/npm-plugin/size-warning-plugin.js +++ b/packages/wmr/src/plugins/npm-plugin/size-warning-plugin.js @@ -3,7 +3,7 @@ const DEFAULT_THRESHOLD = 100000; /** * Warn when huge files are getting piped into node_modules. - * @returns {import("./registry").Plugin} + * @returns {import("rollup").Plugin} */ export default function sizeWarningPlugin({ threshold = DEFAULT_THRESHOLD, factor = 3 } = {}) { return { diff --git a/packages/wmr/src/plugins/npm-plugin/sub-package-legacy.js b/packages/wmr/src/plugins/npm-plugin/sub-package-legacy.js new file mode 100644 index 000000000..7afb3a7c5 --- /dev/null +++ b/packages/wmr/src/plugins/npm-plugin/sub-package-legacy.js @@ -0,0 +1,31 @@ +import path from 'path'; +import { promises as fs } from 'fs'; +import { isDirectory } from '../../lib/fs-utils.js'; + +/** + * Legacy way of defining package entry points before the + * "export" field in `package.json` was a thing. + * @returns {import('rollup').Plugin} + */ +export function subPackageLegacy() { + return { + name: 'legacy-sub-package', + async resolveId(id, importer) { + if (!path.isAbsolute(id) || !(await isDirectory(id))) return; + + try { + const subFile = path.join(id, 'package.json'); + const pkg = JSON.parse(await fs.readFile(subFile, 'utf-8')); + + const nextId = path.join(id, pkg.module || pkg.main); + + const resolved = await this.resolve(nextId, importer, { skipSelf: true }); + + return { + id: resolved ? resolved.id : nextId, + external: true + }; + } catch (err) {} + } + }; +} diff --git a/packages/wmr/src/plugins/npm-plugin/utils.js b/packages/wmr/src/plugins/npm-plugin/utils.js index 616d84fb6..982044671 100644 --- a/packages/wmr/src/plugins/npm-plugin/utils.js +++ b/packages/wmr/src/plugins/npm-plugin/utils.js @@ -1,4 +1,8 @@ -import { get } from 'https'; +import * as path from 'path'; +import { isDirectory } from '../../lib/fs-utils.js'; +import { builtinModules } from 'module'; + +const builtins = new Set(builtinModules); /** * User-friendly registry/network error messages. @@ -13,37 +17,6 @@ export function friendlyNetworkError(err, text) { throw Object.assign(friendlyErr, { code: err.code }); } -/** - * @param {string} url - * @param {Parameters[1]} [config] - */ -export function getJson(url, config) { - return getStream(url, config).then(streamToString).then(JSON.parse); -} - -/** - * @param {string} url - * @param {Parameters[1]} [config] - * @returns {Promise} - */ -export function getStream(url, config) { - return new Promise((resolve, reject) => { - const req = get(url, config || {}, res => { - const status = res.statusCode || 0; - if (status >= 200 && status < 400) { - return resolve(res); - } - const err = Object.assign(Error(`${res.statusMessage}: ${url}`), { - code: status, - status, - res - }); - reject(err); - }); - req.on('error', reject); - }); -} - /** * @param {import('stream').Readable} stream * @returns {Promise} @@ -64,21 +37,147 @@ export function streamToString(stream) { } /** - * Simple single-arity memoize - * @template {(...args: any[]) => any} T - * @param {T} fn - * @param {(...args: Parameters) => string} [getCacheKey] - * @returns {(...args: Parameters) => ReturnType} + * Check if id is a valid npm package name + * @param {string} id + * @returns {boolean} */ -export function memo(fn, getCacheKey) { - const cache = new Map(); - return function (...args) { - const key = getCacheKey ? getCacheKey(...args) : args[0]; - let out = cache.get(key); - if (out === undefined) { - out = fn(...args); - cache.set(key, out); +export function isValidPackageName(id) { + const isValid = + // Must not start with `._` + !/^[._/]/.test(id) && + // Must not match deny list + !/node_modules|favicon\.ico/.test(id) && + // Must not be a built-in node module + !builtins.has(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); + + return isValid; +} + +/** + * Extract package meta information from id. + * + * foo + * -> { name: 'foo', version: '', pathname: '' } + * foo/bar.css + * -> { name: 'foo', version: '', pathname: 'bar.css' } + * foo@1.2.3-rc.1/bar.css + * -> { name: 'foo', version: '1.2.3-rc.1', pathname: 'bar.css' } + * @foo/bar.css + * -> { name: '@foo/bar.css', version: '', pathname: '' } + * @foo/bar/bob.css + * -> { name: '@foo/bar', version: '', pathname: 'bob.css' } + * @foo/bar@1.2.3/bob.css + * -> { name: '@foo/bar', version: '1.2.3', pathname: 'bob.css' } + * @param {string} id + * @returns {{ name: string, version: string, pathname: string }} + */ +export function getPackageInfo(id) { + const match = id.match(/^(@[^/]+\/[^/@]+|[^@][^/@]+)(?:@([^/]+))?(?:\/(.*))?$/); + + if (!match) { + throw new Error(`Unable to extract package meta information from "${id}"`); + } + + const [, name, version = '', pathname = ''] = match; + return { name, version, pathname }; +} + +/** + * Find directory to installed package. We'll + * essentially traverse upwards and search for + * `node_modules`. + * @param {string} root + * @param {string} name + */ +export async function findInstalledPackage(root, name) { + // There may be multiple `node_modules` directories at play + // with monorepo setups. + try { + let dir = root; + + let lastDir = root; + // eslint-disable-next-line no-constant-condition + while (true) { + const maybe = path.join(dir, 'node_modules', name); + if (await isDirectory(maybe)) { + return maybe; + } + + lastDir = dir; + dir = path.dirname(dir); + if (lastDir === dir) { + return; + } } - return out; - }; + } catch (err) { + return; + } +} + +function resolveExportsValue(obj) { + const order = ['import', 'node', 'require', 'default']; + + for (let i = 0; i < order.length; i++) { + const key = order[i]; + if (key in obj) return obj[key]; + } +} + +/** + * Resolve "exports" field in `package.json`. + * @param {Record} pkg Package JSON + * @param {string} pathname + * @returns {string | undefined} + */ +export function resolvePackageExport(pkg, pathname) { + // Main entry + if (!pathname) { + if (typeof pkg.exports === 'string') { + return pkg.exports; + } + + if ('.' in pkg.exports) { + const info = pkg.exports['.']; + if (typeof info === 'string') { + return info; + } + + return resolveExportsValue(info); + } + + return resolveExportsValue(pkg.exports); + } + + // Non-main entry + const maybeEntry = pkg.exports['./' + pathname]; + if (maybeEntry) { + if (typeof maybeEntry === 'string') { + return maybeEntry; + } + + return resolveExportsValue(maybeEntry); + } +} + +/** + * Escape special characters of npm names for filename + * @param {string} str + * @returns {string} + */ +export function escapeFilename(str) { + return str.replace('/', '__'); +} + +export class Deferred { + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } } diff --git a/packages/wmr/src/server.js b/packages/wmr/src/server.js index e0ee613d9..749909a22 100644 --- a/packages/wmr/src/server.js +++ b/packages/wmr/src/server.js @@ -5,11 +5,11 @@ import { createHttp2Server } from './lib/http2.js'; import polka from 'polka'; import sirv from 'sirv'; import compression from './lib/polkompress.js'; -import npmMiddleware from './lib/npm-middleware.js'; import WebSocketServer from './lib/websocket-server.js'; import * as kl from 'kolorist'; import * as errorstacks from 'errorstacks'; import { hasDebugFlag } from './lib/output-utils.js'; +import { npmEtagCache } from './lib/npm-middleware.js'; /** * @typedef CustomServer @@ -18,8 +18,8 @@ import { hasDebugFlag } from './lib/output-utils.js'; /** * @param {object} options - * @param {string} [options.cwd = ''] Directory to serve - * @param {string} [options.root] Virtual process.cwd + * @param {string} options.cwd Directory to serve + * @param {string} options.root Virtual process.cwd * @param {string} [options.publicDir] A directory containing public files, relative to cwd * @param {string} [options.overlayDir] A directory of generated files to serve if present, relative to cwd * @param {polka.Middleware[]} [options.middleware] Additional Polka middlewares to inject @@ -98,6 +98,8 @@ export default async function server({ cwd, root, overlayDir, middleware, http2, app.ws = new WebSocketServer(app.server, '/_hmr'); + app.use(npmEtagCache()); + if (compress) { // @TODO: reconsider now that npm deps are compressed AOT const threshold = compress === true ? 1024 : compress; @@ -109,8 +111,6 @@ export default async function server({ cwd, root, overlayDir, middleware, http2, app.use(...middleware); } - app.use('/@npm', npmMiddleware({ alias, optimize, cwd })); - // Chrome devtools often adds `?%20[sm]` to the url // to differentiate between sourcemaps app.use((req, res, next) => { diff --git a/packages/wmr/src/start.js b/packages/wmr/src/start.js index cef10a81e..10607790f 100644 --- a/packages/wmr/src/start.js +++ b/packages/wmr/src/start.js @@ -5,7 +5,6 @@ import server from './server.js'; import wmrMiddleware from './wmr-middleware.js'; import { getServerAddresses, supportsSearchParams } from './lib/net-utils.js'; import { normalizeOptions } from './lib/normalize-options.js'; -import { setCwd } from './plugins/npm-plugin/registry.js'; import { formatBootMessage, debug } from './lib/output-utils.js'; import { watch } from './lib/fs-watcher.js'; import { injectWmr } from './lib/transform-html.js'; @@ -27,9 +26,6 @@ const deepCloneJSON = obj => JSON.parse(JSON.stringify(obj)); * @param {Parameters[0] & OtherOptions} options */ export default async function start(options = {}) { - // @todo remove this hack once registry.js is instantiable - setCwd(options.cwd); - // TODO: We seem to mutate our config object somewhere const cloned = deepCloneJSON(options); diff --git a/packages/wmr/src/wmr-middleware.js b/packages/wmr/src/wmr-middleware.js index 603dfe027..ea03ffc65 100644 --- a/packages/wmr/src/wmr-middleware.js +++ b/packages/wmr/src/wmr-middleware.js @@ -4,7 +4,6 @@ import * as kl from 'kolorist'; import { getWmrClient } from './plugins/wmr/plugin.js'; import { createPluginContainer } from './lib/rollup-plugin-container.js'; import { transformImports } from './lib/transform-imports.js'; -import { normalizeSpecifier } from './plugins/npm-plugin/index.js'; import { getMimeType } from './lib/mimetypes.js'; import { debug, formatPath } from './lib/output-utils.js'; import { getPlugins } from './lib/plugins.js'; @@ -78,7 +77,7 @@ export default function wmrMiddleware(options) { cwd, disableGlobbing: true, ignoreInitial: true, - ignored: [/(^|[/\\])(node_modules|\.git|\.DS_Store)([/\\]|$)/, resolve(cwd, out), resolve(cwd, distDir)] + ignored: [/(^|[/\\])(node_modules|\.git|\.DS_Store|\.cache)([/\\]|$)/, resolve(cwd, out), resolve(cwd, distDir)] }); const pendingChanges = new Set(); @@ -243,15 +242,11 @@ export default function wmrMiddleware(options) { const queryParams = new URL(req.url, 'file://').searchParams; - if (path.startsWith('/@npm/')) { - return next(); - } - let prefix = ''; // Workaround for transform forcing extensionless ids to be // non-js - let hasIdPrefix = false; + let isVirtual = false; let file = ''; let id = path; @@ -262,7 +257,7 @@ 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. @@ -270,6 +265,10 @@ export default function wmrMiddleware(options) { 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)); @@ -284,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); } @@ -301,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}`; } @@ -336,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 @@ -596,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}`; } @@ -619,29 +618,7 @@ export const TRANSFORMS = { if (aliased) spec = aliased; if (!spec.startsWith('/@alias/')) { - // Check if this is a virtual module path from a plugin. If - // no plugin loads the id, then we know that the bare specifier - // must refer to an npm plugin. - // TODO: Cache the result to avoid having to load an id twice. - const res = await NonRollup.load(spec); - - if (res === null) { - // Bare specifiers are npm packages: - const meta = normalizeSpecifier(spec); - - // // Option 1: resolve all package verions (note: adds non-trivial delay to imports) - // await resolvePackageVersion(meta); - // // Option 2: omit package versions that resolve to the root - // // if ((await resolvePackageVersion({ module: meta.module, version: '' })).version === meta.version) { - // // meta.version = ''; - // // } - // spec = `/@npm/${meta.module}${meta.version ? '@' + meta.version : ''}${meta.path ? '/' + meta.path : ''}`; - - // Option 3: omit root package versions - spec = `/@npm/${meta.module}${meta.path ? '/' + meta.path : ''}`; - } else { - spec = `/@id/${spec}`; - } + spec = `/@id/${spec}`; } } diff --git a/packages/wmr/test/fixtures.test.js b/packages/wmr/test/fixtures.test.js index 110be2631..9ee832082 100644 --- a/packages/wmr/test/fixtures.test.js +++ b/packages/wmr/test/fixtures.test.js @@ -1039,16 +1039,18 @@ describe('fixtures', () => { expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-requirefirst')`)).toEqual({ default: 'import' }); - expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-defaultfirst')`)).toEqual({ - default: 'default' + await withLog(instance.output, async () => { + expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-defaultfirst')`)).toEqual({ + default: 'import' + }); }); // When import/module/browser isn't present (but a random other one is!), we fall back to require/default: expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-requirefallback')`)).toEqual({ - default: 'default' + default: 'require' }); expect(await env.page.evaluate(`import('/@npm/exports-fallbacks-defaultfallback')`)).toEqual({ - default: 'default' + default: 'require' }); }); }); diff --git a/packages/wmr/test/fixtures/npm-auto-install-css-2/index.html b/packages/wmr/test/fixtures/npm-auto-install-css-2/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install-css-2/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-auto-install-css-2/index.js b/packages/wmr/test/fixtures/npm-auto-install-css-2/index.js new file mode 100644 index 000000000..cae2c97ef --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install-css-2/index.js @@ -0,0 +1,4 @@ +import Calendar from 'react-calendar'; +import 'react-calendar/dist/Calendar.css'; + +document.querySelector('h1').textContent = typeof Calendar === 'function' ? 'it works' : "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-auto-install-css-2/wmr.config.mjs b/packages/wmr/test/fixtures/npm-auto-install-css-2/wmr.config.mjs new file mode 100644 index 000000000..111525917 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install-css-2/wmr.config.mjs @@ -0,0 +1,5 @@ +export default { + alias: { + react: 'preact/compat' + } +}; diff --git a/packages/wmr/test/fixtures/npm-auto-install-css/index.html b/packages/wmr/test/fixtures/npm-auto-install-css/index.html new file mode 100644 index 000000000..48f3fd527 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install-css/index.html @@ -0,0 +1,2 @@ +Check color + diff --git a/packages/wmr/test/fixtures/npm-auto-install-css/index.js b/packages/wmr/test/fixtures/npm-auto-install-css/index.js new file mode 100644 index 000000000..47950ace8 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install-css/index.js @@ -0,0 +1 @@ +import 'mvp.css/mvp.css'; diff --git a/packages/wmr/test/fixtures/npm-auto-install-version/index.html b/packages/wmr/test/fixtures/npm-auto-install-version/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install-version/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-auto-install-version/index.js b/packages/wmr/test/fixtures/npm-auto-install-version/index.js new file mode 100644 index 000000000..9d9666e46 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install-version/index.js @@ -0,0 +1,3 @@ +import { forEach } from 'smoldash@0.10.0'; + +document.querySelector('h1').textContent = typeof forEach === 'function' ? 'it works' : "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-auto-install/index.html b/packages/wmr/test/fixtures/npm-auto-install/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-auto-install/index.js b/packages/wmr/test/fixtures/npm-auto-install/index.js new file mode 100644 index 000000000..a65d3ac93 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-auto-install/index.js @@ -0,0 +1,3 @@ +import { useState } from 'preact/hooks'; + +document.querySelector('h1').textContent = typeof useState === 'function' ? 'it works' : "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/browser.js b/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/browser.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/browser.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/index.js new file mode 100644 index 000000000..24cdde51b --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/index.js @@ -0,0 +1 @@ +export { value } from 'fail'; diff --git a/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/package.json new file mode 100644 index 000000000..3c5af49b5 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-bare/-node_modules/foo/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo", + "main": "./index.js", + "browser": { + "fail": "./browser" + } +} diff --git a/packages/wmr/test/fixtures/npm-browser-bare-bare/index.html b/packages/wmr/test/fixtures/npm-browser-bare-bare/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-bare/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-browser-bare-bare/index.js b/packages/wmr/test/fixtures/npm-browser-bare-bare/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-bare/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/browser.js b/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/browser.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/browser.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/index.js new file mode 100644 index 000000000..24cdde51b --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/index.js @@ -0,0 +1 @@ +export { value } from 'fail'; diff --git a/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/package.json new file mode 100644 index 000000000..4c0136a14 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-relative/-node_modules/foo/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo", + "main": "./index.js", + "browser": { + "./fail": "./browser" + } +} diff --git a/packages/wmr/test/fixtures/npm-browser-bare-relative/index.html b/packages/wmr/test/fixtures/npm-browser-bare-relative/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-relative/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-browser-bare-relative/index.js b/packages/wmr/test/fixtures/npm-browser-bare-relative/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-bare-relative/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/browser.js b/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/browser.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/browser.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/index.js b/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/index.js new file mode 100644 index 000000000..aaf092f31 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/index.js @@ -0,0 +1 @@ +export { value } from './fail'; diff --git a/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/package.json b/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/package.json new file mode 100644 index 000000000..9a0ae48a3 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-bare/-node_modules/pkg/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo", + "main": "./index.js", + "browser": { + "foo/bar": "./browser" + } +} diff --git a/packages/wmr/test/fixtures/npm-browser-deep-bare/index.html b/packages/wmr/test/fixtures/npm-browser-deep-bare/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-bare/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-browser-deep-bare/index.js b/packages/wmr/test/fixtures/npm-browser-deep-bare/index.js new file mode 100644 index 000000000..17f293064 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-bare/index.js @@ -0,0 +1,3 @@ +import { value } from 'pkg/foo/bar'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/browser.js b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/browser.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/browser.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/fail.js b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/fail.js new file mode 100644 index 000000000..00946192a --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/fail.js @@ -0,0 +1 @@ +export const value = "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/index.js b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/index.js new file mode 100644 index 000000000..aaf092f31 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/index.js @@ -0,0 +1 @@ +export { value } from './fail'; diff --git a/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/package.json b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/package.json new file mode 100644 index 000000000..4e1c390ec --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-relative/-node_modules/pkg/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo", + "main": "./index.js", + "browser": { + "./foo/bar": "./browser" + } +} diff --git a/packages/wmr/test/fixtures/npm-browser-deep-relative/index.html b/packages/wmr/test/fixtures/npm-browser-deep-relative/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-relative/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-browser-deep-relative/index.js b/packages/wmr/test/fixtures/npm-browser-deep-relative/index.js new file mode 100644 index 000000000..17f293064 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-deep-relative/index.js @@ -0,0 +1,3 @@ +import { value } from 'pkg/foo/bar'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/browser.js b/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/browser.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/browser.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/index.js new file mode 100644 index 000000000..aaf092f31 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/index.js @@ -0,0 +1 @@ +export { value } from './fail'; diff --git a/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/package.json new file mode 100644 index 000000000..3c5af49b5 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-bare/-node_modules/foo/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo", + "main": "./index.js", + "browser": { + "fail": "./browser" + } +} diff --git a/packages/wmr/test/fixtures/npm-browser-relative-bare/index.html b/packages/wmr/test/fixtures/npm-browser-relative-bare/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-bare/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-browser-relative-bare/index.js b/packages/wmr/test/fixtures/npm-browser-relative-bare/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-bare/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/browser.js b/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/browser.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/browser.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/index.js new file mode 100644 index 000000000..aaf092f31 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/index.js @@ -0,0 +1 @@ +export { value } from './fail'; diff --git a/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/package.json new file mode 100644 index 000000000..4c0136a14 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-relative/-node_modules/foo/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo", + "main": "./index.js", + "browser": { + "./fail": "./browser" + } +} diff --git a/packages/wmr/test/fixtures/npm-browser-relative-relative/index.html b/packages/wmr/test/fixtures/npm-browser-relative-relative/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-relative/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-browser-relative-relative/index.js b/packages/wmr/test/fixtures/npm-browser-relative-relative/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-browser-relative-relative/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-commonjs-default/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-commonjs-default/-node_modules/foo/index.js new file mode 100644 index 000000000..017f5b745 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-default/-node_modules/foo/index.js @@ -0,0 +1 @@ +module.exports = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-commonjs-default/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-commonjs-default/-node_modules/foo/package.json new file mode 100644 index 000000000..c3826c1c6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-default/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./index.js" +} diff --git a/packages/wmr/test/fixtures/npm-commonjs-default/index.html b/packages/wmr/test/fixtures/npm-commonjs-default/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-default/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-commonjs-default/index.js b/packages/wmr/test/fixtures/npm-commonjs-default/index.js new file mode 100644 index 000000000..e47dac667 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-default/index.js @@ -0,0 +1,3 @@ +import value from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife-2/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-commonjs-iife-2/-node_modules/foo/main.js new file mode 100644 index 000000000..8932a3879 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife-2/-node_modules/foo/main.js @@ -0,0 +1,246 @@ +/** @license React v17.0.2 + * react-is.development.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +if (process.env.NODE_ENV !== 'production') { + (function () { + 'use strict'; + + // ATTENTION + // When adding new symbols to this file, + // Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols' + // The Symbol used to tag the ReactElement-like types. If there is no native Symbol + // nor polyfill, then a plain number is used for performance. + var REACT_ELEMENT_TYPE = 0xeac7; + var REACT_PORTAL_TYPE = 0xeaca; + var REACT_FRAGMENT_TYPE = 0xeacb; + var REACT_STRICT_MODE_TYPE = 0xeacc; + var REACT_PROFILER_TYPE = 0xead2; + var REACT_PROVIDER_TYPE = 0xeacd; + var REACT_CONTEXT_TYPE = 0xeace; + var REACT_FORWARD_REF_TYPE = 0xead0; + var REACT_SUSPENSE_TYPE = 0xead1; + var REACT_SUSPENSE_LIST_TYPE = 0xead8; + var REACT_MEMO_TYPE = 0xead3; + var REACT_LAZY_TYPE = 0xead4; + var REACT_BLOCK_TYPE = 0xead9; + var REACT_SERVER_BLOCK_TYPE = 0xeada; + var REACT_FUNDAMENTAL_TYPE = 0xead5; + var REACT_SCOPE_TYPE = 0xead7; + var REACT_OPAQUE_ID_TYPE = 0xeae0; + var REACT_DEBUG_TRACING_MODE_TYPE = 0xeae1; + var REACT_OFFSCREEN_TYPE = 0xeae2; + var REACT_LEGACY_HIDDEN_TYPE = 0xeae3; + + if (typeof Symbol === 'function' && Symbol.for) { + var symbolFor = Symbol.for; + REACT_ELEMENT_TYPE = symbolFor('react.element'); + REACT_PORTAL_TYPE = symbolFor('react.portal'); + REACT_FRAGMENT_TYPE = symbolFor('react.fragment'); + REACT_STRICT_MODE_TYPE = symbolFor('react.strict_mode'); + REACT_PROFILER_TYPE = symbolFor('react.profiler'); + REACT_PROVIDER_TYPE = symbolFor('react.provider'); + REACT_CONTEXT_TYPE = symbolFor('react.context'); + REACT_FORWARD_REF_TYPE = symbolFor('react.forward_ref'); + REACT_SUSPENSE_TYPE = symbolFor('react.suspense'); + REACT_SUSPENSE_LIST_TYPE = symbolFor('react.suspense_list'); + REACT_MEMO_TYPE = symbolFor('react.memo'); + REACT_LAZY_TYPE = symbolFor('react.lazy'); + REACT_BLOCK_TYPE = symbolFor('react.block'); + REACT_SERVER_BLOCK_TYPE = symbolFor('react.server.block'); + REACT_FUNDAMENTAL_TYPE = symbolFor('react.fundamental'); + REACT_SCOPE_TYPE = symbolFor('react.scope'); + REACT_OPAQUE_ID_TYPE = symbolFor('react.opaque.id'); + REACT_DEBUG_TRACING_MODE_TYPE = symbolFor('react.debug_trace_mode'); + REACT_OFFSCREEN_TYPE = symbolFor('react.offscreen'); + REACT_LEGACY_HIDDEN_TYPE = symbolFor('react.legacy_hidden'); + } + + // Filter certain DOM attributes (e.g. src, href) if their values are empty strings. + + var enableScopeAPI = false; // Experimental Create Event Handle API. + + function isValidElementType(type) { + if (typeof type === 'string' || typeof type === 'function') { + return true; + } // Note: typeof might be other than 'symbol' or 'number' (e.g. if it's a polyfill). + + if ( + type === REACT_FRAGMENT_TYPE || + type === REACT_PROFILER_TYPE || + type === REACT_DEBUG_TRACING_MODE_TYPE || + type === REACT_STRICT_MODE_TYPE || + type === REACT_SUSPENSE_TYPE || + type === REACT_SUSPENSE_LIST_TYPE || + type === REACT_LEGACY_HIDDEN_TYPE || + enableScopeAPI + ) { + return true; + } + + if (typeof type === 'object' && type !== null) { + if ( + type.$$typeof === REACT_LAZY_TYPE || + type.$$typeof === REACT_MEMO_TYPE || + type.$$typeof === REACT_PROVIDER_TYPE || + type.$$typeof === REACT_CONTEXT_TYPE || + type.$$typeof === REACT_FORWARD_REF_TYPE || + type.$$typeof === REACT_FUNDAMENTAL_TYPE || + type.$$typeof === REACT_BLOCK_TYPE || + type[0] === REACT_SERVER_BLOCK_TYPE + ) { + return true; + } + } + + return false; + } + + function typeOf(object) { + if (typeof object === 'object' && object !== null) { + var $$typeof = object.$$typeof; + + switch ($$typeof) { + case REACT_ELEMENT_TYPE: + var type = object.type; + + switch (type) { + case REACT_FRAGMENT_TYPE: + case REACT_PROFILER_TYPE: + case REACT_STRICT_MODE_TYPE: + case REACT_SUSPENSE_TYPE: + case REACT_SUSPENSE_LIST_TYPE: + return type; + + default: + var $$typeofType = type && type.$$typeof; + + switch ($$typeofType) { + case REACT_CONTEXT_TYPE: + case REACT_FORWARD_REF_TYPE: + case REACT_LAZY_TYPE: + case REACT_MEMO_TYPE: + case REACT_PROVIDER_TYPE: + return $$typeofType; + + default: + return $$typeof; + } + } + + case REACT_PORTAL_TYPE: + return $$typeof; + } + } + + return undefined; + } + var ContextConsumer = REACT_CONTEXT_TYPE; + var ContextProvider = REACT_PROVIDER_TYPE; + var Element = REACT_ELEMENT_TYPE; + var ForwardRef = REACT_FORWARD_REF_TYPE; + var Fragment = REACT_FRAGMENT_TYPE; + var Lazy = REACT_LAZY_TYPE; + var Memo = REACT_MEMO_TYPE; + var Portal = REACT_PORTAL_TYPE; + var Profiler = REACT_PROFILER_TYPE; + var StrictMode = REACT_STRICT_MODE_TYPE; + var Suspense = REACT_SUSPENSE_TYPE; + var hasWarnedAboutDeprecatedIsAsyncMode = false; + var hasWarnedAboutDeprecatedIsConcurrentMode = false; // AsyncMode should be deprecated + + function isAsyncMode(object) { + { + if (!hasWarnedAboutDeprecatedIsAsyncMode) { + hasWarnedAboutDeprecatedIsAsyncMode = true; // Using console['warn'] to evade Babel and ESLint + + console['warn']( + 'The ReactIs.isAsyncMode() alias has been deprecated, ' + 'and will be removed in React 18+.' + ); + } + } + + return false; + } + function isConcurrentMode(object) { + { + if (!hasWarnedAboutDeprecatedIsConcurrentMode) { + hasWarnedAboutDeprecatedIsConcurrentMode = true; // Using console['warn'] to evade Babel and ESLint + + console['warn']( + 'The ReactIs.isConcurrentMode() alias has been deprecated, ' + 'and will be removed in React 18+.' + ); + } + } + + return false; + } + function isContextConsumer(object) { + return typeOf(object) === REACT_CONTEXT_TYPE; + } + function isContextProvider(object) { + return typeOf(object) === REACT_PROVIDER_TYPE; + } + function isElement(object) { + return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE; + } + function isForwardRef(object) { + return typeOf(object) === REACT_FORWARD_REF_TYPE; + } + function isFragment(object) { + return typeOf(object) === REACT_FRAGMENT_TYPE; + } + function isLazy(object) { + return typeOf(object) === REACT_LAZY_TYPE; + } + function isMemo(object) { + return typeOf(object) === REACT_MEMO_TYPE; + } + function isPortal(object) { + return typeOf(object) === REACT_PORTAL_TYPE; + } + function isProfiler(object) { + return typeOf(object) === REACT_PROFILER_TYPE; + } + function isStrictMode(object) { + return typeOf(object) === REACT_STRICT_MODE_TYPE; + } + function isSuspense(object) { + return typeOf(object) === REACT_SUSPENSE_TYPE; + } + + exports.ContextConsumer = ContextConsumer; + exports.ContextProvider = ContextProvider; + exports.Element = Element; + exports.ForwardRef = ForwardRef; + exports.Fragment = Fragment; + exports.Lazy = Lazy; + exports.Memo = Memo; + exports.Portal = Portal; + exports.Profiler = Profiler; + exports.StrictMode = StrictMode; + exports.Suspense = Suspense; + exports.isAsyncMode = isAsyncMode; + exports.isConcurrentMode = isConcurrentMode; + exports.isContextConsumer = isContextConsumer; + exports.isContextProvider = isContextProvider; + exports.isElement = isElement; + exports.isForwardRef = isForwardRef; + exports.isFragment = isFragment; + exports.isLazy = isLazy; + exports.isMemo = isMemo; + exports.isPortal = isPortal; + exports.isProfiler = isProfiler; + exports.isStrictMode = isStrictMode; + exports.isSuspense = isSuspense; + exports.isValidElementType = isValidElementType; + exports.typeOf = typeOf; + })(); +} diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife-2/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-commonjs-iife-2/-node_modules/foo/package.json new file mode 100644 index 000000000..fb49ff022 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife-2/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife-2/index.html b/packages/wmr/test/fixtures/npm-commonjs-iife-2/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife-2/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife-2/index.js b/packages/wmr/test/fixtures/npm-commonjs-iife-2/index.js new file mode 100644 index 000000000..9b04cddea --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife-2/index.js @@ -0,0 +1,3 @@ +import { isElement } from 'foo'; + +document.querySelector('h1').textContent = typeof isElement === 'function' ? 'it works' : "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-commonjs-iife/-node_modules/foo/main.js new file mode 100644 index 000000000..f36861c9f --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife/-node_modules/foo/main.js @@ -0,0 +1,5 @@ +if (process.env.NODE_ENV === 'development') { + (function () { + module.exports = 'it works'; + })(); +} diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-commonjs-iife/-node_modules/foo/package.json new file mode 100644 index 000000000..fb49ff022 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife/index.html b/packages/wmr/test/fixtures/npm-commonjs-iife/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-commonjs-iife/index.js b/packages/wmr/test/fixtures/npm-commonjs-iife/index.js new file mode 100644 index 000000000..e47dac667 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-iife/index.js @@ -0,0 +1,3 @@ +import value from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/dev.js b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/dev.js new file mode 100644 index 000000000..ddfc2ca36 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/dev.js @@ -0,0 +1 @@ +export const value = 'This is development'; diff --git a/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/main.js new file mode 100644 index 000000000..c8a703496 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/main.js @@ -0,0 +1,5 @@ +if (process.env.NODE_ENV === 'production') { + module.exports = require('./prod.js'); +} else { + module.exports = require('./dev.js'); +} diff --git a/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/package.json new file mode 100644 index 000000000..fb49ff022 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/prod.js b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/prod.js new file mode 100644 index 000000000..c0e764675 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-proxy/-node_modules/foo/prod.js @@ -0,0 +1 @@ +export const value = 'This is production'; diff --git a/packages/wmr/test/fixtures/npm-commonjs-proxy/index.html b/packages/wmr/test/fixtures/npm-commonjs-proxy/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-proxy/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-commonjs-proxy/index.js b/packages/wmr/test/fixtures/npm-commonjs-proxy/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-commonjs-proxy/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/bar/main.js b/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/bar/main.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/bar/main.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/bar/package.json b/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/bar/package.json new file mode 100644 index 000000000..b930c9c95 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "bar", + "main": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/package.json new file mode 100644 index 000000000..c3826c1c6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-main/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./index.js" +} diff --git a/packages/wmr/test/fixtures/npm-deep-main/index.html b/packages/wmr/test/fixtures/npm-deep-main/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-main/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-deep-main/index.js b/packages/wmr/test/fixtures/npm-deep-main/index.js new file mode 100644 index 000000000..96c6c1006 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-main/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo/bar'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/bar/module.js b/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/bar/module.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/bar/module.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/bar/package.json b/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/bar/package.json new file mode 100644 index 000000000..9a72c2575 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/bar/package.json @@ -0,0 +1,5 @@ +{ + "name": "bar", + "main": "./fail.js", + "module": "./module.js" +} diff --git a/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/package.json new file mode 100644 index 000000000..c3826c1c6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-module/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./index.js" +} diff --git a/packages/wmr/test/fixtures/npm-deep-module/index.html b/packages/wmr/test/fixtures/npm-deep-module/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-module/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-deep-module/index.js b/packages/wmr/test/fixtures/npm-deep-module/index.js new file mode 100644 index 000000000..96c6c1006 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-module/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo/bar'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bar/main.js b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bar/main.js new file mode 100644 index 000000000..5c162a5df --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bar/main.js @@ -0,0 +1 @@ +export { value } from 'foo/bob'; diff --git a/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bar/package.json b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bar/package.json new file mode 100644 index 000000000..b930c9c95 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "bar", + "main": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bob/main.js b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bob/main.js new file mode 100644 index 000000000..5c2879e53 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bob/main.js @@ -0,0 +1 @@ +export { value } from 'foo'; diff --git a/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bob/package.json b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bob/package.json new file mode 100644 index 000000000..d77fbdcc6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/bob/package.json @@ -0,0 +1,4 @@ +{ + "name": "bob", + "main": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/main.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/main.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/package.json new file mode 100644 index 000000000..fb49ff022 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-deep-nested/index.html b/packages/wmr/test/fixtures/npm-deep-nested/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-deep-nested/index.js b/packages/wmr/test/fixtures/npm-deep-nested/index.js new file mode 100644 index 000000000..96c6c1006 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-deep-nested/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo/bar'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/fail.js b/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/fail.js new file mode 100644 index 000000000..00946192a --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/fail.js @@ -0,0 +1 @@ +export const value = "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/main.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/main.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/package.json new file mode 100644 index 000000000..d7e03663e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-default/-node_modules/foo/package.json @@ -0,0 +1,8 @@ +{ + "name": "foo", + "main": "./fail.js", + "module": "./fail.js", + "exports": { + "default": "./main.js" + } +} diff --git a/packages/wmr/test/fixtures/npm-export-default/index.html b/packages/wmr/test/fixtures/npm-export-default/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-default/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-export-default/index.js b/packages/wmr/test/fixtures/npm-export-default/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-default/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/fail.js b/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/fail.js new file mode 100644 index 000000000..00946192a --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/fail.js @@ -0,0 +1 @@ +export const value = "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/main.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/main.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/package.json new file mode 100644 index 000000000..8198da2cc --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import-first/-node_modules/foo/package.json @@ -0,0 +1,10 @@ +{ + "name": "foo", + "main": "./fail.js", + "module": "./fail.js", + "exports": { + "node": "./fail.js", + "default": "./fail.js", + "import": "./main.js" + } +} diff --git a/packages/wmr/test/fixtures/npm-export-import-first/index.html b/packages/wmr/test/fixtures/npm-export-import-first/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import-first/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-export-import-first/index.js b/packages/wmr/test/fixtures/npm-export-import-first/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import-first/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/fail.js b/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/fail.js new file mode 100644 index 000000000..00946192a --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/fail.js @@ -0,0 +1 @@ +export const value = "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/main.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/main.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/package.json new file mode 100644 index 000000000..96d456f13 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import/-node_modules/foo/package.json @@ -0,0 +1,8 @@ +{ + "name": "foo", + "main": "./fail.js", + "module": "./fail.js", + "exports": { + "import": "./main.js" + } +} diff --git a/packages/wmr/test/fixtures/npm-export-import/index.html b/packages/wmr/test/fixtures/npm-export-import/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-export-import/index.js b/packages/wmr/test/fixtures/npm-export-import/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-import/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/fail.js b/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/fail.js new file mode 100644 index 000000000..00946192a --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/fail.js @@ -0,0 +1 @@ +export const value = "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/main.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/main.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/package.json new file mode 100644 index 000000000..c9e7ccbc3 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-node/-node_modules/foo/package.json @@ -0,0 +1,8 @@ +{ + "name": "foo", + "main": "./fail.js", + "module": "./fail.js", + "exports": { + "node": "./main.js" + } +} diff --git a/packages/wmr/test/fixtures/npm-export-node/index.html b/packages/wmr/test/fixtures/npm-export-node/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-node/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-export-node/index.js b/packages/wmr/test/fixtures/npm-export-node/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-node/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/fail.js b/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/fail.js new file mode 100644 index 000000000..00946192a --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/fail.js @@ -0,0 +1 @@ +export const value = "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/main.js b/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/main.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/main.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/package.json new file mode 100644 index 000000000..372ec1869 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-sugar/-node_modules/foo/package.json @@ -0,0 +1,6 @@ +{ + "name": "foo", + "main": "./fail.js", + "module": "./fail.js", + "exports": "./main.js" +} diff --git a/packages/wmr/test/fixtures/npm-export-sugar/index.html b/packages/wmr/test/fixtures/npm-export-sugar/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-sugar/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-export-sugar/index.js b/packages/wmr/test/fixtures/npm-export-sugar/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-export-sugar/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/bar/index.js b/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/bar/index.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/bar/index.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/bar/package.json b/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/bar/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/bar/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/package.json new file mode 100644 index 000000000..de6dc1db0 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete-deep/-node_modules/foo/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/packages/wmr/test/fixtures/npm-incomplete-deep/index.html b/packages/wmr/test/fixtures/npm-incomplete-deep/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete-deep/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-incomplete-deep/index.js b/packages/wmr/test/fixtures/npm-incomplete-deep/index.js new file mode 100644 index 000000000..96c6c1006 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete-deep/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo/bar'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-incomplete/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-incomplete/-node_modules/foo/index.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete/-node_modules/foo/index.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-incomplete/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-incomplete/-node_modules/foo/package.json new file mode 100644 index 000000000..de6dc1db0 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete/-node_modules/foo/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/packages/wmr/test/fixtures/npm-incomplete/index.html b/packages/wmr/test/fixtures/npm-incomplete/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-incomplete/index.js b/packages/wmr/test/fixtures/npm-incomplete/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-incomplete/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-json/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-json/-node_modules/foo/index.js new file mode 100644 index 000000000..1e91128a5 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-json/-node_modules/foo/index.js @@ -0,0 +1 @@ +exports.value = require('./package.json').result; diff --git a/packages/wmr/test/fixtures/npm-json/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-json/-node_modules/foo/package.json new file mode 100644 index 000000000..b59cb88ed --- /dev/null +++ b/packages/wmr/test/fixtures/npm-json/-node_modules/foo/package.json @@ -0,0 +1,5 @@ +{ + "name": "foo", + "main": "./index.js", + "result": "it works" +} diff --git a/packages/wmr/test/fixtures/npm-json/index.html b/packages/wmr/test/fixtures/npm-json/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-json/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-json/index.js b/packages/wmr/test/fixtures/npm-json/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-json/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-main-scoped/-node_modules/@foo/bar/index.js b/packages/wmr/test/fixtures/npm-main-scoped/-node_modules/@foo/bar/index.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main-scoped/-node_modules/@foo/bar/index.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-main-scoped/-node_modules/@foo/bar/package.json b/packages/wmr/test/fixtures/npm-main-scoped/-node_modules/@foo/bar/package.json new file mode 100644 index 000000000..19edfe3e7 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main-scoped/-node_modules/@foo/bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "@foo/bar", + "main": "./index.js" +} diff --git a/packages/wmr/test/fixtures/npm-main-scoped/index.html b/packages/wmr/test/fixtures/npm-main-scoped/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main-scoped/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-main-scoped/index.js b/packages/wmr/test/fixtures/npm-main-scoped/index.js new file mode 100644 index 000000000..bf40293ca --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main-scoped/index.js @@ -0,0 +1,3 @@ +import { value } from '@foo/bar'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-main/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-main/-node_modules/foo/index.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main/-node_modules/foo/index.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-main/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-main/-node_modules/foo/package.json new file mode 100644 index 000000000..c3826c1c6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./index.js" +} diff --git a/packages/wmr/test/fixtures/npm-main/index.html b/packages/wmr/test/fixtures/npm-main/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-main/index.js b/packages/wmr/test/fixtures/npm-main/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-main/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-module/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-module/-node_modules/foo/index.js new file mode 100644 index 000000000..00946192a --- /dev/null +++ b/packages/wmr/test/fixtures/npm-module/-node_modules/foo/index.js @@ -0,0 +1 @@ +export const value = "it doesn't work"; diff --git a/packages/wmr/test/fixtures/npm-module/-node_modules/foo/module.js b/packages/wmr/test/fixtures/npm-module/-node_modules/foo/module.js new file mode 100644 index 000000000..2c47d2b98 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-module/-node_modules/foo/module.js @@ -0,0 +1 @@ +export const value = 'it works'; diff --git a/packages/wmr/test/fixtures/npm-module/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-module/-node_modules/foo/package.json new file mode 100644 index 000000000..29f6d3b78 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-module/-node_modules/foo/package.json @@ -0,0 +1,5 @@ +{ + "name": "foo", + "main": "./index.js", + "module": "./module.js" +} diff --git a/packages/wmr/test/fixtures/npm-module/index.html b/packages/wmr/test/fixtures/npm-module/index.html new file mode 100644 index 000000000..9cfb45a0e --- /dev/null +++ b/packages/wmr/test/fixtures/npm-module/index.html @@ -0,0 +1,2 @@ +

it doesn't work

+ diff --git a/packages/wmr/test/fixtures/npm-module/index.js b/packages/wmr/test/fixtures/npm-module/index.js new file mode 100644 index 000000000..850e1b5f6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-module/index.js @@ -0,0 +1,3 @@ +import { value } from 'foo'; + +document.querySelector('h1').textContent = value; diff --git a/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/foo.css b/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/foo.css new file mode 100644 index 000000000..818d40628 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/foo.css @@ -0,0 +1,3 @@ +h1 { + color: red; +} diff --git a/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/index.js b/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/index.js new file mode 100644 index 000000000..24cdde51b --- /dev/null +++ b/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/index.js @@ -0,0 +1 @@ +export { value } from 'fail'; diff --git a/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/package.json b/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/package.json new file mode 100644 index 000000000..c3826c1c6 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-styles/-node_modules/foo/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "main": "./index.js" +} diff --git a/packages/wmr/test/fixtures/npm-styles/index.html b/packages/wmr/test/fixtures/npm-styles/index.html new file mode 100644 index 000000000..b7f4c6e06 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-styles/index.html @@ -0,0 +1,2 @@ +

this should be red

+ diff --git a/packages/wmr/test/fixtures/npm-styles/index.js b/packages/wmr/test/fixtures/npm-styles/index.js new file mode 100644 index 000000000..3201c7685 --- /dev/null +++ b/packages/wmr/test/fixtures/npm-styles/index.js @@ -0,0 +1 @@ +import 'foo/foo.css'; diff --git a/packages/wmr/test/npm.test.js b/packages/wmr/test/npm.test.js new file mode 100644 index 000000000..d4b5bd966 --- /dev/null +++ b/packages/wmr/test/npm.test.js @@ -0,0 +1,382 @@ +import path from 'path'; +import { + getOutput, + loadFixture, + runWmr, + runWmrFast, + serveStatic, + setupTest, + teardown, + waitForMessage, + waitForPass, + withLog +} from './test-helpers.js'; + +jest.setTimeout(30000); + +describe('node modules', () => { + describe('development', () => { + /** @type {TestEnv} */ + let env; + /** @type {WmrInstance} */ + let instance; + + beforeEach(async () => { + env = await setupTest(); + }); + + afterEach(async () => { + await teardown(env); + instance.close(); + }); + + it('should resolve "main" field', async () => { + await loadFixture('npm-main', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve "module" field', async () => { + await loadFixture('npm-module', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve scoped pacakges field', async () => { + await loadFixture('npm-main-scoped', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should load *.json files', async () => { + await loadFixture('npm-json', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve assets', async () => { + await loadFixture('npm-styles', env); + instance = await runWmrFast(env.tmp.path); + await getOutput(env, instance); + + await withLog(instance.output, async () => { + await waitForPass(async () => { + const color = await env.page.$eval('h1', el => getComputedStyle(el).color); + expect(color).toBe('rgb(255, 0, 0)'); + }); + }); + }); + + describe('legacy', () => { + it('should resolve deep "main" field', async () => { + await loadFixture('npm-deep-main', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve deep "module" field', async () => { + await loadFixture('npm-deep-module', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve deep sub packages as package', async () => { + await loadFixture('npm-deep-nested', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve to "index.js" if neither "main", "module" or "exports" is present', async () => { + await loadFixture('npm-incomplete', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve to "index.js" if neither "main", "module" or "exports" is present in deep import', async () => { + await loadFixture('npm-incomplete-deep', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + }); + + describe('browser field', () => { + it('should resolve relative "browser" field', async () => { + await loadFixture('npm-browser-bare-relative', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve bare "browser" field', async () => { + await loadFixture('npm-browser-bare-bare', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve relative import with relative "browser" field', async () => { + await loadFixture('npm-browser-relative-relative', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve relative import with bare "browser" field', async () => { + await loadFixture('npm-browser-relative-bare', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve deep relative "browser" field', async () => { + await loadFixture('npm-browser-deep-relative', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve deep bare "browser" field', async () => { + await loadFixture('npm-browser-deep-bare', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + }); + + describe('"exports" field', () => { + it('should resolve `exports: "./foo.js"`', async () => { + await loadFixture('npm-export-sugar', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve `exports: { node: "./foo.js" }`', async () => { + await loadFixture('npm-export-node', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve `exports: { import: "./foo.js" }`', async () => { + await loadFixture('npm-export-import', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve `exports: { default: "./foo.js" }`', async () => { + await loadFixture('npm-export-default', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should resolve "import" first', async () => { + await loadFixture('npm-export-import-first', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + }); + + describe('commonjs', () => { + it('should resolve "module.exports = ..."', async () => { + await loadFixture('npm-commonjs-default', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should inline proxy modules based on `process.env.NODE_ENV`', async () => { + await loadFixture('npm-commonjs-proxy', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/This is development/); + }); + }); + + it('should inline single top level function iife', async () => { + await loadFixture('npm-commonjs-iife', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should inline single top level function iife #2', async () => { + await loadFixture('npm-commonjs-iife-2', env); + instance = await runWmrFast(env.tmp.path); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + }); + + describe('auto install', () => { + it('should install package', async () => { + await loadFixture('npm-auto-install', env); + instance = await runWmrFast(env.tmp.path, '--autoInstall', { env: { DISABLE_LOCAL_NPM: true } }); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + it('should install versioned package', async () => { + await loadFixture('npm-auto-install-version', env); + instance = await runWmrFast(env.tmp.path, '--autoInstall', { env: { DISABLE_LOCAL_NPM: true } }); + await withLog(instance.output, async () => { + const text = await getOutput(env, instance); + expect(text).toMatch(/it works/); + }); + }); + + // eslint-disable-next-line jest/expect-expect + it('should fetch package from --registry', async () => { + await loadFixture('npm-auto-install-version', env); + instance = await runWmrFast(env.tmp.path, '--autoInstall', '--registry', 'https://example.com', { + env: { DISABLE_LOCAL_NPM: true } + }); + await getOutput(env, instance); + await waitForMessage(instance.output, /500.*https:\/\/example\.com\/smoldash/); + }); + + it('should load CSS from installed package', async () => { + await loadFixture('npm-auto-install-css', env); + instance = await runWmrFast(env.tmp.path, '--autoInstall', { env: { DISABLE_LOCAL_NPM: true } }); + await getOutput(env, instance); + + await withLog(instance.output, async () => { + await waitForPass(async () => { + const color = await env.page.$eval('a', el => getComputedStyle(el).color); + expect(color).toBe('rgb(17, 139, 238)'); + }); + }); + }); + + it('should load CSS from installed package #2', async () => { + await loadFixture('npm-auto-install-css-2', env); + instance = await runWmrFast(env.tmp.path, '--autoInstall', { env: { DISABLE_LOCAL_NPM: true } }); + await getOutput(env, instance); + + await withLog(instance.output, async () => { + await waitForPass(async () => { + const color = await env.page.$eval('a', el => getComputedStyle(el).color); + expect(color).toBe('rgb(17, 139, 238)'); + }); + }); + }); + }); + }); + + describe('production', () => { + /** @type {TestEnv} */ + let env; + /** @type {WmrInstance} */ + let instance; + /** @type {(()=>void)[]} */ + let cleanup = []; + + beforeEach(async () => { + env = await setupTest(); + }); + + afterEach(async () => { + await teardown(env); + instance.close(); + await Promise.all(cleanup.map(c => Promise.resolve().then(c))); + cleanup.length = 0; + }); + + it('should bundle npm deps', async () => { + await loadFixture('npm-main', env); + instance = await runWmr(env.tmp.path, 'build'); + + expect(await instance.done).toEqual(0); + const { address, stop } = serveStatic(path.join(env.tmp.path, 'dist')); + cleanup.push(stop); + await env.page.goto(address, { + waitUntil: ['networkidle0', 'load'] + }); + + await withLog(instance.output, async () => { + const text = await env.page.content(); + expect(text).toMatch(/it works/); + }); + }); + + it('should load assets', async () => { + await loadFixture('npm-styles', env); + instance = await runWmr(env.tmp.path, 'build'); + + expect(await instance.done).toEqual(0); + const { address, stop } = serveStatic(path.join(env.tmp.path, 'dist')); + cleanup.push(stop); + await env.page.goto(address, { + waitUntil: ['networkidle0', 'load'] + }); + + await withLog(instance.output, async () => { + await waitForPass(async () => { + const color = await env.page.$eval('h1', el => getComputedStyle(el).color); + expect(color).toBe('rgb(255, 0, 0)'); + }); + }); + }); + }); +}); diff --git a/packages/wmr/test/production.test.js b/packages/wmr/test/production.test.js index 2948afec2..1032c6ab4 100644 --- a/packages/wmr/test/production.test.js +++ b/packages/wmr/test/production.test.js @@ -60,36 +60,37 @@ describe('production', () => { it('should allow overwriting url loader', async () => { await loadFixture('overwrite-loader-url', env); instance = await runWmr(env.tmp.path, 'build'); - const code = await instance.done; - const output = instance.output.join('\n'); - console.log(output); - expect(code).toEqual(0); + await withLog(instance.output, async () => { + const code = await instance.done; + expect(code).toEqual(0); - const { address, stop } = serveStatic(path.join(env.tmp.path, 'dist')); - cleanup.push(stop); + const { address, stop } = serveStatic(path.join(env.tmp.path, 'dist')); + cleanup.push(stop); - await env.page.goto(address, { - waitUntil: ['networkidle0', 'load'] - }); + await env.page.goto(address, { + waitUntil: ['networkidle0', 'load'] + }); - const text = await env.page.content(); - expect(text).toMatch(/my-url: \/assets\/foo\..*\.svg/); - expect(text).toMatch(/url: \/assets\/foo\..*\.svg/); - expect(text).toMatch(/fallback: \/assets\/foo\..*\.svg/); + const text = await env.page.content(); + expect(text).toMatch(/my-url: \/assets\/foo\..*\.svg/); + expect(text).toMatch(/url: \/assets\/foo\..*\.svg/); + expect(text).toMatch(/fallback: \/assets\/foo\..*\.svg/); + }); }); it('should show all generated files in cli output', async () => { await loadFixture('file-import', env); instance = await runWmr(env.tmp.path, 'build'); const code = await instance.done; - const output = instance.output; - console.log(output); - expect(code).toEqual(0); + await withLog(instance.output, async () => { + const output = instance.output; + expect(code).toEqual(0); - const stats = output.slice(output.findIndex(line => /Wrote.*to disk/.test(line))); - expect(stats.join('\n')).toMatch(/img\..*\.jpg/); + const stats = output.slice(output.findIndex(line => /Wrote.*to disk/.test(line))); + expect(stats.join('\n')).toMatch(/img\..*\.jpg/); + }); }); it('should support base64 in HTML', async () => { @@ -473,7 +474,7 @@ describe('production', () => { for (const d of ['dist', 'node_modules', '.cache']) { await fs.rmdir(path.join(env.tmp.path, d), { recursive: true }); } - instance = await runWmr(env.tmp.path, 'build', '--prerender'); + instance = await runWmr(env.tmp.path, 'build', '--prerender', '--autoInstall'); const code = await instance.done; const output = instance.output.join('\n'); console.log(output); @@ -571,62 +572,64 @@ describe('production', () => { describe('CSS Asset Graph Optimization', () => { it('should hoist dynamically imported CSS into unconditionally loaded parent', async () => { await loadFixture('css', env); - instance = await runWmr(env.tmp.path, 'build'); - const code = await instance.done; - console.info(instance.output.join('\n')); - expect(code).toBe(0); + instance = await runWmr(env.tmp.path, 'build', '--autoInstall'); - const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); - const assets = await readdir('dist/assets'); - const chunks = await readdir('dist/chunks'); + await withLog(instance.output, async () => { + const code = await instance.done; + expect(code).toBe(0); - expect(assets).toEqual([expect.stringMatching(/^style\.\w+\.css$/)]); - expect(chunks).toEqual([expect.stringMatching(/^index\.\w+\.js$/), expect.stringMatching(/^index\.\w+\.js$/)]); + const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); + const assets = await readdir('dist/assets'); + const chunks = await readdir('dist/chunks'); - const css = await fs.readFile(path.join(env.tmp.path, 'dist/assets', assets[0]), 'utf-8'); - // ensure all the CSS got merged: - expect(css).toMatch(/body\s*,\s*html/); - expect(css).toMatch(/\.app_\w+/); - expect(css).toMatch(/\.home_\w+/); - expect(css).toMatch(/\.profile_\w+/); + expect(assets).toEqual([expect.stringMatching(/^style\.\w+\.css$/)]); + expect(chunks).toEqual([expect.stringMatching(/^index\.\w+\.js$/), expect.stringMatching(/^index\.\w+\.js$/)]); - const { address, stop } = serveStatic(path.join(env.tmp.path, 'dist')); - cleanup.push(stop); + const css = await fs.readFile(path.join(env.tmp.path, 'dist/assets', assets[0]), 'utf-8'); + // ensure all the CSS got merged: + expect(css).toMatch(/body\s*,\s*html/); + expect(css).toMatch(/\.app_\w+/); + expect(css).toMatch(/\.home_\w+/); + expect(css).toMatch(/\.profile_\w+/); - const logs = []; - function log(type, text) { - logs.push(`${type}: ${text}`); - console.log(` ${type}: ${text}`); - } - env.page.on('console', m => log(m.type(), m.text())); - env.page.on('error', err => log('error', err)); - env.page.on('pageerror', err => log('page error', err)); + const { address, stop } = serveStatic(path.join(env.tmp.path, 'dist')); + cleanup.push(stop); - const requests = []; - await env.page.setCacheEnabled(false); - await env.page.setRequestInterception(true); - page.on('request', req => { - requests.push(req.url().replace(/^https?:\/\/[^/]+/, '')); - req.continue(); - }); + const logs = []; + function log(type, text) { + logs.push(`${type}: ${text}`); + console.log(` ${type}: ${text}`); + } + env.page.on('console', m => log(m.type(), m.text())); + env.page.on('error', err => log('error', err)); + env.page.on('pageerror', err => log('page error', err)); + + const requests = []; + await env.page.setCacheEnabled(false); + await env.page.setRequestInterception(true); + page.on('request', req => { + requests.push(req.url().replace(/^https?:\/\/[^/]+/, '')); + req.continue(); + }); - await env.page.goto(address, { waitUntil: ['domcontentloaded', 'networkidle2'] }); - expect(await env.page.content()).toMatch(/This is the home page/); + await env.page.goto(address, { waitUntil: ['domcontentloaded', 'networkidle2'] }); + expect(await env.page.content()).toMatch(/This is the home page/); - expect(logs).toEqual([]); - expect(requests.filter(url => /\.css$/.test(url))).toEqual([ - expect.stringMatching(/^\/assets\/style\.\w+\.css$/) - ]); + expect(logs).toEqual([]); + expect(requests.filter(url => /\.css$/.test(url))).toEqual([ + expect.stringMatching(/^\/assets\/style\.\w+\.css$/) + ]); - logs.length = requests.length = 0; + logs.length = requests.length = 0; - await env.page.goto(address + '/profile/foo', { waitUntil: ['domcontentloaded', 'networkidle2'] }); - expect(await env.page.content()).toMatch(/This is the profile page for foo/); + await env.page.goto(address + '/profile/foo', { waitUntil: ['domcontentloaded', 'networkidle2'] }); + expect(await env.page.content()).toMatch(/This is the profile page for foo/); - expect(logs).toEqual([]); - expect(requests.filter(url => /\.css$/.test(url))).toEqual([ - expect.stringMatching(/^\/assets\/style\.\w+\.css$/) - ]); + expect(logs).toEqual([]); + expect(requests.filter(url => /\.css$/.test(url))).toEqual([ + expect.stringMatching(/^\/assets\/style\.\w+\.css$/) + ]); + }); }); it('should merge duplicate CSS imports', async () => { @@ -663,9 +666,9 @@ describe('production', () => { it('should hoist entry CSS into HTML tag', async () => { await loadFixture('css-entry', env); instance = await runWmr(env.tmp.path, 'build'); - const code = await instance.done; await withLog(instance.output, async () => { + const code = await instance.done; expect(code).toBe(0); const files = (await fs.readdir(path.join(env.tmp.path, 'dist', 'assets'))).filter(f => f[0] !== '.'); @@ -687,75 +690,84 @@ describe('production', () => { it('should respect `config.publicPath` value', async () => { await loadFixture('publicpath', env); instance = await runWmr(env.tmp.path, 'build'); - const code = await instance.done; - console.info(instance.output.join('\n')); - expect(code).toBe(0); + await withLog(instance.output, async () => { + const code = await instance.done; + expect(code).toBe(0); - const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); + const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); - const assets = await readdir('dist/assets'); - const chunks = await readdir('dist/chunks'); - const roots = await readdir('dist'); + const assets = await readdir('dist/assets'); + const chunks = await readdir('dist/chunks'); + const roots = await readdir('dist'); - expect(assets).toEqual([expect.stringMatching(/^index\.\w+\.css$/), expect.stringMatching(/^math\.\w+\.css$/)]); + expect(assets).toEqual([expect.stringMatching(/^index\.\w+\.css$/), expect.stringMatching(/^math\.\w+\.css$/)]); - expect(chunks).toEqual([expect.stringMatching(/^constants\.\w+\.js$/), expect.stringMatching(/^math\.\w+\.js$/)]); + expect(chunks).toEqual([ + expect.stringMatching(/^constants\.\w+\.js$/), + expect.stringMatching(/^math\.\w+\.js$/) + ]); - expect(roots).toEqual(['assets', 'chunks', expect.stringMatching(/^index\.\w+\.js$/), 'index.html']); + expect(roots).toEqual(['assets', 'chunks', expect.stringMatching(/^index\.\w+\.js$/), 'index.html']); - const html = await fs.readFile(path.join(env.tmp.path, 'dist', 'index.html'), 'utf8'); - const math = await fs.readFile(path.join(env.tmp.path, 'dist', 'chunks', chunks[1]), 'utf8'); - const main = await fs.readFile(path.join(env.tmp.path, 'dist', roots[2]), 'utf8'); + const html = await fs.readFile(path.join(env.tmp.path, 'dist', 'index.html'), 'utf8'); + const math = await fs.readFile(path.join(env.tmp.path, 'dist', 'chunks', chunks[1]), 'utf8'); + const main = await fs.readFile(path.join(env.tmp.path, 'dist', roots[2]), 'utf8'); - // https://cdn.example.com/assets/math.d41e7373.css - expect(math).toMatch(`("https://cdn.example.com/assets/${assets[1]}")`); - expect(math).toMatch(`import("./${chunks[0]}")`); + // https://cdn.example.com/assets/math.d41e7373.css + expect(math).toMatch(`("https://cdn.example.com/assets/${assets[1]}")`); + expect(math).toMatch(`import("./${chunks[0]}")`); - // (preload) https://cdn.example.com/assets/math.d41e7373.css - expect(main).toMatch(`$w_s$("https://cdn.example.com/assets/${assets[1]}")`); + // (preload) https://cdn.example.com/assets/math.d41e7373.css + expect(main).toMatch(`$w_s$("https://cdn.example.com/assets/${assets[1]}")`); - // HTML stylesheet: https://cdn.example.com/assets/index.0544f0a6.css - expect(html).toMatch(`href="https://cdn.example.com/assets/${assets[0]}"`); + // HTML stylesheet: https://cdn.example.com/assets/index.0544f0a6.css + expect(html).toMatch(`href="https://cdn.example.com/assets/${assets[0]}"`); - // HTML script: https://cdn.example.com/assets/index.0544f0a6.css - expect(html).toMatch(`src="https://cdn.example.com/${roots[2]}"`); + // HTML script: https://cdn.example.com/assets/index.0544f0a6.css + expect(html).toMatch(`src="https://cdn.example.com/${roots[2]}"`); + }); }); it('should respect `config.publicPath` value (ts)', async () => { await loadFixture('publicpath-typescript', env); instance = await runWmr(env.tmp.path, 'build'); - const code = await instance.done; - console.info(instance.output.join('\n')); - expect(code).toBe(0); - const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); + await withLog(instance.output, async () => { + const code = await instance.done; + expect(code).toBe(0); - const assets = await readdir('dist/assets'); - const chunks = await readdir('dist/chunks'); - const roots = await readdir('dist'); + const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); + + const assets = await readdir('dist/assets'); + const chunks = await readdir('dist/chunks'); + const roots = await readdir('dist'); - expect(assets).toEqual([expect.stringMatching(/^index\.\w+\.css$/), expect.stringMatching(/^math\.\w+\.css$/)]); + expect(assets).toEqual([expect.stringMatching(/^index\.\w+\.css$/), expect.stringMatching(/^math\.\w+\.css$/)]); - expect(chunks).toEqual([expect.stringMatching(/^constants\.\w+\.js$/), expect.stringMatching(/^math\.\w+\.js$/)]); + expect(chunks).toEqual([ + expect.stringMatching(/^constants\.\w+\.js$/), + expect.stringMatching(/^math\.\w+\.js$/) + ]); - expect(roots).toEqual(['assets', 'chunks', expect.stringMatching(/^index\.\w+\.js$/), 'index.html']); + expect(roots).toEqual(['assets', 'chunks', expect.stringMatching(/^index\.\w+\.js$/), 'index.html']); - const html = await fs.readFile(path.join(env.tmp.path, 'dist', 'index.html'), 'utf8'); - const math = await fs.readFile(path.join(env.tmp.path, 'dist', 'chunks', chunks[1]), 'utf8'); - const main = await fs.readFile(path.join(env.tmp.path, 'dist', roots[2]), 'utf8'); + const html = await fs.readFile(path.join(env.tmp.path, 'dist', 'index.html'), 'utf8'); + const math = await fs.readFile(path.join(env.tmp.path, 'dist', 'chunks', chunks[1]), 'utf8'); + const main = await fs.readFile(path.join(env.tmp.path, 'dist', roots[2]), 'utf8'); - // https://cdn.example.com/assets/math.d41e7373.css - expect(math).toMatch(`("https://cdn.example.com/assets/${assets[1]}")`); - expect(math).toMatch(`import("./${chunks[0]}")`); + // https://cdn.example.com/assets/math.d41e7373.css + expect(math).toMatch(`("https://cdn.example.com/assets/${assets[1]}")`); + expect(math).toMatch(`import("./${chunks[0]}")`); - // (preload) https://cdn.example.com/assets/math.d41e7373.css - expect(main).toMatch(`$w_s$("https://cdn.example.com/assets/${assets[1]}")`); + // (preload) https://cdn.example.com/assets/math.d41e7373.css + expect(main).toMatch(`$w_s$("https://cdn.example.com/assets/${assets[1]}")`); - // HTML stylesheet: https://cdn.example.com/assets/index.0544f0a6.css - expect(html).toMatch(`href="https://cdn.example.com/assets/${assets[0]}"`); + // HTML stylesheet: https://cdn.example.com/assets/index.0544f0a6.css + expect(html).toMatch(`href="https://cdn.example.com/assets/${assets[0]}"`); - // HTML script: https://cdn.example.com/assets/index.0544f0a6.css - expect(html).toMatch(`src="https://cdn.example.com/${roots[2]}"`); + // HTML script: https://cdn.example.com/assets/index.0544f0a6.css + expect(html).toMatch(`src="https://cdn.example.com/${roots[2]}"`); + }); }); }); @@ -826,35 +838,41 @@ describe('production', () => { it('should support prerendering json', async () => { await loadFixture('prod-prerender-json', env); instance = await runWmr(env.tmp.path, 'build', '--prerender'); - const code = await instance.done; - console.info(instance.output.join('\n')); - expect(code).toBe(0); - const indexHtml = path.join(env.tmp.path, 'dist', 'index.html'); - const index = await fs.readFile(indexHtml, 'utf8'); - expect(index).toMatch(/{"foo":42,"bar":"bar"}/); + await withLog(instance.output, async () => { + const code = await instance.done; + expect(code).toBe(0); + + const indexHtml = path.join(env.tmp.path, 'dist', 'index.html'); + const index = await fs.readFile(indexHtml, 'utf8'); + expect(index).toMatch(/{"foo":42,"bar":"bar"}/); + }); }); it('should not crash during prerendering', async () => { await loadFixture('prerender-crash', env); instance = await runWmr(env.tmp.path, 'build', '--prerender'); - const code = await instance.done; - console.info(instance.output.join('\n')); - expect(instance.output.join('\n')).toMatch(/Error: fail/); - // Check if stack trace is present - expect(instance.output.join('\n')).toMatch(/^\s+at\s\w+/gm); - expect(code).toBe(1); + + await withLog(instance.output, async () => { + const code = await instance.done; + expect(instance.output.join('\n')).toMatch(/Error: fail/); + // Check if stack trace is present + expect(instance.output.join('\n')).toMatch(/^\s+at\s\w+/gm); + expect(code).toBe(1); + }); }); it('config should support supplying additional links to prerender', async () => { await loadFixture('prerender-additional-links', env); instance = await runWmr(env.tmp.path, 'build', '--prerender'); - const code = await instance.done; - console.info(instance.output.join('\n')); - expect(instance.output.join('\n')).toMatch(/Prerendered 2 pages/i); - expect(code).toBe(0); - expect(await fs.access(path.join(env.tmp.path, 'dist', 'non-existent-link', 'index.html'))).toBeUndefined(); + await withLog(instance.output, async () => { + const code = await instance.done; + expect(instance.output.join('\n')).toMatch(/Prerendered 2 pages/i); + expect(code).toBe(0); + + expect(await fs.access(path.join(env.tmp.path, 'dist', 'non-existent-link', 'index.html'))).toBeUndefined(); + }); }); it('config should throw if no prerender function is exported', async () => { @@ -927,9 +945,12 @@ describe('production', () => { describe('Prerender', () => { it('should remove search params', async () => { await loadFixture('prod-routes', env); - instance = await runWmr(env.tmp.path, 'build', '--prerender'); - const code = await instance.done; - expect(code).toBe(0); + + instance = await runWmr(env.tmp.path, 'build', '--prerender', '--autoInstall'); + await withLog(instance.output, async () => { + const code = await instance.done; + expect(code).toBe(0); + }); const readdir = async f => (await fs.readdir(path.join(env.tmp.path, f))).filter(f => f[0] !== '.'); diff --git a/packages/wmr/tools/run-fixture.js b/packages/wmr/tools/run-fixture.js index 85deb0802..1499e8429 100644 --- a/packages/wmr/tools/run-fixture.js +++ b/packages/wmr/tools/run-fixture.js @@ -48,6 +48,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); spawn(process.execPath, ['src/cli.js', command, '--cwd', 'test/fixtures/' + name, ...rest], { stdio: 'inherit', env: { + ...process.env, // Package "application-config-path" needs this (required by devcert) HOME: process.env.HOME, FORCE_COLOR: '1', diff --git a/packages/wmr/types.d.ts b/packages/wmr/types.d.ts index 9a3f5281e..dc0ea2bd0 100644 --- a/packages/wmr/types.d.ts +++ b/packages/wmr/types.d.ts @@ -79,6 +79,15 @@ declare module 'wmr' { visualize: boolean; debug: boolean; customRoutes: string[]; + /** + * Automatically fetch missing npm packages from npm registry. + * This is only intended for quick prototyping. + */ + autoInstall: boolean; + /** + * NPM registry url to use if `--autoInstall` is enabled + */ + registry: string; } export type BuildError = RollupError & { clientMessage?: string }; diff --git a/yarn.lock b/yarn.lock index d69450745..6acdbe5e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1341,7 +1341,7 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-import-assertions@^1.7.2: +acorn-import-assertions@^1.7.2, acorn-import-assertions@^1.7.5: version "1.7.6" resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz#580e3ffcae6770eebeec76c3b9723201e9d01f78" integrity sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA== @@ -1356,6 +1356,11 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + acorn-logical-assignment@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/acorn-logical-assignment/-/acorn-logical-assignment-0.1.4.tgz#1a143a21f022e1707b2bc82f587ae2943f0a570e" @@ -1395,6 +1400,11 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" + integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -9642,3 +9652,17 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zecorn@^0.9.5: + version "0.9.5" + resolved "https://registry.yarnpkg.com/zecorn/-/zecorn-0.9.5.tgz#cd8202f38582e3b6ba1738623e13c0307723ef36" + integrity sha512-XYSyr8q2UQBXRVWue41LgDGSWrCY1s6quGCk5WjiVyva61Qz1NTpU5/JKHmPWXv5czpj8RydUfJalc/TRNbFgA== + dependencies: + acorn "^8.5.0" + acorn-class-fields "^1.0.0" + acorn-import-assertions "^1.7.5" + acorn-jsx "^5.3.2" + acorn-logical-assignment "^0.1.4" + acorn-private-methods "^1.0.0" + acorn-static-class-features "^1.0.0" + sourcemap-codec "^1.4.8"