veryfront 0.1.92 → 0.1.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/esm/cli/commands/styles/command-help.d.ts +3 -0
  2. package/esm/cli/commands/styles/command-help.d.ts.map +1 -0
  3. package/esm/cli/commands/styles/command-help.js +19 -0
  4. package/esm/cli/commands/styles/command.d.ts +3 -0
  5. package/esm/cli/commands/styles/command.d.ts.map +1 -0
  6. package/esm/cli/commands/styles/command.js +198 -0
  7. package/esm/cli/commands/styles/handler.d.ts +24 -0
  8. package/esm/cli/commands/styles/handler.d.ts.map +1 -0
  9. package/esm/cli/commands/styles/handler.js +17 -0
  10. package/esm/cli/help/command-definitions.d.ts.map +1 -1
  11. package/esm/cli/help/command-definitions.js +2 -0
  12. package/esm/cli/router.d.ts.map +1 -1
  13. package/esm/cli/router.js +2 -0
  14. package/esm/deno.d.ts +1 -0
  15. package/esm/deno.js +2 -1
  16. package/esm/src/html/styles-builder/css-pregeneration.d.ts +9 -0
  17. package/esm/src/html/styles-builder/css-pregeneration.d.ts.map +1 -1
  18. package/esm/src/html/styles-builder/css-pregeneration.js +26 -15
  19. package/esm/src/jobs/schemas.d.ts +40 -40
  20. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
  21. package/esm/src/platform/adapters/fs/veryfront/adapter.js +15 -3
  22. package/esm/src/platform/adapters/fs/veryfront/types.d.ts +2 -0
  23. package/esm/src/platform/adapters/fs/veryfront/types.d.ts.map +1 -1
  24. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +5 -1
  25. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  26. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +10 -2
  27. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts +2 -1
  28. package/esm/src/platform/adapters/veryfront-api-client/client.d.ts.map +1 -1
  29. package/esm/src/platform/adapters/veryfront-api-client/client.js +3 -0
  30. package/esm/src/platform/adapters/veryfront-api-client/index.d.ts +1 -1
  31. package/esm/src/platform/adapters/veryfront-api-client/index.d.ts.map +1 -1
  32. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts +11 -2
  33. package/esm/src/platform/adapters/veryfront-api-client/operations.d.ts.map +1 -1
  34. package/esm/src/platform/adapters/veryfront-api-client/operations.js +30 -0
  35. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts +14 -3
  36. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.d.ts.map +1 -1
  37. package/esm/src/platform/adapters/veryfront-api-client/schemas/api.schema.js +8 -1
  38. package/esm/src/rendering/styles.d.ts +9 -0
  39. package/esm/src/rendering/styles.d.ts.map +1 -0
  40. package/esm/src/rendering/styles.js +8 -0
  41. package/esm/src/sandbox/index.d.ts +1 -1
  42. package/esm/src/sandbox/index.d.ts.map +1 -1
  43. package/esm/src/sandbox/sandbox.d.ts +14 -3
  44. package/esm/src/sandbox/sandbox.d.ts.map +1 -1
  45. package/esm/src/sandbox/sandbox.js +20 -6
  46. package/esm/src/security/http/base-handler.d.ts.map +1 -1
  47. package/esm/src/security/http/base-handler.js +5 -2
  48. package/esm/src/server/context/request-context.d.ts.map +1 -1
  49. package/esm/src/server/context/request-context.js +4 -2
  50. package/esm/src/server/handlers/dev/scripts/hmr-scripts.d.ts.map +1 -1
  51. package/esm/src/server/handlers/dev/scripts/hmr-scripts.js +91 -4
  52. package/esm/src/server/handlers/dev/styles-css.handler.d.ts +2 -0
  53. package/esm/src/server/handlers/dev/styles-css.handler.d.ts.map +1 -1
  54. package/esm/src/server/handlers/dev/styles-css.handler.js +23 -0
  55. package/esm/src/server/handlers/preview/hmr-message-router.d.ts +2 -1
  56. package/esm/src/server/handlers/preview/hmr-message-router.d.ts.map +1 -1
  57. package/esm/src/server/handlers/preview/hmr-message-router.js +15 -5
  58. package/esm/src/server/handlers/preview/hmr.handler.js +1 -1
  59. package/esm/src/server/handlers/preview/markdown-preview.handler.d.ts.map +1 -1
  60. package/esm/src/server/handlers/preview/markdown-preview.handler.js +4 -2
  61. package/esm/src/server/handlers/request/css.handler.d.ts.map +1 -1
  62. package/esm/src/server/handlers/request/css.handler.js +4 -2
  63. package/esm/src/server/handlers/request/ssr/ssr.handler.d.ts.map +1 -1
  64. package/esm/src/server/handlers/request/ssr/ssr.handler.js +4 -2
  65. package/esm/src/server/reload-notifier.d.ts +3 -1
  66. package/esm/src/server/reload-notifier.d.ts.map +1 -1
  67. package/esm/src/utils/version.d.ts +1 -1
  68. package/esm/src/utils/version.js +1 -1
  69. package/package.json +1 -1
  70. package/src/cli/commands/styles/command-help.ts +21 -0
  71. package/src/cli/commands/styles/command.ts +296 -0
  72. package/src/cli/commands/styles/handler.ts +23 -0
  73. package/src/cli/help/command-definitions.ts +2 -0
  74. package/src/cli/router.ts +2 -0
  75. package/src/deno.js +2 -1
  76. package/src/src/html/styles-builder/css-pregeneration.ts +57 -29
  77. package/src/src/platform/adapters/fs/veryfront/adapter.ts +18 -4
  78. package/src/src/platform/adapters/fs/veryfront/types.ts +2 -0
  79. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +18 -3
  80. package/src/src/platform/adapters/veryfront-api-client/client.ts +8 -0
  81. package/src/src/platform/adapters/veryfront-api-client/index.ts +1 -0
  82. package/src/src/platform/adapters/veryfront-api-client/operations.ts +48 -2
  83. package/src/src/platform/adapters/veryfront-api-client/schemas/api.schema.ts +9 -1
  84. package/src/src/rendering/styles.ts +15 -0
  85. package/src/src/sandbox/index.ts +1 -0
  86. package/src/src/sandbox/sandbox.ts +33 -6
  87. package/src/src/security/http/base-handler.ts +5 -2
  88. package/src/src/server/context/request-context.ts +4 -2
  89. package/src/src/server/handlers/dev/scripts/hmr-scripts.ts +93 -4
  90. package/src/src/server/handlers/dev/styles-css.handler.ts +31 -0
  91. package/src/src/server/handlers/preview/hmr-message-router.ts +15 -5
  92. package/src/src/server/handlers/preview/hmr.handler.ts +1 -1
  93. package/src/src/server/handlers/preview/markdown-preview.handler.ts +4 -2
  94. package/src/src/server/handlers/request/css.handler.ts +4 -2
  95. package/src/src/server/handlers/request/ssr/ssr.handler.ts +4 -2
  96. package/src/src/server/reload-notifier.ts +3 -1
  97. package/src/src/utils/version.ts +1 -1
