snowpack
Version:
The ESM-powered frontend build tool. Fast, lightweight, unbundled.
281 lines (280 loc) • 12.9 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileBuilder = void 0;
const fs_1 = require("fs");
const mkdirp_1 = __importDefault(require("mkdirp"));
const path_1 = __importDefault(require("path"));
const url_1 = __importDefault(require("url"));
const rewrite_imports_1 = require("../rewrite-imports");
const scan_imports_1 = require("../scan-imports");
const util_1 = require("../sources/util");
const util_2 = require("../util");
const build_import_proxy_1 = require("./build-import-proxy");
const build_pipeline_1 = require("./build-pipeline");
const file_urls_1 = require("./file-urls");
const import_resolver_1 = require("./import-resolver");
/**
* FileBuilder - This class is responsible for building a file. It is broken into
* individual stages so that the entire application build process can be tackled
* in stages (build -> resolve -> get response).
*/
class FileBuilder {
constructor({ loc, isDev, isHMR, isSSR, config, hmrEngine, }) {
this.buildOutput = {};
this.resolvedOutput = {};
this.hmrEngine = null;
this.loc = loc;
this.isDev = isDev;
this.isHMR = isHMR;
this.isSSR = isSSR;
this.config = config;
this.hmrEngine = hmrEngine || null;
const urls = file_urls_1.getUrlsForFile(loc, config);
if (!urls) {
throw new Error(`No mounted URLs configured for file: ${loc}`);
}
this.urls = urls;
}
verifyRequestFromBuild(type) {
const possibleExtensions = this.urls.map((url) => path_1.default.extname(url));
if (!possibleExtensions.includes(type))
throw new Error(`${this.loc} - Requested content "${type}" but only built ${possibleExtensions.join(', ')}`);
return this.resolvedOutput[type];
}
/**
* Resolve Imports: Resolved imports are based on the state of the file
* system, so they can't be cached long-term with the build.
*/
async resolveImports(isResolve, hmrParam, importMap) {
var _a;
const urlPathDirectory = path_1.default.posix.dirname(this.urls[0]);
const pkgSource = util_1.getPackageSource(this.config);
const resolvedImports = [];
for (const [type, outputResult] of Object.entries(this.buildOutput)) {
if (!(type === '.js' || type === '.html' || type === '.css')) {
continue;
}
let contents = typeof outputResult.code === 'string'
? outputResult.code
: outputResult.code.toString('utf8');
// Handle attached CSS.
if (type === '.js' && this.buildOutput['.css']) {
const relativeCssImport = `./${util_2.replaceExtension(path_1.default.posix.basename(this.urls[0]), '.js', '.css')}`;
contents = `import '${relativeCssImport}';\n` + contents;
}
// Finalize the response
contents = this.finalizeResult(type, contents);
//
const resolveImportGlobSpecifier = import_resolver_1.createImportGlobResolver({
fileLoc: this.loc,
config: this.config,
});
// resolve all imports
const resolveImportSpecifier = import_resolver_1.createImportResolver({
fileLoc: this.loc,
config: this.config,
});
const resolveImport = async (spec) => {
var _a;
// Ignore packages marked as external
if ((_a = this.config.packageOptions.external) === null || _a === void 0 ? void 0 : _a.includes(spec)) {
return spec;
}
if (util_2.isRemoteUrl(spec)) {
return spec;
}
// Try to resolve the specifier to a known URL in the project
let resolvedImportUrl = resolveImportSpecifier(spec);
// Handle a package import
if (!resolvedImportUrl) {
try {
return await pkgSource.resolvePackageImport(spec, {
importMap: importMap || (isResolve ? undefined : { imports: {} }),
});
}
catch (err) {
if (!isResolve && /not included in import map./.test(err.message)) {
return spec;
}
throw err;
}
}
return resolvedImportUrl || spec;
};
const scannedImports = await scan_imports_1.scanImportsFromFiles([
{
baseExt: type,
root: this.config.root,
locOnDisk: this.loc,
contents,
},
], this.config);
contents = await build_import_proxy_1.transformGlobImports({ contents, resolveImportGlobSpecifier });
contents = await rewrite_imports_1.transformFileImports({ type, contents }, async (spec) => {
let resolvedImportUrl = await resolveImport(spec);
// Handle normal "./" & "../" import specifiers
const importExtName = path_1.default.posix.extname(resolvedImportUrl);
const isProxyImport = importExtName && importExtName !== '.js' && importExtName !== '.mjs';
const isAbsoluteUrlPath = path_1.default.posix.isAbsolute(resolvedImportUrl);
if (isAbsoluteUrlPath) {
if (isResolve && this.config.buildOptions.resolveProxyImports && isProxyImport) {
resolvedImportUrl = resolvedImportUrl + '.proxy.js';
}
resolvedImports.push(util_2.createInstallTarget(resolvedImportUrl));
}
else {
resolvedImports.push(...scannedImports
.filter(({ specifier }) => specifier === spec)
.map((installTarget) => {
installTarget.specifier = resolvedImportUrl;
return installTarget;
}));
}
if (isAbsoluteUrlPath) {
// When dealing with an absolute import path, we need to honor the baseUrl
// proxy modules may attach code to the root HTML (like style) so don't resolve
resolvedImportUrl = util_2.relativeURL(urlPathDirectory, resolvedImportUrl);
}
return resolvedImportUrl;
});
// This is a hack since we can't currently scan "script" `src=` tags as imports.
// Either move these to inline JavaScript in the script body, or add support for
// `script.src=` and `link.href` scanning & resolving in transformFileImports().
if (type === '.html' && this.isHMR) {
if (contents.includes(build_import_proxy_1.SRI_CLIENT_HMR_SNOWPACK)) {
resolvedImports.push(util_2.createInstallTarget(build_import_proxy_1.getMetaUrlPath('hmr-client.js', this.config)));
}
if (contents.includes(build_import_proxy_1.SRI_ERROR_HMR_SNOWPACK)) {
resolvedImports.push(util_2.createInstallTarget(build_import_proxy_1.getMetaUrlPath('hmr-error-overlay.js', this.config)));
}
}
if (type === '.js' && hmrParam) {
contents = await rewrite_imports_1.transformEsmImports(contents, (imp) => {
var _a, _b;
const importUrl = path_1.default.posix.resolve(urlPathDirectory, imp);
const node = (_a = this.hmrEngine) === null || _a === void 0 ? void 0 : _a.getEntry(importUrl);
if (node && node.needsReplacement) {
(_b = this.hmrEngine) === null || _b === void 0 ? void 0 : _b.markEntryForReplacement(node, false);
return `${imp}?${hmrParam}`;
}
return imp;
});
}
if (type === '.js') {
const isHmrEnabled = contents.includes('import.meta.hot');
const rawImports = await rewrite_imports_1.scanCodeImportsExports(contents);
const resolvedImports = rawImports.map((imp) => {
let spec = contents.substring(imp.s, imp.e).replace(/(\/|\\)+$/, '');
if (imp.d > -1) {
spec = scan_imports_1.matchDynamicImportValue(spec) || '';
}
spec = spec.replace(/\?mtime=[0-9]+$/, '');
return path_1.default.posix.resolve(urlPathDirectory, spec);
});
(_a = this.hmrEngine) === null || _a === void 0 ? void 0 : _a.setEntry(this.urls[0], resolvedImports, isHmrEnabled);
}
// Update the output with the new resolved imports
this.resolvedOutput[type].code = contents;
this.resolvedOutput[type].map = undefined;
}
return resolvedImports;
}
/**
* Given a file, build it. Building a file sends it through our internal
* file builder pipeline, and outputs a build map representing the final
* build. A Build Map is used because one source file can result in multiple
* built files (Example: .svelte -> .js & .css).
*/
async build(isStatic) {
if (this.buildPromise) {
return this.buildPromise;
}
const fileBuilderPromise = (async () => {
if (isStatic) {
return {
[path_1.default.extname(this.loc)]: {
code: await fs_1.promises.readFile(this.loc),
map: undefined,
},
};
}
const builtFileOutput = await build_pipeline_1.buildFile(url_1.default.pathToFileURL(this.loc), {
config: this.config,
isDev: this.isDev,
isSSR: this.isSSR,
isPackage: false,
isHmrEnabled: this.isHMR,
});
return builtFileOutput;
})();
this.buildPromise = fileBuilderPromise;
try {
this.resolvedOutput = {};
this.buildOutput = await fileBuilderPromise;
for (const [outputKey, { code, map }] of Object.entries(this.buildOutput)) {
this.resolvedOutput[outputKey] = { code, map };
}
}
finally {
this.buildPromise = undefined;
}
}
finalizeResult(type, content) {
// Wrap the response.
switch (type) {
case '.html': {
content = build_import_proxy_1.wrapHtmlResponse({
code: content,
hmr: this.isHMR,
hmrPort: this.hmrEngine ? this.hmrEngine.port : undefined,
isDev: this.isDev,
config: this.config,
mode: this.isDev ? 'development' : 'production',
});
break;
}
case '.css': {
break;
}
case '.js':
{
content = build_import_proxy_1.wrapImportMeta({
code: content,
env: true,
hmr: this.isHMR,
config: this.config,
});
}
break;
}
// Return the finalized response.
return content;
}
getResult(type) {
const result = this.verifyRequestFromBuild(type);
if (result) {
// TODO: return result.map
return result.code;
}
}
getSourceMap(type) {
return this.resolvedOutput[type].map;
}
async getProxy(_url, type) {
const code = this.resolvedOutput[type].code;
const url = this.isDev ? _url : this.config.buildOptions.baseUrl + util_2.removeLeadingSlash(_url);
return await build_import_proxy_1.wrapImportProxy({ url, code, hmr: this.isHMR, config: this.config });
}
async writeToDisk(dir, results) {
await mkdirp_1.default(path_1.default.dirname(path_1.default.join(dir, this.urls[0])));
for (const outUrl of this.urls) {
const buildOutput = results[outUrl].contents;
const encoding = typeof buildOutput === 'string' ? 'utf8' : undefined;
await fs_1.promises.writeFile(path_1.default.join(dir, outUrl), buildOutput, encoding);
}
}
}
exports.FileBuilder = FileBuilder;