@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.
- package/dist/cli.js +166 -249
- package/dist/compiler.js +100 -188
- package/package.json +5 -5
- package/src/cli.ts +6 -6
- package/src/compile-cli.ts +6 -4
- package/src/compiler.ts +35 -29
- package/src/shared.ts +402 -231
- package/src/site/content-loader.ts +54 -29
- package/src/site/context-injection.ts +13 -9
- package/src/site/format-host.ts +26 -15
- package/src/site/head-merger.ts +37 -18
- package/src/site/image-cache.ts +13 -12
- package/src/site/image-optimizer.ts +32 -23
- package/src/site/image-transform.ts +172 -81
- package/src/site/layout-resolver.ts +33 -18
- package/src/site/pages-discovery.ts +61 -39
- package/src/site/prototype-resolver.ts +45 -21
- package/src/site/site-build.ts +232 -183
- package/src/site/site-loader.ts +71 -52
- package/src/targets/compile-class.ts +70 -51
- package/src/targets/compile-client.ts +271 -227
- package/src/targets/compile-element.ts +230 -146
- package/src/targets/compile-server.ts +16 -62
- package/src/targets/compile-static.ts +27 -19
- package/src/targets/compile-image-endpoint.ts +0 -138
package/src/site/site-loader.ts
CHANGED
|
@@ -5,51 +5,68 @@
|
|
|
5
5
|
* defaults for all project-level properties.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
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
|
-
|
|
27
|
-
|
|
36
|
+
build: {
|
|
37
|
+
format: "directory",
|
|
38
|
+
outDir: "./dist",
|
|
39
|
+
provider: null,
|
|
40
|
+
trailingSlash: "always",
|
|
41
|
+
},
|
|
28
42
|
contentTypes: {},
|
|
29
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
65
|
-
} catch (
|
|
66
|
-
|
|
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)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (raw
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (raw.
|
|
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 {
|
|
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 {
|
|
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:
|
|
22
|
+
export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unknown> = {}) {
|
|
18
23
|
const className = classDef.title;
|
|
19
|
-
if (!className)
|
|
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
|
|
23
|
-
const fields =
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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,
|
|
38
|
-
|
|
39
|
-
|
|
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,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
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,
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
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 {
|
|
163
|
+
* @param {JxClassDef["extends"]} ext
|
|
150
164
|
* @returns {string}
|
|
151
165
|
*/
|
|
152
|
-
function resolveBaseClass(ext:
|
|
153
|
-
if (!ext)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
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 {
|
|
183
|
+
* @param {JxClassParamDef[]} params
|
|
168
184
|
* @returns {string}
|
|
169
185
|
*/
|
|
170
|
-
function resolveParams(params:
|
|
186
|
+
function resolveParams(params: JxClassParamDef[]) {
|
|
171
187
|
return params
|
|
172
|
-
.map((
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 {
|
|
201
|
+
* @param {string | string[] | undefined} body
|
|
184
202
|
* @returns {string[]}
|
|
185
203
|
*/
|
|
186
|
-
function resolveBody(body:
|
|
187
|
-
if (!body)
|
|
188
|
-
|
|
189
|
-
|
|
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 {
|
|
214
|
+
* @param {JxClassMethodDef | JxClassCtorDef} method
|
|
196
215
|
* @returns {boolean}
|
|
197
216
|
*/
|
|
198
|
-
function isMethodAsync(method:
|
|
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
|
}
|