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.
239 lines
10 KiB
239 lines
10 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}`);
|
|
}
|
|
// The word lists that /create fetches (src/lib/inspector/create-words.ts)
|
|
// are not code and have a license of their own, which the README of
|
|
// wordlists/, copied from datekeys-go, gives with their source.
|
|
const lists = readFileSync(join(root, 'wordlists', 'README.md'), 'utf8').trim();
|
|
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,
|
|
'Listas de palabras',
|
|
RULE,
|
|
'',
|
|
'Las palabras al azar de /create salen de las listas de wordlists/, copiadas de',
|
|
'datekeys-go. No son código de datekeys-ts ni tienen su licencia: su README, que',
|
|
'sigue, dice de dónde sale cada una, cómo se hizo y su licencia.',
|
|
'',
|
|
lists,
|
|
'',
|
|
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'],
|
|
},
|
|
},
|
|
});
|