@@ -64,19 +64,28 @@ export interface ResolveStyleArtifactInput extends StyleArtifactSelector {
64
64
  styleProfileHash: string;
65
65
  }
66
66
 
67
+ export interface EnsureStyleArtifactBuildInput extends ResolveStyleArtifactInput {
68
+ force?: boolean;
69
+ }
70
+
67
71
  export interface UpsertStyleArtifactInput extends ResolveStyleArtifactInput {
68
- artifactHash: string;
72
+ status?: "building" | "ready" | "failed";
73
+ artifactHash?: string;
69
74
  assetPath?: string;
70
75
  contentType?: string;
71
76
  etag?: string;
77
+ buildJobId?: string;
78
+ failureReason?: string;
72
79
  }
73
80
 
74
81
  export interface ProjectStyleArtifactResolution {
75
- status: "ready" | "missing";
82
+ status: "ready" | "missing" | "building" | "failed";
76
83
  artifactHash?: string;
77
84
  assetPath?: string;
78
85
  etag?: string;
79
86
  contentType?: string;
87
+ buildJobId?: string;
88
+ failureReason?: string;
80
89
  updatedAt?: string;
81
90
  }
82
91
 
@@ -128,6 +137,8 @@ function mapStyleArtifactResolution(raw: unknown): ProjectStyleArtifactResolutio
128
137
  assetPath: response.asset_path,
129
138
  etag: response.etag,
130
139
  contentType: response.content_type,
140
+ buildJobId: response.build_job_id,
141
+ failureReason: response.failure_reason,
131
142
  updatedAt: response.updated_at,
132
143
  };
133
144
  }
@@ -491,6 +502,37 @@ export class VeryfrontAPIOperations {
491
502
  return mapStyleArtifactResolution(await this.request(url));
492
503
  }
493
504
 
