veryfront 0.1.26 → 0.1.28

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 (124) hide show
  1. package/README.md +3 -11
  2. package/esm/cli/app/shell.d.ts.map +1 -1
  3. package/esm/cli/app/shell.js +9 -5
  4. package/esm/cli/commands/demo/demo.js +1 -1
  5. package/esm/cli/commands/init/catalog.d.ts.map +1 -1
  6. package/esm/cli/commands/init/catalog.js +13 -5
  7. package/esm/cli/commands/init/command-help.js +4 -4
  8. package/esm/cli/commands/init/types.d.ts +1 -1
  9. package/esm/cli/commands/init/types.d.ts.map +1 -1
  10. package/esm/cli/commands/serve/command.d.ts.map +1 -1
  11. package/esm/cli/commands/serve/command.js +0 -4
  12. package/esm/cli/commands/start/command.d.ts.map +1 -1
  13. package/esm/cli/commands/start/command.js +16 -9
  14. package/esm/cli/help/tips.js +6 -6
  15. package/esm/cli/mcp/remote-file-tools.js +1 -1
  16. package/esm/cli/mcp/tools/catalog-tools.d.ts +3 -3
  17. package/esm/cli/mcp/tools/catalog-tools.d.ts.map +1 -1
  18. package/esm/cli/mcp/tools/catalog-tools.js +21 -13
  19. package/esm/cli/mcp/tools/project-tools.js +1 -1
  20. package/esm/cli/templates/index.js +11 -11
  21. package/esm/cli/templates/manifest.d.ts +22 -15
  22. package/esm/cli/templates/manifest.js +24 -17
  23. package/esm/cli/templates/types.d.ts +1 -1
  24. package/esm/cli/templates/types.d.ts.map +1 -1
  25. package/esm/cli/utils/index.d.ts.map +1 -1
  26. package/esm/cli/utils/index.js +13 -1
  27. package/esm/deno.js +1 -1
  28. package/esm/src/html/html-shell-generator.d.ts.map +1 -1
  29. package/esm/src/html/html-shell-generator.js +2 -0
  30. package/esm/src/html/styles-builder/project-css-cache.d.ts +8 -1
  31. package/esm/src/html/styles-builder/project-css-cache.d.ts.map +1 -1
  32. package/esm/src/html/styles-builder/project-css-cache.js +13 -2
  33. package/esm/src/html/styles-builder/tailwind-compiler.d.ts +2 -0
  34. package/esm/src/html/styles-builder/tailwind-compiler.d.ts.map +1 -1
  35. package/esm/src/html/styles-builder/tailwind-compiler.js +52 -19
  36. package/esm/src/modules/react-loader/css-import-collector.d.ts +29 -0
  37. package/esm/src/modules/react-loader/css-import-collector.d.ts.map +1 -0
  38. package/esm/src/modules/react-loader/css-import-collector.js +41 -0
  39. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  40. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +6 -0
  41. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.d.ts.map +1 -1
  42. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.js +5 -0
  43. package/esm/src/platform/adapters/fs/factory.d.ts.map +1 -1
  44. package/esm/src/platform/adapters/fs/factory.js +5 -1
  45. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +1 -0
  46. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  47. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +19 -5
  48. package/esm/src/platform/compat/process.d.ts.map +1 -1
  49. package/esm/src/platform/compat/process.js +20 -3
  50. package/esm/src/proxy/main.js +31 -12
  51. package/esm/src/proxy/token-manager.d.ts +2 -0
  52. package/esm/src/proxy/token-manager.d.ts.map +1 -1
  53. package/esm/src/proxy/token-manager.js +47 -8
  54. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts +23 -0
  55. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -0
  56. package/esm/src/rendering/orchestrator/css-candidate-manifest.js +132 -0
  57. package/esm/src/rendering/orchestrator/html.d.ts +11 -1
  58. package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
  59. package/esm/src/rendering/orchestrator/html.js +103 -18
  60. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  61. package/esm/src/rendering/orchestrator/pipeline.js +14 -2
  62. package/esm/src/server/bootstrap.d.ts +2 -0
  63. package/esm/src/server/bootstrap.d.ts.map +1 -1
  64. package/esm/src/server/bootstrap.js +10 -0
  65. package/esm/src/server/handlers/preview/markdown-html-generator.d.ts.map +1 -1
  66. package/esm/src/server/handlers/preview/markdown-html-generator.js +11 -5
  67. package/esm/src/server/production-server.js +10 -2
  68. package/esm/src/studio/bridge-template.d.ts +2 -0
  69. package/esm/src/studio/bridge-template.d.ts.map +1 -1
  70. package/esm/src/studio/bridge-template.js +3390 -52
  71. package/esm/src/transforms/css-modules/naming.d.ts +33 -0
  72. package/esm/src/transforms/css-modules/naming.d.ts.map +1 -0
  73. package/esm/src/transforms/css-modules/naming.js +128 -0
  74. package/esm/src/transforms/esm/import-parser.d.ts +1 -0
  75. package/esm/src/transforms/esm/import-parser.d.ts.map +1 -1
  76. package/esm/src/transforms/esm/import-parser.js +16 -5
  77. package/esm/src/transforms/pipeline/index.d.ts.map +1 -1
  78. package/esm/src/transforms/pipeline/index.js +3 -1
  79. package/esm/src/transforms/pipeline/stages/index.d.ts +1 -0
  80. package/esm/src/transforms/pipeline/stages/index.d.ts.map +1 -1
  81. package/esm/src/transforms/pipeline/stages/index.js +1 -0
  82. package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts +18 -0
  83. package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts.map +1 -0
  84. package/esm/src/transforms/pipeline/stages/ssr-css-strip.js +168 -0
  85. package/package.json +1 -1
  86. package/src/cli/app/shell.ts +9 -5
  87. package/src/cli/commands/demo/demo.ts +1 -1
  88. package/src/cli/commands/init/catalog.ts +13 -5
  89. package/src/cli/commands/init/command-help.ts +4 -4
  90. package/src/cli/commands/init/types.ts +5 -5
  91. package/src/cli/commands/serve/command.ts +0 -5
  92. package/src/cli/commands/start/command.ts +15 -10
  93. package/src/cli/help/tips.ts +6 -6
  94. package/src/cli/mcp/remote-file-tools.ts +1 -1
  95. package/src/cli/mcp/tools/catalog-tools.ts +21 -13
  96. package/src/cli/mcp/tools/project-tools.ts +1 -1
  97. package/src/cli/templates/index.ts +11 -11
  98. package/src/cli/templates/manifest.js +24 -17
  99. package/src/cli/templates/types.ts +5 -5
  100. package/src/cli/utils/index.ts +12 -1
  101. package/src/deno.js +1 -1
  102. package/src/src/html/html-shell-generator.ts +2 -0
  103. package/src/src/html/styles-builder/project-css-cache.ts +24 -1
  104. package/src/src/html/styles-builder/tailwind-compiler.ts +67 -26
  105. package/src/src/modules/react-loader/css-import-collector.ts +50 -0
  106. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +7 -0
  107. package/src/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.ts +6 -0
  108. package/src/src/platform/adapters/fs/factory.ts +5 -1
  109. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +21 -5
  110. package/src/src/platform/compat/process.ts +28 -4
  111. package/src/src/proxy/main.ts +32 -12
  112. package/src/src/proxy/token-manager.ts +54 -8
  113. package/src/src/rendering/orchestrator/css-candidate-manifest.ts +176 -0
  114. package/src/src/rendering/orchestrator/html.ts +128 -16
  115. package/src/src/rendering/orchestrator/pipeline.ts +183 -165
  116. package/src/src/server/bootstrap.ts +16 -0
  117. package/src/src/server/handlers/preview/markdown-html-generator.ts +12 -5
  118. package/src/src/server/production-server.ts +12 -2
  119. package/src/src/studio/bridge-template.ts +3392 -52
  120. package/src/src/transforms/css-modules/naming.ts +152 -0
  121. package/src/src/transforms/esm/import-parser.ts +15 -5
  122. package/src/src/transforms/pipeline/index.ts +3 -0
  123. package/src/src/transforms/pipeline/stages/index.ts +1 -0
  124. package/src/src/transforms/pipeline/stages/ssr-css-strip.ts +201 -0
