@jay-framework/production-server 0.22.2 → 0.23.1

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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BuildOptions, R as RouteManifest, a as RouteEntry } from './serve-index-lg6huJYm.js';
2
- export { A as ActionEntry, e as ArtifactStore, c as BuildMetadata, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, d as PageModule, P as PluginEntry, n as PreImportedPlugin, b as RouteSegment, S as ServerElementModule, g as fetchActionRequest, f as fetchPageRequest, j as fetchStaticFile, k as initializeServices, l as initializeServicesFromModules, i as isActionRequest, m as matchRequest, r as registerActionsFromManifest, h as registerActionsFromModules } from './serve-index-lg6huJYm.js';
1
+ import { B as BuildOptions, R as RouteManifest, a as RouteEntry } from './serve-index-Ccw8CNDg.js';
2
+ export { A as ActionEntry, b as ArtifactStore, c as BuildMetadata, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, P as PageModule, d as PluginEntry, e as PreImportedPlugin, f as RouteSegment, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, m as matchRequest, r as registerActionsFromManifest, n as registerActionsFromModules } from './serve-index-Ccw8CNDg.js';
3
3
  import '@jay-framework/ssr-runtime';
4
4
  import '@jay-framework/compiler-shared';
5
5
  import '@jay-framework/stack-server-runtime';
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
7
  import { createRequire } from "node:module";
8
8
  import { DevSlowlyChangingPhase, slowRenderInstances, scanPlugins, runLoadParams, parseCookies } from "@jay-framework/stack-server-runtime";
9
- import { l as loadProductionPageParts, g as buildPagePartsConfig, d as initializeServices, r as registerActionsFromManifest, i as isActionRequest, a as fetchActionRequest, c as fetchStaticFile, m as matchRequest, f as fetchPageRequest, F as FilesystemArtifactStore } from "./init-services-zmGSxSck.js";
10
- import { e, b } from "./init-services-zmGSxSck.js";
9
+ import { l as loadProductionPageParts, g as buildPagePartsConfig, F as FilesystemArtifactStore, i as initializeServices, r as registerActionsFromManifest, d as isActionRequest, f as fetchActionRequest, b as fetchStaticFile, m as matchRequest, a as fetchPageRequest } from "./init-services-BVEuyIVG.js";
10
+ import { c, e } from "./init-services-BVEuyIVG.js";
11
11
  import crypto, { createHash } from "node:crypto";
12
12
  import fs$1 from "node:fs";
13
13
  import { jayRuntime } from "@jay-framework/vite-plugin";
@@ -18,8 +18,6 @@ import { transform } from "esbuild";
18
18
  import http from "node:http";
19
19
  import { Readable } from "node:stream";
20
20
  import { isJayWebhook } from "@jay-framework/fullstack-component";
