@jxsuite/compiler 5.1.2 → 5.1.3

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 (40) hide show
  1. package/dist/cli.js +220 -351
  2. package/dist/compiler.js +126 -257
  3. package/package.json +4 -4
  4. package/src/cli.ts +12 -14
  5. package/src/compiler.ts +45 -23
  6. package/src/shadow.ts +9 -8
  7. package/src/shared.ts +350 -296
  8. package/src/site/asset-mounts.ts +22 -12
  9. package/src/site/bundler.ts +38 -12
  10. package/src/site/client-runtime.ts +31 -17
  11. package/src/site/csp.ts +20 -11
  12. package/src/site/db-push.ts +38 -24
  13. package/src/site/dev-command.ts +3 -3
  14. package/src/site/format-host.ts +56 -45
  15. package/src/site/headers-emitter.ts +7 -4
  16. package/src/site/i18n.ts +20 -19
  17. package/src/site/image-cache.ts +16 -3
  18. package/src/site/image-optimizer.ts +19 -5
  19. package/src/site/image-transform.ts +56 -60
  20. package/src/site/img-loading.ts +11 -5
  21. package/src/site/layout-resolver.ts +7 -6
  22. package/src/site/link-relations.ts +10 -6
  23. package/src/site/locale-negotiation.ts +1 -1
  24. package/src/site/pages-discovery.ts +65 -47
  25. package/src/site/preview-server.ts +22 -19
  26. package/src/site/project-sections.ts +12 -3
  27. package/src/site/prototype-resolver.ts +94 -42
  28. package/src/site/schema-command.ts +26 -13
  29. package/src/site/service-worker.ts +2 -2
  30. package/src/site/site-build.ts +379 -262
  31. package/src/site/site-loader.ts +6 -3
  32. package/src/site/static-host.ts +3 -3
  33. package/src/site/validate-command.ts +64 -34
  34. package/src/site/well-known.ts +57 -12
  35. package/src/targets/compile-class.ts +17 -5
  36. package/src/targets/compile-client.ts +141 -106
  37. package/src/targets/compile-element.ts +157 -140
  38. package/src/targets/compile-server.ts +11 -5
  39. package/src/targets/compile-static.ts +57 -63
  40. package/src/types.ts +5 -6
@@ -10,7 +10,7 @@ import { dereference } from "@apidevtools/json-schema-ref-parser";
10
10
  import { collectServerEntries } from "../shared.ts";
11
11
  import { localeNegotiationMiddleware } from "../site/locale-negotiation.ts";
12
12
  import type { ResolvedI18n } from "../site/i18n.ts";
13
- import type { JxElement } from "@jxsuite/schema/types";
13
+ import type { JxDocumentValue, JxElement } from "@jxsuite/schema/types";
14
14
 
15
15
  /**
16
16
  * Compile a Jx document to a Hono server handler file. The handler exposes each `timing: "server"`
@@ -21,18 +21,21 @@ import type { JxElement } from "@jxsuite/schema/types";
21
21
  * const server = await compileServer("./dashboard.json");
22
22
  * if (server) fs.writeFileSync("dist/server.js", server);
23
23
  *
24
- * @param {string | object} sourcePath - Path to .json file, URL, or raw object
24
+ * @param {string | JxElement} sourcePath - Path to .json file, URL, or raw document
25
25
  * @param {object} [opts]
26
26
  * @param {string} [opts.baseUrl] - Base path for server endpoints. Default is `'/_jx/server'`
27
27
  * @returns {Promise<string | null>} Hono server handler source string, or null
28
28
  */
