@vesk/vesk-cli 0.2.50-canary.4 → 0.2.50-canary.5

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 (2) hide show
  1. package/dist/cli.js +244 -9
  2. package/package.json +5 -5
package/dist/cli.js CHANGED
@@ -399,6 +399,13 @@ var init_ir = __esm({
399
399
  importedNames;
400
400
  staticProps;
401
401
  loadFn;
402
+ /**
403
+ * Source of `export const metadata = defineMetadata({...})`, when present.
404
+ * Static by construction: evaluated with only `defineMetadata` in scope, at
405
+ * build time where possible and in a sealed sandbox at request time
406
+ * otherwise, so the head never depends on the component's frame.
407
+ */
408
+ metadataSource;
402
409
  topLevelCode;
403
410
  /**
404
411
  * Same-file specifier exports (`export { A }`, `export { A as B }`) as
@@ -1966,8 +1973,16 @@ function writeCache(key, data2) {
1966
1973
  const entry = getClientCache().get(key);
1967
1974
  if (!entry || entry.data !== data2) getClientCache().set(key, { data: data2, fetchedAt: Date.now() });
1968
1975
  }
1969
- function trackSsrPromise(promise) {
1976
+ function trackSsrPromise(promise, defer = false) {
1970
1977
  const tk = g().__vsk_ssr_token || "";
1978
+ if (defer) {
1979
+ const deferredKey = tk ? `__vsk_ssr_deferred_${tk}` : "__vsk_ssr_deferred";
1980
+ if (!g()[deferredKey]) g()[deferredKey] = [];
1981
+ g()[deferredKey].push(promise);
1982
+ promise.then(void 0, () => {
1983
+ });
1984
+ return;
1985
+ }
1971
1986
  const promisesKey = tk ? `__vsk_ssr_promises_${tk}` : "__vsk_ssr_promises";
1972
1987
  if (!g()[promisesKey]) g()[promisesKey] = [];
1973
1988
  g()[promisesKey].push(promise);
@@ -2034,7 +2049,7 @@ function startRequest(handle2, skipCache) {
2034
2049
  } else {
2035
2050
  attachSettle(handle2, prom2);
2036
2051
  }
2037
- trackSsrPromise(prom2);
2052
+ trackSsrPromise(prom2, options.ssr === "defer");
2038
2053
  return prom2;
2039
2054
  }
2040
2055
  if (!skipCache && options.staleTime && options.staleTime > 0) {
@@ -21292,6 +21307,7 @@ function generateIR(ast, source, filename) {
21292
21307
  const importedNames = /* @__PURE__ */ new Set();
21293
21308
  let staticProps = null;
21294
21309
  let loadFn = null;
21310
+ let metadataSource = null;
21295
21311
  const topLevelCode = [];
21296
21312
  const exportAliases = [];
21297
21313
  const reexportSources = [];
@@ -21327,6 +21343,14 @@ function generateIR(ast, source, filename) {
21327
21343
  continue;
21328
21344
  }
21329
21345
  }
21346
+ if (node.type === "ExportNamedDeclaration" && node.declaration && !metadataSource) {
21347
+ const decl = node.declaration;
21348
+ const first = decl.type === "VariableDeclaration" ? decl.declarations[0] : null;
21349
+ if (first?.id?.name === "metadata") {
21350
+ metadataSource = first.init ? stripCodeTypes(getSource(source, first.init)) : null;
21351
+ continue;
21352
+ }
21353
+ }
21330
21354
  if (node.type === "ExportNamedDeclaration" && node.declaration && !loadFn) {
21331
21355
  const decl = node.declaration;
21332
21356
  const fnName = decl.type === "FunctionDeclaration" ? decl.id?.name : decl.type === "VariableDeclaration" ? decl.declarations[0]?.id?.name : null;
@@ -21543,7 +21567,9 @@ function generateIR(ast, source, filename) {
21543
21567
  imports.push(`import { ${missing.join(", ")} } from '@vesk/runtime';`);
21544
21568
  }
21545
21569
  }
21546
- return new IRRoot(components, imports, importedNames, staticProps, loadFn, topLevelCode, exportAliases, reexportSources);
21570
+ const irRoot = new IRRoot(components, imports, importedNames, staticProps, loadFn, topLevelCode, exportAliases, reexportSources);
21571
+ irRoot.metadataSource = metadataSource;
21572
+ return irRoot;
21547
21573
  } finally {
21548
21574
  currentVskNamespaceLocals = prevNamespaceLocals;
21549
21575
  currentSourceFile = prevSourceFile;
@@ -26614,6 +26640,177 @@ var init_action = __esm({
26614
26640
  }
26615
26641
  });