21
- import "@jay-framework/view-state-merge";
22
- import "@jay-framework/ssr-runtime";
23
21
  async function discoverServerEntries(projectRoot, pagesRoot) {
24
22
  const logger = getLogger();
25
23
  const routes = await scanRoutes(pagesRoot, {
@@ -51,8 +49,7 @@ async function discoverServerEntries(projectRoot, pagesRoot) {
51
49
  try {
52
50
  const dirs = await fs.readdir(scanDir, { withFileTypes: true });
53
51
  for (const dir of dirs) {
54
- if (!dir.isDirectory())
55
- continue;
52
+ if (!dir.isDirectory()) continue;
56
53
  const dirPath = path.join(scanDir, dir.name);
57
54
  const files = await fs.readdir(dirPath);
58
55
  for (const file of files) {
@@ -194,8 +191,7 @@ async function parseViteManifest(outputDir, packages) {
194
191
  }
195
192
  const manifest = {};
196
193
  for (const [, entry] of Object.entries(raw)) {
197
- if (!entry.isEntry)
198
- continue;
194
+ if (!entry.isEntry) continue;
199
195
  const outputBase = path.basename(entry.file, ".js");
200
196
  for (const [varName, pkg] of varNameToPackage) {
201
197
  if (outputBase.startsWith(varName)) {
@@ -218,8 +214,7 @@ function hashParams(params, suffix) {
218
214
  {}
219
215
  );
220
216
  const json = JSON.stringify(sorted);
221
- if (json === "{}" && !suffix)
222
- return "";
217
+ if (json === "{}" && !suffix) return "";
223
218
  const input = suffix ? json + ":" + suffix : json;
224
219
  return "_" + crypto.createHash("md5").update(input).digest("hex").substring(0, 8);
225
220
  }
@@ -245,36 +240,32 @@ async function buildInstance(route, params, pageModule, ctx, routeServerElementP
245
240
  );
246
241
  const contracts = [
247
242
  .../* @__PURE__ */ new Set([
248
- ...pageParts.headlessInstanceComponents.map((c) => c.contractName),
243
+ ...pageParts.headlessInstanceComponents.map((c2) => c2.contractName),
249
244
  ...pageParts.parts.filter((p) => p.contractInfo?.contractName).map((p) => p.contractInfo.contractName)
250
245
  ])
251
246
  ];
252
247
  const pagePartsConfigPath = path.join(backendInstanceDir, "page-parts.json");
253
- try {
254
- await fs.access(pagePartsConfigPath);
255
- } catch {
256
- const exportName = route.componentExport || "page";
257
- let pageServerModule = "";
258
- let pageIsPlugin = false;
259
- if (route.compPath) {
260
- if (route.componentExport) {
261
- pageServerModule = route.packageName || route.compPath;
262
- pageIsPlugin = true;
263
- } else {
264
- const relativePath = path.relative(ctx.projectRoot, route.compPath);
265
- pageServerModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
266
- }
248
+ const exportName = route.componentExport || "page";
249
+ let pageServerModule = "";
250
+ let pageIsPlugin = false;
251
+ if (route.compPath) {
252
+ if (route.componentExport) {
253
+ pageServerModule = route.packageName || route.compPath;
254
+ pageIsPlugin = true;
255
+ } else {
256
+ const relativePath = path.relative(ctx.projectRoot, route.compPath);
257
+ pageServerModule = relativePath.replace(/^src\//, "server/").replace(/\.ts$/, ".js").replace(/\[/g, "_").replace(/\]/g, "_");
267
258
  }
268
- const config = buildPagePartsConfig(
269
- pageParts,
270
- pageServerModule,
271
- exportName,
272
- ctx.backendDir,
273
- pageIsPlugin
274
- );
275
- await fs.writeFile(pagePartsConfigPath, JSON.stringify(config, null, 2));
276
- logger.info(`[Build] Page parts config: ${routeDir}/page-parts.json`);
277
259
  }
260
+ const config = buildPagePartsConfig(
261
+ pageParts,
262
+ pageServerModule,
263
+ exportName,
264
+ ctx.backendDir,
265
+ pageIsPlugin
266
+ );
267
+ await fs.writeFile(pagePartsConfigPath, JSON.stringify(config, null, 2));
268
+ logger.info(`[Build] Page parts config: ${routeDir}/page-parts.json`);
278
269
  const slowPhase = new DevSlowlyChangingPhase();
279
270
  const slowResult = await slowPhase.runSlowlyForPage(
280
271
  params,
@@ -377,7 +368,8 @@ function buildRouteEntry(route, serverModulePath) {
377
368
  segments: convertSegments(route.segments),
378
369
  serverModule: serverModulePath,
379
370
  componentExport: route.componentExport,
380
- instances: []
371
+ instances: [],
372
+ ...route.devOnly && { devOnly: true }
381
373
  };
382
374
  }
383
375
  async function discoverActions(actionPaths, serverOutputDir, buildDir, projectRoot) {
@@ -404,8 +396,7 @@ async function discoverActions(actionPaths, serverOutputDir, buildDir, projectRo
404
396
  try {
405
397
  const scannedPlugins = await scanPlugins({ projectRoot });
406
398
  for (const [packageName, plugin] of scannedPlugins) {
407
- if (plugin.isLocal)
408
- continue;
399
+ if (plugin.isLocal) continue;
409
400
  plugins.push({ name: plugin.manifest.name, packageName });
410
401
  const pluginActions = plugin.manifest.actions;
411
402
  if (pluginActions && pluginActions.length > 0) {
@@ -441,10 +432,8 @@ async function scanPluginRoutes(projectRoot, projectRoutes) {
441
432
  const projectPaths = new Set(projectRoutes.map((r) => r.rawRoute));
442
433
  const pluginRoutes = [];
443
434
  for (const [, plugin] of plugins) {
444
- if (plugin.isLocal)
445
- continue;
446
- if (!plugin.manifest.routes)
447
- continue;
435
+ if (plugin.isLocal) continue;
436
+ if (!plugin.manifest.routes) continue;
448
437
  for (const route of plugin.manifest.routes) {
449
438
  if (projectPaths.has(route.path)) {
450
439
  logger.info(
@@ -467,9 +456,12 @@ async function scanPluginRoutes(projectRoot, projectRoutes) {
467
456
  jayHtmlPath,
468
457
  compPath,
469
458
  componentExport,
470
- packageName: plugin.packageName
459
+ packageName: plugin.packageName,
460
+ ...route.devOnly === true && { devOnly: true }
471
461
  });
472
- logger.info(`[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}`);
462
+ logger.info(
463
+ `[Routes] Plugin "${plugin.manifest.name}" provides route ${route.path}${route.devOnly ? " (dev-only)" : ""}`
464
+ );
473
465
  }
474
466
  }
475
467
  return pluginRoutes;
@@ -484,8 +476,7 @@ function resolvePluginExport(pluginPath, exportSubpath) {
484
476
  const exportValue = packageJson.exports[exportKey];
485
477
  if (exportValue) {
486
478
  const resolved = typeof exportValue === "string" ? exportValue : exportValue.default || exportValue.import || exportValue.require;
487
- if (resolved)
488
- return path.join(pluginPath, resolved);
479
+ if (resolved) return path.join(pluginPath, resolved);
489
480
  }
490
481
  }
491
482
  } catch {
@@ -508,8 +499,7 @@ function resolvePluginModule(pluginPath) {
508
499
  const mainPath = typeof mainExport === "string" ? mainExport : mainExport?.default || mainExport?.import || pkg.main;
509
500
  if (mainPath) {
510
501
  const resolved = path.join(pluginPath, mainPath);
511
- if (fs$1.existsSync(resolved))
512
- return resolved;
502
+ if (fs$1.existsSync(resolved)) return resolved;
513
503
  }
514
504
  } catch {
515
505
  }
@@ -558,14 +548,11 @@ async function compileServerElement(jayHtmlContent, jayHtmlFilename, jayHtmlDir,
558
548
  const css = parsedJayFile.css;
559
549
  if (css) {
560
550
  cssImports = extractCssImportUrls(css);
561
- const cssFilename = path.basename(outputPath, ".server-element.js") + ".css";
562
- const cssPath = path.join(outputDir, cssFilename);
563
- if (minifyCss) {
564
- const minified = await transform(css, { loader: "css", minify: true });
565
- await fs.writeFile(cssPath, minified.code, "utf-8");
566
- } else {
567
- await fs.writeFile(cssPath, css, "utf-8");
568
- }
551
+ const cssContent = minifyCss ? (await transform(css, { loader: "css", minify: true })).code : css;
552
+ const hash = createHash("sha256").update(cssContent).digest("hex").slice(0, 8);
553
+ const baseName = path.basename(outputPath, ".server-element.js");
554
+ const cssFilename = `${baseName}-${hash}.css`;
555
+ await fs.writeFile(path.join(outputDir, cssFilename), cssContent, "utf-8");
569
556
  cssFile = cssFilename;
570
557
  }
571
558
  getLogger().info(`[Build] Compiled server element: ${path.basename(outputPath)}`);
@@ -636,8 +623,7 @@ async function compileRouteHydrateScript(jayHtmlPath, outputDir, projectRoot, ts
636
623
  const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
637
624
  await fs.rm(manifestPath, { force: true });
638
625
  const entryKey = Object.keys(manifest).find((k) => manifest[k].isEntry);
639
- if (!entryKey)
640
- throw new Error("No entry in route hydrate manifest");
626
+ if (!entryKey) throw new Error("No entry in route hydrate manifest");
641
627
  const jsFile = manifest[entryKey].file;
642
628
  getLogger().info(`[Build] Compiled route hydrate script: ${jsFile}`);
643
629
  return { jsFile };
@@ -652,8 +638,7 @@ function resolveJayHtmlPaths(html, sourceDir, targetDir) {
652
638
  if (val && (val.startsWith("./") || val.startsWith("../"))) {
653
639
  const abs = path.resolve(sourceDir, val);
654
640
  let rel = path.relative(targetDir, abs);
655
- if (!rel.startsWith("."))
656
- rel = "./" + rel;
641
+ if (!rel.startsWith(".")) rel = "./" + rel;
657
642
  el.setAttribute(attr, rel);
658
643
  }
659
644
  };
@@ -784,10 +769,8 @@ async function buildInstanceClient(hydrateEntryPath, instanceId, outputDir, proj
784
769
  return result;
785
770
  }
786
771
  function crossProductParams(parts) {
787
- if (parts.length === 0)
788
- return [];
789
- if (parts.length === 1)
790
- return parts[0].values;
772
+ if (parts.length === 0) return [];
773
+ if (parts.length === 1) return parts[0].values;
791
774
  const logger = getLogger();
792
775
  for (let i = 0; i < parts.length; i++) {
793
776
  for (let j = i + 1; j < parts.length; j++) {
@@ -805,8 +788,8 @@ function crossProductParams(parts) {
805
788
  const next = parts[i].values;
806
789
  const combined = [];
807
790
  for (const a of result) {
808
- for (const b2 of next) {
809
- combined.push({ ...a, ...b2 });
791
+ for (const b of next) {
792
+ combined.push({ ...a, ...b });
810
793
  }
811
794
  }
812
795
  result = combined;
@@ -815,8 +798,7 @@ function crossProductParams(parts) {
815
798
  }
816
799
  function paramsMatchInferred(params, inferredParams, optionalSegments) {
817
800
  return Object.entries(inferredParams).every(([k, v]) => {
818
- if (optionalSegments?.has(k))
819
- return true;
801
+ if (optionalSegments?.has(k)) return true;
820
802
  return params[k] === v;
821
803
  });
822
804
  }
@@ -829,10 +811,8 @@ function computeSpecificity(route) {
829
811
  function buildUrl(route, params) {
830
812
  return route.rawRoute.replace(/\[\[(\w+)\]\]/g, (_, name) => {
831
813
  const value = params[name];
832
- if (!value)
833
- return "";
834
- if (route.inferredParams?.[name] === value)
835
- return "";
814
+ if (!value) return "";
815
+ if (route.inferredParams?.[name] === value) return "";
836
816
  return value;
837
817
  }).replace(/\[(\w+)\]/g, (_, name) => params[name] || "").replace(/\/\/+/g, "/").replace(/\/$/, "") || "/";
838
818
  }
@@ -889,8 +869,7 @@ async function discoverPluginClientPackages(projectRoot) {
889
869
  const seen = /* @__PURE__ */ new Set();
890
870
  const result = [];
891
871
  async function walk(pkgName) {
892
- if (seen.has(pkgName))
893
- return;
872
+ if (seen.has(pkgName)) return;
894
873
  seen.add(pkgName);
895
874
  try {
896
875
  const mainPath = projectRequire.resolve(pkgName);
@@ -1026,8 +1005,7 @@ async function buildVersion(options) {
1026
1005
  await discoverPluginsWithInit({ projectRoot: options.projectRoot })
1027
1006
  );
1028
1007
  for (const pluginInit of allPluginsWithInit) {
1029
- if (pluginInit.isLocal)
1030
- continue;
1008
+ if (pluginInit.isLocal) continue;
1031
1009
  const clientImportPath = `${pluginInit.packageName}/client`;
1032
1010
  try {
1033
1011
  const clientModule = await import(clientImportPath);
@@ -1092,10 +1070,8 @@ async function buildVersion(options) {
1092
1070
  logger.important(`[Build] ${instanceCount}/${totalExpected} ${routeName}${paramStr}`);
1093
1071
  }
1094
1072
  async function loadPageModule(entry) {
1095
- if (!entry.serverModule)
1096
- return {};
1097
- if (entry.isPlugin)
1098
- return import(entry.serverModule);
1073
+ if (!entry.serverModule) return {};
1074
+ if (entry.isPlugin) return import(entry.serverModule);
1099
1075
  return import(path.join(backendDir, entry.serverModule));
1100
1076
  }
1101
1077
  const routeInfos = routeEntries.map((re) => {
@@ -1111,8 +1087,7 @@ async function buildVersion(options) {
1111
1087
  const loadParamsCache = /* @__PURE__ */ new Map();
1112
1088
  const loadParamsResults = /* @__PURE__ */ new Map();
1113
1089
  for (const info of routeInfos) {
1114
- if (!info.hasDynamicParams)
1115
- continue;
1090
+ if (!info.hasDynamicParams) continue;
1116
1091
  const { route, entry } = info.routeEntry;
1117
1092
  let pageModule;
1118
1093
  try {
@@ -1130,12 +1105,12 @@ async function buildVersion(options) {
1130
1105
  serverOutputDir
1131
1106
  );
1132
1107
  const partsWithLoadParams = pageParts.parts.filter((p) => p.compDefinition?.loadParams);
1133
- if (partsWithLoadParams.length === 0)
1134
- continue;
1108
+ if (partsWithLoadParams.length === 0) continue;
1135
1109
  const paramParts = [];
1136
1110
  for (const part of partsWithLoadParams) {
1137
- const fn = part.compDefinition.loadParams;
1138
- if (!loadParamsCache.has(fn)) {
1111
+ const propsKey = JSON.stringify(part.headlessProps ?? {});
1112
+ const cacheKey = `${part.key ?? ""}:${propsKey}`;
1113
+ if (!loadParamsCache.has(cacheKey)) {
1139
1114
  logger.important(`[Build] Loading params for ${route.rawRoute}...`);
1140
1115
  const partParams = [];
1141
1116
  let batchIndex = 0;
@@ -1146,9 +1121,9 @@ async function buildVersion(options) {
1146
1121
  logger.important(`[Build] ...${partParams.length} params so far`);
1147
1122
  }
1148
1123
  }
1149
- loadParamsCache.set(fn, partParams);
1124
+ loadParamsCache.set(cacheKey, partParams);
1150
1125
  }
1151
- const cached = loadParamsCache.get(fn);
1126
+ const cached = loadParamsCache.get(cacheKey);
1152
1127
  const keys = new Set(cached.flatMap((p) => Object.keys(p)));
1153
1128
  paramParts.push({ keys, values: cached });
1154
1129
  }
@@ -1160,14 +1135,12 @@ async function buildVersion(options) {
1160
1135
  const byRoute = /* @__PURE__ */ new Map();
1161
1136
  for (const materialized2 of deduped) {
1162
1137
  const info = materialized2.route;
1163
- if (!byRoute.has(info))
1164
- byRoute.set(info, []);
1138
+ if (!byRoute.has(info)) byRoute.set(info, []);
1165
1139
  byRoute.get(info).push(materialized2.params);
1166
1140
  }
1167
1141
  for (const [info] of byRoute) {
1168
1142
  const { route, entry } = info.routeEntry;
1169
- if (!route.jayHtmlPath)
1170
- continue;
1143
+ if (!route.jayHtmlPath) continue;
1171
1144
  const routeDir = route.rawRoute.replace(/^\//, "") || "index";
1172
1145
  const frontendSafeRouteDir = routeDir.replace(/\[/g, "%5B").replace(/\]/g, "%5D");
1173
1146
  const backendRouteDir = path.join(backendDir, "pre-rendered", routeDir);
@@ -1356,8 +1329,7 @@ function toFetchRequest(req) {
1356
1329
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
1357
1330
  const headers = new Headers();
1358
1331
  for (const [key, value] of Object.entries(req.headers)) {
1359
- if (value)
1360
- headers.set(key, Array.isArray(value) ? value.join(", ") : value);
1332
+ if (value) headers.set(key, Array.isArray(value) ? value.join(", ") : value);
1361
1333
  }
1362
1334
  const init = { method: req.method, headers };
1363
1335
  if (req.method !== "GET" && req.method !== "HEAD") {
@@ -1377,8 +1349,7 @@ async function pipeFetchResponse(response, res) {
1377
1349
  try {
1378
1350
  while (true) {
1379
1351
  const { done, value } = await reader.read();
1380
- if (done)
1381
- break;
1352
+ if (done) break;
1382
1353
  res.write(value);
1383
1354
  }
1384
1355
  } finally {
@@ -1458,8 +1429,7 @@ async function startMainServer(options) {
1458
1429
  await pipeFetchResponse(response, res);
1459
1430
  } catch (err) {
1460
1431
  logger.error(`[Server] Error handling ${url.pathname}: ${err.message}`);
1461
- if (err.stack)
1462
- logger.error(err.stack);
1432
+ if (err.stack) logger.error(err.stack);
1463
1433
  if (!res.headersSent) {
1464
1434
  res.writeHead(500);
1465
1435
  res.end("Internal Server Error");
@@ -1485,11 +1455,9 @@ async function discoverWebhooks(projectRoot, serverBuildDir) {
1485
1455
  try {
1486
1456
  const plugins = await scanPlugins({ projectRoot });
1487
1457
  for (const [packageName, plugin] of plugins) {
1488
- if (plugin.isLocal)
1489
- continue;
1458
+ if (plugin.isLocal) continue;
1490
1459
  const declaredWebhooks = plugin.manifest.webhooks;
1491
- if (!declaredWebhooks || declaredWebhooks.length === 0)
1492
- continue;
1460
+ if (!declaredWebhooks || declaredWebhooks.length === 0) continue;
1493
1461
  try {
1494
1462
  const pluginModule = await import(packageName);
1495
1463
  for (const entry of declaredWebhooks) {
@@ -1521,11 +1489,9 @@ async function discoverWebhooks(projectRoot, serverBuildDir) {
1521
1489
  try {
1522
1490
  const plugins = await scanPlugins({ projectRoot });
1523
1491
  for (const [, plugin] of plugins) {
1524
- if (!plugin.isLocal)
1525
- continue;
1492
+ if (!plugin.isLocal) continue;
1526
1493
  const declaredWebhooks = plugin.manifest.webhooks;
1527
- if (!declaredWebhooks || declaredWebhooks.length === 0)
1528
- continue;
1494
+ if (!declaredWebhooks || declaredWebhooks.length === 0) continue;
1529
1495
  const pluginDirName = path.basename(plugin.pluginPath);
1530
1496
  for (const entry of declaredWebhooks) {
1531
1497
  const exportName = typeof entry === "string" ? entry : entry.name;
@@ -1556,8 +1522,7 @@ async function discoverWebhooks(projectRoot, serverBuildDir) {
1556
1522
  try {
1557
1523
  const files = await fs.readdir(webhooksDir);
1558
1524
  for (const file of files) {
1559
- if (!file.endsWith(".js"))
1560
- continue;
1525
+ if (!file.endsWith(".js")) continue;
1561
1526
  try {
1562
1527
  const mod = await import(path.join(webhooksDir, file));
1563
1528
  for (const [, value] of Object.entries(mod)) {
@@ -1740,10 +1705,8 @@ async function rebuildContract(options) {
1740
1705
  });
1741
1706
  }
1742
1707
  async function loadRouteModule(route, buildDir) {
1743
- if (!route.serverModule)
1744
- return {};
1745
- if (route.isPlugin)
1746
- return import(route.serverModule);
1708
+ if (!route.serverModule) return {};
1709
+ if (route.isPlugin) return import(route.serverModule);
1747
1710
  return import(path.join(buildDir, route.serverModule));
1748
1711
  }
1749
1712
  async function resolveJayRouteFromManifest(route, options) {
@@ -1763,8 +1726,7 @@ async function resolveJayRouteFromManifest(route, options) {
1763
1726
  return {
1764
1727
  rawRoute: route.pattern,
1765
1728
  segments: route.segments.map((s) => {
1766
- if (s.type === "static")
1767
- return s.value;
1729
+ if (s.type === "static") return s.value;
1768
1730
  return { name: s.value, type: segmentTypeMap[s.type] };
1769
1731
  }),
1770
1732
  jayHtmlPath: resolvedJayHtmlPath,
@@ -1781,11 +1743,9 @@ function paramsMatch(instanceParams, targetParams) {
1781
1743
  return Object.entries(targetParams).every(([key, value]) => instanceParams[key] === value);
1782
1744
  }
1783
1745
  function collectInstanceFiles(instance) {
1784
- if (!instance.cachePath)
1785
- return [];
1746
+ if (!instance.cachePath) return [];
1786
1747
  const files = [instance.cachePath, instance.serverElementPath, instance.clientBundlePath];
1787
- if (instance.clientCssPath)
1788
- files.push(instance.clientCssPath);
1748
+ if (instance.clientCssPath) files.push(instance.clientCssPath);
1789
1749
  return files.filter(Boolean);
1790
1750
  }
1791
1751
  async function appendCleanupManifest(buildDir, files) {
@@ -1961,13 +1921,13 @@ export {
1961
1921
  fetchPageRequest,
1962
1922
  fetchStaticFile,
1963
1923
  initializeServices,
1964
- e as initializeServicesFromModules,
1924
+ c as initializeServicesFromModules,
1965
1925
  isActionRequest,
1966
1926
  matchRequest,
1967
1927
  rebuild,
1968
1928
  rebuildContract,
1969
1929
  registerActionsFromManifest,
1970
- b as registerActionsFromModules,
1930
+ e as registerActionsFromModules,
1971
1931
  resolveContractToRoutes,
1972
1932
  startMainServer,
1973
1933
  startRendererServer
@@ -1,9 +1,6 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
- var __publicField = (obj, key, value) => {
4
- __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
- return value;
6
- };
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
7
4
  import fs from "node:fs/promises";
8
5
  import path from "node:path";
9
6
  import { renderFastChangingData, headMetaToHeadTags, mergeHeadTags, serializeHeadTags, getClientInitData, actionRegistry, setClientInitData } from "@jay-framework/stack-server-runtime";
@@ -14,7 +11,7 @@ import { getLogger } from "@jay-framework/logger";
14
11
  import { parseJayFile, JAY_IMPORT_RESOLVER, injectHeadfullFSTemplates, assignCoordinatesToJayHtml, discoverHeadlessInstances } from "@jay-framework/compiler-jay-html";
15
12
  import { checkValidationErrors } from "@jay-framework/compiler-shared";
16
13
  import { isJayAction, isJayStreamAction } from "@jay-framework/fullstack-component";
17
- const require2 = createRequire(import.meta.url);
14
+ const require$1 = createRequire(import.meta.url);
18
15
  async function loadProductionPageParts(route, pageModule, jayHtmlContent, projectRoot, tsConfigFilePath, serverBuildDir) {
19
16
  const exportName = route.componentExport || "page";
20
17
  const compDefinition = pageModule[exportName] ?? pageModule.default;
@@ -64,9 +61,14 @@ async function loadProductionPageParts(route, pageModule, jayHtmlContent, projec
64
61
  } else {
65
62
  modulePath = module;
66
63
  }
67
- const resolvedModulePath = isLocalModule ? modulePath : require2.resolve(module, { paths: [dirName] });
68
- const headlessModule = await import(resolvedModulePath);
69
- const headlessCompDef = headlessModule[name];
64
+ let headlessCompDef;
65
+ if (headlessImport.structural) {
66
+ continue;
67
+ } else {
68
+ const resolvedModulePath = isLocalModule ? modulePath : require$1.resolve(module, { paths: [dirName] });
69
+ const headlessModule = await import(resolvedModulePath);
70
+ headlessCompDef = headlessModule[name];
71
+ }
70
72
  if (headlessImport.key) {
71
73
  const clientModulePath = isLocalModule ? path.resolve(dirName, module) : `${module}/client`;
72
74
  const ci = headlessImport.contract ? { contractName: headlessImport.contract.name, metadata: headlessImport.metadata } : void 0;
@@ -78,18 +80,22 @@ async function loadProductionPageParts(route, pageModule, jayHtmlContent, projec
78
80
  contractInfo: ci,
79
81
  headlessProps: headlessImport.headlessProps
80
82
  });
81
- keyedPartModules.push({
82
- key: headlessImport.key,
83
- modulePath: clientModulePath,
84
- exportName: name
85
- });
83
+ const hasClientComp = !!headlessCompDef?.fastRender || !!headlessCompDef?.hasInteractive;
84
+ if (!headlessImport.structural && hasClientComp) {
85
+ keyedPartModules.push({
86
+ key: headlessImport.key,
87
+ modulePath: clientModulePath,
88
+ exportName: name
89
+ });
90
+ }
86
91
  headlessModuleInfos.push({
87
92
  modulePath,
88
93
  exportName: name,
89
94
  isLocal: isLocalModule,
90
95
  key: headlessImport.key,
91
96
  contractInfo: ci,
92
- headlessProps: headlessImport.headlessProps
97
+ headlessProps: headlessImport.headlessProps,
98
+ structural: headlessImport.structural
93
99
  });
94
100
  }
95
101
  if (!headlessImport.key && headlessImport.contract) {
@@ -103,7 +109,8 @@ async function loadProductionPageParts(route, pageModule, jayHtmlContent, projec
103
109
  exportName: name,
104
110
  isLocal: isLocalModule,
105
111
  contractName: headlessImport.contractName,
106
- propNames: headlessImport.contract.props?.map((p) => p.name) ?? []
112
+ propNames: headlessImport.contract.props?.map((p) => p.name) ?? [],
113
+ structural: headlessImport.structural
107
114
  });
108
115
  }
109
116
  }
@@ -163,11 +170,12 @@ function buildPagePartsConfig(pageParts, pageServerModule, pageExportName, build
163
170
  for (const info of pageParts.headlessModuleInfos) {
164
171
  if (info.contractName) {
165
172
  instanceComponents.push({
166
- modulePath: info.isLocal ? path.relative(buildDir, info.modulePath) : info.modulePath,
173
+ modulePath: info.structural ? "" : info.isLocal ? path.relative(buildDir, info.modulePath) : info.modulePath,
167
174
  exportName: info.exportName,
168
175
  source: info.isLocal ? "local" : "npm",
169
176
  contractName: info.contractName,
170
- propNames: info.propNames ?? []
177
+ propNames: info.propNames ?? [],
178
+ ...info.structural ? { structural: true } : {}
171
179
  });
172
180
  }
173
181
  }
@@ -207,10 +215,11 @@ async function loadPagePartsFromConfig(configPath, artifacts) {
207
215
  }
208
216
  const headlessInstanceComponents = [];
209
217
  for (const entry of config.instanceComponents) {
210
- const mod = await importModule(entry);
211
218
  const serveTimeContract = {
212
219
  props: entry.propNames.map((name) => ({ name }))
213
220
  };
221
+ if (entry.structural) continue;
222
+ const mod = await importModule(entry);
214
223
  headlessInstanceComponents.push({
215
224
  contractName: entry.contractName,
216
225
  compDefinition: mod[entry.exportName] ?? mod.default,
@@ -283,8 +292,7 @@ class FilesystemArtifactStore {
283
292
  this.moduleCache.set(modulePath, { module: mod, mtime: stat.mtimeMs });
284
293
  return mod;
285
294
  } catch {
286
- if (local)
287
- throw new Error(`Local module not found: ${fullPath}`);
295
+ if (local) throw new Error(`Local module not found: ${fullPath}`);
288
296
  }
289
297
  }
290
298
  return import(modulePath);
@@ -314,8 +322,7 @@ function matchSegments(routeSegments, urlSegments) {
314
322
  }
315
323
  urlIdx++;
316
324
  } else if (seg.type === "param") {
317
- if (urlIdx >= urlSegments.length)
318
- return void 0;
325
+ if (urlIdx >= urlSegments.length) return void 0;
319
326
  params[seg.value] = urlSegments[urlIdx];
320
327
  urlIdx++;
321
328
  } else if (seg.type === "optional") {
@@ -324,8 +331,7 @@ function matchSegments(routeSegments, urlSegments) {
324
331
  urlIdx++;
325
332
  }
326
333
  } else if (seg.type === "catchAll") {
327
- if (urlIdx >= urlSegments.length)
328
- return void 0;
334
+ if (urlIdx >= urlSegments.length) return void 0;
329
335
  params[seg.value] = urlSegments.slice(urlIdx).join("/");
330
336
  urlIdx = urlSegments.length;
331
337
  } else if (seg.type === "optionalCatchAll") {
@@ -335,8 +341,7 @@ function matchSegments(routeSegments, urlSegments) {
335
341
  }
336
342
  }
337
343
  }
338
- if (urlIdx !== urlSegments.length)
339
- return void 0;
344
+ if (urlIdx !== urlSegments.length) return void 0;
340
345
  return params;
341
346
  }
342
347
  function findInstance(route, params) {
@@ -347,10 +352,8 @@ function findInstance(route, params) {
347
352
  for (const name of paramNames) {
348
353
  const urlVal = params[name];
349
354
  const instVal = instance.params[name];
350
- if (urlVal === void 0 && instVal === void 0)
351
- continue;
352
- if (urlVal !== instVal)
353
- return false;
355
+ if (urlVal === void 0 && instVal === void 0) continue;
356
+ if (urlVal !== instVal) return false;
354
357
  }
355
358
  return true;
356
359
  });
@@ -366,8 +369,7 @@ const pagePartsCache = /* @__PURE__ */ new Map();
366
369
  async function getPageParts(route, artifacts, cachePath) {
367
370
  const cacheKey = route.pattern;
368
371
  const cached = pagePartsCache.get(cacheKey);
369
- if (cached)
370
- return cached;
372
+ if (cached) return cached;
371
373
  const routeDir = path.dirname(cachePath);
372
374
  const configRelPath = path.join(routeDir, "page-parts.json");
373
375
  const parts = await loadPagePartsFromConfig(configRelPath, artifacts);
@@ -424,19 +426,27 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
424
426
  fastViewState,
425
427
  route.trackByMap || {}
426
428
  );
429
+ fullViewState.__jay = {
430
+ params: match.params,
431
+ url: { path: requestUrl.pathname }
432
+ };
427
433
  const headTagSources = [];
428
434
  const slowHeadTags = cf.__slowHeadTags;
429
- if (slowHeadTags)
430
- headTagSources.push(...slowHeadTags);
431
- if (fastResult.headTags)
432
- headTagSources.push(fastResult.headTags);
435
+ if (slowHeadTags) headTagSources.push(...slowHeadTags);
436
+ if (fastResult.headTags) headTagSources.push(fastResult.headTags);
433
437
  const templateHeadTags = headMetaToHeadTags(route.headMeta, fullViewState);
434
- if (templateHeadTags.length > 0)
435
- headTagSources.push(templateHeadTags);
438
+ if (templateHeadTags.length > 0) headTagSources.push(templateHeadTags);
436
439
  const headTags = headTagSources.length > 0 ? mergeHeadTags(headTagSources) : [];
437
440
  const hasCustomTitle = headTags.some((t) => t.tag?.toLowerCase() === "title");
438
441
  const titleTag = hasCustomTitle ? "" : " <title>Vite + TS</title>\n";
439
- const headTagsHtml = headTags.length > 0 ? serializeHeadTags(headTags) + "\n" : "";
442
+ const preconnectTags = headTags.filter(
443
+ (t) => t.tag === "link" && t.attrs?.rel === "preconnect"
444
+ );
445
+ const otherHeadTags = headTags.filter(
446
+ (t) => !(t.tag === "link" && t.attrs?.rel === "preconnect")
447
+ );
448
+ const preconnectHtml = preconnectTags.length > 0 ? serializeHeadTags(preconnectTags) + "\n" : "";
449
+ const headTagsHtml = otherHeadTags.length > 0 ? serializeHeadTags(otherHeadTags) + "\n" : "";
440
450
  const serverElementPath = route.serverElementPath || instance.serverElementPath;
441
451
  const tLoadStart = Date.now();
442
452
  const serverElement = await artifacts.loadServerElement(serverElementPath);
@@ -463,7 +473,7 @@ async function fetchPageRequest(match, manifest, requestUrl, artifacts, staticBa
463
473
  <head>
464
474
  <meta charset="UTF-8" />
465
475
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
466
- ${titleTag}${[cssPreload, cssImportPreloads].filter(Boolean).map((l) => l + "\n").join(
476
+ ${titleTag}${[preconnectHtml, cssPreload, cssImportPreloads].filter(Boolean).map((l) => l + "\n").join(
467
477
  ""
468
478
  )} <script type="importmap">${JSON.stringify({ imports: importMap })}<\/script>
469
479
  ${headParts}
@@ -485,8 +495,7 @@ ${headParts}
485
495
  const tSsr = Date.now();
486
496
  write("</div>");
487
497
  const asyncScripts = (await Promise.all(asyncPromises)).filter((s) => s).join("");
488
- if (asyncScripts)
489
- write(asyncScripts);
498
+ if (asyncScripts) write(asyncScripts);
490
499
  const clientInitData = getClientInitData();
491
500
  const initArgs = route.routeClientBundlePath ? `${JSON.stringify(fullSlowViewState)}, ${JSON.stringify(fastViewState)}, ${JSON.stringify(fastCarryForward)}, ${JSON.stringify(clientInitData)}` : `${JSON.stringify(fastViewState)}, ${JSON.stringify(fastCarryForward)}, ${JSON.stringify(clientInitData)}`;
492
501
  const tTotal = Date.now() - t0;
@@ -494,7 +503,6 @@ ${headParts}
494
503
  const serverTiming = {
495
504
  cache: tCache,
496
505
  parts: tParts,
497
- data: tData - t0,
498
506
  fast: tFast - tData,
499
507
  load: tLoadMs,
500
508
  ssr: tSsr - tSsrStart,
@@ -673,8 +681,7 @@ async function registerActionsFromModules(modules, registry = actionRegistry) {
673
681
  logger.info(`[Server] Registered ${count} actions from pre-imported modules`);
674
682
  }
675
683
  function getStatusCode(code, isActionError) {
676
- if (isActionError)
677
- return 422;
684
+ if (isActionError) return 422;
678
685
  switch (code) {
679
686
  case "ACTION_NOT_FOUND":
680
687
  return 404;
@@ -709,8 +716,7 @@ async function fetchStaticFile(pathname, frontendDir) {
709
716
  const normalizedBase = path.resolve(frontendDir);
710
717
  for (const candidate of [path.join(frontendDir, pathname)]) {
711
718
  const normalizedFile = path.resolve(candidate);
712
- if (!normalizedFile.startsWith(normalizedBase))
713
- continue;
719
+ if (!normalizedFile.startsWith(normalizedBase)) continue;
714
720
  try {
715
721
  const content = await fs.readFile(candidate);
716
722
  const ext = path.extname(candidate);
@@ -736,8 +742,7 @@ async function initializeServicesFromModules(plugins, label) {
736
742
  if (plugin.init?._serverInit) {
737
743
  logger.info(`[${label}] Running plugin init: ${plugin.name}`);
738
744
  const data = await plugin.init._serverInit();
739
- if (data)
740
- setClientInitData(plugin.name, data);
745
+ if (data) setClientInitData(plugin.name, data);
741
746
  }
742
747
  } catch (err) {
743
748
  logger.warn(`[${label}] Plugin init failed: ${plugin.name}: ${err.message}`);
@@ -772,8 +777,7 @@ async function initializeServices(buildDir, projectRoot, label) {
772
777
  if (init?._serverInit) {
773
778
  logger.info(`[${label}] Running plugin init: ${pluginInit.name}`);
774
779
  const data = await init._serverInit();
775
- if (data)
776
- setClientInitData(pluginInit.name, data);
780
+ if (data) setClientInitData(pluginInit.name, data);
777
781
  }
778
782
  } catch (err) {
779
783
  logger.warn(`[${label}] Plugin init failed: ${pluginInit.name}: ${err.message}`);
@@ -788,22 +792,21 @@ async function initializeServices(buildDir, projectRoot, label) {
788
792
  if (init?._serverInit) {
789
793
  logger.info(`[${label}] Running server init...`);
790
794
  const data = await init._serverInit();
791
- if (data)
792
- setClientInitData("project", data);
795
+ if (data) setClientInitData("project", data);
793
796
  }
794
797
  } catch {
795
798
  }
796
799
  }
797
800
  export {
798
801
  FilesystemArtifactStore as F,
799
- fetchActionRequest as a,
800
- registerActionsFromModules as b,
801
- fetchStaticFile as c,
802
- initializeServices as d,
803
- initializeServicesFromModules as e,
804
- fetchPageRequest as f,
802
+ fetchPageRequest as a,
803
+ fetchStaticFile as b,
804
+ initializeServicesFromModules as c,
805
+ isActionRequest as d,
806
+ registerActionsFromModules as e,
807
+ fetchActionRequest as f,
805
808
  buildPagePartsConfig as g,
806
- isActionRequest as i,
809
+ initializeServices as i,
807
810
  loadProductionPageParts as l,
808
811
  matchRequest as m,
809
812
  registerActionsFromManifest as r
@@ -36,6 +36,8 @@ interface RouteEntry {
36
36
  instances: InstanceEntry[];
37
37
  isPlugin?: boolean;
38
38
  pluginName?: string;
39
+ /** When true, dev-server tooling route (future: excluded from production builds). */
40
+ devOnly?: boolean;
39
41
  }
40
42
  interface InstanceEntry {
41
43
  params: Record<string, string>;
@@ -85,6 +87,7 @@ interface PagePartsConfigEntry {
85
87
  modulePath: string;
86
88
  exportName: string;
87
89
  source: 'npm' | 'local';
90
+ structural?: boolean;
88
91
  }
89
92
  interface PagePartsConfig {
90
93
  parts: Array<PagePartsConfigEntry & {
@@ -180,4 +183,4 @@ interface PreImportedPlugin {
180
183
  declare function initializeServicesFromModules(plugins: PreImportedPlugin[], label: string): Promise<void>;
181
184
  declare function initializeServices(buildDir: string, projectRoot: string, label: string): Promise<void>;
182
185
 
183
- export { type ActionEntry as A, type BuildOptions as B, type CacheEntry as C, FilesystemArtifactStore as F, type InstanceEntry as I, type MatchResult as M, type PluginEntry as P, type RouteManifest as R, type ServerElementModule as S, type RouteEntry as a, type RouteSegment as b, type BuildMetadata as c, type PageModule as d, type ArtifactStore as e, fetchPageRequest as f, fetchActionRequest as g, registerActionsFromModules as h, isActionRequest as i, fetchStaticFile as j, initializeServices as k, initializeServicesFromModules as l, matchRequest as m, type PreImportedPlugin as n, registerActionsFromManifest as r };
186
+ export { type ActionEntry as A, type BuildOptions as B, type CacheEntry as C, FilesystemArtifactStore as F, type InstanceEntry as I, type MatchResult as M, type PageModule as P, type RouteManifest as R, type ServerElementModule as S, type RouteEntry as a, type ArtifactStore as b, type BuildMetadata as c, type PluginEntry as d, type PreImportedPlugin as e, type RouteSegment as f, fetchActionRequest as g, fetchPageRequest as h, fetchStaticFile as i, initializeServices as j, initializeServicesFromModules as k, isActionRequest as l, matchRequest as m, registerActionsFromModules as n, registerActionsFromManifest as r };
@@ -1,4 +1,4 @@
1
- export { e as ArtifactStore, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, n as PreImportedPlugin, a as RouteEntry, R as RouteManifest, S as ServerElementModule, g as fetchActionRequest, f as fetchPageRequest, j as fetchStaticFile, k as initializeServices, l as initializeServicesFromModules, i as isActionRequest, m as matchRequest, r as registerActionsFromManifest, h as registerActionsFromModules } from './serve-index-lg6huJYm.js';
1
+ export { b as ArtifactStore, C as CacheEntry, F as FilesystemArtifactStore, I as InstanceEntry, M as MatchResult, e as PreImportedPlugin, a as RouteEntry, R as RouteManifest, S as ServerElementModule, g as fetchActionRequest, h as fetchPageRequest, i as fetchStaticFile, j as initializeServices, k as initializeServicesFromModules, l as isActionRequest, m as matchRequest, r as registerActionsFromManifest, n as registerActionsFromModules } from './serve-index-Ccw8CNDg.js';
2
2
  import '@jay-framework/ssr-runtime';
3
3
  import '@jay-framework/compiler-shared';
4
4
  import '@jay-framework/stack-server-runtime';
@@ -1,23 +1,13 @@
1
- import { F, a, f, c, d, e, i, m, r, b } from "./init-services-zmGSxSck.js";
2
- import "node:fs/promises";
3
- import "node:path";
4
- import "@jay-framework/stack-server-runtime";
5
- import "@jay-framework/view-state-merge";
6
- import "@jay-framework/ssr-runtime";
7
- import "node:module";
8
- import "@jay-framework/logger";
9
- import "@jay-framework/compiler-jay-html";
10
- import "@jay-framework/compiler-shared";
11
- import "@jay-framework/fullstack-component";
1
+ import { F, f, a, b, i, c, d, m, r, e } from "./init-services-BVEuyIVG.js";
12
2
  export {
13
3
  F as FilesystemArtifactStore,
14
- a as fetchActionRequest,
15
- f as fetchPageRequest,
16
- c as fetchStaticFile,
17
- d as initializeServices,
18
- e as initializeServicesFromModules,
19
- i as isActionRequest,
4
+ f as fetchActionRequest,
5
+ a as fetchPageRequest,
6
+ b as fetchStaticFile,
7
+ i as initializeServices,
8
+ c as initializeServicesFromModules,
9
+ d as isActionRequest,
20
10
  m as matchRequest,
21
11
  r as registerActionsFromManifest,
22
- b as registerActionsFromModules
12
+ e as registerActionsFromModules
23
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/production-server",
3
- "version": "0.22.2",
3
+ "version": "0.23.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,20 +30,20 @@
30
30
  "test:watch": "vitest"
31
31
  },
32
32
  "dependencies": {
33
- "@jay-framework/compiler-jay-html": "^0.22.2",
34
- "@jay-framework/compiler-jay-stack": "^0.22.2",
35
- "@jay-framework/compiler-shared": "^0.22.2",
36
- "@jay-framework/fullstack-component": "^0.22.2",
37
- "@jay-framework/logger": "^0.22.2",
38
- "@jay-framework/ssr-runtime": "^0.22.2",
39
- "@jay-framework/stack-route-scanner": "^0.22.2",
40
- "@jay-framework/stack-server-runtime": "^0.22.2",
41
- "@jay-framework/view-state-merge": "^0.22.2",
42
- "@jay-framework/vite-plugin": "^0.22.2",
33
+ "@jay-framework/compiler-jay-html": "^0.23.1",
34
+ "@jay-framework/compiler-jay-stack": "^0.23.1",
35
+ "@jay-framework/compiler-shared": "^0.23.1",
36
+ "@jay-framework/fullstack-component": "^0.23.1",
37
+ "@jay-framework/logger": "^0.23.1",
38
+ "@jay-framework/ssr-runtime": "^0.23.1",
39
+ "@jay-framework/stack-route-scanner": "^0.23.1",
40
+ "@jay-framework/stack-server-runtime": "^0.23.1",
41
+ "@jay-framework/view-state-merge": "^0.23.1",
42
+ "@jay-framework/vite-plugin": "^0.23.1",
43
43
  "vite": "^5.0.11"
44
44
  },
45
45
  "devDependencies": {
46
- "@jay-framework/dev-environment": "^0.22.2",
46
+ "@jay-framework/dev-environment": "^0.23.1",
47
47
  "@types/node": "^22.15.21",
48
48
  "rimraf": "^5.0.5",
49
49
  "tsup": "^8.0.1",