@barefootjs/hono 0.4.0 → 0.5.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.
@@ -44,6 +44,7 @@ export declare class HonoAdapter extends JsxAdapter implements IRNodeEmitter<Hon
44
44
  name: string;
45
45
  extension: string;
46
46
  clientShimSource: string;
47
+ importMapInjection: 'component';
47
48
  acceptsTemplateCall: () => boolean;
48
49
  protected jsxConfig: JsxAdapterConfig;
49
50
  private options;
@@ -77,7 +78,7 @@ export declare class HonoAdapter extends JsxAdapter implements IRNodeEmitter<Hon
77
78
  emitElement(node: IRElement, ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string;
78
79
  emitText(node: IRText): string;
79
80
  emitExpression(node: IRExpression): string;
80
- emitConditional(node: IRConditional, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string;
81
+ emitConditional(node: IRConditional, ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string;
81
82
  emitLoop(node: IRLoop, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string;
82
83
  emitComponent(node: IRComponent, ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string;
83
84
  emitFragment(node: IRFragment, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string;
@@ -90,7 +91,14 @@ export declare class HonoAdapter extends JsxAdapter implements IRNodeEmitter<Hon
90
91
  }): string;
91
92
  private renderText;
92
93
  renderExpression(expr: IRExpression): string;
93
- renderConditional(cond: IRConditional): string;
94
+ renderConditional(cond: IRConditional, ctx?: HonoRenderCtx): string;
95
+ /**
96
+ * Like the base `renderNodeRaw`, but threads a render ctx through to
97
+ * `renderNode` so a conditional branch can mark its element as a loop item
98
+ * root (#1665). The `null` / `undefined` expression branch carries no
99
+ * element, so it short-circuits exactly as the base helper does.
100
+ */
101
+ private renderNodeRawCtx;
94
102
  private wrapWithCondMarker;
95
103
  renderLoop(loop: IRLoop): string;
96
104
  private renderChildrenInLoop;
@@ -1 +1 @@
1
- {"version":3,"file":"hono-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/hono-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hono-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/hono-adapter.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,7 @@ class HonoAdapter extends JsxAdapter {
21
21
  name = "hono";
22
22
  extension = ".tsx";
23
23
  clientShimSource = "@barefootjs/hono/client-shim";
24
+ importMapInjection = "component";
24
25
  acceptsTemplateCall = () => true;
25
26
  jsxConfig = { preserveTypes: true };
26
27
  options;
@@ -259,9 +260,13 @@ export default ${this.componentName}` : "";
259
260
  }
260
261
  fullPropsDestructure = `{ ${parts.join(", ")} }`;
261
262
  }
263
+ const hasRequiredProps = ir.metadata.propsParams.some((p) => !p.optional && p.defaultValue === undefined && !p.isRest);
264
+ const wantsNoArgDefault = propsObjectName ? !propsTypeName : !hasRequiredProps;
265
+ const propsTypeExpr = typeAnnotation.replace(/^:\s*/, "");
266
+ const noArgDefault = wantsNoArgDefault ? ` = {} as ${propsTypeExpr}` : "";
262
267
  const lines = [];
263
268
  const exportPrefix = ir.metadata.isExported === false ? "" : "export ";
264
- lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}) {`);
269
+ lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}${noArgDefault}) {`);
265
270
  if (propsExtraction) {
266
271
  lines.push(propsExtraction);
267
272
  }
@@ -312,8 +317,8 @@ export default ${this.componentName}` : "";
312
317
  emitExpression(node) {
313
318
  return this.renderExpression(node);
314
319
  }
315
- emitConditional(node, _ctx, _emit) {
316
- return this.renderConditional(node);
320
+ emitConditional(node, ctx, _emit) {
321
+ return this.renderConditional(node, ctx);
317
322
  }
318
323
  emitLoop(node, _ctx, _emit) {
319
324
  return this.renderLoop(node);
@@ -400,12 +405,13 @@ export default ${this.componentName}` : "";
400
405
  }
401
406
  return `{${expr.expr}}`;
402
407
  }
403
- renderConditional(cond) {
408
+ renderConditional(cond, ctx) {
404
409
  if (cond.clientOnly && cond.slotId) {
405
410
  return `{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}`;
406
411
  }
407
- const whenTrue = this.renderNodeRaw(cond.whenTrue);
408
- let whenFalse = this.renderNodeRaw(cond.whenFalse);
412
+ const branchCtx = ctx?.isLoopItemRoot ? { isLoopItemRoot: true } : undefined;
413
+ const whenTrue = this.renderNodeRawCtx(cond.whenTrue, branchCtx);
414
+ let whenFalse = this.renderNodeRawCtx(cond.whenFalse, branchCtx);
409
415
  if (!whenFalse || whenFalse === "" || whenFalse === "null") {
410
416
  whenFalse = "null";
411
417
  }
@@ -416,6 +422,14 @@ export default ${this.componentName}` : "";
416
422
  }
417
423
  return `{${cond.condition} ? ${whenTrue} : ${whenFalse}}`;
418
424
  }
425
+ renderNodeRawCtx(node, ctx) {
426
+ if (node.type === "expression") {
427
+ if (node.expr === "null" || node.expr === "undefined")
428
+ return "null";
429
+ return node.expr;
430
+ }
431
+ return this.renderNode(node, ctx);
432
+ }
419
433
  wrapWithCondMarker(node, content, condId) {
420
434
  if (node.type === "component") {
421
435
  return `<>{bfComment("cond-start:${condId}")}${content}{bfComment("cond-end:${condId}")}</>`;
@@ -446,6 +460,8 @@ export default ${this.componentName}` : "";
446
460
  let safeChildren = children.startsWith("{") ? `<>${children}</>` : children;
447
461
  if (loop.bodyIsMultiRoot) {
448
462
  safeChildren = `<>{bfComment('bf-loop-i')}${children}</>`;
463
+ } else if (loop.bodyIsItemConditional && loop.key) {
464
+ safeChildren = `<>{bfComment('loop-i:' + String(${loop.key}))}${children}</>`;
449
465
  }
450
466
  let chainedArray = applyHonoLoopChain(loop);
451
467
  const iterMethod = loop.method ?? "map";
package/dist/app.d.ts CHANGED
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import type { MiddlewareHandler } from 'hono';
29
29
  import type { HtmlEscapedString } from 'hono/utils/html';
30
+ import { type ImportMapManifest } from '@barefootjs/jsx/import-map';
30
31
  /**
31
32
  * Build manifest shape produced by `bf build`. Each compiled
32
33
  * component is keyed by its manifest name; `__barefoot__` is the
@@ -63,22 +64,6 @@ export interface BarefootBuildManifest {
63
64
  */
64
65
  export declare function manifestToScriptUrls(manifest: BarefootBuildManifest, base: string): string[];
65
66
  export declare function relPathFromComponentsBase(p: string): string;
66
- /**
67
- * Shape of `barefoot-externals.json`, written by `bf build` when
68
- * `externals` / `bundleEntries` are configured. Only the fields
69
- * `BfImportMap` consumes are typed here; the build also emits an
70
- * `externals` array (the `--external` list) which the importmap
71
- * doesn't need. Fields are optional so a partial/hand-written
72
- * manifest still type-checks. See issue #1639.
73
- */
74
- export interface BarefootExternalsManifest {
75
- /** Entries for the `<script type="importmap">`. */
76
- importmap?: {
77
- imports?: Record<string, string>;
78
- };
79
- /** URLs to emit as `<link rel="modulepreload">`. */
80
- preloads?: string[];
81
- }
82
67
  export interface BfImportMapProps {
83
68
  /** Base URL where the runtime + component bundles are served. */
84
69
  base: string;
@@ -89,8 +74,12 @@ export interface BfImportMapProps {
89
74
  * configured externals (e.g. `zod`, `@barefootjs/form`) resolve in
90
75
  * the browser. When omitted, only the `@barefootjs/client*`
91
76
  * mappings are emitted — the pre-#1639 behavior.
77
+ *
78
+ * Typed with the shared {@link ImportMapManifest} from `@barefootjs/jsx`,
79
+ * so the component and the `bf build` snippet path describe the manifest
80
+ * with one type.
92
81
  */
93
- externals?: BarefootExternalsManifest;
82
+ externals?: ImportMapManifest;
94
83
  /**
95
84
  * Whether to also emit `<link rel="modulepreload">` for the
96
85
  * manifest's `preloads`. Defaults to `true`; set `false` to emit
@@ -105,6 +94,15 @@ export interface BfImportMapProps {
105
94
  * passed via the `externals` prop. Also emits `<link rel="modulepreload">`
106
95
  * for the manifest's `preloads` unless `preload` is `false`. Place in
107
96
  * `<head>`.
97
+ *
98
+ * The merge of the `@barefootjs/client*` defaults (synthesized from `base`
99
+ * for prop-less / hand-written manifests) is Hono-specific; the actual HTML
100
+ * rendering — importmap JSON escaping, `<link rel="modulepreload">`
101
+ * emission with `crossorigin` (#1648) — is delegated to the shared
102
+ * `renderImportMapHtml` so this path can never drift from the static
103
+ * `barefoot-importmap.html` snippet `bf build` emits for template-string
104
+ * adapters (#1644). Imported from the `@barefootjs/jsx/import-map` subpath,
105
+ * a zero-dependency module, to keep this runtime file free of the compiler.
108
106
  */
109
107
  export declare function BfImportMap(props: BfImportMapProps): HtmlEscapedString | Promise<HtmlEscapedString>;
110
108
  export interface BfScriptsProps {
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAQxD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACpC,CAAC,aAAa,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,SAAS,CAAA;CAChF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,qBAAqB,EAC/B,IAAI,EAAE,MAAM,GACX,MAAM,EAAE,CAWV;AAED,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAID;;;;;;;GAOG;AACH,MAAM,WAAW,yBAAyB;IACxC,mDAAmD;IACnD,SAAS,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAA;IAChD,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAA;IACrC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuBnG;AAOD,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,QAAQ,EAAE,qBAAqB,CAAA;CAChC;AAQD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAa/F;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,GAAE,gBAAqB,GAAG,iBAAiB,GAAG,IAAI,CAelF;AAID,MAAM,WAAW,wBAAwB;IACvC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,iBAAiB,CAanF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAE7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAIxD,OAAO,EAAuB,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAA;AAOxF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,qBAAqB;IACpC,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACpC,CAAC,aAAa,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,SAAS,CAAA;CAChF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,qBAAqB,EAC/B,IAAI,EAAE,MAAM,GACX,MAAM,EAAE,CAWV;AAED,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAID,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAA;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAanG;AAED,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,QAAQ,EAAE,qBAAqB,CAAA;CAChC;AAQD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAa/F;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,GAAE,gBAAqB,GAAG,iBAAiB,GAAG,IAAI,CAelF;AAID,MAAM,WAAW,wBAAwB;IACvC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,iBAAiB,CAanF"}
package/dist/app.js CHANGED
@@ -64,6 +64,7 @@ data: ${BOOT_ID}
64
64
  // src/app.ts
65
65
  import { html, raw } from "hono/html";
66
66
  import { useRequestContext } from "hono/jsx-renderer";
67
+ import { renderImportMapHtml } from "@barefootjs/jsx/import-map";
67
68
  var DEV_RELOAD_ENDPOINT_KEY = "bfDevReloadEndpoint";
68
69
  function manifestToScriptUrls(manifest, base) {
69
70
  const out = [];
@@ -89,13 +90,8 @@ function BfImportMap(props) {
89
90
  "@barefootjs/client/runtime": `${base}/barefoot.js`,
90
91
  ...props.externals?.importmap?.imports ?? {}
91
92
  };
92
- const json = JSON.stringify({ imports });
93
93
  const preloads = props.preload === false ? [] : props.externals?.preloads ?? [];
94
- const links = preloads.map((href) => `<link rel="modulepreload" href="${escapeAttr(href)}" crossorigin>`).join("");
95
- return html`<script type="importmap">${raw(json)}</script>${raw(links)}`;
96
- }
97
- function escapeAttr(value) {
98
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
94
+ return html`${raw(renderImportMapHtml({ importmap: { imports }, preloads }))}`;
99
95
  }
100
96
  var __bfEmptyManifestWarned = false;
101
97
  function BfScripts(props) {
package/dist/build.js CHANGED
@@ -21,6 +21,7 @@ class HonoAdapter extends JsxAdapter {
21
21
  name = "hono";
22
22
  extension = ".tsx";
23
23
  clientShimSource = "@barefootjs/hono/client-shim";
24
+ importMapInjection = "component";
24
25
  acceptsTemplateCall = () => true;
25
26
  jsxConfig = { preserveTypes: true };
26
27
  options;
@@ -259,9 +260,13 @@ export default ${this.componentName}` : "";
259
260
  }
260
261
  fullPropsDestructure = `{ ${parts.join(", ")} }`;
261
262
  }
263
+ const hasRequiredProps = ir.metadata.propsParams.some((p) => !p.optional && p.defaultValue === undefined && !p.isRest);
264
+ const wantsNoArgDefault = propsObjectName ? !propsTypeName : !hasRequiredProps;
265
+ const propsTypeExpr = typeAnnotation.replace(/^:\s*/, "");
266
+ const noArgDefault = wantsNoArgDefault ? ` = {} as ${propsTypeExpr}` : "";
262
267
  const lines = [];
263
268
  const exportPrefix = ir.metadata.isExported === false ? "" : "export ";
264
- lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}) {`);
269
+ lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}${noArgDefault}) {`);
265
270
  if (propsExtraction) {
266
271
  lines.push(propsExtraction);
267
272
  }
@@ -312,8 +317,8 @@ export default ${this.componentName}` : "";
312
317
  emitExpression(node) {
313
318
  return this.renderExpression(node);
314
319
  }
315
- emitConditional(node, _ctx, _emit) {
316
- return this.renderConditional(node);
320
+ emitConditional(node, ctx, _emit) {
321
+ return this.renderConditional(node, ctx);
317
322
  }
318
323
  emitLoop(node, _ctx, _emit) {
319
324
  return this.renderLoop(node);
@@ -400,12 +405,13 @@ export default ${this.componentName}` : "";
400
405
  }
