snowpack
Version:
The ESM-powered frontend build tool. Fast, lightweight, unbundled.
392 lines (391 loc) • 20.6 kB
JavaScript
;
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;
},
};