@@ -46,6 +46,10 @@ import { withTimeout, withTimeoutThrow } from "../utils/stream-utils.js";
46
46
  import { generateTailwind4CSS } from "../../html/styles-builder/index.js";
47
47
  import { createEsmCache, createModuleCache, loadModule } from "./module-loader/index.js";
48
48
  import type { ModuleLoaderConfig } from "./module-loader/index.js";
49
+ import {
50
+ getCSSImports,
51
+ runWithCSSCollector,
52
+ } from "../../modules/react-loader/css-import-collector.js";
49
53
 
50
54
  // Extracted modules
51
55
  import { EMPTY_LAYOUT_RESULT, isDotPath } from "./path-helpers.js";
@@ -339,186 +343,200 @@ export class RenderPipeline {
339
343
 
340
344
  return withSpan(
341
345
  "render.page",
342
- async () => {
343
- const pageResolveStart = performance.now();
344
- const pageInfo = await withSpan(
345
- "render.resolve_page",
346
- () => this.config.pageResolver.resolvePage(slug),
347
- { "render.slug": slug },
348
- );
349
- timing.pageResolve = Math.round(performance.now() - pageResolveStart);
346
+ () =>
347
+ runWithCSSCollector(async () => {
348
+ const pageResolveStart = performance.now();
349
+ const pageInfo = await withSpan(
350
+ "render.resolve_page",
351
+ () => this.config.pageResolver.resolvePage(slug),
352
+ { "render.slug": slug },
353
+ );
354
+ timing.pageResolve = Math.round(performance.now() - pageResolveStart);
350
355
 
351
- const skipLayouts = isDotPath(slug, pageInfo.entity.path);
356
+ const skipLayouts = isDotPath(slug, pageInfo.entity.path);
352
357
 
353
- const layoutCollectStart = performance.now();
354
- const layoutResult = skipLayouts ? EMPTY_LAYOUT_RESULT : await withSpan(
355
- "render.collect_layouts",
356
- () => this.config.layoutOrchestrator.collectLayouts(pageInfo),
357
- { "render.slug": slug },
358
- );
359
- timing.layoutCollect = Math.round(performance.now() - layoutCollectStart);
360
-
361
- const layoutPreloadPromise = !skipLayouts && layoutResult.nestedLayouts.length > 0
362
- ? this.config.layoutOrchestrator.preloadLayoutModules(layoutResult.nestedLayouts)
363
- : Promise.resolve();
364
-
365
- let dataFetchingProps: Record<string, unknown> | undefined;
366
- let resolvedParams: Record<string, string | string[]> = options?.params
367
- ? { ...options.params }
368
- : {};
369
- let layoutDataMap = new Map<string, Record<string, unknown>>();
370
-
371
- const dataFetchStart = performance.now();
372
- if (options?.request && options?.url) {
373
- await withSpan(
374
- "render.data_fetching",
375
- async () => {
376
- try {
377
- const dataResolution = await this.resolveDataFetching(
378
- slug,
379
- pageInfo.entity.path,
380
- layoutResult.nestedLayouts,
381
- options,
382
- );
383
- resolvedParams = dataResolution.params;
384
- dataFetchingProps = Object.keys(dataResolution.pageProps).length > 0
385
- ? dataResolution.pageProps
386
- : undefined;
387
- layoutDataMap = dataResolution.layoutProps;
388
- } catch (error) {
389
- if (error instanceof VeryfrontError) throw error;
390
-
391
- renderPageLog.error("Data fetching error", {
392
- slug,
393
- error: error instanceof Error ? error.message : String(error),
394
- });
395
- throw error;
396
- }
397
- },
358
+ const layoutCollectStart = performance.now();
359
+ const layoutResult = skipLayouts ? EMPTY_LAYOUT_RESULT : await withSpan(
360
+ "render.collect_layouts",
361
+ () => this.config.layoutOrchestrator.collectLayouts(pageInfo),
398
362
  { "render.slug": slug },
399
363
  );
400
- }
401
- timing.dataFetch = Math.round(performance.now() - dataFetchStart);
402
-
403
- const hasResolvedParams = Object.keys(resolvedParams).length > 0;
404
- const mergedOptions = (dataFetchingProps || hasResolvedParams)
405
- ? {
406
- ...options,
407
- ...(hasResolvedParams ? { params: resolvedParams } : {}),
408
- ...(dataFetchingProps ? { props: { ...options?.props, ...dataFetchingProps } } : {}),
364
+ timing.layoutCollect = Math.round(performance.now() - layoutCollectStart);
365
+
366
+ const layoutPreloadPromise = !skipLayouts && layoutResult.nestedLayouts.length > 0
367
+ ? this.config.layoutOrchestrator.preloadLayoutModules(layoutResult.nestedLayouts)
368
+ : Promise.resolve();
369
+
370
+ let dataFetchingProps: Record<string, unknown> | undefined;
371
+ let resolvedParams: Record<string, string | string[]> = options?.params
372
+ ? { ...options.params }
373
+ : {};
374
+ let layoutDataMap = new Map<string, Record<string, unknown>>();
375
+
376
+ const dataFetchStart = performance.now();
377
+ if (options?.request && options?.url) {
378
+ await withSpan(
379
+ "render.data_fetching",
380
+ async () => {
381
+ try {
382
+ const dataResolution = await this.resolveDataFetching(
383
+ slug,
384
+ pageInfo.entity.path,
385
+ layoutResult.nestedLayouts,
386
+ options,
387
+ );
388
+ resolvedParams = dataResolution.params;
389
+ dataFetchingProps = Object.keys(dataResolution.pageProps).length > 0
390
+ ? dataResolution.pageProps
391
+ : undefined;
392
+ layoutDataMap = dataResolution.layoutProps;
393
+ } catch (error) {
394
+ if (error instanceof VeryfrontError) throw error;
395
+
396
+ renderPageLog.error("Data fetching error", {
397
+ slug,
398
+ error: error instanceof Error ? error.message : String(error),
399
+ });
400
+ throw error;
401
+ }
402
+ },
403
+ { "render.slug": slug },
404
+ );
409
405
  }
410
- : options;
411
-
412
- const bundlePrepStart = performance.now();
413
- const pageBundleResult = await withSpan(
414
- "render.prepare_bundles",
415
- () =>
416
- this.config.pageRenderer.preparePageBundles(
417
- pageInfo,
418
- slug,
419
- cacheResult?.cachedModule,
420
- mergedOptions,
421
- ),
422
- { "render.slug": slug },
423
- );
424
- timing.bundlePrep = Math.round(performance.now() - bundlePrepStart);
406
+ timing.dataFetch = Math.round(performance.now() - dataFetchStart);
407
+
408
+ const hasResolvedParams = Object.keys(resolvedParams).length > 0;
409
+ const mergedOptions = (dataFetchingProps || hasResolvedParams)
410
+ ? {
411
+ ...options,
412
+ ...(hasResolvedParams ? { params: resolvedParams } : {}),
413
+ ...(dataFetchingProps ? { props: { ...options?.props, ...dataFetchingProps } } : {}),
414
+ }
415
+ : options;
416
+
417
+ const bundlePrepStart = performance.now();
418
+ const pageBundleResult = await withSpan(
419
+ "render.prepare_bundles",
420
+ () =>
421
+ this.config.pageRenderer.preparePageBundles(
422
+ pageInfo,
423
+ slug,
424
+ cacheResult?.cachedModule,
425
+ mergedOptions,
426
+ ),
427
+ { "render.slug": slug },
428
+ );
429
+ timing.bundlePrep = Math.round(performance.now() - bundlePrepStart);
425
430
 
426
- if (pageBundleResult.scriptResult) return pageBundleResult.scriptResult;
431
+ if (pageBundleResult.scriptResult) return pageBundleResult.scriptResult;
427
432
 
428
- if (!pageBundleResult.pageElement || !pageBundleResult.pageBundle) {
429
- throw RENDER_ERROR.create({
430
- detail: "Failed to prepare page bundle",
431
- context: { slug },
432
- });
433
- }
433
+ if (!pageBundleResult.pageElement || !pageBundleResult.pageBundle) {
434
+ throw RENDER_ERROR.create({
435
+ detail: "Failed to prepare page bundle",
436
+ context: { slug },
437
+ });
438
+ }
434
439
 
435
- const { pageElement, pageBundle } = pageBundleResult;
436
-
437
- const mergedFrontmatter = {
438
- ...pageInfo.entity.frontmatter,
439
- ...(pageBundle as MdxBundle).frontmatter,
440
- };
441
-
442
- const headings = (pageBundle as PageBundle).headings || [];
443
-
444
- await layoutPreloadPromise;
445
-
446
- const layoutApplyStart = performance.now();
447
- const wrappedElement = await withSpan(
448
- "render.apply_layouts",
449
- () =>
450
- this.config.layoutOrchestrator.applyLayoutsAndWrappers(
451
- pageElement,
452
- pageInfo,
453
- layoutResult.layoutBundle,
454
- layoutResult.nestedLayouts,
455
- layoutDataMap,
456
- options?.url,
457
- mergedFrontmatter,
458
- headings,
459
- options?.projectSlug,
460
- ),
461
- { "render.slug": slug, "render.layout_count": layoutResult.nestedLayouts.length },
462
- );
463
- timing.layoutApply = Math.round(performance.now() - layoutApplyStart);
464
-
465
- const ssrStart = performance.now();
466
- const ssrResult = await withSpan(
467
- "render.ssr",
468
- () =>
469
- withTimeoutThrow(
470
- this.config.ssrOrchestrator.performSSRRendering(
471
- wrappedElement,
472
- {
473
- pageInfo,
474
- pageBundle,
475
- layoutBundle: layoutResult.layoutBundle,
476
- nestedLayouts: layoutResult.nestedLayouts,
477
- collectedMetadata: pageBundleResult.collectedMetadata,
478
- slug,
479
- },
480
- mergedOptions,
440
+ const { pageElement, pageBundle } = pageBundleResult;
441
+
442
+ const mergedFrontmatter = {
443
+ ...pageInfo.entity.frontmatter,
444
+ ...(pageBundle as MdxBundle).frontmatter,
445
+ };
446
+
447
+ const headings = (pageBundle as PageBundle).headings || [];
448
+
449
+ await layoutPreloadPromise;
450
+
451
+ const layoutApplyStart = performance.now();
452
+ const wrappedElement = await withSpan(
453
+ "render.apply_layouts",
454
+ () =>
455
+ this.config.layoutOrchestrator.applyLayoutsAndWrappers(
456
+ pageElement,
457
+ pageInfo,
458
+ layoutResult.layoutBundle,
459
+ layoutResult.nestedLayouts,
460
+ layoutDataMap,
461
+ options?.url,
462
+ mergedFrontmatter,
463
+ headings,
464
+ options?.projectSlug,
481
465
  ),
482
- SSR_RENDER_TIMEOUT_MS,
483
- `SSR rendering for ${slug}`,
484
- ),
485
- { "render.slug": slug, "render.delivery": mergedOptions?.delivery || "full" },
486
- );
487
- timing.ssr = Math.round(performance.now() - ssrStart);
466
+ { "render.slug": slug, "render.layout_count": layoutResult.nestedLayouts.length },
467
+ );
468
+ timing.layoutApply = Math.round(performance.now() - layoutApplyStart);
469
+
470
+ // Snapshot CSS imports collected during module loading (before SSR rendering).
471
+ // These are passed to the HTML generator to be included in the output.
472
+ const collectedCSSImports = getCSSImports();
473
+
474
+ const ssrStart = performance.now();
475
+ const ssrResult = await withSpan(
476
+ "render.ssr",
477
+ () =>
478
+ withTimeoutThrow(
479
+ this.config.ssrOrchestrator.performSSRRendering(
480
+ wrappedElement,
481
+ {
482
+ pageInfo,
483
+ pageBundle,
484
+ layoutBundle: layoutResult.layoutBundle,
485
+ nestedLayouts: layoutResult.nestedLayouts,
486
+ collectedMetadata: pageBundleResult.collectedMetadata,
487
+ slug,
488
+ cssImports: collectedCSSImports,
489
+ },
490
+ mergedOptions,
491
+ ),
492
+ SSR_RENDER_TIMEOUT_MS,
493
+ `SSR rendering for ${slug}`,
494
+ ),
495
+ { "render.slug": slug, "render.delivery": mergedOptions?.delivery || "full" },
496
+ );
497
+ timing.ssr = Math.round(performance.now() - ssrStart);
488
498
 
489
- const pageModule = pageBundleResult.clientModuleCode && pageBundleResult.pageModuleType
490
- ? {
491
- slug,
492
- code: pageBundleResult.clientModuleCode,
493
- type: pageBundleResult.pageModuleType,
499
+ if (collectedCSSImports.length > 0) {
500
+ renderPipelineLog.debug("CSS imports collected for HTML generation", {
501
+ slug,
502
+ count: collectedCSSImports.length,
503
+ paths: collectedCSSImports.map((p) => p.split("/").pop()),
504
+ });
494
505
  }
495
- : undefined;
496
-
497
- const result: RenderResult = {
498
- html: ssrResult.fullHtml,
499
- frontmatter: (pageBundleResult.pageBundle as MdxBundle).frontmatter || {},
500
- headings: pageBundleResult.pageBundle.headings || [],
501
- nodeMap: pageBundleResult.pageBundle.nodeMap,
502
- stream: ssrResult.finalStream,
503
- ssrHash: ssrResult.ssrHash,
504
- ...(pageModule ? { pageModule } : {}),
505
- };
506
-
507
- if (shouldCache && !options?.skipCachePersist) {
508
- this.config.cacheCoordinator.persistResult(result, slug, cacheKey).catch((error) => {
509
- renderPipelineLog.error("Cache persist failed", {
506
+
507
+ const pageModule = pageBundleResult.clientModuleCode && pageBundleResult.pageModuleType
508
+ ? {
510
509
  slug,
511
- error: error instanceof Error ? error.message : String(error),
512
- stack: error instanceof Error ? error.stack : undefined,
510
+ code: pageBundleResult.clientModuleCode,
511
+ type: pageBundleResult.pageModuleType,
512
+ }
513
+ : undefined;
514
+
515
+ const result: RenderResult = {
516
+ html: ssrResult.fullHtml,
517
+ frontmatter: (pageBundleResult.pageBundle as MdxBundle).frontmatter || {},
518
+ headings: pageBundleResult.pageBundle.headings || [],
519
+ nodeMap: pageBundleResult.pageBundle.nodeMap,
520
+ stream: ssrResult.finalStream,
521
+ ssrHash: ssrResult.ssrHash,
522
+ ...(pageModule ? { pageModule } : {}),
523
+ };
524
+
525
+ if (shouldCache && !options?.skipCachePersist) {
526
+ this.config.cacheCoordinator.persistResult(result, slug, cacheKey).catch((error) => {
527
+ renderPipelineLog.error("Cache persist failed", {
528
+ slug,
529
+ error: error instanceof Error ? error.message : String(error),
530
+ stack: error instanceof Error ? error.stack : undefined,
531
+ });
513
532
  });
514
- });
515
- }
533
+ }
516
534
 
517
- timing.total = Math.round(performance.now() - pipelineStartTime);
518
- renderPipelineLog.debug("Complete", { slug, timing });
535
+ timing.total = Math.round(performance.now() - pipelineStartTime);
536
+ renderPipelineLog.debug("Complete", { slug, timing });
519
537
 
520
- return result;
521
- },
538
+ return result;
539
+ }).then(({ result }) => result),
522
540
  {
523
541
  "render.slug": slug,
524
542
  "render.project_id": options?.projectId || this.config.projectDir,
@@ -8,6 +8,7 @@ import {
8
8
  } from "../config/environment-config.js";
9
9
  import { getErrorMessage } from "../errors/veryfront-error.js";
10
10
  import { enhanceAdapterWithFS } from "../platform/adapters/fs/integration.js";
11
+ import { isExtendedFSAdapter } from "../platform/adapters/fs/wrapper.js";
11
12
  import { getEnv } from "../platform/compat/process.js";
12
13
  import { initializeEsbuild } from "../platform/compat/esbuild.js";
13
14
  import { logger } from "../utils/index.js";
@@ -38,6 +39,9 @@ export interface BootstrapResult {
38
39
 
39
40
  /** FSAdapter type (if used) */
40
41
  fsAdapterType?: string;
42
+
43
+ /** Dispose FSAdapter resources (WebSocket connections, caches) */
44
+ dispose?: () => void;
41
45
  }
42
46
 
43
47
  let envLogged = false;
@@ -174,11 +178,23 @@ export async function bootstrap(
174
178
  fsAdapter: fsType,
175
179
  });
176
180
 
181
+ let dispose: (() => void) | undefined;
182
+ if (isExtendedFSAdapter(enhancedAdapter.fs)) {
183
+ const underlying = enhancedAdapter.fs.getUnderlyingAdapter();
184
+ if (
185
+ "dispose" in underlying &&
186
+ typeof (underlying as { dispose?: () => void }).dispose === "function"
187
+ ) {
188
+ dispose = () => (underlying as { dispose: () => void }).dispose();
189
+ }
190
+ }
191
+
177
192
  return {
178
193
  adapter: enhancedAdapter,
179
194
  config,
180
195
  usingFSAdapter: true,
181
196
  fsAdapterType: fsType,
197
+ dispose,
182
198
  };
183
199
  }
184
200
 
@@ -54,17 +54,24 @@ function detectTheme(req: dntShim.Request, url: URL): "light" | "dark" | null {
54
54
  }
55
55
 
56
56
  /**
57
- * Generate the studio bridge `<script>` tag when embedded in Studio.
58
- * Returns an empty string when not in studio embed mode.
57
+ * Generate the studio bridge `<script>` tag.
58
+ * Injected when embedded in Studio (`studio_embed=true`) or for standalone
59
+ * markdown/MDX pages so the edit button and editor features are available.
59
60
  */
60
61
  function buildStudioScript(url: URL, projectId: string, filePath: string): string {
61
62
  const studioEmbed = url.searchParams.get("studio_embed") === "true";
62
- if (!studioEmbed) return "";
63
+ const isMarkdown = /\.mdx?$/i.test(filePath);
64
+ if (!studioEmbed && !isMarkdown) return "";
65
+
66
+ const queryProjectId = url.searchParams.get("vf_project_id")?.trim() || "";
67
+ const queryFileId = url.searchParams.get("vf_file_id")?.trim() || "";
68
+ const canonicalProjectId = queryProjectId || projectId;
69
+ const canonicalPageId = queryFileId || filePath;
63
70
 
64
71
  return `<script>${
65
72
  generateStudioBridgeScript({
66
- projectId,
67
- pageId: filePath,
73
+ projectId: canonicalProjectId,
74
+ pageId: canonicalPageId,
68
75
  pagePath: filePath,
69
76
  })
70
77
  }</script>`;
@@ -293,12 +293,15 @@ if (import.meta.main) {
293
293
  // Note: Don't use HOSTNAME - K8s sets it to pod name which resolves to pod IP
294
294
  const bindAddress = adapter.env.get("BIND_ADDRESS") ?? "0.0.0.0";
295
295
 
296
+ const bootstrap = await bootstrapProd(projectDir, adapter);
297
+
296
298
  const server = await startProductionServer({
297
299
  projectDir,
298
300
  port,
299
301
  bindAddress,
300
302
  debug: isDebugEnabled(adapter.env),
301
303
  adapter, // Pass adapter to avoid re-detection
304
+ bootstrapResult: bootstrap,
302
305
  signal: shutdownController.signal,
303
306
  });
304
307
 
@@ -338,6 +341,7 @@ if (import.meta.main) {
338
341
  // Phase 3: Stop accepting new connections and clean up
339
342
  stopMemoryMonitoring();
340
343
  requestTracker.shutdown();
344
+ bootstrap.dispose?.();
341
345
  shutdownController.abort();
342
346
  await server.stop();
343
347
  await shutdownOTLP();
@@ -348,8 +352,14 @@ if (import.meta.main) {
348
352
  }
349
353
  };
350
354
 
351
- onSignal("SIGINT", () => void shutdown("SIGINT"));
352
- onSignal("SIGTERM", () => void shutdown("SIGTERM"));
355
+ const handleSignal = (signal: "SIGINT" | "SIGTERM"): void => {
356
+ void shutdown(signal).catch((error) => {
357
+ logger.warn("Unhandled error while shutting down production server", { signal, error });
358
+ });
359
+ };
360
+
361
+ onSignal("SIGINT", () => handleSignal("SIGINT"));
362
+ onSignal("SIGTERM", () => handleSignal("SIGTERM"));
353
363
  } catch (e) {
354
364
  logger.error("Failed to start production server:", e);
355
365
  }