import { readFileSync, readdirSync, writeFileSync } from 'node:fs' import { resolve } from 'node:path' import type { Plugin } from 'vite' import { configDefaults, defineConfig } from 'vitest/config' // @ts-expect-error plain-node script, no type declarations import { applyDevChannel } from './scripts/dev-channel.mjs' // SUBSTANDARD_CHANNEL=dev builds the dev channel: inverted icons, a suffixed // name, and the commit stamped into version_name, so an unpacked install is // distinguishable from the store one in the same browser and says which build // it is. See scripts/dev-channel.mjs. function devChannel(): Plugin | undefined { if (process.env.SUBSTANDARD_CHANNEL !== 'dev') return undefined return { name: 'substandard:dev-channel', // Only the client environment copies public/ into dist/, and that copy // happens before the bundle is written — so patching here lands on top of // it, on the first build and on every watch rebuild. applyToEnvironment: (environment) => environment.name === 'client', writeBundle(options) { const dist = options.dir ?? resolve(__dirname, 'dist') const { name, version_name } = applyDevChannel(__dirname, dist) const build = version_name ? `, build ${version_name}` : '' console.log(`[dev-channel] ${dist}: icons inverted, name "${name}"${build}`) }, } } // Ship sourcemaps carrying the source of the code we wrote, and not of the // code we merely depend on. // // The store reviews minified code more slowly than readable code — Google's // guidance is to submit code as authored where feasible — and this extension // asks for all-hosts access, which already earns an in-depth review anyway. // Maps let a reviewer read the TypeScript instead of the minified output. // // The catch is `sourcesContent`: it inlines the full text of every source in // the chunk, and the OAuth chunk is mostly @atproto, so its map alone is // 3.7 MB — tripling the upload and burying our ~2 kLOC in several megabytes // of third-party source. That is more for a reviewer to wade through, not // less, and those sources are public and versioned on npm regardless. // // So the mappings stay complete for every file (stack traces still resolve), // and only the inlined *text* of node_modules sources is dropped. // // Done on disk in writeBundle, not by mutating `chunk.map` in generateBundle: // changes there are not carried through to the file rollup writes, which // shows up as a build log reporting the trimmed size while the artifact keeps // the full one. The dev channel above patches build output the same way. function ownCodeSourcemapsOnly(): Plugin { return { name: 'substandard:own-sourcemaps', writeBundle(options) { const dist = options.dir ?? resolve(__dirname, 'dist') for (const name of readdirSync(dist, { recursive: true })) { if (!String(name).endsWith('.js.map')) continue const file = resolve(dist, String(name)) const map = JSON.parse(readFileSync(file, 'utf8')) as { sources: string[] sourcesContent?: (string | null)[] } if (!map.sourcesContent) continue map.sourcesContent = map.sourcesContent.map((content, i) => map.sources[i]?.includes('node_modules') ? null : content, ) writeFileSync(file, JSON.stringify(map)) } }, } } // One build, two environments (Vite 6 builder API — `vite build` builds both // in order): // // - `client`: the popup/offscreen/welcome pages and the background service // worker, as ES modules. Empties dist/ and copies public/ (manifest, icons). // - `content`: the content script as a single IIFE file, because MV3 content // scripts cannot be ES modules. Builds second, into the same dist/. // // scripts/dev.mjs runs the same two environments in watch mode. export default defineConfig({ test: { // web/ runs its own node:test suite (`npm --prefix web test`); vitest's // default glob would sweep those files up and fail on them. .claude/ // holds agent worktrees — full repo copies whose tests are not ours to run. exclude: [...configDefaults.exclude, 'web/**', '.claude/**'], }, plugins: [devChannel(), ownCodeSourcemapsOnly()], builder: {}, environments: { client: { build: { modulePreload: false, sourcemap: true, rollupOptions: { input: { popup: resolve(__dirname, 'popup.html'), offscreen: resolve(__dirname, 'offscreen.html'), welcome: resolve(__dirname, 'welcome.html'), background: resolve(__dirname, 'src/background.ts'), }, output: { entryFileNames: '[name].js', chunkFileNames: 'chunks/[name]-[hash].js', assetFileNames: 'assets/[name][extname]', }, }, }, }, content: { consumer: 'client', build: { sourcemap: true, emptyOutDir: false, // shares dist/ with the client environment copyPublicDir: false, lib: { entry: resolve(__dirname, 'src/content.ts'), name: 'substandardContent', formats: ['iife'], fileName: () => 'content.js', }, }, }, }, })