26616
26642
 
26643
+ // ../runtime/src/metadata.ts
26644
+ function assertPlainObject(value, where) {
26645
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
26646
+ throw new Error(`defineMetadata: ${where} must be a plain object, got ${Array.isArray(value) ? "an array" : typeof value}`);
26647
+ }
26648
+ return value;
26649
+ }
26650
+ function defineMetadata(input) {
26651
+ const meta = assertPlainObject(input, "the argument");
26652
+ if ("title" in meta && meta.title !== void 0) {
26653
+ if (typeof meta.title !== "string" && typeof meta.title !== "object") {
26654
+ throw new Error("defineMetadata: `title` must be a string or { default, template }");
26655
+ }
26656
+ if (typeof meta.title === "object") {
26657
+ const t = assertPlainObject(meta.title, "`title`");
26658
+ for (const key of Object.keys(t)) {
26659
+ if (key !== "default" && key !== "template" && key !== "absolute") {
26660
+ throw new Error(`defineMetadata: unknown \`title.${key}\` (expected default, template or absolute)`);
26661
+ }
26662
+ }
26663
+ }
26664
+ }
26665
+ if ("description" in meta && meta.description !== void 0 && typeof meta.description !== "string") {
26666
+ throw new Error("defineMetadata: `description` must be a string");
26667
+ }
26668
+ for (const key of ["openGraph", "twitter"]) {
26669
+ if (key in meta && meta[key] !== void 0) assertPlainObject(meta[key], `\`${key}\``);
26670
+ }
26671
+ if ("alternates" in meta && meta.alternates !== void 0) {
26672
+ const alt = assertPlainObject(meta.alternates, "`alternates`");
26673
+ for (const key of Object.keys(alt)) {
26674
+ if (key !== "canonical" && key !== "languages") {
26675
+ throw new Error(`defineMetadata: unknown \`alternates.${key}\` (expected canonical or languages)`);
26676
+ }
26677
+ }
26678
+ }
26679
+ for (const key of Object.keys(meta)) {
26680
+ if (!KNOWN_KEYS.has(key)) {
26681
+ throw new Error(
26682
+ `defineMetadata: unknown key "${key}". Known keys: ${[...KNOWN_KEYS].join(", ")}. (\`openGraph\` carries og:* tags, not top-level \`ogTitle\`.)`
26683
+ );
26684
+ }
26685
+ }
26686
+ return meta;
26687
+ }
26688
+ function resolveTitle(meta, child) {
26689
+ if (!meta || meta.title === void 0 || meta.title === null) return null;
26690
+ if (typeof meta.title === "string") return meta.title;
26691
+ if (meta.title.absolute) return meta.title.absolute;
26692
+ if (meta.title.default) return meta.title.default;
26693
+ if (meta.title.template && child) {
26694
+ const childTitle = resolveTitle(child);
26695
+ return childTitle === null ? null : meta.title.template.replace("%s", childTitle);
26696
+ }
26697
+ return null;
26698
+ }
26699
+ function mergeMetadata(...sources) {
26700
+ const out = {};
26701
+ let template;
26702
+ let sawTemplate = false;
26703
+ for (const source of sources) {
26704
+ if (!source) continue;
26705
+ for (const [key, value] of Object.entries(source)) {
26706
+ if (value === void 0) continue;
26707
+ if (key === "openGraph" || key === "twitter") {
26708
+ out[key] = { ...out[key] || {}, ...value };
26709
+ continue;
26710
+ }
26711
+ if (key === "title") {
26712
+ if (value && typeof value === "object" && value.template) {
26713
+ sawTemplate = true;
26714
+ template = value.template || template;
26715
+ }
26716
+ out.title = value;
26717
+ continue;
26718
+ }
26719
+ out[key] = value;
26720
+ }
26721
+ }
26722
+ if (sawTemplate && out.title !== void 0) {
26723
+ const own = out.title;
26724
+ if (typeof own === "string") out.title = { absolute: own };
26725
+ else if (own && typeof own === "object" && own.template) {
26726
+ out.title = { default: own.default, template: own.template || template };
26727
+ } else if (own && typeof own === "object") {
26728
+ out.title = { ...own, template };
26729
+ }
26730
+ }
26731
+ return out;
26732
+ }
26733
+ function escapeAttr2(value) {
26734
+ return String(value).split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;").split('"').join("&quot;");
26735
+ }
26736
+ function escapeText(value) {
26737
+ return String(value).split("&").join("&amp;").split("<").join("&lt;").split(">").join("&gt;");
26738
+ }
26739
+ function metadataToHtml(meta, opts = {}) {
26740
+ if (!meta) return "";
26741
+ const lines = [];
26742
+ const title = resolveTitle(meta, opts.child);
26743
+ if (title !== null) lines.push(`<title data-vesk-head>${escapeText(title)}</title>`);
26744
+ if (typeof meta.description === "string" && meta.description.length > 0) {
26745
+ lines.push(`<meta name="description" content="${escapeAttr2(meta.description)}" data-vesk-head />`);
26746
+ }
26747
+ if (meta.robots) {
26748
+ const content = typeof meta.robots === "string" ? meta.robots : Object.entries(meta.robots).filter(([, on]) => on).map(([k]) => k).join(", ");
26749
+ if (content) lines.push(`<meta name="robots" content="${escapeAttr2(content)}" data-vesk-head />`);
26750
+ }
26751
+ if (meta.themeColor) {
26752
+ lines.push(`<meta name="theme-color" content="${escapeAttr2(meta.themeColor)}" data-vesk-head />`);
26753
+ }
26754
+ const og = meta.openGraph;
26755
+ if (og) {
26756
+ const ogTitle = resolveTitle(og) ?? title;
26757
+ const ogFields = [
26758
+ ["og:title", ogTitle],
26759
+ ["og:description", og.description ?? meta.description],
26760
+ ["og:url", og.url],
26761
+ ["og:site_name", og.siteName],
26762
+ ["og:image", og.image],
26763
+ ["og:type", og.type]
26764
+ ];
26765
+ for (const [prop, value] of ogFields) {
26766
+ if (value === void 0 || value === null || value === "") continue;
26767
+ lines.push(`<meta property="${prop}" content="${escapeAttr2(value)}" data-vesk-head />`);
26768
+ }
26769
+ }
26770
+ const tw = meta.twitter;
26771
+ if (tw) {
26772
+ const twFields = [
26773
+ ["twitter:card", tw.card],
26774
+ ["twitter:site", tw.site],
26775
+ ["twitter:title", resolveTitle(tw) ?? title],
26776
+ ["twitter:description", tw.description ?? meta.description],
26777
+ ["twitter:image", tw.image]
26778
+ ];
26779
+ for (const [name, value] of twFields) {
26780
+ if (value === void 0 || value === null || value === "") continue;
26781
+ lines.push(`<meta name="${name}" content="${escapeAttr2(value)}" data-vesk-head />`);
26782
+ }
26783
+ }
26784
+ const alt = meta.alternates;
26785
+ if (alt) {
26786
+ if (typeof alt === "object" && typeof alt.canonical === "string") {
26787
+ lines.push(`<link rel="canonical" href="${escapeAttr2(alt.canonical)}" data-vesk-head />`);
26788
+ }
26789
+ const languages = alt.languages;
26790
+ if (languages && typeof languages === "object") {
26791
+ for (const [lang, href] of Object.entries(languages)) {
26792
+ lines.push(`<link rel="alternate" hreflang="${escapeAttr2(lang)}" href="${escapeAttr2(href)}" data-vesk-head />`);
26793
+ }
26794
+ }
26795
+ }
26796
+ return lines.join("\n");
26797
+ }
26798
+ var KNOWN_KEYS;
26799
+ var init_metadata = __esm({
26800
+ "../runtime/src/metadata.ts"() {
26801
+ "use strict";
26802
+ KNOWN_KEYS = /* @__PURE__ */ new Set([
26803
+ "title",
26804
+ "description",
26805
+ "robots",
26806
+ "openGraph",
26807
+ "twitter",
26808
+ "alternates",
26809
+ "themeColor"
26810
+ ]);
26811
+ }
26812
+ });
26813
+
26617
26814
  // ../runtime/src/index-server.ts
