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

fix: esbuild glob resolve error #14533

Merged
merged 1 commit into from
Oct 9, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -380,116 +381,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