Merge some standalone Vite entries into index.js (#37085)

Keep `swagger` and `external-render-helper` as a standalone entries for
external render.

- Move `devtest.ts` to `modules/` as init functions
- Make external renders correctly load its helper JS and Gitea's current theme
- Make external render iframe inherit Gitea's iframe's background color to avoid flicker
- Add e2e tests for external render and OpenAPI iframe

---------

Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-04-05 21:13:34 +02:00
committed by GitHub
parent 5f443184f3
commit a8938115d4
35 changed files with 419 additions and 247 deletions
+44 -30
View File
@@ -2,13 +2,14 @@ import {build, defineConfig} from 'vite';
import vuePlugin from '@vitejs/plugin-vue';
import {stringPlugin} from 'vite-string-plugin';
import {readFileSync, writeFileSync, mkdirSync, unlinkSync, globSync} from 'node:fs';
import path, {join, parse} from 'node:path';
import path, {basename, join, parse} from 'node:path';
import {env} from 'node:process';
import tailwindcss from 'tailwindcss';
import tailwindConfig from './tailwind.config.ts';
import wrapAnsi from 'wrap-ansi';
import licensePlugin from 'rollup-plugin-license';
import type {InlineConfig, Plugin, Rolldown} from 'vite';
import {camelize} from 'vue';
const isProduction = env.NODE_ENV !== 'development';
@@ -76,13 +77,14 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig {
};
}
const iifeEntry = join(import.meta.dirname, 'web_src/js/iife.ts');
function iifeBuildOpts({entryFileNames, write}: {entryFileNames: string, write?: boolean}) {
function iifeBuildOpts({sourceFileName, write}: {sourceFileName: string, write?: boolean}) {
const sourceBaseName = basename(sourceFileName, '.ts');
// HINT: VITE-OUTPUT-DIR: all outputted JS files are in "js" directory
const entryFileName = `js/${sourceBaseName}.[hash:8].js`;
return commonViteOpts({
build: {
lib: {entry: iifeEntry, formats: ['iife'], name: 'iife'},
rolldownOptions: {output: {entryFileNames}},
lib: {entry: join(import.meta.dirname, 'web_src/js', sourceFileName), name: camelize(sourceBaseName), formats: ['iife']},
rolldownOptions: {output: {entryFileNames: entryFileName}},
...(write === false && {write: false}),
},
plugins: [stringPlugin()],
@@ -91,19 +93,20 @@ function iifeBuildOpts({entryFileNames, write}: {entryFileNames: string, write?:
// Build iife.js as a blocking IIFE bundle. In dev mode, serves it from memory
// and rebuilds on file changes. In prod mode, writes to disk during closeBundle.
function iifePlugin(): Plugin {
let iifeCode = '';
let iifeMap = '';
function iifePlugin(sourceFileName: string): Plugin {
let iifeCode = '', iifeMap = '';
const iifeModules = new Set<string>();
let isBuilding = false;
const sourceBaseName = path.basename(sourceFileName, '.ts');
return {
name: 'iife',
name: `iife:${sourceFileName}`, // plugin name
async configureServer(server) {
const buildAndCache = async () => {
const result = await build(iifeBuildOpts({entryFileNames: 'js/iife.js', write: false}));
const result = await build(iifeBuildOpts({sourceFileName, write: false}));
const output = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput;
const chunk = output.output[0];
iifeCode = chunk.code.replace(/\/\/# sourceMappingURL=.*/, '//# sourceMappingURL=__vite_iife.js.map');
iifeCode = chunk.code.replace(/\/\/# sourceMappingURL=.*/, `//# sourceMappingURL=${sourceBaseName}.js.map`);
const mapAsset = output.output.find((o) => o.fileName.endsWith('.map'));
iifeMap = mapAsset && 'source' in mapAsset ? String(mapAsset.source) : '';
iifeModules.clear();
@@ -129,15 +132,15 @@ function iifePlugin(): Plugin {
});
server.middlewares.use((req, res, next) => {
// "__vite_iife" is a virtual file in memory, serve it directly
// on the dev server, an "iife" file is a virtual file in memory, serve it directly
const pathname = req.url!.split('?')[0];
if (pathname === '/web_src/js/__vite_dev_server_check') {
res.end('ok');
} else if (pathname === '/web_src/js/__vite_iife.js') {
} else if (pathname === `/web_src/js/${sourceFileName}`) {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Cache-Control', 'no-store');
res.end(iifeCode);
} else if (pathname === '/web_src/js/__vite_iife.js.map') {
} else if (pathname === `/web_src/js/${sourceBaseName}.js.map`) {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'no-store');
res.end(iifeMap);
@@ -147,29 +150,38 @@ function iifePlugin(): Plugin {
});
},
async closeBundle() {
for (const file of globSync('js/iife.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file));
const result = await build(iifeBuildOpts({entryFileNames: 'js/iife.[hash:8].js'}));
for (const file of globSync(`js/${sourceBaseName}.*.js*`, {cwd: outDir})) unlinkSync(join(outDir, file));
const result = await build(iifeBuildOpts({sourceFileName}));
const buildOutput = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput;
const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/iife.'));
const entry = buildOutput.output.find((o) => o.fileName.startsWith(`js/${sourceBaseName}.`));
if (!entry) throw new Error('IIFE build produced no output');
const manifestPath = join(outDir, '.vite', 'manifest.json');
writeFileSync(manifestPath, JSON.stringify({
...JSON.parse(readFileSync(manifestPath, 'utf8')),
'web_src/js/iife.ts': {file: entry.fileName, name: 'iife', isEntry: true},
}, null, 2));
const manifestData = JSON.parse(readFileSync(manifestPath, 'utf8'));
manifestData[`web_src/js/${sourceFileName}`] = {file: entry.fileName, name: sourceBaseName, isEntry: true};
writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2));
},
};
}
// In reduced sourcemap mode, only keep sourcemaps for main files
function reducedSourcemapPlugin(): Plugin {
const standalonePrefixes = [
'js/index.',
'js/iife.',
'js/swagger.',
'js/external-render-helper.',
'js/eventsource.sharedworker.',
];
return {
name: 'reduced-sourcemap',
apply: 'build',
closeBundle() {
if (enableSourcemap !== 'reduced') return;
for (const file of globSync('{js,css}/*.map', {cwd: outDir})) {
if (!file.startsWith('js/index.') && !file.startsWith('js/iife.')) unlinkSync(join(outDir, file));
if (standalonePrefixes.some((prefix) => file.startsWith(prefix))) continue;
unlinkSync(join(outDir, file));
}
},
};
@@ -215,6 +227,7 @@ export default defineConfig(commonViteOpts({
open: false,
host: '0.0.0.0',
strictPort: false,
cors: true,
fs: {
// VITE-DEV-SERVER-SECURITY: the dev server will be exposed to public by Gitea's web server, so we need to strictly limit the access
// Otherwise `/@fs/*` will be able to access any file (including app.ini which contains INTERNAL_TOKEN)
@@ -245,15 +258,15 @@ export default defineConfig(commonViteOpts({
rolldownOptions: {
input: {
index: join(import.meta.dirname, 'web_src/js/index.ts'),
swagger: join(import.meta.dirname, 'web_src/js/standalone/swagger.ts'),
'external-render-iframe': join(import.meta.dirname, 'web_src/js/standalone/external-render-iframe.ts'),
'eventsource.sharedworker': join(import.meta.dirname, 'web_src/js/features/eventsource.sharedworker.ts'),
...(!isProduction && {
devtest: join(import.meta.dirname, 'web_src/js/standalone/devtest.ts'),
}),
swagger: join(import.meta.dirname, 'web_src/js/swagger.ts'),
'eventsource.sharedworker': join(import.meta.dirname, 'web_src/js/eventsource.sharedworker.ts'),
devtest: join(import.meta.dirname, 'web_src/css/devtest.css'),
...themes,
},
output: {
// HINT: VITE-OUTPUT-DIR: all outputted JS files are in "js" directory
// So standalone/iife source files should also be in "js" directory,
// to keep consistent between production and dev server, avoid unexpected behaviors.
entryFileNames: 'js/[name].[hash:8].js',
chunkFileNames: 'js/[name].[hash:8].js',
assetFileNames: ({names}) => {
@@ -287,7 +300,8 @@ export default defineConfig(commonViteOpts({
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
},
plugins: [
iifePlugin(),
iifePlugin('iife.ts'),
iifePlugin('external-render-helper.ts'),
viteDevServerPortPlugin(),
reducedSourcemapPlugin(),
filterCssUrlPlugin(),