505
+ async ensureStyleArtifactBuild(
506
+ projectRef: string,
507
+ input: EnsureStyleArtifactBuildInput,
508
+ ): Promise<ProjectStyleArtifactResolution> {
509
+ const url = `/projects/${encodeURIComponent(projectRef)}/style-artifacts/current/builds`;
510
+ logger.debug("ensureStyleArtifactBuild", {
511
+ projectRef,
512
+ branch: input.branch,
513
+ environmentName: input.environmentName,
514
+ releaseId: input.releaseId,
515
+ styleProfileHash: input.styleProfileHash,
516
+ force: input.force ?? false,
517
+ });
518
+
519
+ return mapStyleArtifactResolution(
520
+ await this.request(url, {
521
+ method: "POST",
522
+ headers: {
523
+ "Content-Type": "application/json",
524
+ },
525
+ body: JSON.stringify({
526
+ style_profile_hash: input.styleProfileHash,
527
+ branch: input.branch,
528
+ environment_name: input.environmentName,
529
+ release_id: input.releaseId,
530
+ force: input.force ?? false,
531
+ }),
532
+ }),
533
+ );
534
+ }
535
+
494
536
  async upsertStyleArtifact(
495
537
  projectRef: string,
496
538
  input: UpsertStyleArtifactInput,
@@ -502,6 +544,7 @@ export class VeryfrontAPIOperations {
502
544
  environmentName: input.environmentName,
503
545
  releaseId: input.releaseId,
504
546
  styleProfileHash: input.styleProfileHash,
547
+ status: input.status ?? "ready",
505
548
  artifactHash: input.artifactHash,
506
549
  });
507
550
 
@@ -516,10 +559,13 @@ export class VeryfrontAPIOperations {
516
559
  branch: input.branch,
517
560
  environment_name: input.environmentName,
518
561
  release_id: input.releaseId,
562
+ status: input.status ?? "ready",
519
563
  artifact_hash: input.artifactHash,
520
564
  asset_path: input.assetPath,
521
565
  content_type: input.contentType,
522
566
  etag: input.etag,
567
+ build_job_id: input.buildJobId,
568
+ failure_reason: input.failureReason,
523
569
  }),
524
570
  }),
525
571
  );
@@ -148,11 +148,13 @@ export const LookupDomainResponseSchema = z.object({
148
148
  });
149
149
 
150
150
  export const StyleArtifactResolveResponseSchema = z.object({
151
- status: z.enum(["ready", "missing"]),
151
+ status: z.enum(["ready", "missing", "building", "failed"]),
152
152
  artifact_hash: z.string().optional(),
153
153
  asset_path: z.string().optional(),
154
154
  etag: z.string().optional(),
155
155
  content_type: z.string().optional(),
156
+ build_job_id: z.string().uuid().optional(),
157
+ failure_reason: z.string().optional(),
156
158
  updated_at: z.string().optional(),
157
159
  });
158
160
 
@@ -228,4 +230,10 @@ export const API_ENDPOINTS = {
228
230
  description:
229
231
  "Resolve metadata for the latest ready style artifact for a branch, environment, or release selector",
230
232
  },
233
+ ensureStyleArtifactBuild: {
234
+ method: "POST" as const,
235
+ path: "/projects/{projectRef}/style-artifacts/current/builds",
236
+ description:
237
+ "Ensure a background style artifact build exists for a branch, environment, or release selector",
238
+ },
231
239
  } as const;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public style artifact helpers used by CLI and worker entrypoints.
