Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optimize custom extensions #6801

Merged
merged 8 commits into from Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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')
})
Expand Down
1 change: 1 addition & 0 deletions packages/playground/optimize-deps/dep-not-js/foo.js
@@ -0,0 +1 @@
export const foo = '[success] imported from .notjs file'
4 changes: 4 additions & 0 deletions packages/playground/optimize-deps/dep-not-js/index.notjs
@@ -0,0 +1,4 @@
<notjs>
import { foo } from './foo'
export const notjsValue = foo
</notjs>
6 changes: 6 additions & 0 deletions 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"
}
6 changes: 6 additions & 0 deletions packages/playground/optimize-deps/index.html
Expand Up @@ -38,6 +38,9 @@ <h2>Optimizing force included dep even when it's linked</h2>
<h2>import * as ...</h2>
<div class="import-star"></div>

<h2>Import from dependency with .notjs files</h2>
<div class="not-js"></div>

<h2>Dep w/ special file format supported via plugins</h2>
<div class="plugin"></div>

Expand Down Expand Up @@ -70,6 +73,9 @@ <h2>Nested include</h2>
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') {
Expand Down
1 change: 1 addition & 0 deletions packages/playground/optimize-deps/package.json
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions packages/playground/optimize-deps/vite.config.js
@@ -1,3 +1,4 @@
const fs = require('fs')
const vue = require('@vitejs/plugin-vue')

/**
Expand Down Expand Up @@ -36,6 +37,7 @@ module.exports = {

plugins: [
vue(),
notjs(),
// for axios request test
{
name: 'mock',
Expand All @@ -48,3 +50,39 @@ module.exports = {
}
]
}

// Handles .notjs file, basically remove wrapping <notjs> and </notjs> 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('<notjs>', '')
.replace('</notjs>', '')
return { contents, loader: 'js' }
})
}
}
]
}
}
}
},
transform(code, id) {
if (id.endsWith('.notjs')) {
code = code.replace('<notjs>', '').replace('</notjs>', '')
return { code }
}
}
}
}
15 changes: 10 additions & 5 deletions 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 {
Expand Down Expand Up @@ -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 })

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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',
bluwy marked this conversation as resolved.
Show resolved Hide resolved
contents,
resolveDir: root
}
Expand Down
64 changes: 44 additions & 20 deletions packages/vite/src/node/optimizer/index.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
33 changes: 21 additions & 12 deletions packages/vite/src/node/optimizer/scan.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -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'
}
})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 })
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
}
7 changes: 6 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.