diff --git a/esinstall/src/index.ts b/esinstall/src/index.ts index f975440284..b77574e0c5 100644 --- a/esinstall/src/index.ts +++ b/esinstall/src/index.ts @@ -306,7 +306,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), 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 9ed0320f3a..4f4278ff62 100644 --- a/plugins/web-test-runner-plugin/plugin.js +++ b/plugins/web-test-runner-plugin/plugin.js @@ -19,6 +19,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 73dc831861..2957fb2823 100644 --- a/snowpack/package.json +++ b/snowpack/package.json @@ -56,7 +56,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..bbed3f12c3 100644 --- a/snowpack/src/build/file-urls.ts +++ b/snowpack/src/build/file-urls.ts @@ -1,11 +1,11 @@ import path from 'path'; 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 +15,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 +68,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', 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 347495f69d..312bab604e 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 31e80b25f0..9beccd32a9 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..783fcdee77 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,193 @@ 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; - } - } - } - - // 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 = {}; + const devServer = await startServer(commandOptions, {isDev}); - /** 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, + ignore: [...config.exclude, ...config.testOptions.files], }); + for (const f of files) { + const fileUrls = getUrlsForFile(f, 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.'); }, }; } diff --git a/snowpack/src/commands/dev.ts b/snowpack/src/commands/dev.ts index ad436152e0..3a3e3029f7 100644 --- a/snowpack/src/commands/dev.ts +++ b/snowpack/src/commands/dev.ts @@ -1,38 +1,12 @@ -/** - * 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'; @@ -43,56 +17,56 @@ 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, OnFileChangeCallback, RouteConfigObject, - SnowpackBuildMap, - SnowpackDevServer, ServerRuntime, + SnowpackDevServer, + LoadUrlOptions, } 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 +105,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(lookups?: string[]) { + if (!lookups) { + super(`NOT_FOUND`); + } else { + super(`NOT_FOUND:\n${lookups.map((loc) => ' ✘ ' + loc).join('\n')}`); + } } } @@ -231,12 +206,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] ${req.url}${err.message === 'NOT_FOUND' ? '' : '\n' + err.message}`); } sendResponseError(req, res, 404); return; } + console.log(err); logger.error(err.toString()); logger.error(`[500] ${req.url}`, { // @ts-ignore @@ -264,20 +239,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 +289,24 @@ 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(); - 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}'`); - } - }, - }); + 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}); + for (const f of files) { + fileToUrlMapping.add(f, getUrlsForFile(f, config)!); + } + } + + 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 +346,10 @@ export async function startServer(commandOptions: CommandOptions): Promise { if (msg === 'CONSOLE_INFO') { @@ -390,63 +371,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 +411,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(); } - 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); + return { + imports: loadedModule.imports, + contents: encodeResponse(loadedModule.contents, encoding), + originalFileLoc: null, + contentType: mime.lookup(reqPath) || 'application/javascript', + }; } - 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]); - } - } + let foundFile: FoundFile; - /** - * 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; + // * 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 symlinkResourcePath = '/' + reqPath.substr(PACKAGE_LINK_PATH_PREFIX.length); + const symlinkResourceDirectory = path.dirname(symlinkResourcePath); + const fileStat = await fs.stat(symlinkResourcePath).catch(() => null); + if (!fileStat) { + throw new NotFoundError([symlinkResourcePath]); } - 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, + // 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, + )}'`, ); - 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}); - } - } - - /** - * 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}); - } - - // source mapping - if (sourceMap) code = jsSourceMappingURL(code, sourceMappingURL); - - break; + for (const f of glob.sync(path.join(symlinkResourceDirectory, '*'), {nodir: true})) { + fileToUrlMapping.add( + f, + getBuiltFileUrls(f, config).map((u) => path.posix.join(PACKAGE_LINK_PATH_PREFIX, u)), + ); } } - - // by default, return file from disk - return code; + foundFile = { + loc: fileToUrlMapping.key(reqPath)!, + type: path.extname(reqPath), + isStatic: false, + isResolve: true, + }; } - - /** - * 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; - }, - ); - - // 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, - }); + // * 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([resourcePath]); } - 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); - } + const [, mountEntry] = getMountEntryForFile(attemptedFileLoc, config)!; - wrappedResponse = code; - return wrappedResponse; + // 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, + }; } - /** - * 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; - } - // Resolve imports. - if ( - requestedFileExt === '.js' || - requestedFileExt === '.html' || - requestedFileExt === '.css' - ) { - finalResponse = await resolveResponseImports( - fileLoc, - requestedFileExt, - finalResponse as string, - ); - } - // Wrap the response. - finalResponse = await wrapResponse(finalResponse, { - sourceMap: map, - sourceMappingURL: path.basename(requestedFile.base) + '.map', - }); - // Return the finalized response. - return finalResponse; - } + const {loc: fileLoc, type: responseType} = foundFile; - 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'); + // - isStatic Workaround: HMR plugins need to add scripts to HTML file, even if static. + const isStatic = foundFile.isStatic && responseType !== '.html'; + const isResolve = _isResolve ?? true; // 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; - } - if (!responseContent) { - throw new NotFoundError([fileLoc]); - } - return { - contents: encodeResponse(responseContent, encoding), - originalFileLoc: fileLoc, - contentType: mime.lookup(responseFileExt), - }; - } - - // 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)); - - // 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), - }; + 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); } - - // 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, - ); - - 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; - } - - 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; - }, - }; - } - } + if (Object.keys(fileBuilder.buildOutput).length === 0) { + await fileBuilder.build(isStatic); } - // 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 (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(); } - // 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), }; } @@ -1171,27 +676,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(() => { @@ -1247,7 +773,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'); @@ -1298,85 +805,63 @@ 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); }); - // 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; @@ -1384,6 +869,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 596e748228..e2baff2ee5 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..63e7788249 100644 --- a/snowpack/src/sources/local-install.ts +++ b/snowpack/src/sources/local-install.ts @@ -1,50 +1,40 @@ -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...')); + 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)), @@ -53,24 +43,39 @@ export async function run({ error: (...args: [any, ...any[]]) => logger.error(util.format(...args)), }, ...installOptions, + rollup: { + plugins: [ + { + name: 'esinstall:snowpack', + async load(id: string) { + 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; + }, + }, + ], + }, }); 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..d6b881c4ab 100644 --- a/snowpack/src/sources/local.ts +++ b/snowpack/src/sources/local.ts @@ -1,16 +1,24 @@ -import rimraf from 'rimraf'; import crypto from 'crypto'; +import {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 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'; const PROJECT_CACHE_DIR = projectCacheDir({name: 'snowpack'}) || @@ -21,48 +29,119 @@ 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'); + const packageParts = parts.pop()!.split('/').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 = 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.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); + // TODO: Can we support real tree-shaking here? + 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 +154,177 @@ 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 your dependencies...', + ); + } + 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 += path.sep + specParts.shift(); + } + const isSymlink = !entrypoint.includes(path.join('node_modules', _packageName)); + if (isSymlink) { + const builtEntrypointUrls = getBuiltFileUrls(entrypoint, config); + allSymlinkImports[builtEntrypointUrls[0]] = entrypoint; + return path.posix.join(config.buildOptions.metaUrlPath, 'link', builtEntrypointUrls[0]); } - 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; + const rootPackageDirectory = getRootPackageDirectory(entrypoint); + const packageManifestLoc = path.join(rootPackageDirectory, 'package.json'); + const packageManifestStr = await fs.readFile(packageManifestLoc, 'utf8'); + const packageManifest = JSON.parse(packageManifestStr); + const packageName = packageManifest.packageName || _packageName; + const packageVersion = packageManifest.packageVersion || '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(`- import "${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 = { + dest: installDest, + cwd: packageManifestLoc, + env: {NODE_ENV: process.env.NODE_ENV || 'development'}, + treeshake: false, + external: externalPackages, + externalEsm: externalPackages, + }; + const {importMap: newImportMap, needsSsrBuild} = await installPackages({ + config, + isDev: true, + isSSR: false, + installTargets, + installOptions, + }); + logger.debug(colors.yellow(`- import "${spec}"... DONE`)); + if (needsSsrBuild) { + logger.info(colors.yellow(`- import "${spec}" (ssr)...`)); + await installPackages({ + config, + isDev: true, + isSSR: true, + installTargets, + installOptions: { + ...installOptions, + dest: installDest + '-ssr', + }, + }); + logger.debug(colors.yellow(`- import "${spec}" (ssr)... DONE`)); + } + 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), + ), + ); + } + + // 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() { @@ -131,3 +335,9 @@ export default { return PROJECT_CACHE_DIR; }, } as PackageSource; + +// TODO: Handle monorepo! +// TODO: Handle passing all relevant install options +// InstallOptions: alias, importMap (for CDN aliases?), verbose?, env, polyfillNode, sourcemap?, external, packageLookupFields, packageExportLookupFields, namedExports, +// TODO: Handle svelte plugin telling us about svelte entrypoint +// TODO: Handle svelte plugin telling us about svelte = needing ssr? 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..80a6445568 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()) { diff --git a/test-dev/__snapshots__/dev.test.ts.snap b/test-dev/__snapshots__/dev.test.ts.snap index faf420ed6c..7941407e53 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.vunknown.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/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/esinstall/exclude-external-packages/__snapshots__ b/test/esinstall/exclude-external-packages/__snapshots__ index ed2211a901..8d15718419 100644 --- a/test/esinstall/exclude-external-packages/__snapshots__ +++ b/test/esinstall/exclude-external-packages/__snapshots__ @@ -18,7 +18,7 @@ exports[`exclude-external-packages matches the snapshot: web_modules/import-map. `; exports[`exclude-external-packages matches the snapshot: web_modules/react-dom.js 1`] = ` -"import require$$0 from 'react'; +"import * as react from 'react'; function createCommonjsModule(fn, basedir, module) { return module = { path: basedir, @@ -28,6 +28,9 @@ function createCommonjsModule(fn, basedir, module) { } }, fn(module, module.exports), module.exports; } +function getDefaultExportFromNamespaceIfNotNamed (n) { + return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; +} function commonjsRequire () { throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); } @@ -1096,6 +1099,7 @@ var tracing = createCommonjsModule(function (module) { module.exports = schedulerTracing_development; } }); +var require$$0 = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(react); var reactDom_development = createCommonjsModule(function (module, exports) { { (function() { 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 584f0e9b6e..768f9b9d61 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" @@ -13800,11 +13800,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" @@ -13832,6 +13842,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" @@ -14494,11 +14509,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"