@jxsuite/compiler 0.28.0 → 0.28.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.
@@ -5,51 +5,68 @@
5
5
  * defaults for all project-level properties.
6
6
  */
7
7
 
8
- import { readFileSync, existsSync } from "node:fs";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import type { ImageConfig, ProjectConfig } from "@jxsuite/schema/types";
10
+ import { errorMessage, parseProjectConfig } from "@jxsuite/schema/parse";
9
11
  import { resolve } from "node:path";
10
12
 
11
13
  /**
12
14
  * Default project configuration. All properties are optional in project.json; these defaults fill
13
15
  * in anything the author omits.
14
16
  */
17
+ /** A project config after merging with DEFAULTS — the option groups are always present. */
18
+ export interface ResolvedProjectConfig extends ProjectConfig {
19
+ name: string;
20
+ url: string;
21
+ defaults: NonNullable<ProjectConfig["defaults"]>;
22
+ images: ImageConfig & { service: "build" | "cloudflare" };
23
+ build: {
24
+ adapter?: string;
25
+ outDir: string;
26
+ format: string;
27
+ trailingSlash: string;
28
+ provider?: string | null;
29
+ [key: string]: unknown;
30
+ };
31
+ }
32
+
15
33
  const DEFAULTS = {
16
- name: "Jx Site",
17
- url: "",
18
- defaults: {
19
- layout: null,
20
- lang: "en",
21
- charset: "utf-8",
22
- },
23
34
  $head: [],
24
- imports: {},
25
35
  $media: {},
26
- style: {},
27
- state: {},
36
+ build: {
37
+ format: "directory",
38
+ outDir: "./dist",
39
+ provider: null,
40
+ trailingSlash: "always",
41
+ },
28
42
  contentTypes: {},
29
- redirects: {},
43
+ defaults: {
44
+ charset: "utf8",
45
+ lang: "en",
46
+ layout: null,
47
+ },
30
48
  images: {
31
- optimize: true,
32
- widths: [320, 640, 960, 1280, 1920],
33
49
  formats: ["webp", "avif"],
34
- quality: { webp: 80, avif: 65, jpeg: 80, png: 80 },
35
- sizes: "(max-width: 768px) 100vw, 50vw",
36
50
  lazyLoad: true,
51
+ optimize: true,
52
+ quality: { avif: 65, jpeg: 80, png: 80, webp: 80 },
37
53
  service: "build",
38
- binding: "IMAGES",
39
- },
40
- build: {
41
- outDir: "./dist",
42
- format: "directory",
43
- trailingSlash: "always",
44
- provider: null,
54
+ sizes: "(max-width: 768px) 100vw, 50vw",
55
+ widths: [320, 640, 960, 1280, 1920],
45
56
  },
46
- };
57
+ imports: {},
58
+ name: "Jx Site",
59
+ redirects: {},
60
+ state: {},
61
+ style: {},
62
+ url: "",
63
+ } satisfies Partial<ResolvedProjectConfig>;
47
64
 
48
65
  /**
49
66
  * Load and validate project.json from a project root.
50
67
  *
51
68
  * @param {string} projectRoot - Absolute path to the project directory
52
- * @returns {{ config: Record<string, any>; configPath: string; projectRoot: string }}
69
+ * @returns {{ config: ProjectConfig; configPath: string; projectRoot: string }}
53
70
  * @throws {Error} If project.json is missing or invalid JSON
54
71
  */
55
72
  export function loadProjectConfig(projectRoot: string) {
@@ -59,35 +76,44 @@ export function loadProjectConfig(projectRoot: string) {
59
76
  throw new Error(`project.json not found in ${projectRoot}`);
60
77
  }
61
78
 
62
- let raw;
79
+ let raw: ProjectConfig;
63
80
  try {
64
- raw = JSON.parse(readFileSync(configPath, "utf8"));
65
- } catch (e) {
66
- const err = e as Error;
67
- throw new Error(`Invalid JSON in ${configPath}: ${err.message}`);
68
- }
69
-
70
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
71
- throw new Error(`project.json must be a JSON object, got ${typeof raw}`);
81
+ raw = parseProjectConfig(readFileSync(configPath, "utf8"), configPath);
82
+ } catch (error) {
83
+ throw new Error(`Invalid project.json: ${errorMessage(error)}`, { cause: error });
72
84
  }
