mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Storage buckets, indexer names, the bleve analyzer, the lock prefix, SSH host keys, the markdown front-matter key, the container data paths, the RPM repo id and the API security scheme all read as this product now, and app.example.ini matches the code again. The upstream maintainer list and the changelog generator config went with the release history they belonged to, and the citation comments state the fact rather than link the issue. Three inert rules turn out to have been inert because they named a module path this tree does not use: gci grouped no first-party imports under prefix(), and depguard let models/migrations import models and modules/structs. They now name github.com/hanzoai/git. The config example also documented .gitea/workflows while the code has read .hanzo/workflows for some time. The authorized_keys marker follows the delegate-hook precedent: the new name is written and the old one is still recognised, because a marker we do not know is copied through as a hand-added key and leaves a revoked key authorized. The stored user_setting key keeps its value; only a migration may change that. Co-authored-by: Hanzo Dev <[email protected]>
356 lines
12 KiB
TypeScript
356 lines
12 KiB
TypeScript
import {build, defineConfig} from 'vite';
|
|
import vuePlugin from '@vitejs/plugin-vue';
|
|
import {stringPlugin} from 'vite-string-plugin';
|
|
import {licensePlugin, wrap} from 'rolldown-license-plugin';
|
|
import {readFileSync, writeFileSync, mkdirSync, unlinkSync, globSync} from 'node:fs';
|
|
import path, {basename, join, parse} from 'node:path';
|
|
import {env} from 'node:process';
|
|
import tailwindcss from 'tailwindcss';
|
|
import tailwindConfig from './tailwind.config.ts';
|
|
import type {InlineConfig, Plugin, Rolldown} from 'vite';
|
|
import {camelize} from 'vue';
|
|
|
|
const isProduction = env.NODE_ENV !== 'development';
|
|
|
|
// ENABLE_SOURCEMAP accepts the following values:
|
|
// true - all sourcemaps enabled, the default in development
|
|
// reduced - sourcemaps only for index.js, the default in production
|
|
// false - all sourcemaps disabled
|
|
let enableSourcemap: string;
|
|
if ('ENABLE_SOURCEMAP' in env) {
|
|
enableSourcemap = ['true', 'false'].includes(env.ENABLE_SOURCEMAP!) ? env.ENABLE_SOURCEMAP! : 'reduced';
|
|
} else {
|
|
enableSourcemap = isProduction ? 'reduced' : 'true';
|
|
}
|
|
const outDir = join(import.meta.dirname, 'public/assets');
|
|
|
|
const themes: Record<string, string> = {};
|
|
for (const path of globSync('web_src/css/themes/*.css', {cwd: import.meta.dirname})) {
|
|
themes[parse(path).name] = join(import.meta.dirname, path);
|
|
}
|
|
|
|
const webComponents = new Set([
|
|
// our own, in web_src/js/webcomponents
|
|
'overflow-menu',
|
|
'relative-time',
|
|
// from dependencies
|
|
'markdown-toolbar',
|
|
'text-expander',
|
|
]);
|
|
|
|
function failOnWarningsPlugin(): Rolldown.Plugin {
|
|
let warningCount = 0;
|
|
return {
|
|
name: 'fail-on-warnings',
|
|
onLog(level) {
|
|
if (level === 'warn') warningCount++;
|
|
},
|
|
closeBundle() {
|
|
if (!warningCount) return;
|
|
throw new Error(`${warningCount} warnings present`);
|
|
},
|
|
};
|
|
}
|
|
|
|
const commonRolldownOptions: Rolldown.RolldownOptions = {
|
|
checks: {
|
|
pluginTimings: false,
|
|
},
|
|
...(env.CI && {plugins: [failOnWarningsPlugin()]}),
|
|
};
|
|
|
|
function commonViteOpts({build, ...other}: InlineConfig): InlineConfig {
|
|
const {rolldownOptions, ...otherBuild} = build || {};
|
|
return {
|
|
base: './', // make all asset URLs relative, so it works in subdirectory deployments
|
|
configFile: false,
|
|
root: import.meta.dirname,
|
|
publicDir: false,
|
|
build: {
|
|
outDir,
|
|
emptyOutDir: false,
|
|
sourcemap: enableSourcemap !== 'false',
|
|
target: 'es2020',
|
|
minify: isProduction ? 'oxc' : false,
|
|
cssMinify: isProduction ? 'esbuild' : false,
|
|
chunkSizeWarningLimit: Infinity,
|
|
assetsInlineLimit: 32768,
|
|
reportCompressedSize: false,
|
|
rolldownOptions: {
|
|
...commonRolldownOptions,
|
|
...rolldownOptions,
|
|
},
|
|
...otherBuild,
|
|
},
|
|
...other,
|
|
};
|
|
}
|
|
|
|
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: join(import.meta.dirname, 'web_src/js', sourceFileName), name: camelize(sourceBaseName), formats: ['iife']},
|
|
rolldownOptions: {output: {entryFileNames: entryFileName}},
|
|
...(write === false && {write: false}),
|
|
},
|
|
plugins: [stringPlugin()],
|
|
});
|
|
}
|
|
|
|
// 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 and updates "manifest.json".
|
|
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:${sourceFileName}`, // plugin name
|
|
async configureServer(server) {
|
|
const buildAndCache = async () => {
|
|
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=${sourceBaseName}.js.map`);
|
|
const mapAsset = output.output.find((o) => o.fileName.endsWith('.map'));
|
|
iifeMap = mapAsset && 'source' in mapAsset ? String(mapAsset.source) : '';
|
|
iifeModules.clear();
|
|
for (const id of Object.keys(chunk.modules)) iifeModules.add(id);
|
|
};
|
|
await buildAndCache();
|
|
|
|
let needsRebuild = false;
|
|
server.watcher.on('change', async (path) => {
|
|
if (!iifeModules.has(path)) return;
|
|
needsRebuild = true;
|
|
if (isBuilding) return;
|
|
isBuilding = true;
|
|
try {
|
|
do {
|
|
needsRebuild = false;
|
|
await buildAndCache();
|
|
} while (needsRebuild);
|
|
server.ws.send({type: 'full-reload'});
|
|
} finally {
|
|
isBuilding = false;
|
|
}
|
|
});
|
|
|
|
server.middlewares.use((req, res, next) => {
|
|
// 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/${sourceFileName}`) {
|
|
res.setHeader('Content-Type', 'application/javascript');
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
res.end(iifeCode);
|
|
} else if (pathname === `/web_src/js/${sourceBaseName}.js.map`) {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Cache-Control', 'no-store');
|
|
res.end(iifeMap);
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
},
|
|
async writeBundle() {
|
|
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/${sourceBaseName}.`));
|
|
if (!entry) throw new Error('IIFE build produced no output');
|
|
|
|
const manifestPath = join(outDir, '.vite', 'manifest.json');
|
|
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-frontend.',
|
|
'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 (standalonePrefixes.some((prefix) => file.startsWith(prefix))) continue;
|
|
unlinkSync(join(outDir, file));
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
// Filter out legacy font formats from CSS, keeping only woff2
|
|
function filterCssUrlPlugin(): Plugin {
|
|
return {
|
|
name: 'filter-css-url',
|
|
enforce: 'pre',
|
|
transform(code, id) {
|
|
if (!id.endsWith('.css') || !id.includes('katex')) return null;
|
|
return code.replace(/,\s*url\([^)]*\.(?:woff|ttf)\)\s*format\("[^"]*"\)/gi, '');
|
|
},
|
|
};
|
|
}
|
|
|
|
const viteDevServerPort = Number(env.FRONTEND_DEV_SERVER_PORT) || 3001;
|
|
const viteDevPortFilePath = join(outDir, '.vite', 'dev-port');
|
|
|
|
// Write the Vite dev server's actual port to a file so the Go server can discover it for proxying.
|
|
function viteDevServerPortPlugin(): Plugin {
|
|
return {
|
|
name: 'vite-dev-server-port',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
server.httpServer!.once('listening', () => {
|
|
const addr = server.httpServer!.address();
|
|
if (typeof addr === 'object' && addr) {
|
|
mkdirSync(path.dirname(viteDevPortFilePath), {recursive: true});
|
|
writeFileSync(viteDevPortFilePath, String(addr.port));
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig(commonViteOpts({
|
|
appType: 'custom', // Go serves all HTML, disable Vite's HTML handling
|
|
clearScreen: false,
|
|
server: {
|
|
port: viteDevServerPort,
|
|
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 the 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)
|
|
strict: true,
|
|
allow: [
|
|
'assets',
|
|
'node_modules',
|
|
'public',
|
|
'web_src',
|
|
// do not add any other directories here, unless you are absolutely sure it's safe to expose them to the public
|
|
],
|
|
},
|
|
headers: {
|
|
'Cache-Control': 'no-store', // prevent browser disk cache
|
|
},
|
|
warmup: {
|
|
clientFiles: [
|
|
// warmup the important entry points
|
|
'web_src/js/index.ts',
|
|
'web_src/css/index.css',
|
|
'web_src/css/themes/*.css',
|
|
],
|
|
},
|
|
},
|
|
build: {
|
|
modulePreload: false,
|
|
manifest: true,
|
|
rolldownOptions: {
|
|
input: {
|
|
index: join(import.meta.dirname, 'web_src/js/index.ts'),
|
|
swagger: join(import.meta.dirname, 'web_src/js/swagger.ts'),
|
|
'external-render-frontend': join(import.meta.dirname, 'web_src/js/external-render-frontend.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}) => {
|
|
const name = names[0];
|
|
if (name.endsWith('.css')) return 'css/[name].[hash:8].css';
|
|
if (/\.(ttf|woff2?)$/.test(name)) return 'fonts/[name].[hash:8].[ext]';
|
|
return '[name].[hash:8].[ext]';
|
|
},
|
|
},
|
|
},
|
|
},
|
|
worker: {
|
|
rolldownOptions: {
|
|
...commonRolldownOptions,
|
|
output: {
|
|
entryFileNames: 'js/[name].[hash:8].js',
|
|
},
|
|
},
|
|
},
|
|
css: {
|
|
transformer: 'postcss',
|
|
postcss: {
|
|
plugins: [
|
|
tailwindcss(tailwindConfig),
|
|
],
|
|
},
|
|
},
|
|
define: {
|
|
__VUE_OPTIONS_API__: true,
|
|
__VUE_PROD_DEVTOOLS__: false,
|
|
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
|
|
},
|
|
plugins: [
|
|
iifePlugin('iife.ts'),
|
|
iifePlugin('external-render-helper.ts'),
|
|
viteDevServerPortPlugin(),
|
|
reducedSourcemapPlugin(),
|
|
filterCssUrlPlugin(),
|
|
stringPlugin(),
|
|
vuePlugin({
|
|
template: {
|
|
compilerOptions: {
|
|
isCustomElement: (tag) => webComponents.has(tag),
|
|
},
|
|
},
|
|
}),
|
|
isProduction ? licensePlugin({
|
|
done(deps, context) {
|
|
const line = '-'.repeat(80);
|
|
const goLicenses = JSON.parse(readFileSync(join(import.meta.dirname, 'assets/go-licenses.json'), 'utf8'));
|
|
const combined: Record<string, string> = {};
|
|
for (const {name, licenseText} of goLicenses) {
|
|
combined[name] = wrap(licenseText || '', 80).trim();
|
|
}
|
|
for (const {name, version, licenseText} of deps) {
|
|
combined[`${name}@${version}`] = wrap(licenseText, 80).trim();
|
|
}
|
|
const content = Object.entries(combined)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([title, body]) => `${line}\n${title}\n${line}\n${body}`).join('\n');
|
|
context.emitFile({type: 'asset', fileName: 'licenses.txt', source: content});
|
|
},
|
|
match: /^((UN)?LICEN(S|C)E|COPYING).*$/i, // also defined in build/generate-go-licenses.go
|
|
allow(dep) {
|
|
if (dep.name === 'khroma') return true; // MIT: https://github.com/fabiospampinato/khroma/pull/33
|
|
return /(Apache-2\.0|0BSD|BSD-2-Clause|BSD-3-Clause|MIT|ISC|CPAL-1\.0|Unlicense|EPL-1\.0|EPL-2\.0)/.test(dep.license);
|
|
},
|
|
}) : {
|
|
name: 'dev-licenses-stub',
|
|
configureServer() {
|
|
writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development');
|
|
},
|
|
},
|
|
],
|
|
}));
|