Skip to content

Commit

Permalink
feat: optimize custom extensions (#6801)
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy authored Mar 3, 2022
1 parent 4517c2b commit c11af23
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 37 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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 @@ -73,6 +76,9 @@ <h2>Alias with colon</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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const fs = require('fs')
const vue = require('@vitejs/plugin-vue')

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

plugins: [
vue(),
notjs(),
// for axios request test
{
name: 'mock',
Expand All @@ -51,3 +53,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
Original file line number Diff line number Diff line change
@@ -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',
contents,
resolveDir: root
}
Expand Down
64 changes: 44 additions & 20 deletions packages/vite/src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
}
5 changes: 5 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit c11af23

Please sign in to comment.