73
85
 
74
86
  // Deep merge with defaults
75
- const config = {
87
+ const config: ResolvedProjectConfig = {
76
88
  ...DEFAULTS,
77
89
  ...raw,
90
+ build: { ...DEFAULTS.build, ...raw.build },
78
91
  defaults: { ...DEFAULTS.defaults, ...raw.defaults },
79
92
  images: { ...DEFAULTS.images, ...raw.images },
80
- build: { ...DEFAULTS.build, ...raw.build },
81
93
  };
82
94
 
83
95
  // Preserve arrays and objects that shouldn't be shallow-merged
84
- if (raw.$head) config.$head = raw.$head;
85
- if (raw.$media) config.$media = raw.$media;
86
- if (raw.style) config.style = raw.style;
87
- if (raw.state) config.state = raw.state;
88
- if (raw.redirects) config.redirects = raw.redirects;
89
- if (raw.imports) config.imports = raw.imports;
90
- if (raw.contentTypes) config.contentTypes = raw.contentTypes;
96
+ if (raw.$head) {
97
+ config.$head = raw.$head;
98
+ }
99
+ if (raw.$media) {
100
+ config.$media = raw.$media;
101
+ }
102
+ if (raw.style) {
103
+ config.style = raw.style;
104
+ }
105
+ if (raw.state) {
106
+ config.state = raw.state;
107
+ }
108
+ if (raw.redirects) {
109
+ config.redirects = raw.redirects;
110
+ }
111
+ if (raw.imports) {
112
+ config.imports = raw.imports;
113
+ }
114
+ if (raw.contentTypes) {
115
+ config.contentTypes = raw.contentTypes;
116
+ }
91
117
 
92
118
  // Validate adapter
93
119
  const VALID_ADAPTERS = ["cloudflare-workers", "cloudflare-pages", "node", "bun"];
@@ -98,7 +124,8 @@ export function loadProjectConfig(projectRoot: string) {
98
124
  );
99
125
  }
100
126
 
101
- // Validate image service
127
+ // Validate image service. The "cloudflare" service emits /cdn-cgi/image transform URLs,
128
+ // Which work on any zone served through Cloudflare regardless of the build adapter.
102
129
  const VALID_IMAGE_SERVICES = ["build", "cloudflare"];
