UNPKG

snowpack

Version:

The ESM-powered frontend build tool. Fast, lightweight, unbundled.

392 lines (391 loc) 20.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getLinkedUrl = void 0; const crypto_1 = __importDefault(require("crypto")); const esinstall_1 = require("esinstall"); const find_cache_dir_1 = __importDefault(require("find-cache-dir")); const find_up_1 = __importDefault(require("find-up")); const fs_1 = require("fs"); const colors = __importStar(require("kleur/colors")); const mkdirp_1 = __importDefault(require("mkdirp")); const p_queue_1 = __importDefault(require("p-queue")); const path_1 = __importDefault(require("path")); const rimraf_1 = __importDefault(require("rimraf")); const slash_1 = __importDefault(require("slash")); const file_urls_1 = require("../build/file-urls"); const logger_1 = require("../logger"); const rewrite_imports_1 = require("../rewrite-imports"); const scan_imports_1 = require("../scan-imports"); const util_1 = require("../util"); const local_install_1 = require("./local-install"); const PROJECT_CACHE_DIR = find_cache_dir_1.default({ name: 'snowpack' }) || // If `projectCacheDir()` is null, no node_modules directory exists. // Use the current path (hashed) to create a cache entry in the global cache instead. // Because this is specifically for dependencies, this fallback should rarely be used. path_1.default.join(util_1.GLOBAL_CACHE_DIR, crypto_1.default.createHash('md5').update(process.cwd()).digest('hex')); const NEVER_PEER_PACKAGES = [ '@babel/runtime', '@babel/runtime-corejs3', 'babel-runtime', 'dom-helpers', 'es-abstract', 'node-fetch', 'whatwg-fetch', 'tslib', '@ant-design/icons-svg', ]; const memoizedResolve = {}; function getRootPackageDirectory(loc) { const parts = loc.split('node_modules'); if (parts.length === 1) { return undefined; } const packageParts = parts.pop().split(path_1.default.sep).filter(Boolean); const packageRoot = path_1.default.join(parts.join('node_modules'), 'node_modules'); if (packageParts[0].startsWith('@')) { return path_1.default.join(packageRoot, packageParts[0], packageParts[1]); } else { return path_1.default.join(packageRoot, packageParts[0]); } } // A bit of a hack: we keep this in local state and populate it // during the "prepare" call. Useful so that we don't need to pass // this implementation detail around outside of this interface. // Can't add it to the exported interface due to TS. let config; const allPackageImports = {}; const allSymlinkImports = {}; const allKnownSpecs = new Set(); const inProgressBuilds = new p_queue_1.default({ concurrency: 1 }); let hasWorkspaceWarningFired = false; function getLinkedUrl(builtUrl) { return allSymlinkImports[builtUrl]; } exports.getLinkedUrl = getLinkedUrl; /** * Local Package Source: A generic interface through which Snowpack * interacts with esinstall and your locally installed dependencies. */ exports.default = { async load(id, isSSR) { const packageImport = allPackageImports[id]; if (!packageImport) { return; } const { loc, entrypoint, packageName, packageVersion } = packageImport; let { installDest } = packageImport; if (isSSR && fs_1.existsSync(installDest + '-ssr')) { installDest += '-ssr'; } // Wait for any in progress builds to complete, in case they've // cleared out the directory that you're trying to read out of. await inProgressBuilds.onIdle(); let packageCode = await fs_1.promises.readFile(loc, 'utf8'); const imports = []; const type = path_1.default.extname(loc); if (!(type === '.js' || type === '.html' || type === '.css')) { return { contents: packageCode, imports }; } const packageImportMap = JSON.parse(await fs_1.promises.readFile(path_1.default.join(installDest, 'import-map.json'), 'utf8')); const resolveImport = async (spec) => { if (util_1.isRemoteUrl(spec)) { return spec; } if (spec.startsWith('/')) { return spec; } // These are a bit tricky: relative paths within packages always point to // relative files within the built package (ex: 'pkg/common/XXX-hash.js`). // We resolve these to a new kind of "internal" import URL that's different // from the normal, flattened URL for public imports. if (util_1.isPathImport(spec)) { const newLoc = path_1.default.resolve(path_1.default.dirname(loc), spec); const resolvedSpec = slash_1.default(path_1.default.relative(installDest, newLoc)); const publicImportEntry = Object.entries(packageImportMap.imports).find(([, v]) => v === './' + resolvedSpec); // If this matches the destination of a public package import, resolve to it. if (publicImportEntry) { spec = publicImportEntry[0]; return await this.resolvePackageImport(entrypoint, spec, config); } // Otherwise, create a relative import ID for the internal file. const relativeImportId = path_1.default.posix.join(`${packageName}.v${packageVersion}`, resolvedSpec); allPackageImports[relativeImportId] = { entrypoint: path_1.default.join(installDest, 'package.json'), loc: newLoc, installDest, packageVersion, packageName, }; return path_1.default.posix.join(config.buildOptions.metaUrlPath, 'pkg', relativeImportId); } // Otherwise, resolve this specifier as an external package. return await this.resolvePackageImport(entrypoint, spec, config); }; packageCode = await rewrite_imports_1.transformFileImports({ type, contents: packageCode }, async (spec) => { let resolvedImportUrl = await resolveImport(spec); const importExtName = path_1.default.posix.extname(resolvedImportUrl); const isProxyImport = importExtName && importExtName !== '.js' && importExtName !== '.mjs'; if (config.buildOptions.resolveProxyImports && isProxyImport) { resolvedImportUrl = resolvedImportUrl + '.proxy.js'; } imports.push(util_1.createInstallTarget(path_1.default.resolve(path_1.default.posix.join(config.buildOptions.metaUrlPath, 'pkg', id), resolvedImportUrl))); return resolvedImportUrl; }); return { contents: packageCode, imports }; }, modifyBuildInstallOptions({ installOptions, config: _config }) { config = config || _config; if (config.packageOptions.source !== 'local') { return installOptions; } installOptions.cwd = config.root; installOptions.rollup = config.packageOptions.rollup; installOptions.sourcemap = config.buildOptions.sourcemap; installOptions.polyfillNode = config.packageOptions.polyfillNode; installOptions.packageLookupFields = config.packageOptions.packageLookupFields; installOptions.packageExportLookupFields = config.packageOptions.packageExportLookupFields; return installOptions; }, // TODO: in build+watch, run prepare() // then, no import map // async prepare(commandOptions) { config = commandOptions.config; const DEV_DEPENDENCIES_DIR = path_1.default.join(PROJECT_CACHE_DIR, process.env.NODE_ENV || 'development'); const installDirectoryHashLoc = path_1.default.join(DEV_DEPENDENCIES_DIR, '.meta'); const installDirectoryHash = await fs_1.promises .readFile(installDirectoryHashLoc, 'utf-8') .catch(() => null); if (installDirectoryHash === 'v1') { logger_1.logger.debug(`Install directory ".meta" tag is up-to-date. Welcome back!`); return; } else if (installDirectoryHash) { logger_1.logger.info('Snowpack updated! Rebuilding your dependencies for the latest version of Snowpack...'); } else { logger_1.logger.info(`${colors.bold('Welcome to Snowpack!')} Because this is your first time running\n` + `this project${process.env.NODE_ENV === 'test' ? ` (mode: test)` : ``}, Snowpack needs to prepare your dependencies. This is a one-time step\n` + `and the results will be cached for the lifetime of your project. Please wait...`); } const installTargets = await scan_imports_1.getInstallTargets(config, config.packageOptions.knownEntrypoints); if (installTargets.length === 0) { logger_1.logger.info('No dependencies detected. Ready!'); return; } await Promise.all([...new Set(installTargets.map((t) => t.specifier))].map((spec) => { return this.resolvePackageImport(path_1.default.join(config.root, 'package.json'), spec, config); })); await inProgressBuilds.onIdle(); await mkdirp_1.default(path_1.default.dirname(installDirectoryHashLoc)); await fs_1.promises.writeFile(installDirectoryHashLoc, 'v1', 'utf-8'); logger_1.logger.info(colors.bold('Ready!')); return; }, async resolvePackageImport(source, spec, _config, importMap, depth = 0) { config = config || _config; // Imports in the same project should never change once resolved. Check the momized cache here to speed up faster repeat page loads. // NOTE(fks): This is mainly needed because `resolveEntrypoint` can be slow and blocking, which creates issues when many files // are loaded/resolved at once (ex: antd). If we can improve the performance there and make that async, this may no longer be // necessary. if (!importMap) { if (!memoizedResolve[source]) { memoizedResolve[source] = {}; } else if (memoizedResolve[source][spec]) { return memoizedResolve[source][spec]; } } const aliasEntry = util_1.findMatchingAliasEntry(config, spec); if (aliasEntry && aliasEntry.type === 'package') { const { from, to } = aliasEntry; spec = spec.replace(from, to); } const entrypoint = esinstall_1.resolveEntrypoint(spec, { cwd: path_1.default.dirname(source), packageLookupFields: [ 'snowpack:source', ...(_config.packageOptions.packageLookupFields || []), ], }); const specParts = spec.split('/'); let _packageName = specParts.shift(); if (_packageName === null || _packageName === void 0 ? void 0 : _packageName.startsWith('@')) { _packageName += '/' + specParts.shift(); } const isSymlink = !entrypoint.includes(path_1.default.join('node_modules', _packageName)); const isWithinRoot = config.workspaceRoot && entrypoint.startsWith(config.workspaceRoot); if (isSymlink && config.workspaceRoot && isWithinRoot) { const builtEntrypointUrls = file_urls_1.getBuiltFileUrls(entrypoint, config); const builtEntrypointUrl = slash_1.default(path_1.default.relative(config.workspaceRoot, builtEntrypointUrls[0])); allSymlinkImports[builtEntrypointUrl] = entrypoint; return path_1.default.posix.join(config.buildOptions.metaUrlPath, 'link', builtEntrypointUrl); } else if (isSymlink && config.workspaceRoot !== false && !hasWorkspaceWarningFired) { hasWorkspaceWarningFired = true; logger_1.logger.warn(colors.bold(`${spec}: Locally linked package detected outside of project root.\n`) + `If you are working in a workspace/monorepo, set your snowpack.config.js "workspaceRoot" to your workspace\n` + `directory to take advantage of fast HMR updates for linked packages. Otherwise, this package will be\n` + `cached until its package.json "version" changes. To silence this warning, set "workspaceRoot: false".`); } if (importMap) { if (importMap.imports[spec]) { return path_1.default.posix.join(config.buildOptions.metaUrlPath, 'pkg', importMap.imports[spec]); } throw new Error(`Unexpected: spec ${spec} not included in import map.`); } let rootPackageDirectory = getRootPackageDirectory(entrypoint); if (!rootPackageDirectory) { const rootPackageManifestLoc = await find_up_1.default('package.json', { cwd: entrypoint }); if (!rootPackageManifestLoc) { throw new Error(`Error resolving import ${spec}: No parent package.json found.`); } rootPackageDirectory = path_1.default.dirname(rootPackageManifestLoc); } const packageManifestLoc = path_1.default.join(rootPackageDirectory, 'package.json'); const packageManifestStr = await fs_1.promises.readFile(packageManifestLoc, 'utf8'); const packageManifest = JSON.parse(packageManifestStr); const packageName = packageManifest.name || _packageName; const packageVersion = packageManifest.version || 'unknown'; const DEV_DEPENDENCIES_DIR = path_1.default.join(PROJECT_CACHE_DIR, process.env.NODE_ENV || 'development'); const packageUID = packageName + '@' + packageVersion; const installDest = path_1.default.join(DEV_DEPENDENCIES_DIR, packageUID); allKnownSpecs.add(`${packageUID}:${spec}`); const newImportMap = await inProgressBuilds.add(async () => { // Look up the import map of the already-installed package. // If spec already exists, then this import map is valid. const lineBullet = colors.dim(depth === 0 ? '+' : '└──'.padStart(depth * 2 + 1, ' ')); let packageFormatted = spec + colors.dim('@' + packageVersion); const existingImportMapLoc = path_1.default.join(installDest, 'import-map.json'); const existingImportMap = (await fs_1.promises.stat(existingImportMapLoc).catch(() => null)) && JSON.parse(await fs_1.promises.readFile(existingImportMapLoc, 'utf8')); if (existingImportMap && existingImportMap.imports[spec]) { if (depth > 0) { logger_1.logger.info(`${lineBullet} ${packageFormatted} ${colors.dim(`(dedupe)`)}`); } return existingImportMap; } // Otherwise, kick off a new build to generate a fresh import map. logger_1.logger.info(`${lineBullet} ${packageFormatted}`); const installTargets = [...allKnownSpecs] .filter((spec) => spec.startsWith(packageUID)) .map((spec) => spec.substr(packageUID.length + 1)); // TODO: external should be a function in esinstall const externalPackages = [ ...Object.keys(packageManifest.dependencies || {}), ...Object.keys(packageManifest.devDependencies || {}), ...Object.keys(packageManifest.peerDependencies || {}), ].filter((ext) => ext !== _packageName && !NEVER_PEER_PACKAGES.includes(ext)); const installOptions = { dest: installDest, cwd: packageManifestLoc, env: { NODE_ENV: process.env.NODE_ENV || 'development' }, treeshake: false, sourcemap: config.buildOptions.sourcemap, alias: config.alias, external: externalPackages, externalEsm: true, }; if (config.packageOptions.source === 'local') { if (config.packageOptions.polyfillNode !== undefined) { installOptions.polyfillNode = config.packageOptions.polyfillNode; } if (config.packageOptions.packageLookupFields !== undefined) { installOptions.packageLookupFields = config.packageOptions.packageLookupFields; } if (config.packageOptions.namedExports !== undefined) { installOptions.namedExports = config.packageOptions.namedExports; } } const { importMap: newImportMap, needsSsrBuild } = await local_install_1.installPackages({ config, isDev: true, isSSR: false, installTargets, installOptions, }); logger_1.logger.debug(`${lineBullet} ${packageFormatted} DONE`); if (needsSsrBuild) { logger_1.logger.info(`${lineBullet} ${packageFormatted} ${colors.dim(`(ssr)`)}`); await local_install_1.installPackages({ config, isDev: true, isSSR: true, installTargets, installOptions: { ...installOptions, dest: installDest + '-ssr', }, }); logger_1.logger.debug(`${lineBullet} ${packageFormatted} (ssr) DONE`); } const dependencyFileLoc = path_1.default.join(installDest, newImportMap.imports[spec]); const loadedFile = await fs_1.promises.readFile(dependencyFileLoc); if (util_1.isJavaScript(dependencyFileLoc)) { const packageImports = new Set(); const code = loadedFile.toString('utf8'); for (const imp of await rewrite_imports_1.scanCodeImportsExports(code)) { const spec = code.substring(imp.s, imp.e); if (util_1.isRemoteUrl(spec)) { continue; } if (util_1.isPathImport(spec)) { continue; } packageImports.add(spec); } [...packageImports].map((packageImport) => this.resolvePackageImport(entrypoint, packageImport, config, undefined, depth + 1)); // Kick off to a future event loop run, so that the `this.resolvePackageImport()` calls // above have a chance to enter the queue. Prevents a premature exit. await new Promise((resolve) => setTimeout(resolve, 5)); } return newImportMap; }, { priority: depth }); const dependencyFileLoc = path_1.default.join(installDest, newImportMap.imports[spec]); // Flatten the import map value into a resolved, public import ID. // ex: "./react.js" -> "react.v17.0.1.js" const importId = newImportMap.imports[spec] .replace(/\//g, '.') .replace(/^\.+/g, '') .replace(/\.([^\.]*?)$/, `.v${packageVersion}.$1`); allPackageImports[importId] = { entrypoint, loc: dependencyFileLoc, installDest, packageName, packageVersion, }; // Memoize the result, for faster repeat lookups. memoizedResolve[source][spec] = path_1.default.posix.join(config.buildOptions.metaUrlPath, 'pkg', importId); return memoizedResolve[source][spec]; }, clearCache() { return rimraf_1.default.sync(PROJECT_CACHE_DIR); }, getCacheFolder() { return PROJECT_CACHE_DIR; }, };