Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion lib/cmd/vite/generate-kiln-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,36 @@ async function generateViteKilnEditEntry() {
// If neither is present, fall back to the namespace itself.
lines.push('');
lines.push('function _resolveDefault(ns) { return (ns && ns.default !== undefined) ? ns.default : ns; }');
lines.push('function _serializeKilnInitError(error) {');
lines.push(' if (error == null) return { message: "Unknown error" };');
lines.push(' if (typeof error === "string") return { message: error };');
lines.push(' if (typeof error !== "object") return { message: String(error) };');
lines.push('');
lines.push(' var payload = {');
lines.push(' name: error.name || null,');
lines.push(' message: error.message || String(error),');
lines.push(' stack: error.stack || null,');
lines.push(' };');
lines.push('');
lines.push(' if (error.code != null) payload.code = error.code;');
lines.push(' if (error.cause != null) payload.cause = _serializeKilnInitError(error.cause);');
lines.push('');
lines.push(' return payload;');
lines.push('}');
lines.push('');
lines.push('function _reportKilnInitError(error) {');
lines.push(' var payload = _serializeKilnInitError(error);');
lines.push(' var message = payload.message || "Unknown error";');
lines.push(' var header = "[clay vite] kiln plugin init error: " + message;');
lines.push('');
lines.push(' if (console.groupCollapsed) {');
lines.push(' console.groupCollapsed(header);');
lines.push(' console.error(payload);');
lines.push(' console.groupEnd();');
lines.push(' } else {');
lines.push(' console.error(header, payload);');
lines.push(' }');
lines.push('}');

lines.push('');
lines.push('window.kiln = window.kiln || {};');
Expand All @@ -94,7 +124,16 @@ async function generateViteKilnEditEntry() {
// Run site kiln plugin initializer
if (hasKilnPlugin) {
lines.push('var _initKilnPlugins = _resolveDefault(_kilnPluginNs);');
lines.push('if (typeof _initKilnPlugins === "function") _initKilnPlugins();');
lines.push('if (typeof _initKilnPlugins === "function") {');
lines.push(' try {');
lines.push(' var _kilnInitResult = _initKilnPlugins();');
lines.push(' if (_kilnInitResult && typeof _kilnInitResult.then === "function") {');
lines.push(' _kilnInitResult.catch(_reportKilnInitError);');
lines.push(' }');
lines.push(' } catch (error) {');
lines.push(' _reportKilnInitError(error);');
lines.push(' }');
lines.push('}');
}

await fs.ensureDir(CLAY_DIR);
Expand Down
4 changes: 3 additions & 1 deletion lib/cmd/vite/generators.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ describe('generate-vite env/bootstrap/kiln generators', () => {
expect(content).toContain('import * as _kilnPluginNs from "../services/kiln/index.js";');
expect(content).toContain('window.kiln.componentModels["article"] = _resolveDefault(_m0);');
expect(content).toContain('window.kiln.componentKilnjs["article"] = _resolveDefault(_k0);');
expect(content).toContain('if (typeof _initKilnPlugins === "function") _initKilnPlugins();');
expect(content).toContain('function _reportKilnInitError(error) {');
expect(content).toContain('var _kilnInitResult = _initKilnPlugins();');
expect(content).toContain('_kilnInitResult.catch(_reportKilnInitError);');
});

it('does not import kiln plugin when services/kiln/index.js is missing', async () => {
Expand Down
6 changes: 4 additions & 2 deletions lib/cmd/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function getEditScripts(assetPath) {
* Populate media.moduleScripts and media.modulePreloads for amphora-html.
*
* In view mode: one bootstrap URL.
* In edit mode: kiln edit bundle + bootstrap.
* In edit mode: bootstrap + kiln edit bundle.
*
* @param {object} media
* @param {string} assetPath
Expand All @@ -113,7 +113,9 @@ function resolveModuleScripts(media, assetPath, options) {
if (edit) {
const editScripts = getEditScripts(assetPath);

media.moduleScripts = [...editScripts, ...viewScripts];
// Keep bootstrap first so shared env/bootstrap globals are available
// before the kiln edit entry executes.
media.moduleScripts = [...viewScripts, ...editScripts];
media.modulePreloads = preloadEditBundle
? [...preloads, ...editScripts]
: preloads;
Expand Down
6 changes: 4 additions & 2 deletions lib/cmd/vite/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('index — view mode scripts', () => {
// ── edit mode scripts ─────────────────────────────────────────────────────────

describe('index — edit mode scripts', () => {
it('includes both kiln edit bundle and bootstrap in edit mode', () => {
it('includes bootstrap first, then kiln edit bundle in edit mode', () => {
const { mod, cleanup } = withManifest({
[BOOTSTRAP_KEY]: { file: '/js/bootstrap-abc.js', imports: [] },
[KILN_EDIT_KEY]: { file: '/js/kiln-edit-xyz.js', imports: ['/js/vue-chunk.js'] },
Expand All @@ -174,8 +174,10 @@ describe('index — edit mode scripts', () => {
mod.resolveModuleScripts(media, '', { edit: true });
cleanup();

expect(media.moduleScripts).toContain('/js/kiln-edit-xyz.js');
expect(media.moduleScripts).toContain('/js/bootstrap-abc.js');
expect(media.moduleScripts).toContain('/js/kiln-edit-xyz.js');
expect(media.moduleScripts.indexOf('/js/bootstrap-abc.js'))
.toBeLessThan(media.moduleScripts.indexOf('/js/kiln-edit-xyz.js'));
});

it('still serves view scripts when kiln entry is absent from manifest', () => {
Expand Down
57 changes: 57 additions & 0 deletions lib/cmd/vite/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,54 @@ function getViteConfig(cliOptions = {}) {

exports.getViteConfig = getViteConfig;

/**
* Build the `process.env` define that redirects client env reads to the
* RUNTIME-hydrated `window.process.env`.
*
* This is the Vite equivalent of the legacy Browserify pipeline's transformEnv
* plugin (lib/cmd/compile/scripts.js), which rewrites every `process.env` in
* client code to `window.process.env` and inlines NO values ("reference window,
* so browserify doesn't bundle in `process`").
*
* ── Why a redirect, not an inline ───────────────────────────────────────────
*
* Vite does not inline arbitrary env, and @rollup/plugin-commonjs rewrites the
* bare `process.env` of a CJS module into a build-time-empty local stub
* (literally `var pVt = {}`). So `const HOST = process.env.PYXIS_HOST || '…'`
* compiles to `pVt.PYXIS_HOST || '…'` → always the fallback, and the read is dead
* at build time. That silently breaks every Kiln plugin / universal service that
* reads env (pyxis.js, glaze/agora helpers, …): they work in Browserify but not
* under Vite.
*
* Defining `process.env` → `window.process.env` makes `process.env.PYXIS_HOST`
* compile to `window.process.env.PYXIS_HOST` — a live read of the object
* .clay/_env-init.js hydrates from `window.kiln.preloadData._envVars` (amphora-html
* forwards the server's live env in edit mode, gated by client-env.json). It also
* turns master's `window.process.env.X = process.env.X` plugin line into a harmless
* self-assign instead of clobbering the hydrated value with "".
*
* We deliberately do NOT inline values from the build env. The Docker image build
* is bare (only CLAYCLI_* args), so there is nothing to inline in prod anyway; and
* inlining from the full build `process.env` would bake any referenced var —
* including secrets that are NOT on the client-env.json allowlist (e.g.
* CORAL_GRAPHQL_TOKEN) — into the public bundle on any env-present build (local
* `make assets`, future build-args, …). The runtime object is allowlist-gated, so
* a pure redirect keeps Vite's client-env exposure identical to Browserify's.
*
* `process.env.NODE_ENV` is kept as a build-time literal by buildDefines (a
* more-specific key, so it wins over this catch-all) → `process.env.NODE_ENV ===
* 'production'` still tree-shakes.
*
* @returns {object} `{ 'process.env': 'window.process.env' }`
*/
function buildClientEnvDefines() {
// A member-expression value (not a JSON literal) tells esbuild/Vite to REWRITE
// the `process.env` token to `window.process.env` rather than substitute a
// constant. `window.process.env` on the left-hand side of an assignment is left
// alone (its `process` is a member of `window`, not the free `process` token).
return { 'process.env': 'window.process.env' };
}

/**
* Build the define map injected into every module at compile time.
*
Expand Down Expand Up @@ -222,10 +270,19 @@ function buildDefines(userDefines = {}) {
// of window. globalThis is universally available in ES2017+ environments.
global: 'globalThis',
},
// Client env (see buildClientEnvDefines): redirect `process.env` →
// `window.process.env` so reads resolve to the runtime-hydrated object
// (.clay/_env-init.js), matching the legacy Browserify transformEnv plugin —
// no build-time values are inlined. `process.env.NODE_ENV` above is more
// specific and still wins as a literal. Comes before userDefines so a site
// can still override a specific var via bundlerConfig().define if needed.
buildClientEnvDefines(),
userDefines
);
}

exports.buildDefines = buildDefines;

/**
* Assemble the Vite plugin array for a build pass.
*
Expand Down
46 changes: 46 additions & 0 deletions lib/cmd/vite/scripts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,52 @@ describe('vite scripts', () => {
expect(cfg.sourcemap).toBe(true);
});

it('buildDefines redirects process.env to window.process.env and never inlines build env (Browserify parity)', async () => {
await setupTmp('claycli-vite-scripts-clientenv-');

const prev = process.env.PYXIS_HOST;

// Even when the build env holds a value, it must NOT be baked in: the Docker
// image build is bare, and inlining from the full build env would leak vars
// outside the client-env.json allowlist into the public bundle. Reads resolve
// at runtime via window.process.env (hydrated by .clay/_env-init.js), exactly
// like the legacy Browserify transformEnv plugin. This also turns master's
// `window.process.env.X = process.env.X` into a harmless self-assign.
process.env.PYXIS_HOST = 'https://build-env-should-not-be-inlined.test';

try {
const { buildDefines } = require('./scripts');
const define = buildDefines();

expect(define['process.env']).toBe('window.process.env');
expect(define['process.env.PYXIS_HOST']).toBeUndefined();
} finally {
if (prev === undefined) delete process.env.PYXIS_HOST;
else process.env.PYXIS_HOST = prev;
}
});

it('buildDefines keeps NODE_ENV a build-time literal so guards still tree-shake', async () => {
await setupTmp('claycli-vite-scripts-nodeenv-');

const prev = process.env.NODE_ENV;

process.env.NODE_ENV = 'production';

try {
const { buildDefines } = require('./scripts');
const define = buildDefines();

// The more-specific `process.env.NODE_ENV` key wins over `process.env` in
// esbuild, so `process.env.NODE_ENV === 'production'` folds to a literal.
expect(define['process.env.NODE_ENV']).toBe(JSON.stringify('production'));
expect(define['process.env']).toBe('window.process.env');
} finally {
if (prev === undefined) delete process.env.NODE_ENV;
else process.env.NODE_ENV = prev;
}
});

it('buildJS runs split builds, writes manifest and client-env', async () => {
await setupTmp('claycli-vite-scripts-build-');

Expand Down
Loading