26618
26815
  var index_server_exports = {};
26619
26816
  __export(index_server_exports, {
@@ -26675,6 +26872,7 @@ __export(index_server_exports, {
26675
26872
  custom: () => custom,
26676
26873
  defineAction: () => defineAction,
26677
26874
  defineHook: () => defineHook,
26875
+ defineMetadata: () => defineMetadata,
26678
26876
  defineRoute: () => defineRoute,
26679
26877
  derived: () => derived,
26680
26878
  destroy_block: () => destroy_block,
@@ -26702,6 +26900,8 @@ __export(index_server_exports, {
26702
26900
  leaf: () => leaf,
26703
26901
  locals: () => locals,
26704
26902
  maxLength: () => maxLength,
26903
+ mergeMetadata: () => mergeMetadata,
26904
+ metadataToHtml: () => metadataToHtml,
26705
26905
  minLength: () => minLength,
26706
26906
  mutate: () => mutate,
26707
26907
  notFound: () => notFound,
@@ -26722,6 +26922,7 @@ __export(index_server_exports, {
26722
26922
  required: () => required,
26723
26923
  rerenderNode: () => rerenderNode,
26724
26924
  resolveSsrResources: () => resolveSsrResources,
26925
+ resolveTitle: () => resolveTitle,
26725
26926
  resume_block: () => resume_block,
26726
26927
  revalidateComponent: () => revalidateComponent,
26727
26928
  revalidatePath: () => revalidatePath,
@@ -26786,6 +26987,7 @@ var init_index_server = __esm({
26786
26987
  init_primitive();
26787
26988
  init_loading_indicator();
26788
26989
  init_action();
26990
+ init_metadata();
26789
26991
  }
26790
26992
  });
26791
26993
 
@@ -31410,6 +31612,17 @@ function irNodeToHeadHtml(node, ctx2, reconcileable = false) {
31410
31612
  }
31411
31613
  return "";
31412
31614
  }
31615
+ function renderStaticMetadataHtml(source) {
31616
+ if (!source) return "";
31617
+ try {
31618
+ const fn = new Function("defineMetadata", "return (" + source + ")");
31619
+ const meta = fn(defineMetadata);
31620
+ return metadataToHtml(meta);
31621
+ } catch (e) {
31622
+ console.error("vesk: static metadata could not be evaluated:", e instanceof Error ? e.message : e);
31623
+ return "";
31624
+ }
31625
+ }
31413
31626
  function renderHeadHtml(comp, props = {}, scope2 = {}) {
31414
31627
  const tracked2 = collectTrackedNames(comp.body);
31415
31628
  const needed = headNeededNames(comp);
@@ -31577,6 +31790,7 @@ var init_server_head = __esm({
31577
31790
  init_scan();
31578
31791
  init_parser();
31579
31792
  init_client_codegen();
31793
+ init_metadata();
31580
31794
  headRuntimeShims = () => ({
31581
31795
  track: (init) => headCell(init),
31582
31796
  derived: (fn) => headCell(typeof fn === "function" ? fn() : fn),
@@ -32244,6 +32458,7 @@ function compileFile(source, options) {
32244
32458
  const owners = new VskComponentOwners();
32245
32459
  const result = compileFileInternal(source, options?.sourcePath, /* @__PURE__ */ new Set(), owners);
32246
32460
  owners.report("SSR");
32461
+ result.metadataSource = result.ir.metadataSource || null;
32247
32462
  return result;
32248
32463
  }
32249
32464
  function compileFileInternal(source, sourcePath2, seenImportFiles, owners) {
@@ -32346,6 +32561,7 @@ function renderPage(source, componentName, props = {}, registry = /* @__PURE__ *
32346
32561
  if (!renderFn) throw new Error(`Component "${componentName}" not found in source`);
32347
32562
  const fullRegistry = new Map([...registry, ...componentMap]);
32348
32563
  const targetComp = ir.components.find((c) => c.name === componentName);
32564
+ const metadataSource = ir.metadataSource || null;
32349
32565
  const doRender = (ssrProps) => {
32350
32566
  globalThis.__vsk_ssr = true;
32351
32567
  if (!globalThis.__vsk_ssr_token) globalThis.__vsk_ssr_token = Math.random().toString(36).slice(2);
@@ -32364,9 +32580,11 @@ function renderPage(source, componentName, props = {}, registry = /* @__PURE__ *
32364
32580
  }
32365
32581
  return {
32366
32582
  body: bodyHtml2,
32367
- // The component's own scope, so head expressions that read an import
32368
- // resolve (see renderHeadHtml).
32369
- head: renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk)),
32583
+ // A static `export const metadata` wins: it is evaluated in a sealed
32584
+ // sandbox, so it cannot depend on the component's frame resolving
32585
+ // (the failure mode that shipped an empty <title>). Whatever the
32586
+ // page's `<Head>` adds is appended, not lost.
32587
+ head: [renderStaticMetadataHtml(metadataSource), renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk))].filter(Boolean).join("\n"),
32370
32588
  props: ssrProps
32371
32589
  };
32372
32590
  })();
@@ -32377,7 +32595,10 @@ function renderPage(source, componentName, props = {}, registry = /* @__PURE__ *
32377
32595
  } finally {
32378
32596
  delete globalThis.__vsk_ssr;
32379
32597
  }
32380
- const headHtml = targetComp ? renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk)) : "";
32598
+ const headHtml = [
32599
+ renderStaticMetadataHtml(metadataSource),
32600
+ targetComp ? renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk)) : ""
32601
+ ].filter(Boolean).join("\n");
32381
32602
  const pending = globalThis[`__vsk_ssr_promises_${renderToken}`];
32382
32603
  if (pending && pending.length > 0) {
32383
32604
  return (async () => {
@@ -32407,6 +32628,7 @@ function renderPage(source, componentName, props = {}, registry = /* @__PURE__ *
32407
32628
  function clearSsrCells(token) {
32408
32629
  if (!token) return;
32409
32630
  delete globalThis[`__vsk_ssr_promises_${token}`];
32631
+ delete globalThis[`__vsk_ssr_deferred_${token}`];
32410
32632
  delete globalThis[`__vsk_ssr_failures_${token}`];
32411
32633
  dropSsrSlot(token);
32412
32634
  const cells = globalThis.__vsk_ssr_cells;
@@ -32679,6 +32901,7 @@ function renderPageStream(source, componentName, props = {}, registry = /* @__PU
32679
32901
  if (!renderFn) throw new Error(`Component "${componentName}" not found in source`);
32680
32902
  resetVskState(!!options.hydrate, options.markerless !== false);
32681
32903
  const targetComp = ir.components.find((c) => c.name === componentName);
32904
+ const metadataSource = cached?.metadataSource ?? ir.metadataSource ?? null;
32682
32905
  const cssUrls = options.cssUrls || (options.cssUrl ? [options.cssUrl] : []);
32683
32906
  const cssLink = cssUrls.map((u) => ` <link rel="stylesheet" href="${u}" />
32684
32907
  `).join("");
@@ -32692,7 +32915,10 @@ function renderPageStream(source, componentName, props = {}, registry = /* @__PU
32692
32915
  }
32693
32916
  let streamHeadHtml = null;
32694
32917
  if (targetComp) {
32695
- let headHtml = renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk));
32918
+ let headHtml = [
32919
+ renderStaticMetadataHtml(metadataSource),
32920
+ renderHeadHtml(targetComp, ssrProps, scopedVesk(renderFn, __vesk))
32921
+ ].filter(Boolean).join("\n");
32696
32922
  if (options.pageHead) {
32697
32923
  const merged = mergeHeadHtml(options.pageHead, headHtml);
32698
32924
  headHtml = merged.html;
@@ -46657,6 +46883,7 @@ function precompileFileInternal(source, sourcePath2, seenImportFiles) {
46657
46883
  setVskHydrate(false);
46658
46884
  }
46659
46885
  const topLevelCode = transformTopLevelForActions(ir.topLevelCode, "server");
46886
+ const metadataSource = ir.metadataSource || null;
46660
46887
  const runtimeImports = ir.imports.slice();
46661
46888
  const collector = { keysByAbs: /* @__PURE__ */ new Map(), modules: [] };
46662
46889
  const bindings = sourcePath2 !== void 0 ? collectModuleBundle(ir.imports, dirname6(sourcePath2), collector) : [];
@@ -46684,7 +46911,10 @@ function precompileFileInternal(source, sourcePath2, seenImportFiles) {
46684
46911
  modules: collector.modules,
46685
46912
  bindings,
46686
46913
  topLevelCode,
46687
- subFiles
46914
+ subFiles,
46915
+ // A static `export const metadata` rides along so a serverless function can
46916
+ // serialize the head without the `.vsk` source being present at all.
46917
+ ...metadataSource ? { metadataSource } : {}
46688
46918
  };
46689
46919
  }
46690
46920
 
@@ -62534,6 +62764,7 @@ __export(index_server_exports2, {
62534
62764
  custom: () => custom,
62535
62765
  defineAction: () => defineAction,
62536
62766
  defineHook: () => defineHook,
62767
+ defineMetadata: () => defineMetadata,
62537
62768
  defineRoute: () => defineRoute,
62538
62769
  derived: () => derived,
62539
62770
  destroy_block: () => destroy_block,
@@ -62561,6 +62792,8 @@ __export(index_server_exports2, {
62561
62792
  leaf: () => leaf,
62562
62793
  locals: () => locals,
62563
62794
  maxLength: () => maxLength,
62795
+ mergeMetadata: () => mergeMetadata,
62796
+ metadataToHtml: () => metadataToHtml,
62564
62797
  minLength: () => minLength,
62565
62798
  mutate: () => mutate,
62566
62799
  notFound: () => notFound,
@@ -62581,6 +62814,7 @@ __export(index_server_exports2, {
62581
62814
  required: () => required,
62582
62815
  rerenderNode: () => rerenderNode,
62583
62816
  resolveSsrResources: () => resolveSsrResources,
62817
+ resolveTitle: () => resolveTitle,
62584
62818
  resume_block: () => resume_block,
62585
62819
  revalidateComponent: () => revalidateComponent,
62586
62820
  revalidatePath: () => revalidatePath,
@@ -62642,6 +62876,7 @@ init_headless();
62642
62876
  init_primitive();
62643
62877
  init_loading_indicator();
62644
62878
  init_action();
62879
+ init_metadata();
62645
62880
 
62646
62881
  // src/index.ts
62647
62882
  var __filename2 = fileURLToPath9(import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/vesk-cli",
3
- "version": "0.2.50-canary.4",
3
+ "version": "0.2.50-canary.5",
4
4
  "type": "module",
5
5
  "description": "Vesk — compiler-first framework for the post-VDOM web. vesk dev, vesk build.",
6
6
  "bin": {
@@ -8,10 +8,10 @@
8
8
  },
9
9
  "main": "./dist/cli.js",
10
10
  "dependencies": {
11
- "@vesk/adapter": "^0.2.50-canary.4",
12
- "@vesk/compiler": "^0.2.50-canary.4",
13
- "@vesk/plugin-tailwind": "^0.2.50-canary.4",
14
- "@vesk/runtime": "^0.2.50-canary.4",
11
+ "@vesk/adapter": "^0.2.50-canary.5",
12
+ "@vesk/compiler": "^0.2.50-canary.5",
13
+ "@vesk/plugin-tailwind": "^0.2.50-canary.5",
14
+ "@vesk/runtime": "^0.2.50-canary.5",
15
15
  "esrap": "^2.3.0",
16
16
  "typescript": "^5.8.0",
17
17
  "ws": "^8.18.0",