|
20 | 20 | 8. [Code References](#8-code-references) |
21 | 21 | - [Why `_globals-init.js` exists as a separate file](#why-_globals-initjs-exists-as-a-separate-file) |
22 | 22 | - [Why the build runs in two passes](#why-the-build-runs-in-two-passes) |
| 23 | + - [How client-env.json is generated](#how-client-envjson-is-generated) |
23 | 24 | 9. [Performance](#9-performance) |
24 | 25 | 10. [Learning Curve](#10-learning-curve) |
25 | 26 | 11. [For Product Managers](#11-for-product-managers) |
@@ -168,8 +169,8 @@ clay vite |
168 | 169 | ├── media.js ← fs-extra copy |
169 | 170 | │ └── components/**/media/* → public/media/ |
170 | 171 | │ |
171 | | -└── client-env.json ← generated by generateClientEnv() |
172 | | - └── scans source files for process.env.VAR references → client-env.json |
| 172 | +└── client-env.json ← generated by viteClientEnvPlugin (createClientEnvCollector) |
| 173 | + └── collected as a side-effect of the Rollup transform pass — no extra file I/O |
173 | 174 | (required by amphora-html's addEnvVars() at render time) |
174 | 175 | ``` |
175 | 176 |
|
@@ -621,7 +622,7 @@ RUN if [ "$CLAYCLI_VITE_ENABLED" = "true" ]; then \ |
621 | 622 | | `.clay/vite-bootstrap.js` | `generate-bootstrap.js` | Imports every `client.js`; mounts via dynamic `import()` on DOM presence | |
622 | 623 | | `.clay/_kiln-edit-init.js` | `generate-kiln-edit.js` | Imports every `model.js` + `kiln.js`; registers on `window.kiln.componentModels` | |
623 | 624 | | `.clay/_globals-init.js` | `generate-globals-init.js` | Imports all `global/js/*.js` into one non-splitting entry | |
624 | | -| `client-env.json` | `generateClientEnv()` | JSON array of `process.env.VAR_NAME` identifiers for `amphora-html` | |
| 625 | +| `client-env.json` | `createClientEnvCollector` (Rollup plugin) | JSON array of `process.env.VAR_NAME` identifiers for `amphora-html` | |
625 | 626 |
|
626 | 627 | > **Why `.clay/` exists — and why the old pipeline didn't need it** |
627 | 628 | > |
@@ -754,6 +755,68 @@ final state after full ESM migration. |
754 | 755 |
|
755 | 756 | --- |
756 | 757 |
|
| 758 | +### How client-env.json is generated |
| 759 | + |
| 760 | +`client-env.json` is a JSON array of `process.env.VAR_NAME` identifiers that `amphora-html` |
| 761 | +reads at server startup. For every name in the array, `amphora-html` injects |
| 762 | +`window.process.env.VAR = process.env.VAR` into the rendered page so that browser code |
| 763 | +can read it at runtime. |
| 764 | + |
| 765 | +#### The old way — grep scan (~30 seconds) |
| 766 | + |
| 767 | +The previous implementation (`generateClientEnv`) ran a full file-system scan on every |
| 768 | +build: glob-expand every `.js` and `.vue` file under `components/`, `layouts/`, `services/`, |
| 769 | +`global/`, and `amphora/`, then `fs.readFile` each one sequentially, regex-match for |
| 770 | +`process\.env\.([A-Z_][A-Z0-9_]*)`, and write the result. |
| 771 | + |
| 772 | +Two problems: |
| 773 | +1. **Speed** — O(N files), sequential I/O, ~30s standalone step on top of an already |
| 774 | + 30-second JS build. |
| 775 | +2. **Scope** — it scanned *all* source files, including server-only utilities and |
| 776 | + `model.js` hooks that call external APIs. Server secrets like `STRIPE_API_SECRET` |
| 777 | + ended up in `client-env.json` even though no browser code ever reads them, creating |
| 778 | + unnecessary exposure in edit mode via `window.process.env`. |
| 779 | + |
| 780 | +#### The new way — Rollup transform hook (effectively free) |
| 781 | + |
| 782 | +`createClientEnvCollector` in `lib/cmd/vite/plugins/client-env.js` returns a pair: |
| 783 | + |
| 784 | +- **`plugin()`** — a Rollup plugin whose `transform` hook scans each module as Rollup |
| 785 | + already processes it. No extra file I/O — the source is already in memory. |
| 786 | +- **`write()`** — flushes the accumulated Set to `client-env.json` after all passes complete. |
| 787 | + |
| 788 | +Both the view pass and the kiln pass receive their own plugin instance from the same |
| 789 | +collector, so a single `write()` call covers `process.env` references from both |
| 790 | +`client.js` files and `model.js`/`kiln.js` files. |
| 791 | + |
| 792 | +The scope boundary is now the Rollup module graph: only files that are actually imported |
| 793 | +(directly or transitively) by `vite-bootstrap.js` or `_kiln-edit-init.js` contribute to |
| 794 | +`client-env.json`. Pure server-side utilities outside the graph — amphora route handlers, |
| 795 | +`services/server/*` — are never seen by Rollup and are therefore never included, which |
| 796 | +is exactly the right behavior. |
| 797 | + |
| 798 | +``` |
| 799 | +Browserify pipeline: Vite pipeline: |
| 800 | + grep all *.js + *.vue ───► 30s Rollup transform hook ──► ~0s |
| 801 | + (sequential, N files) (already traversing graph) |
| 802 | + includes server-only files only files in module graph |
| 803 | + may expose server secrets scope-bounded to browser surface |
| 804 | +``` |
| 805 | + |
| 806 | +#### Watch mode behavior |
| 807 | + |
| 808 | +In watch mode, the collector is created once for the entire session. After every |
| 809 | +incremental rebuild (`BUNDLE_END`), `write()` is called again with whatever is in the Set. |
| 810 | +Because Rollup re-transforms any changed module, a newly added `process.env.MY_VAR` |
| 811 | +reference is picked up automatically on the next rebuild. |
| 812 | + |
| 813 | +The Set is intentionally **append-only** within a session — removing a reference leaves a |
| 814 | +stale entry until the next full build. This is the safe default: a stale extra entry |
| 815 | +causes the server to inject `undefined` (harmless), while a missing entry silently breaks |
| 816 | +client-side code. |
| 817 | + |
| 818 | +--- |
| 819 | + |
757 | 820 | ### Sticky events and the `stickyEvents` config key |
758 | 821 |
|
759 | 822 | #### The problem — ESM dynamic-import race condition |
@@ -813,7 +876,9 @@ The watch implementation uses **Rollup watch mode** for JS incremental rebuilds |
813 | 876 | - **JS rebuild:** Rollup reprocesses only changed modules and their dependents |
814 | 877 | - **Bootstrap regeneration:** when a new `client.js` is added, the bootstrap is regenerated |
815 | 878 | automatically so the new component appears in the next rebuild |
816 | | -- **client-env.json regeneration:** any JS file change triggers a `generateClientEnv()` pass |
| 879 | +- **client-env.json regeneration:** the `clay-client-env` Rollup plugin automatically |
| 880 | + collects any new `process.env.VAR` reference as Rollup re-transforms the changed |
| 881 | + module — no separate file scan, no extra I/O step |
817 | 882 | to keep `process.env` variable references in sync |
818 | 883 | - **usePolling: true** — required for Docker + macOS volume mounts where inotify is unreliable |
819 | 884 |
|
|
0 commit comments