Skip to content

Commit bb85bd7

Browse files
dschmidtsapphi-redbluwy
authored
fix(build): ensure amd bundles request require to be injected (#20861)
Co-authored-by: sapphi-red <[email protected]> Co-authored-by: Bjorn Lu <[email protected]>
1 parent 29cdb39 commit bb85bd7

File tree

10 files changed

+210
-0
lines changed

10 files changed

+210
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { completeAmdWrapPlugin } from '../../plugins/completeAmdWrap'
3+
4+
async function createCompleteAmdWrapPluginRenderChunk() {
5+
const instance = completeAmdWrapPlugin()
6+
7+
return async (code: string) => {
8+
// @ts-expect-error transform.handler should exist
9+
const result = await instance.renderChunk.call(instance, code, 'foo.ts', {
10+
format: 'amd',
11+
})
12+
return result?.code || result
13+
}
14+
}
15+
16+
describe('completeAmdWrapPlugin', async () => {
17+
const renderChunk = await createCompleteAmdWrapPluginRenderChunk()
18+
19+
describe('adds require parameter', async () => {
20+
test('without other dependencies', async () => {
21+
expect(
22+
await renderChunk('define((function() { } ))'),
23+
).toMatchInlineSnapshot(`"define(["require"], (function(require) { } ))"`)
24+
})
25+
26+
test('with other dependencies', async () => {
27+
expect(
28+
await renderChunk(
29+
'define(["vue", "vue-router"], function(vue, vueRouter) { } ))',
30+
),
31+
).toMatchInlineSnapshot(
32+
`"define(["require", "vue", "vue-router"], (function(require, vue, vueRouter) { } ))"`,
33+
)
34+
})
35+
36+
test("only if require isn't injected already", async () => {
37+
expect(
38+
await renderChunk('define(["require"], function(require) { } ))'),
39+
).toMatchInlineSnapshot(`"define(["require"], (function(require) { } ))"`)
40+
41+
expect(
42+
await renderChunk(`define(['require'], function(require) { } ))`),
43+
).toMatchInlineSnapshot(`"define(['require'], (function(require) { } ))"`)
44+
})
45+
})
46+
})

packages/vite/src/node/build.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
resolveChokidarOptions,
6666
resolveEmptyOutDir,
6767
} from './watch'
68+
import { completeAmdWrapPlugin } from './plugins/completeAmdWrap'
6869
import { completeSystemWrapPlugin } from './plugins/completeSystemWrap'
6970
import { webWorkerPostPlugin } from './plugins/worker'
7071
import { getHookHandler } from './plugins'
@@ -477,6 +478,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{
477478
}> {
478479
return {
479480
pre: [
481+
completeAmdWrapPlugin(),
480482
completeSystemWrapPlugin(),
481483
...(!config.isWorker ? [prepareOutDirPlugin()] : []),
482484
perEnvironmentPlugin('commonjs', (environment) => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Plugin } from '../plugin'
2+
3+
/**
4+
* ensure amd bundles request `require` to be injected
5+
*/
6+
export function completeAmdWrapPlugin(): Plugin {
7+
const AmdWrapRE =
8+
/\bdefine\((?:\s*\[([^\]]*)\],)?\s*(?:\(\s*)?function\s*\(([^)]*)\)\s*\{/g
9+
10+
return {
11+
name: 'vite:force-amd-wrap-require',
12+
renderChunk(code, _chunk, opts) {
13+
if (opts.format !== 'amd') return
14+
15+
return {
16+
code: code.replace(AmdWrapRE, (_, deps, params) => {
17+
if (deps?.includes(`"require"`) || deps?.includes(`'require'`)) {
18+
return `define([${deps}], (function(${params}) {`
19+
}
20+
21+
const newDeps = deps ? `"require", ${deps}` : '"require"'
22+
const newParams = params.trim() ? `require, ${params}` : 'require'
23+
24+
return `define([${newDeps}], (function(${newParams}) {`
25+
}),
26+
map: null, // no need to generate sourcemap as no mapping exists for the wrapper
27+
}
28+
},
29+
}
30+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { browserLogs, getBg, isBuild, page } from '~utils'
3+
4+
describe.runIf(isBuild)('build', () => {
5+
test('should have no 404s', async () => {
6+
await page.waitForLoadState('networkidle')
7+
browserLogs.forEach((msg) => {
8+
expect(msg).not.toMatch('404')
9+
})
10+
})
11+
12+
test('asset url is correct with `base: "."`', async () => {
13+
await expect
14+
.poll(() => getBg('.assets'))
15+
.toMatch(/\/assets\/asset-[-\w]{8}\.png/)
16+
})
17+
})

playground/amd/asset.png

12.5 KB
Loading

playground/amd/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import asset from './asset.png'
2+
3+
export default function pluginMain() {
4+
return asset
5+
}

playground/amd/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@vitejs/test-amd",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"requirejs": "^2.3.7"
13+
}
14+
}

playground/amd/public/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>amd</title>
6+
</head>
7+
<body>
8+
<h1>AMD</h1>
9+
<p>An image should be shown below</p>
10+
<div
11+
class="assets"
12+
style="background-size: cover; height: 100px; width: 100px"
13+
></div>
14+
15+
<script src="/npm/requirejs.js"></script>
16+
<script type="text/javascript">
17+
requirejs(
18+
['assets/plugin.js'],
19+
(plugin) => {
20+
const url = plugin()
21+
document.querySelector('.assets').style.backgroundImage =
22+
`url(${url})`
23+
},
24+
(err) => {
25+
console.error('Error loading plugin', err)
26+
},
27+
)
28+
</script>
29+
</body>
30+
</html>

playground/amd/vite.config.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
3+
import { type Connect, defineConfig } from 'vite'
4+
5+
export default defineConfig({
6+
base: './',
7+
build: {
8+
outDir: 'dist/nested',
9+
rollupOptions: {
10+
preserveEntrySignatures: 'strict',
11+
input: {
12+
plugin: path.resolve(import.meta.dirname, './index.ts'),
13+
},
14+
output: {
15+
format: 'amd',
16+
entryFileNames: 'assets/[name].js',
17+
},
18+
},
19+
},
20+
plugins: [
21+
{
22+
name: 'serve-npm-code-directly',
23+
configureServer({ middlewares }) {
24+
middlewares.use(serveNpmCodeDirectlyMiddleware)
25+
},
26+
configurePreviewServer({ middlewares }) {
27+
middlewares.use(serveNpmCodeDirectlyMiddleware)
28+
},
29+
},
30+
],
31+
appType: 'mpa', // to cause 404 for incorrect URLs
32+
})
33+
34+
const npmDirectServeConfig = {
35+
'/npm/requirejs.js': 'requirejs/require.js',
36+
}
37+
const serveNpmCodeDirectlyMiddleware: Connect.NextHandleFunction = async (
38+
req,
39+
res,
40+
next,
41+
) => {
42+
for (const [url, file] of Object.entries(npmDirectServeConfig)) {
43+
if (req.originalUrl === url) {
44+
const code = await fs.readFile(
45+
new URL(`./node_modules/${file}`, import.meta.url),
46+
)
47+
res.setHeader('Content-Type', 'text/javascript')
48+
res.end(code)
49+
return
50+
}
51+
}
52+
next()
53+
}

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)