diff --git a/src/config.ts b/src/config.ts index 5b99e39916..125b62dd71 100644 --- a/src/config.ts +++ b/src/config.ts @@ -127,6 +127,6 @@ export default function loadConfig(cliFlags?: SnowpackConfig) { // resolve --dest relative to cwd function normalizeDest(config: SnowpackConfig) { - config.installOptions.dest = path.resolve(process.cwd(), config.installOptions.dest); + config.installOptions.dest = path.resolve(process.cwd(), config.installOptions.dest!); return config; } diff --git a/src/index.ts b/src/index.ts index 5f72d27643..71ebe9e340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import babelPresetEnv from '@babel/preset-env'; import isNodeBuiltin from 'is-builtin-module'; import validatePackageName from 'validate-npm-package-name'; -import * as rollup from 'rollup'; +import {rollup, InputOptions, OutputOptions, Plugin, RollupError} from 'rollup'; import rollupPluginNodeResolve from '@rollup/plugin-node-resolve'; import rollupPluginCommonjs from '@rollup/plugin-commonjs'; import {terser as rollupPluginTerser} from 'rollup-plugin-terser'; @@ -26,7 +26,7 @@ import { DependencyStats, } from './rollup-plugin-dependency-info.js'; import {scanImports, scanDepList, InstallTarget} from './scan-imports.js'; -import {resolveDependencyManifest, MISSING_PLUGIN_SUGGESTIONS} from './util.js'; +import {resolveDependencyManifest, truthy, MISSING_PLUGIN_SUGGESTIONS} from './util.js'; import zlib from 'zlib'; @@ -38,8 +38,8 @@ export interface DependencyLoc { const ALWAYS_SHOW_ERRORS = new Set(['react', 'react-dom']); const cwd = process.cwd(); const banner = chalk.bold(`snowpack`) + ` installing... `; -const installResults = []; -let dependencyStats: DependencyStatsOutput = null; +const installResults: [string, boolean][] = []; +let dependencyStats: DependencyStatsOutput | null = null; let spinner = ora(banner); let spinnerHasError = false; @@ -65,20 +65,20 @@ ${chalk.bold('Options:')} ); } -async function generateHashFromFile(targetLoc) { +async function generateHashFromFile(targetLoc: string) { const longHash = await hasha.fromFile(targetLoc, {algorithm: 'md5'}); - return longHash.slice(0, 10); + return longHash!.slice(0, 10); } -function formatInstallResults(skipFailures): string { +function formatInstallResults(skipFailures: boolean): string { return installResults .map(([d, yn]) => (yn ? chalk.green(d) : skipFailures ? chalk.dim(d) : chalk.red(d))) .join(', '); } -function formatSize(size) { +function formatSize(size: number) { const kb = Math.round((size / 1000) * 100) / 100; - let color; + let color: 'green' | 'yellow' | 'red'; if (kb < 15) { color = 'green'; } else if (kb < 30) { @@ -89,7 +89,7 @@ function formatSize(size) { return chalk[color](`${kb} KB`); } -function formatDelta(delta) { +function formatDelta(delta: number) { const kb = Math.round(delta * 100) / 100; const color = delta > 0 ? 'red' : 'green'; return chalk[color](`Δ ${delta > 0 ? '+' : ''}${kb} KB`); @@ -105,7 +105,7 @@ function formatFileInfo( const filePath = fs.existsSync(commonPath) ? commonPath : path.join(cwd, 'web_modules', filename); const fileContent = fs.readFileSync(filePath, 'utf-8'); const gzipSize = zlib.gzipSync(fileContent).byteLength; - let brSize; + let brSize: number; if (zlib.brotliCompressSync) { brSize = zlib.brotliCompressSync(fileContent).byteLength; } @@ -113,17 +113,16 @@ function formatFileInfo( const lineName = filename.padEnd(padEnd); const fileStat = chalk.dim('[') + formatSize(stats.size) + chalk.dim(']'); const gzipStat = ' [gzip: ' + formatSize(gzipSize) + ']'; - const brotliStat = ' [brotli: ' + formatSize(brSize) + ']'; + const brotliStat = ' [brotli: ' + formatSize(brSize!) + ']'; const lineSize = zlib.brotliCompressSync ? fileStat + gzipStat + brotliStat : fileStat + gzipStat; const lineDelta = stats.delta ? chalk.dim(' [') + formatDelta(stats.delta) + chalk.dim(']') : ''; return ` ${lineGlyph} ${lineName} ${lineSize}${lineDelta}`; } function formatFiles(files: [string, DependencyStats][], title: string) { - const strippedFiles = files.map(([filename, stats]) => [ - filename.replace(/^common\//, ''), - stats, - ]) as [string, DependencyStats][]; + const strippedFiles = files.map( + ([filename, stats]) => [filename.replace(/^common\//, ''), stats] as const, + ); const maxFileNameLength = strippedFiles.reduce( (max, [filename]) => Math.max(filename.length, max), 0, @@ -138,7 +137,7 @@ ${strippedFiles function formatDependencyStats(): string { let output = ''; - const {direct, common} = dependencyStats; + const {direct, common} = dependencyStats!; const allDirect = Object.entries(direct); const allCommon = Object.entries(common); output += formatFiles(allDirect, 'web_modules/'); @@ -148,7 +147,7 @@ function formatDependencyStats(): string { return `\n${output}\n`; } -function logError(msg) { +function logError(msg: string) { if (!spinnerHasError) { spinner.stopAndPersist({symbol: chalk.cyan('⠼')}); } @@ -293,7 +292,7 @@ export async function install( rollup: userDefinedRollup, } = config; - const knownNamedExports = {...namedExports}; + const knownNamedExports = {...namedExports!}; for (const filePath of PACKAGES_TO_AUTO_DETECT_EXPORTS) { knownNamedExports[filePath] = knownNamedExports[filePath] || detectExports(filePath) || []; } @@ -304,8 +303,8 @@ export async function install( const allInstallSpecifiers = new Set(installTargets.map(dep => dep.specifier)); const depObject: {[targetName: string]: string} = {}; const assetObject: {[targetName: string]: string} = {}; - const importMap = {}; - const installTargetsMap = {}; + const importMap: {[installSpecifier: string]: string} = {}; + const installTargetsMap: {[targetLoc: string]: InstallTarget[]} = {}; const skipFailures = !isExplicit; for (const installSpecifier of allInstallSpecifiers) { try { @@ -349,7 +348,7 @@ export async function install( return false; } - const inputOptions = { + const inputOptions: InputOptions = { input: depObject, external: externalPackages, plugins: [ @@ -359,7 +358,7 @@ export async function install( 'process.env.NODE_ENV': isOptimized ? '"production"' : '"development"', }), rollupPluginNodeResolve({ - mainFields: ['browser:module', 'module', 'browser', !isStrict && 'main'].filter(Boolean), + mainFields: ['browser:module', 'module', 'browser', !isStrict && 'main'].filter(truthy), modulesOnly: isStrict, // Default: false extensions: ['.mjs', '.cjs', '.js', '.json'], // Default: [ '.mjs', '.js', '.json', '.node' ] // whether to prefer built-in modules (e.g. `fs`, `path`) or local ones with the same names @@ -398,9 +397,9 @@ export async function install( !!isOptimized && rollupPluginTreeshakeInputs(installTargets), !!isOptimized && rollupPluginTerser(), !!withStats && rollupPluginDependencyStats(info => (dependencyStats = info)), - ...userDefinedRollup.plugins, // load user-defined plugins last + ...userDefinedRollup!.plugins!, // load user-defined plugins last ], - onwarn: ((warning, warn) => { + onwarn(warning, warn) { if (warning.code === 'UNRESOLVED_IMPORT') { logError( `'${warning.source}' is imported by '${warning.importer}', but could not be resolved.`, @@ -424,21 +423,21 @@ export async function install( return; } warn(warning); - }) as any, + }, }; - const outputOptions = { + const outputOptions: OutputOptions = { dir: destLoc, - format: 'esm' as 'esm', - sourcemap: sourceMap === undefined ? isOptimized : sourceMap, - exports: 'named' as 'named', + format: 'esm', + sourcemap: sourceMap ?? isOptimized, + exports: 'named', chunkFileNames: 'common/[name]-[hash].js', }; if (Object.keys(depObject).length > 0) { try { - const packageBundle = await rollup.rollup(inputOptions); + const packageBundle = await rollup(inputOptions); await packageBundle.write(outputOptions); } catch (err) { - const {loc} = err as rollup.RollupError; + const {loc} = err as RollupError; if (!loc || !loc.file) { throw err; } @@ -459,7 +458,7 @@ export async function install( if (nomodule) { const nomoduleStart = Date.now(); - function rollupResolutionHelper() { + function rollupResolutionHelper(): Plugin { return { name: 'rename-import-plugin', resolveId(source) { @@ -470,7 +469,7 @@ export async function install( // resolve web_modules if (source.includes('/web_modules/')) { const suffix = source.split('/web_modules/')[1]; - return {id: path.join(destLoc, suffix)}; + return {id: path.join(destLoc!, suffix)}; } // null means try to resolve as-is return null; @@ -478,13 +477,13 @@ export async function install( }; } try { - const noModuleBundle = await rollup.rollup({ + const noModuleBundle = await rollup({ input: path.resolve(cwd, nomodule), inlineDynamicImports: true, - plugins: [...inputOptions.plugins, rollupResolutionHelper()], + plugins: [...inputOptions.plugins!, rollupResolutionHelper()], }); await noModuleBundle.write({ - file: path.resolve(destLoc, nomoduleOutput), + file: path.resolve(destLoc!, nomoduleOutput!), format: 'iife', name: 'App', }); @@ -505,7 +504,7 @@ export async function install( } } fs.writeFileSync( - path.join(destLoc, 'import-map.json'), + path.join(destLoc!, 'import-map.json'), JSON.stringify({imports: importMap}, undefined, 2), {encoding: 'utf8'}, ); @@ -561,7 +560,7 @@ export async function cli(args: string[]) { fs.existsSync(path.join(cwd, 'browserslist')); let isExplicit = false; - const installTargets = []; + const installTargets: InstallTarget[] = []; if (webDependencies) { isExplicit = true; diff --git a/src/rollup-plugin-dependency-info.ts b/src/rollup-plugin-dependency-info.ts index 5e32d9ec32..3ef435e9f2 100644 --- a/src/rollup-plugin-dependency-info.ts +++ b/src/rollup-plugin-dependency-info.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import {OutputOptions, OutputBundle} from 'rollup'; +import {OutputBundle, Plugin} from 'rollup'; export type DependencyStats = { size: number; @@ -12,7 +12,9 @@ type DependencyStatsMap = { type DependencyType = 'direct' | 'common'; export type DependencyStatsOutput = Record; -export function rollupPluginDependencyStats(cb: (dependencyInfo: DependencyStatsOutput) => void) { +export function rollupPluginDependencyStats( + cb: (dependencyInfo: DependencyStatsOutput) => void, +): Plugin { let outputDir: string; let cache: {[fileName: string]: number} = {}; let output: DependencyStatsOutput = { @@ -46,20 +48,22 @@ export function rollupPluginDependencyStats(cb: (dependencyInfo: DependencyStats } return { - generateBundle(options: OutputOptions, bundle: OutputBundle) { - outputDir = options.dir; + name: 'pika:rollup-plugin-dependency-info', + generateBundle(options, bundle) { + outputDir = options.dir!; buildCache(bundle); }, - writeBundle(bundle: OutputBundle) { - const files = Object.keys(bundle); + writeBundle(bundle) { + const directDependencies: string[] = []; + const commonDependencies: string[] = []; - const [directDependencies, commonDependencies] = files.reduce( - ([direct, common], fileName) => - fileName.startsWith('common') - ? [direct, [...common, fileName]] - : [[...direct, fileName], common], - [[], []], - ); + for (const fileName of Object.keys(bundle)) { + if (fileName.startsWith('common')) { + commonDependencies.push(fileName); + } else { + directDependencies.push(fileName); + } + } compareDependencies(directDependencies, 'direct'); compareDependencies(commonDependencies, 'common'); diff --git a/src/rollup-plugin-entrypoint-alias.ts b/src/rollup-plugin-entrypoint-alias.ts index 7e7359d17b..548e6cfc22 100644 --- a/src/rollup-plugin-entrypoint-alias.ts +++ b/src/rollup-plugin-entrypoint-alias.ts @@ -1,4 +1,5 @@ import path from 'path'; +import {Plugin} from 'rollup'; import {resolveDependencyManifest} from './util'; const IS_DEEP_PACKAGE_IMPORT = /^(@[\w-]+\/)?([\w-]+)\/(.*)/; @@ -12,14 +13,14 @@ const IS_DEEP_PACKAGE_IMPORT = /^(@[\w-]+\/)?([\w-]+)\/(.*)/; * Even though both eventually resolve to the same place, without this plugin * we lose the ability to mark "lit-html" as an external package. */ -export function rollupPluginEntrypointAlias({cwd}: {cwd: string}) { +export function rollupPluginEntrypointAlias({cwd}: {cwd: string}): Plugin { return { name: 'pika:rollup-plugin-entrypoint-alias', - resolveId(source: string, importer) { + resolveId(source, importer) { if (!IS_DEEP_PACKAGE_IMPORT.test(source)) { return null; } - const [, packageScope, packageName] = source.match(IS_DEEP_PACKAGE_IMPORT); + const [, packageScope, packageName] = source.match(IS_DEEP_PACKAGE_IMPORT)!; const packageFullName = packageScope ? `${packageScope}${packageName}` : packageName; const [, manifest] = resolveDependencyManifest(packageFullName, cwd); if (!manifest) { @@ -36,7 +37,7 @@ export function rollupPluginEntrypointAlias({cwd}: {cwd: string}) { return null; } - return this.resolve(packageFullName, importer, {skipSelf: true}).then(resolved => { + return this.resolve(packageFullName, importer!, {skipSelf: true}).then(resolved => { return resolved || null; }); }, diff --git a/src/rollup-plugin-remote-resolve.ts b/src/rollup-plugin-remote-resolve.ts index 54dbdbfc68..1af4821abe 100644 --- a/src/rollup-plugin-remote-resolve.ts +++ b/src/rollup-plugin-remote-resolve.ts @@ -1,3 +1,5 @@ +import {Plugin} from 'rollup'; + /** * rollup-plugin-remote-resolve * @@ -10,11 +12,11 @@ export function rollupPluginRemoteResolve({ }: { remoteUrl: string; remotePackages: [string, string][]; -}) { +}): Plugin { const remotePackageMap = new Map(remotePackages); return { name: 'pika:peer-dependency-resolver', - resolveId(source: string) { + resolveId(source) { if (remotePackageMap.has(source)) { let urlSourcePath = source; // NOTE(@fks): This is really Pika CDN specific, but no one else should be using this option. @@ -29,7 +31,7 @@ export function rollupPluginRemoteResolve({ } return null; }, - load(id) { + load() { return null; }, }; diff --git a/src/rollup-plugin-treeshake-inputs.ts b/src/rollup-plugin-treeshake-inputs.ts index c83dd1f4b9..05661a15d9 100644 --- a/src/rollup-plugin-treeshake-inputs.ts +++ b/src/rollup-plugin-treeshake-inputs.ts @@ -1,4 +1,4 @@ -import {InputOptions} from 'rollup'; +import {Plugin} from 'rollup'; import {InstallTarget} from './scan-imports'; import path from 'path'; @@ -12,31 +12,32 @@ import path from 'path'; * a. That virtual file contains only `export ... from 'ACTUAL_FILE_PATH';` exports * b. Rollup uses those exports to drive its tree-shaking algorithm. */ -export function rollupPluginTreeshakeInputs(allImports: InstallTarget[]) { +export function rollupPluginTreeshakeInputs(allImports: InstallTarget[]): Plugin { const installTargetsByFile: {[loc: string]: InstallTarget[]} = {}; return { name: 'pika:treeshake-inputs', // Mark some inputs for tree-shaking. - options(inputOptions: InputOptions) { - for (const [key, val] of Object.entries(inputOptions.input)) { + options(inputOptions) { + const input = inputOptions.input as {[entryAlias: string]: string}; + for (const [key, val] of Object.entries(input)) { installTargetsByFile[val] = allImports.filter(imp => imp.specifier === key); // If an input has known install targets, and none of those have "all=true", mark for treeshaking. if ( installTargetsByFile[val].length > 0 && !installTargetsByFile[val].some(imp => imp.all) ) { - inputOptions.input[key] = `pika-treeshake:${val}`; + input[key] = `pika-treeshake:${val}`; } } return inputOptions; }, - resolveId(source: string) { + resolveId(source) { if (source.startsWith('pika-treeshake:')) { return source; } return null; }, - load(id: string) { + load(id) { if (!id.startsWith('pika-treeshake:')) { return null; } diff --git a/src/scan-imports.ts b/src/scan-imports.ts index a03ed1d9b2..a4761798b3 100644 --- a/src/scan-imports.ts +++ b/src/scan-imports.ts @@ -3,6 +3,7 @@ import fs from 'fs'; import glob from 'glob'; import validatePackageName from 'validate-npm-package-name'; import {init as initESModuleLexer, parse, ImportSpecifier} from 'es-module-lexer'; +import {truthy} from './util'; const WEB_MODULES_TOKEN = 'web_modules/'; const WEB_MODULES_TOKEN_LENGTH = WEB_MODULES_TOKEN.length; @@ -50,7 +51,7 @@ function removeSpecifierQueryString(specifier: string) { return specifier; } -function getWebModuleSpecifierFromCode(code, imp: ImportSpecifier) { +function getWebModuleSpecifierFromCode(code: string, imp: ImportSpecifier) { if (imp.d > -1) { return code.substring(imp.s + 1, imp.e - 1); } @@ -97,10 +98,10 @@ function parseImportStatement(code: string, imp: ImportSpecifier): null | Instal const defaultImport = !dynamicImport && DEFAULT_IMPORT_REGEX.test(importStatement); const namespaceImport = !dynamicImport && importStatement.includes('*'); - const namedImports = (importStatement.match(HAS_NAMED_IMPORTS_REGEX) || [, ''])[1] + const namedImports = (importStatement.match(HAS_NAMED_IMPORTS_REGEX)! || [, ''])[1] .split(SPLIT_NAMED_IMPORTS_REGEX) .map(name => name.trim()) - .filter(Boolean); + .filter(truthy); return { specifier: webModuleSpecifier, @@ -115,7 +116,7 @@ function getInstallTargetsForFile(filePath: string, code: string): InstallTarget const [imports] = parse(code) || []; const allImports: InstallTarget[] = imports .map(imp => parseImportStatement(code, imp)) - .filter(Boolean); + .filter(truthy); return allImports; } diff --git a/src/util.ts b/src/util.ts index af2c7c705f..f400d02965 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,10 @@ import fs from 'fs'; import path from 'path'; +export function truthy(item: T | false | null | undefined): item is T { + return Boolean(item); +} + /** * Given a package name, look for that package's package.json manifest. * Return both the manifest location (if believed to exist) and the diff --git a/tsconfig.json b/tsconfig.json index b77cd1fa1c..223cb5bb1b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,10 @@ "target": "es2020", "module": "esnext", "moduleResolution": "node", - "esModuleInterop": true + "esModuleInterop": true, + "strict": true, + "noImplicitAny": false }, "include": ["src/**/*", "@types/**/*"], "exclude": ["node_modules"] -} \ No newline at end of file +}