();
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(),
diff --git a/web_src/css/standalone/devtest.css b/web_src/css/devtest.css
similarity index 100%
rename from web_src/css/standalone/devtest.css
rename to web_src/css/devtest.css
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index d0655af0027..e7a967a7c64 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -529,6 +529,7 @@ html[data-gitea-theme-dark="false"] .markup img[src*="#gh-dark-mode-only"] {
.external-render-iframe {
width: 100%;
height: max(300px, 80vh);
+ border: none;
}
.markup-content-iframe {
diff --git a/web_src/css/standalone/external-render-iframe.css b/web_src/css/standalone/external-render-iframe.css
deleted file mode 100644
index 2997587d820..00000000000
--- a/web_src/css/standalone/external-render-iframe.css
+++ /dev/null
@@ -1 +0,0 @@
-/* dummy */
diff --git a/web_src/css/standalone/swagger.css b/web_src/css/standalone/swagger.css
deleted file mode 100644
index e65af5ded63..00000000000
--- a/web_src/css/standalone/swagger.css
+++ /dev/null
@@ -1,46 +0,0 @@
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-
-body {
- margin: 0;
- background: #fff;
-}
-
-.swagger-back-link {
- color: #4990e2;
- text-decoration: none;
- position: absolute;
- top: 1rem;
- right: 1.5rem;
- display: flex;
- align-items: center;
-}
-
-@media (prefers-color-scheme: dark) {
- body {
- background: #1c2022;
- }
- .swagger-back-link {
- color: #51a8ff;
- }
- .swagger-ui table.headers td {
- color: #aeb4c4; /** fix low contrast */
- }
-}
-
-.swagger-back-link:hover {
- text-decoration: underline;
-}
-
-.swagger-back-link svg {
- color: inherit;
- fill: currentcolor;
- margin-right: 0.5rem;
-}
-
-.swagger-spec-content {
- display: none;
-}
diff --git a/web_src/css/swagger.css b/web_src/css/swagger.css
new file mode 100644
index 00000000000..c20eda7948d
--- /dev/null
+++ b/web_src/css/swagger.css
@@ -0,0 +1,41 @@
+@import "../../node_modules/swagger-ui-dist/swagger-ui.css";
+
+body {
+ margin: 0;
+}
+
+html,
+html body,
+html .swagger-ui,
+html .swagger-ui .scheme-container {
+ background: var(--gitea-iframe-bgcolor, var(--color-box-body)) !important;
+}
+
+/* swagger's bug: the selector was incorrectly written in "thead": "html.dark-mode .swagger-ui .opblock.opblock-get thead tr td" */
+html.dark-mode .swagger-ui table.headers td {
+ color: var(--color-text) !important;
+}
+
+.swagger-back-link {
+ color: var(--color-primary);
+ text-decoration: none;
+ position: absolute;
+ top: 1rem;
+ right: 1.5rem;
+ display: flex;
+ align-items: center;
+}
+
+.swagger-back-link:hover {
+ text-decoration: underline;
+}
+
+.swagger-back-link svg {
+ color: inherit;
+ fill: currentcolor;
+ margin-right: 0.5rem;
+}
+
+.swagger-spec-content {
+ display: none;
+}
diff --git a/web_src/js/features/eventsource.sharedworker.ts b/web_src/js/eventsource.sharedworker.ts
similarity index 100%
rename from web_src/js/features/eventsource.sharedworker.ts
rename to web_src/js/eventsource.sharedworker.ts
diff --git a/web_src/js/standalone/external-render-iframe.ts b/web_src/js/external-render-helper.ts
similarity index 61%
rename from web_src/js/standalone/external-render-iframe.ts
rename to web_src/js/external-render-helper.ts
index 3b489f8ee38..3acac8db141 100644
--- a/web_src/js/standalone/external-render-iframe.ts
+++ b/web_src/js/external-render-helper.ts
@@ -1,3 +1,7 @@
+// External render JS must be a IIFE module to run as early as possible to set up the environment for the content page.
+// Avoid unnecessary dependency.
+// Do NOT introduce global pollution, because the content page should be fully controlled by the external render.
+
/* To manually test:
[markup.in-iframe]
@@ -11,22 +15,39 @@ RENDER_COMMAND = `echo ' Toast | null>;
+
+export function initDevtest() {
+ registerGlobalInitFunc('initDevtestPage', () => {
+ const els = document.querySelectorAll('.toast-test-button');
+ if (!els.length) return;
+ const levelMap: LevelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast};
+ for (const el of els) {
+ el.addEventListener('click', () => {
+ const level = el.getAttribute('data-toast-level')!;
+ const message = el.getAttribute('data-toast-message')!;
+ levelMap[level](message);
+ });
+ }
+ });
+}
diff --git a/web_src/js/standalone/devtest.ts b/web_src/js/standalone/devtest.ts
deleted file mode 100644
index 20ab163d1a2..00000000000
--- a/web_src/js/standalone/devtest.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import '../../css/standalone/devtest.css';
-import {showInfoToast, showWarningToast, showErrorToast, type Toast} from '../modules/toast.ts';
-
-type LevelMap = Record Toast | null>;
-
-function initDevtestToast() {
- const levelMap: LevelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast};
- for (const el of document.querySelectorAll('.toast-test-button')) {
- el.addEventListener('click', () => {
- const level = el.getAttribute('data-toast-level')!;
- const message = el.getAttribute('data-toast-message')!;
- levelMap[level](message);
- });
- }
-}
-
-// NOTICE: keep in mind that this file is not in "index.js", they do not share the same module system.
-initDevtestToast();
diff --git a/web_src/js/standalone/swagger.ts b/web_src/js/standalone/swagger.ts
deleted file mode 100644
index fc44c8501ac..00000000000
--- a/web_src/js/standalone/swagger.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import '../../css/standalone/swagger.css';
-import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js';
-import 'swagger-ui-dist/swagger-ui.css';
-import {load as loadYaml} from 'js-yaml';
-
-const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
-const apply = () => document.documentElement.classList.toggle('dark-mode', prefersDark.matches);
-apply();
-prefersDark.addEventListener('change', apply);
-
-window.addEventListener('load', async () => {
- const elSwaggerUi = document.querySelector('#swagger-ui')!;
- const url = elSwaggerUi.getAttribute('data-source')!;
- let spec: any;
- if (url) {
- const res = await fetch(url); // eslint-disable-line no-restricted-globals
- spec = await res.json();
- } else {
- const elSpecContent = elSwaggerUi.querySelector('.swagger-spec-content')!;
- const filename = elSpecContent.getAttribute('data-spec-filename');
- const isJson = filename?.toLowerCase().endsWith('.json');
- spec = isJson ? JSON.parse(elSpecContent.value) : loadYaml(elSpecContent.value);
- }
-
- // Make the page's protocol be at the top of the schemes list
- const proto = window.location.protocol.slice(0, -1);
- if (spec?.schemes) {
- spec.schemes.sort((a: string, b: string) => {
- if (a === proto) return -1;
- if (b === proto) return 1;
- return 0;
- });
- }
-
- SwaggerUI({
- spec,
- dom_id: '#swagger-ui',
- deepLinking: true,
- docExpansion: 'none',
- defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
- presets: [
- SwaggerUI.presets.apis,
- ],
- plugins: [
- SwaggerUI.plugins.DownloadUrl,
- ],
- });
-});
diff --git a/web_src/js/swagger.ts b/web_src/js/swagger.ts
new file mode 100644
index 00000000000..b2f6a61030a
--- /dev/null
+++ b/web_src/js/swagger.ts
@@ -0,0 +1,70 @@
+// AVOID importing other unneeded main site JS modules to prevent unnecessary code and dependencies and chunks.
+//
+// Swagger JS is standalone because it is also used by external render like "File View -> OpenAPI render",
+// and it doesn't need any code from main site's modules (at the moment).
+//
+// In the future, if there are common utilities needed by both main site and standalone Swagger,
+// we can merge this standalone module into "index.ts", do pay attention to the following problems:
+// * HINT: SWAGGER-OPENAPI-VIEWER: there are different places rendering the swagger UI.
+// * Handle CSS styles carefully for different cases (standalone page, embedded in iframe)
+// * Take care of the JS code introduced by "index.ts" and "iife.ts", there might be global variable dependency and event listeners.
+
+import '../css/swagger.css';
+import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js';
+import 'swagger-ui-dist/swagger-ui.css';
+import {load as loadYaml} from 'js-yaml';
+
+function syncDarkModeClass(): void {
+ // if the viewer is embedded in an iframe (external render), use the parent's theme (passed via query param)
+ // otherwise, if it is for Gitea's API, it is a standalone page, use the site's theme (detected from theme CSS variable)
+ const url = new URL(window.location.href);
+ const giteaIsDarkTheme = url.searchParams.get('gitea-is-dark-theme') ??
+ window.getComputedStyle(document.documentElement).getPropertyValue('--is-dark-theme').trim();
+ const isDark = giteaIsDarkTheme ? giteaIsDarkTheme === 'true' : window.matchMedia('(prefers-color-scheme: dark)').matches;
+ document.documentElement.classList.toggle('dark-mode', isDark);
+}
+
+async function initSwaggerUI() {
+ // swagger-ui has built-in dark mode triggered by html.dark-mode class
+ syncDarkModeClass();
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncDarkModeClass);
+
+ const elSwaggerUi = document.querySelector('#swagger-ui')!;
+ const url = elSwaggerUi.getAttribute('data-source')!;
+ let spec: any;
+ if (url) {
+ const res = await fetch(url); // eslint-disable-line no-restricted-globals
+ spec = await res.json();
+ } else {
+ const elSpecContent = elSwaggerUi.querySelector('.swagger-spec-content')!;
+ const filename = elSpecContent.getAttribute('data-spec-filename');
+ const isJson = filename?.toLowerCase().endsWith('.json');
+ spec = isJson ? JSON.parse(elSpecContent.value) : loadYaml(elSpecContent.value);
+ }
+
+ // Make the page's protocol be at the top of the schemes list
+ const proto = window.location.protocol.slice(0, -1);
+ if (spec?.schemes) {
+ spec.schemes.sort((a: string, b: string) => {
+ if (a === proto) return -1;
+ if (b === proto) return 1;
+ return 0;
+ });
+ }
+
+ SwaggerUI({
+ spec,
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ docExpansion: 'none',
+ defaultModelRendering: 'model', // don't show examples by default, because they may be incomplete
+ presets: [
+ SwaggerUI.presets.apis,
+ ],
+ plugins: [
+ SwaggerUI.plugins.DownloadUrl,
+ ],
+ });
+}
+
+initSwaggerUI();