401
406
  return `{${expr.expr}}`;
402
407
  }
403
- renderConditional(cond) {
408
+ renderConditional(cond, ctx) {
404
409
  if (cond.clientOnly && cond.slotId) {
405
410
  return `{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}`;
406
411
  }
407
- const whenTrue = this.renderNodeRaw(cond.whenTrue);
408
- let whenFalse = this.renderNodeRaw(cond.whenFalse);
412
+ const branchCtx = ctx?.isLoopItemRoot ? { isLoopItemRoot: true } : undefined;
413
+ const whenTrue = this.renderNodeRawCtx(cond.whenTrue, branchCtx);
414
+ let whenFalse = this.renderNodeRawCtx(cond.whenFalse, branchCtx);
409
415
  if (!whenFalse || whenFalse === "" || whenFalse === "null") {
410
416
  whenFalse = "null";
411
417
  }
@@ -416,6 +422,14 @@ export default ${this.componentName}` : "";
416
422
  }
417
423
  return `{${cond.condition} ? ${whenTrue} : ${whenFalse}}`;
418
424
  }
425
+ renderNodeRawCtx(node, ctx) {
426
+ if (node.type === "expression") {
427
+ if (node.expr === "null" || node.expr === "undefined")
428
+ return "null";
429
+ return node.expr;
430
+ }
431
+ return this.renderNode(node, ctx);
432
+ }
419
433
  wrapWithCondMarker(node, content, condId) {
420
434
  if (node.type === "component") {
421
435
  return `<>{bfComment("cond-start:${condId}")}${content}{bfComment("cond-end:${condId}")}</>`;
@@ -446,6 +460,8 @@ export default ${this.componentName}` : "";
446
460
  let safeChildren = children.startsWith("{") ? `<>${children}</>` : children;
447
461
  if (loop.bodyIsMultiRoot) {
448
462
  safeChildren = `<>{bfComment('bf-loop-i')}${children}</>`;
463
+ } else if (loop.bodyIsItemConditional && loop.key) {
464
+ safeChildren = `<>{bfComment('loop-i:' + String(${loop.key}))}${children}</>`;
449
465
  }
450
466
  let chainedArray = applyHonoLoopChain(loop);
451
467
  const iterMethod = loop.method ?? "map";
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ class HonoAdapter extends JsxAdapter {
21
21
  name = "hono";
22
22
  extension = ".tsx";
23
23
  clientShimSource = "@barefootjs/hono/client-shim";
24
+ importMapInjection = "component";
24
25
  acceptsTemplateCall = () => true;
25
26
  jsxConfig = { preserveTypes: true };
26
27
  options;
@@ -259,9 +260,13 @@ export default ${this.componentName}` : "";
259
260
  }
260
261
  fullPropsDestructure = `{ ${parts.join(", ")} }`;
261
262
  }
263
+ const hasRequiredProps = ir.metadata.propsParams.some((p) => !p.optional && p.defaultValue === undefined && !p.isRest);
264
+ const wantsNoArgDefault = propsObjectName ? !propsTypeName : !hasRequiredProps;
265
+ const propsTypeExpr = typeAnnotation.replace(/^:\s*/, "");
266
+ const noArgDefault = wantsNoArgDefault ? ` = {} as ${propsTypeExpr}` : "";
262
267
  const lines = [];
263
268
  const exportPrefix = ir.metadata.isExported === false ? "" : "export ";
264
- lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}) {`);
269
+ lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}${noArgDefault}) {`);
265
270
  if (propsExtraction) {
266
271
  lines.push(propsExtraction);
267
272
  }
@@ -312,8 +317,8 @@ export default ${this.componentName}` : "";
312
317
  emitExpression(node) {
313
318
  return this.renderExpression(node);
314
319
  }
315
- emitConditional(node, _ctx, _emit) {
316
- return this.renderConditional(node);
320
+ emitConditional(node, ctx, _emit) {
321
+ return this.renderConditional(node, ctx);
317
322
  }
318
323
  emitLoop(node, _ctx, _emit) {
319
324
  return this.renderLoop(node);
@@ -400,12 +405,13 @@ export default ${this.componentName}` : "";
400
405
  }
401
406
  return `{${expr.expr}}`;
402
407
  }
403
- renderConditional(cond) {
408
+ renderConditional(cond, ctx) {
404
409
  if (cond.clientOnly && cond.slotId) {
405
410
  return `{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}`;
406
411
  }
407
- const whenTrue = this.renderNodeRaw(cond.whenTrue);
408
- let whenFalse = this.renderNodeRaw(cond.whenFalse);
412
+ const branchCtx = ctx?.isLoopItemRoot ? { isLoopItemRoot: true } : undefined;
413
+ const whenTrue = this.renderNodeRawCtx(cond.whenTrue, branchCtx);
414
+ let whenFalse = this.renderNodeRawCtx(cond.whenFalse, branchCtx);
409
415
  if (!whenFalse || whenFalse === "" || whenFalse === "null") {
410
416
  whenFalse = "null";
411
417
  }
@@ -416,6 +422,14 @@ export default ${this.componentName}` : "";
416
422
  }
417
423
  return `{${cond.condition} ? ${whenTrue} : ${whenFalse}}`;
418
424
  }
425
+ renderNodeRawCtx(node, ctx) {
426
+ if (node.type === "expression") {
427
+ if (node.expr === "null" || node.expr === "undefined")
428
+ return "null";
429
+ return node.expr;
430
+ }
431
+ return this.renderNode(node, ctx);
432
+ }
419
433
  wrapWithCondMarker(node, content, condId) {
420
434
  if (node.type === "component") {
421
435
  return `<>{bfComment("cond-start:${condId}")}${content}{bfComment("cond-end:${condId}")}</>`;
@@ -446,6 +460,8 @@ export default ${this.componentName}` : "";
446
460
  let safeChildren = children.startsWith("{") ? `<>${children}</>` : children;
447
461
  if (loop.bodyIsMultiRoot) {
448
462
  safeChildren = `<>{bfComment('bf-loop-i')}${children}</>`;
463
+ } else if (loop.bodyIsItemConditional && loop.key) {
464
+ safeChildren = `<>{bfComment('loop-i:' + String(${loop.key}))}${children}</>`;
449
465
  }
450
466
  let chainedArray = applyHonoLoopChain(loop);
451
467
  const iterMethod = loop.method ?? "map";
package/dist/scripts.js CHANGED
@@ -64,6 +64,7 @@ data: ${BOOT_ID}
64
64
  // src/app.ts
65
65
  import { html, raw } from "hono/html";
66
66
  import { useRequestContext } from "hono/jsx-renderer";
67
+ import { renderImportMapHtml } from "@barefootjs/jsx/import-map";
67
68
  var DEV_RELOAD_ENDPOINT_KEY = "bfDevReloadEndpoint";
68
69
  function manifestToScriptUrls(manifest, base) {
69
70
  const out = [];
@@ -89,13 +90,8 @@ function BfImportMap(props) {
89
90
  "@barefootjs/client/runtime": `${base}/barefoot.js`,
90
91
  ...props.externals?.importmap?.imports ?? {}
91
92
  };
92
- const json = JSON.stringify({ imports });
93
93
  const preloads = props.preload === false ? [] : props.externals?.preloads ?? [];
94
- const links = preloads.map((href) => `<link rel="modulepreload" href="${escapeAttr(href)}" crossorigin>`).join("");
95
- return html`<script type="importmap">${raw(json)}</script>${raw(links)}`;
96
- }
97
- function escapeAttr(value) {
98
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
94
+ return html`${raw(renderImportMapHtml({ importmap: { imports }, preloads }))}`;
99
95
  }
100
96
  var __bfEmptyManifestWarned = false;
101
97
  function BfScripts(props) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/hono",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Hono integration for BarefootJS",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -7,7 +7,8 @@
7
7
  * when no externals are passed.
8
8
  */
9
9
  import { describe, test, expect } from 'bun:test'
10
- import { BfImportMap, type BarefootExternalsManifest } from '../app'
10
+ import { BfImportMap } from '../app'
11
+ import type { ImportMapManifest } from '@barefootjs/jsx'
11
12
 
12
13
  function parseImportMap(html: string): Record<string, string> {
13
14
  const match = html.match(/<script type="importmap">(.*?)<\/script>/s)
@@ -31,7 +32,7 @@ describe('BfImportMap', () => {
31
32
  })
32
33
 
33
34
  test('merges externals importmap on top of the client defaults', () => {
34
- const externals: BarefootExternalsManifest = {
35
+ const externals: ImportMapManifest = {
35
36
  importmap: {
36
37
  imports: {
37
38
  zod: 'https://esm.sh/zod@4.4.3',
@@ -50,7 +51,7 @@ describe('BfImportMap', () => {
50
51
  })
51
52
 
52
53
  test('manifest @barefootjs/client mapping wins over the prop-derived one', () => {
53
- const externals: BarefootExternalsManifest = {
54
+ const externals: ImportMapManifest = {
54
55
  importmap: { imports: { '@barefootjs/client': '/vendor/barefoot.js' } },
55
56
  }
56
57
  const imports = parseImportMap(String(BfImportMap({ base: '/components', externals })))
@@ -58,7 +59,7 @@ describe('BfImportMap', () => {
58
59
  })
59
60
 
60
61
  test('emits modulepreload links for manifest preloads', () => {
61
- const externals: BarefootExternalsManifest = {
62
+ const externals: ImportMapManifest = {
62
63
  importmap: { imports: {} },
63
64
  preloads: ['/components/form.js', 'https://esm.sh/zod@4.4.3'],
64
65
  }
@@ -68,7 +69,7 @@ describe('BfImportMap', () => {
68
69
  })
69
70
 
70
71
  test('emits crossorigin on modulepreload so cross-origin CDN preloads are reused', () => {
71
- const externals: BarefootExternalsManifest = {
72
+ const externals: ImportMapManifest = {
72
73
  preloads: ['https://esm.sh/zod@4.4.3'],
73
74
  }
74
75
  const html = String(BfImportMap({ base: '/components', externals }))
@@ -77,7 +78,7 @@ describe('BfImportMap', () => {
77
78
  })
78
79
 
79
80
  test('preload=false suppresses modulepreload links', () => {
80
- const externals: BarefootExternalsManifest = {
81
+ const externals: ImportMapManifest = {
81
82
  preloads: ['/components/form.js'],
82
83
  }
83
84
  const html = String(BfImportMap({ base: '/components', externals, preload: false }))
@@ -87,7 +88,7 @@ describe('BfImportMap', () => {
87
88
  })
88
89
 
89
90
  test('escapes double quotes in preload hrefs', () => {
90
- const externals: BarefootExternalsManifest = {
91
+ const externals: ImportMapManifest = {
91
92
  preloads: ['/components/"onerror=alert(1).js'],
92
93
  }
93
94
  const html = String(BfImportMap({ base: '/components', externals }))
@@ -103,6 +103,9 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
103
103
  name = 'hono'
104
104
  extension = '.tsx'
105
105
  clientShimSource = '@barefootjs/hono/client-shim'
106
+ // Importmap is injected at render time by the `BfImportMap` component
107
+ // (reads `barefoot-externals.json`), so `bf build` emits no static snippet.
108
+ importMapInjection = 'component' as const
106
109
 
107
110
  // The Hono SSR runtime is JavaScript (Node / Bun / CF Workers), so any
108
111
  // synchronous JS call the user writes can be rendered as-is at template
@@ -507,11 +510,28 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
507
510
  fullPropsDestructure = `{ ${parts.join(', ')} }`
508
511
  }
509
512
 
513
+ // Default the props param to `{}` when the component has no required
514
+ // props, so a bare no-arg call (`Foo()`) doesn't crash on destructuring
515
+ // `undefined`. This makes a JSX-returning arrow hoisted from an
516
+ // object-literal value (e.g. `THEME_LOGOS[id]()`) renderable at SSR
517
+ // (#1663). `hasRequiredProps` ignores props that carry a destructuring
518
+ // default, but the declared props type may still mark that field
519
+ // required — so a bare `= {}` would fail `tsc`. Assert the default to the
520
+ // param's own annotated type (`{} as T`); the destructuring defaults
521
+ // supply the values at runtime. The SolidJS-style (`propsObjectName`)
522
+ // branch opts in whenever the annotation is satisfiable by `{} as T`.
523
+ const hasRequiredProps = ir.metadata.propsParams.some(
524
+ (p: ParamInfo) => !p.optional && p.defaultValue === undefined && !p.isRest,
525
+ )
526
+ const wantsNoArgDefault = propsObjectName ? !propsTypeName : !hasRequiredProps
527
+ const propsTypeExpr = typeAnnotation.replace(/^:\s*/, '')
528
+ const noArgDefault = wantsNoArgDefault ? ` = {} as ${propsTypeExpr}` : ''
529
+
510
530
  const lines: string[] = []
511
531
  // Module-export keyword belongs to the adapter: it knows the target language
512
532
  // and whether the source declared the component as exported.
513
533
  const exportPrefix = ir.metadata.isExported === false ? '' : 'export '
514
- lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}) {`)
534
+ lines.push(`${exportPrefix}function ${name}(${fullPropsDestructure}${typeAnnotation}${noArgDefault}) {`)
515
535
 
516
536
  // Add props extraction for SolidJS-style pattern
517
537
  if (propsExtraction) {
@@ -597,8 +617,8 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
597
617
  return this.renderExpression(node)
598
618
  }
599
619
 
600
- emitConditional(node: IRConditional, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
601
- return this.renderConditional(node)
620
+ emitConditional(node: IRConditional, ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
621
+ return this.renderConditional(node, ctx)
602
622
  }
603
623
 
604
624
  emitLoop(node: IRLoop, _ctx: HonoRenderCtx, _emit: EmitIRNode<HonoRenderCtx>): string {
@@ -718,14 +738,21 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
718
738
  return `{${expr.expr}}`
719
739
  }
720
740
 
721
- renderConditional(cond: IRConditional): string {
741
+ renderConditional(cond: IRConditional, ctx?: HonoRenderCtx): string {
722
742
  // Handle @client directive - render comment markers for client-side evaluation
723
743
  if (cond.clientOnly && cond.slotId) {
724
744
  return `{bfComment("cond-start:${cond.slotId}")}{bfComment("cond-end:${cond.slotId}")}`
725
745
  }
726
746
 
727
- const whenTrue = this.renderNodeRaw(cond.whenTrue)
728
- let whenFalse = this.renderNodeRaw(cond.whenFalse)
747
+ // A conditional that is itself a loop item root (#1665 whole-item
748
+ // conditional: `arr.map(t => cond && <li/>)`) makes its branch element the
749
+ // loop item's root, so the `data-key` that reconciliation/hydration expect
750
+ // belongs on that element — exactly like a non-conditional loop root. Pass
751
+ // the flag through so `renderElement` emits `data-key`, matching the Go /
752
+ // CSR adapters' generic `key`→`data-key` rewrite.
753
+ const branchCtx: HonoRenderCtx | undefined = ctx?.isLoopItemRoot ? { isLoopItemRoot: true } : undefined
754
+ const whenTrue = this.renderNodeRawCtx(cond.whenTrue, branchCtx)
755
+ let whenFalse = this.renderNodeRawCtx(cond.whenFalse, branchCtx)
729
756
 
730
757
  // Handle empty/null whenFalse
731
758
  if (!whenFalse || whenFalse === '' || whenFalse === 'null') {
@@ -746,6 +773,20 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
746
773
  return `{${cond.condition} ? ${whenTrue} : ${whenFalse}}`
747
774
  }
748
775
 
776
+ /**
777
+ * Like the base `renderNodeRaw`, but threads a render ctx through to
778
+ * `renderNode` so a conditional branch can mark its element as a loop item
779
+ * root (#1665). The `null` / `undefined` expression branch carries no
780
+ * element, so it short-circuits exactly as the base helper does.
781
+ */
782
+ private renderNodeRawCtx(node: IRNode, ctx?: HonoRenderCtx): string {
783
+ if (node.type === 'expression') {
784
+ if (node.expr === 'null' || node.expr === 'undefined') return 'null'
785
+ return node.expr
786
+ }
787
+ return this.renderNode(node, ctx)
788
+ }
789
+
749
790
  private wrapWithCondMarker(node: IRNode, content: string, condId: string): string {
750
791
  // Components don't reliably forward bf-c to their root element.
751
792
  // Use comment markers so insert() can find them via TreeWalker.
@@ -813,6 +854,14 @@ export class HonoAdapter extends JsxAdapter implements IRNodeEmitter<HonoRenderC
813
854
  // literal here to match the adapter's existing convention of
814
855
  // emitting comment-marker strings directly.
815
856
  safeChildren = `<>{bfComment('bf-loop-i')}${children}</>`
857
+ } else if (loop.bodyIsItemConditional && loop.key) {
858
+ // Whole-item conditional (#1665): a per-item `<!--bf-loop-i:KEY-->`
859
+ // anchor that is ALWAYS present (even when the item's conditional
860
+ // renders nothing), carrying the key so the client's
861
+ // `mapArrayAnchored` can hydrate every SSR-rendered item by its anchor.
862
+ // `bfComment(k)` emits `<!--bf-${k}-->`, so the `loop-i:` argument
863
+ // yields `<!--bf-loop-i:KEY-->`.
864
+ safeChildren = `<>{bfComment('loop-i:' + String(${loop.key}))}${children}</>`
816
865
  }
817
866
  // Apply chained `.sort()` / `.filter()` extracted to
818
867
  // `loop.sortComparator` / `loop.filterPredicate` (#1448 Tier B).
package/src/app.ts CHANGED
@@ -30,6 +30,9 @@ import type { MiddlewareHandler } from 'hono'
30
30
  import { html, raw } from 'hono/html'
31
31
  import type { HtmlEscapedString } from 'hono/utils/html'
32
32
  import { useRequestContext } from 'hono/jsx-renderer'
33
+ // Zero-dependency subpath — keeps the compiler (and its `typescript` dep) out
34
+ // of this runtime/Workers-bundled module while sharing one importmap renderer.
35
+ import { renderImportMapHtml, type ImportMapManifest } from '@barefootjs/jsx/import-map'
33
36
  import { createDevReloader } from './dev-worker'
34
37
 
35
38
  const DEV_RELOAD_ENDPOINT_KEY = 'bfDevReloadEndpoint'
@@ -88,21 +91,6 @@ export function relPathFromComponentsBase(p: string): string {
88
91
 
89
92
  // ── JSX components ─────────────────────────────────────────────────────────
90
93
 
91
- /**
92
- * Shape of `barefoot-externals.json`, written by `bf build` when
93
- * `externals` / `bundleEntries` are configured. Only the fields
94
- * `BfImportMap` consumes are typed here; the build also emits an
95
- * `externals` array (the `--external` list) which the importmap
96
- * doesn't need. Fields are optional so a partial/hand-written
97
- * manifest still type-checks. See issue #1639.
98
- */
99
- export interface BarefootExternalsManifest {
100
- /** Entries for the `<script type="importmap">`. */
101
- importmap?: { imports?: Record<string, string> }
102
- /** URLs to emit as `<link rel="modulepreload">`. */
103
- preloads?: string[]
104
- }
105
-
106
94
  export interface BfImportMapProps {
107
95
  /** Base URL where the runtime + component bundles are served. */
108
96
  base: string
@@ -113,8 +101,12 @@ export interface BfImportMapProps {
113
101
  * configured externals (e.g. `zod`, `@barefootjs/form`) resolve in
114
102
  * the browser. When omitted, only the `@barefootjs/client*`
115
103
  * mappings are emitted — the pre-#1639 behavior.
104
+ *
105
+ * Typed with the shared {@link ImportMapManifest} from `@barefootjs/jsx`,
106
+ * so the component and the `bf build` snippet path describe the manifest
107
+ * with one type.
116
108
  */
117
- externals?: BarefootExternalsManifest
109
+ externals?: ImportMapManifest
118
110
  /**
119
111
  * Whether to also emit `<link rel="modulepreload">` for the
120
112
  * manifest's `preloads`. Defaults to `true`; set `false` to emit
@@ -130,6 +122,15 @@ export interface BfImportMapProps {
130
122
  * passed via the `externals` prop. Also emits `<link rel="modulepreload">`
131
123
  * for the manifest's `preloads` unless `preload` is `false`. Place in
132
124
  * `<head>`.
125
+ *
126
+ * The merge of the `@barefootjs/client*` defaults (synthesized from `base`
127
+ * for prop-less / hand-written manifests) is Hono-specific; the actual HTML
128
+ * rendering — importmap JSON escaping, `<link rel="modulepreload">`
129
+ * emission with `crossorigin` (#1648) — is delegated to the shared
130
+ * `renderImportMapHtml` so this path can never drift from the static
131
+ * `barefoot-importmap.html` snippet `bf build` emits for template-string
132
+ * adapters (#1644). Imported from the `@barefootjs/jsx/import-map` subpath,
133
+ * a zero-dependency module, to keep this runtime file free of the compiler.
133
134
  */
134
135
  export function BfImportMap(props: BfImportMapProps): HtmlEscapedString | Promise<HtmlEscapedString> {
135
136
  const base = props.base.replace(/\/$/, '')
@@ -141,24 +142,9 @@ export function BfImportMap(props: BfImportMapProps): HtmlEscapedString | Promis
141
142
  '@barefootjs/client/runtime': `${base}/barefoot.js`,
142
143
  ...(props.externals?.importmap?.imports ?? {}),
143
144
  }
144
- const json = JSON.stringify({ imports })
145
-
146
145
  const preloads = props.preload === false ? [] : props.externals?.preloads ?? []
147
- // `crossorigin` is required so a cross-origin (CDN) preload's request
148
- // matches the actual module `import` (always a CORS fetch); without it
149
- // the browser discards the preload and re-fetches. It's harmless for
150
- // same-origin module preloads, which use the same credentials mode
151
- // whether or not the attribute is present. See issue #1648.
152
- const links = preloads
153
- .map((href) => `<link rel="modulepreload" href="${escapeAttr(href)}" crossorigin>`)
154
- .join('')
155
-
156
- return html`<script type="importmap">${raw(json)}</script>${raw(links)}`
157
- }
158
146
 
159
- /** Minimal double-quoted-attribute escaping for config-derived URLs. */
160
- function escapeAttr(value: string): string {
161
- return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
147
+ return html`${raw(renderImportMapHtml({ importmap: { imports }, preloads }))}`
162
148
  }
163
149
 
164
150
  export interface BfScriptsProps {