103
130
  if (!VALID_IMAGE_SERVICES.includes(config.images.service)) {
104
131
  throw new Error(
@@ -106,14 +133,6 @@ export function loadProjectConfig(projectRoot: string) {
106
133
  `Valid services: ${VALID_IMAGE_SERVICES.join(", ")}`,
107
134
  );
108
135
  }
109
- const CF_ADAPTERS = ["cloudflare-workers", "cloudflare-pages"];
110
- if (config.images.service === "cloudflare" && !CF_ADAPTERS.includes(config.build.adapter)) {
111
- console.warn(
112
- `images.service "cloudflare" requires a Cloudflare adapter ` +
113
- `(${CF_ADAPTERS.join(", ")}) — falling back to "build"`,
114
- );
115
- config.images.service = "build";
116
- }
117
136
 
118
137
  return {
119
138
  config,
@@ -5,28 +5,40 @@
5
5
  * Schema 2020-12 class definition.
6
6
  */
7
7
 
8
- import type { JxMutableNode } from "@jxsuite/schema/types";
8
+ import type {
9
+ JxClassCtorDef,
10
+ JxClassDef,
11
+ JxClassMethodDef,
12
+ JxClassParamDef,
13
+ } from "@jxsuite/schema/types";
9
14
 
10
15
  /**
11
16
  * Compile a .class.json schema to a JavaScript ES module string.
12
17
  *
13
- * @param {JxMutableNode} classDef - Parsed .class.json content (must have $prototype: "Class")
18
+ * @param {JxClassDef} classDef - Parsed .class.json content (must have $prototype: "Class")
14
19
  * @param {Record<string, unknown>} [_opts]
15
20
  * @returns {string} JavaScript module source code
16
21
  */
17
- export function compileClassJson(classDef: JxMutableNode, _opts: Record<string, unknown> = {}) {
22
+ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unknown> = {}) {
18
23
  const className = classDef.title;
19
- if (!className) throw new Error("compileClassJson: missing title (class name)");
24
+ if (!className) {
25
+ throw new Error("compileClassJson: missing title (class name)");
26
+ }
20
27
 
21
28
  const baseClass = resolveBaseClass(classDef.extends);
22
- const defs = classDef.$defs as Record<string, unknown> | undefined;
23
- const fields = (defs?.fields ?? {}) as Record<string, JxMutableNode>;
24
- const ctor = defs?.constructor as JxMutableNode | undefined;
25
- const methods = (defs?.methods ?? {}) as Record<string, JxMutableNode>;
26
-
27
- const lines: string[] = [];
28
- lines.push("// Generated by @jxsuite/compiler from .class.json — do not edit manually");
29
- if (classDef.$id) lines.push(`// Source: ${classDef.$id}`);
29
+ const defs = classDef.$defs;
30
+ const fields = defs?.fields ?? {};
31
+ // JSON objects inherit Object.prototype.constructor — only an own object value counts.
32
+ const rawCtor = defs?.constructor;
33
+ const ctor = typeof rawCtor === "object" ? rawCtor : undefined;
34
+ const methods = defs?.methods ?? {};
35
+
36
+ const lines: string[] = [
37
+ "// Generated by @jxsuite/compiler from .class.json — do not edit manually",
38
+ ];
39
+ if (classDef.$id) {
40
+ lines.push(`// Source: ${classDef.$id}`);
41
+ }
30
42
  lines.push("");
31
43
 
32
44
  // Class declaration
@@ -34,9 +46,10 @@ export function compileClassJson(classDef: JxMutableNode, _opts: Record<string,
34
46
  lines.push(`class ${className}${extendsClause} {`);
35
47
 
36
48
  // Static fields
37
- for (const [key, field] of Object.entries(fields)) {
38
- const f = field as JxMutableNode;
39
- if (f.scope !== "static") continue;
49
+ for (const [key, f] of Object.entries(fields)) {
50
+ if (f.scope !== "static") {
51
+ continue;
52
+ }
40
53
  const name = f.identifier ?? key;
41
54
  const prefix = f.access === "private" ? "#" : "";
42
55
  const initVal =
@@ -49,15 +62,20 @@ export function compileClassJson(classDef: JxMutableNode, _opts: Record<string,
49
62
  }
50
63
 
51
64
  // Instance field declarations (private)
52
- for (const [key, field] of Object.entries(fields)) {
53
- const f = field as JxMutableNode;
54
- if (f.scope === "static") continue;
55
- if (f.access !== "private") continue;
65
+ for (const [key, f] of Object.entries(fields)) {
66
+ if (f.scope === "static") {
67
+ continue;
68
+ }
69
+ if (f.access !== "private") {
70
+ continue;
71
+ }
56
72
  const name = f.identifier ?? key;
57
73
  lines.push(` #${name};`);
58
74
  }
59
75
 
60
- if (Object.keys(fields).length > 0) lines.push("");
76
+ if (Object.keys(fields).length > 0) {
77
+ lines.push("");
78
+ }
61
79
 
62
80
  // Constructor
63
81
  lines.push(" constructor(config = {}) {");
@@ -67,9 +85,10 @@ export function compileClassJson(classDef: JxMutableNode, _opts: Record<string,
67
85
  }
68
86
 
69
87
  // Initialize instance fields from config or defaults
70
- for (const [key, field] of Object.entries(fields)) {
71
- const f = field as JxMutableNode;
72
- if (f.scope === "static") continue;
88
+ for (const [key, f] of Object.entries(fields)) {
89
+ if (f.scope === "static") {
90
+ continue;
91
+ }
73
92
  const name = f.identifier ?? key;
74
93
  const prefix = f.access === "private" ? "#" : "";
75
94
  const initVal =
@@ -93,18 +112,13 @@ export function compileClassJson(classDef: JxMutableNode, _opts: Record<string,
93
112
  lines.push(" }");
94
113
 
95
114
  // Methods
96
- for (const [key, method] of Object.entries(methods)) {
97
- const m = method as JxMutableNode;
115
+ for (const [key, m] of Object.entries(methods)) {
98
116
  const name = m.identifier ?? key;
99
117
  const isStatic = m.scope === "static";
100
118
  const isPrivate = m.access === "private";
101
119
  const prefix = isPrivate ? "#" : "";
102
120
  const staticPrefix = isStatic ? "static " : "";
103
- const asyncPrefix =
104
- m.returnType?.$ref?.includes("Promise") ||
105
- isMethodAsync(m as unknown as { body?: string | string[] })
106
- ? "async "
107
- : "";
121
+ const asyncPrefix = m.returnType?.$ref?.includes("Promise") || isMethodAsync(m) ? "async " : "";
108
122
 
109
123
  const params = resolveParams(m.parameters ?? []);
110
124
  const bodyStr = resolveBody(m.body);
@@ -146,16 +160,18 @@ export function compileClassJson(classDef: JxMutableNode, _opts: Record<string,
146
160
  /**
147
161
  * Resolve the base class name from an extends value.
148
162
  *
149
- * @param {unknown} ext
163
+ * @param {JxClassDef["extends"]} ext
150
164
  * @returns {string}
151
165
  */
152
- function resolveBaseClass(ext: unknown) {
153
- if (!ext) return "Object";
154
- if (typeof ext === "string") return ext;
155
- const e = ext as JxMutableNode;
156
- if (e.$ref) {
157
- const ref = e.$ref;
158
- const match = ref.match(/([A-Za-z0-9_]+)\.class\.json/);
166
+ function resolveBaseClass(ext: JxClassDef["extends"]) {
167
+ if (!ext) {
168
+ return "Object";
169
+ }
170
+ if (typeof ext === "string") {
171
+ return ext;
172
+ }
173
+ if (ext.$ref) {
174
+ const match = ext.$ref.match(/([A-Za-z0-9_]+)\.class\.json/);
159
175
  return match ? match[1] : "Object";
160
176
  }
161
177
  return "Object";
@@ -164,15 +180,17 @@ function resolveBaseClass(ext: unknown) {
164
180
  /**
165
181
  * Resolve parameter names from $ref or inline definitions.
166
182
  *
167
- * @param {unknown[]} params
183
+ * @param {JxClassParamDef[]} params
168
184
  * @returns {string}
169
185
  */
170
- function resolveParams(params: unknown[]) {
186
+ function resolveParams(params: JxClassParamDef[]) {
171
187
  return params
172
- .map((p) => {
173
- const param = p as JxMutableNode;
174
- if (param.$ref) return param.$ref.split("/").pop();
175
- return param.identifier ?? param.name ?? "arg";
188
+ .map((param) => {
189
+ if (param.$ref) {
190
+ return param.$ref.split("/").pop();
191
+ }
192
+ const name = param.identifier ?? param.name;
193
+ return typeof name === "string" ? name : "arg";
176
194
  })
177
195
  .join(", ");
178
196
  }
@@ -180,22 +198,23 @@ function resolveParams(params: unknown[]) {
180
198
  /**
181
199
  * Resolve body from string or array of strings.
182
200
  *
183
- * @param {unknown} body
201
+ * @param {string | string[] | undefined} body
184
202
  * @returns {string[]}
185
203
  */
186
- function resolveBody(body: unknown) {
187
- if (!body) return [""];
188
- if (Array.isArray(body)) return body;
189
- return [body as string];
204
+ function resolveBody(body: string | string[] | undefined) {
205
+ if (!body) {
206
+ return [""];
207
+ }
208
+ return Array.isArray(body) ? body : [body];
190
209
  }
191
210
 
192
211
  /**
193
212
  * Simple heuristic: does the method body contain await?
194
213
  *
195
- * @param {{ body?: string | string[] }} method
214
+ * @param {JxClassMethodDef | JxClassCtorDef} method
196
215
  * @returns {boolean}
197
216
  */
198
- function isMethodAsync(method: { body?: string | string[] }) {
217
+ function isMethodAsync(method: JxClassMethodDef | JxClassCtorDef) {
199
218
  const body = Array.isArray(method.body) ? method.body.join("\n") : (method.body ?? "");
200
219
  return body.includes("await ");
201
220
  }