29
29
  export async function compileServer(
30
- sourcePath: string | object,
30
+ sourcePath: string | JxElement,
31
31
  opts: {
32
32
  baseUrl?: string;
33
33
  } = {},
34
34
  ) {
35
35
  const { baseUrl = "/_jx/server" } = opts;
36
+ /* SAFETY: the parameter is a Jx element document or the path of one, and `dereference` only
37
+ replaces each `$ref` node with the value it points to, so the result is that same document
38
+ with its references inlined. A file read from disk is not re-validated here. */
36
39
  const doc = (await dereference(sourcePath)) as JxElement;
37
40
  const entries = collectServerEntries(doc);
38
41
  if (entries.length === 0) {
@@ -74,8 +77,11 @@ export interface SiteMountSpec {
74
77
  basePath: string;
75
78
  /** Mount order (`server.order`, ascending). */
76
79
  order: number;
77
- /** JSON-inlined mount options (project sections manifest etc.). */
78
- options: Record<string, unknown>;
80
+ /**
81
+ * JSON-inlined mount options (project sections manifest etc.): project.json data, so document
82
+ * values, and a cleared (`undefined`) member is dropped by the `JSON.stringify` that inlines it.
83
+ */
84
+ options: Record<string, JxDocumentValue | undefined>;
79
85
  }
80
86
 
81
87
  /** A connector provider class the worker imports and hands to mounts via `options.connectors`. */
@@ -19,14 +19,30 @@ import {
19
19
  resolveStaticValue,
20
20
  SELF_CLOSING,
21
21
  } from "../shared.ts";
22
+ import type { CompileContext } from "../shared.ts";
22
23
  import { emitElementModule } from "./compile-element.ts";
23
- import type { JxDocument, JxMutableNode, JxStyle } from "@jxsuite/schema/types";
24
+ import { isBoolean, isNonNullObject, isNumber, isString } from "@jxsuite/schema/guards";
25
+ import type { JsonPrimitive, JxDocument, JxMutableNode, JxStyle } from "@jxsuite/schema/types";
26
+
27
+ /**
28
+ * A member of a node's `children` as a parsed document holds it: an element, a text string, or —
29
+ * since the document is JSON — a number, boolean or `null` the walk renders as text or skips.
30
+ */
31
+ type StaticChild = JxMutableNode | JsonPrimitive;
32
+
33
+ /** A dynamic subtree lifted out of the static page, emitted as its own element module. */
34
+ interface Island {
35
+ def: JxDocument;
36
+ tagName: string;
37
+ className: string;
38
+ }
24
39
 
25
40
  /**
26
41
  * Compile a static document to HTML, with dynamic subtrees as islands.
27
42
  *
28
43
  * @param {JxDocument} raw - Raw JSON document (with $ref pointers preserved)
29
- * @param {Record<string, unknown>} opts
44
+ * @param opts - Page title, CDN sources, project style, pre-paint switch and island `$src`
45
+ * rewriting
30
46
  * @returns {{ html: string; files: { path: string; content: string; tagName: string }[] }}
31
47
  */
32
48
  export function compileStaticPage(
@@ -37,19 +53,16 @@ export function compileStaticPage(
37
53
  litHtmlSrc?: string;
38
54
  projectStyle?: JxStyle | null;
39
55
  prePaintScheme?: boolean;
40
- [key: string]: unknown;
56
+ /** Rewrite an island's Function-def `$src` specifier to its served URL. */
57
+ rewriteSrc?: (specifier: string) => string;
41
58
  },
42
59
  ) {
43
60
  const { title = "Jx App", reactivitySrc, litHtmlSrc } = opts;
44
61
 
45
62
  const rootContext = createCompileContext(raw, null, raw.state ?? {}, raw.$media ?? {});
46
- const styleBlock = compileStyles(
47
- raw,
48
- raw.$media ?? {},
49
- (opts.projectStyle ?? null) as JxStyle | null,
50
- );
51
- const islands: { def: JxMutableNode; tagName: string; className: string }[] = [];
52
- const bodyContent = compileNode(raw, false, raw, rootContext, islands);
63
+ const styleBlock = compileStyles(raw, raw.$media ?? {}, opts.projectStyle ?? null);
64
+ const islands: Island[] = [];
65
+ const bodyContent = compileNode(raw, false, rootContext, islands);
53
66
 
54
67
  /** @type {{ path: string; content: string; tagName: string }[]} */
55
68
  const files = [];
@@ -57,12 +70,7 @@ export function compileStaticPage(
57
70
  let moduleScripts = "";
58
71
  if (islands.length > 0) {
59
72
  for (const island of islands) {
60
- const moduleContent = emitElementModule(
61
- island.def as JxDocument,
62
- island.className,
63
- [],
64
- opts.rewriteSrc as ((specifier: string) => string) | undefined,
65
- );
73
+ const moduleContent = emitElementModule(island.def, island.className, [], opts.rewriteSrc);
66
74
  files.push({
67
75
  content: moduleContent,
68
76
  path: `_islands/${island.tagName}.js`,
@@ -115,48 +123,47 @@ export function compileStaticPage(
115
123
  * Compile a single Jx node to an HTML string. Dynamic nodes become hydration islands; static nodes
116
124
  * become plain HTML.
117
125
  *
118
- * @param {JxMutableNode} def
126
+ * The node is read as authored — `$ref` pointers preserved — because an island's element module
127
+ * resolves them itself at runtime.
128
+ *
129
+ * @param {StaticChild} def
119
130
  * @param {boolean} dynamic
120
- * @param {JxMutableNode} raw
121
- * @param {any} context
122
- * @param {{ def: JxMutableNode; tagName: string; className: string }[]} islands
131
+ * @param {CompileContext} context
132
+ * @param {Island[]} islands
123
133
  * @returns {string}
124
134
  */
125
135
  function compileNode(
126
- def: JxMutableNode,
136
+ def: StaticChild,
127
137
  dynamic: boolean,
128
- raw: JxMutableNode,
129
- context: {
130
- scope: Record<string, unknown> | null;
131
- scopeDefs: Record<string, unknown>;
132
- media: Record<string, string>;
133
- preformatted?: boolean;
134
- },
135
- islands: { def: JxMutableNode; tagName: string; className: string }[],
138
+ context: CompileContext,
139
+ islands: Island[],
136
140
  ): string {
137
141
  // String children are text nodes
138
- if (typeof def === "string") {
142
+ if (isString(def)) {
139
143
  return escapeHtml(def);
140
144
  }
141
- if (typeof def === "number" || typeof def === "boolean") {
145
+ if (isNumber(def) || isBoolean(def)) {
142
146
  return escapeHtml(String(def));
143
147
  }
144
- if (!def || typeof def !== "object") {
148
+ if (!isNonNullObject(def)) {
145
149
  return "";
146
150
  }
147
151
 
148
152
  const nextContext = createCompileContext(
149
- raw,
153
+ def,
150
154
  context.scope,
151
- raw?.state ?? context.scopeDefs,
152
- raw?.$media ?? context.media,
155
+ def.state ?? context.scopeDefs,
156
+ def.$media ?? context.media,
153
157
  );
154
158
 
155
159
  if (dynamic) {
156
160
  const n = islands.length;
157
161
  const tagName = `jx-island-${n}`;
158
162
  const className = `JxIsland${n}`;
159
- const elementDef = { ...(raw ?? def), tagName };
163
+ /* SAFETY: JxDocument is the element-node shape with `tagName` narrowed to a literal string,
164
+ and the island's `tagName` is the literal `jx-island-N` assigned here; every other member is
165
+ the authored node's own, carried over unchanged. */
166
+ const elementDef = { ...def, tagName } as JxDocument;
160
167
  islands.push({ className, def: elementDef, tagName });
161
168
  return `<${tagName}></${tagName}>`;
162
169
  }
@@ -174,7 +181,7 @@ function compileNode(
174
181
 
175
182
  // `white-space` inherits, so once inside a <pre> the whole subtree stays whitespace-significant.
176
183
  const preformatted = context.preformatted === true || PREFORMATTED_TAGS.has(tag);
177
- const inner = buildInnerWithIslands(def, raw, { ...nextContext, preformatted }, islands);
184
+ const inner = buildInnerWithIslands(def, { ...nextContext, preformatted }, islands);
178
185
 
179
186
  return `<${tag}${attrs}>${inner}</${tag}>`;
180
187
  }
@@ -184,40 +191,27 @@ function compileNode(
184
191
  * those that are actually dynamic.
185
192
  *
186
193
  * @param {JxMutableNode} def
187
- * @param {JxMutableNode} raw
188
- * @param {any} context
189
- * @param {{ def: JxMutableNode; tagName: string; className: string }[]} islands
194
+ * @param {CompileContext} context
195
+ * @param {Island[]} islands
190
196
  * @returns {string}
191
197
  */
192
198
  function buildInnerWithIslands(
193
199
  def: JxMutableNode,
194
- raw: JxMutableNode,
195
- context: {
196
- scope: Record<string, unknown> | null;
197
- scopeDefs: Record<string, unknown>;
198
- media: Record<string, string>;
199
- preformatted?: boolean;
200
- },
201
- islands: { def: JxMutableNode; tagName: string; className: string }[],
200
+ context: CompileContext,
201
+ islands: Island[],
202
202
  ): string {
203
- const source = raw ?? def;
204
-
205
- if (source.textContent !== undefined) {
206
- const value = resolveStaticValue(source.textContent, context.scope);
203
+ if (def.textContent !== undefined) {
204
+ const value = resolveStaticValue(def.textContent, context.scope);
207
205
  return value == null ? "" : escapeHtml(String(value));
208
206
  }
209
- if (source.innerHTML) {
210
- return (resolveStaticValue(source.innerHTML, context.scope) as string) ?? source.innerHTML;
207
+ if (def.innerHTML) {
208
+ // Raw HTML may itself be a template; one that resolves to nothing keeps its authored text.
209
+ const resolved = resolveStaticValue(def.innerHTML, context.scope);
210
+ return resolved == null ? def.innerHTML : String(resolved);
211
211
  }
212
- if (Array.isArray(source.children)) {
213
- const rawChildren = Array.isArray(raw?.children) ? raw.children : undefined;
214
- return source.children
215
- .map((c, i: number) => {
216
- const child = c as JxMutableNode;
217
- const childDynamic = isNodeDynamic(child);
218
- const childRaw = (rawChildren?.[i] ?? c) as JxMutableNode;
219
- return compileNode(child, childDynamic, childRaw, context, islands);
220
- })
212
+ if (Array.isArray(def.children)) {
213
+ return def.children
214
+ .map((child) => compileNode(child, isNodeDynamic(child), context, islands))
221
215
  .join(childSeparator(context.preformatted));
222
216
  }
223
217
  return "";
package/src/types.ts CHANGED
@@ -4,9 +4,8 @@
4
4
  */
5
5
  export type { HeadMergeContext } from "@jxsuite/site/head-merger";
6
6
 
7
- export interface SiteRoute {
8
- urlPattern: string;
9
- sourcePath?: string;
10
- _pathParams?: Record<string, string>;
11
- [key: string]: unknown;
12
- }
7
+ /**
8
+ * `SiteRoute` is owned by `@jxsuite/site/context`, whose `injectContext` is the consumer that reads
9
+ * it; the site build hands the same route to both, so there is one declaration of its shape.
10
+ */
11
+ export type { SiteRoute } from "@jxsuite/site/context";