3
+ *
4
+ * @module rendering/styles
5
+ */
6
+
7
+ export {
8
+ buildPreparedCSSArtifactFromFiles,
9
+ type PreparedCSSArtifactBuildResult,
10
+ } from "../html/styles-builder/css-pregeneration.js";
11
+ export {
12
+ createStyleScopeProfile,
13
+ type StyleScopeProfile,
14
+ } from "../html/styles-builder/style-scope-profile.js";
15
+ export { resolveStyleContentVersion } from "../html/styles-builder/content-version.js";
@@ -24,6 +24,7 @@ export {
24
24
  type CommandJobHeartbeatStatus,
25
25
  type CommandJobOutput,
26
26
  type CommandJobStatus,
27
+ type ExecOptions,
27
28
  type ExecResult,
28
29
  type ExecStreamEvent,
29
30
  Sandbox,
@@ -19,6 +19,16 @@ import {
19
19
  import { getVeryfrontCloudAuthToken } from "../platform/cloud/resolver.js";
20
20
  import { getHostEnv } from "../platform/compat/process.js";
21
21
 
22
+ /** Options for command execution: working directory, timeout, and environment variables. */
23
+ export interface ExecOptions {
24
+ /** Working directory for the command. */
25
+ cwd?: string;
26
+ /** Timeout in seconds for the command. */
27
+ timeout_seconds?: number;
28
+ /** Additional environment variables for the command. */
29
+ env?: Record<string, string>;
30
+ }
31
+
22
32
  /** Options for creating a sandbox session. */
23
33
  export interface SandboxOptions {
24
34
  /** Base URL of the Veryfront API. Defaults to VERYFRONT_API_URL env. */
@@ -242,12 +252,12 @@ export class Sandbox {
242
252
  }
243
253
 
244
254
  /** Execute a bash command in the sandbox and return buffered result. */
245
- async executeCommand(command: string): Promise<ExecResult> {
255
+ async executeCommand(command: string, options?: ExecOptions): Promise<ExecResult> {
246
256
  let stdout = "";
247
257
  let stderr = "";
248
258
  let exitCode = 1;
249
259
 
250
- for await (const event of this.executeStream(command)) {
260
+ for await (const event of this.executeStream(command, options)) {
251
261
  switch (event.type) {
252
262
  case "stdout":
253
263
  stdout += event.data ?? "";
@@ -265,14 +275,14 @@ export class Sandbox {
265
275
  }
266
276
 
267
277
  /** Execute a bash command with streaming output (NDJSON). */
268
- async *executeStream(command: string): AsyncGenerator<ExecStreamEvent> {
278
+ async *executeStream(command: string, options?: ExecOptions): AsyncGenerator<ExecStreamEvent> {
269
279
  const res = await dntShim.fetch(`${this.endpoint}/exec`, {
270
280
  method: "POST",
271
281
  headers: {
272
282
  Authorization: `Bearer ${this.authToken}`,
273
283
  "Content-Type": "application/json",
274
284
  },
275
- body: JSON.stringify({ command }),
285
+ body: JSON.stringify({ command, ...options }),
276
286
  });
277
287
 
278
288
  if (!res.ok) {
@@ -343,14 +353,14 @@ export class Sandbox {
343
353
  }
344
354
 
345
355
  /** Start an async command job in the sandbox. */
346
- async startCommandJob(command: string): Promise<CommandJob> {
356
+ async startCommandJob(command: string, options?: ExecOptions): Promise<CommandJob> {
347
357
  const res = await dntShim.fetch(`${this.endpoint}/exec/jobs`, {
348
358
  method: "POST",
349
359
  headers: {
350
360
  Authorization: `Bearer ${this.authToken}`,
351
361
  "Content-Type": "application/json",
352
362
  },
353
- body: JSON.stringify({ command }),
363
+ body: JSON.stringify({ command, ...options }),
354
364
  });
355
365
 
356
366
  if (!res.ok) {
@@ -399,6 +409,23 @@ export class Sandbox {
399
409
  };
400
410
  }
401
411
 
412
+ /** List all command jobs in the sandbox. */
413
+ async listCommandJobs(): Promise<CommandJob[]> {
414
+ const res = await dntShim.fetch(`${this.endpoint}/exec/jobs`, {
415
+ headers: { Authorization: `Bearer ${this.authToken}` },
416
+ });
417
+
418
+ if (!res.ok) {
419
+ throw REQUEST_ERROR.create({
420
+ detail: `List command jobs failed: ${res.status} ${await res.text()}`,
421
+ });
422
+ }
423
+
424
+ const json = await res.json();
425
+ const jobs = Array.isArray(json) ? json : (json.jobs ?? []);
426
+ return jobs.map((j: Record<string, unknown>) => Sandbox.mapCommandJob(j));
427
+ }
428
+
402
429
  /** Cancel an async command job. */
403
430
  async cancelCommandJob(jobId: string): Promise<CommandJob> {
404
431
  const res = await dntShim.fetch(`${this.endpoint}/exec/jobs/${jobId}/cancel`, {
@@ -7,6 +7,7 @@ import type {
7
7
  RoutePattern,
8
8
  } from "../../types/index.js";
9
9
  import { runWithCacheBatching } from "../../cache/request-cache-batcher.js";
10
+ import { getHostEnv } from "../../platform/compat/process.js";
10
11
  import { serverLogger } from "../../utils/index.js";
11
12
  import { ResponseBuilder } from "./response/index.js";
12
13
 
@@ -78,7 +79,7 @@ export abstract class BaseHandler implements Handler {
78
79
  }
79
80
 
80
81
  protected logDebug(message: string, extra?: Record<string, unknown>, ctx?: HandlerContext): void {
81
- if (!ctx?.debug && !ctx?.adapter.env.get("VERYFRONT_DEBUG")) return;
82
+ if (!ctx?.debug && !getHostEnv("VERYFRONT_DEBUG")) return;
82
83
  serverLogger.debug(`[${this.metadata.name}] ${message}`, extra ?? undefined);
83
84
  }
84
85
 
@@ -108,7 +109,9 @@ export abstract class BaseHandler implements Handler {
108
109
  fn: () => Promise<T>,
109
110
  options: { requireToken?: boolean } = {},
110
111
  ): Promise<T> {
111
- const effectiveToken = ctx.proxyToken || ctx.adapter.env.get("VERYFRONT_API_TOKEN") || "";
112
+ // Framework-owned token: bypass project env overlay so proxy mode works
113
+ // when a remote project overlay is active.
114
+ const effectiveToken = ctx.proxyToken || getHostEnv("VERYFRONT_API_TOKEN") || "";
112
115
  const fsWrapper = ctx.adapter.fs as {
113
116
  setRequestToken?: (t: string) => void;
114
117
  setRequestBranch?: (b: string | null) => void;
@@ -1,5 +1,5 @@
1
1
  import * as dntShim from "../../../_dnt.shims.js";
2
- import { getEnv } from "../../platform/compat/process.js";
2
+ import { getHostEnv } from "../../platform/compat/process.js";
3
3
  import { parseProjectDomain } from "../utils/domain-parser.js";
4
4
 
5
5
  export interface RequestContext {
@@ -28,7 +28,9 @@ export function createRequestContext(req: dntShim.Request): RequestContext {
28
28
  : "production";
29
29
 
30
30
  return {
31
- token: req.headers.get("x-token") ?? getEnv("VERYFRONT_API_TOKEN") ?? "",
31
+ // Framework-owned token: bypass project env overlay so proxy mode works
32
+ // when a remote project overlay is active.
33
+ token: req.headers.get("x-token") ?? getHostEnv("VERYFRONT_API_TOKEN") ?? "",
32
34
  slug: req.headers.get("x-project-slug") ?? parsed.slug ?? "",
33
35
  branch: parsed.branch,
34
36
  mode,
@@ -57,11 +57,94 @@ function getUpdateJSFunction(logPrefix: string): string {
57
57
  return false;
58
58
  }
59
59
 
60
+ async function swapTailwindStylesheet(nextHref) {
61
+ const current = document.getElementById('vf-tailwind-css');
62
+ if (!(current instanceof HTMLLinkElement) || !nextHref || !current.parentNode) {
63
+ return false;
64
+ }
65
+
66
+ const nextUrl = new URL(nextHref, window.location.origin).toString();
67
+ const currentHref = current.getAttribute('href');
68
+ const currentUrl = currentHref ? new URL(currentHref, window.location.href).toString() : '';
69
+ if (currentUrl === nextUrl) {
70
+ return true;
71
+ }
72
+
73
+ const pending = current.cloneNode(false);
74
+ if (!(pending instanceof HTMLLinkElement)) {
75
+ return false;
76
+ }
77
+
78
+ pending.removeAttribute('id');
79
+ pending.setAttribute('data-vf-tailwind-pending', 'true');
80
+ pending.href = nextHref;
81
+
82
+ await new Promise((resolve, reject) => {
83
+ const timeoutId = window.setTimeout(() => {
84
+ cleanup(new Error('stylesheet-timeout'));
85
+ }, 5000);
86
+
87
+ let settled = false;
88
+
89
+ function cleanup(error) {
90
+ if (settled) return;
91
+ settled = true;
92
+ window.clearTimeout(timeoutId);
93
+ pending.removeEventListener('load', onLoad);
94
+ pending.removeEventListener('error', onError);
95
+
96
+ if (error) {
97
+ pending.remove();
98
+ reject(error);
99
+ return;
100
+ }
101
+
102
+ pending.id = 'vf-tailwind-css';
103
+ current.remove();
104
+ resolve(true);
105
+ }
106
+
107
+ function onLoad() {
108
+ cleanup(null);
109
+ }
110
+
111
+ function onError() {
112
+ cleanup(new Error('stylesheet-load-failed'));
113
+ }
114
+
115
+ pending.addEventListener('load', onLoad, { once: true });
116
+ pending.addEventListener('error', onError, { once: true });
117
+ current.parentNode.insertBefore(pending, current.nextSibling);
118
+ });
119
+
120
+ return true;
121
+ }
122
+
123
+ async function applyStyleUpdate(changedPath, styleHref) {
124
+ if (styleHref) {
125
+ try {
126
+ const swapped = await swapTailwindStylesheet(styleHref);
127
+ if (swapped) {
128
+ ${
129
+ logPrefix === "[HMR]"
130
+ ? `console.log('${logPrefix} Swapped stylesheet:', styleHref);`
131
+ : `dlog('${logPrefix} Swapped stylesheet:', styleHref);`
132
+ }
133
+ return true;
134
+ }
135
+ } catch (error) {
136
+ console.warn('${logPrefix} Failed to swap stylesheet:', error);
137
+ }
138
+ }
139
+
140
+ return refreshStylesheets(changedPath) || refreshStylesheets();
141
+ }
142
+
60
143
  function getRenderPath() {
61
144
  return window.location.pathname + window.location.search + window.location.hash;
62
145
  }
63
146
 
64
- async function updateJS(path) {
147
+ async function updateJS(path, styleHref) {
65
148
  ${
66
149
  logPrefix === "[HMR]"
67
150
  ? `console.log('${logPrefix} Updating JS module:', path);`
@@ -83,7 +166,7 @@ function getUpdateJSFunction(logPrefix: string): string {
83
166
  }
84
167
 
85
168
  // Refresh Tailwind CSS (new classes may be needed from JS changes)
86
- refreshStylesheets();
169
+ await applyStyleUpdate(path, styleHref);
87
170
 
88
171
  // Re-render the page with fresh modules
89
172
  if (window.__veryfrontRenderPage) {
@@ -271,6 +354,7 @@ function generateHMRClient(opts: HMRScriptOptions): string {
271
354
 
272
355
  // Debounce updates to prevent flashing from rapid-fire changes
273
356
  let pendingPaths = [];
357
+ let pendingStyleHref = null;
274
358
  let updateDebounceTimer = null;
275
359
  const UPDATE_DEBOUNCE_MS = 150;
276
360
 
@@ -282,7 +366,7 @@ function generateHMRClient(opts: HMRScriptOptions): string {
282
366
 
283
367
  // CSS changes: hot-swap stylesheet without full page reload
284
368
  if (update.path.endsWith('.css')) {
285
- const didRefresh = refreshStylesheets(update.path) || refreshStylesheets();
369
+ const didRefresh = await applyStyleUpdate(update.path, update.styleHref);
286
370
  if (!didRefresh) {
287
371
  notifyStudioAndReload('css-update-no-stylesheet');
288
372
  return;
@@ -293,12 +377,17 @@ function generateHMRClient(opts: HMRScriptOptions): string {
293
377
 
294
378
  // Debounce JS updates — batch rapid updates into single re-render
295
379
  pendingPaths.push(update.path);
380
+ if (typeof update.styleHref === 'string') {
381
+ pendingStyleHref = update.styleHref;
382
+ }
296
383
 
297
384
  if (updateDebounceTimer) clearTimeout(updateDebounceTimer);
298
385
 
299
386
  updateDebounceTimer = setTimeout(async () => {
300
387
  const paths = pendingPaths;
388
+ const styleHref = pendingStyleHref;
301
389
  pendingPaths = [];
390
+ pendingStyleHref = null;
302
391
  updateDebounceTimer = null;
303
392
 
304
393
  if (paths.length > 1) {
@@ -306,7 +395,7 @@ function generateHMRClient(opts: HMRScriptOptions): string {
306
395
  }
307
396
 
308
397
  // Single re-render handles all paths (server propagates timestamps to all imports)
309
- if (paths.length > 0) await updateJS(paths[0]);
398
+ if (paths.length > 0) await updateJS(paths[0], styleHref);
310
399
  }, UPDATE_DEBOUNCE_MS);
311
400
  }
312
401
  ${getUpdateJSFunction(logPrefix)}
@@ -29,6 +29,7 @@ import { createStyleScopeProfile } from "../../../html/styles-builder/style-scop
29
29
  import { serverLogger } from "../../../utils/index.js";
30
30
  import type { ResolvedContentContext } from "../../../platform/adapters/fs/veryfront/types.js";
31
31
  import type {
32
+ EnsureStyleArtifactBuildInput,
32
33
  ResolveStyleArtifactInput,
33
34
  VeryfrontApiClient,
34
35
  } from "../../../platform/adapters/veryfront-api-client/index.js";
@@ -353,6 +354,9 @@ body::before {
353
354
  });
354
355
 
355
356
  if (resolved.status !== "ready" || !resolved.artifactHash) {
357
+ if (resolved.status !== "building") {
358
+ await this.ensureRemotePreparedCSSBuild(client, selector, styleProfileHash);
359
+ }
356
360
  return undefined;
357
361
  }
358
362
 
@@ -415,4 +419,31 @@ body::before {
415
419
  });
416
420
  }
417
421
  }
422
+
423
+ private shouldEnsureRemoteStyleArtifactBuild(selector: StyleArtifactSelectorContext): boolean {
424
+ return Boolean(selector.environmentName || selector.releaseId);
425
+ }
426
+
427
+ private async ensureRemotePreparedCSSBuild(
428
+ client: VeryfrontApiClient,
429
+ selector: StyleArtifactSelectorContext,
430
+ styleProfileHash: string,
431
+ ): Promise<void> {
432
+ if (!this.shouldEnsureRemoteStyleArtifactBuild(selector)) return;
433
+
434
+ try {
435
+ await client.ensureStyleArtifactBuild(
436
+ {
437
+ ...selector,
438
+ styleProfileHash,
439
+ } satisfies EnsureStyleArtifactBuildInput,
440
+ );
441
+ } catch (error) {
442
+ logger.debug("Failed to ensure remote prepared CSS build", {
443
+ selector,
444
+ styleProfileHash,
445
+ error: error instanceof Error ? error.message : String(error),
446
+ });
447
+ }
448
+ }
418
449
  }
@@ -1,4 +1,5 @@
1
1
  import { serverLogger } from "../../../utils/index.js";
2
+ import type { ReloadProjectInfo } from "../../reload-notifier.js";
2
3
  import { getClientCount, getOpenSockets } from "./hmr-client-manager.js";
3
4
 
4
5
  const logger = serverLogger.component("hmr-handler");
@@ -19,6 +20,13 @@ export function getMetrics(): { clients: number } & HMRMetrics {
19
20
  return { clients: getClientCount(), ...metrics };
20
21
  }
21
22
 
23
+ function buildStyleUpdatePayload(project?: ReloadProjectInfo): Record<string, string> {
24
+ const payload: Record<string, string> = {};
25
+ if (project?.styleAssetPath) payload.styleHref = project.styleAssetPath;
26
+ if (project?.styleArtifactHash) payload.styleHash = project.styleArtifactHash;
27
+ return payload;
28
+ }
29
+
22
30
  function requiresFullReload(path: string): boolean {
23
31
  const ext = path.split(".").pop()?.toLowerCase();
24
32
  return ext === "mdx" || ext === "md" || path.includes("veryfront.config");
@@ -28,11 +36,12 @@ function requiresFullReload(path: string): boolean {
28
36
  * Broadcast update to all connected HMR clients, optionally filtered by projectSlug.
29
37
  * No server-side debounce here — ReloadNotifier already debounces (300ms).
30
38
  */
31
- export function broadcastUpdate(changedPaths?: string[], projectSlug?: string): void {
39
+ export function broadcastUpdate(changedPaths?: string[], project?: ReloadProjectInfo): void {
32
40
  logger.debug("broadcastUpdate called", {
33
41
  changedPaths,
34
42
  totalClients: getClientCount(),
35
- projectSlug,
43
+ projectSlug: project?.projectSlug,
44
+ styleAssetPath: project?.styleAssetPath,
36
45
  });
37
46
 
38
47
  const timestamp = Date.now();
@@ -44,12 +53,13 @@ export function broadcastUpdate(changedPaths?: string[], projectSlug?: string):
44
53
 
45
54
  if (needsFullReload) {
46
55
  const message = JSON.stringify({ type: "reload", timestamp });
47
- broadcastMessage(message, projectSlug);
56
+ broadcastMessage(message, project?.projectSlug);
48
57
  metrics.messagesForwarded++;
49
58
  } else {
59
+ const stylePayload = buildStyleUpdatePayload(project);
50
60
  for (const path of changedPaths) {
51
- const message = JSON.stringify({ type: "update", path, timestamp });
52
- broadcastMessage(message, projectSlug);
61
+ const message = JSON.stringify({ type: "update", path, timestamp, ...stylePayload });
62
+ broadcastMessage(message, project?.projectSlug);
53
63
  metrics.messagesForwarded++;
54
64
  }
55
65
  }
@@ -78,7 +78,7 @@ export class HMRHandler extends BaseHandler {
78
78
  return;
79
79
  }
80
80
 
81
- broadcastUpdate(changedPaths, project?.projectSlug);
81
+ broadcastUpdate(changedPaths, project);
82
82
  });
83
83
 
84
84
  startPingInterval();
@@ -16,7 +16,7 @@ import { HTTP_OK } from "../../../utils/constants/index.js";
16
16
  import { compileMarkdownRuntime } from "../../../transforms/md/compiler/md-compiler.js";
17
17
  import { extract } from "../../../platform/compat/std/front-matter-yaml.js";
18
18
  import { isExtendedFSAdapter } from "../../../platform/adapters/fs/wrapper.js";
19
- import { getEnv } from "../../../platform/compat/process.js";
19
+ import { getHostEnv } from "../../../platform/compat/process.js";
20
20
  import { tryNotFoundFallback } from "../request/ssr/not-found-fallback.js";
21
21
  import { generateMarkdownHtml } from "./markdown-html-generator.js";
22
22
  import { validatePathSync } from "../../../security/index.js";
@@ -70,7 +70,9 @@ export class MarkdownPreviewHandler extends BaseHandler {
70
70
  const hasMultiProjectSupport = isExtendedFSAdapter(fsAdapter) && fsAdapter.isMultiProjectMode();
71
71
 
72
72
  if (ctx.projectSlug && hasMultiProjectSupport) {
73
- const effectiveToken = ctx.proxyToken || getEnv("VERYFRONT_API_TOKEN") || "";
73
+ // Framework-owned token: bypass project env overlay so proxy mode works
74
+ // when a remote project overlay is active.
75
+ const effectiveToken = ctx.proxyToken || getHostEnv("VERYFRONT_API_TOKEN") || "";
74
76
  const branch = ctx.parsedDomain?.branch ?? null;
75
77
 
76
78
  return await fsAdapter.runWithContext(
@@ -10,7 +10,7 @@ import {
10
10
  extractCacheKeyContext,
11
11
  runWithCacheKeyContext,
12
12
  } from "../../../cache/cache-key-builder.js";
13
- import { getEnv } from "../../../platform/compat/process.js";
13
+ import { getHostEnv } from "../../../platform/compat/process.js";
14
14
  import { runWithRequestContext } from "../../../platform/adapters/fs/veryfront/multi-project-adapter.js";
15
15
 
16
16
  /** Pattern to match hashed CSS URLs: /_vf/css/[8-char-hash].css */
@@ -50,7 +50,9 @@ export class CSSHandler extends BaseHandler {
50
50
  // the distributed API cache backend can't authenticate and silently returns
51
51
  // null — causing cross-pod cache misses. Wrap the lookup in request context
52
52
  // so the API backend can resolve the token and project.
53
- const effectiveToken = ctx.proxyToken || getEnv("VERYFRONT_API_TOKEN") || "";
53
+ // Framework-owned token: bypass project env overlay so proxy mode works
54
+ // when a remote project overlay is active.
55
+ const effectiveToken = ctx.proxyToken || getHostEnv("VERYFRONT_API_TOKEN") || "";
54
56
  const lookup = () =>
55
57
  runWithCacheKeyContext(cacheCtx, () =>
56
58
  getCSSWithJITFallback(
@@ -19,7 +19,7 @@ import type {
19
19
  import { PRIORITY_LOW } from "../../../../utils/constants/index.js";
20
20
  import { generateNonce } from "../../../../security/http/response/security-handler.js";
21
21
  import { isExtendedFSAdapter } from "../../../../platform/adapters/fs/wrapper.js";
22
- import { getEnv } from "../../../../platform/compat/process.js";
22
+ import { getHostEnv } from "../../../../platform/compat/process.js";
23
23
  import { shouldUseNoCacheHeadersFromHandler } from "../../../context/enriched-context.js";
24
24
  import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
25
25
  import { serverLogger } from "../../../../utils/index.js";
@@ -117,7 +117,9 @@ export class SSRHandler extends BaseHandler {
117
117
  if (ctx.projectSlug && isExtended && fsAdapter.isMultiProjectMode()) {
118
118
  const prodMode = isProductionMode(ctx, url);
119
119
  const branch = ctx.parsedDomain?.branch ?? null;
120
- const effectiveToken = ctx.proxyToken || getEnv("VERYFRONT_API_TOKEN") || "";
120
+ // Framework-owned token: bypass project env overlay so proxy mode works
121
+ // when a remote project overlay is active.
122
+ const effectiveToken = ctx.proxyToken || getHostEnv("VERYFRONT_API_TOKEN") || "";
121
123
 
122
124
  logger.debug("Using multi-project context", {
123
125
  projectSlug: ctx.projectSlug,
@@ -3,13 +3,15 @@ import { serverLogger } from "../utils/index.js";
3
3
 
4
4
  const logger = serverLogger.component("reload-notifier");
5
5
 
6
- interface ReloadProjectInfo {
6
+ export interface ReloadProjectInfo {
7
7
  projectSlug?: string;
8
8
  projectId?: string;
9
9
  projectDir?: string;
10
10
  environment?: "preview" | "production";
11
11
  branch?: string | null;
12
12
  releaseId?: string | null;
13
+ styleArtifactHash?: string;
14
+ styleAssetPath?: string;
13
15
  }
14
16
 
15
17
  type ReloadListener = (changedPaths?: string[], project?: ReloadProjectInfo) => void;
@@ -3,7 +3,7 @@ import { getEnv } from "../platform/compat/process.js";
3
3
 
4
4
  // Keep in sync with deno.json version.
5
5
  // scripts/release.ts updates this constant during releases.
6
- export const VERSION = "0.1.92";
6
+ export const VERSION = "0.1.94";
7
7
 
8
8
  export function normalizeVeryfrontVersion(version: string | undefined): string | undefined {
9
9
  if (!version) return undefined;