From ab38057bacc99f3adf880efa70e0ed3b8806509f Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Mon, 22 Feb 2021 14:49:13 -0800 Subject: [PATCH 01/40] refactor snowpack internals --- esinstall/src/index.ts | 7 +- plugins/plugin-svelte/plugin.js | 35 +- plugins/plugin-svelte/test/plugin.test.js | 1 - plugins/web-test-runner-plugin/plugin.js | 4 + snowpack/LICENSE | 27 + snowpack/package.json | 1 - snowpack/src/build/build-import-proxy.ts | 24 +- snowpack/src/build/build-pipeline.ts | 42 +- snowpack/src/build/file-builder.ts | 335 +++++ snowpack/src/build/file-urls.ts | 41 +- snowpack/src/build/import-resolver.ts | 5 +- snowpack/src/build/optimize.ts | 8 +- snowpack/src/commands/build.ts | 750 +++-------- snowpack/src/commands/dev.ts | 1092 +++++----------- snowpack/src/config.ts | 14 +- snowpack/src/index.ts | 16 +- snowpack/src/rewrite-imports.ts | 40 +- snowpack/src/scan-imports.ts | 11 +- snowpack/src/sources/local-install.ts | 110 +- snowpack/src/sources/local.ts | 380 ++++-- snowpack/src/sources/remote.ts | 34 +- snowpack/src/sources/util.ts | 8 +- snowpack/src/types.ts | 71 +- snowpack/src/util.ts | 17 +- test-dev/__snapshots__/dev.test.ts.snap | 8 +- test/build/cdn/cdn.test.js | 4 +- test/build/cdn/src/index.jsx | 4 +- .../__snapshots__/config-alias.test.js.snap | 12 +- test/build/config-alias/config-alias.test.js | 2 +- test/build/config-alias/snowpack.config.js | 5 +- test/build/config-alias/src/index.html | 6 +- test/build/config-alias/src/index.js | 6 +- test/build/config-mount/config-mount.test.js | 2 +- test/build/config-mount/src/g/main.html | 2 +- test/build/config-out-flag/src/index.js | 2 +- test/build/config-out/src/index.js | 2 +- test/build/import-json/import-json.test.js | 1 + .../packages/json-test-pkg/package.json | 2 +- .../module-resolution.test.js | 2 +- .../package-workspace.test.js | 21 + test/build/package-workspace/package.json | 15 + .../package-workspace/snowpack.config.js | 15 + test/build/package-workspace/src/index.html | 22 + test/build/package-workspace/src/index.svelte | 10 + .../plugin-build-svelte.test.js.snap | 12 + test/build/plugin-build-svelte/package.json | 16 + .../svelte-package-a/SvelteComponent.svelte | 1 + .../packages/svelte-package-a/index.js | 3 + .../packages/svelte-package-a/package.json | 6 + .../plugin-build-svelte.test.js | 26 + .../plugin-build-svelte/snowpack.config.js | 10 + .../plugin-build-svelte/src/index.svelte | 12 + test/build/test-workspace-component/README.md | 3 + .../SvelteComponent.svelte | 10 + test/build/test-workspace-component/index.mjs | 3 + .../test-workspace-component/package.json | 10 + .../create-snowpack-app.test.js.snap | 1094 +++++++---------- test/test-utils.js | 1 + yarn.lock | 28 +- 59 files changed, 2062 insertions(+), 2389 deletions(-) create mode 100644 snowpack/src/build/file-builder.ts create mode 100644 test/build/package-workspace/package-workspace.test.js create mode 100644 test/build/package-workspace/package.json create mode 100644 test/build/package-workspace/snowpack.config.js create mode 100644 test/build/package-workspace/src/index.html create mode 100644 test/build/package-workspace/src/index.svelte create mode 100644 test/build/plugin-build-svelte/__snapshots__/plugin-build-svelte.test.js.snap create mode 100644 test/build/plugin-build-svelte/package.json create mode 100644 test/build/plugin-build-svelte/packages/svelte-package-a/SvelteComponent.svelte create mode 100644 test/build/plugin-build-svelte/packages/svelte-package-a/index.js create mode 100644 test/build/plugin-build-svelte/packages/svelte-package-a/package.json create mode 100644 test/build/plugin-build-svelte/plugin-build-svelte.test.js create mode 100644 test/build/plugin-build-svelte/snowpack.config.js create mode 100644 test/build/plugin-build-svelte/src/index.svelte create mode 100644 test/build/test-workspace-component/README.md create mode 100755 test/build/test-workspace-component/SvelteComponent.svelte create mode 100755 test/build/test-workspace-component/index.mjs create mode 100644 test/build/test-workspace-component/package.json diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 64644178e9..de8f342f02 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -308,7 +308,7 @@ ${colors.dim( input: installEntrypoints, context: userDefinedRollup.context, external: (id) => external.some((packageName) => isImportOfPackage(id, packageName)), - treeshake: {moduleSideEffects: 'no-external'}, + treeshake: {moduleSideEffects: true}, plugins: [ rollupPluginAlias({ entries: [ @@ -346,7 +346,7 @@ ${colors.dim( rollupPluginReplace(generateEnvReplacements(env)), rollupPluginCommonjs({ extensions: ['.js', '.cjs'], - esmExternals: externalEsm, + esmExternals: (id) => externalEsm.some((packageName) => isImportOfPackage(id, packageName)), requireReturnsDefault: 'auto', } as RollupCommonJSOptions), rollupPluginWrapInstallTargets(!!isTreeshake, autoDetectNamedExports, installTargets, logger), @@ -366,8 +366,7 @@ ${colors.dim( isFatalWarningFound = true; // Display posix-style on all environments, mainly to help with CI :) if (warning.id) { - const fileName = path.relative(cwd, warning.id).replace(/\\/g, '/'); - logger.error(`${fileName}\n ${warning.message}`); + logger.error(`${warning.id}\n ${warning.message}`); } else { logger.error( `${warning.message}. See https://www.snowpack.dev/reference/common-error-details`, diff --git a/plugins/plugin-svelte/plugin.js b/plugins/plugin-svelte/plugin.js index f5cf9714c1..6e1333e9b5 100644 --- a/plugins/plugin-svelte/plugin.js +++ b/plugins/plugin-svelte/plugin.js @@ -13,22 +13,27 @@ module.exports = function plugin(snowpackConfig, pluginOptions = {}) { const isDev = process.env.NODE_ENV !== 'production'; const useSourceMaps = snowpackConfig.buildOptions.sourcemap || snowpackConfig.buildOptions.sourceMaps; + // Old Snowpack versions wouldn't build dependencies. Starting in v3.1, Snowpack's build pipeline + // is run on all files, including npm package files. The rollup plugin is no longer needed. + const needsRollupPlugin = typeof snowpackConfig.buildOptions.resolveProxyImports === 'undefined'; // Support importing Svelte files when you install dependencies. const packageOptions = snowpackConfig.packageOptions || snowpackConfig.installOptions; if (packageOptions.source === 'local') { - packageOptions.rollup = packageOptions.rollup || {}; - packageOptions.rollup.plugins = packageOptions.rollup.plugins || []; - packageOptions.rollup.plugins.push( - svelteRollupPlugin({ - include: /\.svelte$/, - compilerOptions: {dev: isDev}, - // Snowpack wraps JS-imported CSS in a JS wrapper, so use - // Svelte's own first-class `emitCss: false` here. - // TODO: Remove once Snowpack adds first-class CSS import support in deps. - emitCss: false, - }), - ); + if (needsRollupPlugin) { + packageOptions.rollup = packageOptions.rollup || {}; + packageOptions.rollup.plugins = packageOptions.rollup.plugins || []; + packageOptions.rollup.plugins.push( + svelteRollupPlugin({ + include: /\.svelte$/, + compilerOptions: {dev: isDev}, + // Snowpack wraps JS-imported CSS in a JS wrapper, so use + // Svelte's own first-class `emitCss: false` here. + // TODO: Remove once Snowpack adds first-class CSS import support in deps. + emitCss: false, + }), + ); + } // Support importing sharable Svelte components. packageOptions.packageLookupFields.push('svelte'); } @@ -97,7 +102,7 @@ module.exports = function plugin(snowpackConfig, pluginOptions = {}) { 'svelte-hmr/runtime/hot-api-esm.js', 'svelte-hmr/runtime/proxy-adapter-dom.js', ], - async load({filePath, isHmrEnabled, isSSR}) { + async load({filePath, isHmrEnabled, isSSR, isPackage}) { let codeToCompile = await fs.promises.readFile(filePath, 'utf-8'); // PRE-PROCESS if (preprocessOptions !== false) { @@ -110,9 +115,9 @@ module.exports = function plugin(snowpackConfig, pluginOptions = {}) { const finalCompileOptions = { generate: isSSR ? 'ssr' : 'dom', - css: false, + css: isPackage ? true : false, ...compilerOptions, // Note(drew) should take precedence over generate above - dev: isDev, + dev: isHmrEnabled || isDev, outputFilename: filePath, filename: filePath, }; diff --git a/plugins/plugin-svelte/test/plugin.test.js b/plugins/plugin-svelte/test/plugin.test.js index c8e8bf930f..6fda7fe7dc 100644 --- a/plugins/plugin-svelte/test/plugin.test.js +++ b/plugins/plugin-svelte/test/plugin.test.js @@ -15,7 +15,6 @@ describe('@snowpack/plugin-svelte (mocked)', () => { buildOptions: {sourcemap: false}, packageOptions: { source: 'local', - rollup: {plugins: []}, packageLookupFields: [], }, }; diff --git a/plugins/web-test-runner-plugin/plugin.js b/plugins/web-test-runner-plugin/plugin.js index 27c1587786..8b4ebc62ae 100644 --- a/plugins/web-test-runner-plugin/plugin.js +++ b/plugins/web-test-runner-plugin/plugin.js @@ -27,6 +27,10 @@ To Resolve: packageOptions: {external: ['/__web-dev-server__web-socket.js']}, devOptions: {open: 'none', output: 'stream', hmr: false}, }); + // npm packages should be installed/prepared ahead of time. + console.log('[snowpack] preparing npm packages...'); + await snowpack.preparePackages({config, lockfile: null}); + console.log('[snowpack] starting server...'); fileWatcher.add(Object.keys(config.mount)); server = await snowpack.startServer({ config, diff --git a/snowpack/LICENSE b/snowpack/LICENSE index 4fa554b1c2..9f8a8ff785 100644 --- a/snowpack/LICENSE +++ b/snowpack/LICENSE @@ -19,3 +19,30 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +""" + +This license applies to parts of the src/dev.ts file originating from the +https://github.com/lukejacksonn/servor repository: + +MIT License +Copyright (c) 2019 Luke Jackson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/snowpack/package.json b/snowpack/package.json index e722ca9611..8f5cd45bfd 100644 --- a/snowpack/package.json +++ b/snowpack/package.json @@ -57,7 +57,6 @@ "devDependencies": { "@types/cheerio": "0.22.22", "bufferutil": "^4.0.2", - "cacache": "^15.0.0", "cachedir": "^2.3.0", "cheerio": "1.0.0-rc.3", "chokidar": "^3.4.0", diff --git a/snowpack/src/build/build-import-proxy.ts b/snowpack/src/build/build-import-proxy.ts index 6ac687ed3b..46958a216c 100644 --- a/snowpack/src/build/build-import-proxy.ts +++ b/snowpack/src/build/build-import-proxy.ts @@ -6,8 +6,8 @@ import {SnowpackConfig} from '../types'; import {appendHtmlToHead, hasExtension, HMR_CLIENT_CODE, HMR_OVERLAY_CODE} from '../util'; import {generateSRI} from './import-sri'; -const SRI_CLIENT_HMR_SNOWPACK = generateSRI(Buffer.from(HMR_CLIENT_CODE)); -const SRI_ERROR_HMR_SNOWPACK = generateSRI(Buffer.from(HMR_OVERLAY_CODE)); +export const SRI_CLIENT_HMR_SNOWPACK = generateSRI(Buffer.from(HMR_CLIENT_CODE)); +export const SRI_ERROR_HMR_SNOWPACK = generateSRI(Buffer.from(HMR_OVERLAY_CODE)); const importMetaRegex = /import\s*\.\s*meta/; @@ -235,18 +235,16 @@ export async function wrapImportProxy({ hmr: boolean; config: SnowpackConfig; }) { - if (typeof code === 'string') { - if (hasExtension(url, '.json')) { - return generateJsonImportProxy({code, hmr, config}); - } + if (hasExtension(url, '.json')) { + return generateJsonImportProxy({code: code.toString(), hmr, config}); + } - if (hasExtension(url, '.css')) { - // if proxying a CSS file, remove its source map (the path no longer applies) - const sanitized = code.replace(/\/\*#\s*sourceMappingURL=[^/]+\//gm, ''); - return hasExtension(url, '.module.css') - ? generateCssModuleImportProxy({url, code: sanitized, hmr, config}) - : generateCssImportProxy({code: sanitized, hmr, config}); - } + if (hasExtension(url, '.css')) { + // if proxying a CSS file, remove its source map (the path no longer applies) + const sanitized = code.toString().replace(/\/\*#\s*sourceMappingURL=[^/]+\//gm, ''); + return hasExtension(url, '.module.css') + ? generateCssModuleImportProxy({url, code: sanitized, hmr, config}) + : generateCssImportProxy({code: sanitized, hmr, config}); } return generateDefaultImportProxy(url); diff --git a/snowpack/src/build/build-pipeline.ts b/snowpack/src/build/build-pipeline.ts index b89eed7716..1fe59f01b1 100644 --- a/snowpack/src/build/build-pipeline.ts +++ b/snowpack/src/build/build-pipeline.ts @@ -1,42 +1,19 @@ import path from 'path'; +import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map'; import url from 'url'; import {validatePluginLoadResult} from '../config'; import {logger} from '../logger'; -import {SnowpackBuildMap, SnowpackConfig, SnowpackPlugin, PluginTransformResult} from '../types'; -import {getExtension, readFile, removeExtension, replaceExtension} from '../util'; -import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map'; +import {PluginTransformResult, SnowpackBuildMap, SnowpackConfig} from '../types'; +import {getExtension, readFile, removeExtension} from '../util'; export interface BuildFileOptions { isDev: boolean; isSSR: boolean; + isPackage: boolean; isHmrEnabled: boolean; config: SnowpackConfig; } -export function getInputsFromOutput(fileLoc: string, plugins: SnowpackPlugin[]) { - const srcFile = removeExtension(fileLoc, '.map'); // if this is a .map file, try loading source - - const potentialInputs = new Set([srcFile]); - for (const plugin of plugins) { - if (!plugin.resolve) { - continue; - } - const isHubExt = plugin.resolve.output.length > 1; - const matchedOutputExt = plugin.resolve.output.find((ext) => srcFile.endsWith(ext)); - if (!matchedOutputExt) { - continue; - } - plugin.resolve.input.forEach((inputExt) => - potentialInputs.add( - isHubExt - ? removeExtension(srcFile, matchedOutputExt) - : replaceExtension(srcFile, matchedOutputExt, inputExt), - ), - ); - } - return Array.from(potentialInputs); -} - /** * Build Plugin First Pass: If a plugin defines a * `resolve` object, check it against the current @@ -48,7 +25,7 @@ export function getInputsFromOutput(fileLoc: string, plugins: SnowpackPlugin[]) */ async function runPipelineLoadStep( srcPath: string, - {isDev, isSSR, isHmrEnabled, config}: BuildFileOptions, + {isDev, isSSR, isPackage, isHmrEnabled, config}: BuildFileOptions, ): Promise { const srcExt = getExtension(srcPath); for (const step of config.plugins) { @@ -66,6 +43,7 @@ async function runPipelineLoadStep( filePath: srcPath, isDev, isSSR, + isPackage, isHmrEnabled, }); logger.debug(`✔ load() success [${debugPath}]`, {name: step.name}); @@ -143,7 +121,7 @@ async function composeSourceMaps( async function runPipelineTransformStep( output: SnowpackBuildMap, srcPath: string, - {isDev, isHmrEnabled, isSSR, config}: BuildFileOptions, + {isDev, isHmrEnabled, isPackage, isSSR, config}: BuildFileOptions, ): Promise { const rootFilePath = removeExtension(srcPath, getExtension(srcPath)); const rootFileName = path.basename(rootFilePath); @@ -163,6 +141,7 @@ async function runPipelineTransformStep( const result = await step.transform({ contents: code, isDev, + isPackage, fileExt: destExt, id: filePath, // @ts-ignore: Deprecated @@ -206,7 +185,10 @@ async function runPipelineTransformStep( return output; } -export async function runPipelineOptimizeStep(buildDirectory: string, {config}: BuildFileOptions) { +export async function runPipelineOptimizeStep( + buildDirectory: string, + {config}: {config: SnowpackConfig}, +) { for (const step of config.plugins) { if (!step.optimize) { continue; diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts new file mode 100644 index 0000000000..6fae8b0ca1 --- /dev/null +++ b/snowpack/src/build/file-builder.ts @@ -0,0 +1,335 @@ +import {InstallTarget} from 'esinstall'; +import {promises as fs} from 'fs'; +import mkdirp from 'mkdirp'; +import path from 'path'; +import url from 'url'; +import {EsmHmrEngine} from '../hmr-server-engine'; +import { + scanCodeImportsExports, + transformEsmImports, + transformFileImports, +} from '../rewrite-imports'; +import {matchDynamicImportValue, scanImportsFromFiles} from '../scan-imports'; +import {getPackageSource} from '../sources/util'; +import { + ImportMap, + SnowpackBuildMap, + SnowpackBuildResultFileManifest, + SnowpackBuiltFile, + SnowpackConfig, +} from '../types'; +import {createInstallTarget, isRemoteUrl, relativeURL, replaceExtension} from '../util'; +import { + getMetaUrlPath, + SRI_CLIENT_HMR_SNOWPACK, + SRI_ERROR_HMR_SNOWPACK, + wrapHtmlResponse, + wrapImportMeta, + wrapImportProxy, +} from './build-import-proxy'; +import {buildFile} from './build-pipeline'; +import {getUrlsForFile} from './file-urls'; +import {createImportResolver} from './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). + */ +export class FileBuilder { + buildOutput: SnowpackBuildMap = {}; + resolvedOutput: SnowpackBuildMap = {}; + + isDev: boolean; + isHMR: boolean; + isSSR: boolean; + buildPromise: Promise | undefined; + + readonly loc: string; + readonly urls: string[]; + readonly config: SnowpackConfig; + hmrEngine: EsmHmrEngine | null = null; + + constructor({ + loc, + isDev, + isHMR, + isSSR, + config, + hmrEngine, + }: { + loc: string; + isDev: boolean; + isHMR: boolean; + isSSR: boolean; + config: SnowpackConfig; + hmrEngine?: EsmHmrEngine | null; + }) { + this.loc = loc; + this.isDev = isDev; + this.isHMR = isHMR; + this.isSSR = isSSR; + this.config = config; + this.hmrEngine = hmrEngine || null; + this.urls = getUrlsForFile(loc, config); + } + + private verifyRequestFromBuild(type: string): SnowpackBuiltFile { + // Verify that the requested file exists in the build output map. + if (!this.resolvedOutput[type] || !Object.keys(this.resolvedOutput)) { + throw new Error( + `${this.loc} - Requested content "${type}" but built ${Object.keys(this.resolvedOutput)}`, + ); + } + 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( + isResolveBareImports: boolean, + hmrParam?: string | false, + importMap?: ImportMap, + ): Promise { + const urlPathDirectory = path.posix.dirname(this.urls[0]!); + const pkgSource = getPackageSource(this.config.packageOptions.source); + const resolvedImports: InstallTarget[] = []; + 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 = `./${replaceExtension( + path.posix.basename(this.urls[0]!), + '.js', + '.css', + )}`; + contents = `import '${relativeCssImport}';\n` + contents; + } + // Finalize the response + contents = this.finalizeResult(type, contents); + // resolve all imports + const resolveImportSpecifier = createImportResolver({ + fileLoc: this.loc, + config: this.config, + }); + const resolveImport = async (spec) => { + // Try to resolve the specifier to a known URL in the project + let resolvedImportUrl = resolveImportSpecifier(spec); + if (!isResolveBareImports) { + return resolvedImportUrl || spec; + } + // Handle a package import + if (!resolvedImportUrl && importMap) { + if (importMap.imports[spec]) { + const PACKAGE_PATH_PREFIX = path.posix.join( + this.config.buildOptions.metaUrlPath, + 'pkg/', + ); + return path.posix.join(PACKAGE_PATH_PREFIX, importMap.imports[spec]); + } + throw new Error(`Unexpected: spec ${spec} not included in import map.`); + } + // Ignore packages marked as external + if (this.config.packageOptions.external?.includes(spec)) { + return spec; + } + if (isRemoteUrl(spec)) { + return spec; + } + if (!resolvedImportUrl) { + resolvedImportUrl = await pkgSource.resolvePackageImport(this.loc, spec, this.config); + } + return resolvedImportUrl || spec; + }; + + const scannedImports = await scanImportsFromFiles( + [ + { + baseExt: type, + root: this.config.root, + locOnDisk: this.loc, + contents, + }, + ], + this.config, + ); + contents = await transformFileImports({type, contents}, async (spec) => { + let resolvedImportUrl = await resolveImport(spec); + + // Handle normal "./" & "../" import specifiers + const importExtName = path.posix.extname(resolvedImportUrl); + const isProxyImport = importExtName && importExtName !== '.js' && importExtName !== '.mjs'; + const isAbsoluteUrlPath = path.posix.isAbsolute(resolvedImportUrl); + if (isAbsoluteUrlPath) { + if (this.config.buildOptions.resolveProxyImports && isProxyImport) { + resolvedImportUrl = resolvedImportUrl + '.proxy.js'; + } + resolvedImports.push(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 = 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(SRI_CLIENT_HMR_SNOWPACK)) { + resolvedImports.push(createInstallTarget(getMetaUrlPath('hmr-client.js', this.config))); + } + if (contents.includes(SRI_ERROR_HMR_SNOWPACK)) { + resolvedImports.push( + createInstallTarget(getMetaUrlPath('hmr-error-overlay.js', this.config)), + ); + } + } + + if (type === '.js' && hmrParam) { + contents = await transformEsmImports(contents as string, (imp) => { + const importUrl = path.posix.resolve(urlPathDirectory, imp); + const node = this.hmrEngine?.getEntry(importUrl); + if (node && node.needsReplacement) { + this.hmrEngine?.markEntryForReplacement(node, false); + return `${imp}?${hmrParam}`; + } + return imp; + }); + } + + if (type === '.js') { + const isHmrEnabled = contents.includes('import.meta.hot'); + const rawImports = await scanCodeImportsExports(contents); + const resolvedImports = rawImports.map((imp) => { + let spec = contents.substring(imp.s, imp.e); + if (imp.d > -1) { + spec = matchDynamicImportValue(spec) || ''; + } + spec = spec.replace(/\?mtime=[0-9]+$/, ''); + return path.posix.resolve(urlPathDirectory, spec); + }); + this.hmrEngine?.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: boolean) { + if (this.buildPromise) { + return this.buildPromise; + } + const fileBuilderPromise = (async () => { + if (isStatic) { + return { + [path.extname(this.loc)]: { + code: await fs.readFile(this.loc), + map: undefined, + }, + }; + } + const builtFileOutput = await buildFile(url.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; + } + } + + private finalizeResult(type: string, content: string): string { + // Wrap the response. + switch (type) { + case '.html': { + content = wrapHtmlResponse({ + code: content as string, + 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 = wrapImportMeta({ + code: content as string, + env: true, + hmr: this.isHMR, + config: this.config, + }); + } + break; + } + // Return the finalized response. + return content; + } + + getResult(type: string): string | Buffer { + const {code /*, map */} = this.verifyRequestFromBuild(type); + return code; + } + + getSourceMap(type: string): string | undefined { + return this.resolvedOutput[type].map; + } + + async getProxy(url: string, type: string) { + const code = this.resolvedOutput[type].code; + return await wrapImportProxy({url, code, hmr: this.isHMR, config: this.config}); + } + + async writeToDisk(dir: string, results: SnowpackBuildResultFileManifest) { + await mkdirp(path.dirname(path.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.writeFile(path.join(dir, outUrl), buildOutput, encoding); + } + } +} diff --git a/snowpack/src/build/file-urls.ts b/snowpack/src/build/file-urls.ts index e7a451b2ed..6194e89b5d 100644 --- a/snowpack/src/build/file-urls.ts +++ b/snowpack/src/build/file-urls.ts @@ -1,11 +1,12 @@ import path from 'path'; +import slash from 'slash'; import {MountEntry, SnowpackConfig} from '../types'; -import {replaceExtension, getExtensionMatch, addExtension} from '../util'; +import {addExtension, getExtensionMatch, replaceExtension} from '../util'; /** * Map a file path to the hosted URL for a given "mount" entry. */ -export function getUrlForFileMount({ +export function getUrlsForFileMount({ fileLoc, mountKey, mountEntry, @@ -15,23 +16,32 @@ export function getUrlForFileMount({ mountKey: string; mountEntry: MountEntry; config: SnowpackConfig; -}): string { - const fileName = path.basename(fileLoc); +}): string[] { const resolvedDirUrl = mountEntry.url === '/' ? '' : mountEntry.url; const mountedUrl = fileLoc.replace(mountKey, resolvedDirUrl).replace(/[/\\]+/g, '/'); if (mountEntry.static) { - return mountedUrl; + return [mountedUrl]; } + return getBuiltFileUrls(mountedUrl, config); +} + +/** + * Map a file path to the hosted URL for a given "mount" entry. + */ +export function getBuiltFileUrls(filepath: string, config: SnowpackConfig): string[] { + const fileName = path.basename(filepath); const extensionMatch = getExtensionMatch(fileName, config._extensionMap); if (!extensionMatch) { - return mountedUrl; + return [filepath]; } const [inputExt, outputExts] = extensionMatch; - if (outputExts.length > 1) { - return addExtension(mountedUrl, outputExts[0]); - } else { - return replaceExtension(mountedUrl, inputExt, outputExts[0]); - } + return outputExts.map((outputExt) => { + if (outputExts.length > 1) { + return addExtension(filepath, outputExt); + } else { + return replaceExtension(filepath, inputExt, outputExt); + } + }); } /** @@ -59,11 +69,14 @@ export function getMountEntryForFile( /** * Get the final, hosted URL path for a given file on disk. */ -export function getUrlForFile(fileLoc: string, config: SnowpackConfig): string | null { +export function getUrlsForFile(fileLoc: string, config: SnowpackConfig): string[] { const mountEntryResult = getMountEntryForFile(fileLoc, config); if (!mountEntryResult) { - return null; + const builtEntrypointUrls = getBuiltFileUrls(fileLoc, config); + return builtEntrypointUrls.map((u) => + path.posix.join(config.buildOptions.metaUrlPath, 'link', slash(path.relative(config.root, u))), + ); } const [mountKey, mountEntry] = mountEntryResult; - return getUrlForFileMount({fileLoc, mountKey, mountEntry, config}); + return getUrlsForFileMount({fileLoc, mountKey, mountEntry, config}); } diff --git a/snowpack/src/build/import-resolver.ts b/snowpack/src/build/import-resolver.ts index a815d17a6b..af7acb51c7 100644 --- a/snowpack/src/build/import-resolver.ts +++ b/snowpack/src/build/import-resolver.ts @@ -9,7 +9,7 @@ import { isRemoteUrl, replaceExtension, } from '../util'; -import {getUrlForFile} from './file-urls'; +import {getUrlsForFile} from './file-urls'; /** Perform a file disk lookup for the requested import specifier. */ export function getFsStat(importedFileOnDisk: string): fs.Stats | false { @@ -59,7 +59,8 @@ function resolveSourceSpecifier(lazyFileLoc: string, config: SnowpackConfig) { } } - return getUrlForFile(lazyFileLoc, config); + const resolvedUrls = getUrlsForFile(lazyFileLoc, config); + return resolvedUrls ? resolvedUrls[0] : resolvedUrls; } /** diff --git a/snowpack/src/build/optimize.ts b/snowpack/src/build/optimize.ts index 9e4e70fcd5..5ae931ff14 100644 --- a/snowpack/src/build/optimize.ts +++ b/snowpack/src/build/optimize.ts @@ -16,7 +16,7 @@ import { removeTrailingSlash, deleteFromBuildSafe, } from '../util'; -import {getUrlForFile} from './file-urls'; +import {getUrlsForFile} from './file-urls'; interface ESBuildMetaInput { bytes: number; @@ -291,11 +291,11 @@ async function resolveEntrypoints( const resolvedSourceFile = path.resolve(cwd, entrypoint); let resolvedSourceEntrypoint: string | undefined; if (await fs.stat(resolvedSourceFile).catch(() => null)) { - const resolvedSourceUrl = getUrlForFile(resolvedSourceFile, config); - if (resolvedSourceUrl) { + const resolvedSourceUrls = getUrlsForFile(resolvedSourceFile, config); + if (resolvedSourceUrls) { resolvedSourceEntrypoint = path.resolve( buildDirectoryLoc, - removeLeadingSlash(resolvedSourceUrl), + removeLeadingSlash(resolvedSourceUrls[0]), ); if (await fs.stat(resolvedSourceEntrypoint).catch(() => null)) { return resolvedSourceEntrypoint; diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index 570056a5f4..3a29157ce4 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -1,79 +1,31 @@ +import {ImportMap, InstallTarget} from 'esinstall'; import {promises as fs} from 'fs'; import glob from 'glob'; import * as colors from 'kleur/colors'; import mkdirp from 'mkdirp'; -import PQueue from 'p-queue'; import path from 'path'; import {performance} from 'perf_hooks'; -import url from 'url'; -import { - generateEnvModule, - wrapHtmlResponse, - wrapImportMeta, - wrapImportProxy, -} from '../build/build-import-proxy'; -import {buildFile, runPipelineCleanupStep, runPipelineOptimizeStep} from '../build/build-pipeline'; -import {getMountEntryForFile, getUrlForFileMount} from '../build/file-urls'; -import {createImportResolver} from '../build/import-resolver'; +import {wrapImportProxy} from '../build/build-import-proxy'; +import {runPipelineCleanupStep, runPipelineOptimizeStep} from '../build/build-pipeline'; +import {getUrlsForFile} from '../build/file-urls'; import {runBuiltInOptimize} from '../build/optimize'; -import {EsmHmrEngine} from '../hmr-server-engine'; import {logger} from '../logger'; -import {transformFileImports} from '../rewrite-imports'; -import {getInstallTargets} from '../scan-imports'; -import localPackageSource from '../sources/local'; +import {installPackages} from '../sources/local-install'; +import {getPackageSource} from '../sources/util'; import { + LoadUrlOptions, CommandOptions, - ImportMap, - MountEntry, OnFileChangeCallback, SnowpackBuildResult, - SnowpackBuildResultFileManifest, SnowpackConfig, - SnowpackSourceFile, } from '../types'; -import { - addExtension, - cssSourceMappingURL, - deleteFromBuildSafe, - getExtensionMatch, - HMR_CLIENT_CODE, - HMR_OVERLAY_CODE, - isFsEventsEnabled, - isRemoteUrl, - jsSourceMappingURL, - readFile, - relativeURL, - removeLeadingSlash, - replaceExtension, -} from '../util'; -import {run as installRunner} from '../sources/local-install'; -import {getPackageSource} from '../sources/util'; - -const CONCURRENT_WORKERS = require('os').cpus().length; +import {deleteFromBuildSafe, isRemoteUrl} from '../util'; +import {startServer} from './dev'; -let hmrEngine: EsmHmrEngine | null = null; function getIsHmrEnabled(config: SnowpackConfig) { return config.buildOptions.watch && !!config.devOptions.hmr; } -function handleFileError(err: Error, builder: FileBuilder) { - logger.error(`✘ ${builder.fileURL}`); - throw err; -} - -function createBuildFileManifest(allFiles: FileBuilder[]): SnowpackBuildResultFileManifest { - const result: SnowpackBuildResultFileManifest = {}; - for (const sourceFile of allFiles) { - for (const outputFile of Object.entries(sourceFile.output)) { - result[outputFile[0]] = { - source: url.fileURLToPath(sourceFile.fileURL), - contents: outputFile[1], - }; - } - } - return result; -} - /** * Scan a directory and remove any empty folders, recursively. */ @@ -98,7 +50,7 @@ async function removeEmptyFolders(directoryLoc: string): Promise { } async function installOptimizedDependencies( - scannedFiles: SnowpackSourceFile[], + installTargets: InstallTarget[], installDest: string, commandOptions: CommandOptions, ) { @@ -117,592 +69,198 @@ async function installOptimizedDependencies( config: commandOptions.config, lockfile: commandOptions.lockfile, }); - - // 1. Scan imports from your final built JS files. - // Unlike dev (where we scan from source code) the built output guarantees that we - // will can scan all used entrypoints. Set to `[]` to improve tree-shaking performance. - const installTargets = await getInstallTargets(commandOptions.config, [], scannedFiles); // 2. Install dependencies, based on the scan of your final build. - const installResult = await installRunner({ + const installResult = await installPackages({ + config: commandOptions.config, + isSSR: commandOptions.config.buildOptions.ssr, + isDev: false, installTargets, installOptions, - config: commandOptions.config, - shouldPrintStats: false, }); return installResult; } -/** - * 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 -> write to disk). - */ -class FileBuilder { - output: Record = {}; - filesToResolve: Record = {}; - filesToProxy: string[] = []; - - readonly fileURL: URL; - readonly mountEntry: MountEntry; - readonly outDir: string; - readonly config: SnowpackConfig; - - constructor({ - fileURL, - mountEntry, - outDir, - config, - }: { - fileURL: URL; - mountEntry: MountEntry; - outDir: string; - config: SnowpackConfig; - }) { - this.fileURL = fileURL; - this.mountEntry = mountEntry; - this.outDir = outDir; - this.config = config; - } - - async buildFile() { - this.filesToResolve = {}; - const isSSR = this.config.buildOptions.ssr; - const srcExt = path.extname(url.fileURLToPath(this.fileURL)); - const fileOutput = this.mountEntry.static - ? {[srcExt]: {code: await readFile(this.fileURL)}} - : await buildFile(this.fileURL, { - config: this.config, - isDev: false, - isSSR, - isHmrEnabled: false, - }); - - for (const [fileExt, buildResult] of Object.entries(fileOutput)) { - let {code, map} = buildResult; - if (!code) { - continue; - } - let outFilename = path.basename(url.fileURLToPath(this.fileURL)); - const extensionMatch = getExtensionMatch(this.fileURL.toString(), this.config._extensionMap); - if (extensionMatch) { - const [inputExt, outputExts] = extensionMatch; - if (outputExts.length > 1) { - outFilename = addExtension(path.basename(url.fileURLToPath(this.fileURL)), fileExt); - } else { - outFilename = replaceExtension( - path.basename(url.fileURLToPath(this.fileURL)), - inputExt, - fileExt, - ); - } - } - const outLoc = path.join(this.outDir, outFilename); - const sourceMappingURL = outFilename + '.map'; - if (this.mountEntry.resolve && typeof code === 'string') { - switch (fileExt) { - case '.css': { - if (map) code = cssSourceMappingURL(code, sourceMappingURL); - this.filesToResolve[outLoc] = { - baseExt: fileExt, - root: this.config.root, - contents: code, - locOnDisk: url.fileURLToPath(this.fileURL), - }; - break; - } - - case '.js': { - if (fileOutput['.css']) { - // inject CSS if imported directly - code = `import './${replaceExtension(outFilename, '.js', '.css')}';\n` + code; - } - code = wrapImportMeta({code, env: true, hmr: false, config: this.config}); - if (map) code = jsSourceMappingURL(code, sourceMappingURL); - this.filesToResolve[outLoc] = { - baseExt: fileExt, - root: this.config.root, - contents: code, - locOnDisk: url.fileURLToPath(this.fileURL), - }; - break; - } - - case '.html': { - code = wrapHtmlResponse({ - code, - hmr: getIsHmrEnabled(this.config), - hmrPort: hmrEngine ? hmrEngine.port : undefined, - isDev: false, - config: this.config, - mode: 'production', - }); - this.filesToResolve[outLoc] = { - baseExt: fileExt, - root: this.config.root, - contents: code, - locOnDisk: url.fileURLToPath(this.fileURL), - }; - break; - } - } - } - - this.output[outLoc] = code; - if (map) { - this.output[path.join(this.outDir, sourceMappingURL)] = map; - } - } - } - - async resolveImports(importMap: ImportMap) { - let isSuccess = true; - this.filesToProxy = []; - for (const [outLoc, rawFile] of Object.entries(this.filesToResolve)) { - // don’t transform binary file contents - if (Buffer.isBuffer(rawFile.contents)) { - continue; - } - const file = rawFile as SnowpackSourceFile; - const resolveImportSpecifier = createImportResolver({ - fileLoc: file.locOnDisk!, // we’re confident these are reading from disk because we just read them - config: this.config, - }); - const resolvedCode = await transformFileImports(file, (spec) => { - // Try to resolve the specifier to a known URL in the project - let resolvedImportUrl = resolveImportSpecifier(spec); - // If not resolved, then this is a package. During build, dependencies are always - // installed locally via esinstall, so use localPackageSource here. - if (importMap.imports[spec]) { - resolvedImportUrl = localPackageSource.resolvePackageImport(spec, importMap, this.config); - } - // If still not resolved, then this imported package somehow evaded detection - // when we scanned it in the previous step. If you find a bug here, report it! - if (!resolvedImportUrl) { - isSuccess = false; - logger.error(`${file.locOnDisk} - Could not resolve unknown import "${spec}".`); - return spec; - } - // Ignore "http://*" imports - if (isRemoteUrl(resolvedImportUrl)) { - return resolvedImportUrl; - } - // Ignore packages marked as external - if (this.config.packageOptions.external?.includes(resolvedImportUrl)) { - return resolvedImportUrl; - } - // Handle normal "./" & "../" import specifiers - const importExtName = path.extname(resolvedImportUrl); - const isBundling = !!this.config.optimize?.bundle; - const isProxyImport = - importExtName && - importExtName !== '.js' && - !path.posix.isAbsolute(spec) && - // If using our built-in bundler, treat CSS as a first class citizen (no proxy file needed). - // TODO: Remove special `.module.css` handling by building css modules to native JS + CSS. - (!isBundling || !/(? { const {config} = commandOptions; const isDev = !!config.buildOptions.watch; const isSSR = !!config.buildOptions.ssr; - - // Fill in any command-specific plugin methods. - // NOTE: markChanged only needed during dev, but may not be true for all. - if (isDev) { - for (const p of config.plugins) { - p.markChanged = (fileLoc) => onWatchEvent(fileLoc) || undefined; - } - } + const isHMR = getIsHmrEnabled(config); + config.buildOptions.resolveProxyImports = !config.optimize?.bundle; + config.devOptions.hmrPort = isHMR ? config.devOptions.hmrPort : undefined; + config.devOptions.port = 0; const buildDirectoryLoc = config.buildOptions.out; - const internalFilesBuildLoc = path.join(buildDirectoryLoc, config.buildOptions.metaUrlPath); - if (config.buildOptions.clean) { deleteFromBuildSafe(buildDirectoryLoc, config); } mkdirp.sync(buildDirectoryLoc); - mkdirp.sync(internalFilesBuildLoc); - for (const runPlugin of config.plugins) { - if (runPlugin.run) { - logger.debug(`starting ${runPlugin.name} run() (isDev=${isDev})`); - const runJob = runPlugin - .run({ - isDev: isDev, - // @ts-ignore: deprecated - isHmrEnabled: getIsHmrEnabled(config), - // @ts-ignore: internal API only - log: (msg, data: {msg: string} = {}) => { - if (msg === 'CONSOLE_INFO' || msg === 'WORKER_MSG') { - logger.info(data.msg.trim(), {name: runPlugin.name}); - } - }, - }) - .catch((err) => { - logger.error(err.toString(), {name: runPlugin.name}); - if (!isDev) { - process.exit(1); - } - }); - // Wait for the job to complete before continuing (unless in watch mode) - if (!isDev) { - await runJob; - } - } - } + const devServer = await startServer(commandOptions, {isDev}); - // Write the `import.meta.env` contents file to disk - logger.debug(`generating meta files`); - await fs.writeFile( - path.join(internalFilesBuildLoc, 'env.js'), - generateEnvModule({mode: 'production', isSSR}), - ); - if (getIsHmrEnabled(config)) { - await fs.writeFile(path.resolve(internalFilesBuildLoc, 'hmr-client.js'), HMR_CLIENT_CODE); - await fs.writeFile( - path.resolve(internalFilesBuildLoc, 'hmr-error-overlay.js'), - HMR_OVERLAY_CODE, - ); - hmrEngine = new EsmHmrEngine({port: config.devOptions.hmrPort}); - } - - logger.info(colors.yellow('! building source files...')); - const buildStart = performance.now(); - const buildPipelineFiles: Record = {}; - - /** Install all needed dependencies, based on the master buildPipelineFiles list. */ - async function installDependencies() { - const scannedFiles = Object.values(buildPipelineFiles) - .map((f) => Object.values(f.filesToResolve)) - .reduce((flat, item) => flat.concat(item), []); - const installDest = path.join(buildDirectoryLoc, config.buildOptions.metaUrlPath, 'pkg'); - const installResult = await installOptimizedDependencies( - scannedFiles, - installDest, - commandOptions, - ); - const allFiles = glob.sync(`**/*`, { - cwd: installDest, - absolute: true, + const allFileUrls: string[] = []; + for (const [mountKey, mountEntry] of Object.entries(config.mount)) { + logger.debug(`Mounting directory: '${mountKey}' as URL '${mountEntry.url}'`); + const files = glob.sync(path.join(mountKey, '**'), { nodir: true, - dot: true, - follow: true, + absolute: true, + ignore: [ + ...config.exclude, + ...(process.env.NODE_ENV === 'test' ? [] : config.testOptions.files), + ], }); + for (const f of files) { + const normalizedFileLoc = path.normalize(f); + const fileUrls = getUrlsForFile(normalizedFileLoc, config)!; + // Only push the first URL. In multi-file builds, this is always the JS that the + // CSS is imported from (if it exists). That CSS may not exist, and we don't know + // until the JS has been built/loaded. + allFileUrls.push(fileUrls[0]); + } + } - if (!config.optimize?.bundle) { - for (const installedFileLoc of allFiles) { - if ( - !installedFileLoc.endsWith('import-map.json') && - path.extname(installedFileLoc) !== '.js' - ) { - const proxiedCode = await readFile(url.pathToFileURL(installedFileLoc)); - const importProxyFileLoc = installedFileLoc + '.proxy.js'; - const proxiedUrl = installedFileLoc.substr(buildDirectoryLoc.length).replace(/\\/g, '/'); - const proxyCode = await wrapImportProxy({ - url: proxiedUrl, - code: proxiedCode, - hmr: false, + const pkgUrlPrefix = path.posix.join(config.buildOptions.metaUrlPath, 'pkg/'); + const allBareModuleSpecifiers: InstallTarget[] = []; + const allFileUrlsUnique = new Set(allFileUrls); + let allFileUrlsToProcess = [...allFileUrlsUnique]; + + async function flushFileQueue( + ignorePkg: boolean, + loadOptions: LoadUrlOptions & {encoding?: undefined}, + ) { + logger.debug(`QUEUE: ${allFileUrlsToProcess}`); + while (allFileUrlsToProcess.length > 0) { + const fileUrl = allFileUrlsToProcess.shift()!; + const fileDestinationLoc = path.join(buildDirectoryLoc, fileUrl); + logger.debug(`BUILD: ${fileUrl}`); + // ignore package URLs when `ignorePkg` is true, EXCEPT proxy imports. Those can sometimes + // be added after the intial package scan, depending on how a non-JS package is imported. + if (ignorePkg && fileUrl.startsWith(pkgUrlPrefix)) { + if (fileUrl.endsWith('.proxy.js')) { + const pkgContents = await fs.readFile( + path.join(buildDirectoryLoc, fileUrl.replace('.proxy.js', '')), + ); + const pkgContentsProxy = await wrapImportProxy({ + url: fileUrl.replace('.proxy.js', ''), + code: pkgContents, + hmr: isHMR, config: config, }); - await fs.writeFile(importProxyFileLoc, proxyCode, 'utf8'); + await fs.writeFile(fileDestinationLoc, pkgContentsProxy); } + continue; } - } - return installResult; - } - - // 0. Find all source files. - for (const [mountedDir, mountEntry] of Object.entries(config.mount)) { - const finalDestLocMap = new Map(); - const allFiles = glob.sync(`**/*`, { - ignore: [...config.exclude, ...config.testOptions.files], - cwd: mountedDir, - absolute: true, - nodir: true, - dot: true, - follow: true, - }); - for (const rawLocOnDisk of allFiles) { - const fileLoc = path.resolve(rawLocOnDisk); // this is necessary since glob.sync() returns paths with / on windows. path.resolve() will switch them to the native path separator. - const finalUrl = getUrlForFileMount({fileLoc, mountKey: mountedDir, mountEntry, config})!; - const finalDestLoc = path.join(buildDirectoryLoc, finalUrl); - - const existedFileLoc = finalDestLocMap.get(finalDestLoc); - if (existedFileLoc) { - const errorMessage = - `Error: Two files overlap and build to the same destination: ${finalDestLoc}\n` + - ` File 1: ${existedFileLoc}\n` + - ` File 2: ${fileLoc}\n`; - throw new Error(errorMessage); + const result = await devServer.loadUrl(fileUrl, loadOptions); + await mkdirp(path.dirname(fileDestinationLoc)); + await fs.writeFile(fileDestinationLoc, result.contents); + for (const installTarget of result.imports) { + const importedUrl = installTarget.specifier; + logger.debug(`ADD: ${importedUrl}`); + if (isRemoteUrl(importedUrl)) { + // do nothing + } else if (importedUrl.startsWith('./') || importedUrl.startsWith('../')) { + logger.warn(`warn: import "${importedUrl}" of "${fileUrl}" could not be resolved.`); + } else if (!importedUrl.startsWith('/')) { + allBareModuleSpecifiers.push(installTarget); + } else if (!allFileUrlsUnique.has(importedUrl)) { + allFileUrlsUnique.add(importedUrl); + allFileUrlsToProcess.push(importedUrl); + } } - - const outDir = path.dirname(finalDestLoc); - const buildPipelineFile = new FileBuilder({ - fileURL: url.pathToFileURL(fileLoc), - mountEntry, - outDir, - config, - }); - buildPipelineFiles[fileLoc] = buildPipelineFile; - - finalDestLocMap.set(finalDestLoc, fileLoc); } } - // 1. Build all files for the first time, from source. - const parallelWorkQueue = new PQueue({concurrency: CONCURRENT_WORKERS}); - const allBuildPipelineFiles = Object.values(buildPipelineFiles); - for (const buildPipelineFile of allBuildPipelineFiles) { - parallelWorkQueue.add(() => - buildPipelineFile.buildFile().catch((err) => handleFileError(err, buildPipelineFile)), - ); - } - await parallelWorkQueue.onIdle(); - + logger.info(colors.yellow('! building files...')); + const buildStart = performance.now(); + await flushFileQueue(false, {isSSR, isHMR, isResolve: false}); const buildEnd = performance.now(); logger.info( - `${colors.green('✔')} build complete ${colors.dim( + `${colors.green('✔')} build complete. ${colors.dim( `[${((buildEnd - buildStart) / 1000).toFixed(2)}s]`, )}`, ); - // 2. Install all dependencies. This gets us the import map we need to resolve imports. - let installResult = await installDependencies(); - - logger.info(colors.yellow('! verifying build...')); - - // 3. Resolve all built file imports. - const verifyStart = performance.now(); - for (const buildPipelineFile of allBuildPipelineFiles) { - parallelWorkQueue.add(() => - buildPipelineFile - .resolveImports(installResult.importMap!) - .catch((err) => handleFileError(err, buildPipelineFile)), + let optimizedImportMap: undefined | ImportMap; + if (!config.buildOptions.watch) { + const packagesStart = performance.now(); + const installDest = path.join(buildDirectoryLoc, config.buildOptions.metaUrlPath, 'pkg'); + const installResult = await installOptimizedDependencies( + [...allBareModuleSpecifiers], + installDest, + commandOptions, + ); + const packagesEnd = performance.now(); + logger.info( + `${colors.green('✔')} packages optimized. ${colors.dim( + `[${((packagesEnd - packagesStart) / 1000).toFixed(2)}s]`, + )}`, ); + optimizedImportMap = installResult.importMap; } - await parallelWorkQueue.onIdle(); - const verifyEnd = performance.now(); + + logger.info(colors.yellow('! writing files...')); + const writeStart = performance.now(); + allFileUrlsToProcess = [...allFileUrlsUnique]; + await flushFileQueue(!config.buildOptions.watch, { + isSSR, + isHMR, + isResolve: true, + importMap: optimizedImportMap, + }); + const writeEnd = performance.now(); logger.info( - `${colors.green('✔')} verification complete ${colors.dim( - `[${((verifyEnd - verifyStart) / 1000).toFixed(2)}s]`, + `${colors.green('✔')} write complete. ${colors.dim( + `[${((writeEnd - writeStart) / 1000).toFixed(2)}s]`, )}`, ); - // 4. Write files to disk. - logger.info(colors.yellow('! writing build to disk...')); - const allImportProxyFiles = new Set( - allBuildPipelineFiles.map((b) => b.filesToProxy).reduce((flat, item) => flat.concat(item), []), - ); - for (const buildPipelineFile of allBuildPipelineFiles) { - parallelWorkQueue.add(() => buildPipelineFile.writeToDisk()); - for (const builtFile of Object.keys(buildPipelineFile.output)) { - if (allImportProxyFiles.has(builtFile)) { - parallelWorkQueue.add(() => - buildPipelineFile - .writeProxyToDisk(builtFile) - .catch((err) => handleFileError(err, buildPipelineFile)), - ); - } - } - } - await parallelWorkQueue.onIdle(); - - const buildResultManifest = createBuildFileManifest(allBuildPipelineFiles); - // TODO(fks): Add support for virtual files (injected by snowpack, plugins) - // and web_modules in this manifest. - // buildResultManifest[path.join(internalFilesBuildLoc, 'env.js')] = { - // source: null, - // contents: generateEnvModule({mode: 'production', isSSR}), - // }; - - // "--watch --hmr" mode - Tell users about the HMR WebSocket URL - if (hmrEngine) { - logger.info( - `[HMR] WebSocket URL available at ${colors.cyan(`ws://localhost:${hmrEngine.port}`)}`, - ); - } - - // 5. Optimize the build. - if (!config.buildOptions.watch) { - if (config.optimize || config.plugins.some((p) => p.optimize)) { - const optimizeStart = performance.now(); - logger.info(colors.yellow('! optimizing build...')); - await runBuiltInOptimize(config); - await runPipelineOptimizeStep(buildDirectoryLoc, { - config, - isDev: false, - isSSR: config.buildOptions.ssr, - isHmrEnabled: false, + // "--watch" mode - Start watching the file system. + if (config.buildOptions.watch) { + logger.info(colors.cyan('watching for file changes...')); + let onFileChangeCallback: OnFileChangeCallback = () => {}; + devServer.onFileChange(async ({filePath}) => { + // First, do our own re-build logic + allFileUrlsToProcess.push(...getUrlsForFile(filePath, config)!); + await flushFileQueue(true, { + isSSR, + isHMR, + isResolve: true, + importMap: optimizedImportMap, }); - const optimizeEnd = performance.now(); - logger.info( - `${colors.green('✔')} optimize complete ${colors.dim( - `[${((optimizeEnd - optimizeStart) / 1000).toFixed(2)}s]`, - )}`, - ); - await removeEmptyFolders(buildDirectoryLoc); - await runPipelineCleanupStep(config); - logger.info(`${colors.underline(colors.green(colors.bold('▶ Build Complete!')))}\n\n`); - return { - result: buildResultManifest, - onFileChange: () => { - throw new Error('build().onFileChange() only supported in "watch" mode.'); - }, - shutdown: () => { - throw new Error('build().shutdown() only supported in "watch" mode.'); - }, - }; - } + // Then, call the user's onFileChange callback (if one was provided) + await onFileChangeCallback({filePath}); + }); + return { + onFileChange: (callback) => (onFileChangeCallback = callback), + shutdown() { + return devServer.shutdown(); + }, + }; } - // "--watch" mode - Start watching the file system. - // Defer "chokidar" loading to here, to reduce impact on overall startup time - logger.info(colors.cyan('watching for changes...')); - const chokidar = await import('chokidar'); - - function onDeleteEvent(fileLoc: string) { - delete buildPipelineFiles[fileLoc]; - } - async function onWatchEvent(fileLoc: string) { - logger.info(colors.cyan('File changed...')); - const mountEntryResult = getMountEntryForFile(fileLoc, config); - if (!mountEntryResult) { - return; - } - onFileChangeCallback({filePath: fileLoc}); - const [mountKey, mountEntry] = mountEntryResult; - const finalUrl = getUrlForFileMount({fileLoc, mountKey, mountEntry, config})!; - const finalDest = path.join(buildDirectoryLoc, finalUrl); - const outDir = path.dirname(finalDest); - const changedPipelineFile = new FileBuilder({ - fileURL: url.pathToFileURL(fileLoc), - mountEntry, - outDir, - config, - }); - buildPipelineFiles[fileLoc] = changedPipelineFile; - // 1. Build the file. - await changedPipelineFile.buildFile().catch((err) => { - logger.error(fileLoc + ' ' + err.toString(), {name: err.__snowpackBuildDetails?.name}); - hmrEngine && - hmrEngine.broadcastMessage({ - type: 'error', - title: - `Build Error` + err.__snowpackBuildDetails - ? `: ${err.__snowpackBuildDetails.name}` - : '', - errorMessage: err.toString(), - fileLoc, - errorStackTrace: err.stack, - }); - }); - // 2. Resolve any ESM imports. Handle new imports by triggering a re-install. - let resolveSuccess = await changedPipelineFile.resolveImports(installResult.importMap!); - if (!resolveSuccess) { - await installDependencies(); - resolveSuccess = await changedPipelineFile.resolveImports(installResult.importMap!); - if (!resolveSuccess) { - logger.error('Exiting...'); - process.exit(1); - } - } - // 3. Write to disk. If any proxy imports are needed, write those as well. - await changedPipelineFile.writeToDisk(); - const allBuildPipelineFiles = Object.values(buildPipelineFiles); - const allImportProxyFiles = new Set( - allBuildPipelineFiles - .map((b) => b.filesToProxy) - .reduce((flat, item) => flat.concat(item), []), + // "--optimize" mode - Optimize the build. + if (config.optimize || config.plugins.some((p) => p.optimize)) { + const optimizeStart = performance.now(); + logger.info(colors.yellow('! optimizing build...')); + await runBuiltInOptimize(config); + await runPipelineOptimizeStep(buildDirectoryLoc, {config}); + const optimizeEnd = performance.now(); + logger.info( + `${colors.green('✔')} optimize complete ${colors.dim( + `[${((optimizeEnd - optimizeStart) / 1000).toFixed(2)}s]`, + )}`, ); - for (const builtFile of Object.keys(changedPipelineFile.output)) { - if (allImportProxyFiles.has(builtFile)) { - await changedPipelineFile.writeProxyToDisk(builtFile); - } - } - - if (hmrEngine) { - hmrEngine.broadcastMessage({type: 'reload'}); - } } - const watcher = chokidar.watch(Object.keys(config.mount), { - ignored: config.exclude, - ignoreInitial: true, - persistent: true, - disableGlobbing: false, - useFsEvents: isFsEventsEnabled(), - }); - watcher.on('add', (fileLoc) => onWatchEvent(fileLoc)); - watcher.on('change', (fileLoc) => onWatchEvent(fileLoc)); - watcher.on('unlink', (fileLoc) => onDeleteEvent(fileLoc)); - - // Allow the user to hook into this callback, if they like (noop by default) - let onFileChangeCallback: OnFileChangeCallback = () => {}; + await removeEmptyFolders(buildDirectoryLoc); + await runPipelineCleanupStep(config); + logger.info(`${colors.underline(colors.green(colors.bold('▶ Build Complete!')))}\n\n`); + await devServer.shutdown(); return { - result: buildResultManifest, - onFileChange: (callback) => (onFileChangeCallback = callback), - async shutdown() { - await watcher.close(); + onFileChange: () => { + throw new Error('build().onFileChange() only supported in "watch" mode.'); + }, + shutdown: () => { + throw new Error('build().shutdown() only supported in "watch" mode.'); }, }; } @@ -712,7 +270,7 @@ export async function command(commandOptions: CommandOptions) { await build(commandOptions); } catch (err) { logger.error(err.message); - logger.debug(err.stack); + logger.error(err.stack); process.exit(1); } diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 03d875cd23..34e0af7154 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -1,98 +1,73 @@ -/** - * This license applies to parts of this file originating from the - * https://github.com/lukejacksonn/servor repository: - * - * MIT License - * Copyright (c) 2019 Luke Jackson - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import cacache from 'cacache'; import isCompressible from 'compressible'; -import {createLoader as createServerRuntime} from '../ssr-loader'; +import detectPort from 'detect-port'; +import {InstallTarget} from 'esinstall'; import etag from 'etag'; import {EventEmitter} from 'events'; import {createReadStream, promises as fs, statSync} from 'fs'; +import {glob} from 'glob'; import http from 'http'; import http2 from 'http2'; -import {isBinaryFile} from 'isbinaryfile'; import * as colors from 'kleur/colors'; import mime from 'mime-types'; import os from 'os'; import path from 'path'; import {performance} from 'perf_hooks'; import onProcessExit from 'signal-exit'; +import slash from 'slash'; import stream from 'stream'; import url from 'url'; import util from 'util'; import zlib from 'zlib'; -import { - generateEnvModule, - getMetaUrlPath, - wrapHtmlResponse, - wrapImportMeta, - wrapImportProxy, -} from '../build/build-import-proxy'; -import {buildFile as _buildFile, getInputsFromOutput} from '../build/build-pipeline'; -import {getUrlForFile} from '../build/file-urls'; -import {createImportResolver} from '../build/import-resolver'; +import {generateEnvModule, getMetaUrlPath, wrapImportProxy} from '../build/build-import-proxy'; +import {FileBuilder} from '../build/file-builder'; +import {getBuiltFileUrls, getMountEntryForFile, getUrlsForFile} from '../build/file-urls'; import {EsmHmrEngine} from '../hmr-server-engine'; import {logger} from '../logger'; -import { - scanCodeImportsExports, - transformEsmImports, - transformFileImports, -} from '../rewrite-imports'; -import {matchDynamicImportValue} from '../scan-imports'; +import {getPackageSource} from '../sources/util'; +import {createLoader as createServerRuntime} from '../ssr-loader'; import { CommandOptions, LoadResult, - MountEntry, + LoadUrlOptions, OnFileChangeCallback, RouteConfigObject, - SnowpackBuildMap, - SnowpackDevServer, ServerRuntime, + SnowpackDevServer, } from '../types'; -import { - BUILD_CACHE, - cssSourceMappingURL, - hasExtension, - HMR_CLIENT_CODE, - HMR_OVERLAY_CODE, - isFsEventsEnabled, - isRemoteUrl, - jsSourceMappingURL, - openInBrowser, - parsePackageImportSpecifier, - readFile, - relativeURL, - removeExtension, - replaceExtension, - resolveDependencyManifest, -} from '../util'; -import {getPort, getServerInfoMessage, paintDashboard, paintEvent} from './paint'; -import {getPackageSource} from '../sources/util'; +import {hasExtension, HMR_CLIENT_CODE, HMR_OVERLAY_CODE, openInBrowser} from '../util'; +import {getPort, paintDashboard, paintEvent} from './paint'; + +export class OneToManyMap { + readonly keyToValue = new Map(); + readonly valueToKey = new Map(); + add(key: string, _value: string | string[]) { + const value = Array.isArray(_value) ? _value : [_value]; + this.keyToValue.set(key, value); + for (const val of value) { + this.valueToKey.set(val, key); + } + } + delete(key: string) { + const value = this.value(key); + this.keyToValue.delete(key); + if (value) { + for (const val of value) { + this.keyToValue.delete(val); + } + } + } + key(value: string) { + return this.valueToKey.get(value); + } + value(key: string) { + return this.keyToValue.get(key); + } +} interface FoundFile { - fileLoc: string; + loc: string; + type: string; + // contents: Buffer; isStatic: boolean; isResolve: boolean; } @@ -131,11 +106,12 @@ function getCacheKey(fileLoc: string, {isSSR, env}) { * A helper class for "Not Found" errors, storing data about what file lookups were attempted. */ class NotFoundError extends Error { - lookups: string[]; - - constructor(lookups: string[]) { - super('NOT_FOUND'); - this.lookups = lookups; + constructor(url: string, lookups?: string[]) { + if (!lookups) { + super(`Not Found (${url})`); + } else { + super(`Not Found (${url}):\n${lookups.map((loc) => ' ✘ ' + loc).join('\n')}`); + } } } @@ -231,12 +207,12 @@ function handleResponseError(req, res, err: Error | 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') { - const attemptedFilesMessage = err.lookups.map((loc) => ' ✘ ' + loc).join('\n'); - logger.error(`[404] ${req.url}\n${attemptedFilesMessage}`); + logger.error(`[404] ${err.message}`); } sendResponseError(req, res, 404); return; } + console.log(err); logger.error(err.toString()); logger.error(`[500] ${req.url}`, { // @ts-ignore @@ -264,20 +240,26 @@ function getServerRuntime( return runtime; } -export async function startServer(commandOptions: CommandOptions): Promise { +export async function startServer( + commandOptions: CommandOptions, + {isDev}: {isDev: boolean} = {isDev: true}, +): Promise { const {config} = commandOptions; // Start the startup timer! let serverStart = performance.now(); const {port: defaultPort, hostname, open} = config.devOptions; const messageBus = new EventEmitter(); - const port = await getPort(defaultPort); const pkgSource = getPackageSource(config.packageOptions.source); const PACKAGE_PATH_PREFIX = path.posix.join(config.buildOptions.metaUrlPath, 'pkg/'); - - // Reset the clock if we had to wait for the user prompt to select a new port. - if (port !== defaultPort) { - serverStart = performance.now(); + const PACKAGE_LINK_PATH_PREFIX = path.posix.join(config.buildOptions.metaUrlPath, 'link/'); + let port: number | undefined; + if (defaultPort !== 0) { + port = await getPort(defaultPort); + // Reset the clock if we had to wait for the user prompt to select a new port. + if (port !== defaultPort) { + serverStart = performance.now(); + } } // Fill in any command-specific plugin methods. @@ -308,24 +290,32 @@ export async function startServer(commandOptions: CommandOptions): Promise { - console.log(getServerInfoMessage(info)); + logger.info(`Server started in ${info.startTimeMs}ms.`); }); } - const inMemoryBuildCache = new Map(); - const filesBeingDeleted = new Set(); - const filesBeingBuilt = new Map>(); + const symlinkDirectories = new Set(); + const inMemoryBuildCache = new Map(); + let fileToUrlMapping = new OneToManyMap(); + + for (const [mountKey, mountEntry] of Object.entries(config.mount)) { + logger.debug(`Mounting directory: '${mountKey}' as URL '${mountEntry.url}'`); + const files = glob.sync(path.join(mountKey, '**'), { + absolute: true, + nodir: true, + ignore: [ + ...config.exclude, + ...(process.env.NODE_ENV === 'test' ? [] : config.testOptions.files), + ], + }); + for (const f of files) { + const normalizedFileLoc = path.normalize(f); + fileToUrlMapping.add(normalizedFileLoc, getUrlsForFile(normalizedFileLoc, config)!); + } + } - logger.debug(`Using in-memory cache.`); - logger.debug(`Mounting directories:`, { - task: () => { - for (const [mountKey, mountEntry] of Object.entries(config.mount)) { - logger.debug(` -> '${mountKey}' as URL '${mountEntry.url}'`); - } - }, - }); + logger.debug(`Using in-memory cache: ${fileToUrlMapping}`); - let sourceImportMap = await pkgSource.prepare(commandOptions); const readCredentials = async (cwd: string) => { const [cert, key] = await Promise.all([ fs.readFile(path.join(cwd, 'snowpack.crt')), @@ -365,10 +355,10 @@ export async function startServer(commandOptions: CommandOptions): Promise { if (msg === 'CONSOLE_INFO') { @@ -390,63 +380,39 @@ export async function startServer(commandOptions: CommandOptions): Promise>; function loadUrl( reqUrl: string, - { - isSSR: _isSSR, - allowStale: _allowStale, - encoding: _encoding, - }: {isSSR?: boolean; allowStale?: boolean; encoding: BufferEncoding}, + opt: LoadUrlOptions & {encoding: BufferEncoding}, ): Promise>; function loadUrl( reqUrl: string, - { - isSSR: _isSSR, - allowStale: _allowStale, - encoding: _encoding, - }: {isSSR?: boolean; allowStale?: boolean; encoding: null}, + opt: LoadUrlOptions & {encoding: null}, ): Promise>; async function loadUrl( reqUrl: string, { isSSR: _isSSR, isHMR: _isHMR, - allowStale: _allowStale, + isResolve: _isResolve, encoding: _encoding, - }: { - isSSR?: boolean; - isHMR?: boolean; - allowStale?: boolean; - encoding?: BufferEncoding | null; - } = {}, + importMap, + }: LoadUrlOptions = {}, ): Promise { const isSSR = _isSSR ?? false; - // Default to HMR on, but disable HMR if SSR mode is enabled. + // // Default to HMR on, but disable HMR if SSR mode is enabled. const isHMR = _isHMR ?? ((config.devOptions.hmr ?? true) && !isSSR); - const allowStale = _allowStale ?? false; const encoding = _encoding ?? null; const reqUrlHmrParam = reqUrl.includes('?mtime=') && reqUrl.split('?')[1]; - let reqPath = decodeURI(url.parse(reqUrl).pathname!); - const originalReqPath = reqPath; - let isProxyModule = false; - let isSourceMap = false; - if (hasExtension(reqPath, '.proxy.js')) { - isProxyModule = true; - reqPath = removeExtension(reqPath, '.proxy.js'); - } else if (hasExtension(reqPath, '.map')) { - isSourceMap = true; - reqPath = removeExtension(reqPath, '.map'); - } + const reqPath = decodeURI(url.parse(reqUrl).pathname!); + const resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, ''); + const resourceType = path.extname(resourcePath) || '.html'; if (reqPath === getMetaUrlPath('/hmr-client.js', config)) { return { contents: encodeResponse(HMR_CLIENT_CODE, encoding), + imports: [], originalFileLoc: null, contentType: 'application/javascript', }; @@ -454,6 +420,7 @@ export async function startServer(commandOptions: CommandOptions): Promise { - if (attemptedFileLoads.includes(requestedFile)) { - return Promise.resolve(null); + const webModuleUrl = resourcePath.substr(PACKAGE_PATH_PREFIX.length); + let loadedModule = await pkgSource.load(webModuleUrl, isSSR, commandOptions); + if (!loadedModule) { + throw new NotFoundError(reqPath); } - attemptedFileLoads.push(requestedFile); - return fs - .stat(requestedFile) - .then((stat) => (stat.isFile() ? requestedFile : null)) - .catch(() => null /* ignore */); - } - - let requestedFile = path.parse(reqPath); - let requestedFileExt = requestedFile.ext.toLowerCase(); - let responseFileExt = requestedFileExt; - let isRoute = !requestedFileExt || requestedFileExt === '.html'; - - async function getFileFromMount( - requestedFile: string, - mountEntry: MountEntry, - ): Promise { - const fileLocExact = await attemptLoadFile(requestedFile); - if (fileLocExact) { + if (reqPath.endsWith('.proxy.js')) { return { - fileLoc: fileLocExact, - isStatic: mountEntry.static, - isResolve: mountEntry.resolve, + imports: [], + contents: await wrapImportProxy({ + url: resourcePath, + code: loadedModule.contents, + hmr: isHMR, + config: config, + }), + originalFileLoc: null, + contentType: 'application/javascript', }; } - if (!mountEntry.static) { - for (const potentialSourceFile of getInputsFromOutput(requestedFile, config.plugins)) { - const fileLoc = await attemptLoadFile(potentialSourceFile); - if (fileLoc) { - return { - fileLoc, - isStatic: mountEntry.static, - isResolve: mountEntry.resolve, - }; - } - } - } - return null; - } - - async function getFileFromUrl(reqPath: string): Promise { - for (const [mountKey, mountEntry] of Object.entries(config.mount)) { - let requestedFile: string; - if (mountEntry.url === '/') { - requestedFile = path.join(mountKey, reqPath); - } else if (reqPath.startsWith(mountEntry.url)) { - requestedFile = path.join(mountKey, reqPath.replace(mountEntry.url, './')); - } else { - continue; - } - - const file = await getFileFromMount(requestedFile, mountEntry); - if (file) { - return file; - } - } - return null; - } - - async function getFileFromLazyUrl(reqPath: string): Promise { - for (const [mountKey, mountEntry] of Object.entries(config.mount)) { - let requestedFile: string; - if (mountEntry.url === '/') { - requestedFile = path.join(mountKey, reqPath); - } else if (reqPath.startsWith(mountEntry.url)) { - requestedFile = path.join(mountKey, reqPath.replace(mountEntry.url, './')); - } else { - continue; - } - const file = - (await getFileFromMount(requestedFile + '.html', mountEntry)) || - (await getFileFromMount(requestedFile + 'index.html', mountEntry)) || - (await getFileFromMount(requestedFile + '/index.html', mountEntry)); - if (file) { - requestedFileExt = '.html'; - responseFileExt = '.html'; - return file; - } - } - return null; - } - - let foundFile = await getFileFromUrl(reqPath); - if (!foundFile && isRoute) { - foundFile = await getFileFromLazyUrl(reqPath); - } - - if (!foundFile) { - throw new NotFoundError(attemptedFileLoads); - } - - if (!isRoute && !isProxyModule && !isSourceMap) { - const cleanUrl = url.parse(reqUrl).pathname; - const cleanUrlWithMainExtension = - cleanUrl && replaceExtension(cleanUrl, path.extname(cleanUrl), '.js'); - const expectedUrl = getUrlForFile(foundFile.fileLoc, config); - if (cleanUrl !== expectedUrl && cleanUrlWithMainExtension !== expectedUrl) { - logger.warn(`Bad Request: "${reqUrl}" should be requested as "${expectedUrl}".`); - throw new NotFoundError([foundFile.fileLoc]); - } - } - - /** - * 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 function buildFile(fileLoc: string): Promise { - const existingBuilderPromise = filesBeingBuilt.get(fileLoc); - if (existingBuilderPromise) { - return existingBuilderPromise; - } - const fileBuilderPromise = (async () => { - const builtFileOutput = await _buildFile(url.pathToFileURL(fileLoc), { - config, - isDev: true, - isSSR, - isHmrEnabled: isHMR, - }); - inMemoryBuildCache.set( - getCacheKey(fileLoc, {isSSR, env: process.env.NODE_ENV}), - builtFileOutput, - ); - return builtFileOutput; - })(); - filesBeingBuilt.set(fileLoc, fileBuilderPromise); - try { - messageBus.emit(paintEvent.BUILD_FILE, {id: fileLoc, isBuilding: true}); - return await fileBuilderPromise; - } finally { - filesBeingBuilt.delete(fileLoc); - messageBus.emit(paintEvent.BUILD_FILE, {id: fileLoc, isBuilding: false}); - } + return { + imports: loadedModule.imports, + contents: encodeResponse(loadedModule.contents, encoding), + originalFileLoc: null, + contentType: mime.lookup(reqPath) || 'application/javascript', + }; } - /** - * Wrap Response: The same build result can be expressed in different ways - * based on the URL. For example, "App.css" should return CSS but - * "App.css.proxy.js" should return a JS representation of that CSS. This is - * handled in the wrap step. - */ - async function wrapResponse( - code: string | Buffer, - { - sourceMap, - sourceMappingURL, - }: { - sourceMap?: string; - sourceMappingURL: string; - }, - ) { - // transform special requests - if (isRoute) { - code = wrapHtmlResponse({ - code: code as string, - hmr: isHMR, - hmrPort: hmrEngine.port !== port ? hmrEngine.port : undefined, - isDev: true, - config, - mode: 'development', - }); - } else if (isProxyModule) { - responseFileExt = '.js'; - } else if (isSourceMap && sourceMap) { - responseFileExt = '.map'; - code = sourceMap; - } - - // transform other files - switch (responseFileExt) { - case '.css': { - if (sourceMap) code = cssSourceMappingURL(code as string, sourceMappingURL); - break; - } - case '.js': { - if (isProxyModule) { - code = await wrapImportProxy({url: reqPath, code, hmr: isHMR, config}); - } else { - code = wrapImportMeta({code: code as string, env: true, hmr: isHMR, config}); - } + let foundFile: FoundFile; - // source mapping - if (sourceMap) code = jsSourceMappingURL(code, sourceMappingURL); - - break; - } - } - - // by default, return file from disk - return code; - } - - /** - * 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 function resolveResponseImports( - fileLoc: string, - responseExt: string, - wrappedResponse: string, - retryMissing = true, - ): Promise { - let missingPackages: string[] = []; - const resolveImportSpecifier = createImportResolver({ - fileLoc, - config, - }); - wrappedResponse = await transformFileImports( - { - locOnDisk: fileLoc, - contents: wrappedResponse, - root: config.root, - baseExt: responseExt, - }, - (spec) => { - // Try to resolve the specifier to a known URL in the project - let resolvedImportUrl = resolveImportSpecifier(spec); - // Handle a package import - if (!resolvedImportUrl) { - resolvedImportUrl = pkgSource.resolvePackageImport(spec, sourceImportMap, config); - } - // Handle a package import that couldn't be resolved - if (!resolvedImportUrl) { - missingPackages.push(spec); - return spec; - } - // Ignore "http://*" imports - if (isRemoteUrl(resolvedImportUrl)) { - return resolvedImportUrl; - } - // Ignore packages marked as external - if (config.packageOptions.external?.includes(resolvedImportUrl)) { - return spec; - } - // Handle normal "./" & "../" import specifiers - const importExtName = path.posix.extname(resolvedImportUrl); - const isProxyImport = importExtName && importExtName !== '.js'; - const isAbsoluteUrlPath = path.posix.isAbsolute(resolvedImportUrl); - if (isProxyImport) { - resolvedImportUrl = resolvedImportUrl + '.proxy.js'; - } - - // 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 - if (isAbsoluteUrlPath && !isProxyModule) { - resolvedImportUrl = relativeURL(path.posix.dirname(reqPath), resolvedImportUrl); - } - // Make sure that a relative URL always starts with "./" - if (!resolvedImportUrl.startsWith('.') && !resolvedImportUrl.startsWith('/')) { - resolvedImportUrl = './' + resolvedImportUrl; - } - return resolvedImportUrl; - }, + // * 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 (reqPath.startsWith(PACKAGE_LINK_PATH_PREFIX)) { + const symlinkResourceUrl = reqPath.substr(PACKAGE_LINK_PATH_PREFIX.length); + const symlinkResourceLoc = path.resolve( + config.root, + process.platform === 'win32' ? symlinkResourceUrl.replace(/\//g, '\\') : symlinkResourceUrl, ); - - // A missing package is a broken import, so we need to recover instantly if possible. - if (missingPackages.length > 0) { - // if retryMissing is true, do a fresh dependency install and then retry. - // Only retry once, to prevent an infinite loop when a package doesn't actually exist. - if (retryMissing) { - try { - sourceImportMap = await pkgSource.recoverMissingPackageImport(missingPackages, config); - return resolveResponseImports(fileLoc, responseExt, wrappedResponse, false); - } catch (err) { - const errorTitle = `Dependency Install Error`; - const errorMessage = err.message; - logger.error(`${errorTitle}: ${errorMessage}`); - hmrEngine.broadcastMessage({ - type: 'error', - title: errorTitle, - errorMessage, - fileLoc, - }); - return wrappedResponse; - } - } - // Otherwise, we need to send an error to the user, telling them about this issue. - // A failed retry usually means that Snowpack couldn't detect the import that the browser - // eventually saw post-build. In that case, you need to add it manually. - const errorTitle = `Error: Import "${missingPackages[0]}" could not be resolved.`; - const errorMessage = `If this import doesn't exist in the source file, add ${colors.bold( - `"knownEntrypoints": ["${missingPackages[0]}"]`, - )} to your Snowpack config "packageOptions".`; - logger.error(`${errorTitle}\n${errorMessage}`); - hmrEngine.broadcastMessage({ - type: 'error', - title: errorTitle, - errorMessage, - fileLoc, - }); - } - - let code = wrappedResponse; - if (responseFileExt === '.js' && reqUrlHmrParam) - code = await transformEsmImports(code as string, (imp) => { - const importUrl = path.posix.resolve(path.posix.dirname(reqPath), imp); - const node = hmrEngine.getEntry(importUrl); - if (node && node.needsReplacement) { - hmrEngine.markEntryForReplacement(node, false); - return `${imp}?${reqUrlHmrParam}`; - } - return imp; - }); - - if (responseFileExt === '.js') { - const isHmrEnabled = code.includes('import.meta.hot'); - const rawImports = await scanCodeImportsExports(code); - const resolvedImports = rawImports.map((imp) => { - let spec = code.substring(imp.s, imp.e); - if (imp.d > -1) { - spec = matchDynamicImportValue(spec) || ''; - } - spec = spec.replace(/\?mtime=[0-9]+$/, ''); - return path.posix.resolve(path.posix.dirname(reqPath), spec); - }); - hmrEngine.setEntry(originalReqPath, resolvedImports, isHmrEnabled); - } - - wrappedResponse = code; - return wrappedResponse; - } - - /** - * Given a build, finalize it for the response. This involves running - * individual steps needed to go from build result to sever response, - * including: - * - wrapResponse(): Wrap responses - * - resolveResponseImports(): Resolve all ESM imports - */ - async function finalizeResponse( - fileLoc: string, - requestedFileExt: string, - output: SnowpackBuildMap, - ): Promise { - // Verify that the requested file exists in the build output map. - if (!output[requestedFileExt] || !Object.keys(output)) { - return null; - } - const {code, map} = output[requestedFileExt]; - let finalResponse = code; - // Handle attached CSS. - if (requestedFileExt === '.js' && output['.css']) { - finalResponse = `import '${replaceExtension(reqPath, '.js', '.css')}';\n` + finalResponse; + const symlinkResourceDirectory = path.dirname(symlinkResourceLoc); + const fileStat = await fs.stat(symlinkResourceDirectory).catch(() => null); + if (!fileStat) { + throw new NotFoundError(reqPath, [symlinkResourceDirectory]); } - // Resolve imports. - if ( - requestedFileExt === '.js' || - requestedFileExt === '.html' || - requestedFileExt === '.css' - ) { - finalResponse = await resolveResponseImports( - fileLoc, - requestedFileExt, - finalResponse as string, + // If this is the first file served out of this linked directory, add it to our file watcher + // (to enable HMR) PLUS add it to our file<>URL mapping for future lookups. Each directory + // is scanned shallowly, so nested directories inside of `symlinkDirectories` are okay. + if (!symlinkDirectories.has(symlinkResourceDirectory)) { + symlinkDirectories.add(symlinkResourceDirectory); + watcher && watcher.add(symlinkResourceDirectory); + logger.debug( + `Mounting symlink directory: '${symlinkResourceDirectory}' as URL '${path.dirname( + reqPath, + )}'`, ); + for (const f of glob.sync(path.join(symlinkResourceDirectory, '*'), { + nodir: true, + absolute: true, + })) { + const normalizedFileLoc = path.normalize(f); + fileToUrlMapping.add( + normalizedFileLoc, + getBuiltFileUrls(normalizedFileLoc, config).map((u) => + path.posix.join( + config.buildOptions.metaUrlPath, + 'link', + slash(path.relative(config.root, u)), + ), + ), + ); + } } - // Wrap the response. - finalResponse = await wrapResponse(finalResponse, { - sourceMap: map, - sourceMappingURL: path.basename(requestedFile.base) + '.map', - }); - // Return the finalized response. - return finalResponse; - } - - const {fileLoc, isStatic: _isStatic, isResolve} = foundFile; - // Workaround: HMR plugins need to add scripts to HTML file, even if static. - // TODO: Once plugins are able to add virtual files + imports, this will no longer be needed. - const isStatic = _isStatic && !hasExtension(fileLoc, '.html'); - - // 1. Check the hot build cache. If it's already found, then just serve it. - let hotCachedResponse: SnowpackBuildMap | undefined = inMemoryBuildCache.get( - getCacheKey(fileLoc, {isSSR, env: process.env.NODE_ENV}), - ); - if (hotCachedResponse) { - let responseContent: string | Buffer | null; - try { - responseContent = await finalizeResponse(fileLoc, requestedFileExt, hotCachedResponse); - } catch (err) { - logger.error(FILE_BUILD_RESULT_ERROR); - hmrEngine.broadcastMessage({ - type: 'error', - title: FILE_BUILD_RESULT_ERROR, - errorMessage: err.toString(), - fileLoc, - errorStackTrace: err.stack, - }); - throw err; + const fileLocation = fileToUrlMapping.key(reqPath); + if (!fileLocation) { + throw new NotFoundError(reqPath); } - if (!responseContent) { - throw new NotFoundError([fileLoc]); + const fileLocationExists = await fs.stat(fileLocation).catch(() => null); + if (!fileLocationExists) { + throw new NotFoundError(reqPath, [fileLocation]); } - return { - contents: encodeResponse(responseContent, encoding), - originalFileLoc: fileLoc, - contentType: mime.lookup(responseFileExt), + foundFile = { + loc: fileLocation, + type: path.extname(reqPath), + 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 { + const attemptedFileLoc = + fileToUrlMapping.key(resourcePath) || + fileToUrlMapping.key(resourcePath + '.html') || + fileToUrlMapping.key(resourcePath + 'index.html') || + fileToUrlMapping.key(resourcePath + '/index.html'); + if (!attemptedFileLoc) { + throw new NotFoundError(reqPath, [resourcePath]); + } - // 2. Load the file from disk. We'll need it to check the cold cache or build from scratch. - const fileContents = await readFile(url.pathToFileURL(fileLoc)); + const [, mountEntry] = getMountEntryForFile(attemptedFileLoc, config)!; - // 3. Send static files directly, since they were already build & resolved at install time. - if (!isProxyModule && isStatic) { - // If no resolution needed, just send the file directly. - if (!isResolve) { - return { - contents: encodeResponse(fileContents, encoding), - originalFileLoc: fileLoc, - contentType: mime.lookup(responseFileExt), - }; - } - // Otherwise, finalize the response (where resolution happens) before sending. - let responseContent: string | Buffer | null; - try { - responseContent = await finalizeResponse(fileLoc, requestedFileExt, { - [requestedFileExt]: {code: fileContents}, - }); - } catch (err) { - logger.error(FILE_BUILD_RESULT_ERROR); - hmrEngine.broadcastMessage({ - type: 'error', - title: FILE_BUILD_RESULT_ERROR, - errorMessage: err.toString(), - fileLoc, - errorStackTrace: err.stack, - }); - throw err; - } - if (!responseContent) { - throw new NotFoundError([fileLoc]); - } - return { - contents: encodeResponse(responseContent, encoding), - originalFileLoc: fileLoc, - contentType: mime.lookup(responseFileExt), + // 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. + foundFile = { + loc: attemptedFileLoc, + type: path.extname(reqPath) || '.html', + isStatic: mountEntry.static, + isResolve: mountEntry.resolve, }; } - // 4. Check the persistent cache. If found, serve it via a - // "trust-but-verify" strategy. Build it after sending, and if it no longer - // matches then assume the entire cache is suspect. In that case, clear the - // persistent cache and then force a live-reload of the page. - const cachedBuildData = - allowStale && - process.env.NODE_ENV !== 'test' && - !filesBeingDeleted.has(fileLoc) && - !(await isBinaryFile(fileLoc)) && - (await cacache - .get(BUILD_CACHE, getCacheKey(fileLoc, {isSSR, env: process.env.NODE_ENV})) - .catch(() => null)); - if (cachedBuildData) { - const {originalFileHash} = cachedBuildData.metadata; - const newFileHash = etag(fileContents); - if (originalFileHash === newFileHash) { - // IF THIS FAILS TS CHECK: If you are changing the structure of - // SnowpackBuildMap, be sure to also update `BUILD_CACHE` in util.ts to - // a new unique name, to guarantee a clean cache for our users. - const coldCachedResponse: SnowpackBuildMap = JSON.parse( - cachedBuildData.data.toString(), - ) as Record< - string, - { - code: string; - map?: string; - } - >; - inMemoryBuildCache.set( - getCacheKey(fileLoc, {isSSR, env: process.env.NODE_ENV}), - coldCachedResponse, - ); + const {loc: fileLoc, type: responseType} = foundFile; - let wrappedResponse: string | Buffer | null; - try { - wrappedResponse = await finalizeResponse(fileLoc, requestedFileExt, coldCachedResponse); - } catch (err) { - logger.error(FILE_BUILD_RESULT_ERROR); - hmrEngine.broadcastMessage({ - type: 'error', - title: FILE_BUILD_RESULT_ERROR, - errorMessage: err.toString(), - fileLoc, - errorStackTrace: err.stack, - }); - throw err; - } + // 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 ?? true; - if (!wrappedResponse) { - logger.warn(`WARN: Failed to load ${fileLoc} from cold cache.`); - } else { - // Trust... - return { - contents: encodeResponse(wrappedResponse, encoding), - originalFileLoc: fileLoc, - contentType: mime.lookup(responseFileExt), - // ...but verify. - checkStale: async () => { - let checkFinalBuildResult: SnowpackBuildMap | null = null; - try { - checkFinalBuildResult = await buildFile(fileLoc!); - } catch (err) { - // safe to ignore, it will be surfaced later anyway - } finally { - if ( - !checkFinalBuildResult || - !cachedBuildData.data.equals(Buffer.from(JSON.stringify(checkFinalBuildResult))) - ) { - inMemoryBuildCache.clear(); - await cacache.rm.all(BUILD_CACHE); - hmrEngine.broadcastMessage({type: 'reload'}); - } - } - return; - }, - }; - } - } + // 1. Check the hot build cache. If it's already found, then just serve it. + const cacheKey = getCacheKey(fileLoc, {isSSR, env: process.env.NODE_ENV}); + let fileBuilder: FileBuilder | undefined = inMemoryBuildCache.get(cacheKey); + if (!fileBuilder) { + fileBuilder = new FileBuilder({ + loc: fileLoc, + isDev, + isSSR, + isHMR, + config, + hmrEngine, + }); + inMemoryBuildCache.set(cacheKey, fileBuilder); } - // 5. Final option: build the file, serve it, and cache it. - let responseContent: string | Buffer | null; - let responseOutput: SnowpackBuildMap; - - try { - responseOutput = await buildFile(fileLoc); - } catch (err) { + function handleFinalizeError(err: Error) { + logger.error(FILE_BUILD_RESULT_ERROR); hmrEngine.broadcastMessage({ type: 'error', - title: - `Build Error` + - (err.__snowpackBuildDetails ? `: ${err.__snowpackBuildDetails.name}` : ''), + title: FILE_BUILD_RESULT_ERROR, errorMessage: err.toString(), fileLoc, errorStackTrace: err.stack, }); - throw err; } + + let finalizedResponse: string | Buffer | undefined; + let resolvedImports: InstallTarget[] = []; try { - responseContent = await finalizeResponse(fileLoc, requestedFileExt, responseOutput); + if (Object.keys(fileBuilder.buildOutput).length === 0) { + await fileBuilder.build(isStatic); + } + if (reqPath.endsWith('.proxy.js')) { + finalizedResponse = await fileBuilder.getProxy(resourcePath, resourceType); + } else if (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) { - logger.error(FILE_BUILD_RESULT_ERROR); - hmrEngine.broadcastMessage({ - type: 'error', - title: FILE_BUILD_RESULT_ERROR, - errorMessage: err.toString(), - fileLoc, - errorStackTrace: err.stack, - }); + handleFinalizeError(err); throw err; } - if (!responseContent) { - throw new NotFoundError([fileLoc]); + if (!finalizedResponse) { + throw new NotFoundError(reqPath); } - // Save the file to the cold cache for reuse across restarts. - cacache - .put( - BUILD_CACHE, - getCacheKey(fileLoc, {isSSR, env: process.env.NODE_ENV}), - Buffer.from(JSON.stringify(responseOutput)), - { - metadata: {originalFileHash: etag(fileContents)}, - }, - ) - .catch((err) => { - logger.error(`Cache Error: ${err.toString()}`); - }); - return { - contents: encodeResponse(responseContent, encoding), + imports: resolvedImports, + contents: encodeResponse(finalizedResponse, encoding), originalFileLoc: fileLoc, - contentType: mime.lookup(responseFileExt), + contentType: mime.lookup(responseType), }; } @@ -1174,27 +710,48 @@ export async function startServer(commandOptions: CommandOptions): Promise { - // Attach a request logger. - res.on('finish', () => { - const {method, url} = req; - const {statusCode} = res; - logger.debug(`[${statusCode}] ${method} ${url}`); - }); - // Otherwise, pass requests directly to Snowpack's request handler. - handleRequest(req, res); - }) - .on('error', (err: Error) => { - logger.error(colors.red(` ✘ Failed to start server at port ${colors.bold(port)}.`), err); - server.close(); - process.exit(1); + let server: ReturnType | undefined; + if (port) { + server = createServer(async (req, res) => { + // Attach a request logger. + res.on('finish', () => { + const {method, url} = req; + const {statusCode} = res; + logger.debug(`[${statusCode}] ${method} ${url}`); + }); + // Otherwise, pass requests directly to Snowpack's request handler. + handleRequest(req, res); }) - .listen(port); + .on('error', (err: Error) => { + 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.networkInterfaces()) + .reduce((every: os.NetworkInterfaceInfo[], i) => [...every, ...(i || [])], []) + .filter((i) => i.family === 'IPv4' && i.internal === false) + .map((i) => i.address); + const protocol = config.devOptions.secure ? 'https:' : 'http:'; + messageBus.emit(paintEvent.SERVER_START, { + protocol, + hostname, + port, + remoteIp: remoteIps[0], + startTimeMs: Math.round(performance.now() - serverStart), + }); + } const {hmrDelay} = config.devOptions; + const hmrPort = + config.devOptions.hmrPort || + config.devOptions.port || + (await detectPort(config.devOptions.hmrPort || config.devOptions.port)); const hmrEngineOptions = Object.assign( {delay: hmrDelay}, - config.devOptions.hmrPort ? {port: config.devOptions.hmrPort} : {server, port}, + config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, ); const hmrEngine = new EsmHmrEngine(hmrEngineOptions); onProcessExit(() => { @@ -1250,7 +807,9 @@ export async function startServer(commandOptions: CommandOptions): Promise [...every, ...(i || [])], []) - .filter((i) => i.family === 'IPv4' && i.internal === false) - .map((i) => i.address); - const protocol = config.devOptions.secure ? 'https:' : 'http:'; - messageBus.emit(paintEvent.SERVER_START, { - protocol, - hostname, - port, - remoteIp: remoteIps[0], - startTimeMs: Math.round(performance.now() - serverStart), - }); - - // Open the user's browser (ignore if failed) - if (open !== 'none') { - await openInBrowser(protocol, hostname, port, open).catch((err) => { - logger.debug(`Browser open error: ${err}`); - }); - } - // Start watching the file system. // Defer "chokidar" loading to here, to reduce impact on overall startup time const chokidar = await import('chokidar'); @@ -1301,85 +839,64 @@ export async function startServer(commandOptions: CommandOptions): Promise { knownETags.clear(); onWatchEvent(fileLoc); + fileToUrlMapping.add(fileLoc, getUrlsForFile(fileLoc, config)!); }); watcher.on('unlink', (fileLoc) => { knownETags.clear(); onWatchEvent(fileLoc); + fileToUrlMapping.delete(fileLoc); }); watcher.on('change', (fileLoc) => { onWatchEvent(fileLoc); }); + logger.info(colors.cyan('watching for file changes...')); - // Watch node_modules & rerun snowpack install if symlinked dep updates - const symlinkedFileLocs = new Set( - Object.keys(sourceImportMap.imports) - .map((specifier) => { - const [packageName] = parsePackageImportSpecifier(specifier); - return resolveDependencyManifest(packageName, config.root); - }) // resolve symlink src location - .filter(([_, packageManifest]) => packageManifest && !packageManifest['_id']) // only watch symlinked deps for now - .map(([fileLoc]) => `${path.dirname(fileLoc!)}/**`), - ); - function onDepWatchEvent() { - hmrEngine.broadcastMessage({type: 'reload'}); + // Open the user's browser (ignore if failed) + if (server && port && open && open !== 'none') { + const protocol = config.devOptions.secure ? 'https:' : 'http:'; + await openInBrowser(protocol, hostname, port, open).catch((err) => { + logger.debug(`Browser open error: ${err}`); + }); } - const depWatcher = chokidar.watch([...symlinkedFileLocs], { - cwd: '/', // we’re using absolute paths, so watch from root - persistent: true, - ignoreInitial: true, - disableGlobbing: false, - useFsEvents: isFsEventsEnabled(), - }); - depWatcher.on('add', onDepWatchEvent); - depWatcher.on('change', onDepWatchEvent); - depWatcher.on('unlink', onDepWatchEvent); const sp = { port, + hmrEngine, loadUrl, handleRequest, sendResponseFile, sendResponseError, - getUrlForFile: (fileLoc: string) => getUrlForFile(fileLoc, config), + getUrlForFile: (fileLoc: string) => { + const result = getUrlsForFile(fileLoc, config); + return result ? result[0] : result; + }, onFileChange: (callback) => (onFileChangeCallback = callback), getServerRuntime: (options) => getServerRuntime(sp, options), async shutdown() { await watcher.close(); - server.close(); + server && server.close(); }, } as SnowpackDevServer; return sp; @@ -1387,6 +904,13 @@ export async function startServer(commandOptions: CommandOptions): Promise { export async function transformEsmImports( _code: string, - replaceImport: (specifier: string) => string, + replaceImport: (specifier: string) => string | Promise, ) { const imports = await scanCodeImportsExports(_code); let rewrittenCode = _code; @@ -40,7 +39,7 @@ export async function transformEsmImports( webpackMagicCommentMatches = spec.match(WEBPACK_MAGIC_COMMENT_REGEX); spec = matchDynamicImportValue(spec) || ''; } - let rewrittenImport = replaceImport(spec); + let rewrittenImport = await replaceImport(spec); if (imp.d > -1) { rewrittenImport = webpackMagicCommentMatches ? `${webpackMagicCommentMatches.join(' ')} ${JSON.stringify(rewrittenImport)}` @@ -51,7 +50,10 @@ export async function transformEsmImports( return rewrittenCode; } -async function transformHtmlImports(code: string, replaceImport: (specifier: string) => string) { +async function transformHtmlImports( + code: string, + replaceImport: (specifier: string) => string | Promise, +) { let rewrittenCode = code; let match; const jsImportRegex = new RegExp(HTML_JS_REGEX); @@ -83,7 +85,10 @@ async function transformHtmlImports(code: string, replaceImport: (specifier: str return rewrittenCode; } -async function transformCssImports(code: string, replaceImport: (specifier: string) => string) { +async function transformCssImports( + code: string, + replaceImport: (specifier: string) => string | Promise, +) { let rewrittenCode = code; let match; const importRegex = new RegExp(CSS_REGEX); @@ -93,7 +98,7 @@ async function transformCssImports(code: string, replaceImport: (specifier: stri rewrittenCode = spliceString( rewrittenCode, // CSS doesn't support proxy files, so always point to the original file - `@import "${replaceImport(spec).replace('.proxy.js', '')}";`, + `@import "${(await replaceImport(spec)).replace('.proxy.js', '')}";`, match.index, match.index + fullMatch.length, ); @@ -102,19 +107,28 @@ async function transformCssImports(code: string, replaceImport: (specifier: stri } export async function transformFileImports( - {baseExt, contents}: SnowpackSourceFile, - replaceImport: (specifier: string) => string, + {type, contents}: {type: string; contents: string}, + replaceImport: (specifier: string) => string | Promise, ) { - if (baseExt === '.js') { + if (type === '.js') { return transformEsmImports(contents, replaceImport); } - if (baseExt === '.html') { + if (type === '.html') { return transformHtmlImports(contents, replaceImport); } - if (baseExt === '.css') { + if (type === '.css') { return transformCssImports(contents, replaceImport); } throw new Error( - `Incompatible filetype: cannot scan ${baseExt} files for ESM imports. This is most likely an error within Snowpack.`, + `Incompatible filetype: cannot scan ${type} files for ESM imports. This is most likely an error within Snowpack.`, ); } + +export async function transformAddMissingDefaultExport(_code: string) { + // We need to add a default export, just so that our re-importer doesn't break + const [, allExports] = await parse(_code); + if (!allExports.includes('default')) { + return _code + '\n\nexport default null;'; + } + return _code; +} diff --git a/snowpack/src/scan-imports.ts b/snowpack/src/scan-imports.ts index f811678975..f347814f0a 100644 --- a/snowpack/src/scan-imports.ts +++ b/snowpack/src/scan-imports.ts @@ -7,6 +7,7 @@ import url from 'url'; import {logger} from './logger'; import {SnowpackConfig, SnowpackSourceFile} from './types'; import { + createInstallTarget, CSS_REGEX, findMatchingAliasEntry, getExtension, @@ -30,16 +31,6 @@ const HAS_NAMED_IMPORTS_REGEX = /^[\w\s\,]*\{(.*)\}/s; const STRIP_AS = /\s+as\s+.*/; // for `import { foo as bar }`, strips “as bar” const DEFAULT_IMPORT_REGEX = /import\s+(\w)+(,\s\{[\w\s]*\})?\s+from/s; -function createInstallTarget(specifier: string, all = true): InstallTarget { - return { - specifier, - all, - default: false, - namespace: false, - named: [], - }; -} - export async function getInstallTargets( config: SnowpackConfig, knownEntrypoints: string[], diff --git a/snowpack/src/sources/local-install.ts b/snowpack/src/sources/local-install.ts index 3e16031d30..0253d54dc2 100644 --- a/snowpack/src/sources/local-install.ts +++ b/snowpack/src/sources/local-install.ts @@ -1,76 +1,92 @@ -import { - DependencyStatsOutput, - install, - InstallOptions as EsinstallOptions, - InstallTarget, - printStats, -} from 'esinstall'; -import * as colors from 'kleur/colors'; -import {performance} from 'perf_hooks'; +import {install, InstallOptions as EsinstallOptions, InstallTarget} from 'esinstall'; +import url from 'url'; import util from 'util'; +import {buildFile} from '../build/build-pipeline'; import {logger} from '../logger'; import {ImportMap, SnowpackConfig} from '../types'; -interface InstallRunOptions { +interface InstallOptions { config: SnowpackConfig; + isDev: boolean; + isSSR: boolean; installOptions: EsinstallOptions; - installTargets: InstallTarget[]; - shouldPrintStats: boolean; + installTargets: (InstallTarget | string)[]; } -interface InstallRunResult { +interface InstallResult { importMap: ImportMap; - newLockfile: ImportMap | null; - stats: DependencyStatsOutput | null; + needsSsrBuild: boolean; } -export async function run({ +export async function installPackages({ config, + isDev, + isSSR, installOptions, installTargets, - shouldPrintStats, -}: InstallRunOptions): Promise { +}: InstallOptions): Promise { if (installTargets.length === 0) { return { importMap: {imports: {}} as ImportMap, - newLockfile: null, - stats: null, + needsSsrBuild: false, }; } - // start - const installStart = performance.now(); - logger.info(colors.yellow('! building dependencies...')); + const loggerName = + installTargets.length === 1 + ? `prepare:${ + typeof installTargets[0] === 'string' ? installTargets[0] : installTargets[0].specifier + }` + : `prepare`; + let needsSsrBuild = false; - let newLockfile: ImportMap | null = null; const finalResult = await install(installTargets, { cwd: config.root, - importMap: newLockfile || undefined, alias: config.alias, logger: { - debug: (...args: [any, ...any[]]) => logger.debug(util.format(...args)), - log: (...args: [any, ...any[]]) => logger.info(util.format(...args)), - warn: (...args: [any, ...any[]]) => logger.warn(util.format(...args)), - error: (...args: [any, ...any[]]) => logger.error(util.format(...args)), + debug: (...args: [any, ...any[]]) => logger.debug(util.format(...args), {name: loggerName}), + log: (...args: [any, ...any[]]) => logger.info(util.format(...args), {name: loggerName}), + warn: (...args: [any, ...any[]]) => logger.warn(util.format(...args), {name: loggerName}), + error: (...args: [any, ...any[]]) => logger.error(util.format(...args), {name: loggerName}), }, ...installOptions, + rollup: { + plugins: [ + { + name: 'esinstall:snowpack', + async load(id: string) { + // SSR Packages: Some file types build differently for SSR vs. non-SSR. + // This line checks for those file types. Svelte is the only known file + // type for now, but you can add to this line if you encounter another. + needsSsrBuild = needsSsrBuild || id.endsWith('.svelte'); + // TODO: Since this is new, only introduce for non-JS files. + // Consider running on all files in future versions. + if (id.endsWith('.js')) { + return; + } + const output = await buildFile(url.pathToFileURL(id), { + config, + isDev, + isSSR, + isPackage: true, + isHmrEnabled: false, + }); + let jsResponse; + for (const [outputType, outputContents] of Object.entries(output)) { + if (jsResponse) { + console.log(`load() Err: ${Object.keys(output)}`); + } + if (!jsResponse || outputType === '.js') { + jsResponse = outputContents; + } + } + return jsResponse; + }, + }, + ], + }, + }).catch((err) => { + throw new Error(`Package(s) failed to build: ${err.message}`); }); logger.debug('Successfully ran esinstall.'); - - // finish - const installEnd = performance.now(); - logger.info( - `${colors.green(`✔`) + ' dependencies ready!'} ${colors.dim( - `[${((installEnd - installStart) / 1000).toFixed(2)}s]`, - )}`, - ); - - if (shouldPrintStats && finalResult.stats) { - logger.info(printStats(finalResult.stats)); - } - - return { - importMap: finalResult.importMap, - newLockfile, - stats: finalResult.stats!, - }; + return {importMap: finalResult.importMap, needsSsrBuild}; } diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index ef1fe814c4..dde9fca89c 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -1,16 +1,26 @@ -import rimraf from 'rimraf'; import crypto from 'crypto'; +import {InstallOptions, InstallTarget, resolveEntrypoint} from 'esinstall'; import projectCacheDir from 'find-cache-dir'; -import merge from 'deepmerge'; -import {ImportMap, InstallOptions as EsinstallOptions} from 'esinstall'; import {existsSync, promises as fs} from 'fs'; +import PQueue from 'p-queue'; import * as colors from 'kleur/colors'; +import slash from 'slash'; import path from 'path'; -import {run as installRunner} from './local-install'; +import rimraf from 'rimraf'; +import {getBuiltFileUrls} from '../build/file-urls'; import {logger} from '../logger'; +import {scanCodeImportsExports, transformFileImports} from '../rewrite-imports'; import {getInstallTargets} from '../scan-imports'; -import {CommandOptions, PackageSource, PackageSourceLocal, SnowpackConfig} from '../types'; -import {checkLockfileHash, GLOBAL_CACHE_DIR, updateLockfileHash} from '../util'; +import { + CommandOptions, + ImportMap, + PackageSource, + SnowpackConfig, + PackageSourceLocal, +} from '../types'; +import {createInstallTarget, GLOBAL_CACHE_DIR, isJavaScript, isRemoteUrl} from '../util'; +import {installPackages} from './local-install'; +import findUp from 'find-up'; const PROJECT_CACHE_DIR = projectCacheDir({name: 'snowpack'}) || @@ -21,48 +31,121 @@ const PROJECT_CACHE_DIR = const DEV_DEPENDENCIES_DIR = path.join(PROJECT_CACHE_DIR, process.env.NODE_ENV || 'development'); -/** - * Install dependencies needed in "dev" mode. Generally speaking, this scans - * your entire source app for dependency install targets, installs them, - * and then updates the "hash" file used to check node_modules freshness. - */ -async function installDependencies(config: SnowpackConfig) { - const installTargets = await getInstallTargets( - config, - config.packageOptions.source === 'local' ? config.packageOptions.knownEntrypoints : [], - ); - if (installTargets.length === 0) { - logger.info('Nothing to install.'); - return; +function getRootPackageDirectory(loc: string) { + const parts = loc.split('node_modules'); + if (parts.length === 1) { + return undefined; + } + const packageParts = parts.pop()!.split(path.sep).filter(Boolean); + const packageRoot = path.join(parts.join('node_modules'), 'node_modules'); + if (packageParts[0].startsWith('@')) { + return path.join(packageRoot, packageParts[0], packageParts[1]); + } else { + return path.join(packageRoot, packageParts[0]); } - // 2. Install dependencies, based on the scan of your final build. - const installResult = await installRunner({ - config, - installTargets, - installOptions, - shouldPrintStats: false, - }); - await updateLockfileHash(DEV_DEPENDENCIES_DIR); - return installResult; } // 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 installOptions: EsinstallOptions; +let config: SnowpackConfig; + +type PackageImportData = { + entrypoint: string; + loc: string; + installDest: string; + packageVersion: string; + packageName: string; +}; +const allPackageImports: Record = {}; +const allSymlinkImports: Record = {}; +const allKnownSpecs = new Set(); +const inProgressBuilds = new PQueue({concurrency: 1}); + +export function getLinkedUrl(builtUrl: string) { + return allSymlinkImports[builtUrl]; +} /** * Local Package Source: A generic interface through which Snowpack * interacts with esinstall and your locally installed dependencies. */ export default { - async load(spec: string): Promise { - const dependencyFileLoc = path.join(DEV_DEPENDENCIES_DIR, spec); - return fs.readFile(dependencyFileLoc); + async load(id: string, isSSR: boolean) { + const packageImport = allPackageImports[id]; + if (!packageImport) { + return; + } + const {loc, entrypoint, packageName, packageVersion} = packageImport; + let {installDest} = packageImport; + if (isSSR && 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.readFile(loc, 'utf8'); + const imports: InstallTarget[] = []; + const type = path.extname(loc); + if (!(type === '.js' || type === '.html' || type === '.css')) { + return {contents: packageCode, imports}; + } + + const packageImportMap = JSON.parse( + await fs.readFile(path.join(installDest, 'import-map.json'), 'utf8'), + ); + const resolveImport = async (spec): Promise => { + if (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 (spec.startsWith('./') || spec.startsWith('../')) { + const newLoc = path.resolve(path.dirname(loc), spec); + const resolvedSpec = slash(path.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.posix.join(`${packageName}.v${packageVersion}`, resolvedSpec); + allPackageImports[relativeImportId] = { + entrypoint: path.join(installDest, 'package.json'), + loc: newLoc, + installDest, + packageVersion, + packageName, + }; + return path.posix.join(config.buildOptions.metaUrlPath, 'pkg', relativeImportId); + } + // Otherwise, resolve this specifier as an external package. + return await this.resolvePackageImport(entrypoint, spec, config); + }; + packageCode = await transformFileImports({type, contents: packageCode}, async (spec) => { + const resolvedSpec = await resolveImport(spec); + imports.push( + createInstallTarget( + path.resolve(path.posix.join(config.buildOptions.metaUrlPath, 'pkg', id), resolvedSpec), + ), + ); + return resolvedSpec; + }); + return {contents: packageCode, imports}; }, - modifyBuildInstallOptions({installOptions, config}) { + modifyBuildInstallOptions({installOptions, config: _config}) { + config = config || _config; if (config.packageOptions.source !== 'local') { return installOptions; } @@ -75,52 +158,207 @@ export default { return installOptions; }, + // TODO: in build+watch, run prepare() + // then, no import map + // + async prepare(commandOptions: CommandOptions) { - const {config} = commandOptions; - // Set the proper install options, in case an install is needed. - const dependencyImportMapLoc = path.join(DEV_DEPENDENCIES_DIR, 'import-map.json'); - logger.debug(`Using cache folder: ${path.relative(config.root, DEV_DEPENDENCIES_DIR)}`); - installOptions = merge(commandOptions.config.packageOptions as PackageSourceLocal, { - dest: DEV_DEPENDENCIES_DIR, - env: {NODE_ENV: process.env.NODE_ENV || 'development'}, - treeshake: false, - }); - // Start with a fresh install of your dependencies, if needed. - let dependencyImportMap = {imports: {}}; - try { - dependencyImportMap = JSON.parse( - await fs.readFile(dependencyImportMapLoc, {encoding: 'utf8'}), + config = commandOptions.config; + const installDirectoryHashLoc = path.join(DEV_DEPENDENCIES_DIR, '.meta'); + const installDirectoryHash = await fs + .readFile(installDirectoryHashLoc, 'utf-8') + .catch(() => null); + if (installDirectoryHash === 'v1') { + logger.debug(`Install directory ".meta" tag is up-to-date. Welcome back!`); + return; + } else if (installDirectoryHash) { + logger.info( + 'Snowpack updated! Rebuilding your dependencies for the latest version of Snowpack...', ); - } catch (err) { - // no import-map found, safe to ignore - } - if (!(await checkLockfileHash(DEV_DEPENDENCIES_DIR)) || !existsSync(dependencyImportMapLoc)) { - logger.debug('Cache out of date or missing. Updating...'); - const installResult = await installDependencies(config); - dependencyImportMap = installResult?.importMap || {imports: {}}; } else { - logger.debug(`Cache up-to-date. Using existing cache`); + logger.info( + `${colors.bold( + 'Welcome to Snowpack!', + )} Because this is your first time running this project${ + process.env.NODE_ENV === 'test' ? ` (mode: test)` : `` + }, \n` + + 'Snowpack needs to prepare your dependencies. This is a one-time step and the results \n' + + 'will be reused for the lifetime of your project. Please wait while we prepare...', + ); + } + const installTargets = await getInstallTargets( + config, + config.packageOptions.source === 'local' ? config.packageOptions.knownEntrypoints : [], + ); + if (installTargets.length === 0) { + logger.info('No dependencies detected. Set up complete!'); + return; } - return dependencyImportMap; + await Promise.all( + [...new Set(installTargets.map((t) => t.specifier))].map((spec) => { + return this.resolvePackageImport(path.join(config.root, 'package.json'), spec, config); + }), + ); + await fs.writeFile(installDirectoryHashLoc, 'v1', 'utf-8'); + logger.info(colors.bold('Set up complete!')); + return; }, - resolvePackageImport( - spec: string, - dependencyImportMap: ImportMap, - config: SnowpackConfig, - ): string | false { - if (dependencyImportMap.imports[spec]) { - const importMapEntry = dependencyImportMap.imports[spec]; - return path.posix.join(config.buildOptions.metaUrlPath, 'pkg', importMapEntry); + async resolvePackageImport(source: string, spec: string, _config: SnowpackConfig) { + config = config || _config; + const entrypoint = resolveEntrypoint(spec, { + cwd: path.dirname(source), + packageLookupFields: (_config.packageOptions as PackageSourceLocal).packageLookupFields || [], + }); + const specParts = spec.split('/'); + let _packageName: string = specParts.shift()!; + if (_packageName?.startsWith('@')) { + _packageName += '/' + specParts.shift(); + } + const isSymlink = !entrypoint.includes(path.join('node_modules', _packageName)); + const isWithinRoot = entrypoint.startsWith(config.root); + if (isSymlink && isWithinRoot) { + const builtEntrypointUrls = getBuiltFileUrls(entrypoint, config); + const builtEntrypointUrl = slash(path.relative(config.root, builtEntrypointUrls[0]!)); + allSymlinkImports[builtEntrypointUrl] = entrypoint; + return path.posix.join(config.buildOptions.metaUrlPath, 'link', builtEntrypointUrl); + } + + let rootPackageDirectory = getRootPackageDirectory(entrypoint); + if (!rootPackageDirectory) { + const rootPackageManifestLoc = await findUp('package.json', {cwd: entrypoint}); + if (!rootPackageManifestLoc) { + throw new Error(`Error resolving import ${spec}: No parent package.json found.`); + } + rootPackageDirectory = path.dirname(rootPackageManifestLoc); + } + const packageManifestLoc = path.join(rootPackageDirectory, 'package.json'); + const packageManifestStr = await fs.readFile(packageManifestLoc, 'utf8'); + const packageManifest = JSON.parse(packageManifestStr); + const packageName = packageManifest.name || _packageName; + const packageVersion = packageManifest.version || 'unknown'; + const installDest = path.join(DEV_DEPENDENCIES_DIR, packageName + '@' + packageVersion); + + let isNew = !allKnownSpecs.has(spec); + allKnownSpecs.add(spec); + const [newImportMap, loadedFile] = await inProgressBuilds.add( + async (): Promise<[ImportMap, Buffer]> => { + // Look up the import map of the already-installed package. + // If spec already exists, then this import map is valid. + const existingImportMapLoc = path.join(installDest, 'import-map.json'); + const existingImportMap = + (await fs.stat(existingImportMapLoc).catch(() => null)) && + JSON.parse(await fs.readFile(existingImportMapLoc, 'utf8')); + if (existingImportMap && existingImportMap.imports[spec]) { + logger.debug(spec + ' CACHED! (already exists)'); + const dependencyFileLoc = path.join(installDest, existingImportMap.imports[spec]); + return [existingImportMap, await fs.readFile(dependencyFileLoc!)]; + } + // Otherwise, kick off a new build to generate a fresh import map. + logger.info(colors.yellow(`⦿ ${spec}`)); + + const installTargets = [...allKnownSpecs].filter( + (spec) => spec === _packageName || spec.startsWith(_packageName + '/'), + ); + // TODO: external should be a function in esinstall + const externalPackages = [ + ...Object.keys(packageManifest.dependencies || {}), + ...Object.keys(packageManifest.devDependencies || {}), + ...Object.keys(packageManifest.peerDependencies || {}), + ]; + + const installOptions: InstallOptions = { + dest: installDest, + cwd: packageManifestLoc, + env: {NODE_ENV: process.env.NODE_ENV || 'development'}, + treeshake: false, + external: externalPackages, + externalEsm: externalPackages, + sourcemap: config.buildOptions.sourcemap, + alias: config.alias, + }; + 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 installPackages({ + config, + isDev: true, + isSSR: false, + installTargets, + installOptions, + }); + logger.debug(colors.yellow(`⦿ ${spec} DONE`)); + if (needsSsrBuild) { + logger.info(colors.yellow(`⦿ ${spec} (ssr)`)); + await installPackages({ + config, + isDev: true, + isSSR: true, + installTargets, + installOptions: { + ...installOptions, + dest: installDest + '-ssr', + }, + }); + logger.debug(colors.yellow(`⦿ ${spec} (ssr) DONE`)); + } + if (isSymlink) { + logger.info( + colors.bold(`Locally linked package detected outside of project root.\n`) + + `Locally linked/symlinked packages are treated as static by default, and will not be\n` + + `rebuilt until its "package.json" version changes. To enable local updates for this\n` + + `package, set your project root to match your monorepo/workspace root directory.`, + ); + } + const dependencyFileLoc = path.join(installDest, newImportMap.imports[spec]); + return [newImportMap, await fs.readFile(dependencyFileLoc!)]; + }, + ); + + const dependencyFileLoc = path.join(installDest, newImportMap.imports[spec]); + if (isNew && isJavaScript(dependencyFileLoc)) { + await inProgressBuilds.onIdle(); + const packageImports = new Set(); + const code = loadedFile.toString('utf8'); + for (const imp of await scanCodeImportsExports(code)) { + const spec = code.substring(imp.s, imp.e); + if (isRemoteUrl(spec)) { + continue; + } + if (spec.startsWith('/') || spec.startsWith('./') || spec.startsWith('../')) { + continue; + } + packageImports.add(spec); + } + await Promise.all( + [...packageImports].map((packageImport) => + this.resolvePackageImport(entrypoint, packageImport, config), + ), + ); } - return false; - }, - async recoverMissingPackageImport(_, config): Promise { - logger.info(colors.yellow('Dependency cache out of date. Updating...')); - const installResult = await installDependencies(config); - const dependencyImportMap = installResult!.importMap; - return dependencyImportMap; + // 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, + }; + return path.posix.join(config.buildOptions.metaUrlPath, 'pkg', importId); }, clearCache() { diff --git a/snowpack/src/sources/remote.ts b/snowpack/src/sources/remote.ts index 86b9613f17..7e1801febe 100644 --- a/snowpack/src/sources/remote.ts +++ b/snowpack/src/sources/remote.ts @@ -5,13 +5,7 @@ import rimraf from 'rimraf'; import {clearCache as clearSkypackCache, rollupPluginSkypack} from 'skypack'; import util from 'util'; import {logger} from '../logger'; -import { - ImportMap, - LockfileManifest, - PackageSource, - PackageSourceRemote, - SnowpackConfig, -} from '../types'; +import {LockfileManifest, PackageSource, PackageSourceRemote, SnowpackConfig} from '../types'; import {convertLockfileToSkypackImportMap, isJavaScript, remotePackageSDK} from '../util'; const fetchedPackages = new Set(); @@ -74,7 +68,6 @@ export default { logger.info(`types updated. ${colors.dim('→ ./.snowpack/types')}`, { name: 'packageOptions.types', }); - return {imports: {}}; }, modifyBuildInstallOptions({installOptions, config, lockfile}) { @@ -103,8 +96,9 @@ export default { async load( spec: string, + _isSSR: boolean, {config, lockfile}: {config: SnowpackConfig; lockfile: LockfileManifest | null}, - ): Promise { + ) { let body: Buffer; if ( spec.startsWith('-/') || @@ -157,21 +151,21 @@ export default { } const ext = path.extname(spec); if (!ext || isJavaScript(spec)) { - return body - .toString() - .replace(/(from|import) \'\//g, `$1 '${config.buildOptions.metaUrlPath}/pkg/`) - .replace(/(from|import) \"\//g, `$1 "${config.buildOptions.metaUrlPath}/pkg/`); + return { + contents: body + .toString() + .replace(/(from|import) \'\//g, `$1 '${config.buildOptions.metaUrlPath}/pkg/`) + .replace(/(from|import) \"\//g, `$1 "${config.buildOptions.metaUrlPath}/pkg/`), + imports: [], + }; } - return body; + return {contents: body, imports: []}; }, - resolvePackageImport(missingPackage: string, _: ImportMap, config: SnowpackConfig): string { - return path.posix.join(config.buildOptions.metaUrlPath, 'pkg', missingPackage); - }, - - async recoverMissingPackageImport(): Promise { - throw new Error('Unexpected Error: No such thing as a "missing" package import with Skypack.'); + // TODO: Remove need for lookup URLs + async resolvePackageImport(_source: string, spec: string, config: SnowpackConfig) { + return path.posix.join(config.buildOptions.metaUrlPath, 'pkg', spec); }, clearCache() { diff --git a/snowpack/src/sources/util.ts b/snowpack/src/sources/util.ts index 7a49a18cfa..58a7c088fa 100644 --- a/snowpack/src/sources/util.ts +++ b/snowpack/src/sources/util.ts @@ -1,15 +1,9 @@ -import cacache from 'cacache'; import {PackageSource} from '../types'; -import {BUILD_CACHE} from '../util'; import localPackageSource from './local'; import remotePackageSource from './remote'; export async function clearCache() { - return Promise.all([ - cacache.rm.all(BUILD_CACHE), - localPackageSource.clearCache(), - remotePackageSource.clearCache(), - ]); + return Promise.all([localPackageSource.clearCache(), remotePackageSource.clearCache()]); } export function getPackageSource(source: 'remote' | 'local'): PackageSource { diff --git a/snowpack/src/types.ts b/snowpack/src/types.ts index 7b1ab049e7..3ee9192e12 100644 --- a/snowpack/src/types.ts +++ b/snowpack/src/types.ts @@ -1,5 +1,6 @@ -import type {InstallOptions as EsinstallOptions} from 'esinstall'; +import type {InstallOptions as EsinstallOptions, InstallTarget} from 'esinstall'; import type * as http from 'http'; +import type {EsmHmrEngine} from './hmr-server-engine'; // RawSourceMap is inlined here for bundle purposes. // import type {RawSourceMap} from 'source-map'; @@ -36,41 +37,30 @@ export interface ServerRuntimeModule { export interface LoadResult { contents: T; + imports: InstallTarget[]; originalFileLoc: string | null; contentType: string | false; checkStale?: () => Promise; } export type OnFileChangeCallback = ({filePath: string}) => any; +export interface LoadUrlOptions { + isSSR?: boolean; + isHMR?: boolean; + isResolve?: boolean; + allowStale?: boolean; + encoding?: undefined | BufferEncoding | null; + importMap?: ImportMap; +} export interface SnowpackDevServer { port: number; + hmrEngine: EsmHmrEngine; loadUrl: { - ( - reqUrl: string, - opt?: - | { - isSSR?: boolean | undefined; - allowStale?: boolean | undefined; - encoding?: undefined; - } - | undefined, - ): Promise>; - ( - reqUrl: string, - opt: { - isSSR?: boolean; - allowStale?: boolean; - encoding: BufferEncoding; - }, - ): Promise>; - ( - reqUrl: string, - opt: { - isSSR?: boolean; - allowStale?: boolean; - encoding: null; - }, - ): Promise>; + (reqUrl: string, opt?: (LoadUrlOptions & {encoding?: undefined}) | undefined): Promise< + LoadResult + >; + (reqUrl: string, opt: LoadUrlOptions & {encoding: BufferEncoding}): Promise>; + (reqUrl: string, opt: LoadUrlOptions & {encoding: null}): Promise>; }; handleRequest: ( req: http.IncomingMessage, @@ -95,7 +85,7 @@ export type SnowpackBuildResultFileManifest = Record< >; export interface SnowpackBuildResult { - result: SnowpackBuildResultFileManifest; + // result: SnowpackBuildResultFileManifest; onFileChange: (callback: OnFileChangeCallback) => void; shutdown(): Promise; } @@ -126,10 +116,12 @@ export interface PluginLoadOptions { fileExt: string; /** True if builder is in dev mode (`snowpack dev` or `snowpack build --watch`) */ isDev: boolean; - /** True if builder is in SSR mode */ - isSSR: boolean; /** True if HMR is enabled (add any HMR code to the output here). */ isHmrEnabled: boolean; + /** True if builder is in SSR mode */ + isSSR: boolean; + /** True if file being transformed is inside of a package. */ + isPackage: boolean; } export interface PluginTransformOptions { @@ -145,6 +137,8 @@ export interface PluginTransformOptions { isHmrEnabled: boolean; /** True if builder is in SSR mode */ isSSR: boolean; + /** True if file being transformed is inside of a package. */ + isPackage: boolean; } export interface PluginRunOptions { @@ -257,8 +251,8 @@ export interface SnowpackConfig { secure: boolean; hostname: string; port: number; - open: string; - output: 'stream' | 'dashboard'; + open?: string; + output?: 'stream' | 'dashboard'; hmr?: boolean; hmrDelay: number; hmrPort: number | undefined; @@ -275,6 +269,7 @@ export interface SnowpackConfig { jsxFactory: string | undefined; jsxFragment: string | undefined; ssr: boolean; + resolveProxyImports: boolean; }; testOptions: { files: string[]; @@ -353,22 +348,18 @@ export interface PackageSource { * for this to complete before continuing. Example: For "local", this involves * running esinstall (if needed) to prepare your local dependencies as ESM. */ - prepare(commandOptions: CommandOptions): Promise; + prepare(commandOptions: CommandOptions): Promise; /** * Load a dependency with the given spec (ex: "/pkg/react" -> "react") * If load fails or is unsuccessful, reject the promise. */ load( spec: string, + isSSR: boolean, options: {config: SnowpackConfig; lockfile: LockfileManifest | null}, - ): Promise; + ): Promise; /** Resolve a package import to URL (ex: "react" -> "/pkg/react") */ - resolvePackageImport(spec: string, importMap: ImportMap, config: SnowpackConfig): string | false; - /** Handle 1+ missing package imports before failing, if possible. */ - recoverMissingPackageImport( - missingPackages: string[], - config: SnowpackConfig, - ): Promise; + resolvePackageImport(source: string, spec: string, config: SnowpackConfig): Promise; /** Modify the build install config for optimized build install. */ modifyBuildInstallOptions(options: { installOptions: EsinstallOptions; diff --git a/snowpack/src/util.ts b/snowpack/src/util.ts index d172dd28b4..2d0cda0848 100644 --- a/snowpack/src/util.ts +++ b/snowpack/src/util.ts @@ -8,10 +8,11 @@ import mkdirp from 'mkdirp'; import open from 'open'; import path from 'path'; import rimraf from 'rimraf'; -import {SkypackSDK} from 'skypack'; import url from 'url'; import getDefaultBrowserId from 'default-browser-id'; -import {ImportMap, LockfileManifest, SnowpackConfig} from './types'; +import type {ImportMap, LockfileManifest, SnowpackConfig} from './types'; +import type {InstallTarget} from 'esinstall'; +import {SkypackSDK} from 'skypack'; // (!) Beware circular dependencies! No relative imports! // Because this file is imported from so many different parts of Snowpack, @@ -77,6 +78,16 @@ export async function readLockfile(cwd: string): Promise(originalObject: Record): Record { const newObject = {}; for (const key of Object.keys(originalObject).sort()) { @@ -352,7 +363,7 @@ export function getExtensionMatch( let extensionPartial; let extensionMatch; // If a full URL is given, start at the basename. Otherwise, start at zero. - let extensionMatchIndex = Math.max(0, fileName.lastIndexOf('/')); + let extensionMatchIndex = Math.max(0, fileName.lastIndexOf('/'), fileName.lastIndexOf('\\')); // Grab expanded file extensions, from longest to shortest. while (!extensionMatch && extensionMatchIndex > -1) { extensionMatchIndex++; diff --git a/test-dev/__snapshots__/dev.test.ts.snap b/test-dev/__snapshots__/dev.test.ts.snap index 1789ead516..fafc15ddf9 100644 --- a/test-dev/__snapshots__/dev.test.ts.snap +++ b/test-dev/__snapshots__/dev.test.ts.snap @@ -3,7 +3,8 @@ exports[`snowpack dev smoke: about 1`] = ` " - + +

this is a template in some language that builds to .html

@@ -20,7 +21,8 @@ exports[`snowpack dev smoke: html 1`] = ` Snowpack App - + + @@ -47,7 +49,7 @@ exports[`snowpack dev smoke: js 1`] = ` * When you're ready to start on your site, clear the file. Happy hacking! **/ -import confetti from '../_snowpack/pkg/canvas-confetti.js'; +import confetti from '../_snowpack/pkg/canvas-confetti.v1.3.2.js'; confetti.create(document.getElementById('canvas'), { resize: true, diff --git a/test/build/cdn/cdn.test.js b/test/build/cdn/cdn.test.js index 127d1ad4a8..44781176c1 100644 --- a/test/build/cdn/cdn.test.js +++ b/test/build/cdn/cdn.test.js @@ -20,10 +20,10 @@ describe('CDN URLs', () => { it('JS: preserves CDN URLs', () => { expect(files['/_dist_/index.js']).toEqual( - expect.stringContaining('import React from "https://cdn.pika.dev/react@^16.13.1";'), + expect.stringContaining('import React from "https://cdn.skypack.dev/react@^17.0.0";'), ); expect(files['/_dist_/index.js']).toEqual( - expect.stringContaining('import ReactDOM from "https://cdn.pika.dev/react-dom@^16.13.1";'), + expect.stringContaining('import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.0";'), ); }); diff --git a/test/build/cdn/src/index.jsx b/test/build/cdn/src/index.jsx index 3ecc350288..c5e80697c7 100644 --- a/test/build/cdn/src/index.jsx +++ b/test/build/cdn/src/index.jsx @@ -1,5 +1,5 @@ -import React from 'https://cdn.pika.dev/react@^16.13.1'; -import ReactDOM from 'https://cdn.pika.dev/react-dom@^16.13.1'; +import React from 'https://cdn.skypack.dev/react@^17.0.0'; +import ReactDOM from 'https://cdn.skypack.dev/react-dom@^17.0.0'; const App = () =>
I’m an app!
; diff --git a/test/build/config-alias/__snapshots__/config-alias.test.js.snap b/test/build/config-alias/__snapshots__/config-alias.test.js.snap index f5814591f8..ebd4e0444d 100644 --- a/test/build/config-alias/__snapshots__/config-alias.test.js.snap +++ b/test/build/config-alias/__snapshots__/config-alias.test.js.snap @@ -35,9 +35,9 @@ exports[`config: alias generates imports as expected 1`] = ` console.log(oneToManyBuild); // Importing an absolute URL: we don't touch these - import absoluteUrl from './sort.js'; // absolute import - import absoluteUrl_ from '../foo.svelte'; // absolute URL, plugin-provided file extension - import absoluteUrl__ from '../test-mjs'; // absolute URL, missing file extension + import absoluteUrl from './sort.js'; // absolute URL + import absoluteUrl_ from './foo.svelte.js'; // absolute URL + import absoluteUrl__ from './test-mjs.js'; // absolute URL console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); // Importing a directory index.js file @@ -105,9 +105,9 @@ import oneToManyBuild from './foo.svelte.js'; // plugin-provided file extension console.log(oneToManyBuild); // Importing an absolute URL: we don't touch these -import absoluteUrl from './sort.js'; // absolute import -import absoluteUrl_ from '../foo.svelte'; // absolute URL, plugin-provided file extension -import absoluteUrl__ from '../test-mjs'; // absolute URL, missing file extension +import absoluteUrl from './sort.js'; // absolute URL +import absoluteUrl_ from './foo.svelte.js'; // absolute URL +import absoluteUrl__ from './test-mjs.js'; // absolute URL console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); diff --git a/test/build/config-alias/config-alias.test.js b/test/build/config-alias/config-alias.test.js index f3d7dd7a33..27984cd49a 100644 --- a/test/build/config-alias/config-alias.test.js +++ b/test/build/config-alias/config-alias.test.js @@ -8,8 +8,8 @@ let files = {}; describe('config: alias', () => { beforeAll(() => { setupBuildTest(__dirname); - files = readFiles(cwd); + console.error('GO!'); }); it('generates imports as expected', () => { diff --git a/test/build/config-alias/snowpack.config.js b/test/build/config-alias/snowpack.config.js index db263a86d5..d485b13f24 100644 --- a/test/build/config-alias/snowpack.config.js +++ b/test/build/config-alias/snowpack.config.js @@ -15,5 +15,8 @@ module.exports = { baseUrl: 'https://example.com/foo', metaUrlPath: '/TEST_WMU/', }, - plugins: ['./simple-file-extension-change-plugin.js'], + plugins: [ + '@snowpack/plugin-svelte', + './simple-file-extension-change-plugin.js' + ], }; diff --git a/test/build/config-alias/src/index.html b/test/build/config-alias/src/index.html index 949f6f83bc..f90492b20a 100644 --- a/test/build/config-alias/src/index.html +++ b/test/build/config-alias/src/index.html @@ -32,9 +32,9 @@ console.log(oneToManyBuild); // Importing an absolute URL: we don't touch these - import absoluteUrl from '/_dist_/sort.js'; // absolute import - import absoluteUrl_ from '/foo.svelte'; // absolute URL, plugin-provided file extension - import absoluteUrl__ from '/test-mjs'; // absolute URL, missing file extension + import absoluteUrl from '/_dist_/sort.js'; // absolute URL + import absoluteUrl_ from '/_dist_/foo.svelte.js'; // absolute URL + import absoluteUrl__ from '/_dist_/test-mjs.js'; // absolute URL console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); // Importing a directory index.js file diff --git a/test/build/config-alias/src/index.js b/test/build/config-alias/src/index.js index 94af24875f..d407ce4bdb 100644 --- a/test/build/config-alias/src/index.js +++ b/test/build/config-alias/src/index.js @@ -21,9 +21,9 @@ import oneToManyBuild from './foo.svelte'; // plugin-provided file extension console.log(oneToManyBuild); // Importing an absolute URL: we don't touch these -import absoluteUrl from '/_dist_/sort.js'; // absolute import -import absoluteUrl_ from '/foo.svelte'; // absolute URL, plugin-provided file extension -import absoluteUrl__ from '/test-mjs'; // absolute URL, missing file extension +import absoluteUrl from '/_dist_/sort.js'; // absolute URL +import absoluteUrl_ from '/_dist_/foo.svelte.js'; // absolute URL +import absoluteUrl__ from '/_dist_/test-mjs.js'; // absolute URL console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); diff --git a/test/build/config-mount/config-mount.test.js b/test/build/config-mount/config-mount.test.js index 051ef176c4..2d477655af 100644 --- a/test/build/config-mount/config-mount.test.js +++ b/test/build/config-mount/config-mount.test.js @@ -71,7 +71,7 @@ describe('config: mount', () => { it('url', () => { const $ = cheerio.load(files['/new-g/main.html']); expect(files['/new-g/index.js']).toEqual(expect.stringContaining(`import "./dep.js";`)); // formatter ran - expect($('script[type="module"]').attr('src')).toBe('/_dist_/index.js'); // JS resolved + expect($('script[type="module"]').attr('src')).toBe('/g/index.js'); // JS resolved }); it('static', () => { diff --git a/test/build/config-mount/src/g/main.html b/test/build/config-mount/src/g/main.html index 738c1b619d..565e0c0737 100644 --- a/test/build/config-mount/src/g/main.html +++ b/test/build/config-mount/src/g/main.html @@ -7,7 +7,7 @@ Snowpack App - + + + + diff --git a/test/build/package-workspace/src/index.svelte b/test/build/package-workspace/src/index.svelte new file mode 100644 index 0000000000..c093c78a0e --- /dev/null +++ b/test/build/package-workspace/src/index.svelte @@ -0,0 +1,10 @@ + + + + diff --git a/test/build/plugin-build-svelte/__snapshots__/plugin-build-svelte.test.js.snap b/test/build/plugin-build-svelte/__snapshots__/plugin-build-svelte.test.js.snap new file mode 100644 index 0000000000..e5cbcfee70 --- /dev/null +++ b/test/build/plugin-build-svelte/__snapshots__/plugin-build-svelte.test.js.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@snowpack/plugin-build-svelte builds package svelte files as expected: svelte-awesome treeshaking 1`] = ` +"var refresh = { refresh: { width: 1536, height: 1792, paths: [{ d: 'M1511 1056q0 5-1 7-64 268-268 434.5t-478 166.5q-146 0-282.5-55t-243.5-157l-129 129q-19 19-45 19t-45-19-19-45v-448q0-26 19-45t45-19h448q26 0 45 19t19 45-19 45l-137 137q71 66 161 102t187 36q134 0 250-65t186-179q11-17 53-117 8-23 30-23h192q13 0 22.5 9.5t9.5 22.5zM1536 256v448q0 26-19 45t-45 19h-448q-26 0-45-19t-19-45 19-45l138-138q-148-137-349-137-134 0-250 65t-186 179q-11 17-53 117-8 23-30 23h-199q-13 0-22.5-9.5t-9.5-22.5v-7q65-268 270-434.5t480-166.5q146 0 284 55.5t245 156.5l130-129q19-19 45-19t45 19 19 45z' }] } }; + +var camera = { camera: { width: 1920, height: 1792, paths: [{ d: 'M960 672q119 0 203.5 84.5t84.5 203.5-84.5 203.5-203.5 84.5-203.5-84.5-84.5-203.5 84.5-203.5 203.5-84.5zM1664 256q106 0 181 75t75 181v896q0 106-75 181t-181 75h-1408q-106 0-181-75t-75-181v-896q0-106 75-181t181-75h224l51-136q19-49 69.5-84.5t103.5-35.5h512q53 0 103.5 35.5t69.5 84.5l51 136h224zM960 1408q185 0 316.5-131.5t131.5-316.5-131.5-316.5-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5z' }] } }; + +var comment = { comment: { width: 1792, height: 1792, paths: [{ d: 'M1792 896q0 174-120 321.5t-326 233-450 85.5q-70 0-145-8-198 175-460 242-49 14-114 22-17 2-30.5-9t-17.5-29v-1q-3-4-0.5-12t2-10 4.5-9.5l6-9t7-8.5 8-9q7-8 31-34.5t34.5-38 31-39.5 32.5-51 27-59 26-76q-157-89-247.5-220t-90.5-281q0-130 71-248.5t191-204.5 286-136.5 348-50.5q244 0 450 85.5t326 233 120 321.5z' }] } }; + +export { camera, comment, refresh }; +" +`; diff --git a/test/build/plugin-build-svelte/package.json b/test/build/plugin-build-svelte/package.json new file mode 100644 index 0000000000..059594578a --- /dev/null +++ b/test/build/plugin-build-svelte/package.json @@ -0,0 +1,16 @@ +{ + "private": true, + "version": "1.0.1", + "name": "@snowpack/test-plugin-build-svelte", + "description": "A test to make sure @snowpack/plugin-svelte works", + "scripts": { + "testbuild": "snowpack build" + }, + "devDependencies": { + "snowpack": "^3.0.0" + }, + "dependencies": { + "svelte-awesome": "^2.3.0", + "svelte-package-a": "file:./packages/svelte-package-a" + } +} diff --git a/test/build/plugin-build-svelte/packages/svelte-package-a/SvelteComponent.svelte b/test/build/plugin-build-svelte/packages/svelte-package-a/SvelteComponent.svelte new file mode 100644 index 0000000000..209087a019 --- /dev/null +++ b/test/build/plugin-build-svelte/packages/svelte-package-a/SvelteComponent.svelte @@ -0,0 +1 @@ +
I am an npm package svelte component!
\ No newline at end of file diff --git a/test/build/plugin-build-svelte/packages/svelte-package-a/index.js b/test/build/plugin-build-svelte/packages/svelte-package-a/index.js new file mode 100644 index 0000000000..1df3ed5712 --- /dev/null +++ b/test/build/plugin-build-svelte/packages/svelte-package-a/index.js @@ -0,0 +1,3 @@ +export function notExpected() { + throw new Error("This test should use the Svelte entrypoint instead."); +} \ No newline at end of file diff --git a/test/build/plugin-build-svelte/packages/svelte-package-a/package.json b/test/build/plugin-build-svelte/packages/svelte-package-a/package.json new file mode 100644 index 0000000000..79b2d5c530 --- /dev/null +++ b/test/build/plugin-build-svelte/packages/svelte-package-a/package.json @@ -0,0 +1,6 @@ +{ + "name": "svelte-package-a", + "version": "1.2.3", + "main": "index.js", + "svelte": "SvelteComponent.svelte" +} diff --git a/test/build/plugin-build-svelte/plugin-build-svelte.test.js b/test/build/plugin-build-svelte/plugin-build-svelte.test.js new file mode 100644 index 0000000000..3333586518 --- /dev/null +++ b/test/build/plugin-build-svelte/plugin-build-svelte.test.js @@ -0,0 +1,26 @@ +const fs = require('fs'); +const path = require('path'); +const {setupBuildTest} = require('../../test-utils'); + +const cwd = path.join(__dirname, 'build'); + +describe('@snowpack/plugin-build-svelte', () => { + beforeAll(() => { + setupBuildTest(__dirname); + }); + + it('builds source svelte files as expected', () => { + const jsLoc = path.join(cwd, '_dist_', 'index.svelte.js'); + expect(fs.existsSync(jsLoc)).toBe(true); // file exists + expect(fs.readFileSync(jsLoc, 'utf-8')).toContain(`import { refresh, comment, camera } from "../_snowpack/pkg/svelte-awesome/icons.js";`); // file has expected imports + expect(fs.readFileSync(jsLoc, 'utf-8')).toContain(`import './index.svelte.css.proxy.js';`); // file has expected imports + + const cssLoc = path.join(cwd, '_dist_', 'index.svelte.css.proxy.js'); + expect(fs.existsSync(cssLoc)).toBe(true); // file exists + }); + + it('builds package svelte files as expected', () => { + expect(fs.existsSync(path.join(cwd, '_snowpack', 'pkg', 'svelte-awesome.js'))).toBe(true); // import exists + expect(fs.readFileSync(path.join(cwd, '_snowpack', 'pkg', 'svelte-awesome', 'icons.js'), 'utf-8')).toMatchSnapshot('svelte-awesome treeshaking'); // import exists, and was tree-shaken + }); +}); diff --git a/test/build/plugin-build-svelte/snowpack.config.js b/test/build/plugin-build-svelte/snowpack.config.js new file mode 100644 index 0000000000..f3ae35825b --- /dev/null +++ b/test/build/plugin-build-svelte/snowpack.config.js @@ -0,0 +1,10 @@ +module.exports = { + mount: { + src: '/_dist_', + }, + plugins: [ + [ + '@snowpack/plugin-svelte', + ], + ], +}; diff --git a/test/build/plugin-build-svelte/src/index.svelte b/test/build/plugin-build-svelte/src/index.svelte new file mode 100644 index 0000000000..1a50098100 --- /dev/null +++ b/test/build/plugin-build-svelte/src/index.svelte @@ -0,0 +1,12 @@ + + + + +
Hello, test!
\ No newline at end of file diff --git a/test/build/test-workspace-component/README.md b/test/build/test-workspace-component/README.md new file mode 100644 index 0000000000..361014ccf9 --- /dev/null +++ b/test/build/test-workspace-component/README.md @@ -0,0 +1,3 @@ +# test-workspace-component + +This is a test component for the workspace that lets us test symlink support. It is private, not published, and should not ever be needed outside of testing. \ No newline at end of file diff --git a/test/build/test-workspace-component/SvelteComponent.svelte b/test/build/test-workspace-component/SvelteComponent.svelte new file mode 100755 index 0000000000..f8dcb725bd --- /dev/null +++ b/test/build/test-workspace-component/SvelteComponent.svelte @@ -0,0 +1,10 @@ + + + + + +
+ Hello! This is a test component! +
diff --git a/test/build/test-workspace-component/index.mjs b/test/build/test-workspace-component/index.mjs new file mode 100755 index 0000000000..7e66e5111f --- /dev/null +++ b/test/build/test-workspace-component/index.mjs @@ -0,0 +1,3 @@ +export function testComponent() { + return 42; +} \ No newline at end of file diff --git a/test/build/test-workspace-component/package.json b/test/build/test-workspace-component/package.json new file mode 100644 index 0000000000..a28aa347a2 --- /dev/null +++ b/test/build/test-workspace-component/package.json @@ -0,0 +1,10 @@ +{ + "name": "test-workspace-component", + "private": true, + "version": "1.0.0", + "license": "MIT", + "main": "index.mjs", + "dependencies": { + "canvas-confetti": "^1.2.0" + } +} diff --git a/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap b/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap index 8422fc6521..4e67e0e576 100644 --- a/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap +++ b/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap @@ -1,45 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`create-snowpack-app app-template-11ty > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-11ty > build: _snowpack/pkg/canvas-confetti.js 1`] = ` -"// canvas-confetti v1.3.2 built on 2020-11-12T12:53:54.473Z -var module = {}; -// source content -(function main(global, module, isWorker, workerSize) { - var canUseWorker = !!( - global.Worker && - global.Blob && - global.Promise && - global.OffscreenCanvas && - global.OffscreenCanvasRenderingContext2D && - global.HTMLCanvasElement && - global.HTMLCanvasElement.prototype.transferControlToOffscreen && - global.URL && - global.URL.createObjectURL); - function noop() {} - // create a promise if it exists, otherwise, just - // call the function directly +"var module = {}; +(function main(global, module2, isWorker, workerSize) { + var canUseWorker = !!(global.Worker && global.Blob && global.Promise && global.OffscreenCanvas && global.OffscreenCanvasRenderingContext2D && global.HTMLCanvasElement && global.HTMLCanvasElement.prototype.transferControlToOffscreen && global.URL && global.URL.createObjectURL); + function noop() { + } function promise(func) { - var ModulePromise = module.exports.Promise; + var ModulePromise = module2.exports.Promise; var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise; - if (typeof Prom === 'function') { + if (typeof Prom === \\"function\\") { return new Prom(func); } func(noop, noop); return null; } - var raf = (function () { - var TIME = Math.floor(1000 / 60); + var raf = function() { + var TIME = Math.floor(1e3 / 60); var frame, cancel; var frames = {}; var lastFrameTime = 0; - if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') { - frame = function (cb) { + if (typeof requestAnimationFrame === \\"function\\" && typeof cancelAnimationFrame === \\"function\\") { + frame = function(cb) { var id = Math.random(); frames[id] = requestAnimationFrame(function onFrame(time) { if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) { @@ -52,103 +34,102 @@ var module = {}; }); return id; }; - cancel = function (id) { + cancel = function(id) { if (frames[id]) { cancelAnimationFrame(frames[id]); } }; } else { - frame = function (cb) { + frame = function(cb) { return setTimeout(cb, TIME); }; - cancel = function (timer) { + cancel = function(timer) { return clearTimeout(timer); }; } - return { frame: frame, cancel: cancel }; - }()); - var getWorker = (function () { + return {frame, cancel}; + }(); + var getWorker = function() { var worker; var prom; var resolves = {}; - function decorate(worker) { + function decorate(worker2) { function execute(options, callback) { - worker.postMessage({ options: options || {}, callback: callback }); + worker2.postMessage({options: options || {}, callback}); } - worker.init = function initWorker(canvas) { + worker2.init = function initWorker(canvas) { var offscreen = canvas.transferControlToOffscreen(); - worker.postMessage({ canvas: offscreen }, [offscreen]); + worker2.postMessage({canvas: offscreen}, [offscreen]); }; - worker.fire = function fireWorker(options, size, done) { + worker2.fire = function fireWorker(options, size, done) { if (prom) { execute(options, null); return prom; } var id = Math.random().toString(36).slice(2); - prom = promise(function (resolve) { + prom = promise(function(resolve) { function workerDone(msg) { if (msg.data.callback !== id) { return; } delete resolves[id]; - worker.removeEventListener('message', workerDone); + worker2.removeEventListener(\\"message\\", workerDone); prom = null; done(); resolve(); } - worker.addEventListener('message', workerDone); + worker2.addEventListener(\\"message\\", workerDone); execute(options, id); - resolves[id] = workerDone.bind(null, { data: { callback: id }}); + resolves[id] = workerDone.bind(null, {data: {callback: id}}); }); return prom; }; - worker.reset = function resetWorker() { - worker.postMessage({ reset: true }); + worker2.reset = function resetWorker() { + worker2.postMessage({reset: true}); for (var id in resolves) { resolves[id](); delete resolves[id]; } }; } - return function () { + return function() { if (worker) { return worker; } if (!isWorker && canUseWorker) { var code = [ - 'var CONFETTI, SIZE = {}, module = {};', - '(' + main.toString() + ')(this, module, true, SIZE);', - 'onmessage = function(msg) {', - ' if (msg.data.options) {', - ' CONFETTI(msg.data.options).then(function () {', - ' if (msg.data.callback) {', - ' postMessage({ callback: msg.data.callback });', - ' }', - ' });', - ' } else if (msg.data.reset) {', - ' CONFETTI.reset();', - ' } else if (msg.data.resize) {', - ' SIZE.width = msg.data.resize.width;', - ' SIZE.height = msg.data.resize.height;', - ' } else if (msg.data.canvas) {', - ' SIZE.width = msg.data.canvas.width;', - ' SIZE.height = msg.data.canvas.height;', - ' CONFETTI = module.exports.create(msg.data.canvas);', - ' }', - '}', - ].join(''); + \\"var CONFETTI, SIZE = {}, module = {};\\", + \\"(\\" + main.toString() + \\")(this, module, true, SIZE);\\", + \\"onmessage = function(msg) {\\", + \\" if (msg.data.options) {\\", + \\" CONFETTI(msg.data.options).then(function () {\\", + \\" if (msg.data.callback) {\\", + \\" postMessage({ callback: msg.data.callback });\\", + \\" }\\", + \\" });\\", + \\" } else if (msg.data.reset) {\\", + \\" CONFETTI.reset();\\", + \\" } else if (msg.data.resize) {\\", + \\" SIZE.width = msg.data.resize.width;\\", + \\" SIZE.height = msg.data.resize.height;\\", + \\" } else if (msg.data.canvas) {\\", + \\" SIZE.width = msg.data.canvas.width;\\", + \\" SIZE.height = msg.data.canvas.height;\\", + \\" CONFETTI = module.exports.create(msg.data.canvas);\\", + \\" }\\", + \\"}\\" + ].join(\\"\\"); try { worker = new Worker(URL.createObjectURL(new Blob([code]))); } catch (e) { - // eslint-disable-next-line no-console - typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null; + typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"\\\\u{1F38A} Could not load worker\\", e) : null; return null; } decorate(worker); } return worker; }; - })(); + }(); var defaults = { particleCount: 50, angle: 90, @@ -159,18 +140,17 @@ var module = {}; ticks: 200, x: 0.5, y: 0.5, - shapes: ['square', 'circle'], + shapes: [\\"square\\", \\"circle\\"], zIndex: 100, colors: [ - '#26ccff', - '#a25afd', - '#ff5e7e', - '#88ff5a', - '#fcff42', - '#ffa62d', - '#ff36ff' + \\"#26ccff\\", + \\"#a25afd\\", + \\"#ff5e7e\\", + \\"#88ff5a\\", + \\"#fcff42\\", + \\"#ffa62d\\", + \\"#ff36ff\\" ], - // probably should be true, but back-compat disableForReducedMotion: false, scalar: 1 }; @@ -178,39 +158,35 @@ var module = {}; return transform ? transform(val) : val; } function isOk(val) { - return !(val === null || val === undefined); + return !(val === null || val === void 0); } function prop(options, name, transform) { - return convert( - options && isOk(options[name]) ? options[name] : defaults[name], - transform - ); + return convert(options && isOk(options[name]) ? options[name] : defaults[name], transform); } - function onlyPositiveInt(number){ + function onlyPositiveInt(number) { return number < 0 ? 0 : Math.floor(number); } function randomInt(min, max) { - // [min, max) return Math.floor(Math.random() * (max - min)) + min; } function toDecimal(str) { return parseInt(str, 16); } function hexToRgb(str) { - var val = String(str).replace(/[^0-9a-f]/gi, ''); + var val = String(str).replace(/[^0-9a-f]/gi, \\"\\"); if (val.length < 6) { - val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2]; + val = val[0] + val[0] + val[1] + val[1] + val[2] + val[2]; } return { - r: toDecimal(val.substring(0,2)), - g: toDecimal(val.substring(2,4)), - b: toDecimal(val.substring(4,6)) + r: toDecimal(val.substring(0, 2)), + g: toDecimal(val.substring(2, 4)), + b: toDecimal(val.substring(4, 6)) }; } function getOrigin(options) { - var origin = prop(options, 'origin', Object); - origin.x = prop(origin, 'x', Number); - origin.y = prop(origin, 'y', Number); + var origin = prop(options, \\"origin\\", Object); + origin.x = prop(origin, \\"x\\", Number); + origin.y = prop(origin, \\"y\\", Number); return origin; } function setCanvasWindowSize(canvas) { @@ -223,11 +199,11 @@ var module = {}; canvas.height = rect.height; } function getCanvas(zIndex) { - var canvas = document.createElement('canvas'); - canvas.style.position = 'fixed'; - canvas.style.top = '0px'; - canvas.style.left = '0px'; - canvas.style.pointerEvents = 'none'; + var canvas = document.createElement(\\"canvas\\"); + canvas.style.position = \\"fixed\\"; + canvas.style.top = \\"0px\\"; + canvas.style.left = \\"0px\\"; + canvas.style.pointerEvents = \\"none\\"; canvas.style.zIndex = zIndex; return canvas; } @@ -246,8 +222,8 @@ var module = {}; x: opts.x, y: opts.y, wobble: Math.random() * 10, - velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity), - angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)), + velocity: opts.startVelocity * 0.5 + Math.random() * opts.startVelocity, + angle2D: -radAngle + (0.5 * radSpread - Math.random() * radSpread), tiltAngle: Math.random() * Math.PI, color: hexToRgb(opts.color), shape: opts.shape, @@ -273,19 +249,17 @@ var module = {}; fetti.tiltSin = Math.sin(fetti.tiltAngle); fetti.tiltCos = Math.cos(fetti.tiltAngle); fetti.random = Math.random() + 5; - fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble)); - fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble)); - var progress = (fetti.tick++) / fetti.totalTicks; - var x1 = fetti.x + (fetti.random * fetti.tiltCos); - var y1 = fetti.y + (fetti.random * fetti.tiltSin); - var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos); - var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin); - context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')'; + fetti.wobbleX = fetti.x + 10 * fetti.scalar * Math.cos(fetti.wobble); + fetti.wobbleY = fetti.y + 10 * fetti.scalar * Math.sin(fetti.wobble); + var progress = fetti.tick++ / fetti.totalTicks; + var x1 = fetti.x + fetti.random * fetti.tiltCos; + var y1 = fetti.y + fetti.random * fetti.tiltSin; + var x2 = fetti.wobbleX + fetti.random * fetti.tiltCos; + var y2 = fetti.wobbleY + fetti.random * fetti.tiltSin; + context.fillStyle = \\"rgba(\\" + fetti.color.r + \\", \\" + fetti.color.g + \\", \\" + fetti.color.b + \\", \\" + (1 - progress) + \\")\\"; context.beginPath(); - if (fetti.shape === 'circle') { - context.ellipse ? - context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : - ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); + if (fetti.shape === \\"circle\\") { + context.ellipse ? context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); } else { context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y)); context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1)); @@ -298,10 +272,10 @@ var module = {}; } function animate(canvas, fettis, resizer, size, done) { var animatingFettis = fettis.slice(); - var context = canvas.getContext('2d'); + var context = canvas.getContext(\\"2d\\"); var animationFrame; var destroy; - var prom = promise(function (resolve) { + var prom = promise(function(resolve) { function onDone() { animationFrame = destroy = null; context.clearRect(0, 0, size.width, size.height); @@ -319,7 +293,7 @@ var module = {}; size.height = canvas.height; } context.clearRect(0, 0, size.width, size.height); - animatingFettis = animatingFettis.filter(function (fetti) { + animatingFettis = animatingFettis.filter(function(fetti) { return updateFetti(context, fetti); }); if (animatingFettis.length) { @@ -332,13 +306,13 @@ var module = {}; destroy = onDone; }); return { - addFettis: function (fettis) { - animatingFettis = animatingFettis.concat(fettis); + addFettis: function(fettis2) { + animatingFettis = animatingFettis.concat(fettis2); return prom; }, - canvas: canvas, + canvas, promise: prom, - reset: function () { + reset: function() { if (animationFrame) { raf.cancel(animationFrame); } @@ -350,73 +324,66 @@ var module = {}; } function confettiCannon(canvas, globalOpts) { var isLibCanvas = !canvas; - var allowResize = !!prop(globalOpts || {}, 'resize'); - var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean); - var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker'); + var allowResize = !!prop(globalOpts || {}, \\"resize\\"); + var globalDisableForReducedMotion = prop(globalOpts, \\"disableForReducedMotion\\", Boolean); + var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, \\"useWorker\\"); var worker = shouldUseWorker ? getWorker() : null; var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize; - var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false; - var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches; + var initialized = canvas && worker ? !!canvas.__confetti_initialized : false; + var preferLessMotion = typeof matchMedia === \\"function\\" && matchMedia(\\"(prefers-reduced-motion)\\").matches; var animationObj; function fireLocal(options, size, done) { - var particleCount = prop(options, 'particleCount', onlyPositiveInt); - var angle = prop(options, 'angle', Number); - var spread = prop(options, 'spread', Number); - var startVelocity = prop(options, 'startVelocity', Number); - var decay = prop(options, 'decay', Number); - var gravity = prop(options, 'gravity', Number); - var colors = prop(options, 'colors'); - var ticks = prop(options, 'ticks', Number); - var shapes = prop(options, 'shapes'); - var scalar = prop(options, 'scalar'); + var particleCount = prop(options, \\"particleCount\\", onlyPositiveInt); + var angle = prop(options, \\"angle\\", Number); + var spread = prop(options, \\"spread\\", Number); + var startVelocity = prop(options, \\"startVelocity\\", Number); + var decay = prop(options, \\"decay\\", Number); + var gravity = prop(options, \\"gravity\\", Number); + var colors = prop(options, \\"colors\\"); + var ticks = prop(options, \\"ticks\\", Number); + var shapes = prop(options, \\"shapes\\"); + var scalar = prop(options, \\"scalar\\"); var origin = getOrigin(options); var temp = particleCount; var fettis = []; var startX = canvas.width * origin.x; var startY = canvas.height * origin.y; while (temp--) { - fettis.push( - randomPhysics({ - x: startX, - y: startY, - angle: angle, - spread: spread, - startVelocity: startVelocity, - color: colors[temp % colors.length], - shape: shapes[randomInt(0, shapes.length)], - ticks: ticks, - decay: decay, - gravity: gravity, - scalar: scalar - }) - ); + fettis.push(randomPhysics({ + x: startX, + y: startY, + angle, + spread, + startVelocity, + color: colors[temp % colors.length], + shape: shapes[randomInt(0, shapes.length)], + ticks, + decay, + gravity, + scalar + })); } - // if we have a previous canvas already animating, - // add to it if (animationObj) { return animationObj.addFettis(fettis); } - animationObj = animate(canvas, fettis, resizer, size , done); + animationObj = animate(canvas, fettis, resizer, size, done); return animationObj.promise; } function fire(options) { - var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean); - var zIndex = prop(options, 'zIndex', Number); + var disableForReducedMotion = globalDisableForReducedMotion || prop(options, \\"disableForReducedMotion\\", Boolean); + var zIndex = prop(options, \\"zIndex\\", Number); if (disableForReducedMotion && preferLessMotion) { - return promise(function (resolve) { + return promise(function(resolve) { resolve(); }); } if (isLibCanvas && animationObj) { - // use existing canvas from in-progress animation canvas = animationObj.canvas; } else if (isLibCanvas && !canvas) { - // create and initialize a new canvas canvas = getCanvas(zIndex); document.body.appendChild(canvas); } if (allowResize && !initialized) { - // initialize the size of a user-supplied canvas resizer(canvas); } var size = { @@ -432,9 +399,8 @@ var module = {}; } function onResize() { if (worker) { - // TODO this really shouldn't be immediate, because it is expensive var obj = { - getBoundingClientRect: function () { + getBoundingClientRect: function() { if (!isLibCanvas) { return canvas.getBoundingClientRect(); } @@ -449,14 +415,12 @@ var module = {}; }); return; } - // don't actually query the size here, since this - // can execute frequently and rapidly size.width = size.height = null; } function done() { animationObj = null; if (allowResize) { - global.removeEventListener('resize', onResize); + global.removeEventListener(\\"resize\\", onResize); } if (isLibCanvas && canvas) { document.body.removeChild(canvas); @@ -465,14 +429,14 @@ var module = {}; } } if (allowResize) { - global.addEventListener('resize', onResize, false); + global.addEventListener(\\"resize\\", onResize, false); } if (worker) { return worker.fire(options, size, done); } return fireLocal(options, size, done); } - fire.reset = function () { + fire.reset = function() { if (worker) { worker.reset(); } @@ -482,18 +446,17 @@ var module = {}; }; return fire; } - module.exports = confettiCannon(null, { useWorker: true, resize: true }); - module.exports.create = confettiCannon; -}((function () { - if (typeof window !== 'undefined') { + module2.exports = confettiCannon(null, {useWorker: true, resize: true}); + module2.exports.create = confettiCannon; +})(function() { + if (typeof window !== \\"undefined\\") { return window; } - if (typeof self !== 'undefined') { + if (typeof self !== \\"undefined\\") { return self; } return this; -})(), module, false)); -// end source content +}(), module, false); var __pika_web_default_export_for_treeshaking__ = module.exports; var create = module.exports.create; export default __pika_web_default_export_for_treeshaking__;" @@ -540,7 +503,6 @@ exports[`create-snowpack-app app-template-11ty > build: about/index.html 1`] = ` exports[`create-snowpack-app app-template-11ty > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "_snowpack/pkg/canvas-confetti.js", "_snowpack/pkg/import-map.json", "about/index.html", @@ -622,46 +584,28 @@ a { }" `; -exports[`create-snowpack-app app-template-blank > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-blank > build: _snowpack/pkg/canvas-confetti.js 1`] = ` -"// canvas-confetti v1.3.2 built on 2020-11-12T12:53:54.473Z -var module = {}; -// source content -(function main(global, module, isWorker, workerSize) { - var canUseWorker = !!( - global.Worker && - global.Blob && - global.Promise && - global.OffscreenCanvas && - global.OffscreenCanvasRenderingContext2D && - global.HTMLCanvasElement && - global.HTMLCanvasElement.prototype.transferControlToOffscreen && - global.URL && - global.URL.createObjectURL); - function noop() {} - // create a promise if it exists, otherwise, just - // call the function directly +"var module = {}; +(function main(global, module2, isWorker, workerSize) { + var canUseWorker = !!(global.Worker && global.Blob && global.Promise && global.OffscreenCanvas && global.OffscreenCanvasRenderingContext2D && global.HTMLCanvasElement && global.HTMLCanvasElement.prototype.transferControlToOffscreen && global.URL && global.URL.createObjectURL); + function noop() { + } function promise(func) { - var ModulePromise = module.exports.Promise; + var ModulePromise = module2.exports.Promise; var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise; - if (typeof Prom === 'function') { + if (typeof Prom === \\"function\\") { return new Prom(func); } func(noop, noop); return null; } - var raf = (function () { - var TIME = Math.floor(1000 / 60); + var raf = function() { + var TIME = Math.floor(1e3 / 60); var frame, cancel; var frames = {}; var lastFrameTime = 0; - if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') { - frame = function (cb) { + if (typeof requestAnimationFrame === \\"function\\" && typeof cancelAnimationFrame === \\"function\\") { + frame = function(cb) { var id = Math.random(); frames[id] = requestAnimationFrame(function onFrame(time) { if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) { @@ -674,103 +618,102 @@ var module = {}; }); return id; }; - cancel = function (id) { + cancel = function(id) { if (frames[id]) { cancelAnimationFrame(frames[id]); } }; } else { - frame = function (cb) { + frame = function(cb) { return setTimeout(cb, TIME); }; - cancel = function (timer) { + cancel = function(timer) { return clearTimeout(timer); }; } - return { frame: frame, cancel: cancel }; - }()); - var getWorker = (function () { + return {frame, cancel}; + }(); + var getWorker = function() { var worker; var prom; var resolves = {}; - function decorate(worker) { + function decorate(worker2) { function execute(options, callback) { - worker.postMessage({ options: options || {}, callback: callback }); + worker2.postMessage({options: options || {}, callback}); } - worker.init = function initWorker(canvas) { + worker2.init = function initWorker(canvas) { var offscreen = canvas.transferControlToOffscreen(); - worker.postMessage({ canvas: offscreen }, [offscreen]); + worker2.postMessage({canvas: offscreen}, [offscreen]); }; - worker.fire = function fireWorker(options, size, done) { + worker2.fire = function fireWorker(options, size, done) { if (prom) { execute(options, null); return prom; } var id = Math.random().toString(36).slice(2); - prom = promise(function (resolve) { + prom = promise(function(resolve) { function workerDone(msg) { if (msg.data.callback !== id) { return; } delete resolves[id]; - worker.removeEventListener('message', workerDone); + worker2.removeEventListener(\\"message\\", workerDone); prom = null; done(); resolve(); } - worker.addEventListener('message', workerDone); + worker2.addEventListener(\\"message\\", workerDone); execute(options, id); - resolves[id] = workerDone.bind(null, { data: { callback: id }}); + resolves[id] = workerDone.bind(null, {data: {callback: id}}); }); return prom; }; - worker.reset = function resetWorker() { - worker.postMessage({ reset: true }); + worker2.reset = function resetWorker() { + worker2.postMessage({reset: true}); for (var id in resolves) { resolves[id](); delete resolves[id]; } }; } - return function () { + return function() { if (worker) { return worker; } if (!isWorker && canUseWorker) { var code = [ - 'var CONFETTI, SIZE = {}, module = {};', - '(' + main.toString() + ')(this, module, true, SIZE);', - 'onmessage = function(msg) {', - ' if (msg.data.options) {', - ' CONFETTI(msg.data.options).then(function () {', - ' if (msg.data.callback) {', - ' postMessage({ callback: msg.data.callback });', - ' }', - ' });', - ' } else if (msg.data.reset) {', - ' CONFETTI.reset();', - ' } else if (msg.data.resize) {', - ' SIZE.width = msg.data.resize.width;', - ' SIZE.height = msg.data.resize.height;', - ' } else if (msg.data.canvas) {', - ' SIZE.width = msg.data.canvas.width;', - ' SIZE.height = msg.data.canvas.height;', - ' CONFETTI = module.exports.create(msg.data.canvas);', - ' }', - '}', - ].join(''); + \\"var CONFETTI, SIZE = {}, module = {};\\", + \\"(\\" + main.toString() + \\")(this, module, true, SIZE);\\", + \\"onmessage = function(msg) {\\", + \\" if (msg.data.options) {\\", + \\" CONFETTI(msg.data.options).then(function () {\\", + \\" if (msg.data.callback) {\\", + \\" postMessage({ callback: msg.data.callback });\\", + \\" }\\", + \\" });\\", + \\" } else if (msg.data.reset) {\\", + \\" CONFETTI.reset();\\", + \\" } else if (msg.data.resize) {\\", + \\" SIZE.width = msg.data.resize.width;\\", + \\" SIZE.height = msg.data.resize.height;\\", + \\" } else if (msg.data.canvas) {\\", + \\" SIZE.width = msg.data.canvas.width;\\", + \\" SIZE.height = msg.data.canvas.height;\\", + \\" CONFETTI = module.exports.create(msg.data.canvas);\\", + \\" }\\", + \\"}\\" + ].join(\\"\\"); try { worker = new Worker(URL.createObjectURL(new Blob([code]))); } catch (e) { - // eslint-disable-next-line no-console - typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null; + typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"\\\\u{1F38A} Could not load worker\\", e) : null; return null; } decorate(worker); } return worker; }; - })(); + }(); var defaults = { particleCount: 50, angle: 90, @@ -781,18 +724,17 @@ var module = {}; ticks: 200, x: 0.5, y: 0.5, - shapes: ['square', 'circle'], + shapes: [\\"square\\", \\"circle\\"], zIndex: 100, colors: [ - '#26ccff', - '#a25afd', - '#ff5e7e', - '#88ff5a', - '#fcff42', - '#ffa62d', - '#ff36ff' + \\"#26ccff\\", + \\"#a25afd\\", + \\"#ff5e7e\\", + \\"#88ff5a\\", + \\"#fcff42\\", + \\"#ffa62d\\", + \\"#ff36ff\\" ], - // probably should be true, but back-compat disableForReducedMotion: false, scalar: 1 }; @@ -800,39 +742,35 @@ var module = {}; return transform ? transform(val) : val; } function isOk(val) { - return !(val === null || val === undefined); + return !(val === null || val === void 0); } function prop(options, name, transform) { - return convert( - options && isOk(options[name]) ? options[name] : defaults[name], - transform - ); + return convert(options && isOk(options[name]) ? options[name] : defaults[name], transform); } - function onlyPositiveInt(number){ + function onlyPositiveInt(number) { return number < 0 ? 0 : Math.floor(number); } function randomInt(min, max) { - // [min, max) return Math.floor(Math.random() * (max - min)) + min; } function toDecimal(str) { return parseInt(str, 16); } function hexToRgb(str) { - var val = String(str).replace(/[^0-9a-f]/gi, ''); + var val = String(str).replace(/[^0-9a-f]/gi, \\"\\"); if (val.length < 6) { - val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2]; + val = val[0] + val[0] + val[1] + val[1] + val[2] + val[2]; } return { - r: toDecimal(val.substring(0,2)), - g: toDecimal(val.substring(2,4)), - b: toDecimal(val.substring(4,6)) + r: toDecimal(val.substring(0, 2)), + g: toDecimal(val.substring(2, 4)), + b: toDecimal(val.substring(4, 6)) }; } function getOrigin(options) { - var origin = prop(options, 'origin', Object); - origin.x = prop(origin, 'x', Number); - origin.y = prop(origin, 'y', Number); + var origin = prop(options, \\"origin\\", Object); + origin.x = prop(origin, \\"x\\", Number); + origin.y = prop(origin, \\"y\\", Number); return origin; } function setCanvasWindowSize(canvas) { @@ -845,11 +783,11 @@ var module = {}; canvas.height = rect.height; } function getCanvas(zIndex) { - var canvas = document.createElement('canvas'); - canvas.style.position = 'fixed'; - canvas.style.top = '0px'; - canvas.style.left = '0px'; - canvas.style.pointerEvents = 'none'; + var canvas = document.createElement(\\"canvas\\"); + canvas.style.position = \\"fixed\\"; + canvas.style.top = \\"0px\\"; + canvas.style.left = \\"0px\\"; + canvas.style.pointerEvents = \\"none\\"; canvas.style.zIndex = zIndex; return canvas; } @@ -868,8 +806,8 @@ var module = {}; x: opts.x, y: opts.y, wobble: Math.random() * 10, - velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity), - angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)), + velocity: opts.startVelocity * 0.5 + Math.random() * opts.startVelocity, + angle2D: -radAngle + (0.5 * radSpread - Math.random() * radSpread), tiltAngle: Math.random() * Math.PI, color: hexToRgb(opts.color), shape: opts.shape, @@ -895,19 +833,17 @@ var module = {}; fetti.tiltSin = Math.sin(fetti.tiltAngle); fetti.tiltCos = Math.cos(fetti.tiltAngle); fetti.random = Math.random() + 5; - fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble)); - fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble)); - var progress = (fetti.tick++) / fetti.totalTicks; - var x1 = fetti.x + (fetti.random * fetti.tiltCos); - var y1 = fetti.y + (fetti.random * fetti.tiltSin); - var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos); - var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin); - context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')'; + fetti.wobbleX = fetti.x + 10 * fetti.scalar * Math.cos(fetti.wobble); + fetti.wobbleY = fetti.y + 10 * fetti.scalar * Math.sin(fetti.wobble); + var progress = fetti.tick++ / fetti.totalTicks; + var x1 = fetti.x + fetti.random * fetti.tiltCos; + var y1 = fetti.y + fetti.random * fetti.tiltSin; + var x2 = fetti.wobbleX + fetti.random * fetti.tiltCos; + var y2 = fetti.wobbleY + fetti.random * fetti.tiltSin; + context.fillStyle = \\"rgba(\\" + fetti.color.r + \\", \\" + fetti.color.g + \\", \\" + fetti.color.b + \\", \\" + (1 - progress) + \\")\\"; context.beginPath(); - if (fetti.shape === 'circle') { - context.ellipse ? - context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : - ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); + if (fetti.shape === \\"circle\\") { + context.ellipse ? context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); } else { context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y)); context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1)); @@ -920,10 +856,10 @@ var module = {}; } function animate(canvas, fettis, resizer, size, done) { var animatingFettis = fettis.slice(); - var context = canvas.getContext('2d'); + var context = canvas.getContext(\\"2d\\"); var animationFrame; var destroy; - var prom = promise(function (resolve) { + var prom = promise(function(resolve) { function onDone() { animationFrame = destroy = null; context.clearRect(0, 0, size.width, size.height); @@ -941,7 +877,7 @@ var module = {}; size.height = canvas.height; } context.clearRect(0, 0, size.width, size.height); - animatingFettis = animatingFettis.filter(function (fetti) { + animatingFettis = animatingFettis.filter(function(fetti) { return updateFetti(context, fetti); }); if (animatingFettis.length) { @@ -954,13 +890,13 @@ var module = {}; destroy = onDone; }); return { - addFettis: function (fettis) { - animatingFettis = animatingFettis.concat(fettis); + addFettis: function(fettis2) { + animatingFettis = animatingFettis.concat(fettis2); return prom; }, - canvas: canvas, + canvas, promise: prom, - reset: function () { + reset: function() { if (animationFrame) { raf.cancel(animationFrame); } @@ -972,73 +908,66 @@ var module = {}; } function confettiCannon(canvas, globalOpts) { var isLibCanvas = !canvas; - var allowResize = !!prop(globalOpts || {}, 'resize'); - var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean); - var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker'); + var allowResize = !!prop(globalOpts || {}, \\"resize\\"); + var globalDisableForReducedMotion = prop(globalOpts, \\"disableForReducedMotion\\", Boolean); + var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, \\"useWorker\\"); var worker = shouldUseWorker ? getWorker() : null; var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize; - var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false; - var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches; + var initialized = canvas && worker ? !!canvas.__confetti_initialized : false; + var preferLessMotion = typeof matchMedia === \\"function\\" && matchMedia(\\"(prefers-reduced-motion)\\").matches; var animationObj; function fireLocal(options, size, done) { - var particleCount = prop(options, 'particleCount', onlyPositiveInt); - var angle = prop(options, 'angle', Number); - var spread = prop(options, 'spread', Number); - var startVelocity = prop(options, 'startVelocity', Number); - var decay = prop(options, 'decay', Number); - var gravity = prop(options, 'gravity', Number); - var colors = prop(options, 'colors'); - var ticks = prop(options, 'ticks', Number); - var shapes = prop(options, 'shapes'); - var scalar = prop(options, 'scalar'); + var particleCount = prop(options, \\"particleCount\\", onlyPositiveInt); + var angle = prop(options, \\"angle\\", Number); + var spread = prop(options, \\"spread\\", Number); + var startVelocity = prop(options, \\"startVelocity\\", Number); + var decay = prop(options, \\"decay\\", Number); + var gravity = prop(options, \\"gravity\\", Number); + var colors = prop(options, \\"colors\\"); + var ticks = prop(options, \\"ticks\\", Number); + var shapes = prop(options, \\"shapes\\"); + var scalar = prop(options, \\"scalar\\"); var origin = getOrigin(options); var temp = particleCount; var fettis = []; var startX = canvas.width * origin.x; var startY = canvas.height * origin.y; while (temp--) { - fettis.push( - randomPhysics({ - x: startX, - y: startY, - angle: angle, - spread: spread, - startVelocity: startVelocity, - color: colors[temp % colors.length], - shape: shapes[randomInt(0, shapes.length)], - ticks: ticks, - decay: decay, - gravity: gravity, - scalar: scalar - }) - ); + fettis.push(randomPhysics({ + x: startX, + y: startY, + angle, + spread, + startVelocity, + color: colors[temp % colors.length], + shape: shapes[randomInt(0, shapes.length)], + ticks, + decay, + gravity, + scalar + })); } - // if we have a previous canvas already animating, - // add to it if (animationObj) { return animationObj.addFettis(fettis); } - animationObj = animate(canvas, fettis, resizer, size , done); + animationObj = animate(canvas, fettis, resizer, size, done); return animationObj.promise; } function fire(options) { - var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean); - var zIndex = prop(options, 'zIndex', Number); + var disableForReducedMotion = globalDisableForReducedMotion || prop(options, \\"disableForReducedMotion\\", Boolean); + var zIndex = prop(options, \\"zIndex\\", Number); if (disableForReducedMotion && preferLessMotion) { - return promise(function (resolve) { + return promise(function(resolve) { resolve(); }); } if (isLibCanvas && animationObj) { - // use existing canvas from in-progress animation canvas = animationObj.canvas; } else if (isLibCanvas && !canvas) { - // create and initialize a new canvas canvas = getCanvas(zIndex); document.body.appendChild(canvas); } if (allowResize && !initialized) { - // initialize the size of a user-supplied canvas resizer(canvas); } var size = { @@ -1054,9 +983,8 @@ var module = {}; } function onResize() { if (worker) { - // TODO this really shouldn't be immediate, because it is expensive var obj = { - getBoundingClientRect: function () { + getBoundingClientRect: function() { if (!isLibCanvas) { return canvas.getBoundingClientRect(); } @@ -1071,14 +999,12 @@ var module = {}; }); return; } - // don't actually query the size here, since this - // can execute frequently and rapidly size.width = size.height = null; } function done() { animationObj = null; if (allowResize) { - global.removeEventListener('resize', onResize); + global.removeEventListener(\\"resize\\", onResize); } if (isLibCanvas && canvas) { document.body.removeChild(canvas); @@ -1087,14 +1013,14 @@ var module = {}; } } if (allowResize) { - global.addEventListener('resize', onResize, false); + global.addEventListener(\\"resize\\", onResize, false); } if (worker) { return worker.fire(options, size, done); } return fireLocal(options, size, done); } - fire.reset = function () { + fire.reset = function() { if (worker) { worker.reset(); } @@ -1104,18 +1030,17 @@ var module = {}; }; return fire; } - module.exports = confettiCannon(null, { useWorker: true, resize: true }); - module.exports.create = confettiCannon; -}((function () { - if (typeof window !== 'undefined') { + module2.exports = confettiCannon(null, {useWorker: true, resize: true}); + module2.exports.create = confettiCannon; +})(function() { + if (typeof window !== \\"undefined\\") { return window; } - if (typeof self !== 'undefined') { + if (typeof self !== \\"undefined\\") { return self; } return this; -})(), module, false)); -// end source content +}(), module, false); var __pika_web_default_export_for_treeshaking__ = module.exports; var create = module.exports.create; export default __pika_web_default_export_for_treeshaking__;" @@ -1131,7 +1056,6 @@ exports[`create-snowpack-app app-template-blank > build: _snowpack/pkg/import-ma exports[`create-snowpack-app app-template-blank > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "_snowpack/pkg/canvas-confetti.js", "_snowpack/pkg/import-map.json", "dist/index.js", @@ -1199,46 +1123,28 @@ exports[`create-snowpack-app app-template-blank > build: index.html 1`] = ` " `; -exports[`create-snowpack-app app-template-blank-typescript > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-blank-typescript > build: _snowpack/pkg/canvas-confetti.js 1`] = ` -"// canvas-confetti v1.3.2 built on 2020-11-12T12:53:54.473Z -var module = {}; -// source content -(function main(global, module, isWorker, workerSize) { - var canUseWorker = !!( - global.Worker && - global.Blob && - global.Promise && - global.OffscreenCanvas && - global.OffscreenCanvasRenderingContext2D && - global.HTMLCanvasElement && - global.HTMLCanvasElement.prototype.transferControlToOffscreen && - global.URL && - global.URL.createObjectURL); - function noop() {} - // create a promise if it exists, otherwise, just - // call the function directly +"var module = {}; +(function main(global, module2, isWorker, workerSize) { + var canUseWorker = !!(global.Worker && global.Blob && global.Promise && global.OffscreenCanvas && global.OffscreenCanvasRenderingContext2D && global.HTMLCanvasElement && global.HTMLCanvasElement.prototype.transferControlToOffscreen && global.URL && global.URL.createObjectURL); + function noop() { + } function promise(func) { - var ModulePromise = module.exports.Promise; + var ModulePromise = module2.exports.Promise; var Prom = ModulePromise !== void 0 ? ModulePromise : global.Promise; - if (typeof Prom === 'function') { + if (typeof Prom === \\"function\\") { return new Prom(func); } func(noop, noop); return null; } - var raf = (function () { - var TIME = Math.floor(1000 / 60); + var raf = function() { + var TIME = Math.floor(1e3 / 60); var frame, cancel; var frames = {}; var lastFrameTime = 0; - if (typeof requestAnimationFrame === 'function' && typeof cancelAnimationFrame === 'function') { - frame = function (cb) { + if (typeof requestAnimationFrame === \\"function\\" && typeof cancelAnimationFrame === \\"function\\") { + frame = function(cb) { var id = Math.random(); frames[id] = requestAnimationFrame(function onFrame(time) { if (lastFrameTime === time || lastFrameTime + TIME - 1 < time) { @@ -1251,103 +1157,102 @@ var module = {}; }); return id; }; - cancel = function (id) { + cancel = function(id) { if (frames[id]) { cancelAnimationFrame(frames[id]); } }; } else { - frame = function (cb) { + frame = function(cb) { return setTimeout(cb, TIME); }; - cancel = function (timer) { + cancel = function(timer) { return clearTimeout(timer); }; } - return { frame: frame, cancel: cancel }; - }()); - var getWorker = (function () { + return {frame, cancel}; + }(); + var getWorker = function() { var worker; var prom; var resolves = {}; - function decorate(worker) { + function decorate(worker2) { function execute(options, callback) { - worker.postMessage({ options: options || {}, callback: callback }); + worker2.postMessage({options: options || {}, callback}); } - worker.init = function initWorker(canvas) { + worker2.init = function initWorker(canvas) { var offscreen = canvas.transferControlToOffscreen(); - worker.postMessage({ canvas: offscreen }, [offscreen]); + worker2.postMessage({canvas: offscreen}, [offscreen]); }; - worker.fire = function fireWorker(options, size, done) { + worker2.fire = function fireWorker(options, size, done) { if (prom) { execute(options, null); return prom; } var id = Math.random().toString(36).slice(2); - prom = promise(function (resolve) { + prom = promise(function(resolve) { function workerDone(msg) { if (msg.data.callback !== id) { return; } delete resolves[id]; - worker.removeEventListener('message', workerDone); + worker2.removeEventListener(\\"message\\", workerDone); prom = null; done(); resolve(); } - worker.addEventListener('message', workerDone); + worker2.addEventListener(\\"message\\", workerDone); execute(options, id); - resolves[id] = workerDone.bind(null, { data: { callback: id }}); + resolves[id] = workerDone.bind(null, {data: {callback: id}}); }); return prom; }; - worker.reset = function resetWorker() { - worker.postMessage({ reset: true }); + worker2.reset = function resetWorker() { + worker2.postMessage({reset: true}); for (var id in resolves) { resolves[id](); delete resolves[id]; } }; } - return function () { + return function() { if (worker) { return worker; } if (!isWorker && canUseWorker) { var code = [ - 'var CONFETTI, SIZE = {}, module = {};', - '(' + main.toString() + ')(this, module, true, SIZE);', - 'onmessage = function(msg) {', - ' if (msg.data.options) {', - ' CONFETTI(msg.data.options).then(function () {', - ' if (msg.data.callback) {', - ' postMessage({ callback: msg.data.callback });', - ' }', - ' });', - ' } else if (msg.data.reset) {', - ' CONFETTI.reset();', - ' } else if (msg.data.resize) {', - ' SIZE.width = msg.data.resize.width;', - ' SIZE.height = msg.data.resize.height;', - ' } else if (msg.data.canvas) {', - ' SIZE.width = msg.data.canvas.width;', - ' SIZE.height = msg.data.canvas.height;', - ' CONFETTI = module.exports.create(msg.data.canvas);', - ' }', - '}', - ].join(''); + \\"var CONFETTI, SIZE = {}, module = {};\\", + \\"(\\" + main.toString() + \\")(this, module, true, SIZE);\\", + \\"onmessage = function(msg) {\\", + \\" if (msg.data.options) {\\", + \\" CONFETTI(msg.data.options).then(function () {\\", + \\" if (msg.data.callback) {\\", + \\" postMessage({ callback: msg.data.callback });\\", + \\" }\\", + \\" });\\", + \\" } else if (msg.data.reset) {\\", + \\" CONFETTI.reset();\\", + \\" } else if (msg.data.resize) {\\", + \\" SIZE.width = msg.data.resize.width;\\", + \\" SIZE.height = msg.data.resize.height;\\", + \\" } else if (msg.data.canvas) {\\", + \\" SIZE.width = msg.data.canvas.width;\\", + \\" SIZE.height = msg.data.canvas.height;\\", + \\" CONFETTI = module.exports.create(msg.data.canvas);\\", + \\" }\\", + \\"}\\" + ].join(\\"\\"); try { worker = new Worker(URL.createObjectURL(new Blob([code]))); } catch (e) { - // eslint-disable-next-line no-console - typeof console !== undefined && typeof console.warn === 'function' ? console.warn('🎊 Could not load worker', e) : null; + typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"\\\\u{1F38A} Could not load worker\\", e) : null; return null; } decorate(worker); } return worker; }; - })(); + }(); var defaults = { particleCount: 50, angle: 90, @@ -1358,18 +1263,17 @@ var module = {}; ticks: 200, x: 0.5, y: 0.5, - shapes: ['square', 'circle'], + shapes: [\\"square\\", \\"circle\\"], zIndex: 100, colors: [ - '#26ccff', - '#a25afd', - '#ff5e7e', - '#88ff5a', - '#fcff42', - '#ffa62d', - '#ff36ff' + \\"#26ccff\\", + \\"#a25afd\\", + \\"#ff5e7e\\", + \\"#88ff5a\\", + \\"#fcff42\\", + \\"#ffa62d\\", + \\"#ff36ff\\" ], - // probably should be true, but back-compat disableForReducedMotion: false, scalar: 1 }; @@ -1377,39 +1281,35 @@ var module = {}; return transform ? transform(val) : val; } function isOk(val) { - return !(val === null || val === undefined); + return !(val === null || val === void 0); } function prop(options, name, transform) { - return convert( - options && isOk(options[name]) ? options[name] : defaults[name], - transform - ); + return convert(options && isOk(options[name]) ? options[name] : defaults[name], transform); } - function onlyPositiveInt(number){ + function onlyPositiveInt(number) { return number < 0 ? 0 : Math.floor(number); } function randomInt(min, max) { - // [min, max) return Math.floor(Math.random() * (max - min)) + min; } function toDecimal(str) { return parseInt(str, 16); } function hexToRgb(str) { - var val = String(str).replace(/[^0-9a-f]/gi, ''); + var val = String(str).replace(/[^0-9a-f]/gi, \\"\\"); if (val.length < 6) { - val = val[0]+val[0]+val[1]+val[1]+val[2]+val[2]; + val = val[0] + val[0] + val[1] + val[1] + val[2] + val[2]; } return { - r: toDecimal(val.substring(0,2)), - g: toDecimal(val.substring(2,4)), - b: toDecimal(val.substring(4,6)) + r: toDecimal(val.substring(0, 2)), + g: toDecimal(val.substring(2, 4)), + b: toDecimal(val.substring(4, 6)) }; } function getOrigin(options) { - var origin = prop(options, 'origin', Object); - origin.x = prop(origin, 'x', Number); - origin.y = prop(origin, 'y', Number); + var origin = prop(options, \\"origin\\", Object); + origin.x = prop(origin, \\"x\\", Number); + origin.y = prop(origin, \\"y\\", Number); return origin; } function setCanvasWindowSize(canvas) { @@ -1422,11 +1322,11 @@ var module = {}; canvas.height = rect.height; } function getCanvas(zIndex) { - var canvas = document.createElement('canvas'); - canvas.style.position = 'fixed'; - canvas.style.top = '0px'; - canvas.style.left = '0px'; - canvas.style.pointerEvents = 'none'; + var canvas = document.createElement(\\"canvas\\"); + canvas.style.position = \\"fixed\\"; + canvas.style.top = \\"0px\\"; + canvas.style.left = \\"0px\\"; + canvas.style.pointerEvents = \\"none\\"; canvas.style.zIndex = zIndex; return canvas; } @@ -1445,8 +1345,8 @@ var module = {}; x: opts.x, y: opts.y, wobble: Math.random() * 10, - velocity: (opts.startVelocity * 0.5) + (Math.random() * opts.startVelocity), - angle2D: -radAngle + ((0.5 * radSpread) - (Math.random() * radSpread)), + velocity: opts.startVelocity * 0.5 + Math.random() * opts.startVelocity, + angle2D: -radAngle + (0.5 * radSpread - Math.random() * radSpread), tiltAngle: Math.random() * Math.PI, color: hexToRgb(opts.color), shape: opts.shape, @@ -1472,19 +1372,17 @@ var module = {}; fetti.tiltSin = Math.sin(fetti.tiltAngle); fetti.tiltCos = Math.cos(fetti.tiltAngle); fetti.random = Math.random() + 5; - fetti.wobbleX = fetti.x + ((10 * fetti.scalar) * Math.cos(fetti.wobble)); - fetti.wobbleY = fetti.y + ((10 * fetti.scalar) * Math.sin(fetti.wobble)); - var progress = (fetti.tick++) / fetti.totalTicks; - var x1 = fetti.x + (fetti.random * fetti.tiltCos); - var y1 = fetti.y + (fetti.random * fetti.tiltSin); - var x2 = fetti.wobbleX + (fetti.random * fetti.tiltCos); - var y2 = fetti.wobbleY + (fetti.random * fetti.tiltSin); - context.fillStyle = 'rgba(' + fetti.color.r + ', ' + fetti.color.g + ', ' + fetti.color.b + ', ' + (1 - progress) + ')'; + fetti.wobbleX = fetti.x + 10 * fetti.scalar * Math.cos(fetti.wobble); + fetti.wobbleY = fetti.y + 10 * fetti.scalar * Math.sin(fetti.wobble); + var progress = fetti.tick++ / fetti.totalTicks; + var x1 = fetti.x + fetti.random * fetti.tiltCos; + var y1 = fetti.y + fetti.random * fetti.tiltSin; + var x2 = fetti.wobbleX + fetti.random * fetti.tiltCos; + var y2 = fetti.wobbleY + fetti.random * fetti.tiltSin; + context.fillStyle = \\"rgba(\\" + fetti.color.r + \\", \\" + fetti.color.g + \\", \\" + fetti.color.b + \\", \\" + (1 - progress) + \\")\\"; context.beginPath(); - if (fetti.shape === 'circle') { - context.ellipse ? - context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : - ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); + if (fetti.shape === \\"circle\\") { + context.ellipse ? context.ellipse(fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI) : ellipse(context, fetti.x, fetti.y, Math.abs(x2 - x1) * fetti.ovalScalar, Math.abs(y2 - y1) * fetti.ovalScalar, Math.PI / 10 * fetti.wobble, 0, 2 * Math.PI); } else { context.moveTo(Math.floor(fetti.x), Math.floor(fetti.y)); context.lineTo(Math.floor(fetti.wobbleX), Math.floor(y1)); @@ -1497,10 +1395,10 @@ var module = {}; } function animate(canvas, fettis, resizer, size, done) { var animatingFettis = fettis.slice(); - var context = canvas.getContext('2d'); + var context = canvas.getContext(\\"2d\\"); var animationFrame; var destroy; - var prom = promise(function (resolve) { + var prom = promise(function(resolve) { function onDone() { animationFrame = destroy = null; context.clearRect(0, 0, size.width, size.height); @@ -1518,7 +1416,7 @@ var module = {}; size.height = canvas.height; } context.clearRect(0, 0, size.width, size.height); - animatingFettis = animatingFettis.filter(function (fetti) { + animatingFettis = animatingFettis.filter(function(fetti) { return updateFetti(context, fetti); }); if (animatingFettis.length) { @@ -1531,13 +1429,13 @@ var module = {}; destroy = onDone; }); return { - addFettis: function (fettis) { - animatingFettis = animatingFettis.concat(fettis); + addFettis: function(fettis2) { + animatingFettis = animatingFettis.concat(fettis2); return prom; }, - canvas: canvas, + canvas, promise: prom, - reset: function () { + reset: function() { if (animationFrame) { raf.cancel(animationFrame); } @@ -1549,73 +1447,66 @@ var module = {}; } function confettiCannon(canvas, globalOpts) { var isLibCanvas = !canvas; - var allowResize = !!prop(globalOpts || {}, 'resize'); - var globalDisableForReducedMotion = prop(globalOpts, 'disableForReducedMotion', Boolean); - var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, 'useWorker'); + var allowResize = !!prop(globalOpts || {}, \\"resize\\"); + var globalDisableForReducedMotion = prop(globalOpts, \\"disableForReducedMotion\\", Boolean); + var shouldUseWorker = canUseWorker && !!prop(globalOpts || {}, \\"useWorker\\"); var worker = shouldUseWorker ? getWorker() : null; var resizer = isLibCanvas ? setCanvasWindowSize : setCanvasRectSize; - var initialized = (canvas && worker) ? !!canvas.__confetti_initialized : false; - var preferLessMotion = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion)').matches; + var initialized = canvas && worker ? !!canvas.__confetti_initialized : false; + var preferLessMotion = typeof matchMedia === \\"function\\" && matchMedia(\\"(prefers-reduced-motion)\\").matches; var animationObj; function fireLocal(options, size, done) { - var particleCount = prop(options, 'particleCount', onlyPositiveInt); - var angle = prop(options, 'angle', Number); - var spread = prop(options, 'spread', Number); - var startVelocity = prop(options, 'startVelocity', Number); - var decay = prop(options, 'decay', Number); - var gravity = prop(options, 'gravity', Number); - var colors = prop(options, 'colors'); - var ticks = prop(options, 'ticks', Number); - var shapes = prop(options, 'shapes'); - var scalar = prop(options, 'scalar'); + var particleCount = prop(options, \\"particleCount\\", onlyPositiveInt); + var angle = prop(options, \\"angle\\", Number); + var spread = prop(options, \\"spread\\", Number); + var startVelocity = prop(options, \\"startVelocity\\", Number); + var decay = prop(options, \\"decay\\", Number); + var gravity = prop(options, \\"gravity\\", Number); + var colors = prop(options, \\"colors\\"); + var ticks = prop(options, \\"ticks\\", Number); + var shapes = prop(options, \\"shapes\\"); + var scalar = prop(options, \\"scalar\\"); var origin = getOrigin(options); var temp = particleCount; var fettis = []; var startX = canvas.width * origin.x; var startY = canvas.height * origin.y; while (temp--) { - fettis.push( - randomPhysics({ - x: startX, - y: startY, - angle: angle, - spread: spread, - startVelocity: startVelocity, - color: colors[temp % colors.length], - shape: shapes[randomInt(0, shapes.length)], - ticks: ticks, - decay: decay, - gravity: gravity, - scalar: scalar - }) - ); + fettis.push(randomPhysics({ + x: startX, + y: startY, + angle, + spread, + startVelocity, + color: colors[temp % colors.length], + shape: shapes[randomInt(0, shapes.length)], + ticks, + decay, + gravity, + scalar + })); } - // if we have a previous canvas already animating, - // add to it if (animationObj) { return animationObj.addFettis(fettis); } - animationObj = animate(canvas, fettis, resizer, size , done); + animationObj = animate(canvas, fettis, resizer, size, done); return animationObj.promise; } function fire(options) { - var disableForReducedMotion = globalDisableForReducedMotion || prop(options, 'disableForReducedMotion', Boolean); - var zIndex = prop(options, 'zIndex', Number); + var disableForReducedMotion = globalDisableForReducedMotion || prop(options, \\"disableForReducedMotion\\", Boolean); + var zIndex = prop(options, \\"zIndex\\", Number); if (disableForReducedMotion && preferLessMotion) { - return promise(function (resolve) { + return promise(function(resolve) { resolve(); }); } if (isLibCanvas && animationObj) { - // use existing canvas from in-progress animation canvas = animationObj.canvas; } else if (isLibCanvas && !canvas) { - // create and initialize a new canvas canvas = getCanvas(zIndex); document.body.appendChild(canvas); } if (allowResize && !initialized) { - // initialize the size of a user-supplied canvas resizer(canvas); } var size = { @@ -1631,9 +1522,8 @@ var module = {}; } function onResize() { if (worker) { - // TODO this really shouldn't be immediate, because it is expensive var obj = { - getBoundingClientRect: function () { + getBoundingClientRect: function() { if (!isLibCanvas) { return canvas.getBoundingClientRect(); } @@ -1648,14 +1538,12 @@ var module = {}; }); return; } - // don't actually query the size here, since this - // can execute frequently and rapidly size.width = size.height = null; } function done() { animationObj = null; if (allowResize) { - global.removeEventListener('resize', onResize); + global.removeEventListener(\\"resize\\", onResize); } if (isLibCanvas && canvas) { document.body.removeChild(canvas); @@ -1664,14 +1552,14 @@ var module = {}; } } if (allowResize) { - global.addEventListener('resize', onResize, false); + global.addEventListener(\\"resize\\", onResize, false); } if (worker) { return worker.fire(options, size, done); } return fireLocal(options, size, done); } - fire.reset = function () { + fire.reset = function() { if (worker) { worker.reset(); } @@ -1681,18 +1569,17 @@ var module = {}; }; return fire; } - module.exports = confettiCannon(null, { useWorker: true, resize: true }); - module.exports.create = confettiCannon; -}((function () { - if (typeof window !== 'undefined') { + module2.exports = confettiCannon(null, {useWorker: true, resize: true}); + module2.exports.create = confettiCannon; +})(function() { + if (typeof window !== \\"undefined\\") { return window; } - if (typeof self !== 'undefined') { + if (typeof self !== \\"undefined\\") { return self; } return this; -})(), module, false)); -// end source content +}(), module, false); var __pika_web_default_export_for_treeshaking__ = module.exports; var create = module.exports.create; export default __pika_web_default_export_for_treeshaking__;" @@ -1708,7 +1595,6 @@ exports[`create-snowpack-app app-template-blank-typescript > build: _snowpack/pk exports[`create-snowpack-app app-template-blank-typescript > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "_snowpack/pkg/canvas-confetti.js", "_snowpack/pkg/import-map.json", "dist/index.js", @@ -1772,12 +1658,6 @@ exports[`create-snowpack-app app-template-blank-typescript > build: index.html 1 " `; -exports[`create-snowpack-app app-template-lit-element > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-lit-element > build: _snowpack/pkg/import-map.json 1`] = ` "{ \\"imports\\": { @@ -4422,7 +4302,6 @@ export { LitElement, css, customElement, html, property };" exports[`create-snowpack-app app-template-lit-element > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/lit-element.js", "dist/app-root.js", @@ -4554,12 +4433,6 @@ exports[`create-snowpack-app app-template-lit-element > build: index.html 1`] = " `; -exports[`create-snowpack-app app-template-lit-element-typescript > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-lit-element-typescript > build: _snowpack/pkg/import-map.json 1`] = ` "{ \\"imports\\": { @@ -7204,7 +7077,6 @@ export { LitElement, css, customElement, html, property };" exports[`create-snowpack-app app-template-lit-element-typescript > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/lit-element.js", "dist/app-root.js", @@ -7336,15 +7208,8 @@ exports[`create-snowpack-app app-template-lit-element-typescript > build: index. " `; -exports[`create-snowpack-app app-template-minimal > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-minimal > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "index.css", "index.html", "index.js", @@ -7432,12 +7297,6 @@ module.exports = { };" `; -exports[`create-snowpack-app app-template-preact > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-preact > build: _snowpack/pkg/import-map.json 1`] = ` "{ \\"imports\\": { @@ -7463,7 +7322,6 @@ export { y as useEffect, l as useState };" exports[`create-snowpack-app app-template-preact > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/preact.js", "_snowpack/pkg/preact/devtools.js", @@ -7627,12 +7485,6 @@ exports[`create-snowpack-app app-template-preact > build: index.html 1`] = ` " `; -exports[`create-snowpack-app app-template-preact-typescript > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; -export const SSR = false;" -`; - exports[`create-snowpack-app app-template-preact-typescript > build: _snowpack/pkg/import-map.json 1`] = ` "{ \\"imports\\": { @@ -7658,7 +7510,6 @@ export { y as useEffect, l as useState };" exports[`create-snowpack-app app-template-preact-typescript > build: allFiles 1`] = ` Array [ - "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/preact.js", "_snowpack/pkg/preact/devtools.js", @@ -7826,8 +7677,8 @@ exports[`create-snowpack-app app-template-preact-typescript > build: index.html `; exports[`create-snowpack-app app-template-react > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; +"export const MODE = \\"development\\"; +export const NODE_ENV = \\"development\\"; export const SSR = false;" `; @@ -8368,8 +8219,8 @@ exports[`create-snowpack-app app-template-react > build: index.html 1`] = ` `; exports[`create-snowpack-app app-template-react-typescript > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; +"export const MODE = \\"development\\"; +export const NODE_ENV = \\"development\\"; export const SSR = false;" `; @@ -8910,8 +8761,8 @@ exports[`create-snowpack-app app-template-react-typescript > build: index.html 1 `; exports[`create-snowpack-app app-template-svelte > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; +"export const MODE = \\"development\\"; +export const NODE_ENV = \\"development\\"; export const SSR = false;" `; @@ -8934,7 +8785,6 @@ Array [ "_snowpack/pkg/import-map.json", "_snowpack/pkg/svelte.js", "_snowpack/pkg/svelte/internal.js", - "dist/App.svelte.css", "dist/App.svelte.css.proxy.js", "dist/App.svelte.js", "dist/index.js", @@ -8945,8 +8795,6 @@ Array [ ] `; -exports[`create-snowpack-app app-template-svelte > build: dist/App.svelte.css 1`] = `"body{margin:0;font-family:Arial, Helvetica, sans-serif}.App.svelte-rq4gzr.svelte-rq4gzr{text-align:center}.App.svelte-rq4gzr code.svelte-rq4gzr{background:#0002;padding:4px 8px;border-radius:4px}.App.svelte-rq4gzr p.svelte-rq4gzr{margin:0.4rem}.App-header.svelte-rq4gzr.svelte-rq4gzr{background-color:#f9f6f6;color:#333;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin)}.App-link.svelte-rq4gzr.svelte-rq4gzr{color:#ff3e00}.App-logo.svelte-rq4gzr.svelte-rq4gzr{height:36vmin;pointer-events:none;margin-bottom:3rem;animation:svelte-rq4gzr-App-logo-pulse infinite 1.6s ease-in-out alternate}@keyframes svelte-rq4gzr-App-logo-pulse{from{transform:scale(1)}to{transform:scale(1.06)}}"`; - exports[`create-snowpack-app app-template-svelte > build: dist/App.svelte.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -9106,8 +8954,8 @@ exports[`create-snowpack-app app-template-svelte > build: index.html 1`] = ` `; exports[`create-snowpack-app app-template-svelte-typescript > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; +"export const MODE = \\"development\\"; +export const NODE_ENV = \\"development\\"; export const SSR = false;" `; @@ -9130,7 +8978,6 @@ Array [ "_snowpack/pkg/import-map.json", "_snowpack/pkg/svelte.js", "_snowpack/pkg/svelte/internal.js", - "dist/App.svelte.css", "dist/App.svelte.css.proxy.js", "dist/App.svelte.js", "dist/index.js", @@ -9141,8 +8988,6 @@ Array [ ] `; -exports[`create-snowpack-app app-template-svelte-typescript > build: dist/App.svelte.css 1`] = `"body{margin:0;font-family:Arial, Helvetica, sans-serif}.App.svelte-1sqyd3v.svelte-1sqyd3v{text-align:center}.App.svelte-1sqyd3v code.svelte-1sqyd3v{background:#0002;padding:4px 8px;border-radius:4px}.App.svelte-1sqyd3v p.svelte-1sqyd3v{margin:0.4rem}.App-header.svelte-1sqyd3v.svelte-1sqyd3v{background-color:#f9f6f6;color:#333;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin)}.App-link.svelte-1sqyd3v.svelte-1sqyd3v{color:#ff3e00}.App-logo.svelte-1sqyd3v.svelte-1sqyd3v{height:36vmin;pointer-events:none;margin-bottom:3rem;animation:svelte-1sqyd3v-App-logo-spin infinite 1.6s ease-in-out alternate}@keyframes svelte-1sqyd3v-App-logo-spin{from{transform:scale(1)}to{transform:scale(1.06)}}"`; - exports[`create-snowpack-app app-template-svelte-typescript > build: dist/App.svelte.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -9300,8 +9145,8 @@ exports[`create-snowpack-app app-template-svelte-typescript > build: index.html `; exports[`create-snowpack-app app-template-vue > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; +"export const MODE = \\"development\\"; +export const NODE_ENV = \\"development\\"; export const SSR = false;" `; @@ -14328,7 +14173,6 @@ Array [ "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/vue.js", - "dist/App.vue.css", "dist/App.vue.css.proxy.js", "dist/App.vue.js", "dist/index.js", @@ -14340,40 +14184,6 @@ Array [ ] `; -exports[`create-snowpack-app app-template-vue > build: dist/App.vue.css 1`] = ` -" -.App { - text-align: center; -} -.App-header { - background-color: #f9f6f6; - color: #32485f; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); -} -.App-link { - color: #00c185; -} -.App-logo { - height: 40vmin; - pointer-events: none; - margin-bottom: 1rem; - animation: App-logo-spin infinite 1.6s ease-in-out alternate; -} -@keyframes App-logo-spin { -from { - transform: scale(1); -} -to { - transform: scale(1.06); -} -}" -`; - exports[`create-snowpack-app app-template-vue > build: dist/App.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -14471,8 +14281,8 @@ exports[`create-snowpack-app app-template-vue > build: index.html 1`] = ` `; exports[`create-snowpack-app app-template-vue-typescript > build: _snowpack/env.js 1`] = ` -"export const MODE = \\"production\\"; -export const NODE_ENV = \\"production\\"; +"export const MODE = \\"development\\"; +export const NODE_ENV = \\"development\\"; export const SSR = false;" `; @@ -19622,19 +19432,16 @@ Array [ "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/vue.js", - "dist/App.vue.css", "dist/App.vue.css.proxy.js", "dist/App.vue.js", "dist/components/Bar.js", "dist/components/Bar.module.css", "dist/components/Bar.module.css.proxy.js", - "dist/components/BarJsx.vue.css", "dist/components/BarJsx.vue.css.proxy.js", "dist/components/BarJsx.vue.js", "dist/components/Foo.js", "dist/components/Foo.module.css", "dist/components/Foo.module.css.proxy.js", - "dist/components/FooTsx.vue.css", "dist/components/FooTsx.vue.css.proxy.js", "dist/components/FooTsx.vue.js", "dist/index.js", @@ -19646,47 +19453,6 @@ Array [ ] `; -exports[`create-snowpack-app app-template-vue-typescript > build: dist/App.vue.css 1`] = ` -" -.App { - text-align: center; -} -.App-header { - background-color: #f9f6f6; - color: #32485f; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); -} -.App-link { - color: #00c185; -} -.App-logo { - height: 40vmin; - pointer-events: none; - margin-bottom: 1rem; - animation: App-logo-spin infinite 1.6s ease-in-out alternate; -} -.App-tsx { - display: flex; -} -.App-tsx > div { - margin-left: 30px; - font-size: 16px; -} -@keyframes App-logo-spin { -from { - transform: scale(1); -} -to { - transform: scale(1.06); -} -}" -`; - exports[`create-snowpack-app app-template-vue-typescript > build: dist/App.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -19811,13 +19577,6 @@ if (typeof document !== 'undefined') { }" `; -exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/BarJsx.vue.css 1`] = ` -" -.bar-jsx-vue { - color: red; -}" -`; - exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/BarJsx.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -19901,13 +19660,6 @@ if (typeof document !== 'undefined') { }" `; -exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/FooTsx.vue.css 1`] = ` -" -.foo-tsx-vue { - color: green; -}" -`; - exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/FooTsx.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { diff --git a/test/test-utils.js b/test/test-utils.js index ed877c6973..e65c1d8ae0 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -21,6 +21,7 @@ const UTF8_FRIENDLY_EXTS = [ /** setup for /tests/build/* */ function setupBuildTest(cwd) { + console.log(cwd); return execSync('yarn testbuild', {cwd}); } exports.setupBuildTest = setupBuildTest; diff --git a/yarn.lock b/yarn.lock index b0ed4e680a..545def1de0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7334,14 +7334,14 @@ fs-extra@^8.1.0: universalify "^0.1.0" fs-extra@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" - universalify "^1.0.0" + universalify "^2.0.0" fs-minipass@^1.2.5: version "1.2.7" @@ -13803,11 +13803,21 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +svelte-awesome@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/svelte-awesome/-/svelte-awesome-2.3.0.tgz#03735c1ba1940e931c41271a5ebf72e2ae9e292c" + integrity sha512-xXU3XYJmr5PK9e3pCpW6feU/tNpAhERWDrgDxkC0DU6LNum02zzc00I17bmUWTSRdKLf+IFbA5hWvCm1V8g+/A== + dependencies: + svelte "^3.15.0" + svelte-hmr@^0.12.1: version "0.12.2" resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.12.2.tgz#689df7681f0461e7a2539b3fad1336ee1da84751" integrity sha512-86fpj4Wjno7OREJsGxQwpVBtB3kmiKWwpOlvdZmfBZYankpL38lcVtAi1zvQXXcN4g8pRXUG68khwp6dYRwpYg== +"svelte-package-a@file:./test/build/plugin-build-svelte/packages/svelte-package-a": + version "1.2.3" + svelte-preprocess-filter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/svelte-preprocess-filter/-/svelte-preprocess-filter-1.0.0.tgz#21f3cee27c0f2232bcd671b183352fb1fdd3fdcb" @@ -13835,6 +13845,11 @@ svelte-routing@^1.4.0: resolved "https://registry.yarnpkg.com/svelte-routing/-/svelte-routing-1.5.0.tgz#a518cf67d8c09dd30a04cf6f9473ce5612fd4c8b" integrity sha512-4ftcSO2x5kzCUWQKm9Td6/C+t7lRjMEo72utRO0liS/aWZuRwAXOBl3y+hWZw8tV+DTGElqaAAyi44AuWXcVBg== +svelte@^3.15.0: + version "3.32.3" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.32.3.tgz#db0c50c65573ecffe4e2f4924e4862d8f9feda74" + integrity sha512-5etu/wDwtewhnYO/631KKTjSmFrKohFLWNm1sWErVHXqGZ8eJLqrW0qivDSyYTcN8GbUqsR4LkIhftNFsjNehg== + svelte@^3.18.2, svelte@^3.21.0, svelte@^3.24.0: version "3.31.2" resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.31.2.tgz#d2ddf6cacbb95e4cc3796207510b660a25586324" @@ -14497,11 +14512,6 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -universalify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" - integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== - universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" From c8d50623f2642516a06dd832ad78f3b018a9b182 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Tue, 23 Feb 2021 17:31:19 -0800 Subject: [PATCH 02/40] fix import proxies --- snowpack/src/build/file-builder.ts | 3 ++- snowpack/src/build/file-urls.ts | 6 +++++- snowpack/src/commands/build.ts | 3 ++- test/build/base-url/base-url.test.js | 6 ++++++ test/build/base-url/src/index.js | 5 ++++- test/build/base-url/src/logo.png | Bin 0 -> 75407 bytes 6 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 test/build/base-url/src/logo.png diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts index 6fae8b0ca1..6f2ce7b799 100644 --- a/snowpack/src/build/file-builder.ts +++ b/snowpack/src/build/file-builder.ts @@ -319,8 +319,9 @@ export class FileBuilder { return this.resolvedOutput[type].map; } - async getProxy(url: string, type: string) { + async getProxy(_url: string, type: string) { const code = this.resolvedOutput[type].code; + const url = path.posix.join(this.isDev ? '/' : this.config.buildOptions.baseUrl, _url); return await wrapImportProxy({url, code, hmr: this.isHMR, config: this.config}); } diff --git a/snowpack/src/build/file-urls.ts b/snowpack/src/build/file-urls.ts index 6194e89b5d..a02fd47ba2 100644 --- a/snowpack/src/build/file-urls.ts +++ b/snowpack/src/build/file-urls.ts @@ -74,7 +74,11 @@ export function getUrlsForFile(fileLoc: string, config: SnowpackConfig): string[ if (!mountEntryResult) { const builtEntrypointUrls = getBuiltFileUrls(fileLoc, config); return builtEntrypointUrls.map((u) => - path.posix.join(config.buildOptions.metaUrlPath, 'link', slash(path.relative(config.root, u))), + path.posix.join( + config.buildOptions.metaUrlPath, + 'link', + slash(path.relative(config.root, u)), + ), ); } const [mountKey, mountEntry] = mountEntryResult; diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index 3a29157ce4..0a30c1336d 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -181,6 +181,7 @@ export async function build(commandOptions: CommandOptions): Promise { expect($('script').attr('src').startsWith('/static/')).toBe(true); }); + it('import proxies works', () => { + expect(files['/_dist_/logo.png.proxy.js']).toEqual( + expect.stringContaining(`export default "/static/_dist_/logo.png";`), + ); + }); + it('import.meta.env works', () => { // env is present in index.js expect(files['/index.js']).toEqual( diff --git a/test/build/base-url/src/index.js b/test/build/base-url/src/index.js index ae056e9d37..1424590a8a 100644 --- a/test/build/base-url/src/index.js +++ b/test/build/base-url/src/index.js @@ -1,10 +1,13 @@ // src/index.js - Test import URLs from the "/_dist_/" directory import {flatten} from 'array-flatten'; +import logo from './logo.png'; export default function doNothing() { // I do nothing 🎉 } // Triggers a snowpack meta import URL -console.log(import.meta.env) \ No newline at end of file +console.log(import.meta.env) +// Test import proxies +console.log(logo) \ No newline at end of file diff --git a/test/build/base-url/src/logo.png b/test/build/base-url/src/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d23fe090eee0feed0e18cb18dcebe4f469730ce7 GIT binary patch literal 75407 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 jDmG}ug_DbWe}<7;;|m Date: Tue, 23 Feb 2021 17:42:07 -0800 Subject: [PATCH 03/40] fix bad default import scanner --- snowpack/src/scan-imports.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snowpack/src/scan-imports.ts b/snowpack/src/scan-imports.ts index f347814f0a..2f7453a9b4 100644 --- a/snowpack/src/scan-imports.ts +++ b/snowpack/src/scan-imports.ts @@ -29,7 +29,7 @@ const ESM_IMPORT_REGEX = /import(?:["'\s]*([\w*${}\n\r\t, ]+)\s*from\s*)?\s*["'] const ESM_DYNAMIC_IMPORT_REGEX = /(? Date: Tue, 23 Feb 2021 21:57:58 -0800 Subject: [PATCH 04/40] add tslib polyfill --- esinstall/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index de8f342f02..27291145af 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -105,11 +105,15 @@ function generateEnvObject(userEnv: EnvVarReplacements): Object { }; } -function generateEnvReplacements(env: Object): {[key: string]: string} { +function generateReplacements(env: Object): {[key: string]: string} { return Object.keys(env).reduce((acc, key) => { acc[`process.env.${key}`] = JSON.stringify(env[key]); return acc; - }, {}); + }, { + // Other find & replacements: + // tslib: fights with Rollup's namespace/default handling, so just remove it. + 'return (mod && mod.__esModule) ? mod : { "default": mod };': 'return mod;' + }); } interface InstallOptions { @@ -343,7 +347,7 @@ ${colors.dim( namedExports: true, }), rollupPluginCss(), - rollupPluginReplace(generateEnvReplacements(env)), + rollupPluginReplace(generateReplacements(env)), rollupPluginCommonjs({ extensions: ['.js', '.cjs'], esmExternals: (id) => externalEsm.some((packageName) => isImportOfPackage(id, packageName)), From 7b1bc00bbbd95f9256bb0c6b666d5e2e688b912e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=92=9E=E7=A5=A8=E6=AC=A0=E5=A4=9A?= <573746388@qq.com> Date: Thu, 25 Feb 2021 01:41:48 +0800 Subject: [PATCH 05/40] Offical wip new pkg (#2726) * add external in prepare * add test add perpare test --- snowpack/src/sources/local.ts | 9 ++++++- .../config-external-package.test.js | 20 ++++++++++++++ .../prepare-external-package/package.json | 27 +++++++++++++++++++ .../prepare-external-package/src/index.js | 3 +++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 test/build/prepare-external-package/config-external-package.test.js create mode 100644 test/build/prepare-external-package/package.json create mode 100644 test/build/prepare-external-package/src/index.js diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index dde9fca89c..1458889d72 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -195,7 +195,14 @@ export default { return; } await Promise.all( - [...new Set(installTargets.map((t) => t.specifier))].map((spec) => { + [ + ...new Set( + installTargets + .map((t) => t.specifier) + // external packages need not prepare + .filter((t) => !config.packageOptions?.external.includes(t)), + ), + ].map((spec) => { return this.resolvePackageImport(path.join(config.root, 'package.json'), spec, config); }), ); diff --git a/test/build/prepare-external-package/config-external-package.test.js b/test/build/prepare-external-package/config-external-package.test.js new file mode 100644 index 0000000000..96d38cdbdb --- /dev/null +++ b/test/build/prepare-external-package/config-external-package.test.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const path = require('path'); +const {setupBuildTest} = require('../../test-utils'); + +describe('prepare: packageOptions.external', () => { + beforeAll(() => { + setupBuildTest(__dirname); + }); + it('prepare external package', () => { + expect( + fs.existsSync(path.join(__dirname, 'node_modules/.cache/snowpack/test/array-flatten@3.0.0')), + ).toEqual(true); + expect(fs.existsSync(path.join(__dirname, 'node_modules/.cache/snowpack/test/fs'))).toEqual( + false, + ); + expect( + fs.existsSync(path.join(__dirname, 'node_modules/.cache/snowpack/test/vue/types')), + ).toEqual(false); + }); +}); diff --git a/test/build/prepare-external-package/package.json b/test/build/prepare-external-package/package.json new file mode 100644 index 0000000000..aed36cf1e3 --- /dev/null +++ b/test/build/prepare-external-package/package.json @@ -0,0 +1,27 @@ +{ + "private": true, + "version": "1.0.0", + "name": "@snowpack/test-prepare-external-package", + "description": "Test for packageOptions.external as it applies to prepare.", + "scripts": { + "testbuild": "snowpack prepare" + }, + "snowpack": { + "mount": { + "./src": "/_dist_" + }, + "packageOptions": { + "external": [ + "fs", + "vue/types" + ], + "source": "local" + } + }, + "devDependencies": { + "snowpack": "link:../../../snowpack" + }, + "dependencies": { + "array-flatten": "^3.0.0" + } +} diff --git a/test/build/prepare-external-package/src/index.js b/test/build/prepare-external-package/src/index.js new file mode 100644 index 0000000000..e139e54352 --- /dev/null +++ b/test/build/prepare-external-package/src/index.js @@ -0,0 +1,3 @@ +import 'fs'; +import 'array-flatten'; +import 'vue/types'; From 8f26514917b5083dac94c0867f8a20db1f330e17 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 09:40:13 -0800 Subject: [PATCH 06/40] clean up snowpack symlink warning --- snowpack/src/sources/local.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 1458889d72..44f0a268a6 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -272,7 +272,7 @@ export default { ...Object.keys(packageManifest.dependencies || {}), ...Object.keys(packageManifest.devDependencies || {}), ...Object.keys(packageManifest.peerDependencies || {}), - ]; + ].filter(ext => ext !== _packageName); const installOptions: InstallOptions = { dest: installDest, @@ -318,11 +318,11 @@ export default { logger.debug(colors.yellow(`⦿ ${spec} (ssr) DONE`)); } if (isSymlink) { - logger.info( + logger.warn( colors.bold(`Locally linked package detected outside of project root.\n`) + - `Locally linked/symlinked packages are treated as static by default, and will not be\n` + - `rebuilt until its "package.json" version changes. To enable local updates for this\n` + - `package, set your project root to match your monorepo/workspace root directory.`, + `If you are working in a workspace/monorepo, set your snowpack.config.js "root"\n` + + `to the workspace root to take advantage of fast HMR updates for linked packages.\n` + + `Otherwise, this package won't be rebuilt until its package.json "version" changes.`, ); } const dependencyFileLoc = path.join(installDest, newImportMap.imports[spec]); From e4f931a36a9c33ceac1e10686a716b2c22f565cf Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 09:47:38 -0800 Subject: [PATCH 07/40] add support for imports of external packages --- esinstall/src/index.ts | 19 +++++++++++-------- snowpack/src/scan-imports.ts | 8 +++++++- snowpack/src/sources/local.ts | 15 ++++----------- snowpack/src/util.ts | 4 ++++ 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 27291145af..0e044e8393 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -106,14 +106,17 @@ function generateEnvObject(userEnv: EnvVarReplacements): Object { } function generateReplacements(env: Object): {[key: string]: string} { - return Object.keys(env).reduce((acc, key) => { - acc[`process.env.${key}`] = JSON.stringify(env[key]); - return acc; - }, { - // Other find & replacements: - // tslib: fights with Rollup's namespace/default handling, so just remove it. - 'return (mod && mod.__esModule) ? mod : { "default": mod };': 'return mod;' - }); + return Object.keys(env).reduce( + (acc, key) => { + acc[`process.env.${key}`] = JSON.stringify(env[key]); + return acc; + }, + { + // Other find & replacements: + // tslib: fights with Rollup's namespace/default handling, so just remove it. + 'return (mod && mod.__esModule) ? mod : { "default": mod };': 'return mod;', + }, + ); } interface InstallOptions { diff --git a/snowpack/src/scan-imports.ts b/snowpack/src/scan-imports.ts index 2f7453a9b4..c8606f1cf4 100644 --- a/snowpack/src/scan-imports.ts +++ b/snowpack/src/scan-imports.ts @@ -13,6 +13,7 @@ import { getExtension, HTML_JS_REGEX, HTML_STYLE_REGEX, + isImportOfPackage, isTruthy, readFile, SVELTE_VUE_REGEX, @@ -46,7 +47,12 @@ export async function getInstallTargets( } else { installTargets.push(...(await scanImports(process.env.NODE_ENV === 'test', config))); } - return installTargets; + return installTargets.filter( + (dep) => + !config.packageOptions.external.some((packageName) => + isImportOfPackage(dep.specifier, packageName), + ), + ); } export function matchDynamicImportValue(importStatement: string) { diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 44f0a268a6..d7d97262d3 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -195,14 +195,7 @@ export default { return; } await Promise.all( - [ - ...new Set( - installTargets - .map((t) => t.specifier) - // external packages need not prepare - .filter((t) => !config.packageOptions?.external.includes(t)), - ), - ].map((spec) => { + [...new Set(installTargets.map((t) => t.specifier))].map((spec) => { return this.resolvePackageImport(path.join(config.root, 'package.json'), spec, config); }), ); @@ -272,7 +265,7 @@ export default { ...Object.keys(packageManifest.dependencies || {}), ...Object.keys(packageManifest.devDependencies || {}), ...Object.keys(packageManifest.peerDependencies || {}), - ].filter(ext => ext !== _packageName); + ].filter((ext) => ext !== _packageName); const installOptions: InstallOptions = { dest: installDest, @@ -320,8 +313,8 @@ export default { if (isSymlink) { logger.warn( colors.bold(`Locally linked package detected outside of project root.\n`) + - `If you are working in a workspace/monorepo, set your snowpack.config.js "root"\n` + - `to the workspace root to take advantage of fast HMR updates for linked packages.\n` + + `If you are working in a workspace/monorepo, set your snowpack.config.js "root"\n` + + `to the workspace root to take advantage of fast HMR updates for linked packages.\n` + `Otherwise, this package won't be rebuilt until its package.json "version" changes.`, ); } diff --git a/snowpack/src/util.ts b/snowpack/src/util.ts index 2d0cda0848..40411af498 100644 --- a/snowpack/src/util.ts +++ b/snowpack/src/util.ts @@ -379,6 +379,10 @@ export function isRemoteUrl(val: string): boolean { return val.startsWith('//') || !!url.parse(val).protocol?.startsWith('http'); } +export function isImportOfPackage(importUrl: string, packageName: string) { + return packageName === importUrl || importUrl.startsWith(packageName + '/'); +} + /** * Sanitizes npm packages that end in .js (e.g `tippy.js` -> `tippyjs`). * This is necessary because Snowpack can’t create both a file and directory From 95e8672ecd94274251236a5b5a4006791869001e Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 11:14:51 -0800 Subject: [PATCH 08/40] add workspaceRoot config --- snowpack/src/build/file-builder.ts | 6 +++++- snowpack/src/build/file-urls.ts | 7 +++++-- snowpack/src/commands/dev.ts | 4 ++-- snowpack/src/config.ts | 5 ++++- snowpack/src/sources/local.ts | 12 ++++++------ snowpack/src/types.ts | 2 ++ test/build/package-workspace/snowpack.config.js | 2 +- www/_template/reference/configuration.md | 10 ++++++++-- 8 files changed, 33 insertions(+), 15 deletions(-) diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts index 6f2ce7b799..ef746e8836 100644 --- a/snowpack/src/build/file-builder.ts +++ b/snowpack/src/build/file-builder.ts @@ -71,7 +71,11 @@ export class FileBuilder { this.isSSR = isSSR; this.config = config; this.hmrEngine = hmrEngine || null; - this.urls = getUrlsForFile(loc, config); + const urls = getUrlsForFile(loc, config); + if (!urls) { + throw new Error(`No mounted URLs configured for file: ${loc}`); + } + this.urls = urls; } private verifyRequestFromBuild(type: string): SnowpackBuiltFile { diff --git a/snowpack/src/build/file-urls.ts b/snowpack/src/build/file-urls.ts index a02fd47ba2..88ba4b292b 100644 --- a/snowpack/src/build/file-urls.ts +++ b/snowpack/src/build/file-urls.ts @@ -69,15 +69,18 @@ export function getMountEntryForFile( /** * Get the final, hosted URL path for a given file on disk. */ -export function getUrlsForFile(fileLoc: string, config: SnowpackConfig): string[] { +export function getUrlsForFile(fileLoc: string, config: SnowpackConfig): undefined | string[] { const mountEntryResult = getMountEntryForFile(fileLoc, config); if (!mountEntryResult) { + if (!config.workspaceRoot) { + return undefined; + } const builtEntrypointUrls = getBuiltFileUrls(fileLoc, config); return builtEntrypointUrls.map((u) => path.posix.join( config.buildOptions.metaUrlPath, 'link', - slash(path.relative(config.root, u)), + slash(path.relative(config.workspaceRoot!, u)), ), ); } diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 34e0af7154..13d6e4b447 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -473,7 +473,7 @@ export async function startServer( if (reqPath.startsWith(PACKAGE_LINK_PATH_PREFIX)) { const symlinkResourceUrl = reqPath.substr(PACKAGE_LINK_PATH_PREFIX.length); const symlinkResourceLoc = path.resolve( - config.root, + config.workspaceRoot!, process.platform === 'win32' ? symlinkResourceUrl.replace(/\//g, '\\') : symlinkResourceUrl, ); const symlinkResourceDirectory = path.dirname(symlinkResourceLoc); @@ -503,7 +503,7 @@ export async function startServer( path.posix.join( config.buildOptions.metaUrlPath, 'link', - slash(path.relative(config.root, u)), + slash(path.relative(config.workspaceRoot!, u)), ), ), ); diff --git a/snowpack/src/config.ts b/snowpack/src/config.ts index cdcbc7e8ea..9219b05f4d 100644 --- a/snowpack/src/config.ts +++ b/snowpack/src/config.ts @@ -1,4 +1,5 @@ import {all as merge} from 'deepmerge'; +import findUp from 'find-up'; import {existsSync} from 'fs'; import {isPlainObject} from 'is-plain-object'; import {validate} from 'jsonschema'; @@ -399,7 +400,6 @@ function normalizeConfig(_config: SnowpackUserConfig): SnowpackConfig { config.exclude = Array.from( new Set([...ALWAYS_EXCLUDE, `${config.buildOptions.out}/**/*`, ...config.exclude]), ); - // normalize config URL/path values config.buildOptions.out = removeTrailingSlash(config.buildOptions.out); config.buildOptions.baseUrl = addTrailingSlash(config.buildOptions.baseUrl); @@ -642,6 +642,9 @@ function resolveRelativeConfig(config: SnowpackUserConfig, configBase: string): if (config.root) { config.root = path.resolve(configBase, config.root); } + if (config.workspaceRoot) { + config.workspaceRoot = path.resolve(configBase, config.workspaceRoot); + } if (config.buildOptions?.out) { config.buildOptions.out = path.resolve(configBase, config.buildOptions.out); } diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index d7d97262d3..b2d7b7ef50 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -216,10 +216,10 @@ export default { _packageName += '/' + specParts.shift(); } const isSymlink = !entrypoint.includes(path.join('node_modules', _packageName)); - const isWithinRoot = entrypoint.startsWith(config.root); - if (isSymlink && isWithinRoot) { + const isWithinRoot = config.workspaceRoot && entrypoint.startsWith(config.workspaceRoot); + if (isSymlink && config.workspaceRoot && isWithinRoot) { const builtEntrypointUrls = getBuiltFileUrls(entrypoint, config); - const builtEntrypointUrl = slash(path.relative(config.root, builtEntrypointUrls[0]!)); + const builtEntrypointUrl = slash(path.relative(config.workspaceRoot, builtEntrypointUrls[0]!)); allSymlinkImports[builtEntrypointUrl] = entrypoint; return path.posix.join(config.buildOptions.metaUrlPath, 'link', builtEntrypointUrl); } @@ -313,9 +313,9 @@ export default { if (isSymlink) { logger.warn( colors.bold(`Locally linked package detected outside of project root.\n`) + - `If you are working in a workspace/monorepo, set your snowpack.config.js "root"\n` + - `to the workspace root to take advantage of fast HMR updates for linked packages.\n` + - `Otherwise, this package won't be rebuilt until its package.json "version" changes.`, + `If you are working in a workspace/monorepo, set your snowpack.config.js "workspaceRoot"\n` + + `to the workspace directory to take advantage of fast HMR updates for linked packages.\n` + + `Otherwise, this package will be cached until its package.json "version" changes.`, ); } const dependencyFileLoc = path.join(installDest, newImportMap.imports[spec]); diff --git a/snowpack/src/types.ts b/snowpack/src/types.ts index 3ee9192e12..6c52157880 100644 --- a/snowpack/src/types.ts +++ b/snowpack/src/types.ts @@ -242,6 +242,7 @@ export interface PackageSourceRemote { // interface this library uses internally export interface SnowpackConfig { root: string; + workspaceRoot?: string; extends?: string; exclude: string[]; mount: Record; @@ -288,6 +289,7 @@ export interface SnowpackConfig { export type SnowpackUserConfig = { root?: string; + workspaceRoot?: string; install?: string[]; extends?: string; exclude?: string[]; diff --git a/test/build/package-workspace/snowpack.config.js b/test/build/package-workspace/snowpack.config.js index 0d1acaf1ac..c781472b86 100644 --- a/test/build/package-workspace/snowpack.config.js +++ b/test/build/package-workspace/snowpack.config.js @@ -1,6 +1,6 @@ /** @type {import("snowpack").SnowpackUserConfig } */ module.exports = { - root: '../', + workspaceRoot: '../', buildOptions: { out: './build' }, diff --git a/www/_template/reference/configuration.md b/www/_template/reference/configuration.md index dba3e936fe..97043947dd 100644 --- a/www/_template/reference/configuration.md +++ b/www/_template/reference/configuration.md @@ -23,9 +23,15 @@ To generate a basic configuration file scaffold in your Snowpack project run `sn **Type**: `string` **Default**: `/` -Specify the root of a project using Snowpack. +Specify the root of a project using Snowpack. (Previously: `config.cwd`) -Previously config.cwd +## workspaceRoot + +**Type**: `string` + +Specify the root of your workspace or monorepo, if you are using one. When configured, Snowpack will treat any sibling packages in your workspace like source files, and pass them through your unbundled Snowpack build pipeline during development. This allows for fast refresh, HMR support, file change watching, and other dev improvements when working in monorepos. + +When you build your site for production, symlinked packages will be treated like any other package, bundled and tree-shaken into single files for faster loading. ## install From 159d50a310798a03fe9fe4c4dd34bd2cc50a50f7 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 11:25:00 -0800 Subject: [PATCH 09/40] lint fix --- snowpack/src/config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/snowpack/src/config.ts b/snowpack/src/config.ts index 9219b05f4d..867f73d439 100644 --- a/snowpack/src/config.ts +++ b/snowpack/src/config.ts @@ -1,5 +1,4 @@ import {all as merge} from 'deepmerge'; -import findUp from 'find-up'; import {existsSync} from 'fs'; import {isPlainObject} from 'is-plain-object'; import {validate} from 'jsonschema'; From adf2580af93bd0589f5fcde93f09d28330b53235 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 11:50:50 -0800 Subject: [PATCH 10/40] treat namedExports as non-ESM by default --- esinstall/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 0e044e8393..8233b00e18 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -353,7 +353,7 @@ ${colors.dim( rollupPluginReplace(generateReplacements(env)), rollupPluginCommonjs({ extensions: ['.js', '.cjs'], - esmExternals: (id) => externalEsm.some((packageName) => isImportOfPackage(id, packageName)), + esmExternals: (id) => !namedExports.some((packageName) => isImportOfPackage(id, packageName)) && externalEsm.some((packageName) => isImportOfPackage(id, packageName)), requireReturnsDefault: 'auto', } as RollupCommonJSOptions), rollupPluginWrapInstallTargets(!!isTreeshake, autoDetectNamedExports, installTargets, logger), From 892834b169615f293c99d567b3b2eb19339b946b Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 12:25:26 -0800 Subject: [PATCH 11/40] add cjs<>esm interop improvements --- esinstall/src/index.ts | 9 +++--- snowpack/src/commands/build.ts | 2 +- snowpack/src/sources/local.ts | 32 ++++++++++++++++--- .../prepare-external-package/package.json | 2 +- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 8233b00e18..d6f5ce40b3 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -45,6 +45,7 @@ export { resolveEntrypoint, explodeExportMap, } from './entrypoints'; +export {resolveDependencyManifest} from './util'; export {printStats} from './stats'; type DependencyLoc = { @@ -132,7 +133,7 @@ interface InstallOptions { polyfillNode: boolean; sourcemap?: boolean | 'inline'; external: string[]; - externalEsm: string[]; + externalEsm: string[] | ((imp: string) => boolean); packageLookupFields: string[]; packageExportLookupFields: string[]; namedExports: string[]; @@ -174,8 +175,8 @@ function setOptionDefaults(_options: PublicInstallOptions): InstallOptions { // TODO: Make this default to false in a v2.0 release stats: true, dest: 'web_modules', - external: [], - externalEsm: [], + external: [] as string[], + externalEsm: [] as string[], polyfillNode: false, packageLookupFields: [], packageExportLookupFields: [], @@ -353,7 +354,7 @@ ${colors.dim( rollupPluginReplace(generateReplacements(env)), rollupPluginCommonjs({ extensions: ['.js', '.cjs'], - esmExternals: (id) => !namedExports.some((packageName) => isImportOfPackage(id, packageName)) && externalEsm.some((packageName) => isImportOfPackage(id, packageName)), + esmExternals: (id) => !namedExports.some((packageName) => isImportOfPackage(id, packageName)) && Array.isArray(externalEsm) ? externalEsm.some((packageName) => isImportOfPackage(id, packageName)) : (externalEsm as Function)(id), requireReturnsDefault: 'auto', } as RollupCommonJSOptions), rollupPluginWrapInstallTargets(!!isTreeshake, autoDetectNamedExports, installTargets, logger), diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index 0a30c1336d..5fed6a2324 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -105,7 +105,7 @@ export async function build(commandOptions: CommandOptions): Promise ext !== _packageName); + function getMemoizedResolveDependencyManifest() { + const results = {}; + return (packageName: string) => + results[packageName] || _resolveDependencyManifest(packageName, rootPackageDirectory!); + } + const resolveDependencyManifest = getMemoizedResolveDependencyManifest(); + const installOptions: InstallOptions = { dest: installDest, cwd: packageManifestLoc, env: {NODE_ENV: process.env.NODE_ENV || 'development'}, treeshake: false, - external: externalPackages, - externalEsm: externalPackages, sourcemap: config.buildOptions.sourcemap, alias: config.alias, + external: externalPackages, + // ESM<>CJS Compatability: If we can detect that a dependency is common.js vs. ESM, then + // we can provide this hint to esinstall to improve our cross-package import support. + externalEsm: (imp) => { + const specParts = imp.split('/'); + let _packageName: string = specParts.shift()!; + if (_packageName?.startsWith('@')) { + _packageName += '/' + specParts.shift(); + } + const [, result] = resolveDependencyManifest(_packageName); + return !result || !!(result.module || result.exports); + }, }; if (config.packageOptions.source === 'local') { if (config.packageOptions.polyfillNode !== undefined) { diff --git a/test/build/prepare-external-package/package.json b/test/build/prepare-external-package/package.json index aed36cf1e3..e47de0f01c 100644 --- a/test/build/prepare-external-package/package.json +++ b/test/build/prepare-external-package/package.json @@ -19,7 +19,7 @@ } }, "devDependencies": { - "snowpack": "link:../../../snowpack" + "snowpack": "^3.0.0" }, "dependencies": { "array-flatten": "^3.0.0" From 72d5dd80247484d46facbd8c9ac0388e604d6104 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 12:27:10 -0800 Subject: [PATCH 12/40] actually memoize the results --- snowpack/src/sources/local.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 3972927763..c9e2160d05 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -276,8 +276,12 @@ export default { function getMemoizedResolveDependencyManifest() { const results = {}; - return (packageName: string) => - results[packageName] || _resolveDependencyManifest(packageName, rootPackageDirectory!); + return (packageName: string) => { + results[packageName] = + results[packageName] || + _resolveDependencyManifest(packageName, rootPackageDirectory!); + return results[packageName]; + }; } const resolveDependencyManifest = getMemoizedResolveDependencyManifest(); From 600cbaf461df3bbc458c3abeeed0b7ae8cfc2b34 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 14:42:05 -0800 Subject: [PATCH 13/40] add back fsevents check --- snowpack/src/commands/dev.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 13d6e4b447..9a78fd0eb5 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -34,7 +34,7 @@ import { ServerRuntime, SnowpackDevServer, } from '../types'; -import {hasExtension, HMR_CLIENT_CODE, HMR_OVERLAY_CODE, openInBrowser} from '../util'; +import {hasExtension, HMR_CLIENT_CODE, HMR_OVERLAY_CODE, isFsEventsEnabled, openInBrowser} from '../util'; import {getPort, paintDashboard, paintEvent} from './paint'; export class OneToManyMap { @@ -854,9 +854,11 @@ export async function startServer( } const watcher = chokidar.watch(Object.keys(config.mount), { + ignored: config.exclude, persistent: true, ignoreInitial: true, disableGlobbing: false, + useFsEvents: isFsEventsEnabled(), }); watcher.on('add', (fileLoc) => { knownETags.clear(); From 5ad4565d93be17381c3e90daa8860dd51ab42974 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 16:11:52 -0800 Subject: [PATCH 14/40] add alias support, better symlink support --- esinstall/src/entrypoints.ts | 7 ++-- esinstall/src/index.ts | 6 +++- snowpack/src/build/file-builder.ts | 33 +++++++++---------- snowpack/src/commands/build.ts | 5 +-- snowpack/src/commands/dev.ts | 8 ++++- snowpack/src/sources/local.ts | 29 ++++++++++++++-- snowpack/src/types.ts | 7 +++- .../__snapshots__/config-alias.test.js.snap | 7 ++-- test/build/config-alias/config-alias.test.js | 1 - test/build/config-alias/src/index.html | 6 ++-- test/build/config-alias/src/index.js | 1 - .../package-workspace.test.js | 5 +-- test/build/package-workspace/src/index.svelte | 2 ++ .../works-without-extension.ts | 3 ++ www/_template/reference/configuration.md | 4 +-- 15 files changed, 83 insertions(+), 41 deletions(-) create mode 100755 test/build/test-workspace-component/works-without-extension.ts diff --git a/esinstall/src/entrypoints.ts b/esinstall/src/entrypoints.ts index acab153c10..a5a5dad6d7 100644 --- a/esinstall/src/entrypoints.ts +++ b/esinstall/src/entrypoints.ts @@ -1,5 +1,6 @@ import {readdirSync, existsSync, realpathSync, statSync} from 'fs'; import path from 'path'; +import builtinModules from 'builtin-modules'; import validatePackageName from 'validate-npm-package-name'; import {ExportField, ExportMapEntry, PackageManifestWithExports, PackageManifest} from './types'; import {parsePackageImportSpecifier, resolveDependencyManifest} from './util'; @@ -184,8 +185,10 @@ export function resolveEntrypoint( } // if, no export map and dep points directly to a file within a package, return that reference. - if (path.extname(dep) && !validatePackageName(dep).validForNewPackages) { - return realpathSync.native(resolve.sync(dep, {basedir: cwd})); + if (builtinModules.indexOf(dep) === -1 && !validatePackageName(dep).validForNewPackages) { + return realpathSync.native( + resolve.sync(dep, {basedir: cwd, extensions: ['.js', '.mjs', '.ts', '.jsx', '.tsx']}), + ); } // Otherwise, resolve directly to the dep specifier. Note that this supports both diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index d6f5ce40b3..1767497bb0 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -354,7 +354,11 @@ ${colors.dim( rollupPluginReplace(generateReplacements(env)), rollupPluginCommonjs({ extensions: ['.js', '.cjs'], - esmExternals: (id) => !namedExports.some((packageName) => isImportOfPackage(id, packageName)) && Array.isArray(externalEsm) ? externalEsm.some((packageName) => isImportOfPackage(id, packageName)) : (externalEsm as Function)(id), + esmExternals: (id) => + !namedExports.some((packageName) => isImportOfPackage(id, packageName)) && + Array.isArray(externalEsm) + ? externalEsm.some((packageName) => isImportOfPackage(id, packageName)) + : (externalEsm as Function)(id), requireReturnsDefault: 'auto', } as RollupCommonJSOptions), rollupPluginWrapInstallTargets(!!isTreeshake, autoDetectNamedExports, installTargets, logger), diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts index ef746e8836..e79f5e7784 100644 --- a/snowpack/src/build/file-builder.ts +++ b/snowpack/src/build/file-builder.ts @@ -126,22 +126,6 @@ export class FileBuilder { config: this.config, }); const resolveImport = async (spec) => { - // Try to resolve the specifier to a known URL in the project - let resolvedImportUrl = resolveImportSpecifier(spec); - if (!isResolveBareImports) { - return resolvedImportUrl || spec; - } - // Handle a package import - if (!resolvedImportUrl && importMap) { - if (importMap.imports[spec]) { - const PACKAGE_PATH_PREFIX = path.posix.join( - this.config.buildOptions.metaUrlPath, - 'pkg/', - ); - return path.posix.join(PACKAGE_PATH_PREFIX, importMap.imports[spec]); - } - throw new Error(`Unexpected: spec ${spec} not included in import map.`); - } // Ignore packages marked as external if (this.config.packageOptions.external?.includes(spec)) { return spec; @@ -149,8 +133,23 @@ export class FileBuilder { if (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) { - resolvedImportUrl = await pkgSource.resolvePackageImport(this.loc, spec, this.config); + try { + return await pkgSource.resolvePackageImport( + this.loc, + spec, + this.config, + importMap || (isResolveBareImports ? undefined : {imports: {}}), + ); + } catch (err) { + if (!isResolveBareImports && /not included in import map./.test(err.message)) { + return spec; + } + throw err; + } } return resolvedImportUrl || spec; }; diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index 5fed6a2324..91cb7453dd 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -103,10 +103,7 @@ export async function build(commandOptions: CommandOptions): Promise; /** Resolve a package import to URL (ex: "react" -> "/pkg/react") */ - resolvePackageImport(source: string, spec: string, config: SnowpackConfig): Promise; + resolvePackageImport( + source: string, + spec: string, + config: SnowpackConfig, + importMap?: ImportMap, + ): Promise; /** Modify the build install config for optimized build install. */ modifyBuildInstallOptions(options: { installOptions: EsinstallOptions; diff --git a/test/build/config-alias/__snapshots__/config-alias.test.js.snap b/test/build/config-alias/__snapshots__/config-alias.test.js.snap index ebd4e0444d..04752e6bea 100644 --- a/test/build/config-alias/__snapshots__/config-alias.test.js.snap +++ b/test/build/config-alias/__snapshots__/config-alias.test.js.snap @@ -35,9 +35,9 @@ exports[`config: alias generates imports as expected 1`] = ` console.log(oneToManyBuild); // Importing an absolute URL: we don't touch these - import absoluteUrl from './sort.js'; // absolute URL - import absoluteUrl_ from './foo.svelte.js'; // absolute URL - import absoluteUrl__ from './test-mjs.js'; // absolute URL + import absoluteUrl from './sort.js'; // absolute import + import absoluteUrl_ from './foo.svelte.js'; // absolute URL, plugin-provided file extension + import absoluteUrl__ from './test-mjs.js'; // absolute URL, missing file extension console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); // Importing a directory index.js file @@ -110,7 +110,6 @@ import absoluteUrl_ from './foo.svelte.js'; // absolute URL import absoluteUrl__ from './test-mjs.js'; // absolute URL console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); - // Importing a directory index.js file import components from './components/index.js'; // relative import import components______ from './components/index.js'; // relative import with trailing slash diff --git a/test/build/config-alias/config-alias.test.js b/test/build/config-alias/config-alias.test.js index 27984cd49a..b6487e77df 100644 --- a/test/build/config-alias/config-alias.test.js +++ b/test/build/config-alias/config-alias.test.js @@ -9,7 +9,6 @@ describe('config: alias', () => { beforeAll(() => { setupBuildTest(__dirname); files = readFiles(cwd); - console.error('GO!'); }); it('generates imports as expected', () => { diff --git a/test/build/config-alias/src/index.html b/test/build/config-alias/src/index.html index f90492b20a..194d73acc5 100644 --- a/test/build/config-alias/src/index.html +++ b/test/build/config-alias/src/index.html @@ -32,9 +32,9 @@ console.log(oneToManyBuild); // Importing an absolute URL: we don't touch these - import absoluteUrl from '/_dist_/sort.js'; // absolute URL - import absoluteUrl_ from '/_dist_/foo.svelte.js'; // absolute URL - import absoluteUrl__ from '/_dist_/test-mjs.js'; // absolute URL + import absoluteUrl from '/_dist_/sort.js'; // absolute import + import absoluteUrl_ from '/_dist_/foo.svelte.js'; // absolute URL, plugin-provided file extension + import absoluteUrl__ from '/_dist_/test-mjs.js'; // absolute URL, missing file extension console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); // Importing a directory index.js file diff --git a/test/build/config-alias/src/index.js b/test/build/config-alias/src/index.js index d407ce4bdb..4ed4c0f44b 100644 --- a/test/build/config-alias/src/index.js +++ b/test/build/config-alias/src/index.js @@ -26,7 +26,6 @@ import absoluteUrl_ from '/_dist_/foo.svelte.js'; // absolute URL import absoluteUrl__ from '/_dist_/test-mjs.js'; // absolute URL console.log(absoluteUrl, absoluteUrl_, absoluteUrl__); - // Importing a directory index.js file import components from './components'; // relative import import components______ from './components/'; // relative import with trailing slash diff --git a/test/build/package-workspace/package-workspace.test.js b/test/build/package-workspace/package-workspace.test.js index 5f73c2fc17..443e64b1f1 100644 --- a/test/build/package-workspace/package-workspace.test.js +++ b/test/build/package-workspace/package-workspace.test.js @@ -12,10 +12,11 @@ describe('test workspace linked packages', () => { it('builds source files as expected', () => { const jsLoc = path.join(cwd, '_dist_', 'index.svelte.js'); expect(fs.existsSync(jsLoc)).toBe(true); // file exists - expect(fs.readFileSync(jsLoc, 'utf-8')).toContain(`../_snowpack/pkg/test-workspace-component/SvelteComponent.svelte.js`); // file has expected imports + expect(fs.readFileSync(jsLoc, 'utf-8')).toContain(`../_snowpack/link/test-workspace-component/SvelteComponent.svelte.js`); // file has expected imports + expect(fs.readFileSync(jsLoc, 'utf-8')).toContain(`../_snowpack/link/test-workspace-component/works-without-extension.js`); // file has expected imports }); it('builds workspace package files as expected', () => { - expect(fs.existsSync(path.join(cwd, '_snowpack', 'pkg', 'test-workspace-component', 'SvelteComponent.svelte.js'))).toBe(true); // import exists + expect(fs.existsSync(path.join(cwd, '_snowpack', 'link', 'test-workspace-component', 'SvelteComponent.svelte.js'))).toBe(true); // import exists }); }); diff --git a/test/build/package-workspace/src/index.svelte b/test/build/package-workspace/src/index.svelte index c093c78a0e..71dff0c577 100644 --- a/test/build/package-workspace/src/index.svelte +++ b/test/build/package-workspace/src/index.svelte @@ -5,6 +5,8 @@ diff --git a/test/build/test-workspace-component/works-without-extension.ts b/test/build/test-workspace-component/works-without-extension.ts new file mode 100755 index 0000000000..53702f2836 --- /dev/null +++ b/test/build/test-workspace-component/works-without-extension.ts @@ -0,0 +1,3 @@ +export function testFn() { + return 42 as number; +} \ No newline at end of file diff --git a/www/_template/reference/configuration.md b/www/_template/reference/configuration.md index 97043947dd..9d9a4c9880 100644 --- a/www/_template/reference/configuration.md +++ b/www/_template/reference/configuration.md @@ -27,9 +27,9 @@ Specify the root of a project using Snowpack. (Previously: `config.cwd`) ## workspaceRoot -**Type**: `string` +**Type**: `string` -Specify the root of your workspace or monorepo, if you are using one. When configured, Snowpack will treat any sibling packages in your workspace like source files, and pass them through your unbundled Snowpack build pipeline during development. This allows for fast refresh, HMR support, file change watching, and other dev improvements when working in monorepos. +Specify the root of your workspace or monorepo, if you are using one. When configured, Snowpack will treat any sibling packages in your workspace like source files, and pass them through your unbundled Snowpack build pipeline during development. This allows for fast refresh, HMR support, file change watching, and other dev improvements when working in monorepos. When you build your site for production, symlinked packages will be treated like any other package, bundled and tree-shaken into single files for faster loading. From d26e13da2dcda0e27f935d72e803b8f27fee9bf1 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 19:09:32 -0800 Subject: [PATCH 15/40] add support for buffer loading --- snowpack/src/sources/local-install.ts | 14 +++++++------- www/_template/reference/configuration.md | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/snowpack/src/sources/local-install.ts b/snowpack/src/sources/local-install.ts index 0253d54dc2..dce1f5dbd5 100644 --- a/snowpack/src/sources/local-install.ts +++ b/snowpack/src/sources/local-install.ts @@ -3,7 +3,7 @@ import url from 'url'; import util from 'util'; import {buildFile} from '../build/build-pipeline'; import {logger} from '../logger'; -import {ImportMap, SnowpackConfig} from '../types'; +import {ImportMap, SnowpackBuiltFile, SnowpackConfig} from '../types'; interface InstallOptions { config: SnowpackConfig; @@ -70,16 +70,16 @@ export async function installPackages({ isPackage: true, isHmrEnabled: false, }); - let jsResponse; + let jsResponse: SnowpackBuiltFile | undefined; for (const [outputType, outputContents] of Object.entries(output)) { - if (jsResponse) { - console.log(`load() Err: ${Object.keys(output)}`); - } - if (!jsResponse || outputType === '.js') { + if (outputContents && outputType === '.js') { jsResponse = outputContents; } } - return jsResponse; + if (jsResponse && Buffer.isBuffer(jsResponse.code )) { + jsResponse.code = jsResponse.code.toString(); + } + return jsResponse as {code: string, map?: string}; }, }, ], diff --git a/www/_template/reference/configuration.md b/www/_template/reference/configuration.md index 9d9a4c9880..bc8b63d752 100644 --- a/www/_template/reference/configuration.md +++ b/www/_template/reference/configuration.md @@ -412,7 +412,7 @@ _NOTE:_ Deprecated, see `buildOptions.metaUrlPath`. ### buildOptions.metaUrlPath **Type**: `string` -**Default**: `_snowpack_` +**Default**: `_snowpack` Rename the default directory for Snowpack metadata. In every build, Snowpack creates meta files for loading things like [HMR](/concepts/hot-module-replacement), [Environment Variables](/reference/environment-variables), and your built npm packages. From a341e48468c638764af4f1de2b0ba0a9b896e0d9 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 19:17:40 -0800 Subject: [PATCH 16/40] fix baseUrl in proxies --- snowpack/src/build/file-builder.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts index e79f5e7784..5d6b34d2d4 100644 --- a/snowpack/src/build/file-builder.ts +++ b/snowpack/src/build/file-builder.ts @@ -18,7 +18,7 @@ import { SnowpackBuiltFile, SnowpackConfig, } from '../types'; -import {createInstallTarget, isRemoteUrl, relativeURL, replaceExtension} from '../util'; +import {createInstallTarget, isRemoteUrl, relativeURL, removeLeadingSlash, replaceExtension} from '../util'; import { getMetaUrlPath, SRI_CLIENT_HMR_SNOWPACK, @@ -324,7 +324,7 @@ export class FileBuilder { async getProxy(_url: string, type: string) { const code = this.resolvedOutput[type].code; - const url = path.posix.join(this.isDev ? '/' : this.config.buildOptions.baseUrl, _url); + const url = this.isDev ? _url : this.config.buildOptions.baseUrl + removeLeadingSlash(_url); return await wrapImportProxy({url, code, hmr: this.isHMR, config: this.config}); } From d493acd65ae4770e5ffcfad250413f64b42c083a Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 24 Feb 2021 19:48:19 -0800 Subject: [PATCH 17/40] add source to entrypoints --- snowpack/src/build/file-builder.ts | 8 +++++++- snowpack/src/sources/local-install.ts | 4 ++-- snowpack/src/sources/local.ts | 5 ++++- test/build/package-workspace/src/index.svelte | 3 ++- test/build/test-workspace-component/Layout.ts | 1 + test/build/test-workspace-component/index.mjs | 2 ++ 6 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 test/build/test-workspace-component/Layout.ts diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts index 5d6b34d2d4..cffc341881 100644 --- a/snowpack/src/build/file-builder.ts +++ b/snowpack/src/build/file-builder.ts @@ -18,7 +18,13 @@ import { SnowpackBuiltFile, SnowpackConfig, } from '../types'; -import {createInstallTarget, isRemoteUrl, relativeURL, removeLeadingSlash, replaceExtension} from '../util'; +import { + createInstallTarget, + isRemoteUrl, + relativeURL, + removeLeadingSlash, + replaceExtension, +} from '../util'; import { getMetaUrlPath, SRI_CLIENT_HMR_SNOWPACK, diff --git a/snowpack/src/sources/local-install.ts b/snowpack/src/sources/local-install.ts index dce1f5dbd5..40e2c75400 100644 --- a/snowpack/src/sources/local-install.ts +++ b/snowpack/src/sources/local-install.ts @@ -76,10 +76,10 @@ export async function installPackages({ jsResponse = outputContents; } } - if (jsResponse && Buffer.isBuffer(jsResponse.code )) { + if (jsResponse && Buffer.isBuffer(jsResponse.code)) { jsResponse.code = jsResponse.code.toString(); } - return jsResponse as {code: string, map?: string}; + return jsResponse as {code: string; map?: string}; }, }, ], diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 129625af46..7c8e2c3ce0 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -231,7 +231,10 @@ export default { const entrypoint = resolveEntrypoint(spec, { cwd: path.dirname(source), - packageLookupFields: (_config.packageOptions as PackageSourceLocal).packageLookupFields || [], + packageLookupFields: [ + 'snowpack:source', + ...((_config.packageOptions as PackageSourceLocal).packageLookupFields || []), + ], }); const specParts = spec.split('/'); let _packageName: string = specParts.shift()!; diff --git a/test/build/package-workspace/src/index.svelte b/test/build/package-workspace/src/index.svelte index 71dff0c577..581e6d6af7 100644 --- a/test/build/package-workspace/src/index.svelte +++ b/test/build/package-workspace/src/index.svelte @@ -6,7 +6,8 @@ diff --git a/test/build/test-workspace-component/Layout.ts b/test/build/test-workspace-component/Layout.ts new file mode 100644 index 0000000000..003632c45a --- /dev/null +++ b/test/build/test-workspace-component/Layout.ts @@ -0,0 +1 @@ +export const bob = 42; \ No newline at end of file diff --git a/test/build/test-workspace-component/index.mjs b/test/build/test-workspace-component/index.mjs index 7e66e5111f..aa2fe9c161 100755 --- a/test/build/test-workspace-component/index.mjs +++ b/test/build/test-workspace-component/index.mjs @@ -1,3 +1,5 @@ +export * from './Layout' + export function testComponent() { return 42; } \ No newline at end of file From 31f1a4a52c555f22fc3b5f729acd12ed98ec5be4 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 16:38:23 -0800 Subject: [PATCH 18/40] small fixes based on pr feedback --- esinstall/src/index.ts | 3 +- plugins/plugin-react-refresh/plugin.js | 5 ++- snowpack/src/commands/build.ts | 5 ++- snowpack/src/commands/dev.ts | 61 ++++++++++++++------------ snowpack/src/config.ts | 2 +- 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 1767497bb0..06b8edacdc 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -392,7 +392,8 @@ ${colors.dim( if ( warning.code === 'CIRCULAR_DEPENDENCY' || warning.code === 'NAMESPACE_CONFLICT' || - warning.code === 'THIS_IS_UNDEFINED' + warning.code === 'THIS_IS_UNDEFINED' || + warning.code === 'EMPTY_BUNDLE' ) { logger.debug(logMessage); } else { diff --git a/plugins/plugin-react-refresh/plugin.js b/plugins/plugin-react-refresh/plugin.js index fd990c09f6..76bb1a0a63 100644 --- a/plugins/plugin-react-refresh/plugin.js +++ b/plugins/plugin-react-refresh/plugin.js @@ -53,7 +53,10 @@ async function transformJs(contents, id, cwd, skipTransform) { sourceMaps: false, configFile: false, babelrc: false, - plugins: [require('react-refresh/babel'), require('@babel/plugin-syntax-class-properties')], + plugins: [ + [require('react-refresh/babel'), {skipEnvCheck: true}], + require('@babel/plugin-syntax-class-properties'), + ], }); fastRefreshEnhancedCode = code; } diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index 91cb7453dd..b83e67bbbf 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -213,7 +213,6 @@ export async function build(commandOptions: CommandOptions): Promise {}; devServer.onFileChange(async ({filePath}) => { // First, do our own re-build logic @@ -227,6 +226,10 @@ export async function build(commandOptions: CommandOptions): Promise (onFileChangeCallback = callback), shutdown() { diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 9c74a92c44..97bf04f3cb 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -1,3 +1,4 @@ +import {FSWatcher} from 'chokidar'; import isCompressible from 'compressible'; import detectPort from 'detect-port'; import {InstallTarget} from 'esinstall'; @@ -753,11 +754,10 @@ export async function startServer( const {hmrDelay} = config.devOptions; const hmrPort = config.devOptions.hmrPort || - config.devOptions.port || - (await detectPort(config.devOptions.hmrPort || config.devOptions.port)); + config.devOptions.port; const hmrEngineOptions = Object.assign( {delay: hmrDelay}, - config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, + (config.devOptions.hmrPort || !server) ? {port: hmrPort} : {server, port: hmrPort}, ); const hmrEngine = new EsmHmrEngine(hmrEngineOptions); onProcessExit(() => { @@ -835,12 +835,9 @@ export async function startServer( } } - // Start watching the file system. - // Defer "chokidar" loading to here, to reduce impact on overall startup time - const chokidar = await import('chokidar'); - // Allow the user to hook into this callback, if they like (noop by default) let onFileChangeCallback: OnFileChangeCallback = () => {}; + let watcher: FSWatcher | undefined; // Watch src files async function onWatchEvent(fileLoc: string) { @@ -859,27 +856,31 @@ export async function startServer( } } - const watcher = chokidar.watch(Object.keys(config.mount), { - ignored: config.exclude, - persistent: true, - ignoreInitial: true, - disableGlobbing: false, - useFsEvents: isFsEventsEnabled(), - }); - watcher.on('add', (fileLoc) => { - knownETags.clear(); - onWatchEvent(fileLoc); - fileToUrlMapping.add(fileLoc, getUrlsForFile(fileLoc, config)!); - }); - watcher.on('unlink', (fileLoc) => { - knownETags.clear(); - onWatchEvent(fileLoc); - fileToUrlMapping.delete(fileLoc); - }); - watcher.on('change', (fileLoc) => { - onWatchEvent(fileLoc); - }); - logger.info(colors.cyan('watching for file changes...')); + if (config.buildOptions.watch) { + // Start watching the file system. + // Defer "chokidar" loading to here, to reduce impact on overall startup time + const chokidar = await import('chokidar'); + const watcher = chokidar.watch(Object.keys(config.mount), { + ignored: config.exclude, + persistent: true, + ignoreInitial: true, + disableGlobbing: false, + useFsEvents: isFsEventsEnabled(), + }); + watcher.on('add', (fileLoc) => { + knownETags.clear(); + onWatchEvent(fileLoc); + fileToUrlMapping.add(fileLoc, getUrlsForFile(fileLoc, config)!); + }); + watcher.on('unlink', (fileLoc) => { + knownETags.clear(); + onWatchEvent(fileLoc); + fileToUrlMapping.delete(fileLoc); + }); + watcher.on('change', (fileLoc) => { + onWatchEvent(fileLoc); + }); + } // Open the user's browser (ignore if failed) if (server && port && open && open !== 'none') { @@ -903,7 +904,7 @@ export async function startServer( onFileChange: (callback) => (onFileChangeCallback = callback), getServerRuntime: (options) => getServerRuntime(sp, options), async shutdown() { - await watcher.close(); + watcher && (await watcher.close()); server && server.close(); }, } as SnowpackDevServer; @@ -916,10 +917,12 @@ export async function command(commandOptions: CommandOptions) { commandOptions.config.devOptions.output = commandOptions.config.devOptions.output || 'dashboard'; commandOptions.config.devOptions.open = commandOptions.config.devOptions.open || 'default'; + commandOptions.config.buildOptions.watch = true; // Start the server const pkgSource = getPackageSource(commandOptions.config.packageOptions.source); await pkgSource.prepare(commandOptions); await startServer(commandOptions); + logger.info(colors.cyan('watching for file changes...')); } catch (err) { logger.error(err.message); logger.debug(err.stack); diff --git a/snowpack/src/config.ts b/snowpack/src/config.ts index 867f73d439..00a6789d4d 100644 --- a/snowpack/src/config.ts +++ b/snowpack/src/config.ts @@ -21,7 +21,7 @@ import { import {addLeadingSlash, addTrailingSlash, NATIVE_REQUIRE, removeTrailingSlash} from './util'; const CONFIG_NAME = 'snowpack'; -const ALWAYS_EXCLUDE = ['**/node_modules/**/*']; +const ALWAYS_EXCLUDE = ['**/node_modules/**/*', '**/*.d.ts']; // default settings const DEFAULT_ROOT = process.cwd(); From eb0fd544f9c9efba0b024662461fc9b1666ac8f4 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 16:43:46 -0800 Subject: [PATCH 19/40] lint fix, and stop ignoring empty files --- esinstall/src/index.ts | 2 +- snowpack/src/build/build-pipeline.ts | 3 --- snowpack/src/commands/build.ts | 4 +++- snowpack/src/commands/dev.ts | 7 ++----- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 06b8edacdc..abd98e37b1 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -392,7 +392,7 @@ ${colors.dim( if ( warning.code === 'CIRCULAR_DEPENDENCY' || warning.code === 'NAMESPACE_CONFLICT' || - warning.code === 'THIS_IS_UNDEFINED' || + warning.code === 'THIS_IS_UNDEFINED' || warning.code === 'EMPTY_BUNDLE' ) { logger.debug(logMessage); diff --git a/snowpack/src/build/build-pipeline.ts b/snowpack/src/build/build-pipeline.ts index 1fe59f01b1..d6ccfc8eed 100644 --- a/snowpack/src/build/build-pipeline.ts +++ b/snowpack/src/build/build-pipeline.ts @@ -72,9 +72,6 @@ async function runPipelineLoadStep( // if source maps disabled, don’t return any if (!config.buildOptions.sourcemap) result[ext].map = undefined; - - // clean up empty files - if (!result[ext].code) delete result[ext]; }); return result; } diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index b83e67bbbf..a71baa4d41 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -228,7 +228,9 @@ export async function build(commandOptions: CommandOptions): Promise (onFileChangeCallback = callback), diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 97bf04f3cb..3e75c5d654 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -1,6 +1,5 @@ import {FSWatcher} from 'chokidar'; import isCompressible from 'compressible'; -import detectPort from 'detect-port'; import {InstallTarget} from 'esinstall'; import etag from 'etag'; import {EventEmitter} from 'events'; @@ -752,12 +751,10 @@ export async function startServer( } const {hmrDelay} = config.devOptions; - const hmrPort = - config.devOptions.hmrPort || - config.devOptions.port; + const hmrPort = config.devOptions.hmrPort || config.devOptions.port; const hmrEngineOptions = Object.assign( {delay: hmrDelay}, - (config.devOptions.hmrPort || !server) ? {port: hmrPort} : {server, port: hmrPort}, + config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, ); const hmrEngine = new EsmHmrEngine(hmrEngineOptions); onProcessExit(() => { From a3683ff4bfed8f2344563f28d76b258430192231 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 16:49:57 -0800 Subject: [PATCH 20/40] add better empty file handling --- snowpack/src/build/file-builder.ts | 4 +++- snowpack/src/commands/dev.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts index cffc341881..5d3fd2b3af 100644 --- a/snowpack/src/build/file-builder.ts +++ b/snowpack/src/build/file-builder.ts @@ -88,7 +88,9 @@ export class FileBuilder { // Verify that the requested file exists in the build output map. if (!this.resolvedOutput[type] || !Object.keys(this.resolvedOutput)) { throw new Error( - `${this.loc} - Requested content "${type}" but built ${Object.keys(this.resolvedOutput)}`, + `${this.loc} - Requested content "${type}" but built ${ + Object.keys(this.resolvedOutput).toString() || 'none' + }.`, ); } return this.resolvedOutput[type]; diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 3e75c5d654..6b9264e60f 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -611,7 +611,7 @@ export async function startServer( handleFinalizeError(err); throw err; } - if (!finalizedResponse) { + if (finalizedResponse === undefined) { throw new NotFoundError(reqPath); } From 7371bdd33885a3a6f5fc83b9074f334b419e8027 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 16:56:21 -0800 Subject: [PATCH 21/40] update test --- esinstall/src/index.ts | 11 +++++------ .../error-missing-dep/error-missing-dep.test.js | 1 + .../esinstall/import-types/import-types.test.js | 17 +++++------------ 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index abd98e37b1..81ab2c5343 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -87,12 +87,11 @@ function resolveWebDependency( dep: string, resolveOptions: {cwd: string; packageLookupFields: string[]}, ): DependencyLoc { - const loc = resolveEntrypoint(dep, resolveOptions); - - return { - loc, - type: getWebDependencyType(loc), - }; + const loc = resolveEntrypoint(dep, resolveOptions); + return { + loc, + type: getWebDependencyType(loc), + }; } function generateEnvObject(userEnv: EnvVarReplacements): Object { diff --git a/test/esinstall/error-missing-dep/error-missing-dep.test.js b/test/esinstall/error-missing-dep/error-missing-dep.test.js index 4e018ac044..4d5f3eea90 100644 --- a/test/esinstall/error-missing-dep/error-missing-dep.test.js +++ b/test/esinstall/error-missing-dep/error-missing-dep.test.js @@ -17,6 +17,7 @@ describe('error-missing-dep', () => { // Run Test try { const {output, snapshotFile} = await runTest(['fakemodule'], {cwd}); + expect(false).toEqual(true); // should not finish } catch (err) { expect(err.message).toEqual('Package "fakemodule" not found. Have you installed it? '); } diff --git a/test/esinstall/import-types/import-types.test.js b/test/esinstall/import-types/import-types.test.js index 7cc5249a8c..b6bba4d708 100644 --- a/test/esinstall/import-types/import-types.test.js +++ b/test/esinstall/import-types/import-types.test.js @@ -2,23 +2,16 @@ const path = require('path'); const {runTest} = require('../esinstall-test-utils.js'); describe('importing types', () => { - it('preserves the types', async () => { + it('generates an error', async () => { const cwd = __dirname; const dest = path.join(cwd, 'test-types-only'); - const spec = 'type-only-pkg'; + // Run Test try { - const { - importMap: {imports}, - } = await runTest([spec, 'array-flatten'], { - cwd, - dest, - }); - - // This package should not have been installed because it only contains types. - expect(imports[spec]).toBeFalsy(); + await runTest(['type-only-pkg', 'array-flatten'], {cwd, dest}); + expect(false).toEqual(true); // should not finish } catch (err) { - expect(err.toString()).toEqual(expect.stringContaining('Unable to find any entrypoint')); + expect(err.message).toContain('Cannot find module'); } }); }); From 5b1701842ad50976c3ade3450360039c1752c5e8 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 21:03:15 -0800 Subject: [PATCH 22/40] fix bad variable scoping --- snowpack/src/commands/dev.ts | 2 +- snowpack/src/sources/local.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 6b9264e60f..30942c3c1a 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -857,7 +857,7 @@ export async function startServer( // Start watching the file system. // Defer "chokidar" loading to here, to reduce impact on overall startup time const chokidar = await import('chokidar'); - const watcher = chokidar.watch(Object.keys(config.mount), { + watcher = chokidar.watch(Object.keys(config.mount), { ignored: config.exclude, persistent: true, ignoreInitial: true, diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 7c8e2c3ce0..28f9183304 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -32,6 +32,7 @@ import { } from '../util'; import {installPackages} from './local-install'; import findUp from 'find-up'; +import mkdirp from 'mkdirp'; const PROJECT_CACHE_DIR = projectCacheDir({name: 'snowpack'}) || @@ -210,6 +211,7 @@ export default { return this.resolvePackageImport(path.join(config.root, 'package.json'), spec, config); }), ); + await mkdirp(path.dirname(installDirectoryHashLoc)); await fs.writeFile(installDirectoryHashLoc, 'v1', 'utf-8'); logger.info(colors.bold('Set up complete!')); return; From 72e076f3b2c7e69d71133168658feffefbf580e2 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 21:13:06 -0800 Subject: [PATCH 23/40] make hmr optional --- snowpack/src/commands/dev.ts | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 30942c3c1a..e23c429ae5 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -408,7 +408,7 @@ export async function startServer( ): Promise { const isSSR = _isSSR ?? false; // // Default to HMR on, but disable HMR if SSR mode is enabled. - const isHMR = _isHMR ?? ((config.devOptions.hmr ?? true) && !isSSR); + const isHMR = _isHMR ?? (!!config.devOptions.hmr && !isSSR); const encoding = _encoding ?? null; const reqUrlHmrParam = reqUrl.includes('?mtime=') && reqUrl.split('?')[1]; const reqPath = decodeURI(url.parse(reqUrl).pathname!); @@ -581,7 +581,7 @@ export async function startServer( function handleFinalizeError(err: Error) { logger.error(FILE_BUILD_RESULT_ERROR); - hmrEngine.broadcastMessage({ + hmrEngine && hmrEngine.broadcastMessage({ type: 'error', title: FILE_BUILD_RESULT_ERROR, errorMessage: err.toString(), @@ -750,15 +750,19 @@ export async function startServer( }); } + let hmrEngine: EsmHmrEngine | undefined; + let handleHmrUpdate: ((fileLoc: string, originalUrl: string) => void) | undefined; + if (config.devOptions.hmr) { const {hmrDelay} = config.devOptions; const hmrPort = config.devOptions.hmrPort || config.devOptions.port; const hmrEngineOptions = Object.assign( {delay: hmrDelay}, config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, ); - const hmrEngine = new EsmHmrEngine(hmrEngineOptions); + const _hmrEngine = new EsmHmrEngine(hmrEngineOptions); + hmrEngine = _hmrEngine; onProcessExit(() => { - hmrEngine.disconnectAllClients(); + _hmrEngine.disconnectAllClients(); }); // Live Reload + File System Watching @@ -768,25 +772,25 @@ export async function startServer( if (visited.has(url)) { return; } - const node = hmrEngine.getEntry(url); + const node = _hmrEngine.getEntry(url); const isBubbled = visited.size > 0; if (node && node.isHmrEnabled) { - hmrEngine.broadcastMessage({type: 'update', url, bubbled: isBubbled}); + _hmrEngine.broadcastMessage({type: 'update', url, bubbled: isBubbled}); } visited.add(url); if (node && node.isHmrAccepted) { // Found a boundary, no bubbling needed } else if (node && node.dependents.size > 0) { node.dependents.forEach((dep) => { - hmrEngine.markEntryForReplacement(node, true); + _hmrEngine.markEntryForReplacement(node, true); updateOrBubble(dep, visited); }); } else { // We've reached the top, trigger a full page refresh - hmrEngine.broadcastMessage({type: 'reload'}); + _hmrEngine.broadcastMessage({type: 'reload'}); } } - function handleHmrUpdate(fileLoc: string, originalUrl: string) { + handleHmrUpdate = function handleHmrUpdate(fileLoc: string, originalUrl: string) { if (isLiveReloadPaused) { return; } @@ -794,7 +798,7 @@ export async function startServer( // CSS files may be loaded directly in the client (not via JS import / .proxy.js) // so send an "update" event to live update if thats the case. if (hasExtension(originalUrl, '.css') && !hasExtension(originalUrl, '.module.css')) { - hmrEngine.broadcastMessage({type: 'update', url: originalUrl, bubbled: false}); + _hmrEngine.broadcastMessage({type: 'update', url: originalUrl, bubbled: false}); } // Append ".proxy.js" to Non-JS files to match their registered URL in the @@ -812,13 +816,13 @@ export async function startServer( const virtualCssFileUrl = updatedUrl.replace(/.js$/, '.css'); const virtualNode = virtualCssFileUrl.includes(path.basename(fileLoc)) && - hmrEngine.getEntry(`${virtualCssFileUrl}.proxy.js`); + _hmrEngine.getEntry(`${virtualCssFileUrl}.proxy.js`); if (virtualNode) { - hmrEngine.markEntryForReplacement(virtualNode, true); + _hmrEngine.markEntryForReplacement(virtualNode, true); } // If the changed file exists on the page, trigger a new HMR update. - if (hmrEngine.getEntry(updatedUrl)) { + if (_hmrEngine.getEntry(updatedUrl)) { updateOrBubble(updatedUrl, new Set()); return; } @@ -827,10 +831,11 @@ export async function startServer( // means that the file likely exists on the current page, but is not // supported by HMR (HTML, image, etc)). if (inMemoryBuildCache.has(getCacheKey(fileLoc, {isSSR: false, env: process.env.NODE_ENV}))) { - hmrEngine.broadcastMessage({type: 'reload'}); + _hmrEngine.broadcastMessage({type: 'reload'}); return; } } +} // Allow the user to hook into this callback, if they like (noop by default) let onFileChangeCallback: OnFileChangeCallback = () => {}; @@ -842,7 +847,7 @@ export async function startServer( await onFileChangeCallback({filePath: fileLoc}); const updatedUrls = getUrlsForFile(fileLoc, config); if (updatedUrls) { - handleHmrUpdate(fileLoc, updatedUrls[0]); + handleHmrUpdate && handleHmrUpdate(fileLoc, updatedUrls[0]); knownETags.delete(updatedUrls[0]); knownETags.delete(updatedUrls[0] + '.proxy.js'); } @@ -915,6 +920,7 @@ export async function command(commandOptions: CommandOptions) { commandOptions.config.devOptions.output || 'dashboard'; commandOptions.config.devOptions.open = commandOptions.config.devOptions.open || 'default'; commandOptions.config.buildOptions.watch = true; + commandOptions.config.devOptions.hmr = true; // Start the server const pkgSource = getPackageSource(commandOptions.config.packageOptions.source); await pkgSource.prepare(commandOptions); From 20ee69e240769ea53bfa6b0a9ed04afe5d7bf2d3 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 21:34:58 -0800 Subject: [PATCH 24/40] move hmr logic into seperate file --- snowpack/src/commands/dev.ts | 115 +++++------------------------------ snowpack/src/dev/hmr.ts | 99 ++++++++++++++++++++++++++++++ snowpack/src/util.ts | 4 ++ 3 files changed, 117 insertions(+), 101 deletions(-) create mode 100644 snowpack/src/dev/hmr.ts diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index e23c429ae5..5d4c296d9c 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -12,7 +12,6 @@ import mime from 'mime-types'; import os from 'os'; import path from 'path'; import {performance} from 'perf_hooks'; -import onProcessExit from 'signal-exit'; import slash from 'slash'; import stream from 'stream'; import url from 'url'; @@ -21,7 +20,7 @@ import zlib from 'zlib'; import {generateEnvModule, getMetaUrlPath, wrapImportProxy} from '../build/build-import-proxy'; import {FileBuilder} from '../build/file-builder'; import {getBuiltFileUrls, getMountEntryForFile, getUrlsForFile} from '../build/file-urls'; -import {EsmHmrEngine} from '../hmr-server-engine'; +import {startHmrEngine} from '../dev/hmr'; import {logger} from '../logger'; import {getPackageSource} from '../sources/util'; import {createLoader as createServerRuntime} from '../ssr-loader'; @@ -35,14 +34,13 @@ import { SnowpackDevServer, } from '../types'; import { - hasExtension, + getCacheKey, HMR_CLIENT_CODE, HMR_OVERLAY_CODE, isFsEventsEnabled, openInBrowser, } from '../util'; import {getPort, paintDashboard, paintEvent} from './paint'; - export class OneToManyMap { readonly keyToValue = new Map(); readonly valueToKey = new Map(); @@ -104,10 +102,6 @@ function encodeResponse( } } -function getCacheKey(fileLoc: string, {isSSR, env}) { - return `${fileLoc}?env=${env}&isSSR=${isSSR ? '1' : '0'}`; -} - /** * A helper class for "Not Found" errors, storing data about what file lookups were attempted. */ @@ -581,13 +575,14 @@ export async function startServer( function handleFinalizeError(err: Error) { logger.error(FILE_BUILD_RESULT_ERROR); - hmrEngine && hmrEngine.broadcastMessage({ - type: 'error', - title: FILE_BUILD_RESULT_ERROR, - errorMessage: err.toString(), - fileLoc, - errorStackTrace: err.stack, - }); + hmrEngine && + hmrEngine.broadcastMessage({ + type: 'error', + title: FILE_BUILD_RESULT_ERROR, + errorMessage: err.toString(), + fileLoc, + errorStackTrace: err.stack, + }); } let finalizedResponse: string | Buffer | undefined; @@ -750,92 +745,10 @@ export async function startServer( }); } - let hmrEngine: EsmHmrEngine | undefined; - let handleHmrUpdate: ((fileLoc: string, originalUrl: string) => void) | undefined; - if (config.devOptions.hmr) { - const {hmrDelay} = config.devOptions; - const hmrPort = config.devOptions.hmrPort || config.devOptions.port; - const hmrEngineOptions = Object.assign( - {delay: hmrDelay}, - config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, - ); - const _hmrEngine = new EsmHmrEngine(hmrEngineOptions); - hmrEngine = _hmrEngine; - onProcessExit(() => { - _hmrEngine.disconnectAllClients(); - }); - - // Live Reload + File System Watching - let isLiveReloadPaused = false; - - function updateOrBubble(url: string, visited: Set) { - if (visited.has(url)) { - return; - } - const node = _hmrEngine.getEntry(url); - const isBubbled = visited.size > 0; - if (node && node.isHmrEnabled) { - _hmrEngine.broadcastMessage({type: 'update', url, bubbled: isBubbled}); - } - visited.add(url); - if (node && node.isHmrAccepted) { - // Found a boundary, no bubbling needed - } else if (node && node.dependents.size > 0) { - node.dependents.forEach((dep) => { - _hmrEngine.markEntryForReplacement(node, true); - updateOrBubble(dep, visited); - }); - } else { - // We've reached the top, trigger a full page refresh - _hmrEngine.broadcastMessage({type: 'reload'}); - } - } - handleHmrUpdate = function handleHmrUpdate(fileLoc: string, originalUrl: string) { - if (isLiveReloadPaused) { - return; - } - - // CSS files may be loaded directly in the client (not via JS import / .proxy.js) - // so send an "update" event to live update if thats the case. - if (hasExtension(originalUrl, '.css') && !hasExtension(originalUrl, '.module.css')) { - _hmrEngine.broadcastMessage({type: 'update', url: originalUrl, bubbled: false}); - } - - // Append ".proxy.js" to Non-JS files to match their registered URL in the - // client app. - let updatedUrl = originalUrl; - if (!hasExtension(updatedUrl, '.js')) { - updatedUrl += '.proxy.js'; - } - - // Check if a virtual file exists in the resource cache (ex: CSS from a - // Svelte file) If it does, mark it for HMR replacement but DONT trigger a - // separate HMR update event. This is because a virtual resource doesn't - // actually exist on disk, so we need the main resource (the JS) to load - // first. Only after that happens will the CSS exist. - const virtualCssFileUrl = updatedUrl.replace(/.js$/, '.css'); - const virtualNode = - virtualCssFileUrl.includes(path.basename(fileLoc)) && - _hmrEngine.getEntry(`${virtualCssFileUrl}.proxy.js`); - if (virtualNode) { - _hmrEngine.markEntryForReplacement(virtualNode, true); - } - - // If the changed file exists on the page, trigger a new HMR update. - if (_hmrEngine.getEntry(updatedUrl)) { - updateOrBubble(updatedUrl, new Set()); - return; - } - - // Otherwise, reload the page if the file exists in our hot cache (which - // means that the file likely exists on the current page, but is not - // supported by HMR (HTML, image, etc)). - if (inMemoryBuildCache.has(getCacheKey(fileLoc, {isSSR: false, env: process.env.NODE_ENV}))) { - _hmrEngine.broadcastMessage({type: 'reload'}); - return; - } - } -} + // HMR Engine + const {hmrEngine, handleHmrUpdate} = config.devOptions.hmr + ? startHmrEngine(inMemoryBuildCache, server, config) + : {hmrEngine: undefined, handleHmrUpdate: undefined}; // Allow the user to hook into this callback, if they like (noop by default) let onFileChangeCallback: OnFileChangeCallback = () => {}; diff --git a/snowpack/src/dev/hmr.ts b/snowpack/src/dev/hmr.ts new file mode 100644 index 0000000000..53127ecef4 --- /dev/null +++ b/snowpack/src/dev/hmr.ts @@ -0,0 +1,99 @@ +import http from 'http'; +import http2 from 'http2'; +import path from 'path'; +import onProcessExit from 'signal-exit'; +import { FileBuilder } from '../build/file-builder'; +import { EsmHmrEngine } from '../hmr-server-engine'; +import { + SnowpackConfig +} from '../types'; +import { + getCacheKey, + hasExtension +} from '../util'; + + +export function startHmrEngine( + inMemoryBuildCache: Map, + server: http.Server | http2.Http2SecureServer | undefined, + config: SnowpackConfig, + ) { + const {hmrDelay} = config.devOptions; + const hmrPort = config.devOptions.hmrPort || config.devOptions.port; + const hmrEngineOptions = Object.assign( + {delay: hmrDelay}, + config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, + ); + const hmrEngine = new EsmHmrEngine(hmrEngineOptions); + onProcessExit(() => { + hmrEngine.disconnectAllClients(); + }); + + // Live Reload + File System Watching + function updateOrBubble(url: string, visited: Set) { + if (visited.has(url)) { + return; + } + const node = hmrEngine.getEntry(url); + const isBubbled = visited.size > 0; + if (node && node.isHmrEnabled) { + hmrEngine.broadcastMessage({type: 'update', url, bubbled: isBubbled}); + } + visited.add(url); + if (node && node.isHmrAccepted) { + // Found a boundary, no bubbling needed + } else if (node && node.dependents.size > 0) { + node.dependents.forEach((dep) => { + hmrEngine.markEntryForReplacement(node, true); + updateOrBubble(dep, visited); + }); + } else { + // We've reached the top, trigger a full page refresh + hmrEngine.broadcastMessage({type: 'reload'}); + } + } + + function handleHmrUpdate(fileLoc: string, originalUrl: string) { + // CSS files may be loaded directly in the client (not via JS import / .proxy.js) + // so send an "update" event to live update if thats the case. + if (hasExtension(originalUrl, '.css') && !hasExtension(originalUrl, '.module.css')) { + hmrEngine.broadcastMessage({type: 'update', url: originalUrl, bubbled: false}); + } + + // Append ".proxy.js" to Non-JS files to match their registered URL in the + // client app. + let updatedUrl = originalUrl; + if (!hasExtension(updatedUrl, '.js')) { + updatedUrl += '.proxy.js'; + } + + // Check if a virtual file exists in the resource cache (ex: CSS from a + // Svelte file) If it does, mark it for HMR replacement but DONT trigger a + // separate HMR update event. This is because a virtual resource doesn't + // actually exist on disk, so we need the main resource (the JS) to load + // first. Only after that happens will the CSS exist. + const virtualCssFileUrl = updatedUrl.replace(/.js$/, '.css'); + const virtualNode = + virtualCssFileUrl.includes(path.basename(fileLoc)) && + hmrEngine.getEntry(`${virtualCssFileUrl}.proxy.js`); + if (virtualNode) { + hmrEngine.markEntryForReplacement(virtualNode, true); + } + + // If the changed file exists on the page, trigger a new HMR update. + if (hmrEngine.getEntry(updatedUrl)) { + updateOrBubble(updatedUrl, new Set()); + return; + } + + // Otherwise, reload the page if the file exists in our hot cache (which + // means that the file likely exists on the current page, but is not + // supported by HMR (HTML, image, etc)). + if (inMemoryBuildCache.has(getCacheKey(fileLoc, {isSSR: false, env: process.env.NODE_ENV}))) { + hmrEngine.broadcastMessage({type: 'reload'}); + return; + } + } + + return {hmrEngine, handleHmrUpdate}; + } \ No newline at end of file diff --git a/snowpack/src/util.ts b/snowpack/src/util.ts index 40411af498..402d7d1ee2 100644 --- a/snowpack/src/util.ts +++ b/snowpack/src/util.ts @@ -41,6 +41,10 @@ export const HTML_STYLE_REGEX = /()(.*?)<\/style>/gims; export const CSS_REGEX = /@import\s*['"](.*?)['"];/gs; export const SVELTE_VUE_REGEX = /(]*>)(.*?)<\/script>/gims; +export function getCacheKey(fileLoc: string, {isSSR, env}) { + return `${fileLoc}?env=${env}&isSSR=${isSSR ? '1' : '0'}`; +} + /** * Like rimraf, but will fail if "dir" is outside of your configured build output directory. */ From c424ed3bbd692927ca918a0209f4f64d16815c86 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 23:38:26 -0800 Subject: [PATCH 25/40] small hmrengine optional fix --- snowpack/src/commands/build.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index a71baa4d41..e64a5d2739 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -227,11 +227,13 @@ export async function build(commandOptions: CommandOptions): Promise (onFileChangeCallback = callback), shutdown() { From 4f933483669e786a1afcaf37ca660efc540369f3 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 23:48:59 -0800 Subject: [PATCH 26/40] format --- esinstall/src/index.ts | 10 +-- snowpack/src/dev/hmr.ts | 176 +++++++++++++++++++--------------------- 2 files changed, 90 insertions(+), 96 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 81ab2c5343..74ce93ca49 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -87,11 +87,11 @@ function resolveWebDependency( dep: string, resolveOptions: {cwd: string; packageLookupFields: string[]}, ): DependencyLoc { - const loc = resolveEntrypoint(dep, resolveOptions); - return { - loc, - type: getWebDependencyType(loc), - }; + const loc = resolveEntrypoint(dep, resolveOptions); + return { + loc, + type: getWebDependencyType(loc), + }; } function generateEnvObject(userEnv: EnvVarReplacements): Object { diff --git a/snowpack/src/dev/hmr.ts b/snowpack/src/dev/hmr.ts index 53127ecef4..63dace5004 100644 --- a/snowpack/src/dev/hmr.ts +++ b/snowpack/src/dev/hmr.ts @@ -2,98 +2,92 @@ import http from 'http'; import http2 from 'http2'; import path from 'path'; import onProcessExit from 'signal-exit'; -import { FileBuilder } from '../build/file-builder'; -import { EsmHmrEngine } from '../hmr-server-engine'; -import { - SnowpackConfig -} from '../types'; -import { - getCacheKey, - hasExtension -} from '../util'; - +import {FileBuilder} from '../build/file-builder'; +import {EsmHmrEngine} from '../hmr-server-engine'; +import {SnowpackConfig} from '../types'; +import {getCacheKey, hasExtension} from '../util'; export function startHmrEngine( - inMemoryBuildCache: Map, - server: http.Server | http2.Http2SecureServer | undefined, - config: SnowpackConfig, - ) { - const {hmrDelay} = config.devOptions; - const hmrPort = config.devOptions.hmrPort || config.devOptions.port; - const hmrEngineOptions = Object.assign( - {delay: hmrDelay}, - config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, - ); - const hmrEngine = new EsmHmrEngine(hmrEngineOptions); - onProcessExit(() => { - hmrEngine.disconnectAllClients(); - }); - - // Live Reload + File System Watching - function updateOrBubble(url: string, visited: Set) { - if (visited.has(url)) { - return; - } - const node = hmrEngine.getEntry(url); - const isBubbled = visited.size > 0; - if (node && node.isHmrEnabled) { - hmrEngine.broadcastMessage({type: 'update', url, bubbled: isBubbled}); - } - visited.add(url); - if (node && node.isHmrAccepted) { - // Found a boundary, no bubbling needed - } else if (node && node.dependents.size > 0) { - node.dependents.forEach((dep) => { - hmrEngine.markEntryForReplacement(node, true); - updateOrBubble(dep, visited); - }); - } else { - // We've reached the top, trigger a full page refresh - hmrEngine.broadcastMessage({type: 'reload'}); - } + inMemoryBuildCache: Map, + server: http.Server | http2.Http2SecureServer | undefined, + config: SnowpackConfig, +) { + const {hmrDelay} = config.devOptions; + const hmrPort = config.devOptions.hmrPort || config.devOptions.port; + const hmrEngineOptions = Object.assign( + {delay: hmrDelay}, + config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, + ); + const hmrEngine = new EsmHmrEngine(hmrEngineOptions); + onProcessExit(() => { + hmrEngine.disconnectAllClients(); + }); + + // Live Reload + File System Watching + function updateOrBubble(url: string, visited: Set) { + if (visited.has(url)) { + return; + } + const node = hmrEngine.getEntry(url); + const isBubbled = visited.size > 0; + if (node && node.isHmrEnabled) { + hmrEngine.broadcastMessage({type: 'update', url, bubbled: isBubbled}); + } + visited.add(url); + if (node && node.isHmrAccepted) { + // Found a boundary, no bubbling needed + } else if (node && node.dependents.size > 0) { + node.dependents.forEach((dep) => { + hmrEngine.markEntryForReplacement(node, true); + updateOrBubble(dep, visited); + }); + } else { + // We've reached the top, trigger a full page refresh + hmrEngine.broadcastMessage({type: 'reload'}); + } + } + + function handleHmrUpdate(fileLoc: string, originalUrl: string) { + // CSS files may be loaded directly in the client (not via JS import / .proxy.js) + // so send an "update" event to live update if thats the case. + if (hasExtension(originalUrl, '.css') && !hasExtension(originalUrl, '.module.css')) { + hmrEngine.broadcastMessage({type: 'update', url: originalUrl, bubbled: false}); + } + + // Append ".proxy.js" to Non-JS files to match their registered URL in the + // client app. + let updatedUrl = originalUrl; + if (!hasExtension(updatedUrl, '.js')) { + updatedUrl += '.proxy.js'; + } + + // Check if a virtual file exists in the resource cache (ex: CSS from a + // Svelte file) If it does, mark it for HMR replacement but DONT trigger a + // separate HMR update event. This is because a virtual resource doesn't + // actually exist on disk, so we need the main resource (the JS) to load + // first. Only after that happens will the CSS exist. + const virtualCssFileUrl = updatedUrl.replace(/.js$/, '.css'); + const virtualNode = + virtualCssFileUrl.includes(path.basename(fileLoc)) && + hmrEngine.getEntry(`${virtualCssFileUrl}.proxy.js`); + if (virtualNode) { + hmrEngine.markEntryForReplacement(virtualNode, true); } - - function handleHmrUpdate(fileLoc: string, originalUrl: string) { - // CSS files may be loaded directly in the client (not via JS import / .proxy.js) - // so send an "update" event to live update if thats the case. - if (hasExtension(originalUrl, '.css') && !hasExtension(originalUrl, '.module.css')) { - hmrEngine.broadcastMessage({type: 'update', url: originalUrl, bubbled: false}); - } - - // Append ".proxy.js" to Non-JS files to match their registered URL in the - // client app. - let updatedUrl = originalUrl; - if (!hasExtension(updatedUrl, '.js')) { - updatedUrl += '.proxy.js'; - } - - // Check if a virtual file exists in the resource cache (ex: CSS from a - // Svelte file) If it does, mark it for HMR replacement but DONT trigger a - // separate HMR update event. This is because a virtual resource doesn't - // actually exist on disk, so we need the main resource (the JS) to load - // first. Only after that happens will the CSS exist. - const virtualCssFileUrl = updatedUrl.replace(/.js$/, '.css'); - const virtualNode = - virtualCssFileUrl.includes(path.basename(fileLoc)) && - hmrEngine.getEntry(`${virtualCssFileUrl}.proxy.js`); - if (virtualNode) { - hmrEngine.markEntryForReplacement(virtualNode, true); - } - - // If the changed file exists on the page, trigger a new HMR update. - if (hmrEngine.getEntry(updatedUrl)) { - updateOrBubble(updatedUrl, new Set()); - return; - } - - // Otherwise, reload the page if the file exists in our hot cache (which - // means that the file likely exists on the current page, but is not - // supported by HMR (HTML, image, etc)). - if (inMemoryBuildCache.has(getCacheKey(fileLoc, {isSSR: false, env: process.env.NODE_ENV}))) { - hmrEngine.broadcastMessage({type: 'reload'}); - return; - } + + // If the changed file exists on the page, trigger a new HMR update. + if (hmrEngine.getEntry(updatedUrl)) { + updateOrBubble(updatedUrl, new Set()); + return; } - - return {hmrEngine, handleHmrUpdate}; - } \ No newline at end of file + + // Otherwise, reload the page if the file exists in our hot cache (which + // means that the file likely exists on the current page, but is not + // supported by HMR (HTML, image, etc)). + if (inMemoryBuildCache.has(getCacheKey(fileLoc, {isSSR: false, env: process.env.NODE_ENV}))) { + hmrEngine.broadcastMessage({type: 'reload'}); + return; + } + } + + return {hmrEngine, handleHmrUpdate}; +} From 2145e292e3945d8e9089beee96f8fab142c539ed Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sat, 27 Feb 2021 23:48:11 -0800 Subject: [PATCH 27/40] wip: new dashboard output --- snowpack/package.json | 5 +- snowpack/src/commands/dev.ts | 37 +++--- snowpack/src/commands/paint.ts | 219 +++++++++++++++++++-------------- snowpack/src/logger.ts | 8 +- snowpack/src/sources/local.ts | 80 ++++++------ snowpack/src/types.ts | 3 +- yarn.lock | 5 + 7 files changed, 202 insertions(+), 155 deletions(-) diff --git a/snowpack/package.json b/snowpack/package.json index 8f5cd45bfd..5bab4e3f36 100644 --- a/snowpack/package.json +++ b/snowpack/package.json @@ -45,11 +45,12 @@ "vendor" ], "dependencies": { + "cli-spinners": "^2.5.0", "default-browser-id": "^2.0.0", "esbuild": "^0.8.7", "open": "^7.0.4", - "rollup": "^2.34.0", - "resolve": "^1.20.0" + "resolve": "^1.20.0", + "rollup": "^2.34.0" }, "optionalDependencies": { "fsevents": "^2.2.0" diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 5d4c296d9c..1d70c64dc4 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -15,7 +15,6 @@ import {performance} from 'perf_hooks'; import slash from 'slash'; import stream from 'stream'; import url from 'url'; -import util from 'util'; import zlib from 'zlib'; import {generateEnvModule, getMetaUrlPath, wrapImportProxy} from '../build/build-import-proxy'; import {FileBuilder} from '../build/file-builder'; @@ -40,7 +39,7 @@ import { isFsEventsEnabled, openInBrowser, } from '../util'; -import {getPort, paintDashboard, paintEvent} from './paint'; +import {getPort, startDashboard, paintEvent} from './paint'; export class OneToManyMap { readonly keyToValue = new Map(); readonly valueToKey = new Map(); @@ -270,28 +269,21 @@ export async function startServer( }; } + messageBus.on(paintEvent.SERVER_START, (info) => { + logger.info(`Server ready in ${info.startTimeMs}ms.`); + logger.info(`${colors.bold('Local:')} ${`${info.protocol}//${hostname}:${port}`}`); + if (info.remoteIp) { + logger.info(`${colors.bold('Network:')} ${`${info.protocol}//${info.remoteIp}:${port}`}`); + } + }); + if (config.devOptions.output === 'dashboard') { - // "dashboard": Pipe console methods to the logger, and then start the dashboard. - logger.debug(`attaching console.log listeners`); - console.log = (...args: [any, ...any[]]) => { - logger.info(util.format(...args)); - }; - console.warn = (...args: [any, ...any[]]) => { - logger.warn(util.format(...args)); - }; - console.error = (...args: [any, ...any[]]) => { - logger.error(util.format(...args)); - }; - paintDashboard(messageBus, config); - logger.debug(`dashboard started`); + startDashboard(messageBus, config); } else { // "stream": Log relevent events to the console. messageBus.on(paintEvent.WORKER_MSG, ({id, msg}) => { logger.info(msg.trim(), {name: id}); }); - messageBus.on(paintEvent.SERVER_START, (info) => { - logger.info(`Server started in ${info.startTimeMs}ms.`); - }); } const symlinkDirectories = new Set(); @@ -756,7 +748,10 @@ export async function startServer( // Watch src files async function onWatchEvent(fileLoc: string) { - logger.info(colors.cyan('File changed...')); + logger.info( + colors.cyan('File changed... ') + + colors.dim(path.relative(config.workspaceRoot || config.root, fileLoc)), + ); await onFileChangeCallback({filePath: fileLoc}); const updatedUrls = getUrlsForFile(fileLoc, config); if (updatedUrls) { @@ -838,7 +833,9 @@ export async function command(commandOptions: CommandOptions) { const pkgSource = getPackageSource(commandOptions.config.packageOptions.source); await pkgSource.prepare(commandOptions); await startServer(commandOptions); - logger.info(colors.cyan('watching for file changes...')); + if (commandOptions.config.devOptions.output !== 'dashboard') { + logger.info(colors.cyan('watching for file changes...')); + } } catch (err) { logger.error(err.message); logger.debug(err.stack); diff --git a/snowpack/src/commands/paint.ts b/snowpack/src/commands/paint.ts index 8cdee3289e..8f7aca1fdc 100644 --- a/snowpack/src/commands/paint.ts +++ b/snowpack/src/commands/paint.ts @@ -1,10 +1,12 @@ import detectPort from 'detect-port'; import {EventEmitter} from 'events'; import * as colors from 'kleur/colors'; +import util from 'util'; import path from 'path'; import readline from 'readline'; import {logger, LogRecord} from '../logger'; import {SnowpackConfig} from '../types'; +import spinners from 'cli-spinners'; const IS_FILE_CHANGED_MESSAGE = /File changed\.\.\./; @@ -73,34 +75,6 @@ export async function getPort(defaultPort: number): Promise { return bestAvailablePort; } -export function getServerInfoMessage( - {startTimeMs, port, protocol, hostname, remoteIp}: ServerInfo, - isBuilding = false, -) { - let output = ''; - const isServerStarted = startTimeMs > 0 && port > 0 && protocol; - if (isServerStarted) { - output += ` ${colors.bold(colors.cyan(`${protocol}//${hostname}:${port}`))}`; - if (remoteIp) { - output += `${colors.cyan(` • `)}${colors.bold( - colors.cyan(`${protocol}//${remoteIp}:${port}`), - )}`; - } - output += '\n'; - output += colors.dim( - // Not to hide slow startup times, but likely there were extraneous factors (prompts, etc.) where the speed isn’t accurate - startTimeMs < 1000 ? ` Server started in ${startTimeMs}ms.` : ` Server started.`, - ); - if (isBuilding) { - output += colors.dim(` Building...`); - } - output += '\n\n'; - } else { - output += colors.dim(` Server starting…`) + '\n\n'; - } - return output; -} - interface ServerInfo { port: number; hostname: string; @@ -116,87 +90,148 @@ interface WorkerState { } const WORKER_BASE_STATE: WorkerState = {done: false, error: null, output: ''}; -export function paintDashboard(bus: EventEmitter, config: SnowpackConfig) { - let serverInfo: ServerInfo; +export function startDashboard(bus: EventEmitter, config: SnowpackConfig) { const allWorkerStates: Record = {}; - const allFileBuilds = new Set(); + let spinnerFrame = 0; - for (const plugin of config.plugins.map((p) => p.name)) { - allWorkerStates[plugin] = {...WORKER_BASE_STATE}; - } + // "dashboard": Pipe console methods to the logger, and then start the dashboard. + logger.debug(`attaching console.log listeners`); + console.log = (...args: [any, ...any[]]) => { + logger.info(util.format(...args)); + }; + console.warn = (...args: [any, ...any[]]) => { + logger.warn(util.format(...args)); + }; + console.error = (...args: [any, ...any[]]) => { + logger.error(util.format(...args)); + }; - function setupWorker(id: string) { - if (!allWorkerStates[id]) { - allWorkerStates[id] = {...WORKER_BASE_STATE}; - } + function paintDashboard() { + let dashboardMsg = ''; + // Header + dashboardMsg += + '\n' + colors.cyan(`${spinners.dots.frames[spinnerFrame]} watching for file changes...`); + // Worker Dashboards + // for (const [script, workerState] of Object.entries(allWorkerStates)) { + // if (!workerState.output) { + // continue; + // } + // const colorsFn = Array.isArray(workerState.error) ? colors.red : colors.reset; + // dashboardMsg += `${colorsFn(colors.underline(colors.bold('▼ ' + script)))}\n\n`; + // dashboardMsg += ' ' + workerState.output.trim().replace(/\n/gm, '\n '); + // dashboardMsg += '\n\n'; + // } + + const lines = dashboardMsg.split('\n').length; + return {msg: dashboardMsg, lines}; } - function repaint() { - // Clear Page - process.stdout.write(process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'); - // Header - process.stdout.write(`${colors.bold(`snowpack`)}\n\n`); - // Server Stats - serverInfo && process.stdout.write(getServerInfoMessage(serverInfo, allFileBuilds.size > 0)); - // Console Output - const history = logger.getHistory(); - if (history.length) { - process.stdout.write(`${colors.underline(colors.bold('▼ Console'))}\n`); - process.stdout.write(summarizeHistory(history)); - process.stdout.write('\n\n'); + function clearDashboard(num, msg?) { + // Clear Info Line + while (num > 0) { + process.stdout.clearLine(0); + process.stdout.cursorTo(0); + process.stdout.moveCursor(0, -1); + num--; } - // Worker Dashboards - for (const [script, workerState] of Object.entries(allWorkerStates)) { - if (!workerState.output) { - continue; - } - const colorsFn = Array.isArray(workerState.error) ? colors.red : colors.reset; - process.stdout.write(`${colorsFn(colors.underline(colors.bold('▼ ' + script)))}\n\n`); - process.stdout.write(' ' + workerState.output.trim().replace(/\n/gm, '\n ')); - process.stdout.write('\n\n'); + if (!msg || cleanTimestamp(msg) !== lastMsg) { + process.stdout.moveCursor(0, 1); } } - bus.on(paintEvent.BUILD_FILE, ({id, isBuilding}) => { - if (isBuilding) { - allFileBuilds.add(path.relative(config.root, id)); + let lastMsg: string = '\0'; + let lastMsgCount = 1; + function addTimestamp(msg: string): string { + let counter = ''; + if (cleanTimestamp(msg) === lastMsg) { + lastMsgCount++; + counter = ` (x${lastMsgCount})`; } else { - allFileBuilds.delete(path.relative(config.root, id)); + lastMsgCount = 1; } - repaint(); - }); + return msg + counter; + } + + function cleanTimestamp(msg: string): string { + return msg.replace(/^.*\]/, ''); + } + + // bus.on(paintEvent.BUILD_FILE, ({id, isBuilding}) => { + // if (isBuilding) { + // allFileBuilds.add(path.relative(config.root, id)); + // } else { + // allFileBuilds.delete(path.relative(config.root, id)); + // } + // repaint(); + // }); bus.on(paintEvent.WORKER_MSG, ({id, msg}) => { - setupWorker(id); - allWorkerStates[id].output += msg; - repaint(); - }); - bus.on(paintEvent.WORKER_COMPLETE, ({id, error}) => { - allWorkerStates[id].done = true; - allWorkerStates[id].error = allWorkerStates[id].error || error; - repaint(); - }); - bus.on(paintEvent.WORKER_RESET, ({id}) => { - allWorkerStates[id] = {...WORKER_BASE_STATE}; - repaint(); - }); - bus.on(paintEvent.SERVER_START, (info: ServerInfo) => { - serverInfo = info; - repaint(); + const cleanedMsg = msg.trim(); + if (!cleanedMsg) { + return; + } + for (const individualMsg of cleanedMsg.split('\n')) { + logger.info(individualMsg, {name: id}); + } }); + // bus.on(paintEvent.WORKER_COMPLETE, ({id, error}) => { + // allWorkerStates[id].done = true; + // allWorkerStates[id].error = allWorkerStates[id].error || error; + // repaint(); + // }); + // bus.on(paintEvent.WORKER_RESET, ({id}) => { + // allWorkerStates[id] = {...WORKER_BASE_STATE}; + // repaint(); + // }); + // bus.on(paintEvent.SERVER_START, (info: ServerInfo) => { + // serverInfo = info; + // }); - // replace logging behavior with repaint (note: messages are retrieved later, with logger.getHistory()) - logger.on('debug', () => { - repaint(); + // // replace logging behavior with repaint (note: messages are retrieved later, with logger.getHistory()) + let lines = 0; + logger.on('debug', (msg) => { + clearDashboard(lines, msg); + process.stdout.write(addTimestamp(msg)); + lastMsg = cleanTimestamp(msg); + process.stdout.write('\n'); + const result = paintDashboard(); + process.stdout.write(result.msg); + lines = result.lines; }); - logger.on('info', () => { - repaint(); + logger.on('info', (msg) => { + clearDashboard(lines, msg); + process.stdout.write(addTimestamp(msg)); + lastMsg = cleanTimestamp(msg); + process.stdout.write('\n'); + const result = paintDashboard(); + process.stdout.write(result.msg); + lines = result.lines; }); - logger.on('warn', () => { - repaint(); + logger.on('warn', (msg) => { + clearDashboard(lines, msg); + process.stdout.write(addTimestamp(msg)); + lastMsg = cleanTimestamp(msg); + process.stdout.write('\n'); + const result = paintDashboard(); + process.stdout.write(result.msg); + lines = result.lines; }); - logger.on('error', () => { - repaint(); + logger.on('error', (msg) => { + clearDashboard(lines, msg); + process.stdout.write(addTimestamp(msg)); + lastMsg = cleanTimestamp(msg); + process.stdout.write('\n'); + const result = paintDashboard(); + process.stdout.write(result.msg); + lines = result.lines; }); - repaint(); + setInterval(() => { + spinnerFrame = (spinnerFrame + 1) % spinners.dots.frames.length; + clearDashboard(lines); + const result = paintDashboard(); + process.stdout.write(result.msg); + lines = result.lines; + }, 1000); + logger.debug(`dashboard started`); + // repaint(); } diff --git a/snowpack/src/logger.ts b/snowpack/src/logger.ts index ec5afebc09..05535d0c6a 100644 --- a/snowpack/src/logger.ts +++ b/snowpack/src/logger.ts @@ -57,7 +57,13 @@ class SnowpackLogger { let text = message; if (level === 'warn') text = colors.yellow(text); if (level === 'error') text = colors.red(text); - const log = `${colors.dim(`[${name}]`)} ${text}`; + const time = new Date(); + const log = `${colors.dim( + `[${String(time.getHours() + 1).padStart(2, '0')}:${String(time.getMinutes() + 1).padStart( + 2, + '0', + )}:${String(time.getSeconds()).padStart(2, '0')}]`, + )} ${colors.dim(`[${name}]`)} ${text}`; // add to log history and remove old logs to keep memory low const lastHistoryItem = this.history[this.history.length - 1]; diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 28f9183304..44e2ddc4e0 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -189,13 +189,11 @@ export default { ); } else { logger.info( - `${colors.bold( - 'Welcome to Snowpack!', - )} Because this is your first time running this project${ - process.env.NODE_ENV === 'test' ? ` (mode: test)` : `` - }, \n` + - 'Snowpack needs to prepare your dependencies. This is a one-time step and the results \n' + - 'will be reused for the lifetime of your project. Please wait while we prepare...', + `${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 getInstallTargets( @@ -203,7 +201,7 @@ export default { config.packageOptions.source === 'local' ? config.packageOptions.knownEntrypoints : [], ); if (installTargets.length === 0) { - logger.info('No dependencies detected. Set up complete!'); + logger.info('No dependencies detected. Ready!'); return; } await Promise.all( @@ -211,9 +209,10 @@ export default { return this.resolvePackageImport(path.join(config.root, 'package.json'), spec, config); }), ); + await inProgressBuilds.onIdle(); await mkdirp(path.dirname(installDirectoryHashLoc)); await fs.writeFile(installDirectoryHashLoc, 'v1', 'utf-8'); - logger.info(colors.bold('Set up complete!')); + logger.info(colors.bold('Ready! Starting up...')); return; }, @@ -222,6 +221,7 @@ export default { spec: string, _config: SnowpackConfig, importMap?: ImportMap, + depth = 0, ) { config = config || _config; @@ -276,10 +276,9 @@ export default { const packageVersion = packageManifest.version || 'unknown'; const installDest = path.join(DEV_DEPENDENCIES_DIR, packageName + '@' + packageVersion); - let isNew = !allKnownSpecs.has(spec); allKnownSpecs.add(spec); - const [newImportMap, loadedFile] = await inProgressBuilds.add( - async (): Promise<[ImportMap, Buffer]> => { + const newImportMap = await inProgressBuilds.add( + async (): Promise => { // Look up the import map of the already-installed package. // If spec already exists, then this import map is valid. const existingImportMapLoc = path.join(installDest, 'import-map.json'); @@ -288,11 +287,12 @@ export default { JSON.parse(await fs.readFile(existingImportMapLoc, 'utf8')); if (existingImportMap && existingImportMap.imports[spec]) { logger.debug(spec + ' CACHED! (already exists)'); - const dependencyFileLoc = path.join(installDest, existingImportMap.imports[spec]); - return [existingImportMap, await fs.readFile(dependencyFileLoc!)]; + return existingImportMap; } // Otherwise, kick off a new build to generate a fresh import map. - logger.info(colors.yellow(`⦿ ${spec}`)); + logger.info( + colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName}@${packageVersion}`), + ); const installTargets = [...allKnownSpecs].filter( (spec) => spec === _packageName || spec.startsWith(_packageName + '/'), @@ -353,9 +353,11 @@ export default { installTargets, installOptions, }); - logger.debug(colors.yellow(`⦿ ${spec} DONE`)); + logger.debug(colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName} DONE`)); if (needsSsrBuild) { - logger.info(colors.yellow(`⦿ ${spec} (ssr)`)); + logger.info( + colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName}@${packageVersion} (ssr)`), + ); await installPackages({ config, isDev: true, @@ -366,7 +368,7 @@ export default { dest: installDest + '-ssr', }, }); - logger.debug(colors.yellow(`⦿ ${spec} (ssr) DONE`)); + logger.debug(colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName} (ssr) DONE`)); } if (isSymlink) { logger.warn( @@ -377,31 +379,31 @@ export default { ); } const dependencyFileLoc = path.join(installDest, newImportMap.imports[spec]); - return [newImportMap, await fs.readFile(dependencyFileLoc!)]; + const loadedFile = await fs.readFile(dependencyFileLoc!); + if (isJavaScript(dependencyFileLoc)) { + const packageImports = new Set(); + const code = loadedFile.toString('utf8'); + for (const imp of await scanCodeImportsExports(code)) { + const spec = code.substring(imp.s, imp.e); + if (isRemoteUrl(spec)) { + continue; + } + if (spec.startsWith('/') || spec.startsWith('./') || spec.startsWith('../')) { + continue; + } + packageImports.add(spec); + } + + [...packageImports].map((packageImport) => + this.resolvePackageImport(entrypoint, packageImport, config, undefined, depth + 1), + ); + } + return newImportMap; }, + {priority: depth}, ); const dependencyFileLoc = path.join(installDest, newImportMap.imports[spec]); - if (isNew && isJavaScript(dependencyFileLoc)) { - await inProgressBuilds.onIdle(); - const packageImports = new Set(); - const code = loadedFile.toString('utf8'); - for (const imp of await scanCodeImportsExports(code)) { - const spec = code.substring(imp.s, imp.e); - if (isRemoteUrl(spec)) { - continue; - } - if (spec.startsWith('/') || spec.startsWith('./') || spec.startsWith('../')) { - continue; - } - packageImports.add(spec); - } - await Promise.all( - [...packageImports].map((packageImport) => - this.resolvePackageImport(entrypoint, packageImport, config), - ), - ); - } // Flatten the import map value into a resolved, public import ID. // ex: "./react.js" -> "react.v17.0.1.js" diff --git a/snowpack/src/types.ts b/snowpack/src/types.ts index 95262213ec..e3b6f3e675 100644 --- a/snowpack/src/types.ts +++ b/snowpack/src/types.ts @@ -54,7 +54,7 @@ export interface LoadUrlOptions { } export interface SnowpackDevServer { port: number; - hmrEngine: EsmHmrEngine; + hmrEngine?: EsmHmrEngine; loadUrl: { (reqUrl: string, opt?: (LoadUrlOptions & {encoding?: undefined}) | undefined): Promise< LoadResult @@ -366,6 +366,7 @@ export interface PackageSource { spec: string, config: SnowpackConfig, importMap?: ImportMap, + depth?: number, ): Promise; /** Modify the build install config for optimized build install. */ modifyBuildInstallOptions(options: { diff --git a/yarn.lock b/yarn.lock index 545def1de0..14202cfbaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5149,6 +5149,11 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-spinners@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" + integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== + cli-width@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" From 17f171e157f90cd61f4e1d1438a26d3fe781f3fa Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 28 Feb 2021 11:40:40 -0800 Subject: [PATCH 28/40] new dashboard output --- .../rollup-plugin-catch-unresolved.ts | 5 + snowpack/src/build/file-urls.ts | 2 +- snowpack/src/commands/build.ts | 22 ++- snowpack/src/commands/dev.ts | 20 +-- snowpack/src/commands/paint.ts | 135 +++--------------- snowpack/src/sources/local-install.ts | 4 +- snowpack/src/sources/local.ts | 40 +++--- snowpack/src/types.ts | 2 +- 8 files changed, 74 insertions(+), 156 deletions(-) diff --git a/esinstall/src/rollup-plugins/rollup-plugin-catch-unresolved.ts b/esinstall/src/rollup-plugins/rollup-plugin-catch-unresolved.ts index 12fba2805b..cabff2a919 100644 --- a/esinstall/src/rollup-plugins/rollup-plugin-catch-unresolved.ts +++ b/esinstall/src/rollup-plugins/rollup-plugin-catch-unresolved.ts @@ -19,6 +19,11 @@ export function rollupPluginCatchUnresolved(): Plugin { id: importer, message: `Module "${id}" (Node.js built-in) is not available in the browser. Run Snowpack with --polyfill-node to fix.`, }); + } else if (id.startsWith('./') || id.startsWith('../')) { + this.warn({ + id: importer, + message: `Import "${id}" could not be resolved from file.`, + }); } else { this.warn({ id: importer, diff --git a/snowpack/src/build/file-urls.ts b/snowpack/src/build/file-urls.ts index 88ba4b292b..6ea37b0b86 100644 --- a/snowpack/src/build/file-urls.ts +++ b/snowpack/src/build/file-urls.ts @@ -80,7 +80,7 @@ export function getUrlsForFile(fileLoc: string, config: SnowpackConfig): undefin path.posix.join( config.buildOptions.metaUrlPath, 'link', - slash(path.relative(config.workspaceRoot!, u)), + slash(path.relative((config.workspaceRoot as string), u)), ), ); } diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index e64a5d2739..73a0418a6b 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -171,14 +171,14 @@ export async function build(commandOptions: CommandOptions): Promise (onFileChangeCallback = callback), @@ -250,7 +245,7 @@ export async function build(commandOptions: CommandOptions): Promise { @@ -272,6 +267,9 @@ export async function build(commandOptions: CommandOptions): Promise { - logger.info(`Server ready in ${info.startTimeMs}ms.`); - logger.info(`${colors.bold('Local:')} ${`${info.protocol}//${hostname}:${port}`}`); + logger.info(colors.green(`Server started in ${info.startTimeMs}ms.`)); + logger.info(`${colors.green('Local:')} ${`${info.protocol}//${hostname}:${port}`}`); if (info.remoteIp) { - logger.info(`${colors.bold('Network:')} ${`${info.protocol}//${info.remoteIp}:${port}`}`); + logger.info(`${colors.green('Network:')} ${`${info.protocol}//${info.remoteIp}:${port}`}`); } }); - if (config.devOptions.output === 'dashboard') { + if (config.devOptions.output === 'dashboard' && process.stdout.isTTY) { startDashboard(messageBus, config); } else { // "stream": Log relevent events to the console. @@ -462,10 +462,10 @@ export async function startServer( // 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 (reqPath.startsWith(PACKAGE_LINK_PATH_PREFIX)) { + if (config.workspaceRoot && reqPath.startsWith(PACKAGE_LINK_PATH_PREFIX)) { const symlinkResourceUrl = reqPath.substr(PACKAGE_LINK_PATH_PREFIX.length); const symlinkResourceLoc = path.resolve( - config.workspaceRoot!, + (config.workspaceRoot as string), process.platform === 'win32' ? symlinkResourceUrl.replace(/\//g, '\\') : symlinkResourceUrl, ); const symlinkResourceDirectory = path.dirname(symlinkResourceLoc); @@ -495,7 +495,7 @@ export async function startServer( path.posix.join( config.buildOptions.metaUrlPath, 'link', - slash(path.relative(config.workspaceRoot!, u)), + slash(path.relative((config.workspaceRoot as string), u)), ), ), ); @@ -749,8 +749,7 @@ export async function startServer( // Watch src files async function onWatchEvent(fileLoc: string) { logger.info( - colors.cyan('File changed... ') + - colors.dim(path.relative(config.workspaceRoot || config.root, fileLoc)), + colors.cyan('File changed: ') + path.relative(config.workspaceRoot || config.root, fileLoc), ); await onFileChangeCallback({filePath: fileLoc}); const updatedUrls = getUrlsForFile(fileLoc, config); @@ -790,6 +789,9 @@ export async function startServer( watcher.on('change', (fileLoc) => { onWatchEvent(fileLoc); }); + if (config.devOptions.output !== 'dashboard' || !process.stdout.isTTY) { + logger.info(colors.cyan('watching for file changes...')); + } } // Open the user's browser (ignore if failed) diff --git a/snowpack/src/commands/paint.ts b/snowpack/src/commands/paint.ts index 8f7aca1fdc..93d885a82d 100644 --- a/snowpack/src/commands/paint.ts +++ b/snowpack/src/commands/paint.ts @@ -1,34 +1,12 @@ +import spinners from 'cli-spinners'; import detectPort from 'detect-port'; import {EventEmitter} from 'events'; import * as colors from 'kleur/colors'; -import util from 'util'; -import path from 'path'; import readline from 'readline'; -import {logger, LogRecord} from '../logger'; +import util from 'util'; +import {logger} from '../logger'; import {SnowpackConfig} from '../types'; -import spinners from 'cli-spinners'; - -const IS_FILE_CHANGED_MESSAGE = /File changed\.\.\./; -/** Convert a logger's history into the proper dev console format. */ -function summarizeHistory(history: readonly LogRecord[]): string { - // Note: history array can get long over time. Performance matters here! - return history.reduce((historyString, record) => { - let line; - // We want to summarize common repeat "file changed" events to reduce noise. - // All other logs should be included verbatim, with all repeats added. - if (record.count === 1) { - line = record.val; - } else if (IS_FILE_CHANGED_MESSAGE.test(record.val)) { - line = record.val + colors.green(` [x${record.count}]`); - } else { - line = Array(record.count).fill(record.val).join('\n'); - } - // Note: this includes an extra '\n' character at the start. - // Fine for our use-case, but be aware. - return historyString + '\n' + line; - }, ''); -} export const paintEvent = { BUILD_FILE: 'BUILD_FILE', LOAD_ERROR: 'LOAD_ERROR', @@ -75,23 +53,7 @@ export async function getPort(defaultPort: number): Promise { return bestAvailablePort; } -interface ServerInfo { - port: number; - hostname: string; - protocol: string; - startTimeMs: number; - remoteIp?: string; -} - -interface WorkerState { - done: boolean; - error: null | Error; - output: string; -} -const WORKER_BASE_STATE: WorkerState = {done: false, error: null, output: ''}; - -export function startDashboard(bus: EventEmitter, config: SnowpackConfig) { - const allWorkerStates: Record = {}; +export function startDashboard(bus: EventEmitter, _config: SnowpackConfig) { let spinnerFrame = 0; // "dashboard": Pipe console methods to the logger, and then start the dashboard. @@ -107,21 +69,9 @@ export function startDashboard(bus: EventEmitter, config: SnowpackConfig) { }; function paintDashboard() { - let dashboardMsg = ''; - // Header - dashboardMsg += - '\n' + colors.cyan(`${spinners.dots.frames[spinnerFrame]} watching for file changes...`); - // Worker Dashboards - // for (const [script, workerState] of Object.entries(allWorkerStates)) { - // if (!workerState.output) { - // continue; - // } - // const colorsFn = Array.isArray(workerState.error) ? colors.red : colors.reset; - // dashboardMsg += `${colorsFn(colors.underline(colors.bold('▼ ' + script)))}\n\n`; - // dashboardMsg += ' ' + workerState.output.trim().replace(/\n/gm, '\n '); - // dashboardMsg += '\n\n'; - // } - + let dashboardMsg = colors.cyan( + `${spinners.dots.frames[spinnerFrame]} watching for file changes...`, + ); const lines = dashboardMsg.split('\n').length; return {msg: dashboardMsg, lines}; } @@ -145,7 +95,7 @@ export function startDashboard(bus: EventEmitter, config: SnowpackConfig) { let counter = ''; if (cleanTimestamp(msg) === lastMsg) { lastMsgCount++; - counter = ` (x${lastMsgCount})`; + counter = colors.yellow(` (x${lastMsgCount})`); } else { lastMsgCount = 1; } @@ -156,14 +106,6 @@ export function startDashboard(bus: EventEmitter, config: SnowpackConfig) { return msg.replace(/^.*\]/, ''); } - // bus.on(paintEvent.BUILD_FILE, ({id, isBuilding}) => { - // if (isBuilding) { - // allFileBuilds.add(path.relative(config.root, id)); - // } else { - // allFileBuilds.delete(path.relative(config.root, id)); - // } - // repaint(); - // }); bus.on(paintEvent.WORKER_MSG, ({id, msg}) => { const cleanedMsg = msg.trim(); if (!cleanedMsg) { @@ -173,65 +115,30 @@ export function startDashboard(bus: EventEmitter, config: SnowpackConfig) { logger.info(individualMsg, {name: id}); } }); - // bus.on(paintEvent.WORKER_COMPLETE, ({id, error}) => { - // allWorkerStates[id].done = true; - // allWorkerStates[id].error = allWorkerStates[id].error || error; - // repaint(); - // }); - // bus.on(paintEvent.WORKER_RESET, ({id}) => { - // allWorkerStates[id] = {...WORKER_BASE_STATE}; - // repaint(); - // }); - // bus.on(paintEvent.SERVER_START, (info: ServerInfo) => { - // serverInfo = info; - // }); - // // replace logging behavior with repaint (note: messages are retrieved later, with logger.getHistory()) - let lines = 0; - logger.on('debug', (msg) => { - clearDashboard(lines, msg); - process.stdout.write(addTimestamp(msg)); - lastMsg = cleanTimestamp(msg); - process.stdout.write('\n'); - const result = paintDashboard(); - process.stdout.write(result.msg); - lines = result.lines; - }); - logger.on('info', (msg) => { - clearDashboard(lines, msg); - process.stdout.write(addTimestamp(msg)); - lastMsg = cleanTimestamp(msg); - process.stdout.write('\n'); - const result = paintDashboard(); - process.stdout.write(result.msg); - lines = result.lines; - }); - logger.on('warn', (msg) => { - clearDashboard(lines, msg); - process.stdout.write(addTimestamp(msg)); - lastMsg = cleanTimestamp(msg); - process.stdout.write('\n'); - const result = paintDashboard(); - process.stdout.write(result.msg); - lines = result.lines; - }); - logger.on('error', (msg) => { - clearDashboard(lines, msg); + let currentDashboardHeight = 1; + + function onLog(msg: string) { + clearDashboard(currentDashboardHeight, msg); process.stdout.write(addTimestamp(msg)); lastMsg = cleanTimestamp(msg); process.stdout.write('\n'); const result = paintDashboard(); process.stdout.write(result.msg); - lines = result.lines; - }); + currentDashboardHeight = result.lines; + } + logger.on('debug', onLog); + logger.on('info', onLog); + logger.on('warn', onLog); + logger.on('error', onLog); setInterval(() => { spinnerFrame = (spinnerFrame + 1) % spinners.dots.frames.length; - clearDashboard(lines); + clearDashboard(currentDashboardHeight); const result = paintDashboard(); process.stdout.write(result.msg); - lines = result.lines; + currentDashboardHeight = result.lines; }, 1000); + logger.debug(`dashboard started`); - // repaint(); } diff --git a/snowpack/src/sources/local-install.ts b/snowpack/src/sources/local-install.ts index 40e2c75400..02639c5ab5 100644 --- a/snowpack/src/sources/local-install.ts +++ b/snowpack/src/sources/local-install.ts @@ -33,10 +33,10 @@ export async function installPackages({ } const loggerName = installTargets.length === 1 - ? `prepare:${ + ? `esinstall:${ typeof installTargets[0] === 'string' ? installTargets[0] : installTargets[0].specifier }` - : `prepare`; + : `esinstall`; let needsSsrBuild = false; const finalResult = await install(installTargets, { diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 44e2ddc4e0..b1cb252011 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -74,6 +74,7 @@ const allPackageImports: Record = {}; const allSymlinkImports: Record = {}; const allKnownSpecs = new Set(); const inProgressBuilds = new PQueue({concurrency: 1}); +let hasWorkspaceWarningFired = false; export function getLinkedUrl(builtUrl: string) { return allSymlinkImports[builtUrl]; @@ -252,6 +253,14 @@ export default { ); allSymlinkImports[builtEntrypointUrl] = entrypoint; return path.posix.join(config.buildOptions.metaUrlPath, 'link', builtEntrypointUrl); + } else if (isSymlink && config.workspaceRoot !== false && !hasWorkspaceWarningFired) { + hasWorkspaceWarningFired = true; + 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) { @@ -281,18 +290,22 @@ export default { async (): Promise => { // 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, ' ')); + const packageFormatted = spec + colors.dim('@' + packageVersion); const existingImportMapLoc = path.join(installDest, 'import-map.json'); const existingImportMap = (await fs.stat(existingImportMapLoc).catch(() => null)) && JSON.parse(await fs.readFile(existingImportMapLoc, 'utf8')); if (existingImportMap && existingImportMap.imports[spec]) { - logger.debug(spec + ' CACHED! (already exists)'); + if (depth > 0) { + logger.info(`${lineBullet} ${packageFormatted} ${colors.dim(`(dedupe)`)}`); + } else { + logger.debug(`${lineBullet} ${packageFormatted} ${colors.dim(`(dedupe)`)}`); + } return existingImportMap; } // Otherwise, kick off a new build to generate a fresh import map. - logger.info( - colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName}@${packageVersion}`), - ); + logger.info(`${lineBullet} ${packageFormatted}`); const installTargets = [...allKnownSpecs].filter( (spec) => spec === _packageName || spec.startsWith(_packageName + '/'), @@ -353,11 +366,9 @@ export default { installTargets, installOptions, }); - logger.debug(colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName} DONE`)); + logger.debug(`${lineBullet} ${packageFormatted} DONE`); if (needsSsrBuild) { - logger.info( - colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName}@${packageVersion} (ssr)`), - ); + logger.info(`${lineBullet} ${packageFormatted} ${colors.dim(`(ssr)`)}`); await installPackages({ config, isDev: true, @@ -368,15 +379,7 @@ export default { dest: installDest + '-ssr', }, }); - logger.debug(colors.yellow(`${''.padStart(depth + 1, '-')} ${packageName} (ssr) DONE`)); - } - if (isSymlink) { - logger.warn( - colors.bold(`Locally linked package detected outside of project root.\n`) + - `If you are working in a workspace/monorepo, set your snowpack.config.js "workspaceRoot"\n` + - `to the workspace directory to take advantage of fast HMR updates for linked packages.\n` + - `Otherwise, this package will be cached until its package.json "version" changes.`, - ); + logger.debug(`${lineBullet} ${packageFormatted} (ssr) DONE`); } const dependencyFileLoc = path.join(installDest, newImportMap.imports[spec]); const loadedFile = await fs.readFile(dependencyFileLoc!); @@ -397,6 +400,9 @@ export default { [...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; }, diff --git a/snowpack/src/types.ts b/snowpack/src/types.ts index e3b6f3e675..0e6f74471f 100644 --- a/snowpack/src/types.ts +++ b/snowpack/src/types.ts @@ -242,7 +242,7 @@ export interface PackageSourceRemote { // interface this library uses internally export interface SnowpackConfig { root: string; - workspaceRoot?: string; + workspaceRoot?: string | false; extends?: string; exclude: string[]; mount: Record; From c67883c26384da3a73ba88cf52501c30209bcbe2 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 28 Feb 2021 12:03:08 -0800 Subject: [PATCH 29/40] add never peer packages to bundle common polyfills --- snowpack/src/sources/local.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index b1cb252011..c7c6f4611c 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -43,6 +43,16 @@ const PROJECT_CACHE_DIR = const DEV_DEPENDENCIES_DIR = path.join(PROJECT_CACHE_DIR, process.env.NODE_ENV || 'development'); +const NEVER_PEER_PACKAGES: string[] = [ + '@babel/runtime', + '@babel/runtime-corejs3', + 'babel-runtime', + 'dom-helpers', + 'es-abstract', + 'node-fetch', + 'whatwg-fetch', +]; + function getRootPackageDirectory(loc: string) { const parts = loc.split('node_modules'); if (parts.length === 1) { @@ -315,7 +325,7 @@ export default { ...Object.keys(packageManifest.dependencies || {}), ...Object.keys(packageManifest.devDependencies || {}), ...Object.keys(packageManifest.peerDependencies || {}), - ].filter((ext) => ext !== _packageName); + ].filter((ext) => ext !== _packageName && !NEVER_PEER_PACKAGES.includes(ext)); function getMemoizedResolveDependencyManifest() { const results = {}; From 510e0fe03e4d19b8d5f3085ae1be8f68117ff149 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 28 Feb 2021 12:36:16 -0800 Subject: [PATCH 30/40] add proxy import support for cross-package imports --- esinstall/src/index.ts | 1 - snowpack/src/sources/local.ts | 18 +++++++++++++----- .../reference/common-error-details.md | 13 +++++++++++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 74ce93ca49..2ca2aabd32 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -354,7 +354,6 @@ ${colors.dim( rollupPluginCommonjs({ extensions: ['.js', '.cjs'], esmExternals: (id) => - !namedExports.some((packageName) => isImportOfPackage(id, packageName)) && Array.isArray(externalEsm) ? externalEsm.some((packageName) => isImportOfPackage(id, packageName)) : (externalEsm as Function)(id), diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index c7c6f4611c..0395ee6e5b 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -156,13 +156,21 @@ export default { return await this.resolvePackageImport(entrypoint, spec, config); }; packageCode = await transformFileImports({type, contents: packageCode}, async (spec) => { - const resolvedSpec = await resolveImport(spec); + let resolvedImportUrl = await resolveImport(spec); + const importExtName = path.posix.extname(resolvedImportUrl); + const isProxyImport = importExtName && importExtName !== '.js' && importExtName !== '.mjs'; + if (config.buildOptions.resolveProxyImports && isProxyImport) { + resolvedImportUrl = resolvedImportUrl + '.proxy.js'; + } imports.push( createInstallTarget( - path.resolve(path.posix.join(config.buildOptions.metaUrlPath, 'pkg', id), resolvedSpec), + path.resolve( + path.posix.join(config.buildOptions.metaUrlPath, 'pkg', id), + resolvedImportUrl, + ), ), ); - return resolvedSpec; + return resolvedImportUrl; }); return {contents: packageCode, imports}; }, @@ -300,7 +308,7 @@ export default { async (): Promise => { // 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, ' ')); + const lineBullet = colors.dim(depth === 0 ? '+' : '└──'.padStart(depth * 2 + 1, ' ')); const packageFormatted = spec + colors.dim('@' + packageVersion); const existingImportMapLoc = path.join(installDest, 'import-map.json'); const existingImportMap = @@ -355,7 +363,7 @@ export default { _packageName += '/' + specParts.shift(); } const [, result] = resolveDependencyManifest(_packageName); - return !result || !!(result.module || result.exports); + return !result || !!(result.module || result.exports || result.type === 'module'); }, }; if (config.packageOptions.source === 'local') { diff --git a/www/_template/reference/common-error-details.md b/www/_template/reference/common-error-details.md index cbe13f331b..a96e86063e 100644 --- a/www/_template/reference/common-error-details.md +++ b/www/_template/reference/common-error-details.md @@ -28,9 +28,18 @@ If you are using TypeScript, this error could occur if you are importing somethi **To solve:** Make sure to use `import type { MyInterfaceName }` instead. -This error could also appear if you importing named exports from older, non-ESM npm packages. We do our best to statically analyze legacy packages for named exports, but this is not always possible. While this used to be a common problem for Snowpack users, thanks to improvements in our scanner this is no longer an issue the latest versions of Snowpack. +This error could also appear if named imports are used with older, Common.js npm packages. Thanks to improvements in our package scanner this is no longer a common issue for most packages. However, some packages are written or compiled in a way that makes automatic import scanning impossible. -**To solve:** Use the default import (`import pkg from 'my-old-package'`) for legacy Common.js/UMD packages that cannot be analyzed. +**To solve:** Use the default import (`import pkg from 'my-old-package'`) for legacy Common.js/UMD packages that cannot be analyzed. Or, add the package name to your `packageOptions.namedExports` configuration for runtime import scanning. + +```js +// snowpack.config.js +{ + "packageOptions": { + "namedExports": ["@shopify/polaris-tokens"] + } +} +``` ### Installing Non-JS Packages From d5b2c05c75ddf2367a4e27b5b9f8ef4188f98bfa Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 28 Feb 2021 13:02:57 -0800 Subject: [PATCH 31/40] a bit of logging cleanup --- snowpack/src/commands/dev.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index afaf1237c1..d706a468e1 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -269,14 +269,6 @@ export async function startServer( }; } - messageBus.on(paintEvent.SERVER_START, (info) => { - logger.info(colors.green(`Server started in ${info.startTimeMs}ms.`)); - logger.info(`${colors.green('Local:')} ${`${info.protocol}//${hostname}:${port}`}`); - if (info.remoteIp) { - logger.info(`${colors.green('Network:')} ${`${info.protocol}//${info.remoteIp}:${port}`}`); - } - }); - if (config.devOptions.output === 'dashboard' && process.stdout.isTTY) { startDashboard(messageBus, config); } else { @@ -728,13 +720,14 @@ export async function startServer( .filter((i) => i.family === 'IPv4' && i.internal === false) .map((i) => i.address); const protocol = config.devOptions.secure ? 'https:' : 'http:'; - messageBus.emit(paintEvent.SERVER_START, { - protocol, - hostname, - port, - remoteIp: remoteIps[0], - startTimeMs: Math.round(performance.now() - serverStart), - }); + + // Log the successful server start. + const startTimeMs = Math.round(performance.now() - serverStart); + logger.info(colors.green(`Server started in ${startTimeMs}ms.`)); + logger.info(`${colors.green('Local:')} ${`${protocol}//${hostname}:${port}`}`); + if (remoteIps.length > 0) { + logger.info(`${colors.green('Network:')} ${`${protocol}//${remoteIps[0]}:${port}`}`); + } } // HMR Engine From b743c118641b4074ff6c64e2b516e0003f89eb2a Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 28 Feb 2021 13:41:58 -0800 Subject: [PATCH 32/40] add fix for source map exact matches --- snowpack/src/build/file-urls.ts | 2 +- snowpack/src/commands/dev.ts | 35 ++++++++++++------- test/build/config-mount/config-mount.test.js | 1 + test/build/config-mount/src/h/dep.js.map | 1 + test/test-utils.js | 2 +- .../reference/common-error-details.md | 2 +- 6 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 test/build/config-mount/src/h/dep.js.map diff --git a/snowpack/src/build/file-urls.ts b/snowpack/src/build/file-urls.ts index 6ea37b0b86..b3d72161d2 100644 --- a/snowpack/src/build/file-urls.ts +++ b/snowpack/src/build/file-urls.ts @@ -80,7 +80,7 @@ export function getUrlsForFile(fileLoc: string, config: SnowpackConfig): undefin path.posix.join( config.buildOptions.metaUrlPath, 'link', - slash(path.relative((config.workspaceRoot as string), u)), + slash(path.relative(config.workspaceRoot as string, u)), ), ); } diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index d706a468e1..65c291c888 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -390,8 +390,6 @@ export async function startServer( const encoding = _encoding ?? null; const reqUrlHmrParam = reqUrl.includes('?mtime=') && reqUrl.split('?')[1]; const reqPath = decodeURI(url.parse(reqUrl).pathname!); - const resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, ''); - const resourceType = path.extname(resourcePath) || '.html'; if (reqPath === getMetaUrlPath('/hmr-client.js', config)) { return { @@ -422,6 +420,7 @@ export async function startServer( // 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)) { + const resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, ''); const webModuleUrl = resourcePath.substr(PACKAGE_PATH_PREFIX.length); let loadedModule = await pkgSource.load(webModuleUrl, isSSR, commandOptions); if (!loadedModule) { @@ -448,16 +447,23 @@ export async function startServer( }; } + // 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 = path.extname(reqPath) || '.html'; let foundFile: 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. + // 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.resolve( - (config.workspaceRoot as string), + config.workspaceRoot as string, process.platform === 'win32' ? symlinkResourceUrl.replace(/\//g, '\\') : symlinkResourceUrl, ); const symlinkResourceDirectory = path.dirname(symlinkResourceLoc); @@ -487,7 +493,7 @@ export async function startServer( path.posix.join( config.buildOptions.metaUrlPath, 'link', - slash(path.relative((config.workspaceRoot as string), u)), + slash(path.relative(config.workspaceRoot as string, u)), ), ), ); @@ -513,13 +519,18 @@ export async function startServer( // Check our file<>URL mapping for the most relevant match, and continue if found. // Otherwise, return a 404. else { - const attemptedFileLoc = + let attemptedFileLoc = fileToUrlMapping.key(resourcePath); + if (!attemptedFileLoc) { + resourcePath = resourcePath.replace(/\.map$/, '').replace(/\.proxy\.js$/, ''); + resourceType = path.extname(resourcePath) || '.html'; + } + attemptedFileLoc = fileToUrlMapping.key(resourcePath) || fileToUrlMapping.key(resourcePath + '.html') || fileToUrlMapping.key(resourcePath + 'index.html') || fileToUrlMapping.key(resourcePath + '/index.html'); if (!attemptedFileLoc) { - throw new NotFoundError(reqPath, [resourcePath]); + throw new NotFoundError(reqPath); } const [, mountEntry] = getMountEntryForFile(attemptedFileLoc, config)!; @@ -575,9 +586,9 @@ export async function startServer( if (Object.keys(fileBuilder.buildOutput).length === 0) { await fileBuilder.build(isStatic); } - if (reqPath.endsWith('.proxy.js')) { + if (resourcePath !== reqPath && reqPath.endsWith('.proxy.js')) { finalizedResponse = await fileBuilder.getProxy(resourcePath, resourceType); - } else if (reqPath.endsWith('.map')) { + } else if (resourcePath !== reqPath && reqPath.endsWith('.map')) { finalizedResponse = fileBuilder.getSourceMap(resourcePath); } else { if (foundFile.isResolve) { @@ -722,7 +733,7 @@ export async function startServer( const protocol = config.devOptions.secure ? 'https:' : 'http:'; // Log the successful server start. - const startTimeMs = Math.round(performance.now() - serverStart); + const startTimeMs = Math.round(performance.now() - serverStart); logger.info(colors.green(`Server started in ${startTimeMs}ms.`)); logger.info(`${colors.green('Local:')} ${`${protocol}//${hostname}:${port}`}`); if (remoteIps.length > 0) { diff --git a/test/build/config-mount/config-mount.test.js b/test/build/config-mount/config-mount.test.js index 2d477655af..400f08d022 100644 --- a/test/build/config-mount/config-mount.test.js +++ b/test/build/config-mount/config-mount.test.js @@ -77,6 +77,7 @@ describe('config: mount', () => { it('static', () => { const $ = cheerio.load(files['/h/main.html']); expect($('script[type="module"]').attr('src')).toBe('/_dist_/index.js'); // JS resolved + expect(files['/h/dep.js.map']).toEqual(`I am a static source map.`); // preserves static source maps }); it('resolve: false', () => { diff --git a/test/build/config-mount/src/h/dep.js.map b/test/build/config-mount/src/h/dep.js.map new file mode 100644 index 0000000000..11cf36e897 --- /dev/null +++ b/test/build/config-mount/src/h/dep.js.map @@ -0,0 +1 @@ +I am a static source map. \ No newline at end of file diff --git a/test/test-utils.js b/test/test-utils.js index e65c1d8ae0..0343e8c020 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -10,6 +10,7 @@ const UTF8_FRIENDLY_EXTS = [ 'css', 'html', 'js', + 'map', 'jsx', 'ts', 'tsx', @@ -21,7 +22,6 @@ const UTF8_FRIENDLY_EXTS = [ /** setup for /tests/build/* */ function setupBuildTest(cwd) { - console.log(cwd); return execSync('yarn testbuild', {cwd}); } exports.setupBuildTest = setupBuildTest; diff --git a/www/_template/reference/common-error-details.md b/www/_template/reference/common-error-details.md index a96e86063e..b0ee3c0cd4 100644 --- a/www/_template/reference/common-error-details.md +++ b/www/_template/reference/common-error-details.md @@ -28,7 +28,7 @@ If you are using TypeScript, this error could occur if you are importing somethi **To solve:** Make sure to use `import type { MyInterfaceName }` instead. -This error could also appear if named imports are used with older, Common.js npm packages. Thanks to improvements in our package scanner this is no longer a common issue for most packages. However, some packages are written or compiled in a way that makes automatic import scanning impossible. +This error could also appear if named imports are used with older, Common.js npm packages. Thanks to improvements in our package scanner this is no longer a common issue for most packages. However, some packages are written or compiled in a way that makes automatic import scanning impossible. **To solve:** Use the default import (`import pkg from 'my-old-package'`) for legacy Common.js/UMD packages that cannot be analyzed. Or, add the package name to your `packageOptions.namedExports` configuration for runtime import scanning. From ab57bc7777b75ca3e6f52e80cfa1c645144902a0 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 28 Feb 2021 18:40:43 -0800 Subject: [PATCH 33/40] update with feedback, get build watch better --- plugins/web-test-runner-plugin/plugin.js | 2 -- snowpack/src/commands/build.ts | 25 +++++++++++-------- snowpack/src/commands/dev.ts | 23 +++++++++-------- snowpack/src/sources/local-install.ts | 1 + snowpack/src/sources/local.ts | 12 ++++++--- .../create-snowpack-app.test.js.snap | 6 ++--- 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/plugins/web-test-runner-plugin/plugin.js b/plugins/web-test-runner-plugin/plugin.js index 8b4ebc62ae..27bbed4a83 100644 --- a/plugins/web-test-runner-plugin/plugin.js +++ b/plugins/web-test-runner-plugin/plugin.js @@ -28,8 +28,6 @@ To Resolve: devOptions: {open: 'none', output: 'stream', hmr: false}, }); // npm packages should be installed/prepared ahead of time. - console.log('[snowpack] preparing npm packages...'); - await snowpack.preparePackages({config, lockfile: null}); console.log('[snowpack] starting server...'); fileWatcher.add(Object.keys(config.mount)); server = await snowpack.startServer({ diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index 73a0418a6b..f5ea0e0435 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -95,7 +95,7 @@ export async function build(commandOptions: CommandOptions): Promise { // First, do our own re-build logic allFileUrlsToProcess.push(...getUrlsForFile(filePath, config)!); - await flushFileQueue(true, { + await flushFileQueue(false, { isSSR, isHMR, isResolve: true, diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 65c291c888..5b8a667600 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -241,15 +241,21 @@ function getServerRuntime( export async function startServer( commandOptions: CommandOptions, - {isDev}: {isDev: boolean} = {isDev: true}, + { + isDev: _isDev, + preparePackages: _preparePackages, + }: {isDev?: boolean; preparePackages?: boolean} = {}, ): Promise { + const isDev = _isDev ?? true; + const isPreparePackages = _preparePackages ?? true; const {config} = commandOptions; - // Start the startup timer! + const pkgSource = getPackageSource(config.packageOptions.source); + if (isPreparePackages) { + await pkgSource.prepare(commandOptions); + } let serverStart = performance.now(); - const {port: defaultPort, hostname, open} = config.devOptions; const messageBus = new EventEmitter(); - const pkgSource = getPackageSource(config.packageOptions.source); const PACKAGE_PATH_PREFIX = path.posix.join(config.buildOptions.metaUrlPath, 'pkg/'); const PACKAGE_LINK_PATH_PREFIX = path.posix.join(config.buildOptions.metaUrlPath, 'link/'); let port: number | undefined; @@ -755,7 +761,6 @@ export async function startServer( logger.info( colors.cyan('File changed: ') + path.relative(config.workspaceRoot || config.root, fileLoc), ); - await onFileChangeCallback({filePath: fileLoc}); const updatedUrls = getUrlsForFile(fileLoc, config); if (updatedUrls) { handleHmrUpdate && handleHmrUpdate(fileLoc, updatedUrls[0]); @@ -764,6 +769,7 @@ export async function startServer( } inMemoryBuildCache.delete(getCacheKey(fileLoc, {isSSR: true, env: process.env.NODE_ENV})); inMemoryBuildCache.delete(getCacheKey(fileLoc, {isSSR: false, env: process.env.NODE_ENV})); + await onFileChangeCallback({filePath: fileLoc}); for (const plugin of config.plugins) { plugin.onChange && plugin.onChange({filePath: fileLoc}); } @@ -794,7 +800,7 @@ export async function startServer( onWatchEvent(fileLoc); }); if (config.devOptions.output !== 'dashboard' || !process.stdout.isTTY) { - logger.info(colors.cyan('watching for file changes...')); + logger.info(colors.cyan('watching for file changes... ')); } } @@ -836,12 +842,7 @@ export async function command(commandOptions: CommandOptions) { commandOptions.config.buildOptions.watch = true; commandOptions.config.devOptions.hmr = true; // Start the server - const pkgSource = getPackageSource(commandOptions.config.packageOptions.source); - await pkgSource.prepare(commandOptions); await startServer(commandOptions); - if (commandOptions.config.devOptions.output !== 'dashboard') { - logger.info(colors.cyan('watching for file changes...')); - } } catch (err) { logger.error(err.message); logger.debug(err.stack); diff --git a/snowpack/src/sources/local-install.ts b/snowpack/src/sources/local-install.ts index 02639c5ab5..c9633f2a47 100644 --- a/snowpack/src/sources/local-install.ts +++ b/snowpack/src/sources/local-install.ts @@ -49,6 +49,7 @@ export async function installPackages({ error: (...args: [any, ...any[]]) => logger.error(util.format(...args), {name: loggerName}), }, ...installOptions, + stats: false, rollup: { plugins: [ { diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 0395ee6e5b..3cb3fe7beb 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -41,8 +41,6 @@ const PROJECT_CACHE_DIR = // Because this is specifically for dependencies, this fallback should rarely be used. path.join(GLOBAL_CACHE_DIR, crypto.createHash('md5').update(process.cwd()).digest('hex')); -const DEV_DEPENDENCIES_DIR = path.join(PROJECT_CACHE_DIR, process.env.NODE_ENV || 'development'); - const NEVER_PEER_PACKAGES: string[] = [ '@babel/runtime', '@babel/runtime-corejs3', @@ -195,6 +193,10 @@ export default { async prepare(commandOptions: CommandOptions) { config = commandOptions.config; + const DEV_DEPENDENCIES_DIR = path.join( + PROJECT_CACHE_DIR, + process.env.NODE_ENV || 'development', + ); const installDirectoryHashLoc = path.join(DEV_DEPENDENCIES_DIR, '.meta'); const installDirectoryHash = await fs .readFile(installDirectoryHashLoc, 'utf-8') @@ -231,7 +233,7 @@ export default { await inProgressBuilds.onIdle(); await mkdirp(path.dirname(installDirectoryHashLoc)); await fs.writeFile(installDirectoryHashLoc, 'v1', 'utf-8'); - logger.info(colors.bold('Ready! Starting up...')); + logger.info(colors.bold('Ready!')); return; }, @@ -301,6 +303,10 @@ export default { const packageManifest = JSON.parse(packageManifestStr); const packageName = packageManifest.name || _packageName; const packageVersion = packageManifest.version || 'unknown'; + const DEV_DEPENDENCIES_DIR = path.join( + PROJECT_CACHE_DIR, + process.env.NODE_ENV || 'development', + ); const installDest = path.join(DEV_DEPENDENCIES_DIR, packageName + '@' + packageVersion); allKnownSpecs.add(spec); diff --git a/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap b/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap index 4e67e0e576..418aa29976 100644 --- a/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap +++ b/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap @@ -122,7 +122,7 @@ exports[`create-snowpack-app app-template-11ty > build: _snowpack/pkg/canvas-con try { worker = new Worker(URL.createObjectURL(new Blob([code]))); } catch (e) { - typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"\\\\u{1F38A} Could not load worker\\", e) : null; + typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"🎊 Could not load worker\\", e) : null; return null; } decorate(worker); @@ -706,7 +706,7 @@ exports[`create-snowpack-app app-template-blank > build: _snowpack/pkg/canvas-co try { worker = new Worker(URL.createObjectURL(new Blob([code]))); } catch (e) { - typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"\\\\u{1F38A} Could not load worker\\", e) : null; + typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"🎊 Could not load worker\\", e) : null; return null; } decorate(worker); @@ -1245,7 +1245,7 @@ exports[`create-snowpack-app app-template-blank-typescript > build: _snowpack/pk try { worker = new Worker(URL.createObjectURL(new Blob([code]))); } catch (e) { - typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"\\\\u{1F38A} Could not load worker\\", e) : null; + typeof console !== void 0 && typeof console.warn === \\"function\\" ? console.warn(\\"🎊 Could not load worker\\", e) : null; return null; } decorate(worker); From cd49f8e5b464c126761d79a1805e851d6095264f Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Sun, 28 Feb 2021 19:47:36 -0800 Subject: [PATCH 34/40] add a warning if you try to link a mounted file --- snowpack/src/commands/dev.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 5b8a667600..c718460199 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -493,16 +493,23 @@ export async function startServer( absolute: true, })) { const normalizedFileLoc = path.normalize(f); - fileToUrlMapping.add( - normalizedFileLoc, - getBuiltFileUrls(normalizedFileLoc, config).map((u) => - path.posix.join( - config.buildOptions.metaUrlPath, - 'link', - slash(path.relative(config.workspaceRoot as string, u)), + if (fileToUrlMapping.value(normalizedFileLoc)) { + 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( + normalizedFileLoc, + getBuiltFileUrls(normalizedFileLoc, config).map((u) => + path.posix.join( + config.buildOptions.metaUrlPath, + 'link', + slash(path.relative(config.workspaceRoot as string, u)), + ), ), - ), - ); + ); + } } } const fileLocation = fileToUrlMapping.key(reqPath); From 1069e26ae2d222a06a10646dfc6b8585f9a2ff59 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Mon, 1 Mar 2021 11:52:55 -0800 Subject: [PATCH 35/40] include non-proxy files in final build --- snowpack/src/build/file-builder.ts | 8 +- .../create-snowpack-app.test.js.snap | 99 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/snowpack/src/build/file-builder.ts b/snowpack/src/build/file-builder.ts index 5d3fd2b3af..0c1a2e1113 100644 --- a/snowpack/src/build/file-builder.ts +++ b/snowpack/src/build/file-builder.ts @@ -101,7 +101,7 @@ export class FileBuilder { * system, so they can't be cached long-term with the build. */ async resolveImports( - isResolveBareImports: boolean, + isResolve: boolean, hmrParam?: string | false, importMap?: ImportMap, ): Promise { @@ -150,10 +150,10 @@ export class FileBuilder { this.loc, spec, this.config, - importMap || (isResolveBareImports ? undefined : {imports: {}}), + importMap || (isResolve ? undefined : {imports: {}}), ); } catch (err) { - if (!isResolveBareImports && /not included in import map./.test(err.message)) { + if (!isResolve && /not included in import map./.test(err.message)) { return spec; } throw err; @@ -181,7 +181,7 @@ export class FileBuilder { const isProxyImport = importExtName && importExtName !== '.js' && importExtName !== '.mjs'; const isAbsoluteUrlPath = path.posix.isAbsolute(resolvedImportUrl); if (isAbsoluteUrlPath) { - if (this.config.buildOptions.resolveProxyImports && isProxyImport) { + if (isResolve && this.config.buildOptions.resolveProxyImports && isProxyImport) { resolvedImportUrl = resolvedImportUrl + '.proxy.js'; } resolvedImports.push(createInstallTarget(resolvedImportUrl)); diff --git a/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap b/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap index 418aa29976..5b524a754b 100644 --- a/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap +++ b/test/create-snowpack-app/__snapshots__/create-snowpack-app.test.js.snap @@ -8785,6 +8785,7 @@ Array [ "_snowpack/pkg/import-map.json", "_snowpack/pkg/svelte.js", "_snowpack/pkg/svelte/internal.js", + "dist/App.svelte.css", "dist/App.svelte.css.proxy.js", "dist/App.svelte.js", "dist/index.js", @@ -8795,6 +8796,8 @@ Array [ ] `; +exports[`create-snowpack-app app-template-svelte > build: dist/App.svelte.css 1`] = `"body{margin:0;font-family:Arial, Helvetica, sans-serif}.App.svelte-rq4gzr.svelte-rq4gzr{text-align:center}.App.svelte-rq4gzr code.svelte-rq4gzr{background:#0002;padding:4px 8px;border-radius:4px}.App.svelte-rq4gzr p.svelte-rq4gzr{margin:0.4rem}.App-header.svelte-rq4gzr.svelte-rq4gzr{background-color:#f9f6f6;color:#333;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin)}.App-link.svelte-rq4gzr.svelte-rq4gzr{color:#ff3e00}.App-logo.svelte-rq4gzr.svelte-rq4gzr{height:36vmin;pointer-events:none;margin-bottom:3rem;animation:svelte-rq4gzr-App-logo-pulse infinite 1.6s ease-in-out alternate}@keyframes svelte-rq4gzr-App-logo-pulse{from{transform:scale(1)}to{transform:scale(1.06)}}"`; + exports[`create-snowpack-app app-template-svelte > build: dist/App.svelte.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -8978,6 +8981,7 @@ Array [ "_snowpack/pkg/import-map.json", "_snowpack/pkg/svelte.js", "_snowpack/pkg/svelte/internal.js", + "dist/App.svelte.css", "dist/App.svelte.css.proxy.js", "dist/App.svelte.js", "dist/index.js", @@ -8988,6 +8992,8 @@ Array [ ] `; +exports[`create-snowpack-app app-template-svelte-typescript > build: dist/App.svelte.css 1`] = `"body{margin:0;font-family:Arial, Helvetica, sans-serif}.App.svelte-1sqyd3v.svelte-1sqyd3v{text-align:center}.App.svelte-1sqyd3v code.svelte-1sqyd3v{background:#0002;padding:4px 8px;border-radius:4px}.App.svelte-1sqyd3v p.svelte-1sqyd3v{margin:0.4rem}.App-header.svelte-1sqyd3v.svelte-1sqyd3v{background-color:#f9f6f6;color:#333;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:calc(10px + 2vmin)}.App-link.svelte-1sqyd3v.svelte-1sqyd3v{color:#ff3e00}.App-logo.svelte-1sqyd3v.svelte-1sqyd3v{height:36vmin;pointer-events:none;margin-bottom:3rem;animation:svelte-1sqyd3v-App-logo-spin infinite 1.6s ease-in-out alternate}@keyframes svelte-1sqyd3v-App-logo-spin{from{transform:scale(1)}to{transform:scale(1.06)}}"`; + exports[`create-snowpack-app app-template-svelte-typescript > build: dist/App.svelte.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -14173,6 +14179,7 @@ Array [ "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/vue.js", + "dist/App.vue.css", "dist/App.vue.css.proxy.js", "dist/App.vue.js", "dist/index.js", @@ -14184,6 +14191,40 @@ Array [ ] `; +exports[`create-snowpack-app app-template-vue > build: dist/App.vue.css 1`] = ` +" +.App { + text-align: center; +} +.App-header { + background-color: #f9f6f6; + color: #32485f; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); +} +.App-link { + color: #00c185; +} +.App-logo { + height: 40vmin; + pointer-events: none; + margin-bottom: 1rem; + animation: App-logo-spin infinite 1.6s ease-in-out alternate; +} +@keyframes App-logo-spin { +from { + transform: scale(1); +} +to { + transform: scale(1.06); +} +}" +`; + exports[`create-snowpack-app app-template-vue > build: dist/App.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -19432,16 +19473,19 @@ Array [ "_snowpack/env.js", "_snowpack/pkg/import-map.json", "_snowpack/pkg/vue.js", + "dist/App.vue.css", "dist/App.vue.css.proxy.js", "dist/App.vue.js", "dist/components/Bar.js", "dist/components/Bar.module.css", "dist/components/Bar.module.css.proxy.js", + "dist/components/BarJsx.vue.css", "dist/components/BarJsx.vue.css.proxy.js", "dist/components/BarJsx.vue.js", "dist/components/Foo.js", "dist/components/Foo.module.css", "dist/components/Foo.module.css.proxy.js", + "dist/components/FooTsx.vue.css", "dist/components/FooTsx.vue.css.proxy.js", "dist/components/FooTsx.vue.js", "dist/index.js", @@ -19453,6 +19497,47 @@ Array [ ] `; +exports[`create-snowpack-app app-template-vue-typescript > build: dist/App.vue.css 1`] = ` +" +.App { + text-align: center; +} +.App-header { + background-color: #f9f6f6; + color: #32485f; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); +} +.App-link { + color: #00c185; +} +.App-logo { + height: 40vmin; + pointer-events: none; + margin-bottom: 1rem; + animation: App-logo-spin infinite 1.6s ease-in-out alternate; +} +.App-tsx { + display: flex; +} +.App-tsx > div { + margin-left: 30px; + font-size: 16px; +} +@keyframes App-logo-spin { +from { + transform: scale(1); +} +to { + transform: scale(1.06); +} +}" +`; + exports[`create-snowpack-app app-template-vue-typescript > build: dist/App.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -19577,6 +19662,13 @@ if (typeof document !== 'undefined') { }" `; +exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/BarJsx.vue.css 1`] = ` +" +.bar-jsx-vue { + color: red; +}" +`; + exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/BarJsx.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { @@ -19660,6 +19752,13 @@ if (typeof document !== 'undefined') { }" `; +exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/FooTsx.vue.css 1`] = ` +" +.foo-tsx-vue { + color: green; +}" +`; + exports[`create-snowpack-app app-template-vue-typescript > build: dist/components/FooTsx.vue.css.proxy.js 1`] = ` "// [snowpack] add styles to the page (skip if no document exists) if (typeof document !== 'undefined') { From 1e82589438cdfc24a926b9ec7c53e84228782bac Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Tue, 2 Mar 2021 07:52:42 -0800 Subject: [PATCH 36/40] fix css issue --- snowpack/src/commands/dev.ts | 17 +++++++++++------ .../package-workspace/package-workspace.test.js | 1 + .../SvelteComponent.svelte | 3 +++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index c718460199..4b78bbee86 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -512,16 +512,21 @@ export async function startServer( } } } - const fileLocation = fileToUrlMapping.key(reqPath); - if (!fileLocation) { + let attemptedFileLoc = fileToUrlMapping.key(reqPath); + if (!attemptedFileLoc) { + resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, ''); + resourceType = path.extname(resourcePath) || '.html'; + } + attemptedFileLoc = fileToUrlMapping.key(resourcePath); + if (!attemptedFileLoc) { throw new NotFoundError(reqPath); } - const fileLocationExists = await fs.stat(fileLocation).catch(() => null); + const fileLocationExists = await fs.stat(attemptedFileLoc).catch(() => null); if (!fileLocationExists) { - throw new NotFoundError(reqPath, [fileLocation]); + throw new NotFoundError(reqPath, [attemptedFileLoc]); } foundFile = { - loc: fileLocation, + loc: attemptedFileLoc, type: path.extname(reqPath), isStatic: false, isResolve: true, @@ -534,7 +539,7 @@ export async function startServer( else { let attemptedFileLoc = fileToUrlMapping.key(resourcePath); if (!attemptedFileLoc) { - resourcePath = resourcePath.replace(/\.map$/, '').replace(/\.proxy\.js$/, ''); + resourcePath = reqPath.replace(/\.map$/, '').replace(/\.proxy\.js$/, ''); resourceType = path.extname(resourcePath) || '.html'; } attemptedFileLoc = diff --git a/test/build/package-workspace/package-workspace.test.js b/test/build/package-workspace/package-workspace.test.js index 443e64b1f1..e18473d74c 100644 --- a/test/build/package-workspace/package-workspace.test.js +++ b/test/build/package-workspace/package-workspace.test.js @@ -18,5 +18,6 @@ describe('test workspace linked packages', () => { it('builds workspace package files as expected', () => { expect(fs.existsSync(path.join(cwd, '_snowpack', 'link', 'test-workspace-component', 'SvelteComponent.svelte.js'))).toBe(true); // import exists + expect(fs.existsSync(path.join(cwd, '_snowpack', 'link', 'test-workspace-component', 'SvelteComponent.svelte.css.proxy.js'))).toBe(true); // import exists }); }); diff --git a/test/build/test-workspace-component/SvelteComponent.svelte b/test/build/test-workspace-component/SvelteComponent.svelte index f8dcb725bd..5fc81169ef 100755 --- a/test/build/test-workspace-component/SvelteComponent.svelte +++ b/test/build/test-workspace-component/SvelteComponent.svelte @@ -3,6 +3,9 @@
From a4bbb5a2cc2d1d781541ae5149c75c89643a7d9f Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Wed, 3 Mar 2021 16:08:36 -0800 Subject: [PATCH 37/40] add better hmr port detection --- snowpack/src/commands/dev.ts | 2 +- snowpack/src/dev/hmr.ts | 9 +++------ snowpack/src/hmr-server-engine.ts | 17 +++-------------- 3 files changed, 7 insertions(+), 21 deletions(-) diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index 4b78bbee86..74524d8142 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -761,7 +761,7 @@ export async function startServer( // HMR Engine const {hmrEngine, handleHmrUpdate} = config.devOptions.hmr - ? startHmrEngine(inMemoryBuildCache, server, config) + ? startHmrEngine(inMemoryBuildCache, server, port, config) : {hmrEngine: undefined, handleHmrUpdate: undefined}; // Allow the user to hook into this callback, if they like (noop by default) diff --git a/snowpack/src/dev/hmr.ts b/snowpack/src/dev/hmr.ts index 63dace5004..e7a482e502 100644 --- a/snowpack/src/dev/hmr.ts +++ b/snowpack/src/dev/hmr.ts @@ -10,15 +10,12 @@ import {getCacheKey, hasExtension} from '../util'; export function startHmrEngine( inMemoryBuildCache: Map, server: http.Server | http2.Http2SecureServer | undefined, + serverPort: number | undefined, config: SnowpackConfig, ) { const {hmrDelay} = config.devOptions; - const hmrPort = config.devOptions.hmrPort || config.devOptions.port; - const hmrEngineOptions = Object.assign( - {delay: hmrDelay}, - config.devOptions.hmrPort || !server ? {port: hmrPort} : {server, port: hmrPort}, - ); - const hmrEngine = new EsmHmrEngine(hmrEngineOptions); + const hmrPort = config.devOptions.hmrPort || serverPort; + const hmrEngine = new EsmHmrEngine({server, port: hmrPort, delay: hmrDelay}); onProcessExit(() => { hmrEngine.disconnectAllClients(); }); diff --git a/snowpack/src/hmr-server-engine.ts b/snowpack/src/hmr-server-engine.ts index 6213146918..5d2ac77585 100644 --- a/snowpack/src/hmr-server-engine.ts +++ b/snowpack/src/hmr-server-engine.ts @@ -27,22 +27,11 @@ type HMRMessage = const DEFAULT_CONNECT_DELAY = 2000; const DEFAULT_PORT = 12321; -interface EsmHmrEngineOptionsCommon { +interface EsmHmrEngineOptions { + server: http.Server | http2.Http2Server | undefined; + port?: number | undefined; delay?: number; } - -type EsmHmrEngineOptions = ( - | { - server: http.Server | http2.Http2Server; - port: number; - } - | { - port?: number; - server?: undefined; - } -) & - EsmHmrEngineOptionsCommon; - export class EsmHmrEngine { clients: Set = new Set(); dependencyTree = new Map(); From b7a50311857eb0748ec78867feefe5fdf3221ed4 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Mon, 8 Mar 2021 20:51:21 -0800 Subject: [PATCH 38/40] add knownEntrypoints support back --- esinstall/src/index.ts | 1 + snowpack/src/config.ts | 1 + snowpack/src/sources/local.ts | 2 +- snowpack/src/types.ts | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index 2ca2aabd32..ade6a1ee87 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -71,6 +71,7 @@ const CJS_PACKAGES_TO_AUTO_DETECT = [ 'react-table', 'chai/index.js', 'events/events.js', + 'uuid/index.js', ]; function isImportOfPackage(importUrl: string, packageName: string) { diff --git a/snowpack/src/config.ts b/snowpack/src/config.ts index 00a6789d4d..a7a2989de3 100644 --- a/snowpack/src/config.ts +++ b/snowpack/src/config.ts @@ -69,6 +69,7 @@ const DEFAULT_PACKAGES_REMOTE_CONFIG: PackageSourceRemote = { source: 'remote', origin: REMOTE_PACKAGE_ORIGIN, external: [], + knownEntrypoints: [], cache: '.snowpack', types: false, }; diff --git a/snowpack/src/sources/local.ts b/snowpack/src/sources/local.ts index 3cb3fe7beb..284c85ffbb 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -219,7 +219,7 @@ export default { } const installTargets = await getInstallTargets( config, - config.packageOptions.source === 'local' ? config.packageOptions.knownEntrypoints : [], + config.packageOptions.knownEntrypoints, ); if (installTargets.length === 0) { logger.info('No dependencies detected. Ready!'); diff --git a/snowpack/src/types.ts b/snowpack/src/types.ts index 0e6f74471f..ce470012d0 100644 --- a/snowpack/src/types.ts +++ b/snowpack/src/types.ts @@ -233,8 +233,9 @@ export interface PackageSourceLocal export interface PackageSourceRemote { source: 'remote'; - origin: string; external: string[]; + knownEntrypoints: string[]; + origin: string; cache: string; types: boolean; } From 7bf6df1aff89ded436a9744806d3ee6e0141b54d Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Mon, 8 Mar 2021 21:02:30 -0800 Subject: [PATCH 39/40] update test --- test/esinstall/import-types/import-types.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/esinstall/import-types/import-types.test.js b/test/esinstall/import-types/import-types.test.js index b6bba4d708..24f6b7dc5f 100644 --- a/test/esinstall/import-types/import-types.test.js +++ b/test/esinstall/import-types/import-types.test.js @@ -11,7 +11,7 @@ describe('importing types', () => { await runTest(['type-only-pkg', 'array-flatten'], {cwd, dest}); expect(false).toEqual(true); // should not finish } catch (err) { - expect(err.message).toContain('Cannot find module'); + expect(err.message).toContain('Unable to find any entrypoint for \"type-only-pkg\"'); } }); }); From 59560fcbee07606141da4dba8c1ebffd7e731ef9 Mon Sep 17 00:00:00 2001 From: "Fred K. Schott" Date: Mon, 8 Mar 2021 21:10:49 -0800 Subject: [PATCH 40/40] revert knownEntrypoints in build, breaking change --- snowpack/src/commands/build.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/snowpack/src/commands/build.ts b/snowpack/src/commands/build.ts index f5ea0e0435..8e1968ae4b 100644 --- a/snowpack/src/commands/build.ts +++ b/snowpack/src/commands/build.ts @@ -185,6 +185,8 @@ export async function build(commandOptions: CommandOptions): Promise