@vesk/adapter 0.2.16 → 0.2.18

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.
@@ -1 +1 @@
1
- {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAqC,MAAM,yBAAyB,CAAC;AAoCrI;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA8BhE;AAoLD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBxD;AAmCD,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CA4T7B;AAMD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+B3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAUlE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCrG"}
1
+ {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAqC,MAAM,yBAAyB,CAAC;AAoCrI;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA8BhE;AAmUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBxD;AAmCD,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CAuX7B;AAMD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+B3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAUlE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCrG"}
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, writeFileSync, unlinkSync, statSync, rmSync } from 'node:fs';
1
+ import { readFileSync, existsSync, writeFileSync, unlinkSync, statSync, rmSync, mkdtempSync } from 'node:fs';
2
2
  import { resolve, join, dirname, relative, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { build } from './esbuild-fallback.js';
@@ -197,18 +197,160 @@ function demoteExports(code) {
197
197
  code = code.slice(0, e.start) + e.text + code.slice(e.end);
198
198
  return code;
199
199
  }
200
+ function hasChunkImports(code) {
201
+ try {
202
+ const ast = parse(code);
203
+ return (ast.body ?? []).some((n) => n.type === 'ImportDeclaration');
204
+ }
205
+ catch {
206
+ return false;
207
+ }
208
+ }
200
209
  /**
201
- * Code-split chunks execute as classic scripts, so NO import statement may
202
- * survive in a contribution (runtime and `.vsk` imports are stripped
203
- * earlier; npm packages and relative `.ts` values are unresolvable from a
204
- * `/_vesk/static/` classic script). Strips every remaining import and
205
- * demotes value exports — server-only components never execute
206
- * client-side, and islands must receive data through props/serialization
207
- * rather than module imports (a stripped binding referenced by an island
208
- * fails loudly at that island instead of killing the whole chunk).
210
+ * Rewrites a compiled file's source specifier so esbuild can resolve it from
211
+ * the temp bundle entry. Bare package specifiers (e.g. `lucide-vesk`) stay
212
+ * as-is; relative specifiers are resolved to absolute filesystem paths with a
213
+ * `.ts` extension appended so esbuild loads the module as TypeScript and
214
+ * inlines its exported values. Offset-based on the parser AST — never a text
215
+ * scan.
216
+ */
217
+ function resolveImportSource(src, filePath) {
218
+ if (src.startsWith('./') || src.startsWith('../')) {
219
+ return resolve(dirname(filePath), src) + '.ts';
220
+ }
221
+ return src;
222
+ }
223
+ /**
224
+ * Accumulates every import a code-split chunk needs and rebuilds it as a
225
+ * single deduped import block.
226
+ *
227
+ * A chunk is the concatenation of several independently compiled files, and
228
+ * each file's component AND hydrator both keep their imports verbatim. So the
229
+ * same named binding frequently appears multiple times in one chunk — once per
230
+ * (file, comp|hyd) pair, plus once per extra file. acorn's `parse()` rejects
231
+ * that with `Identifier has already been declared`, so `hasChunkImports` used
232
+ * to silently return false and esbuild — which rejects the same duplicates —
233
+ * never ran; the chunk shipped with raw `import` statements inside its
234
+ * classic-script IIFE and hydration died with `SyntaxError: Cannot use import
235
+ * statement outside a module`.
236
+ *
237
+ * Processing each file's contribution in isolation avoids the duplicate
238
+ * bindings entirely (a single compiled file parses cleanly), folds the imports
239
+ * into a per-source map keyed by local binding, strips them from the file
240
+ * body, and later emits one merged import block at the top of the chunk.
209
241
  */
210
- function stripChunkImports(code) {
211
- return demoteExports(removeCompiledNodes(code, isAnyImport));
242
+ class ChunkImports {
243
+ bySource = new Map();
244
+ aborted = false;
245
+ abortReason = null;
246
+ fail(reason, code) {
247
+ if (!this.aborted) {
248
+ this.aborted = true;
249
+ this.abortReason = reason;
250
+ }
251
+ return code;
252
+ }
253
+ /** Folds one file's imports into the merged set and returns the code with its imports stripped. */
254
+ add(code, filePath) {
255
+ if (!code || this.aborted)
256
+ return code;
257
+ let ast;
258
+ try {
259
+ ast = parse(code);
260
+ }
261
+ catch {
262
+ // A single compiled file is always valid ESM; if it somehow is not,
263
+ // stop merging so the assembler falls back to the historical path.
264
+ return this.fail(`unparseable contribution from ${filePath}`, code);
265
+ }
266
+ const body = ast.body ?? [];
267
+ const ranges = [];
268
+ for (const raw of body) {
269
+ const node = raw;
270
+ if (node.type !== 'ImportDeclaration' || typeof node.start !== 'number' || typeof node.end !== 'number')
271
+ continue;
272
+ const src = node.source?.value;
273
+ if (typeof src !== 'string')
274
+ continue;
275
+ const resolved = resolveImportSource(src, filePath);
276
+ let group = this.bySource.get(resolved);
277
+ if (!group) {
278
+ group = { named: new Map() };
279
+ this.bySource.set(resolved, group);
280
+ }
281
+ for (const spec of node.specifiers ?? []) {
282
+ const s = spec;
283
+ const local = s.local?.name;
284
+ if (typeof local !== 'string')
285
+ return this.fail(`nameless specifier from '${resolved}' in ${filePath}`, code);
286
+ if (s.type === 'ImportDefaultSpecifier') {
287
+ if (group.default && group.default.local !== local)
288
+ return this.fail(`conflicting default imports from '${resolved}' in ${filePath}`, code);
289
+ group.default = { kind: 'default', local };
290
+ }
291
+ else if (s.type === 'ImportNamespaceSpecifier') {
292
+ if (group.namespace && group.namespace.local !== local)
293
+ return this.fail(`conflicting namespace imports from '${resolved}' in ${filePath}`, code);
294
+ group.namespace = { kind: 'namespace', local };
295
+ }
296
+ else if (s.type === 'ImportSpecifier') {
297
+ const imported = s.imported?.name;
298
+ if (typeof imported !== 'string')
299
+ return this.fail(`nameless named-import from '${resolved}' in ${filePath}`, code);
300
+ const existing = group.named.get(imported);
301
+ if (existing && existing.local !== local)
302
+ return this.fail(`'${imported}' from '${resolved}' bound to both '${existing.local}' and '${local}' in ${filePath}`, code);
303
+ group.named.set(imported, { kind: 'named', local, imported });
304
+ }
305
+ }
306
+ let cut = node.end;
307
+ if (code[cut] === ';')
308
+ cut++;
309
+ ranges.push([node.start, cut]);
310
+ }
311
+ if (ranges.length === 0)
312
+ return code;
313
+ return removeRanges(code, ranges);
314
+ }
315
+ /** True when every file parsed and merged cleanly (no conflict, no parse abort). */
316
+ get ok() {
317
+ return !this.aborted;
318
+ }
319
+ /** Why the merge aborted (null when `ok`). Surfaced by the assembler. */
320
+ get reason() {
321
+ return this.abortReason;
322
+ }
323
+ /** Renders the deduped import block (empty string when there are no imports). */
324
+ render() {
325
+ if (this.aborted || this.bySource.size === 0)
326
+ return '';
327
+ const lines = [];
328
+ const sources = [...this.bySource.keys()].sort();
329
+ for (const src of sources) {
330
+ const g = this.bySource.get(src);
331
+ const parts = [];
332
+ if (g.default)
333
+ parts.push(`import ${g.default.local} from ${JSON.stringify(src)}`);
334
+ if (g.namespace)
335
+ parts.push(`import * as ${g.namespace.local} from ${JSON.stringify(src)}`);
336
+ const named = [...g.named.values()].sort((a, b) => (a.imported < b.imported ? -1 : 1));
337
+ if (named.length > 0) {
338
+ const specs = named.map((sp) => (sp.imported === sp.local ? sp.local : `${sp.imported} as ${sp.local}`)).join(', ');
339
+ parts.push(`import { ${specs} } from ${JSON.stringify(src)}`);
340
+ }
341
+ lines.push(parts.join('\n'));
342
+ }
343
+ return lines.join('\n');
344
+ }
345
+ }
346
+ /** Removes a set of offset ranges from a source string, high-to-low. */
347
+ function removeRanges(code, ranges) {
348
+ const sorted = [...ranges].sort((a, b) => b[0] - a[0]);
349
+ let out = code;
350
+ for (const [start, end] of sorted) {
351
+ out = out.slice(0, start) + out.slice(end);
352
+ }
353
+ return out.trim();
212
354
  }
213
355
  /**
214
356
  * Scopes one file's contribution to the shared chunk: module-level
@@ -365,7 +507,7 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
365
507
  function stripExports(code) {
366
508
  return removeCompiledNodes(code, isComponentExport);
367
509
  }
368
- function compileFile(filePath, resolvedName, output) {
510
+ function compileFile(filePath, resolvedName, output, imports) {
369
511
  if (seen.has(filePath))
370
512
  return;
371
513
  seen.add(filePath);
@@ -390,11 +532,17 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
390
532
  if (cached && cacheUsable) {
391
533
  cachedFileHits++;
392
534
  for (const dep of cached.imports)
393
- compileFile(dep, cache?.files.get(dep)?.actualName ?? '', output);
535
+ compileFile(dep, cache?.files.get(dep)?.actualName ?? '', output, imports);
394
536
  // Comp and hyd contributions of one file share its top-level bindings
395
537
  // (e.g. `const links` used by both the component and its hydrator),
396
- // so they must be scoped together in a single block.
397
- const cachedFileCode = [cached.compCode, cached.hydCode].filter(Boolean).join('\n');
538
+ // so they must be scoped together in a single block. Cached codes keep
539
+ // their imports; each side is folded separately because comp+hyd of one
540
+ // file carry the same import twice (joint parse would abort the merge).
541
+ // Mirror the fresh path exactly: strip, rename the hydrator registry,
542
+ // trim — otherwise warm output drifts from cold output.
543
+ const cachedComp = cached.compCode ? imports.add(cached.compCode, filePath).replace(/^\n+/, '').replace(/\n+$/, '') : '';
544
+ const cachedHyd = cached.hydCode ? imports.add(cached.hydCode, filePath).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
545
+ const cachedFileCode = [cachedComp, cachedHyd].filter(Boolean).join('\n');
398
546
  if (cachedFileCode.trim())
399
547
  output.push(scopeFileContribution(cachedFileCode));
400
548
  for (const n of cached.runtimeNames)
@@ -411,13 +559,17 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
411
559
  src = inlineMdContentAttrs(src, dirname(filePath), guessProjectRoots(appDir));
412
560
  }
413
561
  const namesBefore = cache ? new Set(runtimeImportNames) : null;
414
- const importedPaths = resolveVskImports(filePath, (p, n) => compileFile(p, n || '', output));
562
+ const importedPaths = resolveVskImports(filePath, (p, n) => compileFile(p, n, output, imports));
415
563
  // One parse/IR pass feeds both client modes AND the component-name
416
564
  // lookup — the dev hot path pays the acorn+TS parse once per edit
417
565
  // instead of three times.
418
566
  const { comp: rawComp, hyd: rawHyd, name: actualName } = compileClientBoth(src, null, filePath);
419
- const compCode = rawComp ? stripChunkImports(stripExports(stripVskImports(stripRuntimeImport(rawComp)))).replace(/^\n+/, '').replace(/\n+$/, '') : '';
420
- const hydCode = rawHyd ? stripChunkImports(stripExports(stripVskImports(stripRuntimeImport(rawHyd)))).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
567
+ // Cache keeps the import-carrying codes so a warm build can re-fold them
568
+ // into its own fresh accumulator; the emitted codes are import-stripped.
569
+ const compWithImports = rawComp ? stripExports(stripVskImports(stripRuntimeImport(rawComp))) : '';
570
+ const hydWithImports = rawHyd ? stripExports(stripVskImports(stripRuntimeImport(rawHyd))) : '';
571
+ const compCode = compWithImports ? imports.add(compWithImports, filePath).replace(/^\n+/, '').replace(/\n+$/, '') : '';
572
+ const hydCode = hydWithImports ? imports.add(hydWithImports, filePath).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
421
573
  if (rawComp)
422
574
  collectRuntimeImports(rawComp);
423
575
  if (rawHyd)
@@ -444,8 +596,8 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
444
596
  cache.files.set(filePath, {
445
597
  mtimeMs: st.mtimeMs,
446
598
  size: st.size,
447
- compCode,
448
- hydCode,
599
+ compCode: compWithImports,
600
+ hydCode: hydWithImports,
449
601
  actualName,
450
602
  runtimeNames: [...runtimeImportNames].filter((n) => !namesBefore.has(n)),
451
603
  imports: importedPaths,
@@ -479,57 +631,113 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
479
631
  function walkSplit(nodes, _chain) {
480
632
  for (const node of nodes) {
481
633
  const chunkCode = [];
634
+ const chunkImports = new ChunkImports();
482
635
  const pagePath = resolve(appDir, node.sourceDir, 'page.vsk');
483
636
  if (node.page && existsSync(pagePath)) {
484
- compileFile(pagePath, node.page, chunkCode);
637
+ compileFile(pagePath, node.page, chunkCode, chunkImports);
485
638
  }
486
639
  const layoutPath = resolve(appDir, node.sourceDir, 'layout.vsk');
487
640
  if (node.layout && existsSync(layoutPath)) {
488
- compileFile(layoutPath, node.layout, chunkCode);
641
+ compileFile(layoutPath, node.layout, chunkCode, chunkImports);
489
642
  }
490
643
  const errorPath = resolve(appDir, node.sourceDir, 'error.vsk');
491
644
  if (node.error && existsSync(errorPath)) {
492
- compileFile(errorPath, node.error, chunkCode);
645
+ compileFile(errorPath, node.error, chunkCode, chunkImports);
493
646
  }
494
647
  const notFoundPath = resolve(appDir, node.sourceDir, 'not-found.vsk');
495
648
  if (node.notFound && existsSync(notFoundPath)) {
496
- compileFile(notFoundPath, node.notFound, chunkCode);
649
+ compileFile(notFoundPath, node.notFound, chunkCode, chunkImports);
497
650
  }
498
651
  const offlinePath = resolve(appDir, node.sourceDir, 'offline.vsk');
499
652
  if (node.offline && existsSync(offlinePath)) {
500
- compileFile(offlinePath, node.offline, chunkCode);
653
+ compileFile(offlinePath, node.offline, chunkCode, chunkImports);
501
654
  }
502
655
  const networkPath = resolve(appDir, node.sourceDir, 'network.vsk');
503
656
  if (node.network && existsSync(networkPath)) {
504
- compileFile(networkPath, node.network, chunkCode);
657
+ compileFile(networkPath, node.network, chunkCode, chunkImports);
505
658
  }
506
659
  const loadingPath = resolve(appDir, node.sourceDir, 'loading.vsk');
507
660
  if (node.loading && existsSync(loadingPath)) {
508
- compileFile(loadingPath, node.loading, chunkCode);
661
+ compileFile(loadingPath, node.loading, chunkCode, chunkImports);
509
662
  }
510
663
  if (chunkCode.length > 0) {
511
664
  const chunkName = `page-${buildChunkName(node)}.js`;
512
- chunkEntries.push({ name: chunkName, code: chunkCode.join('\n\n'), node });
665
+ // Prepend this chunk's deduped imports (one statement per source, no
666
+ // duplicate bindings) so the classic script exposes the bare
667
+ // identifiers its file bodies reference, and esbuild can parse and
668
+ // inline them.
669
+ if (!chunkImports.ok)
670
+ console.error('[vesk] chunk import merge aborted for', chunkName + ':', chunkImports.reason);
671
+ const head = chunkImports.render();
672
+ const code = (head ? head + '\n\n' : '') + chunkCode.join('\n\n');
673
+ chunkEntries.push({ name: chunkName, code, node });
513
674
  }
514
675
  walkSplit(node.children || [], [..._chain, node]);
515
676
  }
516
677
  }
517
678
  walkSplit(routeTree, []);
518
679
  const sharedCode = [];
680
+ const sharedImports = new ChunkImports();
519
681
  const compMap = componentMap || new Map();
520
682
  for (const [compName, compPath] of compMap) {
521
- compileFile(compPath, compName, sharedCode);
683
+ compileFile(compPath, compName, sharedCode, sharedImports);
522
684
  }
523
685
  if (sharedCode.length > 0) {
524
- chunkEntries.push({ name: 'shared.js', code: sharedCode.join('\n\n'), node: null });
686
+ if (!sharedImports.ok)
687
+ console.error('[vesk] chunk import merge aborted for shared.js:', sharedImports.reason);
688
+ const head = sharedImports.render();
689
+ const code = (head ? head + '\n\n' : '') + sharedCode.join('\n\n');
690
+ chunkEntries.push({ name: 'shared.js', code, node: null });
525
691
  }
692
+ const chunkIIFE = (body) => `(()=>{\nconst __components = globalThis.__components || (globalThis.__components = {});\nconst __hydrators = globalThis.__hydrators || (globalThis.__hydrators = {});\n${body}\n})();\n`;
526
693
  for (const entry of chunkEntries) {
527
- if (entry.code.trim()) {
528
- chunks.push({
529
- name: entry.name,
530
- code: `(()=>{\nconst __components = globalThis.__components || (globalThis.__components = {});\nconst __hydrators = globalThis.__hydrators || (globalThis.__hydrators = {});\n${entry.code}\n})();\n`,
531
- });
694
+ if (!entry.code.trim())
695
+ continue;
696
+ let finalCode;
697
+ if (hasChunkImports(entry.code)) {
698
+ // Bundle the chunk through esbuild so every remaining import (npm
699
+ // packages like lucide-vesk and rewritten relative `.ts` value
700
+ // modules) is inlined into the IIFE — a classic script cannot carry
701
+ // a live `import`, and a stripped binding referenced by a component
702
+ // would otherwise throw `X is not defined` at hydration. Imports were
703
+ // already deduped per file by `ChunkImports`, so esbuild can parse the
704
+ // bundle entry.
705
+ const tmpBase = mkdtempSync(join(resolve(appDir, '..'), 'tmp-vesk-chunk-'));
706
+ const tmpFile = join(tmpBase, 'entry.js');
707
+ const toBundle = `const __components = globalThis.__components || (globalThis.__components = {});\nconst __hydrators = globalThis.__hydrators || (globalThis.__hydrators = {});\n${entry.code}\n`;
708
+ writeFileSync(tmpFile, toBundle);
709
+ try {
710
+ const result = await build({
711
+ entryPoints: [tmpFile],
712
+ bundle: true,
713
+ format: 'iife',
714
+ platform: 'browser',
715
+ write: false,
716
+ logLevel: 'silent',
717
+ loader: { '.js': 'tsx' },
718
+ });
719
+ finalCode = result.outputFiles[0].text;
720
+ }
721
+ catch (e) {
722
+ const err = e;
723
+ console.error('[vesk] chunk bundle failed', entry.name, err?.errors?.map((x) => x.text).join(' | ') || err?.message || String(e));
724
+ // Fall back to the historical behavior: strip every import so the
725
+ // classic script at least parses (referenced values will surface as
726
+ // ReferenceErrors at runtime — the pre-existing failure mode).
727
+ const stripped = demoteExports(removeCompiledNodes(entry.code, isAnyImport));
728
+ finalCode = chunkIIFE(stripped);
729
+ }
730
+ finally {
731
+ try {
732
+ rmSync(tmpBase, { recursive: true, force: true });
733
+ }
734
+ catch { }
735
+ }
736
+ }
737
+ else {
738
+ finalCode = chunkIIFE(entry.code);
532
739
  }
740
+ chunks.push({ name: entry.name, code: finalCode });
533
741
  }
534
742
  function annotate(nodes) {
535
743
  for (const node of nodes) {
package/dist/css.d.ts CHANGED
@@ -1,26 +1,21 @@
1
- export declare const TAILWIND_CSS_URL = "/_vesk/static/_tailwind.css";
2
1
  export declare const GLOBAL_CSS_URL = "/_vesk/static/global.css";
3
2
  /** Resolve the project's user stylesheet source (`src/global.css`, then `src/app.css`). */
4
3
  export declare function resolveUserCssPath(appDir: string): string | null;
5
4
  /** True when the project has a user stylesheet source (`src/global.css` or `src/app.css`). */
6
5
  export declare function hasUserCss(appDir: string): boolean;
7
- /** True when a non-empty `_tailwind.css` was produced for the build (prod disk output). */
8
- export declare function hasBuiltTailwindCss(outDir: string): boolean;
6
+ /** True when a non-empty `global.css` was produced for the build (prod disk output). */
7
+ export declare function hasBuiltGlobalCss(outDir: string): boolean;
9
8
  /** True when the active plugin pipeline includes the tailwind plugin. */
10
9
  export declare function isTailwindPlugin(plugins: Array<{
11
10
  name?: string;
12
11
  }> | undefined | null): boolean;
13
12
  /**
14
13
  * Single source of truth for the `/_vesk/static/*.css` link list. Callers
15
- * supply environment facts — `tailwind`: tailwind output is available
16
- * (built-file present + non-empty in prod, active plugin in dev);
17
- * `userCss`: a user stylesheet exists — so build, request, dev, and prod
18
- * paths share one ordering.
14
+ * supply environment facts — `enabled`: a global stylesheet output is
15
+ * available (built-file present + non-empty in prod, user stylesheet present
16
+ * in dev) — so build, request, dev, and prod paths share one ordering.
19
17
  */
20
18
  export declare function resolveCssUrls(opts: {
21
- tailwind?: boolean;
22
- userCss?: boolean;
19
+ enabled?: boolean;
23
20
  }): string[];
24
- /** Drop tailwind framework directives from user CSS, keeping hand-written rules. */
25
- export declare function stripTailwindDirectives(css: string): string;
26
21
  //# sourceMappingURL=css.d.ts.map
package/dist/css.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,gBAAgB,gCAAgC,CAAC;AAC9D,eAAO,MAAM,cAAc,6BAA6B,CAAC;AAEzD,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMhE;AAED,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAG3D;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAE9F;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,EAAE,CAKxF;AAED,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAkB3D"}
1
+ {"version":3,"file":"css.d.ts","sourceRoot":"","sources":["../src/css.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,cAAc,6BAA6B,CAAC;AAEzD,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMhE;AAED,8FAA8F;AAC9F,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,wFAAwF;AACxF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAE9F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,EAAE,CAGpE"}
package/dist/css.js CHANGED
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * Shared stylesheet resolution for the adapter build, prod server, generated
3
3
  * SSR functions, dev server, and action handler. Each environment feeds its
4
- * facts (`tailwind` output available, user stylesheet present) into
5
- * `resolveCssUrls`, so the `/_vesk/static/*.css` link list and its ordering
6
- * come from one place instead of six ad-hoc copies.
4
+ * facts (`enabled`: a built global stylesheet exists) into `resolveCssUrls`,
5
+ * so the `/_vesk/static/global.css` link list comes from one place instead of
6
+ * several ad-hoc copies.
7
+ *
8
+ * Vesk follows the single-file CSS convention used by other frameworks
9
+ * (Next.js/Remix/SvelteKit): a single `src/global.css` is the stylesheet
10
+ * source. When the Tailwind plugin is active the whole file is compiled into
11
+ * one `/_vesk/static/global.css`; otherwise the user CSS is served as-is from
12
+ * the same URL. There is no separate `_tailwind.css` anymore — user rules and
13
+ * the compiled Tailwind output live in the same file, in one `<link>`.
7
14
  */
8
15
  import { readFileSync, existsSync } from 'node:fs';
9
16
  import { resolve } from 'node:path';
10
- import { cssBlockEnd } from '@vesk/compiler/src/scan';
11
- export const TAILWIND_CSS_URL = '/_vesk/static/_tailwind.css';
12
17
  export const GLOBAL_CSS_URL = '/_vesk/static/global.css';
13
18
  /** Resolve the project's user stylesheet source (`src/global.css`, then `src/app.css`). */
14
19
  export function resolveUserCssPath(appDir) {
@@ -24,9 +29,9 @@ export function resolveUserCssPath(appDir) {
24
29
  export function hasUserCss(appDir) {
25
30
  return resolveUserCssPath(appDir) !== null;
26
31
  }
27
- /** True when a non-empty `_tailwind.css` was produced for the build (prod disk output). */
28
- export function hasBuiltTailwindCss(outDir) {
29
- const p = resolve(outDir, 'static', '_tailwind.css');
32
+ /** True when a non-empty `global.css` was produced for the build (prod disk output). */
33
+ export function hasBuiltGlobalCss(outDir) {
34
+ const p = resolve(outDir, 'static', 'global.css');
30
35
  return existsSync(p) && readFileSync(p, 'utf-8').trim().length > 0;
31
36
  }
32
37
  /** True when the active plugin pipeline includes the tailwind plugin. */
@@ -35,36 +40,12 @@ export function isTailwindPlugin(plugins) {
35
40
  }
36
41
  /**
37
42
  * Single source of truth for the `/_vesk/static/*.css` link list. Callers
38
- * supply environment facts — `tailwind`: tailwind output is available
39
- * (built-file present + non-empty in prod, active plugin in dev);
40
- * `userCss`: a user stylesheet exists — so build, request, dev, and prod
41
- * paths share one ordering.
43
+ * supply environment facts — `enabled`: a global stylesheet output is
44
+ * available (built-file present + non-empty in prod, user stylesheet present
45
+ * in dev) — so build, request, dev, and prod paths share one ordering.
42
46
  */
43
47
  export function resolveCssUrls(opts) {
44
- const urls = [];
45
- if (opts.tailwind)
46
- urls.push(TAILWIND_CSS_URL);
47
- if (opts.userCss)
48
- urls.push(GLOBAL_CSS_URL);
49
- return urls;
50
- }
51
- /** Drop tailwind framework directives from user CSS, keeping hand-written rules. */
52
- export function stripTailwindDirectives(css) {
53
- const blockStart = /^\s*@(theme\s*\{|layer\s+(components|utilities)\s*\{|utility\s+[\w-]+\s*\{)/;
54
- let result = css.replace(/^\s*@import\s+['"]tailwindcss['"]\s*;?\s*$/gm, '');
55
- result = result.replace(/^\s*@source\s+['"][^'"]+['"]\s*;?\s*$/gm, '');
56
- const output = [];
57
- let pos = 0;
58
- while (pos < result.length) {
59
- const lineEnd = result.indexOf('\n', pos) === -1 ? result.length : result.indexOf('\n', pos) + 1;
60
- const line = result.slice(pos, lineEnd);
61
- if (blockStart.test(line.trim())) {
62
- const end = cssBlockEnd(result, pos);
63
- pos = end;
64
- continue;
65
- }
66
- output.push(line);
67
- pos = lineEnd;
68
- }
69
- return output.join('').trim();
48
+ if (opts.enabled)
49
+ return [GLOBAL_CSS_URL];
50
+ return [];
70
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dev-config.d.ts","sourceRoot":"","sources":["../src/dev-config.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAMjE;AAQD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmC/G;AAED,oEAAoE;AACpE,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAM9E;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAQrG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAc5F;AAoID,wJAAwJ;AACxJ,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAcxD;AAyYD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2EtF;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAqC9F"}
1
+ {"version":3,"file":"dev-config.d.ts","sourceRoot":"","sources":["../src/dev-config.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAMjE;AAQD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmC/G;AAED,oEAAoE;AACpE,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAM9E;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAQrG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAc5F;AAgOD,wJAAwJ;AACxJ,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAcxD;AAyYD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2EtF;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAqC9F"}
@@ -208,7 +208,7 @@ function tryParseJsObject(src) {
208
208
  while (i < src.length && /\s/.test(src[i]))
209
209
  i++;
210
210
  if (src[i] === '=')
211
- return null; // `key = value` — not a literal object; bail
211
+ return null;
212
212
  if (src[i] !== ':')
213
213
  return null;
214
214
  i++;
@@ -222,10 +222,62 @@ function tryParseJsObject(src) {
222
222
  }
223
223
  return out;
224
224
  }
225
+ /** Skip a balanced (…) expression starting at `i`, returning the index after the closing ')' (or on failure, `i` unchanged). */
226
+ function skipBalancedParens(src, i) {
227
+ if (src[i] !== '(')
228
+ return i;
229
+ let depth = 0;
230
+ let inStr = null;
231
+ let esc = false;
232
+ for (let j = i; j < src.length; j++) {
233
+ const ch = src[j];
234
+ if (inStr) {
235
+ if (esc) {
236
+ esc = false;
237
+ continue;
238
+ }
239
+ if (ch === '\\') {
240
+ esc = true;
241
+ continue;
242
+ }
243
+ if (ch === inStr)
244
+ inStr = null;
245
+ continue;
246
+ }
247
+ if (ch === '"' || ch === "'" || ch === '`') {
248
+ inStr = ch;
249
+ continue;
250
+ }
251
+ if (ch === '(')
252
+ depth++;
253
+ if (ch === ')') {
254
+ depth--;
255
+ if (depth === 0)
256
+ return j + 1;
257
+ }
258
+ }
259
+ return i; // unbalanced — bail
260
+ }
261
+ /**
262
+ * Return a raw source span (used for function calls, variable refs, spreads
263
+ * that can't be round-tripped as literals). The value is stored as a wrapper
264
+ * object so serializeValue can emit it verbatim.
265
+ */
266
+ function rawSpan(src, from, to) {
267
+ return { value: { __veskRaw: src.slice(from, to) }, next: to };
268
+ }
225
269
  function parseLiteralValue(src, i) {
226
270
  const c = src[i];
227
271
  if (c === undefined)
228
272
  return null;
273
+ // spread: ...expr
274
+ if (c === '.' && src.slice(i, i + 3) === '...') {
275
+ const expr = parseLiteralValue(src, i + 3);
276
+ if (expr === null || expr.__invalid)
277
+ return rawSpan(src, i, skipToEndOfRaw(src, i + 3));
278
+ // preserve spread marker in the serialized output
279
+ return { value: { __veskRaw: '...' + serializeValue(expr.value) }, next: expr.next };
280
+ }
229
281
  if (c === '{') {
230
282
  const end = findMatchingBrace(src, i);
231
283
  if (end === -1)
@@ -251,8 +303,10 @@ function parseLiteralValue(src, i) {
251
303
  continue;
252
304
  }
253
305
  const r = parseLiteralValue(src, j);
254
- if (r === null || r.__invalid)
255
- return { value: undefined, next: i, __invalid: true };
306
+ if (r === null || r.__invalid) {
307
+ // Can't parse this array element — fall back to raw span for the whole array
308
+ return rawSpan(src, i, skipToEndOfArray(src, i));
309
+ }
256
310
  vals.push(r.value);
257
311
  j = r.next;
258
312
  }
@@ -268,6 +322,7 @@ function parseLiteralValue(src, i) {
268
322
  }
269
323
  return { value: s, next: j + 1 };
270
324
  }
325
+ // identifiers, numbers, booleans, null, and tokens followed by '(' (function calls)
271
326
  let j = i;
272
327
  while (j < src.length && /[\w.\-]/.test(src[j]))
273
328
  j++;
@@ -280,8 +335,86 @@ function parseLiteralValue(src, i) {
280
335
  return { value: null, next: j };
281
336
  if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(token))
282
337
  return { value: Number(token), next: j };
338
+ // token is an identifier or a function name — check if it's followed by '(' (call)
339
+ if (j < src.length && src[j] === '(') {
340
+ const end = skipBalancedParens(src, j);
341
+ return rawSpan(src, i, end);
342
+ }
343
+ // bare variable reference (e.g. `testPlugin`, `tailwindcss`)
344
+ if (token.length > 0)
345
+ return rawSpan(src, i, j);
283
346
  return { value: undefined, next: i, __invalid: true };
284
347
  }
348
+ /** Skip forward from `i` to find the end of a raw expression (used for spread fallback). */
349
+ function skipToEndOfRaw(src, i) {
350
+ let depth = 0;
351
+ let inStr = null;
352
+ let esc = false;
353
+ for (let j = i; j < src.length; j++) {
354
+ const ch = src[j];
355
+ if (inStr) {
356
+ if (esc) {
357
+ esc = false;
358
+ continue;
359
+ }
360
+ if (ch === '\\') {
361
+ esc = true;
362
+ continue;
363
+ }
364
+ if (ch === inStr)
365
+ inStr = null;
366
+ continue;
367
+ }
368
+ if (ch === '"' || ch === "'" || ch === '`') {
369
+ inStr = ch;
370
+ continue;
371
+ }
372
+ if (ch === '(')
373
+ depth++;
374
+ if (ch === ')') {
375
+ if (depth === 0)
376
+ return j;
377
+ depth--;
378
+ }
379
+ if (depth === 0 && (ch === ',' || ch === '}' || ch === ']'))
380
+ return j;
381
+ }
382
+ return src.length;
383
+ }
384
+ /** Skip to the matching ']' for a raw array span. */
385
+ function skipToEndOfArray(src, i) {
386
+ let depth = 0;
387
+ let inStr = null;
388
+ let esc = false;
389
+ for (let j = i; j < src.length; j++) {
390
+ const ch = src[j];
391
+ if (inStr) {
392
+ if (esc) {
393
+ esc = false;
394
+ continue;
395
+ }
396
+ if (ch === '\\') {
397
+ esc = true;
398
+ continue;
399
+ }
400
+ if (ch === inStr)
401
+ inStr = null;
402
+ continue;
403
+ }
404
+ if (ch === '"' || ch === "'" || ch === '`') {
405
+ inStr = ch;
406
+ continue;
407
+ }
408
+ if (ch === '[')
409
+ depth++;
410
+ if (ch === ']') {
411
+ depth--;
412
+ if (depth === 0)
413
+ return j + 1;
414
+ }
415
+ }
416
+ return src.length;
417
+ }
285
418
  function serializeObject(obj) {
286
419
  return Object.keys(obj)
287
420
  .map((k) => `${isBareKey(k) ? k : JSON.stringify(k)}: ${serializeValue(obj[k])}`)
@@ -291,6 +424,9 @@ function isBareKey(k) {
291
424
  return /^[a-zA-Z_$][\w$]*$/.test(k);
292
425
  }
293
426
  function serializeValue(v) {
427
+ if (v !== null && typeof v === 'object' && '__veskRaw' in v) {
428
+ return String(v.__veskRaw);
429
+ }
294
430
  if (v === null)
295
431
  return 'null';
296
432
  if (typeof v === 'string')
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../src/dev-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,WAAW,CAAC;AAOjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAI7C,OAAO,KAAK,EAAa,gBAAgB,EAAwB,MAAM,yBAAyB,CAAC;AAgBjG;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,IAAI,WAAW,CAEhD;AAuCD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkBD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC7B;;8BAE0B;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,WAAW,CAAC;IAChC,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG;IACvE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAChH,CAUA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAa3C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIrD;AA8CD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8hBvG"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../src/dev-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAA6C,MAAM,WAAW,CAAC;AAOjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAI7C,OAAO,KAAK,EAAa,gBAAgB,EAAwB,MAAM,yBAAyB,CAAC;AAgBjG;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB;+EAC2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,IAAI,WAAW,CAEhD;AAuCD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AAkBD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,mHAAmH;IACnH,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC7B;;8BAE0B;IAC1B,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,WAAW,CAAC;IAChC,8EAA8E;IAC9E,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG;IACvE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAChH,CAUA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAa3C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIrD;AA8CD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAikBvG"}
@@ -260,7 +260,10 @@ export async function startDevServer(appDir, options) {
260
260
  async function buildAdapterAgenticProvider(cfg) {
261
261
  const providerName = String(cfg.provider || loadAgenticConfig(agenticProjectDir).provider || 'openai');
262
262
  const apiKey = cfg.apiKey || getApiKey(agenticProjectDir, providerName) || '';
263
- const model = cfg.model || loadAgenticConfig(agenticProjectDir).model || (providerName === 'anthropic' ? 'claude-sonnet-4-6' : providerName === 'google' ? 'gemini-2.0-flash' : providerName === 'ollama' ? 'llama3.1' : providerName === 'opencode' ? 'claude-sonnet-4-6' : providerName === 'opencode-go' ? 'opencode-go/kimi-k3' : providerName === 'openrouter' ? 'openrouter/auto' : 'gpt-4o-mini');
263
+ const model = cfg.model || loadAgenticConfig(agenticProjectDir).model || '';
264
+ if (!model) {
265
+ throw new Error('no model configured — open Settings → Agentic and pick a model');
266
+ }
264
267
  const baseUrl = cfg.baseUrl || loadAgenticConfig(agenticProjectDir).baseUrl || undefined;
265
268
  const maxTokens = typeof cfg.maxTokens === 'number' ? cfg.maxTokens : undefined;
266
269
  if (providerName === 'anthropic')
@@ -652,6 +655,41 @@ export async function startDevServer(appDir, options) {
652
655
  });
653
656
  const hmr = createHmrServer(server, appDir, devDir, componentMap);
654
657
  hmrSession = hmr;
658
+ // ── Console streaming to devtool ────────────────────────────────────────
659
+ // Read `logs` from vesk.config and wire up server-side console interception
660
+ // so console.log/warn/error/info/debug are forwarded to the devtool Log tab.
661
+ {
662
+ let logsLevels = null;
663
+ try {
664
+ const { readConfig } = await import('@vesk/adapter/src/dev-config');
665
+ const cfgResult = await readConfig(resolve(appDir, '..'));
666
+ const logsCfg = cfgResult.config?.logs;
667
+ if (logsCfg === false) {
668
+ logsLevels = null; // disabled
669
+ }
670
+ else if (typeof logsCfg === 'object' && logsCfg !== null) {
671
+ logsLevels = { log: true, warn: true, error: true, info: true, debug: true, ...logsCfg };
672
+ }
673
+ else {
674
+ logsLevels = { log: true, warn: true, error: true, info: true, debug: true };
675
+ }
676
+ }
677
+ catch {
678
+ logsLevels = { log: true, warn: true, error: true, info: true, debug: true };
679
+ }
680
+ if (logsLevels) {
681
+ const origFns = {};
682
+ for (const lvl of ['log', 'warn', 'error', 'info', 'debug']) {
683
+ origFns[lvl] = console[lvl].bind(console);
684
+ console[lvl] = (...args) => {
685
+ origFns[lvl](...args);
686
+ if (!logsLevels[lvl])
687
+ return;
688
+ hmr.broadcast('console', { level: lvl, message: args.map(String).join(' ') });
689
+ };
690
+ }
691
+ }
692
+ }
655
693
  const srcDir = resolve(appDir, '..', 'src');
656
694
  try {
657
695
  if (existsSync(srcDir)) {
package/dist/hmr.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"hmr.d.ts","sourceRoot":"","sources":["../src/hmr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAUxC,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEvG,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AAIzE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAaD,wBAAgB,WAAW,IAAI;IAC7B,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAQA;AAED,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,wBAA6B,GAClC,eAAe,CAsEjB;AA0RD,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC;IACD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAClE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvH,WAAW,EAAE,MAAM,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CACnD,CAyJA"}
1
+ {"version":3,"file":"hmr.d.ts","sourceRoot":"","sources":["../src/hmr.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAUxC,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEvG,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,yBAAyB,CAAC;AAIzE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAaD,wBAAgB,WAAW,IAAI;IAC7B,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAQA;AAED,MAAM,WAAW,wBAAwB;IACvC,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,wBAA6B,GAClC,eAAe,CAsEjB;AAoRD,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC;IACD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAClE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvH,WAAW,EAAE,MAAM,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CACnD,CAyJA"}
package/dist/hmr.js CHANGED
@@ -6,7 +6,7 @@ import { compileClient } from '@vesk/compiler/src/client-codegen';
6
6
  import { buildHmrEvalSnippet } from './client-bundle';
7
7
  import { resolveComponentName, randomToken } from '@vesk/compiler/src/server-codegen';
8
8
  import { resolveErrorFile } from '@vesk/adapter/src/ssr-function';
9
- import { resolveCssUrls, hasUserCss, hasBuiltTailwindCss } from '@vesk/adapter/src/css';
9
+ import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
10
10
  import { isAllowedWsUpgrade } from '@vesk/adapter/src/paths';
11
11
  import { parseCompilerError, buildCodeframe } from '@vesk/adapter/src/error-codeframe';
12
12
  import { suggestFor } from '@vesk/adapter/src/error-tips';
@@ -243,10 +243,8 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
243
243
  const ancestorLayouts = options?.ancestorLayouts || [];
244
244
  const headExtra = options?.headExtra || null;
245
245
  const pagePath = resolve(appDir, routeNode.sourceDir, 'page.vsk');
246
- const layoutPath = resolve(appDir, routeNode.sourceDir, 'layout.vsk');
247
246
  const cssUrls = resolveCssUrls({
248
- tailwind: hasBuiltTailwindCss(outDir),
249
- userCss: hasUserCss(appDir),
247
+ enabled: hasBuiltGlobalCss(outDir),
250
248
  });
251
249
  const cssOption = cssUrls.length > 0 ? `, cssUrls: ${JSON.stringify(cssUrls)}` : '';
252
250
  const headExtraOption = headExtra ? `, headExtra: ${JSON.stringify(headExtra)}` : '';
@@ -255,10 +253,25 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
255
253
  const name = routeName(parts);
256
254
  const funcDir = resolve(outDir, 'server', 'functions');
257
255
  const funcPath = resolve(funcDir, `${name}.js`);
258
- const hasLayout = !!routeNode.layout;
259
- const hasAncestorLayout = ancestorLayouts.length > 0;
260
256
  const pageSrc = readFileSync(pagePath, 'utf-8');
261
257
  const pageComp = extractCompName(pageSrc) || 'Page';
258
+ const layoutStack = [
259
+ ...ancestorLayouts.map((a) => ({ sourceDir: a.sourceDir, layoutCompName: a.layoutCompName })),
260
+ ...(routeNode.layout ? [{ sourceDir: routeNode.sourceDir, layoutCompName: routeNode.layout }] : []),
261
+ ];
262
+ const layoutDecls = [];
263
+ for (const entry of layoutStack) {
264
+ const entryPath = resolve(appDir, entry.sourceDir, 'layout.vsk');
265
+ const entrySrc = readFileSync(entryPath, 'utf-8');
266
+ const entryComp = extractCompName(entrySrc) || 'Layout';
267
+ const escaped = escapeSource(entrySrc);
268
+ layoutDecls.push({
269
+ src: `\`${escaped}\``,
270
+ comp: JSON.stringify(entryComp),
271
+ path: JSON.stringify(entryPath),
272
+ compiled: `(() => { try { setVskHydrate(true); return compileFile(\`${escaped}\`, { sourcePath: ${JSON.stringify(entryPath)} }); } catch { return null; } finally { setVskHydrate(false); } })()`,
273
+ });
274
+ }
262
275
  const errorPath = resolveErrorFile(routeNode.sourceDir, appDir);
263
276
  const errorSrc = errorPath ? readFileSync(errorPath, 'utf-8') : null;
264
277
  const errorComp = errorPath ? (extractCompName(errorSrc) || 'Error') : null;
@@ -268,28 +281,15 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
268
281
  const clientScriptOption = ', clientScriptUrl: "/_vesk/static/client.js"';
269
282
  const dataScriptOption = ', externalDataScript: storeDataScriptGlobal';
270
283
  let src = '';
271
- if (hasLayout) {
272
- const layoutSrc = readFileSync(layoutPath, 'utf-8');
273
- const layoutComp = extractCompName(layoutSrc) || 'Layout';
274
- src = `const _layoutSrc = \`${escapeSource(layoutSrc)}\`;\nconst _pageSrc = \`${escapeSource(pageSrc)}\`;\n`;
275
- src += `const _layoutComp = ${JSON.stringify(layoutComp)};\nconst _pageComp = ${JSON.stringify(pageComp)};\n`;
276
- src += `const _layoutPath = ${JSON.stringify(layoutPath)};\nconst _pagePath = ${JSON.stringify(pagePath)};\n`;
277
- src += `const _layoutCompiled = (() => { try { setVskHydrate(true); return compileFile(_layoutSrc, { sourcePath: _layoutPath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
278
- src += `const _pageCompiled = (() => { try { setVskHydrate(true); return compileFile(_pageSrc, { sourcePath: _pagePath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
279
- src += errorVars;
280
- }
281
- else if (hasAncestorLayout) {
282
- const outerLayout = ancestorLayouts[0];
283
- const outerLayoutPath = resolve(appDir, outerLayout.sourceDir, 'layout.vsk');
284
- const outerLayoutSrc = readFileSync(outerLayoutPath, 'utf-8');
285
- const outerLayoutComp = extractCompName(outerLayoutSrc) || 'Layout';
284
+ if (layoutDecls.length > 0) {
286
285
  src = `const _pageSrc = \`${escapeSource(pageSrc)}\`;\n`;
287
286
  src += `const _pageComp = ${JSON.stringify(pageComp)};\n`;
288
- src += `const _layoutSrc = \`${escapeSource(outerLayoutSrc)}\`;\n`;
289
- src += `const _layoutComp = ${JSON.stringify(outerLayoutComp)};\n`;
290
- src += `const _layoutPath = ${JSON.stringify(outerLayoutPath)};\nconst _pagePath = ${JSON.stringify(pagePath)};\n`;
291
- src += `const _layoutCompiled = (() => { try { setVskHydrate(true); return compileFile(_layoutSrc, { sourcePath: _layoutPath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
287
+ src += `const _pagePath = ${JSON.stringify(pagePath)};\n`;
292
288
  src += `const _pageCompiled = (() => { try { setVskHydrate(true); return compileFile(_pageSrc, { sourcePath: _pagePath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
289
+ src += `const _layoutSrcList = [\n${layoutDecls.map((d) => ' ' + d.src + ',').join('\n')}\n];\n`;
290
+ src += `const _layoutCompList = [${layoutDecls.map((d) => d.comp).join(', ')}];\n`;
291
+ src += `const _layoutPathList = [${layoutDecls.map((d) => d.path).join(', ')}];\n`;
292
+ src += `const _layoutCompiledList = [\n${layoutDecls.map((d) => ' ' + d.compiled + ',').join('\n')}\n];\n`;
293
293
  src += errorVars;
294
294
  }
295
295
  else {
@@ -316,7 +316,7 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
316
316
  registryCode = 'const __componentRegistry = new Map();\n';
317
317
  }
318
318
  let renderCode;
319
- if (hasLayout) {
319
+ if (layoutDecls.length > 0) {
320
320
  renderCode = [
321
321
  ' let page;',
322
322
  ' let caughtError = null;',
@@ -329,24 +329,14 @@ function regenerateSsrFunction(routeNode, appDir, outDir, componentMap, options)
329
329
  " const stack = err && typeof err === 'object' && 'stack' in err ? String(err.stack) : '';",
330
330
  " page = { body: await __renderErrorBody({ params, statusCode: 500, error: message, stack, url: url.href }), head: '' };",
331
331
  ' }',
332
- ' const html = await renderFullPage(_layoutSrc, _layoutComp, { params, children: (caughtError ? \'<!--vesk-ssr-error:\' + (caughtError && typeof caughtError === \'object\' && \'message\' in caughtError ? encodeURIComponent(String(caughtError.message)) : \'\') + \'-->\' : \'\') + page.body }, __componentRegistry, { hydrate: true, cached: _layoutCompiled' + bakedOptions + clientScriptOption + dataScriptOption + ', pageHead: page.head, sourcePath: _layoutPath });',
333
- " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
334
- ].join('\n');
335
- }
336
- else if (hasAncestorLayout) {
337
- renderCode = [
338
- ' let page;',
339
- ' let caughtError = null;',
340
- ' try {',
341
- ' page = await renderPage(_pageSrc, _pageComp, { params }, __componentRegistry, { hydrate: true, cached: _pageCompiled, sourcePath: _pagePath });',
342
- ' } catch (err) {',
343
- " if (err && (err.name === 'NotFoundError' || err.name === 'Redirect')) throw err;",
344
- ' caughtError = err;',
345
- " const message = err && typeof err === 'object' && 'message' in err ? String(err.message) : String(err);",
346
- " const stack = err && typeof err === 'object' && 'stack' in err ? String(err.stack) : '';",
347
- " page = { body: await __renderErrorBody({ params, statusCode: 500, error: message, stack, url: url.href }), head: '' };",
332
+ " let _body = (caughtError ? '<!--vesk-ssr-error:' + (caughtError && typeof caughtError === 'object' && 'message' in caughtError ? encodeURIComponent(String(caughtError.message)) : '') + '-->' : '') + page.body;",
333
+ " let _head = page.head || '';",
334
+ " for (let _i = _layoutSrcList.length - 1; _i > 0; _i--) {",
335
+ " const _inner = await renderPage(_layoutSrcList[_i], _layoutCompList[_i], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[_i], sourcePath: _layoutPathList[_i] });",
336
+ ' _body = _inner.body;',
337
+ " if (_inner.head) _head = _inner.head + _head;",
348
338
  ' }',
349
- ' const html = await renderFullPage(_layoutSrc, _layoutComp, { params, children: (caughtError ? \'<!--vesk-ssr-error:\' + (caughtError && typeof caughtError === \'object\' && \'message\' in caughtError ? encodeURIComponent(String(caughtError.message)) : \'\') + \'-->\' : \'\') + page.body }, __componentRegistry, { hydrate: true, cached: _layoutCompiled' + bakedOptions + clientScriptOption + dataScriptOption + ', pageHead: page.head, sourcePath: _layoutPath });',
339
+ " const html = await renderFullPage(_layoutSrcList[0], _layoutCompList[0], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[0]" + bakedOptions + clientScriptOption + dataScriptOption + ', pageHead: _head, sourcePath: _layoutPathList[0] });',
350
340
  " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
351
341
  ].join('\n');
352
342
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACe,YAAY,EAAE,WAAW,EAC7B,UAAU,EAChC,MAAM,yBAAyB,CAAC;AAiBjC;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AACD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9C,UAAU,EAAE,CAQd;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,UAAU,EAAE,EACrB,KAAK,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GACxC,UAAU,EAAE,CAGd;AA+BD,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAyUpG;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAIhE,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,GAChB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACe,YAAY,EAAE,WAAW,EAC7B,UAAU,EAChC,MAAM,yBAAyB,CAAC;AAiBjC;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AACD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,EAAE,iBAAiB,EAAE,GAAG,IAAI,GAAG,SAAS,GAC9C,UAAU,EAAE,CAQd;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,UAAU,EAAE,EACrB,KAAK,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,GACxC,UAAU,EAAE,CAGd;AA+BD,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAuTpG;AAED,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAIhE,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,eAAe,GAChB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,aAAa,GACd,MAAM,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { resolve, dirname, relative, basename } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { bundleRuntime } from '@vesk/adapter/src/runtime-bundle';
5
5
  import { generateSsrFunction } from '@vesk/adapter/src/ssr-function';
6
- import { resolveUserCssPath, stripTailwindDirectives, isTailwindPlugin, resolveCssUrls, hasUserCss, hasBuiltTailwindCss } from '@vesk/adapter/src/css';
6
+ import { resolveUserCssPath, isTailwindPlugin, resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
7
7
  import { collectActionIds } from '@vesk/compiler/src/actions';
8
8
  import { generateApiFunction } from '@vesk/adapter/src/api-function';
9
9
  import { compileMiddleware, compileMiddlewareCode } from '@vesk/adapter/src/middleware';
@@ -153,43 +153,25 @@ export async function build(appDir, options) {
153
153
  const cssSourcePath = resolveUserCssPath(appDir);
154
154
  if (cssSourcePath !== null) {
155
155
  const cssContent = readFileSync(cssSourcePath, 'utf-8');
156
- const userCss = stripTailwindDirectives(cssContent);
157
- const userCssTarget = resolve(outDir, 'static', 'global.css');
158
- writeFileSync(userCssTarget, userCss, 'utf-8');
159
- console.error(`vesk build: css → static/global.css (${userCss.length} bytes)`);
160
- const twCssTarget = resolve(outDir, 'static', '_tailwind.css');
156
+ const cssTarget = resolve(outDir, 'static', 'global.css');
161
157
  const tailwindActive = isTailwindPlugin(pluginsPipelines);
162
- if (!tailwindActive) {
163
- writeFileSync(twCssTarget, '', 'utf-8');
164
- console.error('vesk build: css → static/_tailwind.css (empty, tailwind plugin inactive)');
165
- }
166
- else {
167
- let twCss = cssContent;
158
+ let finalCss = cssContent;
159
+ if (tailwindActive) {
168
160
  for (const plugin of pluginsPipelines) {
169
161
  if (typeof plugin.onCSS === 'function') {
170
- const result = await plugin.onCSS(twCss, cssSourcePath);
162
+ const result = await plugin.onCSS(finalCss, cssSourcePath);
171
163
  if (result !== null && typeof result === 'string') {
172
- twCss = result;
164
+ finalCss = result;
173
165
  }
174
166
  }
175
167
  }
176
- const hasUnresolvedTailwindImport = /@import\s+['"]tailwindcss['"]/.test(twCss);
177
- if (hasUnresolvedTailwindImport) {
178
- const lines = twCss.split('\n').filter(l => !/^\s*@import\s+['"]tailwindcss['"]/.test(l));
179
- twCss = lines.join('\n').trim();
180
- if (twCss.length === 0) {
181
- writeFileSync(twCssTarget, '', 'utf-8');
182
- console.error('vesk build: css → static/_tailwind.css (empty, tailwind unresolved)');
183
- }
184
- else {
185
- writeFileSync(twCssTarget, twCss, 'utf-8');
186
- console.error(`vesk build: css → static/_tailwind.css (${twCss.length} bytes, tailwind partially unresolved)`);
187
- }
188
- }
189
- else {
190
- writeFileSync(twCssTarget, twCss, 'utf-8');
191
- console.error(`vesk build: css → static/_tailwind.css (${twCss.length} bytes)`);
192
- }
168
+ }
169
+ writeFileSync(cssTarget, finalCss, 'utf-8');
170
+ if (tailwindActive) {
171
+ console.error(`vesk build: css → static/global.css (${finalCss.length} bytes)`);
172
+ }
173
+ else {
174
+ console.error(`vesk build: css → static/global.css (${finalCss.length} bytes, tailwind plugin inactive)`);
193
175
  }
194
176
  }
195
177
  const { scanRoutes, scanComponents } = await resolveCompilerApi('router.js');
@@ -311,8 +293,7 @@ export async function build(appDir, options) {
311
293
  if (options?.ssg) {
312
294
  const { generateSsgRoutes } = await import('./static.js');
313
295
  const cssUrls = resolveCssUrls({
314
- tailwind: hasBuiltTailwindCss(outDir),
315
- userCss: hasUserCss(appDir),
296
+ enabled: hasBuiltGlobalCss(outDir),
316
297
  });
317
298
  prerenderedRoutes = await generateSsgRoutes(routeTree, appDir, outDir, pluginsPipelines, pluginHeadExtra, cssUrls);
318
299
  console.error(`vesk build: ssg → prerendered/ (${prerenderedRoutes.length} pages)`);
@@ -1 +1 @@
1
- {"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AA+CjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AA+FD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmcxI"}
1
+ {"version":3,"file":"prod-server.d.ts","sourceRoot":"","sources":["../src/prod-server.ts"],"names":[],"mappings":"AAEA,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AA+CjG,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIzE;AA+FD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkcxI"}
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { createRateLimiter, resolveComponentName, safeJsonForScript, getClientProtocol, DEFAULT_MAX_BODY_BYTES } from '@vesk/compiler/src/server-codegen';
7
7
  import { securityHeaders } from '@vesk/compiler/src/server-utils';
8
8
  import { resolveWithin, installMdReadHook } from '@vesk/adapter/src/paths';
9
- import { resolveCssUrls, hasUserCss, hasBuiltTailwindCss } from '@vesk/adapter/src/css';
9
+ import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
10
10
  const _require = createRequire(import.meta.url);
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  async function readBody(req, maxBytes = DEFAULT_MAX_BODY_BYTES) {
@@ -436,8 +436,7 @@ export async function startProdServer(outDir, options) {
436
436
  }
437
437
  const appDir = resolve(projectDir, 'app');
438
438
  const pageCssUrls = resolveCssUrls({
439
- tailwind: hasBuiltTailwindCss(outDir),
440
- userCss: hasUserCss(appDir),
439
+ enabled: hasBuiltGlobalCss(outDir),
441
440
  });
442
441
  const nfPath = resolve(appDir, 'not-found.vsk');
443
442
  let notFoundHtml = null;
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-function.d.ts","sourceRoot":"","sources":["../src/ssr-function.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAkB,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAW7F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQjF;AAiDD,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,kBAAkB,GAC3B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAsXtD"}
1
+ {"version":3,"file":"ssr-function.d.ts","sourceRoot":"","sources":["../src/ssr-function.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAkB,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAW7F,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQjF;AAiDD,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,kBAAkB,GAC3B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAoYtD"}
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, existsSync } from 'node:fs';
2
2
  import { resolve, relative, join } from 'node:path';
3
3
  import { resolveComponentName } from '@vesk/compiler/src/server-codegen';
4
- import { resolveCssUrls, hasUserCss, hasBuiltTailwindCss } from '@vesk/adapter/src/css';
4
+ import { resolveCssUrls, hasBuiltGlobalCss } from '@vesk/adapter/src/css';
5
5
  function escapeSource(src) {
6
6
  return src
7
7
  .replace(/\\/g, '\\\\')
@@ -75,22 +75,35 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
75
75
  const middlewareCode = options?.middlewareCode || null;
76
76
  const headExtra = options?.headExtra || null;
77
77
  const pagePath = resolve(appDir, routeNode.sourceDir, 'page.vsk');
78
- const layoutPath = resolve(appDir, routeNode.sourceDir, 'layout.vsk');
79
78
  const parts = routeNode.fullPath.split('/').filter(Boolean);
80
79
  const name = routeName(parts);
81
80
  const funcDir = resolve(outDir, 'server', 'functions');
82
81
  const funcPath = resolve(funcDir, `${name}.js`);
83
82
  const cssUrls = resolveCssUrls({
84
- tailwind: hasBuiltTailwindCss(outDir),
85
- userCss: hasUserCss(appDir),
83
+ enabled: hasBuiltGlobalCss(outDir),
86
84
  });
87
85
  const cssOption = cssUrls.length > 0 ? `, cssUrls: ${JSON.stringify(cssUrls)}` : '';
88
86
  // Static head snippet baked from the active onHead plugins — merged into
89
87
  // every rendered head at request time (page tags win via mergeHeadHtml).
90
88
  const headExtraOption = headExtra ? `, headExtra: ${JSON.stringify(headExtra)}` : '';
91
89
  const bakedOptions = cssOption + headExtraOption;
92
- const hasLayout = !!routeNode.layout;
93
- const hasAncestorLayout = ancestorLayouts.length > 0;
90
+ const layoutStack = [
91
+ ...ancestorLayouts.map((a) => ({ sourceDir: a.sourceDir, layoutCompName: a.layoutCompName })),
92
+ ...(routeNode.layout ? [{ sourceDir: routeNode.sourceDir, layoutCompName: routeNode.layout }] : []),
93
+ ];
94
+ const layoutDecls = [];
95
+ for (const entry of layoutStack) {
96
+ const entryPath = resolve(appDir, entry.sourceDir, 'layout.vsk');
97
+ const entrySrc = readFileSync(entryPath, 'utf-8');
98
+ const entryComp = extractCompName(entrySrc) || 'Layout';
99
+ const escaped = escapeSource(entrySrc);
100
+ layoutDecls.push({
101
+ src: `\`${escaped}\``,
102
+ comp: JSON.stringify(entryComp),
103
+ path: JSON.stringify(entryPath),
104
+ compiled: `(() => { try { setVskHydrate(true); return compileFile(\`${escaped}\`, { sourcePath: ${JSON.stringify(entryPath)} }); } catch { return null; } finally { setVskHydrate(false); } })()`,
105
+ });
106
+ }
94
107
  const pageSrc = readFileSync(pagePath, 'utf-8');
95
108
  const pageComp = extractCompName(pageSrc) || 'Page';
96
109
  const errorPath = resolveErrorFile(routeNode.sourceDir, appDir);
@@ -100,28 +113,15 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
100
113
  ? `const _errorSrc = \`${escapeSource(errorSrc)}\`;\nconst _errorComp = ${JSON.stringify(errorComp)};\nconst _errorPath = ${JSON.stringify(errorPath)};\nconst _errorCompiled = (() => { try { setVskHydrate(true); return compileFile(_errorSrc, { sourcePath: _errorPath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`
101
114
  : 'const _errorSrc = null;\nconst _errorComp = null;\nconst _errorPath = null;\nconst _errorCompiled = null;\n';
102
115
  let src = '';
103
- if (hasLayout) {
104
- const layoutSrc = readFileSync(layoutPath, 'utf-8');
105
- const layoutComp = extractCompName(layoutSrc) || 'Layout';
106
- src = `const _layoutSrc = \`${escapeSource(layoutSrc)}\`;\nconst _pageSrc = \`${escapeSource(pageSrc)}\`;\n`;
107
- src += `const _layoutComp = ${JSON.stringify(layoutComp)};\nconst _pageComp = ${JSON.stringify(pageComp)};\n`;
108
- src += `const _layoutPath = ${JSON.stringify(layoutPath)};\nconst _pagePath = ${JSON.stringify(pagePath)};\n`;
109
- src += `const _layoutCompiled = (() => { try { setVskHydrate(true); return compileFile(_layoutSrc, { sourcePath: _layoutPath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
110
- src += `const _pageCompiled = (() => { try { setVskHydrate(true); return compileFile(_pageSrc, { sourcePath: _pagePath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
111
- src += errorVars;
112
- }
113
- else if (hasAncestorLayout) {
114
- const outerLayout = ancestorLayouts[0];
115
- const outerLayoutPath = resolve(appDir, outerLayout.sourceDir, 'layout.vsk');
116
- const outerLayoutSrc = readFileSync(outerLayoutPath, 'utf-8');
117
- const outerLayoutComp = extractCompName(outerLayoutSrc) || 'Layout';
116
+ if (layoutDecls.length > 0) {
118
117
  src = `const _pageSrc = \`${escapeSource(pageSrc)}\`;\n`;
119
118
  src += `const _pageComp = ${JSON.stringify(pageComp)};\n`;
120
- src += `const _layoutSrc = \`${escapeSource(outerLayoutSrc)}\`;\n`;
121
- src += `const _layoutComp = ${JSON.stringify(outerLayoutComp)};\n`;
122
- src += `const _layoutPath = ${JSON.stringify(outerLayoutPath)};\nconst _pagePath = ${JSON.stringify(pagePath)};\n`;
123
- src += `const _layoutCompiled = (() => { try { setVskHydrate(true); return compileFile(_layoutSrc, { sourcePath: _layoutPath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
119
+ src += `const _pagePath = ${JSON.stringify(pagePath)};\n`;
124
120
  src += `const _pageCompiled = (() => { try { setVskHydrate(true); return compileFile(_pageSrc, { sourcePath: _pagePath }); } catch { return null; } finally { setVskHydrate(false); } })();\n`;
121
+ src += `const _layoutSrcList = [\n${layoutDecls.map((d) => ' ' + d.src + ',').join('\n')}\n];\n`;
122
+ src += `const _layoutCompList = [${layoutDecls.map((d) => d.comp).join(', ')}];\n`;
123
+ src += `const _layoutPathList = [${layoutDecls.map((d) => d.path).join(', ')}];\n`;
124
+ src += `const _layoutCompiledList = [\n${layoutDecls.map((d) => ' ' + d.compiled + ',').join('\n')}\n];\n`;
125
125
  src += errorVars;
126
126
  }
127
127
  else {
@@ -150,7 +150,7 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
150
150
  registryCode = 'const __componentRegistry = new Map();\n';
151
151
  }
152
152
  let htmlFnCode;
153
- if (hasLayout || hasAncestorLayout) {
153
+ if (layoutDecls.length > 0) {
154
154
  htmlFnCode = [
155
155
  'async function __renderErrorBody(props) {',
156
156
  ' if (!_errorSrc) throw props.error || new Error("Internal Server Error");',
@@ -176,7 +176,14 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
176
176
  " const stack = err && typeof err === 'object' && 'stack' in err ? String(err.stack) : '';",
177
177
  " page = { body: await __renderErrorBody({ params, statusCode: 500, error: __expose ? message : 'Internal Server Error', stack: __expose ? stack : '', url: requestUrl || '' }), head: '' };",
178
178
  ' }',
179
- " const html = await renderFullPage(_layoutSrc, _layoutComp, { params, children: (caughtError ? '<!--vesk-ssr-error:' + (caughtError && typeof caughtError === 'object' && 'message' in caughtError ? encodeURIComponent(__expose ? String(caughtError.message) : 'Internal Server Error') : '') + '-->' : '') + page.body }, __componentRegistry, { hydrate: true, cached: _layoutCompiled" + bakedOptions + clientScriptOption + dataScriptOption + ', pageHead: page.head, sourcePath: _layoutPath });',
179
+ ' let _body = (caughtError ? \'<!--vesk-ssr-error:\' + (caughtError && typeof caughtError === \'object\' && \'message\' in caughtError ? encodeURIComponent(__expose ? String(caughtError.message) : \'Internal Server Error\') : \'\') + \'-->\' : \'\') + page.body;',
180
+ ' let _head = page.head || \'\';',
181
+ ' for (let _i = _layoutSrcList.length - 1; _i > 0; _i--) {',
182
+ ' const _inner = await renderPage(_layoutSrcList[_i], _layoutCompList[_i], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[_i], sourcePath: _layoutPathList[_i] });',
183
+ ' _body = _inner.body;',
184
+ ' if (_inner.head) _head = _inner.head + _head;',
185
+ ' }',
186
+ ' const html = await renderFullPage(_layoutSrcList[0], _layoutCompList[0], { params, children: _body }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[0]' + bakedOptions + clientScriptOption + dataScriptOption + ', pageHead: _head, sourcePath: _layoutPathList[0] });',
180
187
  " return new Response(html, { headers: { 'Content-Type': 'text/html' }, status: caughtError ? 500 : 200 });",
181
188
  ' });',
182
189
  '}',
@@ -228,7 +235,7 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
228
235
  }
229
236
  let dataCode;
230
237
  const exposeErr = "process.env.NODE_ENV !== 'production'";
231
- if (hasLayout || hasAncestorLayout) {
238
+ if (layoutDecls.length > 0) {
232
239
  dataCode = [
233
240
  " if (request.headers.get('x-vesk-data') === '1') {",
234
241
  ' let dataPage;',
@@ -239,8 +246,12 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
239
246
  ' const message = err && typeof err === \'object\' && \'message\' in err ? String(err.message) : String(err);',
240
247
  ` return new Response(JSON.stringify({ error: ${exposeErr} ? message : 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' } });`,
241
248
  ' }',
242
- ' const dataLayout = await renderPage(_layoutSrc, _layoutComp, { params, children: \'\' }, __componentRegistry, { hydrate: true, cached: _layoutCompiled, sourcePath: _layoutPath });',
243
- " return new Response(JSON.stringify({ path: url.pathname, params, props: dataPage.props || { params }, head: (dataLayout.head || '') + (dataPage.head || '') }), {",
249
+ ' let _dataHead = dataPage.head || \'\';',
250
+ ' for (let _i = _layoutSrcList.length - 1; _i >= 0; _i--) {',
251
+ ' const _dl = await renderPage(_layoutSrcList[_i], _layoutCompList[_i], { params, children: \'\' }, __componentRegistry, { hydrate: true, cached: _layoutCompiledList[_i], sourcePath: _layoutPathList[_i] });',
252
+ ' _dataHead = (_dl.head || \'\') + _dataHead;',
253
+ ' }',
254
+ " return new Response(JSON.stringify({ path: url.pathname, params, props: dataPage.props || { params }, head: _dataHead }), {",
244
255
  " headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', Vary: 'x-vesk-data' },",
245
256
  ' });',
246
257
  ' }',
@@ -308,12 +319,12 @@ export function generateSsrFunction(routeNode, appDir, outDir, componentMap, opt
308
319
  ].join('\n');
309
320
  }
310
321
  let registerActionsCode;
311
- if (hasLayout || hasAncestorLayout) {
322
+ if (layoutDecls.length > 0) {
312
323
  registerActionsCode = [
313
324
  'async function __registerActions() {',
314
325
  ' if (__actionsRegistered) return;',
315
326
  ' __actionsRegistered = true;',
316
- ' compileFile(_layoutSrc, { sourcePath: _layoutPath });',
327
+ ' for (let _i = 0; _i < _layoutSrcList.length; _i++) compileFile(_layoutSrcList[_i], { sourcePath: _layoutPathList[_i] });',
317
328
  ' compileFile(_pageSrc, { sourcePath: _pagePath });',
318
329
  '}',
319
330
  '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/adapter",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "type": "module",
5
5
  "description": "Vesk adapter — builds deployable .vesk/ output for Deno and Node serverless",
6
6
  "main": "./dist/index.js",
@@ -23,9 +23,9 @@
23
23
  "sharp": "^0.35.3"
24
24
  },
25
25
  "dependencies": {
26
- "@vesk/compiler": "^0.2.16",
27
- "@vesk/runtime": "^0.2.16",
28
- "@vesk/types": "^0.2.16"
26
+ "@vesk/compiler": "^0.2.18",
27
+ "@vesk/runtime": "^0.2.18",
28
+ "@vesk/types": "^0.2.18"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/ws": "^8.18.1",