diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fdc71fe42f0f0..71a4aa09841ebe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -214,6 +214,8 @@ Vite aims to be fully usable as a dependency in a TypeScript project (e.g. it sh To get around this, we inline some of these dependencies' types in `packages/vite/types`. This way we can still expose the typing but bundle the dependency's source code. +Use `pnpm run check-dist-types` to check bundled types does not rely on types in `devDependencies`. If you are adding `dependencies`, make sure to configure `tsconfig.check.json`. + ### Think before adding yet another option We already have many config options, and we should avoid fixing an issue by adding yet another one. Before adding an option, try to think about: diff --git a/packages/vite/package.json b/packages/vite/package.json index 5db998d0c295dd..badcf7bc5d4033 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -49,10 +49,11 @@ "dev": "rimraf dist && pnpm run build-bundle -w", "build": "rimraf dist && run-s build-bundle build-types", "build-bundle": "rollup --config rollup.config.ts --configPlugin typescript", - "build-types": "run-s build-temp-types patch-types roll-types", + "build-types": "run-s build-temp-types patch-types roll-types check-dist-types", "build-temp-types": "tsc --emitDeclarationOnly --outDir temp/node -p src/node", "patch-types": "esno scripts/patchTypes.ts", "roll-types": "api-extractor run && rimraf temp", + "check-dist-types": "tsc --project tsconfig.check.json", "lint": "eslint --ext .ts src/**", "format": "prettier --write --parser typescript \"src/**/*.ts\"", "prepublishOnly": "npm run build" diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ff8c0ebdf6abab..1fc42c3ab00d78 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -37,6 +37,8 @@ export type { DepOptimizationProcessing, OptimizedDepInfo, DepsOptimizer, + EsModuleLexerImportSpecifier, + EsModuleLexerParseReturnType, ExportsData } from './optimizer' export type { Plugin } from './plugin' diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 3cece89d1d7676..034b5365c981f2 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -6,6 +6,7 @@ import colors from 'picocolors' import type { BuildOptions as EsbuildBuildOptions } from 'esbuild' import { build } from 'esbuild' import { init, parse } from 'es-module-lexer' +import type { ImportSpecifier as EsModuleLexerImportSpecifier } from 'types/es-module-lexer' import type { ResolvedConfig } from '../config' import { createDebugger, @@ -31,7 +32,13 @@ const isDebugEnabled = _debug('vite:deps').enabled const jsExtensionRE = /\.js$/i const jsMapExtensionRE = /\.js\.map$/i -export type ExportsData = ReturnType & { +export type { EsModuleLexerImportSpecifier } +export type EsModuleLexerParseReturnType = readonly [ + imports: ReadonlyArray, + exports: ReadonlyArray, + facade: boolean +] +export type ExportsData = EsModuleLexerParseReturnType & { // es-module-lexer has a facade detection but isn't always accurate for our // use case when the module has default export hasReExports?: true diff --git a/packages/vite/tsconfig.check.json b/packages/vite/tsconfig.check.json new file mode 100644 index 00000000000000..cd8f8d6eeb3f2c --- /dev/null +++ b/packages/vite/tsconfig.check.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "noEmit": true, + "moduleResolution": "classic", + // Only add entries to `paths` when you are adding/updating dependencies (not devDependencies) + // See CONTRIBUTING.md "Ensure type support" for more details + "paths": { + // direct + "rollup": ["./node_modules/rollup/dist/rollup.d.ts"], + // direct + "esbuild": ["./node_modules/esbuild/lib/main.d.ts"], + // direct + "postcss": ["./node_modules/postcss/lib/postcss.d.ts"], + // indirect: postcss depends on it + "source-map-js": ["./node_modules/source-map-js/source-map.d.ts"] + }, + "typeRoots": [] + }, + "include": ["dist/**/*.d.ts"] +} diff --git a/packages/vite/types/es-module-lexer.d.ts b/packages/vite/types/es-module-lexer.d.ts new file mode 100644 index 00000000000000..8b405471b1e42f --- /dev/null +++ b/packages/vite/types/es-module-lexer.d.ts @@ -0,0 +1,90 @@ +// Modified and inlined to avoid extra dependency +// Source: https://github.com/guybedford/es-module-lexer/blob/main/types/lexer.d.ts +// MIT Licensed https://github.com/guybedford/es-module-lexer/blob/main/LICENSE + +export interface ImportSpecifier { + /** + * Module name + * + * To handle escape sequences in specifier strings, the .n field of imported specifiers will be provided where possible. + * + * For dynamic import expressions, this field will be empty if not a valid JS string. + * + * @example + * const [imports1, exports1] = parse(String.raw`import './\u0061\u0062.js'`); + * imports1[0].n; + * // Returns "./ab.js" + * + * const [imports2, exports2] = parse(`import("./ab.js")`); + * imports2[0].n; + * // Returns "./ab.js" + * + * const [imports3, exports3] = parse(`import("./" + "ab.js")`); + * imports3[0].n; + * // Returns undefined + */ + readonly n: string | undefined + /** + * Start of module specifier + * + * @example + * const source = `import { a } from 'asdf'`; + * const [imports, exports] = parse(source); + * source.substring(imports[0].s, imports[0].e); + * // Returns "asdf" + */ + readonly s: number + /** + * End of module specifier + */ + readonly e: number + + /** + * Start of import statement + * + * @example + * const source = `import { a } from 'asdf'`; + * const [imports, exports] = parse(source); + * source.substring(imports[0].ss, imports[0].se); + * // Returns `"import { a } from 'asdf';"` + */ + readonly ss: number + /** + * End of import statement + */ + readonly se: number + + /** + * If this import statement is a dynamic import, this is the start value. + * Otherwise this is `-1`. + */ + readonly d: number + + /** + * If this import has an import assertion, this is the start value. + * Otherwise this is `-1`. + */ + readonly a: number +} + +/** + * Wait for init to resolve before calling `parse`. + */ +export const init: Promise + +/** + * Outputs the list of exports and locations of import specifiers, + * including dynamic import and import meta handling. + * + * @param source - Source code to parser + * @param name - Optional sourcename + * @returns Tuple contaning imports list and exports list. + */ +export function parse( + source: string, + name?: string +): readonly [ + imports: ReadonlyArray, + exports: ReadonlyArray, + facade: boolean +]