You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/vite.config.ts

225 lines
9.8 KiB

// Vite configuration of the SvelteKit site. The library tests run with
// vitest.config.ts, which vitest prefers when both files exist.
import { sveltekit } from '@sveltejs/kit/vite';
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { defineConfig, type Plugin } from 'vite';
// Records what the client bundle is made of, for scripts/check-build.mjs:
// each chunk with the chunks it imports and the modules it contains, with
// their rendered sizes. The record goes to .svelte-kit/output, outside the
// site.
function clientModules(): Plugin {
let root = process.cwd();
return {
name: 'datekeys:client-modules',
apply: 'build',
configResolved(config) {
root = config.root;
},
generateBundle(_, bundle) {
if (this.environment.name !== 'client') return;
const chunks: Record<string, { imports: string[]; dynamicImports: string[]; modules: Record<string, number> }> = {};
for (const out of Object.values(bundle)) {
if (out.type !== 'chunk') continue;
chunks[out.fileName] = {
imports: out.imports,
dynamicImports: out.dynamicImports,
modules: Object.fromEntries(Object.entries(out.modules).map(([id, m]) => [id.replace(/\\/g, '/'), m.renderedLength])),
};
}
const dir = join(root, '.svelte-kit', 'output');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'client-modules.json'), JSON.stringify({ chunks }, null, 1));
},
};
}
// The package directory of a module id under node_modules, or undefined.
function packageDir(id: string): string | undefined {
const i = id.lastIndexOf('/node_modules/');
if (i < 0) return undefined;
const parts = id.slice(i + '/node_modules/'.length).split('/');
const name = parts[0]!.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0]!;
return `${id.slice(0, i)}/node_modules/${name}`;
}
// The package of a virtual module of the bundler, such as \0vite/preload-helper.js
// or \0rolldown/runtime.js, or undefined for any other.
function virtualPackageDir(id: string, root: string): string | undefined {
const name = /^\0(vite|rolldown)\//.exec(id)?.[1];
return name === undefined ? undefined : `${root}/node_modules/${name}`;
}
/**
* The license notice kept in the leading comment of a module derived from
* another project, such as src/lib/dkc/ibe.ts: the block of lines indented
* three spaces that holds a copyright line, with the paragraph that
* introduces it. Undefined when the module has none.
*/
function derivedNotice(source: string): string | undefined {
const header: string[] = [];
for (const line of source.split(/\r?\n/)) {
if (!line.startsWith('//')) break;
header.push(line);
}
// The block runs from its first indented line to its last, over blank
// comment lines; list items of the comment are indented too, so it is
// found by its copyright line.
const copyright = header.findIndex((l) => /^\/\/ {3}.*copyright/i.test(l));
if (copyright < 0) return undefined;
let start = copyright;
while (start > 0 && header[start - 1]!.startsWith('// ')) start--;
let end = copyright;
while (end < header.length && (header[end]!.startsWith('// ') || header[end] === '//')) end++;
while (header[end - 1] === '//') end--;
const notice = header.slice(start, end).map((l) => (l === '//' ? '' : l.slice(5)));
// The paragraph before it, which names the project.
let to = start;
while (to > 0 && header[to - 1] === '//') to--;
let from = to;
while (from > 0 && header[from - 1] !== '//') from--;
const intro = header.slice(from, to).map((l) => l.replace(/^\/\/ ?/, ''));
return [...intro, '', ...notice].join('\n');
}
const RULE = '='.repeat(72);
const LINE = '-'.repeat(72);
// Writes licenses.txt at the root of the site: the notices of the third-party
// code in the client bundle, whose minified JavaScript keeps no comments. It
// holds the notice of every module of src/ derived from another project
// (derivedNotice), the license file of every npm package with a module in
// the bundle, and the license of the site itself. scripts/check-build.mjs
// checks that nothing is missing.
function thirdPartyNotices(): Plugin {
let root = process.cwd();
return {
name: 'datekeys:third-party-notices',
apply: 'build',
configResolved(config) {
root = config.root.replace(/\\/g, '/');
},
generateBundle(_, bundle) {
if (this.environment.name !== 'client') return;
const ids = new Set<string>();
const dirs = new Set<string>();
for (const out of Object.values(bundle)) {
if (out.type !== 'chunk') continue;
for (const [raw, m] of Object.entries(out.modules)) {
const id = raw.replace(/\\/g, '/');
ids.add(id);
// The bundler's own virtual modules (\0…) are code of its package.
const dir = id.startsWith('\0') ? virtualPackageDir(id, root) : packageDir(id);
if (dir !== undefined) dirs.add(dir);
else if (id.startsWith('\0') && m.renderedLength > 0) this.error(`no license known for the virtual module ${JSON.stringify(id)}`);
}
}
const derived: string[] = [];
for (const id of [...ids].sort()) {
if (!id.startsWith(`${root}/src/`) || !/\.(ts|js|svelte)$/.test(id)) continue;
const notice = derivedNotice(readFileSync(id, 'utf8'));
if (notice !== undefined) derived.push(`${id.slice(root.length + 1)}\n\n${notice}`);
}
const packages: string[] = [];
for (const dir of [...dirs].sort()) {
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { name: string; version: string; license?: string };
const file = readdirSync(dir).find((f) => /^(licen[cs]e|copying)(\.md|\.txt)?$/i.test(f));
if (file === undefined) this.error(`${pkg.name} ${pkg.version} has no license file`);
const nested = dir.slice(0, dir.lastIndexOf('/node_modules/')).includes('/node_modules/');
const where = nested ? `, anidada bajo ${dir.slice(dir.indexOf('/node_modules/') + '/node_modules/'.length, dir.lastIndexOf('/node_modules/'))}` : '';
let license = readFileSync(join(dir, file), 'utf8').trim();
// Vite's file also carries the licenses of its Node-side dependencies,
// none of which reaches the client: only its own part applies.
const bundled = license.indexOf('\n# Licenses of bundled dependencies');
if (pkg.name === 'vite' && bundled > 0) license = license.slice(0, bundled).trim();
// The code rolldown writes into the bundle derives from Rollup and
// esbuild, whose notices it keeps apart.
const third = join(dir, 'THIRD-PARTY-LICENSE');
if (pkg.name === 'rolldown' && existsSync(third)) license += `\n\n${readFileSync(third, 'utf8').trim()}`;
packages.push(`${LINE}\n${pkg.name} ${pkg.version} (${pkg.license ?? 'sin campo license'}${where})\n${LINE}\n\n${license}`);
}
const text = [
'Avisos de licencia de este sitio',
'',
'Este sitio es datekeys-ts, con licencia Apache-2.0 (al final de este fichero). Su',
'JavaScript lleva, minimizado, código de terceros: módulos derivados de otros',
'proyectos y paquetes npm, cada uno con su aviso de copyright y su licencia.',
'',
RULE,
'Módulos de datekeys-ts derivados de otros proyectos',
RULE,
'',
derived.join(`\n\n${LINE}\n\n`),
'',
RULE,
'Paquetes npm',
RULE,
'',
packages.join('\n\n'),
'',
RULE,
'datekeys-ts (Apache-2.0)',
RULE,
'',
readFileSync(join(root, 'LICENSE'), 'utf8').trim(),
'',
].join('\n');
this.emitFile({ type: 'asset', fileName: 'licenses.txt', source: text });
},
};
}
// `vite preview` sends the pages without Cache-Control, so a browser may show
// an old page after a new build, whose chunks are gone from build/: the local
// preview has the pages revalidated on every load. The hashed files of
// _app/immutable keep the year-long cache that SvelteKit gives them. It goes
// before the SvelteKit plugin, which serves the prerendered pages itself and
// would answer first. A real host should serve the pages the same way.
function revalidatePages(): Plugin {
return {
name: 'datekeys:revalidate-pages',
configurePreviewServer(server) {
server.middlewares.use((req, res, next) => {
if (!req.url?.startsWith('/_app/immutable/')) res.setHeader('Cache-Control', 'no-cache');
next();
});
},
};
}
export default defineConfig({
plugins: [revalidatePages(), sveltekit(), clientModules(), thirdPartyNotices()],
build: {
// Never inline an asset as a data: URL. The CSP allows only the page's
// own origin, so the official fixtures (src/lib/inspector/fixtures.ts)
// must be separate same-origin files, whatever their size.
assetsInlineLimit: 0,
},
optimizeDeps: {
// Every dependency the pages load on demand, pre-bundled when the dev
// server starts. Found only on the first import, as the opening of a
// capsule does, the dev server bundles it then and reloads the page, and
// the import in flight fails: "Failed to fetch dynamically imported
// module". src/lib/dependencies.test.ts checks that the list is complete.
include: [
'@noble/ciphers/chacha.js',
'@noble/curves/bls12-381.js',
'@noble/curves/ed25519.js',
'@noble/curves/nist.js',
'@noble/curves/utils.js',
'@noble/hashes/hkdf.js',
'@noble/hashes/legacy.js',
'@noble/hashes/sha2.js',
'age-encryption',
],
},
server: {
fs: {
// The dev server serves the official fixtures straight from testdata/,
// the single source of truth; the build copies them as hashed assets.
allow: ['testdata/fixtures'],
},
},
});

Powered by TurnKey Linux.