From 5326226a2c9d312995f320f0957767e7a14e7abd Mon Sep 17 00:00:00 2001 From: Jordan Paulino Date: Wed, 6 May 2026 22:54:49 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8D=95=20Add=20scoped=20Vite=20builds?= =?UTF-8?q?=20for=20CSS=20sites=20and=20selected=20asset=20steps.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This expands local Vite optimization controls by combining CLAYCLI_VITE_CSS_SITES with a new --only selector for targeted js/styles/fonts/templates/vendor/media rebuilds. Co-authored-by: Cursor --- cli/vite.js | 26 +++++++- lib/cmd/vite/scripts.js | 52 ++++++++++++--- lib/cmd/vite/scripts.test.js | 126 +++++++++++++++++++++++++++++++++++ lib/cmd/vite/styles.js | 51 +++++++++++++- lib/cmd/vite/styles.test.js | 39 ++++++++++- 5 files changed, 283 insertions(+), 11 deletions(-) diff --git a/cli/vite.js b/cli/vite.js index c25664c..f84bc1d 100644 --- a/cli/vite.js +++ b/cli/vite.js @@ -24,15 +24,39 @@ function builder(yargs) { description: 'Additional entry-point file paths (supplements the default component globs)', default: [], }) + .option('only', { + type: 'array', + description: 'Build only selected steps: js, styles, fonts, templates, vendor, media', + default: [], + coerce: values => { + const list = Array.isArray(values) ? values : [values]; + + return list + .flatMap(v => String(v).split(',')) + .map(v => v.trim()) + .filter(Boolean); + }, + }) .example('$0', 'Build all component scripts and assets with Vite') .example('$0 --watch', 'Rebuild on every file change') - .example('$0 --minify', 'Build and minify for production'); + .example('$0 --minify', 'Build and minify for production') + .example('$0 --only styles,templates', 'Build only selected asset pipelines'); } async function handler(argv) { + const validOnly = new Set(['all', 'js', 'styles', 'fonts', 'templates', 'vendor', 'media']); + const only = (argv.only || []).filter(Boolean); + const invalid = only.filter(item => !validOnly.has(item)); + + if (invalid.length) { + log('error', `Invalid --only value(s): ${invalid.join(', ')}`); + process.exit(1); + } + const options = { minify: argv.minify, extraEntries: argv.entry || [], + only, }; if (argv.watch) { diff --git a/lib/cmd/vite/scripts.js b/lib/cmd/vite/scripts.js index b230954..6b723db 100644 --- a/lib/cmd/vite/scripts.js +++ b/lib/cmd/vite/scripts.js @@ -30,10 +30,45 @@ const { copyMedia } = require('./media'); const CWD = process.cwd(); const DEST = path.join(CWD, 'public', 'js'); const CLAY_DIR = path.join(CWD, '.clay'); +const BUILD_STEP_NAMES = new Set(['js', 'styles', 'fonts', 'templates', 'vendor', 'media']); exports.VITE_BOOTSTRAP_KEY = VITE_BOOTSTRAP_KEY; exports.KILN_EDIT_ENTRY_KEY = KILN_EDIT_ENTRY_KEY; +function normalizeRequestedSteps(only) { + if (!only || Array.isArray(only) && only.length === 0) return null; + + const entries = (Array.isArray(only) ? only : [only]) + .flatMap(v => String(v).split(',')) + .map(v => v.trim()) + .filter(Boolean); + + if (entries.includes('all')) return null; + + const requested = entries.reduce((set, name) => { + if (BUILD_STEP_NAMES.has(name)) set.add(name); + return set; + }, new Set()); + + return requested.size > 0 ? requested : null; +} + +function buildSelectedParallelTasks(step, shouldRun, options) { + return [ + ['js', () => buildJS(options)], + ['styles', () => buildStyles(options)], + ['fonts', () => buildFonts()], + ['templates', () => buildTemplates(options)], + ['vendor', () => copyVendor()], + ] + .filter(([name]) => shouldRun(name)) + .map(([name, runner]) => step(name, runner)); +} + +function shouldRunMediaStep(shouldRun) { + return shouldRun('media') || shouldRun('templates'); +} + // ── Config helpers ────────────────────────────────────────────────────────── /** @@ -746,6 +781,9 @@ exports.buildJS = buildJS; * @returns {Promise} */ async function buildAll(options = {}) { + const requested = normalizeRequestedSteps(options.only); + const shouldRun = step => !requested || requested.has(step); + const runMedia = shouldRunMediaStep(shouldRun); const isTTY = process.stdout.isTTY; const SPINNER = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']; @@ -828,19 +866,17 @@ async function buildAll(options = {}) { // Media must complete before templates — the template step reads SVG files // from public/media/ via {{{ read 'public/media/…' }}} Handlebars helpers. - await step('media', () => copyMedia()); + if (runMedia) { + await step('media', () => copyMedia()); + } if (isTTY) { timer = setInterval(() => { spinFrame++; clearSummary(); writeSummary(); }, 80); } - await Promise.all([ - step('js', () => buildJS(options)), - step('styles', () => buildStyles(options)), - step('fonts', () => buildFonts()), - step('templates', () => buildTemplates(options)), - step('vendor', () => copyVendor()), - ]); + const tasks = buildSelectedParallelTasks(step, shouldRun, options); + + await Promise.all(tasks); if (timer) { clearInterval(timer); timer = null; } clearSummary(); diff --git a/lib/cmd/vite/scripts.test.js b/lib/cmd/vite/scripts.test.js index 21a1eec..7d4e07e 100644 --- a/lib/cmd/vite/scripts.test.js +++ b/lib/cmd/vite/scripts.test.js @@ -242,6 +242,7 @@ describe('vite scripts', () => { })); const restoreTTY = setStdoutTTY(false); + const { buildAll } = require('./scripts'); await buildAll(); @@ -446,12 +447,137 @@ describe('vite scripts', () => { })); const restoreTTY = setStdoutTTY(false); + const { buildAll } = require('./scripts'); await expect(buildAll()).rejects.toThrow(/Build failed: 1 step\(s\) failed — styles failed/); restoreTTY(); }); + it('buildAll honors --only styles by skipping other steps', async () => { + await setupTmp('claycli-vite-scripts-only-styles-'); + + const order = []; + + jest.doMock('./media', () => ({ copyMedia: jest.fn().mockImplementation(async () => order.push('media')) })); + jest.doMock('./styles', () => ({ + buildStyles: jest.fn().mockImplementation(async () => order.push('styles')), + SRC_GLOBS: [], + })); + jest.doMock('./fonts', () => ({ + buildFonts: jest.fn().mockImplementation(async () => order.push('fonts')), + FONTS_SRC_GLOB: '', + })); + jest.doMock('./templates', () => ({ + buildTemplates: jest.fn().mockImplementation(async () => order.push('templates')), + TEMPLATE_GLOB_PATTERN: '*.hbs', + })); + jest.doMock('./vendor', () => ({ copyVendor: jest.fn().mockImplementation(async () => order.push('vendor')) })); + jest.doMock('vite', () => ({ + build: jest.fn().mockResolvedValue({ output: [] }), + })); + jest.doMock('./plugins/client-env', () => ({ + createClientEnvCollector: jest.fn().mockReturnValue({ + plugin: jest.fn().mockReturnValue({ name: 'env-collector' }), + write: jest.fn().mockResolvedValue(undefined), + }), + })); + jest.doMock('./generate-globals-init', () => ({ + generateViteGlobalsInit: jest.fn().mockResolvedValue(null), + })); + jest.doMock('./generate-kiln-edit', () => ({ + generateViteKilnEditEntry: jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js')), + KILN_EDIT_ENTRY_FILE: path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js'), + KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init', + })); + jest.doMock('./generate-bootstrap', () => ({ + generateViteBootstrap: jest.fn().mockImplementation(async () => { + const file = path.join(tmpDir, '.clay', 'vite-bootstrap.js'); + + await fs.ensureDir(path.dirname(file)); + await fs.writeFile(file, '// bootstrap'); + return file; + }), + VITE_BOOTSTRAP_FILE: path.join(tmpDir, '.clay', 'vite-bootstrap.js'), + VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap', + })); + jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' }))); + jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' }))); + jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' }))); + jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' }))); + jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks')); + jest.doMock('../../config-file-helpers', () => ({ + getConfigValue: jest.fn().mockReturnValue(undefined), + })); + + const restoreTTY = setStdoutTTY(false); + const { buildAll } = require('./scripts'); + + await buildAll({ only: ['styles'] }); + restoreTTY(); + + expect(order).toEqual(['styles']); + }); + + it('buildAll auto-runs media when --only templates is selected', async () => { + await setupTmp('claycli-vite-scripts-only-templates-'); + + const order = []; + + jest.doMock('./media', () => ({ copyMedia: jest.fn().mockImplementation(async () => order.push('media')) })); + jest.doMock('./styles', () => ({ buildStyles: jest.fn(), SRC_GLOBS: [] })); + jest.doMock('./fonts', () => ({ buildFonts: jest.fn(), FONTS_SRC_GLOB: '' })); + jest.doMock('./templates', () => ({ + buildTemplates: jest.fn().mockImplementation(async () => order.push('templates')), + TEMPLATE_GLOB_PATTERN: '*.hbs', + })); + jest.doMock('./vendor', () => ({ copyVendor: jest.fn() })); + jest.doMock('vite', () => ({ + build: jest.fn().mockResolvedValue({ output: [] }), + })); + jest.doMock('./plugins/client-env', () => ({ + createClientEnvCollector: jest.fn().mockReturnValue({ + plugin: jest.fn().mockReturnValue({ name: 'env-collector' }), + write: jest.fn().mockResolvedValue(undefined), + }), + })); + jest.doMock('./generate-globals-init', () => ({ + generateViteGlobalsInit: jest.fn().mockResolvedValue(null), + })); + jest.doMock('./generate-kiln-edit', () => ({ + generateViteKilnEditEntry: jest.fn().mockResolvedValue(path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js')), + KILN_EDIT_ENTRY_FILE: path.join(tmpDir, '.clay', 'vite-kiln-edit-init.js'), + KILN_EDIT_ENTRY_KEY: '.clay/vite-kiln-edit-init', + })); + jest.doMock('./generate-bootstrap', () => ({ + generateViteBootstrap: jest.fn().mockImplementation(async () => { + const file = path.join(tmpDir, '.clay', 'vite-bootstrap.js'); + + await fs.ensureDir(path.dirname(file)); + await fs.writeFile(file, '// bootstrap'); + return file; + }), + VITE_BOOTSTRAP_FILE: path.join(tmpDir, '.clay', 'vite-bootstrap.js'), + VITE_BOOTSTRAP_KEY: '.clay/vite-bootstrap', + })); + jest.doMock('./plugins/browser-compat', () => jest.fn(() => ({ name: 'browser-compat' }))); + jest.doMock('./plugins/service-rewrite', () => jest.fn(() => ({ name: 'service-rewrite' }))); + jest.doMock('./plugins/missing-module', () => jest.fn(() => ({ name: 'missing-module' }))); + jest.doMock('./plugins/vue2', () => jest.fn(() => ({ name: 'vue2' }))); + jest.doMock('./plugins/manual-chunks', () => jest.fn(() => 'manual-chunks')); + jest.doMock('../../config-file-helpers', () => ({ + getConfigValue: jest.fn().mockReturnValue(undefined), + })); + + const restoreTTY = setStdoutTTY(false); + const { buildAll } = require('./scripts'); + + await buildAll({ only: ['templates'] }); + restoreTTY(); + + expect(order).toEqual(['media', 'templates']); + }); + it('watch wires rollup/chokidar and dispose closes all watchers', async () => { await setupTmp('claycli-vite-scripts-watch-'); diff --git a/lib/cmd/vite/styles.js b/lib/cmd/vite/styles.js index bec6afd..daac21e 100644 --- a/lib/cmd/vite/styles.js +++ b/lib/cmd/vite/styles.js @@ -14,6 +14,7 @@ const SRC_GLOBS = [ ]; const DEST = path.join(CWD, 'public', 'css'); +const CSS_SITES_ENV = 'CLAYCLI_VITE_CSS_SITES'; const ASSET_HOST = process.env.CLAYCLI_COMPILE_ASSET_HOST ? process.env.CLAYCLI_COMPILE_ASSET_HOST.replace(/\/$/, '') @@ -137,11 +138,59 @@ function resolvePluginConfig(options) { return config; } +/** + * Returns the styleguide slug from a styleguide source file path. + * Example: /repo/styleguides/nymag/components/foo.css -> nymag + * + * @param {string} srcPath + * @returns {string|null} + */ +function getStyleguideFromPath(srcPath) { + const parts = srcPath.split(path.sep); + const styleguidesIdx = parts.lastIndexOf('styleguides'); + + if (styleguidesIdx === -1 || styleguidesIdx + 1 >= parts.length) return null; + + return parts[styleguidesIdx + 1] || null; +} + +/** + * Returns the set of explicitly-targeted styleguides for Vite CSS compilation. + * Unset/empty/"all" means compile all styleguides. + * + * @returns {Set|null} + */ +function getTargetCssSites() { + const raw = (process.env[CSS_SITES_ENV] || '').trim(); + + if (!raw || raw.toLowerCase() === 'all') return null; + + const sites = new Set(raw.split(',').map(s => s.trim()).filter(Boolean)); + + // Most styleguides import shared defaults, so always include _default. + sites.add('_default'); + return sites; +} + async function buildStyles(options = {}) { - const files = options.changedFiles + let files = options.changedFiles ? options.changedFiles : SRC_GLOBS.flatMap(g => globSync(g)); + // Scope full-build CSS compilation to selected styleguides when requested. + // changedFiles is watcher-driven and already explicit, so it bypasses this filter. + if (!options.changedFiles) { + const targetSites = getTargetCssSites(); + + if (targetSites) { + files = files.filter(f => { + const styleguide = getStyleguideFromPath(f); + + return styleguide && targetSites.has(styleguide); + }); + } + } + if (files.length === 0) return []; const { onProgress, onError } = options; diff --git a/lib/cmd/vite/styles.test.js b/lib/cmd/vite/styles.test.js index a4fc025..f68ce58 100644 --- a/lib/cmd/vite/styles.test.js +++ b/lib/cmd/vite/styles.test.js @@ -7,11 +7,12 @@ const os = require('os'); let tmpDir; -let originalCwd; +let originalCwd, originalCssSitesEnv; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claycli-styles-')); originalCwd = process.cwd; + originalCssSitesEnv = process.env.CLAYCLI_VITE_CSS_SITES; process.cwd = () => tmpDir; // Create a minimal CSS source tree with two styleguides @@ -29,6 +30,11 @@ beforeEach(async () => { afterEach(async () => { process.cwd = originalCwd; + if (originalCssSitesEnv === undefined) { + delete process.env.CLAYCLI_VITE_CSS_SITES; + } else { + process.env.CLAYCLI_VITE_CSS_SITES = originalCssSitesEnv; + } await fs.remove(tmpDir); jest.resetModules(); }); @@ -147,4 +153,35 @@ describe('buildStyles', () => { expect(fs.existsSync(path.join(tmpDir, 'public', 'css'))).toBe(true); }); + + it('limits full CSS compilation to selected sites plus _default', async () => { + process.env.CLAYCLI_VITE_CSS_SITES = 'mobile'; + const { buildStyles } = require('./styles'); + const results = await buildStyles(); + + expect(results).toHaveLength(3); + expect(fs.existsSync(path.join(tmpDir, 'public', 'css', 'article.mobile.css'))).toBe(true); + expect(fs.existsSync(path.join(tmpDir, 'public', 'css', 'article._default.css'))).toBe(true); + expect(fs.existsSync(path.join(tmpDir, 'public', 'css', 'article_amp._default.css'))).toBe(true); + }); + + it('treats "all" as compile-all behavior', async () => { + process.env.CLAYCLI_VITE_CSS_SITES = 'all'; + const { buildStyles } = require('./styles'); + const results = await buildStyles(); + + expect(results.length).toBeGreaterThanOrEqual(3); + expect(fs.existsSync(path.join(tmpDir, 'public', 'css', 'article.mobile.css'))).toBe(true); + expect(fs.existsSync(path.join(tmpDir, 'public', 'css', 'article._default.css'))).toBe(true); + }); + + it('does not filter watcher changedFiles by CLAYCLI_VITE_CSS_SITES', async () => { + process.env.CLAYCLI_VITE_CSS_SITES = 'mobile'; + const { buildStyles } = require('./styles'); + const defaultFile = path.join(tmpDir, 'styleguides', '_default', 'components', 'article.css'); + const results = await buildStyles({ changedFiles: [defaultFile] }); + + expect(results).toHaveLength(1); + expect(results[0]).toMatch(/article\._default\.css$/); + }); }); From 0ef4585c9b6b48d5568ccb98a0dc67d3a5d54a93 Mon Sep 17 00:00:00 2001 From: Jordan Paulino Date: Wed, 6 May 2026 23:14:16 -0400 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8D=95=20Update=20CLAY-VITE=20docs=20?= =?UTF-8?q?for=20scoped=20local=20rollout=20controls.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This documents CLAYCLI_VITE_CSS_SITES and clay vite --only usage, corrects stale kilnSplit guidance, and refreshes code references so rollout guidance stays accurate. Co-authored-by: Cursor --- CLAY-VITE.md | 106 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/CLAY-VITE.md b/CLAY-VITE.md index 614f5d7..03bda00 100644 --- a/CLAY-VITE.md +++ b/CLAY-VITE.md @@ -51,12 +51,11 @@ the 2014–2018 JavaScript ecosystem. Over time these became pain points: The new `clay vite` pipeline replaces Browserify/Gulp with **Vite 5 + PostCSS 8**: -- **Vite** uses Rollup 4 internally for production builds, adding `optimizeDeps` pre-bundling - (esbuild converts CJS `node_modules` before Rollup sees them) and a well-maintained plugin +- **Vite** uses Rollup 4 internally for production builds and a well-maintained plugin ecosystem. We use Vite exclusively for its **production build** — the Vite dev server and HMR are not used. Clay runs a full server-rendered architecture (Amphora) where the browser never speaks directly to a Vite dev server; watch mode uses Rollup's incremental rebuild - instead. + instead. (`optimizeDeps` is intentionally disabled in this flow.) - PostCSS 8's programmatic API replaces Gulp's stream-based CSS pipeline - All build steps (JS, CSS, fonts, templates, vendor, media) run **in parallel** - A human-readable `_manifest.json` replaces the numeric `_registry.json` / `_ids.json` pair @@ -97,6 +96,12 @@ clay vite --watch # Minified production build clay vite --minify + +# Build only selected steps (comma-separated) +clay vite --only styles,templates + +# Build only JS +clay vite --only js ``` Both commands read **`claycli.config.js`** in the root of your Clay instance, but they look at @@ -104,6 +109,17 @@ Both commands read **`claycli.config.js`** in the root of your Clay instance, bu The environment variable `CLAYCLI_VITE_ENABLED=true` enables the Vite pipeline in Dockerfiles and CI. +For faster local iteration during dual-pipeline rollout, you can also scope work: + +```bash +# Full build, but CSS only for selected styleguides (+ _default automatically) +CLAYCLI_VITE_CSS_SITES=nymag,strategist clay vite + +# Run only specific build steps +clay vite --only styles +clay vite --only js +``` + ## 3. Architecture: Old vs New ### Old: `clay compile` (Browserify + Gulp) @@ -550,6 +566,48 @@ Both commands are fully independent. You can run either one without affecting th When `CLAYCLI_VITE_ENABLED` is **unset** (or not `"true"`), the old `clay compile` pipeline runs everywhere with zero changes needed. +### Local iteration controls (new) + +These controls are intended for local/QA iteration while Browserify and Vite run in parallel. + +| Control | Scope | Behavior | +|---|---|---| +| `CLAYCLI_VITE_SITES=` | JS pipeline rollout (site integration) | Used by site-level wrappers (for example in `sites`) to enable Vite JS for selected sites during canary rollout | +| `CLAYCLI_VITE_CSS_SITES=` | CSS full builds | Compiles only selected styleguides, always includes `_default`; use `all` (or unset) for normal behavior | +| `clay vite --only ` | Build orchestration | Runs only selected steps: `js`, `styles`, `fonts`, `templates`, `vendor`, `media` | + +#### `CLAYCLI_VITE_CSS_SITES` + +```bash +# Compile CSS only for these sites (+ _default implicitly) +CLAYCLI_VITE_CSS_SITES=wwwthecut,curbed,intelligencer clay vite + +# Explicit "compile all styleguides" +CLAYCLI_VITE_CSS_SITES=all clay vite +``` + +Notes: + +- This filter applies to full CSS builds. +- Watch-mode incremental CSS rebuilds use `changedFiles` and are intentionally not filtered, so local edits still rebuild correctly. +- `_default` is always included when a site list is provided to preserve shared imports. + +#### `clay vite --only` + +```bash +# Fast template-only loop (media still runs first for template reads) +clay vite --only templates + +# Build multiple steps +clay vite --only js,styles +``` + +Notes: + +- `--only templates` still runs `media` first because templates may inline files from `public/media`. +- Invalid values are rejected by the CLI with a clear error. +- `--only all` is equivalent to a normal full build. + ### How to switch pipelines in the Dockerfile ```dockerfile @@ -586,34 +644,34 @@ RUN if [ "$CLAYCLI_VITE_ENABLED" = "true" ]; then \ | Command | File | |---|---| -| `clay vite` | [`cli/vite.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/cli/vite.js) | -| `clay compile` | [`cli/compile/`](https://github.com/clay/claycli/tree/jordan/yolo-update/cli/compile) | +| `clay vite` | [`cli/vite.js`](./cli/vite.js) | +| `clay compile` | [`cli/compile/`](./cli/compile) | ### Vite pipeline modules | Module | File | Purpose | |---|---|---| -| Orchestrator | [`lib/cmd/vite/scripts.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/scripts.js) | Main build + watch orchestration; `getViteConfig`, `baseViteConfig`, `buildAll`, `watch` | -| Bootstrap generator | [`lib/cmd/vite/generate-bootstrap.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/generate-bootstrap.js) | Generates `.clay/vite-bootstrap.js` with component mount runtime | -| Globals init generator | [`lib/cmd/vite/generate-globals-init.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/generate-globals-init.js) | Generates `.clay/_globals-init.js` | -| Kiln edit generator | [`lib/cmd/vite/generate-kiln-edit.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/generate-kiln-edit.js) | Generates `.clay/_kiln-edit-init.js` | -| CSS compilation | [`lib/cmd/vite/styles.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/styles.js) | PostCSS pipeline; `buildStyles`, `SRC_GLOBS` | -| Template compilation | [`lib/cmd/vite/templates.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/templates.js) | Handlebars precompile; `buildTemplates`, `TEMPLATE_GLOB_PATTERN` | -| Font processing | [`lib/cmd/vite/fonts.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/fonts.js) | Font copy + CSS generation; `buildFonts`, `FONTS_SRC_GLOB` | -| Media copy | [`lib/cmd/vite/media.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/media.js) | Copies media files to `public/media/`; `copyMedia` | -| Vendor copy | [`lib/cmd/vite/vendor.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/vendor.js) | Copies `clay-kiln` dist files to `public/js/`; `copyVendor` | -| Manifest writer | [`lib/cmd/vite/scripts.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/scripts.js) | `buildManifest`, `writeManifest` — writes `_manifest.json` | -| Script dependency resolver | [`lib/cmd/vite/index.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/index.js) | `resolveModuleScripts`, `hasManifest` — runtime helpers for `resolve-media.js` | +| Orchestrator | [`lib/cmd/vite/scripts.js`](./lib/cmd/vite/scripts.js) | Main build + watch orchestration; `getViteConfig`, `baseViteConfig`, `buildAll`, `watch` | +| Bootstrap generator | [`lib/cmd/vite/generate-bootstrap.js`](./lib/cmd/vite/generate-bootstrap.js) | Generates `.clay/vite-bootstrap.js` with component mount runtime | +| Globals init generator | [`lib/cmd/vite/generate-globals-init.js`](./lib/cmd/vite/generate-globals-init.js) | Generates `.clay/_globals-init.js` | +| Kiln edit generator | [`lib/cmd/vite/generate-kiln-edit.js`](./lib/cmd/vite/generate-kiln-edit.js) | Generates `.clay/_kiln-edit-init.js` | +| CSS compilation | [`lib/cmd/vite/styles.js`](./lib/cmd/vite/styles.js) | PostCSS pipeline; `buildStyles`, `SRC_GLOBS` | +| Template compilation | [`lib/cmd/vite/templates.js`](./lib/cmd/vite/templates.js) | Handlebars precompile; `buildTemplates`, `TEMPLATE_GLOB_PATTERN` | +| Font processing | [`lib/cmd/vite/fonts.js`](./lib/cmd/vite/fonts.js) | Font copy + CSS generation; `buildFonts`, `FONTS_SRC_GLOB` | +| Media copy | [`lib/cmd/vite/media.js`](./lib/cmd/vite/media.js) | Copies media files to `public/media/`; `copyMedia` | +| Vendor copy | [`lib/cmd/vite/vendor.js`](./lib/cmd/vite/vendor.js) | Copies `clay-kiln` dist files to `public/js/`; `copyVendor` | +| Manifest writer | [`lib/cmd/vite/scripts.js`](./lib/cmd/vite/scripts.js) | `buildManifest`, `writeManifest` — writes `_manifest.json` | +| Script dependency resolver | [`lib/cmd/vite/index.js`](./lib/cmd/vite/index.js) | `resolveModuleScripts`, `hasManifest` — runtime helpers for `resolve-media.js` | ### Vite plugins | Plugin | File | Purpose | |---|---|---| -| Vue 2 SFC | [`lib/cmd/vite/plugins/vue2.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/plugins/vue2.js) | Compiles `.vue` files; replaces `@nymag/vueify` Browserify transform | -| Browser compat | [`lib/cmd/vite/plugins/browser-compat.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/plugins/browser-compat.js) | Stubs server-only Node.js modules (`fs`, `http`, `clay-log`, etc.) | -| Service rewrite | [`lib/cmd/vite/plugins/service-rewrite.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/plugins/service-rewrite.js) | Rewrites `services/server/` imports to `services/client/` | -| Missing module | [`lib/cmd/vite/plugins/missing-module.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/plugins/missing-module.js) | Stubs unresolvable relative imports (legacy compatibility) | -| Manual chunks | [`lib/cmd/vite/plugins/manual-chunks.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/plugins/manual-chunks.js) | `viteManualChunksPlugin` — inlines small private deps into owner chunk | +| Vue 2 SFC | [`lib/cmd/vite/plugins/vue2.js`](./lib/cmd/vite/plugins/vue2.js) | Compiles `.vue` files; replaces `@nymag/vueify` Browserify transform | +| Browser compat | [`lib/cmd/vite/plugins/browser-compat.js`](./lib/cmd/vite/plugins/browser-compat.js) | Stubs server-only Node.js modules (`fs`, `http`, `clay-log`, etc.) | +| Service rewrite | [`lib/cmd/vite/plugins/service-rewrite.js`](./lib/cmd/vite/plugins/service-rewrite.js) | Rewrites `services/server/` imports to `services/client/` | +| Missing module | [`lib/cmd/vite/plugins/missing-module.js`](./lib/cmd/vite/plugins/missing-module.js) | Stubs unresolvable relative imports (legacy compatibility) | +| Manual chunks | [`lib/cmd/vite/plugins/manual-chunks.js`](./lib/cmd/vite/plugins/manual-chunks.js) | `viteManualChunksPlugin` — inlines small private deps into owner chunk | ### Generated files @@ -745,12 +803,12 @@ merge). Running them in `Promise.all()` is free — both passes compile independently and write to different output locations. The wall-clock cost is the longer of the two, not their sum. -#### `kilnSplit: false` — the ESM escape hatch +#### `kilnSplit: true` — the ESM escape hatch Once all `model.js` and `kiln.js` files are native ESM, the synchronous-initialization constraint disappears. ESM `import` statements at the top of a file are resolved statically by the module linker before any code runs, so there is no async gap for Kiln to initialize -into. Setting `kilnSplit: false` in `bundlerConfig` signals that the kiln pass can be +into. Setting `kilnSplit: true` in `bundlerConfig` signals that the kiln pass can be treated as a splitting pass, collapsing the two-pass build into one. This is the planned final state after full ESM migration. @@ -1242,7 +1300,7 @@ full dependency tree enters the browser bundle. This can add hundreds of KB of N transitive dependencies (Elasticsearch clients, `node-fetch`, `iconv-lite` encoding tables, etc.) that the browser never needs and can never actually use. -See [`lib/cmd/vite/plugins/service-rewrite.js`](https://github.com/clay/claycli/blob/jordan/yolo-update/lib/cmd/vite/plugins/service-rewrite.js) +See [`lib/cmd/vite/plugins/service-rewrite.js`](./lib/cmd/vite/plugins/service-rewrite.js) for the full implementation and bundle-size impact documentation. ### Known violations (already fixed) From 787ed1c6e6cf599a39c7d6a180db60830d92d7b1 Mon Sep 17 00:00:00 2001 From: Jordan Paulino Date: Wed, 6 May 2026 23:27:07 -0400 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8D=95=20Add=20intent=20comments=20fo?= =?UTF-8?q?r=20scoped=20Vite=20build=20controls.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This documents non-obvious --only normalization and media/template ordering rules so future changes preserve rollout-safe behavior. Co-authored-by: Cursor --- cli/vite.js | 4 ++++ lib/cmd/vite/scripts.js | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/cli/vite.js b/cli/vite.js index f84bc1d..15bfbe2 100644 --- a/cli/vite.js +++ b/cli/vite.js @@ -29,6 +29,8 @@ function builder(yargs) { description: 'Build only selected steps: js, styles, fonts, templates, vendor, media', default: [], coerce: values => { + // Normalize both "--only a,b" and repeated "--only a --only b" + // into the same array shape for the downstream build orchestrator. const list = Array.isArray(values) ? values : [values]; return list @@ -44,6 +46,8 @@ function builder(yargs) { } async function handler(argv) { + // Keep CLI validation close to parsing so bad values fail fast before we + // do any build preparation work. const validOnly = new Set(['all', 'js', 'styles', 'fonts', 'templates', 'vendor', 'media']); const only = (argv.only || []).filter(Boolean); const invalid = only.filter(item => !validOnly.has(item)); diff --git a/lib/cmd/vite/scripts.js b/lib/cmd/vite/scripts.js index 6b723db..223a56c 100644 --- a/lib/cmd/vite/scripts.js +++ b/lib/cmd/vite/scripts.js @@ -38,11 +38,14 @@ exports.KILN_EDIT_ENTRY_KEY = KILN_EDIT_ENTRY_KEY; function normalizeRequestedSteps(only) { if (!only || Array.isArray(only) && only.length === 0) return null; + // Accept either repeated flags (--only js --only styles) or CSV + // (--only js,styles), then normalize to one deduped Set. const entries = (Array.isArray(only) ? only : [only]) .flatMap(v => String(v).split(',')) .map(v => v.trim()) .filter(Boolean); + // "all" means "no filtering" so buildAll can reuse its normal path. if (entries.includes('all')) return null; const requested = entries.reduce((set, name) => { @@ -54,6 +57,8 @@ function normalizeRequestedSteps(only) { } function buildSelectedParallelTasks(step, shouldRun, options) { + // Keep the existing parallel scheduling model; this only filters which + // steps participate in the Promise.all fan-out. return [ ['js', () => buildJS(options)], ['styles', () => buildStyles(options)], @@ -66,6 +71,8 @@ function buildSelectedParallelTasks(step, shouldRun, options) { } function shouldRunMediaStep(shouldRun) { + // Templates can inline SVG/media files from public/media, so media remains + // a hard prerequisite even when the user requested templates only. return shouldRun('media') || shouldRun('templates'); }