From c11af23287d2af1c39b2ff64daa9f08fb406e08c Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Thu, 3 Mar 2022 17:35:58 +0800 Subject: [PATCH] feat: optimize custom extensions (#6801) --- .../__tests__/optimize-deps.spec.ts | 4 ++ .../optimize-deps/dep-not-js/foo.js | 1 + .../optimize-deps/dep-not-js/index.notjs | 4 ++ .../optimize-deps/dep-not-js/package.json | 6 ++ packages/playground/optimize-deps/index.html | 6 ++ .../playground/optimize-deps/package.json | 1 + .../playground/optimize-deps/vite.config.js | 38 +++++++++++ .../src/node/optimizer/esbuildDepPlugin.ts | 15 +++-- packages/vite/src/node/optimizer/index.ts | 64 +++++++++++++------ packages/vite/src/node/optimizer/scan.ts | 33 ++++++---- pnpm-lock.yaml | 5 ++ 11 files changed, 140 insertions(+), 37 deletions(-) create mode 100644 packages/playground/optimize-deps/dep-not-js/foo.js create mode 100644 packages/playground/optimize-deps/dep-not-js/index.notjs create mode 100644 packages/playground/optimize-deps/dep-not-js/package.json diff --git a/packages/playground/optimize-deps/__tests__/optimize-deps.spec.ts b/packages/playground/optimize-deps/__tests__/optimize-deps.spec.ts index ad063cd04bba35..992627c6374df0 100644 --- a/packages/playground/optimize-deps/__tests__/optimize-deps.spec.ts +++ b/packages/playground/optimize-deps/__tests__/optimize-deps.spec.ts @@ -62,6 +62,10 @@ test('import * from optimized dep', async () => { expect(await page.textContent('.import-star')).toMatch(`[success]`) }) +test('import from dep with .notjs files', async () => { + expect(await page.textContent('.not-js')).toMatch(`[success]`) +}) + test('dep with css import', async () => { expect(await getColor('h1')).toBe('red') }) diff --git a/packages/playground/optimize-deps/dep-not-js/foo.js b/packages/playground/optimize-deps/dep-not-js/foo.js new file mode 100644 index 00000000000000..3d809780820c7b --- /dev/null +++ b/packages/playground/optimize-deps/dep-not-js/foo.js @@ -0,0 +1 @@ +export const foo = '[success] imported from .notjs file' diff --git a/packages/playground/optimize-deps/dep-not-js/index.notjs b/packages/playground/optimize-deps/dep-not-js/index.notjs new file mode 100644 index 00000000000000..b4ef3a6936a797 --- /dev/null +++ b/packages/playground/optimize-deps/dep-not-js/index.notjs @@ -0,0 +1,4 @@ + +import { foo } from './foo' +export const notjsValue = foo + diff --git a/packages/playground/optimize-deps/dep-not-js/package.json b/packages/playground/optimize-deps/dep-not-js/package.json new file mode 100644 index 00000000000000..39ebafb6217b6e --- /dev/null +++ b/packages/playground/optimize-deps/dep-not-js/package.json @@ -0,0 +1,6 @@ +{ + "name": "dep-not-js", + "private": true, + "version": "1.0.0", + "main": "index.notjs" +} diff --git a/packages/playground/optimize-deps/index.html b/packages/playground/optimize-deps/index.html index 8451d968d2745b..e53e3375e79768 100644 --- a/packages/playground/optimize-deps/index.html +++ b/packages/playground/optimize-deps/index.html @@ -38,6 +38,9 @@

Optimizing force included dep even when it's linked

import * as ...

+

Import from dependency with .notjs files

+
+

Dep w/ special file format supported via plugins

@@ -73,6 +76,9 @@

Alias with colon

text('.import-star', `[success] ${keys.join(', ')}`) } + import { notjsValue } from 'dep-not-js' + text('.not-js', notjsValue) + import { createApp } from 'vue' import { createStore } from 'vuex' if (typeof createApp === 'function' && typeof createStore === 'function') { diff --git a/packages/playground/optimize-deps/package.json b/packages/playground/optimize-deps/package.json index 860474673685d3..09c578d684ffe7 100644 --- a/packages/playground/optimize-deps/package.json +++ b/packages/playground/optimize-deps/package.json @@ -17,6 +17,7 @@ "dep-esbuild-plugin-transform": "file:./dep-esbuild-plugin-transform", "dep-linked": "link:./dep-linked", "dep-linked-include": "link:./dep-linked-include", + "dep-not-js": "file:./dep-not-js", "lodash-es": "^4.17.21", "nested-exclude": "file:./nested-exclude", "phoenix": "^1.6.2", diff --git a/packages/playground/optimize-deps/vite.config.js b/packages/playground/optimize-deps/vite.config.js index 1a0ac5afab87c9..a989cf1961de11 100644 --- a/packages/playground/optimize-deps/vite.config.js +++ b/packages/playground/optimize-deps/vite.config.js @@ -1,3 +1,4 @@ +const fs = require('fs') const vue = require('@vitejs/plugin-vue') /** @@ -39,6 +40,7 @@ module.exports = { plugins: [ vue(), + notjs(), // for axios request test { name: 'mock', @@ -51,3 +53,39 @@ module.exports = { } ] } + +// Handles .notjs file, basically remove wrapping and tags +function notjs() { + return { + name: 'notjs', + config() { + return { + optimizeDeps: { + extensions: ['.notjs'], + esbuildOptions: { + plugins: [ + { + name: 'esbuild-notjs', + setup(build) { + build.onLoad({ filter: /\.notjs$/ }, ({ path }) => { + let contents = fs.readFileSync(path, 'utf-8') + contents = contents + .replace('', '') + .replace('', '') + return { contents, loader: 'js' } + }) + } + } + ] + } + } + } + }, + transform(code, id) { + if (id.endsWith('.notjs')) { + code = code.replace('', '').replace('', '') + return { code } + } + } + } +} diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index 6c778e2c8bf8d3..3ff86c213a54a2 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -1,5 +1,5 @@ import path from 'path' -import type { Loader, Plugin, ImportKind } from 'esbuild' +import type { Plugin, ImportKind } from 'esbuild' import { KNOWN_ASSET_TYPES } from '../constants' import type { ResolvedConfig } from '..' import { @@ -40,6 +40,13 @@ export function esbuildDepPlugin( config: ResolvedConfig, ssr?: boolean ): Plugin { + // remove optimizable extensions from `externalTypes` list + const allExternalTypes = config.optimizeDeps.extensions + ? externalTypes.filter( + (type) => !config.optimizeDeps.extensions?.includes('.' + type) + ) + : externalTypes + // default resolver which prefers ESM const _resolve = config.createResolver({ asSrc: false }) @@ -74,7 +81,7 @@ export function esbuildDepPlugin( // externalize assets and commonly known non-js file types build.onResolve( { - filter: new RegExp(`\\.(` + externalTypes.join('|') + `)(\\?.*)?$`) + filter: new RegExp(`\\.(` + allExternalTypes.join('|') + `)(\\?.*)?$`) }, async ({ path: id, importer, kind }) => { const resolved = await resolve(id, importer, kind) @@ -181,10 +188,8 @@ export function esbuildDepPlugin( } } - let ext = path.extname(entryFile).slice(1) - if (ext === 'mjs') ext = 'js' return { - loader: ext as Loader, + loader: 'js', contents, resolveDir: root } diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index da613273c4aa30..302ce66842a276 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -79,6 +79,16 @@ export interface DepOptimizationOptions { * @deprecated use `esbuildOptions.keepNames` */ keepNames?: boolean + /** + * List of file extensions that can be optimized. A corresponding esbuild + * plugin must exist to handle the specific extension. + * + * By default, Vite can optimize `.mjs`, `.js`, and `.ts` files. This option + * allows specifying additional extensions. + * + * @experimental + */ + extensions?: string[] } export interface DepOptimizationMetadata { @@ -244,29 +254,43 @@ export async function optimizeDeps( for (const id in deps) { const flatId = flattenId(id) const filePath = (flatIdDeps[flatId] = deps[id]) - const entryContent = fs.readFileSync(filePath, 'utf-8') let exportsData: ExportsData - try { - exportsData = parse(entryContent) as ExportsData - } catch { - debug( - `Unable to parse dependency: ${id}. Trying again with a JSX transform.` - ) - const transformed = await transformWithEsbuild(entryContent, filePath, { - loader: 'jsx' + if (config.optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { + // For custom supported extensions, build the entry file to transform it into JS, + // and then parse with es-module-lexer. Note that the `bundle` option is not `true`, + // so only the entry file is being transformed. + const result = await build({ + ...esbuildOptions, + plugins, + entryPoints: [filePath], + write: false, + format: 'esm' }) - // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. - // This is useful for packages such as Gatsby. - esbuildOptions.loader = { - '.js': 'jsx', - ...esbuildOptions.loader + exportsData = parse(result.outputFiles[0].text) as ExportsData + } else { + const entryContent = fs.readFileSync(filePath, 'utf-8') + try { + exportsData = parse(entryContent) as ExportsData + } catch { + debug( + `Unable to parse dependency: ${id}. Trying again with a JSX transform.` + ) + const transformed = await transformWithEsbuild(entryContent, filePath, { + loader: 'jsx' + }) + // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. + // This is useful for packages such as Gatsby. + esbuildOptions.loader = { + '.js': 'jsx', + ...esbuildOptions.loader + } + exportsData = parse(transformed.code) as ExportsData } - exportsData = parse(transformed.code) as ExportsData - } - for (const { ss, se } of exportsData[0]) { - const exp = entryContent.slice(ss, se) - if (/export\s+\*\s+from/.test(exp)) { - exportsData.hasReExports = true + for (const { ss, se } of exportsData[0]) { + const exp = entryContent.slice(ss, se) + if (/export\s+\*\s+from/.test(exp)) { + exportsData.hasReExports = true + } } } idToExports[id] = exportsData diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 280776b2e7d38d..b0f5f7985b0f78 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -181,6 +181,10 @@ function esbuildScanPlugin( '@vite/env' ] + const isOptimizable = (id: string) => + OPTIMIZABLE_ENTRY_RE.test(id) || + !!config.optimizeDeps.extensions?.some((ext) => id.endsWith(ext)) + const externalUnlessEntry = ({ path }: { path: string }) => ({ path, external: !entries.includes(path) @@ -218,8 +222,14 @@ function esbuildScanPlugin( // html types: extract script contents ----------------------------------- build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => { + const resolved = await resolve(path, importer) + if (!resolved) return + // It is possible for the scanner to scan html types in node_modules. + // If we can optimize this html type, skip it so it's handled by the + // bare import resolve, and recorded as optimization dep. + if (resolved.includes('node_modules') && isOptimizable(resolved)) return return { - path: await resolve(path, importer), + path: resolved, namespace: 'html' } }) @@ -340,17 +350,19 @@ function esbuildScanPlugin( } if (resolved.includes('node_modules') || include?.includes(id)) { // dependency or forced included, externalize and stop crawling - if (OPTIMIZABLE_ENTRY_RE.test(resolved)) { + if (isOptimizable(resolved)) { depImports[id] = resolved } return externalUnlessEntry({ path: id }) - } else { + } else if (isScannable(resolved)) { const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined // linked package, keep crawling return { path: path.resolve(resolved), namespace } + } else { + return externalUnlessEntry({ path: id }) } } else { missing[id] = normalizePath(importer) @@ -396,7 +408,7 @@ function esbuildScanPlugin( // use vite resolver to support urls and omitted extensions const resolved = await resolve(id, importer) if (resolved) { - if (shouldExternalizeDep(resolved, id)) { + if (shouldExternalizeDep(resolved, id) || !isScannable(resolved)) { return externalUnlessEntry({ path: id }) } @@ -499,10 +511,7 @@ function extractImportPaths(code: string) { return js } -export function shouldExternalizeDep( - resolvedId: string, - rawId: string -): boolean { +function shouldExternalizeDep(resolvedId: string, rawId: string): boolean { // not a valid file path if (!path.isAbsolute(resolvedId)) { return true @@ -511,9 +520,9 @@ export function shouldExternalizeDep( if (resolvedId === rawId || resolvedId.includes('\0')) { return true } - // resolved is not a scannable type - if (!JS_TYPES_RE.test(resolvedId) && !htmlTypesRE.test(resolvedId)) { - return true - } return false } + +function isScannable(id: string): boolean { + return JS_TYPES_RE.test(id) || htmlTypesRE.test(id) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7a9a8e96dc686..9982e48276c096 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,6 +295,7 @@ importers: dep-esbuild-plugin-transform: file:./dep-esbuild-plugin-transform dep-linked: link:./dep-linked dep-linked-include: link:./dep-linked-include + dep-not-js: file:./dep-not-js lodash-es: ^4.17.21 nested-exclude: file:./nested-exclude phoenix: ^1.6.2 @@ -312,6 +313,7 @@ importers: dep-esbuild-plugin-transform: link:dep-esbuild-plugin-transform dep-linked: link:dep-linked dep-linked-include: link:dep-linked-include + dep-not-js: link:dep-not-js lodash-es: 4.17.21 nested-exclude: link:nested-exclude phoenix: 1.6.5 @@ -345,6 +347,9 @@ importers: dependencies: react: 17.0.2 + packages/playground/optimize-deps/dep-not-js: + specifiers: {} + packages/playground/optimize-deps/nested-exclude: specifiers: nested-include: link:./nested-include