Skip to content

Commit

Permalink
fix: esbuild glob resolve error (#14533)
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red committed Oct 9, 2023
1 parent 5004d00 commit 3615c68
Showing 1 changed file with 116 additions and 104 deletions.
220 changes: 116 additions & 104 deletions packages/vite/src/node/optimizer/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
BuildContext,
BuildOptions,
Loader,
OnLoadArgs,
OnLoadResult,
Plugin,
} from 'esbuild'
Expand Down Expand Up @@ -382,116 +383,127 @@ function esbuildScanPlugin(
}
})

// extract scripts inside HTML-like files and treat it as a js module
build.onLoad(
{ filter: htmlTypesRE, namespace: 'html' },
async ({ path }) => {
let raw = await fsp.readFile(path, 'utf-8')
// Avoid matching the content of the comment
raw = raw.replace(commentRE, '<!---->')
const isHtml = path.endsWith('.html')
scriptRE.lastIndex = 0
let js = ''
let scriptId = 0
let match: RegExpExecArray | null
while ((match = scriptRE.exec(raw))) {
const [, openTag, content] = match
const typeMatch = openTag.match(typeRE)
const type =
typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3])
const langMatch = openTag.match(langRE)
const lang =
langMatch && (langMatch[1] || langMatch[2] || langMatch[3])
// skip non type module script
if (isHtml && type !== 'module') {
continue
}
// skip type="application/ld+json" and other non-JS types
if (
type &&
!(
type.includes('javascript') ||
type.includes('ecmascript') ||
type === 'module'
)
) {
continue
}
let loader: Loader = 'js'
if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
loader = lang
} else if (path.endsWith('.astro')) {
loader = 'ts'
}
const srcMatch = openTag.match(srcRE)
if (srcMatch) {
const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
js += `import ${JSON.stringify(src)}\n`
} else if (content.trim()) {
// The reason why virtual modules are needed:
// 1. There can be module scripts (`<script context="module">` in Svelte and `<script>` in Vue)
// or local scripts (`<script>` in Svelte and `<script setup>` in Vue)
// 2. There can be multiple module scripts in html
// We need to handle these separately in case variable names are reused between them

// append imports in TS to prevent esbuild from removing them
// since they may be used in the template
const contents =
content +
(loader.startsWith('ts') ? extractImportPaths(content) : '')

const key = `${path}?id=${scriptId++}`
if (contents.includes('import.meta.glob')) {
scripts[key] = {
loader: 'js', // since it is transpiled
contents: await doTransformGlobImport(contents, path, loader),
pluginData: {
htmlType: { loader },
},
}
} else {
scripts[key] = {
loader,
contents,
pluginData: {
htmlType: { loader },
},
}
const htmlTypeOnLoadCallback: (
args: OnLoadArgs,
) => Promise<OnLoadResult | null | undefined> = async ({ path: p }) => {
let raw = await fsp.readFile(p, 'utf-8')
// Avoid matching the content of the comment
raw = raw.replace(commentRE, '<!---->')
const isHtml = p.endsWith('.html')
scriptRE.lastIndex = 0
let js = ''
let scriptId = 0
let match: RegExpExecArray | null
while ((match = scriptRE.exec(raw))) {
const [, openTag, content] = match
const typeMatch = openTag.match(typeRE)
const type =
typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3])
const langMatch = openTag.match(langRE)
const lang =
langMatch && (langMatch[1] || langMatch[2] || langMatch[3])
// skip non type module script
if (isHtml && type !== 'module') {
continue
}
// skip type="application/ld+json" and other non-JS types
if (
type &&
!(
type.includes('javascript') ||
type.includes('ecmascript') ||
type === 'module'
)
) {
continue
}
let loader: Loader = 'js'
if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
loader = lang
} else if (p.endsWith('.astro')) {
loader = 'ts'
}
const srcMatch = openTag.match(srcRE)
if (srcMatch) {
const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
js += `import ${JSON.stringify(src)}\n`
} else if (content.trim()) {
// The reason why virtual modules are needed:
// 1. There can be module scripts (`<script context="module">` in Svelte and `<script>` in Vue)
// or local scripts (`<script>` in Svelte and `<script setup>` in Vue)
// 2. There can be multiple module scripts in html
// We need to handle these separately in case variable names are reused between them

// append imports in TS to prevent esbuild from removing them
// since they may be used in the template
const contents =
content +
(loader.startsWith('ts') ? extractImportPaths(content) : '')

const key = `${p}?id=${scriptId++}`
if (contents.includes('import.meta.glob')) {
scripts[key] = {
loader: 'js', // since it is transpiled
contents: await doTransformGlobImport(contents, p, loader),
resolveDir: normalizePath(path.dirname(p)),
pluginData: {
htmlType: { loader },
},
}

const virtualModulePath = JSON.stringify(
virtualModulePrefix + key,
)

const contextMatch = openTag.match(contextRE)
const context =
contextMatch &&
(contextMatch[1] || contextMatch[2] || contextMatch[3])

// Especially for Svelte files, exports in <script context="module"> means module exports,
// exports in <script> means component props. To avoid having two same export name from the
// star exports, we need to ignore exports in <script>
if (path.endsWith('.svelte') && context !== 'module') {
js += `import ${virtualModulePath}\n`
} else {
js += `export * from ${virtualModulePath}\n`
} else {
scripts[key] = {
loader,
contents,
resolveDir: normalizePath(path.dirname(p)),
pluginData: {
htmlType: { loader },
},
}
}
}

// This will trigger incorrectly if `export default` is contained
// anywhere in a string. Svelte and Astro files can't have
// `export default` as code so we know if it's encountered it's a
// false positive (e.g. contained in a string)
if (!path.endsWith('.vue') || !js.includes('export default')) {
js += '\nexport default {}'
}
const virtualModulePath = JSON.stringify(virtualModulePrefix + key)

return {
loader: 'js',
contents: js,
const contextMatch = openTag.match(contextRE)
const context =
contextMatch &&
(contextMatch[1] || contextMatch[2] || contextMatch[3])

// Especially for Svelte files, exports in <script context="module"> means module exports,
// exports in <script> means component props. To avoid having two same export name from the
// star exports, we need to ignore exports in <script>
if (p.endsWith('.svelte') && context !== 'module') {
js += `import ${virtualModulePath}\n`
} else {
js += `export * from ${virtualModulePath}\n`
}
}
},
}

// This will trigger incorrectly if `export default` is contained
// anywhere in a string. Svelte and Astro files can't have
// `export default` as code so we know if it's encountered it's a
// false positive (e.g. contained in a string)
if (!p.endsWith('.vue') || !js.includes('export default')) {
js += '\nexport default {}'
}

return {
loader: 'js',
contents: js,
}
}

// extract scripts inside HTML-like files and treat it as a js module
build.onLoad(
{ filter: htmlTypesRE, namespace: 'html' },
htmlTypeOnLoadCallback,
)
// the onResolve above will use namespace=html but esbuild doesn't
// call onResolve for glob imports and those will use namespace=file
// https://github.com/evanw/esbuild/issues/3317
build.onLoad(
{ filter: htmlTypesRE, namespace: 'file' },
htmlTypeOnLoadCallback,
)

// bare imports: record and externalize ----------------------------------
Expand Down

0 comments on commit 3615c68

Please sign in to comment.