snowpack
Version:
The ESM-powered frontend build tool. Fast, lightweight, unbundled.
914 lines (909 loc) • 41.4 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.command = exports.startServer = exports.NotFoundError = exports.OneToManyMap = void 0;
const compressible_1 = __importDefault(require("compressible"));
const etag_1 = __importDefault(require("etag"));
const events_1 = require("events");
const fs_1 = require("fs");
const fdir_1 = require("fdir");
const picomatch_1 = __importDefault(require("picomatch"));
const http_1 = __importDefault(require("http"));
const http2_1 = __importDefault(require("http2"));
const colors = __importStar(require("kleur/colors"));
const mime_types_1 = __importDefault(require("mime-types"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const perf_hooks_1 = require("perf_hooks");
const slash_1 = __importDefault(require("slash"));
const stream_1 = __importDefault(require("stream"));
const url_1 = __importDefault(require("url"));
const zlib_1 = __importDefault(require("zlib"));
const build_import_proxy_1 = require("../build/build-import-proxy");
const file_builder_1 = require("../build/file-builder");
const file_urls_1 = require("../build/file-urls");
const hmr_1 = require("../dev/hmr");
const logger_1 = require("../logger");
const util_1 = require("../sources/util");
const ssr_loader_1 = require("../ssr-loader");
const util_2 = require("../util");
const paint_1 = require("./paint");
const import_css_1 = require("../build/import-css");
const build_pipeline_1 = require("../build/build-pipeline");
class OneToManyMap {
constructor() {
this.keyToValue = new Map();
this.valueToKey = new Map();
}
add(key, _value) {
const value = Array.isArray(_value) ? _value : [_value];
this.keyToValue.set(key, value);
for (const val of value) {
this.valueToKey.set(val, key);
}
}
delete(key) {
const value = this.value(key);
this.keyToValue.delete(key);
if (value) {
for (const val of value) {
this.valueToKey.delete(val);
}
}
}
key(value) {
return this.valueToKey.get(value);
}
value(key) {
return this.keyToValue.get(key);
}
}
exports.OneToManyMap = OneToManyMap;
const FILE_BUILD_RESULT_ERROR = `Build Result Error: There was a problem with a file build result.`;
/**
* If encoding is defined, return a string. Otherwise, return a Buffer.
*/
function encodeResponse(response, encoding) {
if (encoding === undefined) {
return response;
}
if (encoding) {
if (typeof response === 'string') {
return response;
}
else {
return response.toString(encoding);
}
}
if (typeof response === 'string') {
return Buffer.from(response);
}
else {
return response;
}
}
/**
* A helper class for "Not Found" errors, storing data about what file lookups were attempted.
*/
class NotFoundError extends Error {
constructor(url, lookups) {
if (!lookups) {
super(`Not Found (${url})`);
}
else {
super(`Not Found (${url}):\n${lookups.map((loc) => ' ✘ ' + loc).join('\n')}`);
}
}
}
exports.NotFoundError = NotFoundError;
function sendResponseFile(req, res, { contents, originalFileLoc, contentType }) {
var _a;
const body = Buffer.from(contents);
const ETag = etag_1.default(body, { weak: true });
const headers = {
'Accept-Ranges': 'bytes',
'Access-Control-Allow-Origin': '*',
'Content-Type': contentType || 'application/octet-stream',
ETag,
Vary: 'Accept-Encoding',
};
if (req.headers['if-none-match'] === ETag) {
res.writeHead(304, headers);
res.end();
return;
}
let acceptEncoding = req.headers['accept-encoding'] || '';
if (((_a = req.headers['cache-control']) === null || _a === void 0 ? void 0 : _a.includes('no-transform')) ||
['HEAD', 'OPTIONS'].includes(req.method) ||
!contentType ||
!compressible_1.default(contentType)) {
acceptEncoding = '';
}
// Handle gzip compression
if (/\bgzip\b/.test(acceptEncoding) && stream_1.default.Readable.from) {
const bodyStream = stream_1.default.Readable.from([body]);
headers['Content-Encoding'] = 'gzip';
res.writeHead(200, headers);
stream_1.default.pipeline(bodyStream, zlib_1.default.createGzip(), res, function onError(err) {
if (err) {
res.end();
logger_1.logger.error(`✘ An error occurred serving ${colors.bold(req.url)}`);
logger_1.logger.error(typeof err !== 'string' ? err.toString() : err);
}
});
return;
}
// Handle partial requests
// TODO: This throws out a lot of hard work, and ignores any build. Improve.
const { range } = req.headers;
if (range) {
if (!originalFileLoc) {
throw new Error('Virtual files do not support partial requests');
}
const { size: fileSize } = fs_1.statSync(originalFileLoc);
const [rangeStart, rangeEnd] = range.replace(/bytes=/, '').split('-');
const start = parseInt(rangeStart, 10);
const end = rangeEnd ? parseInt(rangeEnd, 10) : fileSize - 1;
const chunkSize = end - start + 1;
const fileStream = fs_1.createReadStream(originalFileLoc, { start, end });
res.writeHead(206, {
...headers,
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Content-Length': chunkSize,
});
fileStream.pipe(res);
return;
}
res.writeHead(200, headers);
res.write(body);
res.end();
}
function sendResponseError(req, res, status) {
const contentType = mime_types_1.default.contentType(path_1.default.extname(req.url) || '.html');
const headers = {
'Access-Control-Allow-Origin': '*',
'Accept-Ranges': 'bytes',
'Content-Type': contentType || 'application/octet-stream',
Vary: 'Accept-Encoding',
};
res.writeHead(status, headers);
res.end();
}
function handleResponseError(req, res, err) {
var _a;
if (err instanceof NotFoundError) {
// Don't log favicon "Not Found" errors. Browsers automatically request a favicon.ico file
// from the server, which creates annoying errors for new apps / first experiences.
if (req.url !== '/favicon.ico') {
logger_1.logger.error(`[404] ${err.message}`);
}
sendResponseError(req, res, 404);
return;
}
console.log(err);
logger_1.logger.error(err.toString());
logger_1.logger.error(`[500] ${req.url}`, {
// @ts-ignore
name: (_a = err.__snowpackBuildDetails) === null || _a === void 0 ? void 0 : _a.name,
});
sendResponseError(req, res, 500);
return;
}
function getServerRuntime(sp, config, options = {}) {
const runtime = ssr_loader_1.createLoader({
config,
load: async (url) => {
const result = await sp.loadUrl(url, { isSSR: true, allowStale: false, encoding: 'utf8' });
if (!result)
throw new NotFoundError(url);
return result;
},
});
if (options.invalidateOnChange !== false) {
sp.onFileChange(({ filePath }) => {
const url = sp.getUrlForFile(filePath);
if (url) {
runtime.invalidateModule(url);
}
});
}
return runtime;
}
async function startServer(commandOptions, { isDev: _isDev, isWatch: _isWatch, preparePackages: _preparePackages, } = {}) {
const { config } = commandOptions;
const isDev = _isDev !== null && _isDev !== void 0 ? _isDev : config.mode !== 'production';
const isWatch = _isWatch !== null && _isWatch !== void 0 ? _isWatch : true;
const isPreparePackages = _preparePackages !== null && _preparePackages !== void 0 ? _preparePackages : true;
const pkgSource = util_1.getPackageSource(config);
if (isPreparePackages) {
await pkgSource.prepare();
logger_1.logger.info(colors.bold('Ready!'));
}
let serverStart = perf_hooks_1.performance.now();
const { port: defaultPort, hostname, open, openUrl } = config.devOptions;
const messageBus = new events_1.EventEmitter();
const PACKAGE_PATH_PREFIX = path_1.default.posix.join(config.buildOptions.metaUrlPath, 'pkg/');
const PACKAGE_LINK_PATH_PREFIX = path_1.default.posix.join(config.buildOptions.metaUrlPath, 'link/');
let port;
let warnedDeprecatedPackageImport = new Set();
if (defaultPort !== 0) {
port = await paint_1.getPort(defaultPort);
// Reset the clock if we had to wait for the user prompt to select a new port.
if (port !== defaultPort) {
serverStart = perf_hooks_1.performance.now();
}
}
// Fill in any command-specific plugin methods.
for (const p of config.plugins) {
p.markChanged = (fileLoc) => {
knownETags.clear();
onWatchEvent(fileLoc);
};
}
if (isWatch && config.devOptions.output === 'dashboard' && process.stdout.isTTY) {
paint_1.startDashboard(messageBus, config);
}
else {
// "stream": Log relevent events to the console.
messageBus.on(paint_1.paintEvent.WORKER_MSG, ({ id, msg }) => {
logger_1.logger.info(msg.trim(), { name: id });
});
}
const symlinkDirectories = new Map();
const inMemoryBuildCache = new Map();
let fileToUrlMapping = new OneToManyMap();
const excludeGlobs = [
...config.exclude,
...(config.mode === 'test' ? [] : config.testOptions.files),
];
const foundExcludeMatch = picomatch_1.default(excludeGlobs, { ignore: '**/node_modules/**' });
for (const [mountKey, mountEntry] of Object.entries(config.mount)) {
logger_1.logger.debug(`Mounting directory: '${mountKey}' as URL '${mountEntry.url}'`);
const files = (await new fdir_1.fdir()
.withFullPaths()
// Note: exclude() only matches directories, and not files. However, the cost
// of false positives here is minor, so do this as a quick check to possibly
// skip scanning into entire folder trees.
.exclude((_, dirPath) => foundExcludeMatch(dirPath))
.crawl(mountKey)
.withPromise());
for (const f of files) {
fileToUrlMapping.add(f, file_urls_1.getUrlsForFile(f, config));
}
}
logger_1.logger.debug(`Using in-memory cache: ${fileToUrlMapping}`);
const readCredentials = async (cwd) => {
const secure = config.devOptions.secure;
let cert;
let key;
if (typeof secure === 'object') {
cert = secure.cert;
key = secure.key;
}
else {
const certPath = path_1.default.join(cwd, 'snowpack.crt');
const keyPath = path_1.default.join(cwd, 'snowpack.key');
[cert, key] = await Promise.all([fs_1.promises.readFile(certPath), fs_1.promises.readFile(keyPath)]);
}
return {
cert,
key,
};
};
let credentials;
if (config.devOptions.secure) {
try {
logger_1.logger.debug(`reading credentials`);
credentials = await readCredentials(config.root);
}
catch (e) {
logger_1.logger.error(`✘ No HTTPS credentials found!`);
logger_1.logger.info(`You can specify HTTPS credentials via either:
- Including credentials in your project config under ${colors.yellow(`devOptions.secure`)}.
- Including ${colors.yellow('snowpack.crt')} and ${colors.yellow('snowpack.key')} files in your project's root directory.
You can automatically generate credentials for your project via either:
- ${colors.cyan('devcert')}: ${colors.yellow('npx devcert-cli generate localhost')}
https://github.com/davewasmer/devcert-cli (no install required)
- ${colors.cyan('mkcert')}: ${colors.yellow('mkcert -install && mkcert -key-file snowpack.key -cert-file snowpack.crt localhost')}
https://github.com/FiloSottile/mkcert (install required)`);
process.exit(1);
}
}
for (const runPlugin of config.plugins) {
if (runPlugin.run) {
logger_1.logger.debug(`starting ${runPlugin.name} run() workers`);
runPlugin
.run({
isDev,
// @ts-ignore: internal API only
log: (msg, data) => {
if (msg === 'CONSOLE_INFO') {
logger_1.logger.info(data.msg, { name: runPlugin.name });
}
else {
messageBus.emit(msg, { ...data, id: runPlugin.name });
}
},
})
.then(() => {
logger_1.logger.info('Command completed.', { name: runPlugin.name });
})
.catch((err) => {
logger_1.logger.error(`Command exited with error code: ${err}`, { name: runPlugin.name });
process.exit(1);
});
}
}
function getOutputExtensionMatch() {
let outputExts = [];
for (const plugin of config.plugins) {
if (plugin.resolve) {
for (const outputExt of plugin.resolve.output) {
const ext = outputExt.toLowerCase();
if (!outputExts.includes(ext)) {
outputExts.push(ext);
}
}
}
}
outputExts = outputExts.sort((a, b) => b.split('.').length - a.split('.').length);
return (base) => {
const basename = base.toLowerCase();
for (const ext of outputExts) {
if (basename.endsWith(ext))
return ext;
}
return path_1.default.extname(basename);
};
}
const matchOutputExt = getOutputExtensionMatch();
async function loadUrl(reqUrl, { isSSR: _isSSR, isHMR: _isHMR, isResolve: _isResolve, encoding: _encoding, importMap, } = {}) {
const isSSR = _isSSR !== null && _isSSR !== void 0 ? _isSSR : false;
// // Default to HMR on, but disable HMR if SSR mode is enabled.
const isHMR = _isHMR !== null && _isHMR !== void 0 ? _isHMR : (!!config.devOptions.hmr && !isSSR);
const encoding = _encoding !== null && _encoding !== void 0 ? _encoding : null;
const reqUrlHmrParam = reqUrl.includes('?mtime=') && reqUrl.split('?')[1];
let reqPath = decodeURI(url_1.default.parse(reqUrl).pathname);
if (reqPath === build_import_proxy_1.getMetaUrlPath('/hmr-client.js', config)) {
return {
contents: encodeResponse(util_2.HMR_CLIENT_CODE, encoding),
imports: [],
originalFileLoc: null,
contentType: 'application/javascript',
};
}
if (reqPath === build_import_proxy_1.getMetaUrlPath('/hmr-error-overlay.js', config)) {
return {
contents: encodeResponse(util_2.HMR_OVERLAY_CODE, encoding),
imports: [],
originalFileLoc: null,
contentType: 'application/javascript',
};
}
if (reqPath === build_import_proxy_1.getMetaUrlPath('/env.js', config)) {
return {
contents: encodeResponse(build_import_proxy_1.generateEnvModule({
mode: config.mode,
isSSR,
configEnv: config.env,
}), encoding),
imports: [],
originalFileLoc: null,
contentType: 'application/javascript',
};
}
// * NPM Packages:
// NPM packages are served via `/_snowpack/pkg/` URLs. Behavior varies based on package source (local, remote)
// but as a general rule all URLs contained within are managed by the package source loader. When this URL
// prefix is hit, we load the file through the selected package source loader.
if (reqPath.startsWith(PACKAGE_PATH_PREFIX)) {
// Backwards-compatable redirect for legacy package URLs: If someone has created an import URL manually
// (ex: /_snowpack/pkg/react.js) then we need to redirect and warn to use our new API in the future.
if (reqUrl.split('.').length <= 2 && config.packageOptions.source !== 'remote') {
if (!warnedDeprecatedPackageImport.has(reqUrl)) {
logger_1.logger.warn(`(${reqUrl}) Deprecated manual package import. Please use snowpack.getUrlForPackage() to create package URLs instead.`);
warnedDeprecatedPackageImport.add(reqUrl);
}
const redirectUrl = await pkgSource.resolvePackageImport(reqUrl.replace(PACKAGE_PATH_PREFIX, '').replace(/\.js/, ''));
reqPath = decodeURI(url_1.default.parse(redirectUrl).pathname);
}
const resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, '');
const webModuleUrl = resourcePath.substr(PACKAGE_PATH_PREFIX.length);
let loadedModule = await pkgSource.load(webModuleUrl, { isSSR });
if (!loadedModule) {
throw new NotFoundError(reqPath);
}
if (reqPath.endsWith('.proxy.js')) {
return {
imports: [],
contents: await build_import_proxy_1.wrapImportProxy({
url: resourcePath,
code: loadedModule.contents,
hmr: isHMR,
config: config,
}),
originalFileLoc: null,
contentType: 'application/javascript',
};
}
return {
imports: loadedModule.imports,
contents: encodeResponse(loadedModule.contents, encoding),
originalFileLoc: null,
contentType: mime_types_1.default.lookup(reqPath) || 'application/javascript',
};
}
// Most of the time, resourcePath should have ".map" and ".proxy.js" extensions stripped to
// match the file on disk. However, sometimes the on disk is an actual source map in a static
// directory, so we can't strip that info just yet. Try the exact match first, and then strip
// it later on if there is no match.
let resourcePath = reqPath;
let resourceType = matchOutputExt(reqPath);
if (util_2.IS_DOTFILE_REGEX.test(reqPath))
resourceType = '';
let foundFile;
// * Workspaces & Linked Packages:
// The "local" package resolver supports npm packages that live in a local directory,
// usually a part of your monorepo/workspace. Snowpack treats these files as source files,
// with each file served individually and rebuilt instantly when changed. In the future,
// these linked packages may be bundled again with a rapid bundler like esbuild.
if (config.workspaceRoot && reqPath.startsWith(PACKAGE_LINK_PATH_PREFIX)) {
const symlinkResourceUrl = reqPath.substr(PACKAGE_LINK_PATH_PREFIX.length);
const symlinkResourceLoc = path_1.default.resolve(config.workspaceRoot, process.platform === 'win32' ? symlinkResourceUrl.replace(/\//g, '\\') : symlinkResourceUrl);
const symlinkResourceDirectory = path_1.default.dirname(symlinkResourceLoc);
const fileStat = await fs_1.promises.stat(symlinkResourceDirectory).catch(() => null);
if (!fileStat) {
throw new NotFoundError(reqPath, [symlinkResourceDirectory]);
}
// If this is the first file served out of this linked directory
// - add it to our file watcher (to enable HMR)
// - add it to our file<>URL mapping for future lookups
// - add a promise to our directory<>promise map, which acts as
// a guard to ensure no loadUrls for this directory proceed before
// proccessing of this directory is done
// Each directory is scanned shallowly, so nested directories inside
// of `symlinkDirectories` are okay.
if (!symlinkDirectories.get(symlinkResourceDirectory)) {
logger_1.logger.debug(`Mounting symlink directory: '${symlinkResourceDirectory}' as URL '${path_1.default.dirname(reqPath)}'`);
symlinkDirectories.set(symlinkResourceDirectory, processDirectory());
watcher && watcher.add(symlinkResourceDirectory);
async function processDirectory() {
const shallowFiles = (await new fdir_1.fdir()
.withFullPaths()
.withMaxDepth(0)
.crawl(symlinkResourceDirectory)
.withPromise());
for (const f of shallowFiles) {
if (fileToUrlMapping.value(f)) {
logger_1.logger.warn(`Warning: mounted file is being imported as a package.\n` +
`Workspace & monorepo packages work automatically and do not need to be mounted.`);
}
else {
fileToUrlMapping.add(f, file_urls_1.getBuiltFileUrls(f, config).map((u) => {
const url = path_1.default.posix.join(config.buildOptions.metaUrlPath, 'link', slash_1.default(path_1.default.relative(config.workspaceRoot, u)));
return url;
}));
}
}
}
}
// guard: ensure directory is properly read and files registered before proceeding
await symlinkDirectories.get(symlinkResourceDirectory);
let attemptedFileLoc = fileToUrlMapping.key(reqPath);
if (!attemptedFileLoc) {
resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, '');
resourceType = path_1.default.extname(resourcePath);
}
attemptedFileLoc = fileToUrlMapping.key(resourcePath);
if (!attemptedFileLoc) {
throw new NotFoundError(reqPath);
}
const fileLocationExists = await fs_1.promises.stat(attemptedFileLoc).catch(() => null);
if (!fileLocationExists) {
throw new NotFoundError(reqPath, [attemptedFileLoc]);
}
let foundType = path_1.default.extname(reqPath);
if (!foundType && attemptedFileLoc.endsWith('.html'))
foundType = '.html';
if (util_2.IS_DOTFILE_REGEX.test(reqPath))
foundType = '';
foundFile = {
loc: attemptedFileLoc,
type: foundType,
isStatic: false,
isResolve: true,
};
}
// * Local Files
// If this is not a special URL route, then treat it as a normal file request.
// Check our file<>URL mapping for the most relevant match, and continue if found.
// Otherwise, return a 404.
else {
let attemptedFileLoc = fileToUrlMapping.key(resourcePath);
if (!attemptedFileLoc) {
resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, '');
if (resourcePath.endsWith('/')) {
resourcePath += 'index.html'; // if trailing slash, pretending like /index.html was requested makes the below much easier
}
resourceType = path_1.default.extname(resourcePath);
}
attemptedFileLoc =
fileToUrlMapping.key(resourcePath) ||
fileToUrlMapping.key(resourcePath + '.html') ||
fileToUrlMapping.key(resourcePath + '/index.html');
if (!attemptedFileLoc) {
// last attempt: if this is a CSS Module, try and load JSON
if (resourcePath.endsWith('.module.css.json')) {
const srcLoc = resourcePath.replace(/\.json$/i, '');
return {
imports: [],
contents: import_css_1.cssModuleJSON(srcLoc),
originalFileLoc: srcLoc,
contentType: mime_types_1.default.lookup('.json'),
};
}
throw new NotFoundError(reqPath);
}
const [, mountEntry] = file_urls_1.getMountEntryForFile(attemptedFileLoc, config);
// TODO: This data type structuring/destructuring is neccesary for now,
// but we hope to add "virtual file" support soon via plugins. This would
// be the interface for those response types.
let foundType = path_1.default.extname(reqPath);
if (!foundType && attemptedFileLoc.endsWith('.html'))
foundType = '.html';
if (util_2.IS_DOTFILE_REGEX.test(reqPath))
foundType = '';
foundFile = {
loc: attemptedFileLoc,
type: foundType,
isStatic: mountEntry.static,
isResolve: mountEntry.resolve,
};
}
const { loc: fileLoc, type: responseType } = foundFile;
// TODO: Once plugins are able to add virtual files + imports, this will no longer be needed.
// - isStatic Workaround: HMR plugins need to add scripts to HTML file, even if static.
const isStatic = foundFile.isStatic && responseType !== '.html';
const isResolve = _isResolve !== null && _isResolve !== void 0 ? _isResolve : true;
// 1. Check the hot build cache. If it's already found, then just serve it.
const cacheKey = util_2.getCacheKey(fileLoc, { isSSR, mode: config.mode });
let fileBuilder = inMemoryBuildCache.get(cacheKey);
if (!fileBuilder) {
fileBuilder = new file_builder_1.FileBuilder({
loc: fileLoc,
isDev,
isSSR,
isHMR,
config,
hmrEngine,
});
// note: for Tailwind, CSS needs to avoid caching in dev server (Tailwind needs to handle rebuilding, not Snowpack)
const isTailwind = config.devOptions.tailwindConfig && (fileLoc.endsWith('.css') || fileLoc.endsWith('.pcss'));
if (!isTailwind) {
inMemoryBuildCache.set(cacheKey, fileBuilder);
}
}
function handleFinalizeError(err) {
logger_1.logger.error(FILE_BUILD_RESULT_ERROR);
hmrEngine &&
hmrEngine.broadcastMessage({
type: 'error',
title: FILE_BUILD_RESULT_ERROR,
errorMessage: err.toString(),
fileLoc,
errorStackTrace: err.stack,
});
}
let finalizedResponse;
let resolvedImports = [];
try {
if (Object.keys(fileBuilder.buildOutput).length === 0) {
await fileBuilder.build(isStatic);
}
if (resourcePath !== reqPath && reqPath.endsWith('.proxy.js')) {
finalizedResponse = await fileBuilder.getProxy(resourcePath, resourceType);
// CSS Modules only: also generate JSON module mapping (not imported so must be added manually)
if (reqPath.endsWith('.module.css.proxy.js') && fileBuilder.buildOutput['.json']) {
resolvedImports.push(util_2.createInstallTarget(`${resourcePath}.json`));
}
}
else if (resourcePath !== reqPath && reqPath.endsWith('.map')) {
finalizedResponse = fileBuilder.getSourceMap(resourcePath);
}
else {
if (foundFile.isResolve) {
// TODO: Warn if reqUrlHmrParam was needed here? HMR can't work if URLs aren't resolved.
resolvedImports = await fileBuilder.resolveImports(isResolve, reqUrlHmrParam, importMap);
}
finalizedResponse = fileBuilder.getResult(resourceType);
}
}
catch (err) {
handleFinalizeError(err);
throw err;
}
if (finalizedResponse) {
return {
imports: resolvedImports,
contents: encodeResponse(finalizedResponse, encoding),
originalFileLoc: fileLoc,
contentType: mime_types_1.default.lookup(responseType),
};
}
}
/**
* A simple map to optimize the speed of our 304 responses. If an ETag check is
* sent in the request, check if it matches the last known etag for tat file.
*
* Remember: This is just a nice-to-have! If we get this logic wrong, it can mean
* stale files in the user's cache. Feel free to clear aggressively, as needed.
*/
const knownETags = new Map();
function matchRouteHandler(reqUrl, expectHandler) {
if (reqUrl.startsWith(config.buildOptions.metaUrlPath)) {
return null;
}
const reqPath = decodeURI(url_1.default.parse(reqUrl).pathname);
const reqExt = matchOutputExt(reqPath);
const isRoute = !reqExt || reqExt.toLowerCase() === '.html';
for (const route of config.routes) {
if (route.match === 'routes' && !isRoute) {
continue;
}
if (!route[expectHandler]) {
continue;
}
if (route._srcRegex.test(reqPath)) {
return route[expectHandler];
}
}
return null;
}
/**
* Fully handle the response for a given request. This is used internally for
* every response that the dev server sends, but it can also be used via the
* JS API to handle most boilerplate around request handling.
*/
async function handleRequest(req, res, { handleError } = {}) {
let reqUrl = req.url;
const matchedRouteHandler = matchRouteHandler(reqUrl, 'dest');
// If a route is matched, rewrite the URL or call the route function
if (matchedRouteHandler) {
if (typeof matchedRouteHandler === 'string') {
reqUrl = matchedRouteHandler;
}
else {
return matchedRouteHandler(req, res);
}
}
// Check if we can send back an optimized 304 response
const quickETagCheck = req.headers['if-none-match'];
const quickETagCheckUrl = reqUrl.replace(/\/$/, '/index.html');
if (quickETagCheck && quickETagCheck === knownETags.get(quickETagCheckUrl)) {
logger_1.logger.debug(`optimized etag! sending 304...`);
res.writeHead(304, { 'Access-Control-Allow-Origin': '*' });
res.end();
return;
}
// Backwards-compatable redirect for legacy package URLs: If someone has created an import URL manually
// (ex: /_snowpack/pkg/react.js) then we need to redirect and warn to use our new API in the future.
if (reqUrl.startsWith(PACKAGE_PATH_PREFIX) &&
reqUrl.split('.').length <= 2 &&
config.packageOptions.source !== 'remote') {
if (!warnedDeprecatedPackageImport.has(reqUrl)) {
logger_1.logger.warn(`(${reqUrl}) Deprecated manual package import. Please use snowpack.getUrlForPackage() to create package URLs instead.`);
warnedDeprecatedPackageImport.add(reqUrl);
}
const redirectUrl = await pkgSource.resolvePackageImport(reqUrl.replace(PACKAGE_PATH_PREFIX, '').replace(/\.js/, ''));
res.writeHead(301, { Location: redirectUrl });
res.end();
return;
}
// Otherwise, load the file and respond if successful.
try {
const result = await loadUrl(reqUrl, { allowStale: true, encoding: null });
if (!result) {
throw new NotFoundError(reqUrl);
}
sendResponseFile(req, res, result);
if (result.checkStale) {
await result.checkStale();
}
if (result.contents) {
const tag = etag_1.default(result.contents, { weak: true });
const reqPath = decodeURI(url_1.default.parse(reqUrl).pathname);
knownETags.set(reqPath, tag);
}
return;
}
catch (err) {
// Some consumers may want to handle/ignore errors themselves.
if (handleError === false) {
throw err;
}
handleResponseError(req, res, err);
}
}
async function handleUpgrade(req, socket, head) {
let reqUrl = req.url;
const matchedRouteHandler = matchRouteHandler(reqUrl, 'upgrade');
if (matchedRouteHandler) {
matchedRouteHandler(req, socket, head);
}
}
const createServer = (responseHandler) => {
if (credentials) {
return http2_1.default.createSecureServer({ ...credentials, allowHTTP1: true }, responseHandler);
}
return http_1.default.createServer(responseHandler);
};
let server;
if (port) {
server = createServer(async (req, res) => {
// Attach a request logger.
res.on('finish', () => {
const { method, url } = req;
const { statusCode } = res;
logger_1.logger.debug(`[${statusCode}] ${method} ${url}`);
});
// Otherwise, pass requests directly to Snowpack's request handler.
handleRequest(req, res);
})
.on('upgrade', (req, socket, head) => {
handleUpgrade(req, socket, head);
})
.on('error', (err) => {
logger_1.logger.error(colors.red(` ✘ Failed to start server at port ${colors.bold(port)}.`), err);
server.close();
process.exit(1);
})
.listen(port);
// Announce server has started
const remoteIps = Object.values(os_1.default.networkInterfaces())
.reduce((every, i) => [...every, ...(i || [])], [])
.filter((i) => i.family === 'IPv4' && i.internal === false)
.map((i) => i.address);
const protocol = config.devOptions.secure ? 'https:' : 'http:';
// Log the successful server start.
const startTimeMs = Math.round(perf_hooks_1.performance.now() - serverStart);
logger_1.logger.info(colors.green(`Server started in ${startTimeMs}ms.`));
logger_1.logger.info(`${colors.green('Local:')} ${`${protocol}//${hostname}:${port}`}`);
if (remoteIps.length > 0) {
logger_1.logger.info(`${colors.green('Network:')} ${`${protocol}//${remoteIps[0]}:${port}`}`);
}
}
// HMR Engine
const { hmrEngine, handleHmrUpdate } = config.devOptions.hmr
? hmr_1.startHmrEngine(inMemoryBuildCache, server, port, config)
: { hmrEngine: undefined, handleHmrUpdate: undefined };
// Allow the user to hook into this callback, if they like (noop by default)
let onFileChangeCallbacks = [];
let watcher;
// Watch src files
async function onWatchEvent(fileLoc) {
logger_1.logger.info(colors.cyan('File changed: ') + path_1.default.relative(config.workspaceRoot || config.root, fileLoc));
const updatedUrls = file_urls_1.getUrlsForFile(fileLoc, config);
if (updatedUrls) {
handleHmrUpdate && handleHmrUpdate(fileLoc, updatedUrls[0]);
knownETags.delete(updatedUrls[0]);
knownETags.delete(updatedUrls[0] + '.proxy.js');
}
inMemoryBuildCache.delete(util_2.getCacheKey(fileLoc, { isSSR: true, mode: config.mode }));
inMemoryBuildCache.delete(util_2.getCacheKey(fileLoc, { isSSR: false, mode: config.mode }));
await Promise.all(onFileChangeCallbacks.map((callback) => callback({ filePath: fileLoc })));
for (const plugin of config.plugins) {
plugin.onChange && plugin.onChange({ filePath: fileLoc });
}
}
if (isWatch) {
// Start watching the file system.
// Defer "chokidar" loading to here, to reduce impact on overall startup time
const chokidar = await Promise.resolve().then(() => __importStar(require('chokidar')));
watcher = chokidar.watch([], {
ignored: config.exclude.filter((k) => k !== '**/_*.{sass,scss}'),
persistent: true,
ignoreInitial: true,
disableGlobbing: false,
useFsEvents: util_2.isFsEventsEnabled(),
});
watcher.on('add', async (fileLoc) => {
knownETags.clear();
await pkgSource.prepareSingleFile(fileLoc);
await onWatchEvent(fileLoc);
fileToUrlMapping.add(fileLoc, file_urls_1.getUrlsForFile(fileLoc, config));
});
watcher.on('unlink', async (fileLoc) => {
knownETags.clear();
await onWatchEvent(fileLoc);
fileToUrlMapping.delete(fileLoc);
});
watcher.on('change', async (fileLoc) => {
// TODO: If this needs to build a new dependency, report to the browser via HMR event.
await pkgSource.prepareSingleFile(fileLoc);
await onWatchEvent(fileLoc);
});
// [hmrDelay] - Let users with noisy startups delay HMR (ex: 11ty, tsc builds)
setTimeout(() => {
watcher.add(Object.keys(config.mount));
if (config.devOptions.output !== 'dashboard' || !process.stdout.isTTY) {
logger_1.logger.info(colors.cyan('watching for file changes... '));
}
}, config.devOptions.hmrDelay);
}
// Open the user's browser (ignore if failed)
if (server && port && open && open !== 'none') {
const protocol = config.devOptions.secure ? 'https:' : 'http:';
await util_2.openInBrowser(protocol, hostname, port, open, openUrl).catch((err) => {
logger_1.logger.debug(`Browser open error: ${err}`);
});
}
const sp = {
port: port || defaultPort,
hmrEngine,
rawServer: server,
loadUrl,
handleRequest,
sendResponseFile,
sendResponseError,
getUrlForPackage: (pkgSpec) => {
return pkgSource.resolvePackageImport(pkgSpec);
},
getUrlForFile: (fileLoc) => {
const result = file_urls_1.getUrlsForFile(fileLoc, config);
return result ? result[0] : null;
},
onFileChange: (callback) => onFileChangeCallbacks.push(callback),
getServerRuntime: (options) => getServerRuntime(sp, config, options),
async shutdown() {
watcher && (await watcher.close());
await build_pipeline_1.runPipelineCleanupStep(config);
server && server.close();
hmrEngine && (await hmrEngine.stop());
},
markChanged(fileLoc) {
knownETags.clear();
onWatchEvent(fileLoc);
},
};
return sp;
}
exports.startServer = startServer;
async function command(commandOptions) {
try {
// Set some CLI-focused defaults
commandOptions.config.devOptions.output =
commandOptions.config.devOptions.output || 'dashboard';
commandOptions.config.devOptions.open = commandOptions.config.devOptions.open || 'default';
commandOptions.config.devOptions.hmr = commandOptions.config.devOptions.hmr !== false;
// Start the server
await startServer(commandOptions, { isWatch: true });
}
catch (err) {
logger_1.logger.error(err.message);
logger_1.logger.debug(err.stack);
process.exit(1);
}
return new Promise(() => { });
}
exports.command = command;
;