@jxsuite/compiler 1.4.0 → 2.0.0

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.
@@ -7,6 +7,8 @@
7
7
 
8
8
  import { dereference } from "@apidevtools/json-schema-ref-parser";
9
9
  import { collectServerEntries } from "../shared.ts";
10
+ import { localeNegotiationMiddleware } from "../site/locale-negotiation.ts";
11
+ import type { ResolvedI18n } from "../site/i18n.ts";
10
12
  import type { JxElement } from "@jxsuite/schema/types";
11
13
 
12
14
  /**
@@ -97,6 +99,7 @@ export interface SiteConnectorSpec {
97
99
  * `"cloudflare-pages"`, `"node"`, `"bun"`
98
100
  * @param {SiteMountSpec[]} [opts.mounts] - Extension server mounts, already sorted by order
99
101
  * @param {SiteConnectorSpec[]} [opts.connectors] - Connector classes used by the mounts
102
+ * @param {ResolvedI18n | null} [opts.i18n] - Locale routing; drives the `/` negotiation middleware
100
103
  * @returns {string | null} Complete worker/server source string, or null
101
104
  */
102
105
  export function compileSiteServer(
@@ -106,9 +109,16 @@ export function compileSiteServer(
106
109
  adapter?: string | null;
107
110
  mounts?: SiteMountSpec[];
108
111
  connectors?: SiteConnectorSpec[];
112
+ i18n?: ResolvedI18n | null;
109
113
  } = {},
110
114
  ) {
111
- const { baseUrl = "/_jx/server", adapter = null, mounts = [], connectors = [] } = opts;
115
+ const {
116
+ baseUrl = "/_jx/server",
117
+ adapter = null,
118
+ mounts = [],
119
+ connectors = [],
120
+ i18n = null,
121
+ } = opts;
112
122
  if (entries.length === 0 && mounts.length === 0 && !adapter) {
113
123
  return null;
114
124
  }
@@ -128,12 +138,19 @@ export function compileSiteServer(
128
138
  ? "\napp.all('*', (c) => c.env.ASSETS.fetch(c.req.raw))\n"
129
139
  : "\n";
130
140
 
141
+ /*
142
+ * Negotiation is middleware and it is registered FIRST, before any route and before the ASSETS
143
+ * fallthrough. It has to be: on `/` it either redirects or hands the request straight back to
144
+ * whatever would have served it, and it cannot do the second from behind a terminal handler.
145
+ */
146
+ const localeBlock = localeNegotiationMiddleware(i18n);
147
+
131
148
  return `// Generated by @jxsuite/compiler — do not edit manually
132
149
  import { Hono } from 'hono'
133
150
  ${imports}${mountBlock.imports}
134
151
 
135
152
  const app = new Hono()
136
- ${mountBlock.routes}${routes}
153
+ ${localeBlock}${mountBlock.routes}${routes}
137
154
  ${adapterBlock}
138
155
  export default app
139
156
  `;
@@ -7,12 +7,15 @@
7
7
 
8
8
  import {
9
9
  buildAttrs,
10
+ childSeparator,
10
11
  colorSchemePrePaintScript,
11
12
  compileStyles,
12
13
  createCompileContext,
13
14
  escapeHtml,
14
15
  isNodeDynamic,
16
+ PREFORMATTED_TAGS,
15
17
  pureSchemeOf,
18
+ resolveStaticTagName,
16
19
  resolveStaticValue,
17
20
  SELF_CLOSING,
18
21
  } from "../shared.ts";
@@ -127,6 +130,7 @@ function compileNode(
127
130
  scope: Record<string, unknown> | null;
128
131
  scopeDefs: Record<string, unknown>;
129
132
  media: Record<string, string>;
133
+ preformatted?: boolean;
130
134
  },
131
135
  islands: { def: JxMutableNode; tagName: string; className: string }[],
132
136
  ): string {
@@ -157,7 +161,9 @@ function compileNode(
157
161
  return `<${tagName}></${tagName}>`;
158
162
  }
159
163
 
160
- const tag = def.tagName ?? "div";
164
+ // Resolved against the same scope the attributes are, so the prerendered element and its
165
+ // Attributes come from one decision rather than two.
166
+ const tag = resolveStaticTagName(def.tagName, nextContext.scope);
161
167
  const attrs = buildAttrs(def, nextContext.scope);
162
168
 
163
169
  // Void elements (br, hr, img, …) have no closing tag.
@@ -166,7 +172,9 @@ function compileNode(
166
172
  return `<${tag}${attrs}>`;
167
173
  }
168
174
 
169
- const inner = buildInnerWithIslands(def, raw, nextContext, islands);
175
+ // `white-space` inherits, so once inside a <pre> the whole subtree stays whitespace-significant.
176
+ const preformatted = context.preformatted === true || PREFORMATTED_TAGS.has(tag);
177
+ const inner = buildInnerWithIslands(def, raw, { ...nextContext, preformatted }, islands);
170
178
 
171
179
  return `<${tag}${attrs}>${inner}</${tag}>`;
172
180
  }
@@ -188,6 +196,7 @@ function buildInnerWithIslands(
188
196
  scope: Record<string, unknown> | null;
189
197
  scopeDefs: Record<string, unknown>;
190
198
  media: Record<string, string>;
199
+ preformatted?: boolean;
191
200
  },
192
201
  islands: { def: JxMutableNode; tagName: string; className: string }[],
193
202
  ): string {
@@ -209,7 +218,7 @@ function buildInnerWithIslands(
209
218
  const childRaw = (rawChildren?.[i] ?? c) as JxMutableNode;
210
219
  return compileNode(child, childDynamic, childRaw, context, islands);
211
220
  })
212
- .join("\n ");
221
+ .join(childSeparator(context.preformatted));
213
222
  }
214
223
  return "";
215
224
  }
package/src/types.ts CHANGED
@@ -6,6 +6,8 @@ export interface HeadMergeContext {
6
6
  url?: string;
7
7
  siteUrl?: string;
8
8
  pageUrl?: string;
9
+ /** `rel="alternate"` links for this page's translations (site-architecture.md §13.5). */
10
+ alternates?: readonly { hreflang: string; href: string }[];
9
11
  }
10
12
 
11
13
  export interface SiteRoute {