@jxsuite/compiler 1.2.0 → 1.4.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.
- package/LICENSE +21 -0
- package/dist/cli.js +219 -210
- package/dist/compiler.js +54 -54
- package/package.json +8 -7
- package/src/cli.ts +9 -12
- package/src/shared.ts +3 -2
- package/src/site/asset-mounts.ts +140 -0
- package/src/site/image-transform.ts +25 -7
- package/src/site/schema-command.ts +22 -9
- package/src/site/site-build.ts +43 -1
- package/src/site/validate-command.ts +264 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"type": "module",
|
|
20
20
|
"exports": {
|
|
21
21
|
".": "./src/compiler.ts",
|
|
22
|
+
"./asset-mounts": "./src/site/asset-mounts.ts",
|
|
22
23
|
"./format-host": "./src/site/format-host.ts",
|
|
23
24
|
"./project-sections": "./src/site/project-sections.ts",
|
|
24
25
|
"./schema-command": "./src/site/schema-command.ts",
|
|
@@ -45,16 +46,16 @@
|
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@apidevtools/json-schema-ref-parser": "^15.5.0",
|
|
48
|
-
"@jxsuite/create": "^1.
|
|
49
|
-
"@jxsuite/runtime": "^1.
|
|
50
|
-
"@jxsuite/schema": "^1.
|
|
49
|
+
"@jxsuite/create": "^1.2.1",
|
|
50
|
+
"@jxsuite/runtime": "^1.3.1",
|
|
51
|
+
"@jxsuite/schema": "^1.4.0",
|
|
51
52
|
"esbuild": "^0.28.1",
|
|
52
53
|
"sharp": "^0.35.3"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"@happy-dom/global-registrator": "^20.
|
|
56
|
-
"@jxsuite/connector": "^0.
|
|
57
|
-
"@jxsuite/parser": "^1.
|
|
56
|
+
"@happy-dom/global-registrator": "^20.11.1",
|
|
57
|
+
"@jxsuite/connector": "^0.4.1",
|
|
58
|
+
"@jxsuite/parser": "^1.4.0",
|
|
58
59
|
"@types/mdast": "^4.0.4"
|
|
59
60
|
}
|
|
60
61
|
}
|
package/src/cli.ts
CHANGED
|
@@ -26,7 +26,7 @@ Commands:
|
|
|
26
26
|
dev [root] Start the dev server (requires @jxsuite/server and Bun)
|
|
27
27
|
preview [root] Serve an already-built dist/ directory
|
|
28
28
|
schema [root] Generate project.schema.json + document.schema.json from project.json#/extensions
|
|
29
|
-
validate [root] Validate project.json
|
|
29
|
+
validate [root] Validate the whole project: project.json, documents, classes, and fragments
|
|
30
30
|
db push [root] Sync the data section's tables to their connections (additive-only)
|
|
31
31
|
|
|
32
32
|
Options:
|
|
@@ -161,18 +161,15 @@ Options:
|
|
|
161
161
|
}
|
|
162
162
|
} else if (command === "validate") {
|
|
163
163
|
try {
|
|
164
|
-
const {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
const { formatProjectTreeIssues, validateProjectTree } =
|
|
165
|
+
await import("./site/validate-command.ts");
|
|
166
|
+
const result = await validateProjectTree(projectRoot);
|
|
167
|
+
if (result.valid) {
|
|
168
|
+
console.log(`Project is valid (${result.checked} files checked in ${projectRoot})`);
|
|
168
169
|
} else {
|
|
169
|
-
console.error(`
|
|
170
|
-
for (const
|
|
171
|
-
|
|
172
|
-
instancePath?: string;
|
|
173
|
-
message?: string;
|
|
174
|
-
};
|
|
175
|
-
console.error(` - ${instancePath || "/"}: ${message ?? JSON.stringify(error)}`);
|
|
170
|
+
console.error(`Project is INVALID (${projectRoot}):`);
|
|
171
|
+
for (const line of formatProjectTreeIssues(result)) {
|
|
172
|
+
console.error(line);
|
|
176
173
|
}
|
|
177
174
|
process.exit(1);
|
|
178
175
|
}
|
package/src/shared.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
isExpressionDef,
|
|
24
24
|
isFunctionDef,
|
|
25
25
|
isMappedArray,
|
|
26
|
+
bodyReturnsValue,
|
|
26
27
|
isNamedFormulaDef,
|
|
27
28
|
isPrototypeDef,
|
|
28
29
|
isRef,
|
|
@@ -79,7 +80,7 @@ export function emitFormulaFn(def: { parameters?: unknown[] }, compiledBody: str
|
|
|
79
80
|
export type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
80
81
|
|
|
81
82
|
// CDN defaults
|
|
82
|
-
export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.
|
|
83
|
+
export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.40";
|
|
83
84
|
export const DEFAULT_LIT_HTML_SRC = "https://esm.sh/lit-html@3.3.0";
|
|
84
85
|
|
|
85
86
|
// ─── Schema keywords & detection ─────────────────────────────────────────────
|
|
@@ -363,7 +364,7 @@ export function buildInitialScope(
|
|
|
363
364
|
const fn = new Function("state", ...names, body) as (
|
|
364
365
|
state: Record<string, unknown>,
|
|
365
366
|
) => unknown;
|
|
366
|
-
if (body
|
|
367
|
+
if (bodyReturnsValue(body)) {
|
|
367
368
|
defineLazyScopeValue(scope, key, () => fn(scope));
|
|
368
369
|
} else {
|
|
369
370
|
setOwnScopeValue(scope, key, fn);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset-mounts — host side of the extension `assets` capability (specs/extensions.md §8.5).
|
|
3
|
+
*
|
|
4
|
+
* A section-owner class publishes directories that may sit outside the project root (an external
|
|
5
|
+
* content source's co-located images) at a stable site URL. The build resolves those URLs while
|
|
6
|
+
* optimizing images, collects the ones its compiled output actually references, and copies just
|
|
7
|
+
* those files into dist — the source directory itself is never mirrored, so entry files never leak
|
|
8
|
+
* into the build output.
|
|
9
|
+
*
|
|
10
|
+
* @module asset-mounts
|
|
11
|
+
* @docs framework/site/images
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { cpSync, existsSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { dirname, resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
collectAssetUrls,
|
|
18
|
+
normalizeAssetPrefix,
|
|
19
|
+
resolveAssetUrl,
|
|
20
|
+
} from "@jxsuite/schema/asset-paths";
|
|
21
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
22
|
+
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
23
|
+
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gather every asset mount contributed by the project's extensions.
|
|
27
|
+
*
|
|
28
|
+
* Gating matches `emit` (extensions.md §8.4): a class owning a project section contributes only
|
|
29
|
+
* when the project declares a non-empty value for that section. Mounts claiming the same URL prefix
|
|
30
|
+
* for different directories are a configuration error — the first wins and the clash is reported.
|
|
31
|
+
*
|
|
32
|
+
* @param {ExtensionRegistry} registry - The project's extension registry
|
|
33
|
+
* @param {ProjectConfig} projectConfig
|
|
34
|
+
* @param {string} projectRoot - Absolute project root
|
|
35
|
+
* @returns {Promise<{ mounts: AssetMount[]; errors: string[] }>}
|
|
36
|
+
*/
|
|
37
|
+
export async function loadAssetMounts(
|
|
38
|
+
registry: ExtensionRegistry,
|
|
39
|
+
projectConfig: ProjectConfig,
|
|
40
|
+
projectRoot: string,
|
|
41
|
+
): Promise<{ mounts: AssetMount[]; errors: string[] }> {
|
|
42
|
+
const mounts: AssetMount[] = [];
|
|
43
|
+
const errors: string[] = [];
|
|
44
|
+
const byPrefix = new Map<string, AssetMount>();
|
|
45
|
+
|
|
46
|
+
for (const entry of registry.assetProviders()) {
|
|
47
|
+
const sectionKey = entry.project?.key;
|
|
48
|
+
const sectionValue = sectionKey
|
|
49
|
+
? (projectConfig as unknown as Record<string, unknown>)[sectionKey]
|
|
50
|
+
: undefined;
|
|
51
|
+
if (
|
|
52
|
+
sectionKey &&
|
|
53
|
+
(sectionValue == null ||
|
|
54
|
+
(typeof sectionValue === "object" && Object.keys(sectionValue).length === 0))
|
|
55
|
+
) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const declared = (await entry.call("assets", sectionValue ?? null, {
|
|
60
|
+
projectConfig,
|
|
61
|
+
root: projectRoot,
|
|
62
|
+
})) as AssetMount[] | null;
|
|
63
|
+
for (const mount of declared ?? []) {
|
|
64
|
+
if (!mount?.urlPrefix || !mount.dir) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const normalized: AssetMount = {
|
|
68
|
+
dir: mount.dir,
|
|
69
|
+
urlPrefix: normalizeAssetPrefix(mount.urlPrefix),
|
|
70
|
+
};
|
|
71
|
+
const prior = byPrefix.get(normalized.urlPrefix);
|
|
72
|
+
if (prior) {
|
|
73
|
+
if (prior.dir !== normalized.dir) {
|
|
74
|
+
errors.push(
|
|
75
|
+
`Asset mount conflict: "${normalized.urlPrefix}" is claimed by both ` +
|
|
76
|
+
`"${prior.dir}" and "${normalized.dir}"`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
byPrefix.set(normalized.urlPrefix, normalized);
|
|
82
|
+
mounts.push(normalized);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
errors.push(`Error in ${entry.name}.assets: ${(error as Error).message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { errors, mounts };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Add every mounted URL referenced by a compiled artifact to `into`.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} text - Compiled HTML or CSS
|
|
96
|
+
* @param {readonly AssetMount[]} mounts
|
|
97
|
+
* @param {Set<string>} into - Accumulator across the whole build
|
|
98
|
+
*/
|
|
99
|
+
export function collectAssetRefs(text: string, mounts: readonly AssetMount[], into: Set<string>) {
|
|
100
|
+
if (mounts.length === 0 || !text) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const url of collectAssetUrls(text, mounts)) {
|
|
104
|
+
into.add(url);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Copy referenced mounted assets into the build output, each at its own URL path.
|
|
110
|
+
*
|
|
111
|
+
* @param {Iterable<string>} urls - Mounted URLs collected from the compiled output
|
|
112
|
+
* @param {readonly AssetMount[]} mounts
|
|
113
|
+
* @param {string} outDir - Absolute build output directory
|
|
114
|
+
* @returns {{ copied: number; missing: string[] }} Files written, and URLs that resolved to nothing
|
|
115
|
+
*/
|
|
116
|
+
export function copyMountedAssets(
|
|
117
|
+
urls: Iterable<string>,
|
|
118
|
+
mounts: readonly AssetMount[],
|
|
119
|
+
outDir: string,
|
|
120
|
+
): { copied: number; missing: string[] } {
|
|
121
|
+
let copied = 0;
|
|
122
|
+
const missing: string[] = [];
|
|
123
|
+
|
|
124
|
+
for (const url of urls) {
|
|
125
|
+
// The single containment gate is resolveAssetUrl: it decodes once and refuses `.`/`..`,
|
|
126
|
+
// Empty segments, and undecodable input, so a surviving URL cannot escape either the mount
|
|
127
|
+
// Directory it reads from or the outDir path it writes to.
|
|
128
|
+
const source = resolveAssetUrl(mounts, url);
|
|
129
|
+
if (!source || !existsSync(source)) {
|
|
130
|
+
missing.push(url);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const target = resolve(outDir, decodeURIComponent(url).replace(/^\//, ""));
|
|
134
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
135
|
+
cpSync(source, target);
|
|
136
|
+
copied += 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { copied, missing };
|
|
140
|
+
}
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
processImage,
|
|
17
17
|
} from "./image-optimizer.ts";
|
|
18
18
|
import { getCached, getImageCacheDir, setCached } from "./image-cache.ts";
|
|
19
|
+
import { resolveAssetUrl } from "@jxsuite/schema/asset-paths";
|
|
19
20
|
|
|
21
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
20
22
|
import type { ImageConfig, ImageManifest } from "./image-optimizer.ts";
|
|
21
23
|
import type { JxDocument, JxMutableNode } from "@jxsuite/schema/types";
|
|
22
24
|
import type { CacheManifest } from "./image-cache.ts";
|
|
@@ -193,13 +195,20 @@ function isAllowedRemote(src: string, config: ImageConfig) {
|
|
|
193
195
|
/**
|
|
194
196
|
* Resolve a src path to an absolute filesystem path.
|
|
195
197
|
*
|
|
196
|
-
*
|
|
198
|
+
* Extension asset mounts win first — they publish directories that may sit outside the project root
|
|
199
|
+
* (extensions.md §8.5), so a mounted src has no other resolution. Otherwise paths starting with "/"
|
|
200
|
+
* resolve into `public/`, and relative ones from the project root.
|
|
197
201
|
*
|
|
198
202
|
* @param {string} src
|
|
199
203
|
* @param {string} projectRoot
|
|
204
|
+
* @param {readonly AssetMount[]} [mounts]
|
|
200
205
|
* @returns {string}
|
|
201
206
|
*/
|
|
202
|
-
function resolveImagePath(src: string, projectRoot: string) {
|
|
207
|
+
function resolveImagePath(src: string, projectRoot: string, mounts?: readonly AssetMount[]) {
|
|
208
|
+
const mounted = mounts?.length ? resolveAssetUrl(mounts, src) : null;
|
|
209
|
+
if (mounted) {
|
|
210
|
+
return mounted;
|
|
211
|
+
}
|
|
203
212
|
if (src.startsWith("/")) {
|
|
204
213
|
return resolve(projectRoot, "public", src.slice(1));
|
|
205
214
|
}
|
|
@@ -218,6 +227,7 @@ function resolveImagePath(src: string, projectRoot: string) {
|
|
|
218
227
|
* @param {string} projectRoot
|
|
219
228
|
* @param {CacheManifest | null} cache - Variant cache (build mode); unused in cloudflare mode
|
|
220
229
|
* @param {ImageMetaCache} [metaCache] - Dimension/hash memo (cloudflare mode)
|
|
230
|
+
* @param {readonly AssetMount[]} [mounts] - Extension asset mounts (extensions.md §8.5)
|
|
221
231
|
* @returns {Promise<{ imageRefs: Map<string, ImageManifest> }>}
|
|
222
232
|
*/
|
|
223
233
|
export async function transformImageNodes(
|
|
@@ -226,6 +236,7 @@ export async function transformImageNodes(
|
|
|
226
236
|
projectRoot: string,
|
|
227
237
|
cache: CacheManifest | null,
|
|
228
238
|
metaCache?: ImageMetaCache,
|
|
239
|
+
mounts: readonly AssetMount[] = [],
|
|
229
240
|
) {
|
|
230
241
|
const imageRefs = new Map<string, ImageManifest>();
|
|
231
242
|
|
|
@@ -234,7 +245,7 @@ export async function transformImageNodes(
|
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
const meta = metaCache ?? new Map();
|
|
237
|
-
await walkAndTransform(doc, config, projectRoot, cache, meta, imageRefs);
|
|
248
|
+
await walkAndTransform(doc, config, projectRoot, cache, meta, imageRefs, mounts);
|
|
238
249
|
|
|
239
250
|
return { imageRefs };
|
|
240
251
|
}
|
|
@@ -246,6 +257,7 @@ export async function transformImageNodes(
|
|
|
246
257
|
* @param {CacheManifest | null} cache
|
|
247
258
|
* @param {ImageMetaCache} metaCache
|
|
248
259
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
260
|
+
* @param {readonly AssetMount[]} mounts
|
|
249
261
|
*/
|
|
250
262
|
async function walkAndTransform(
|
|
251
263
|
node: JxMutableNode | JxDocument,
|
|
@@ -254,13 +266,14 @@ async function walkAndTransform(
|
|
|
254
266
|
cache: CacheManifest | null,
|
|
255
267
|
metaCache: ImageMetaCache,
|
|
256
268
|
imageRefs: Map<string, ImageManifest>,
|
|
269
|
+
mounts: readonly AssetMount[],
|
|
257
270
|
) {
|
|
258
271
|
if (!node || typeof node !== "object") {
|
|
259
272
|
return;
|
|
260
273
|
}
|
|
261
274
|
|
|
262
275
|
if (node.tagName === "img") {
|
|
263
|
-
await transformImgNode(node, config, projectRoot, cache, metaCache, imageRefs);
|
|
276
|
+
await transformImgNode(node, config, projectRoot, cache, metaCache, imageRefs, mounts);
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
if (typeof node.innerHTML === "string" && node.innerHTML.includes("<img")) {
|
|
@@ -271,6 +284,7 @@ async function walkAndTransform(
|
|
|
271
284
|
cache,
|
|
272
285
|
metaCache,
|
|
273
286
|
imageRefs,
|
|
287
|
+
mounts,
|
|
274
288
|
);
|
|
275
289
|
}
|
|
276
290
|
|
|
@@ -279,7 +293,7 @@ async function walkAndTransform(
|
|
|
279
293
|
if (typeof child === "string") {
|
|
280
294
|
continue;
|
|
281
295
|
}
|
|
282
|
-
await walkAndTransform(child, config, projectRoot, cache, metaCache, imageRefs);
|
|
296
|
+
await walkAndTransform(child, config, projectRoot, cache, metaCache, imageRefs, mounts);
|
|
283
297
|
}
|
|
284
298
|
}
|
|
285
299
|
}
|
|
@@ -291,6 +305,7 @@ async function walkAndTransform(
|
|
|
291
305
|
* @param {CacheManifest | null} cache
|
|
292
306
|
* @param {ImageMetaCache} metaCache
|
|
293
307
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
308
|
+
* @param {readonly AssetMount[]} mounts
|
|
294
309
|
*/
|
|
295
310
|
async function transformImgNode(
|
|
296
311
|
node: JxMutableNode | JxDocument,
|
|
@@ -299,6 +314,7 @@ async function transformImgNode(
|
|
|
299
314
|
cache: CacheManifest | null,
|
|
300
315
|
metaCache: ImageMetaCache,
|
|
301
316
|
imageRefs: Map<string, ImageManifest>,
|
|
317
|
+
mounts: readonly AssetMount[],
|
|
302
318
|
) {
|
|
303
319
|
if (!node.attributes) {
|
|
304
320
|
node.attributes = {};
|
|
@@ -325,7 +341,7 @@ async function transformImgNode(
|
|
|
325
341
|
// Fit=scale-down avoid upscaling past the source size.
|
|
326
342
|
srcset = buildCloudflareSrcset(src, config, Infinity, null);
|
|
327
343
|
} else {
|
|
328
|
-
const absoluteSrc = resolveImagePath(src, projectRoot);
|
|
344
|
+
const absoluteSrc = resolveImagePath(src, projectRoot, mounts);
|
|
329
345
|
if (!existsSync(absoluteSrc)) {
|
|
330
346
|
return;
|
|
331
347
|
}
|
|
@@ -382,6 +398,7 @@ const DATA_NO_OPT_RE = /\bdata-no-optimize\b/;
|
|
|
382
398
|
* @param {CacheManifest | null} cache
|
|
383
399
|
* @param {ImageMetaCache} metaCache
|
|
384
400
|
* @param {Map<string, ImageManifest>} imageRefs
|
|
401
|
+
* @param {readonly AssetMount[]} mounts
|
|
385
402
|
* @returns {Promise<string>}
|
|
386
403
|
*/
|
|
387
404
|
async function transformInnerHtmlImages(
|
|
@@ -391,6 +408,7 @@ async function transformInnerHtmlImages(
|
|
|
391
408
|
cache: CacheManifest | null,
|
|
392
409
|
metaCache: ImageMetaCache,
|
|
393
410
|
imageRefs: Map<string, ImageManifest>,
|
|
411
|
+
mounts: readonly AssetMount[],
|
|
394
412
|
) {
|
|
395
413
|
/** @type {{ match: string; replacement: string }[]} */
|
|
396
414
|
const replacements = [];
|
|
@@ -418,7 +436,7 @@ async function transformInnerHtmlImages(
|
|
|
418
436
|
continue;
|
|
419
437
|
}
|
|
420
438
|
|
|
421
|
-
const absoluteSrc = remote ? null : resolveImagePath(src, projectRoot);
|
|
439
|
+
const absoluteSrc = remote ? null : resolveImagePath(src, projectRoot, mounts);
|
|
422
440
|
if (absoluteSrc && !existsSync(absoluteSrc)) {
|
|
423
441
|
continue;
|
|
424
442
|
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Composes the per-project entry documents (project.schema.json and document.schema.json) from the
|
|
5
5
|
* core schemas and each enabled extension's shipped fragments, then writes them into the project
|
|
6
|
-
* root as committed
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* root as committed, SELF-CONTAINED bundles: every fragment ref is resolved through
|
|
7
|
+
* {@link bundleSchema}, embedding the core + fragment resources under `$defs` keyed by their
|
|
8
|
+
* canonical `$id`s. Relative `./node_modules/...` refs only resolve through Node module resolution,
|
|
9
|
+
* which editors do not perform (hoisted workspaces leave projects without their own node_modules) —
|
|
10
|
+
* the bundled form resolves offline for ajv `jx validate`, VS Code's JSON language service, and the
|
|
11
|
+
* studio's Monaco alike, with one resolution behavior for all consumers.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -57,11 +59,22 @@ export async function writeProjectSchemas(projectRoot: string) {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Bundle before committing: the emitted relative refs are an intermediate form only.
|
|
63
|
+
const root = resolve(projectRoot);
|
|
64
|
+
const loadJson = restrictedSchemaLoader(root);
|
|
65
|
+
const projectSchema = await bundleSchema(
|
|
66
|
+
emitProjectSchema({ corePath, fieldSchemaRefs, fragments }),
|
|
67
|
+
loadJson,
|
|
68
|
+
root,
|
|
69
|
+
);
|
|
70
|
+
const documentSchema = await bundleSchema(
|
|
71
|
+
emitDocumentSchema({
|
|
72
|
+
corePath: coreSchemaRef(projectRoot, CORE_DOCUMENT_SPECIFIER),
|
|
73
|
+
pathsValueRefs,
|
|
74
|
+
}),
|
|
75
|
+
loadJson,
|
|
76
|
+
root,
|
|
77
|
+
);
|
|
65
78
|
|
|
66
79
|
const projectSchemaPath = resolve(projectRoot, "project.schema.json");
|
|
67
80
|
const documentSchemaPath = resolve(projectRoot, "document.schema.json");
|
package/src/site/site-build.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { loadProjectConfig } from "./site-loader.ts";
|
|
|
32
32
|
import { discoverPages, expandDynamicRoutes, readPageDocument } from "./pages-discovery.ts";
|
|
33
33
|
import { buildProjectExtensionRegistry } from "./format-host.ts";
|
|
34
34
|
import { loadProjectSections } from "./project-sections.ts";
|
|
35
|
+
import { collectAssetRefs, copyMountedAssets, loadAssetMounts } from "./asset-mounts.ts";
|
|
36
|
+
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
35
37
|
import type { FormatEntry, FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
36
38
|
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
37
39
|
import { resolveLayout } from "./layout-resolver.ts";
|
|
@@ -208,6 +210,26 @@ export async function buildSite(
|
|
|
208
210
|
log(` Loaded ${sectionKeys.length} section(s): ${sectionKeys.join(", ")}`);
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
// ── 3c. Extension asset mounts (extensions.md §8.5) ─────────────────────
|
|
214
|
+
// Directories published at a site URL — typically an external content source's co-located
|
|
215
|
+
// Images. They resolve for image optimization below, and the files pages actually reference
|
|
216
|
+
// Are copied into dist in step 7c.
|
|
217
|
+
const { errors: mountErrors, mounts: assetMounts } = await loadAssetMounts(
|
|
218
|
+
registry,
|
|
219
|
+
projectConfig,
|
|
220
|
+
projectRoot,
|
|
221
|
+
);
|
|
222
|
+
errors.push(...mountErrors);
|
|
223
|
+
for (const message of mountErrors) {
|
|
224
|
+
console.error(message);
|
|
225
|
+
}
|
|
226
|
+
if (assetMounts.length > 0) {
|
|
227
|
+
log(
|
|
228
|
+
` Mounted ${assetMounts.length} asset dir(s): ${assetMounts.map((m) => m.urlPrefix).join(", ")}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const assetRefs = new Set<string>();
|
|
232
|
+
|
|
211
233
|
// ── 4. Expand dynamic routes ────────────────────────────────────────────
|
|
212
234
|
const routes = await expandDynamicRoutes(
|
|
213
235
|
staticRoutes,
|
|
@@ -262,6 +284,7 @@ export async function buildSite(
|
|
|
262
284
|
const css = buildComponentCSS(doc.tagName, doc.style, doc, projectConfig.$media ?? {});
|
|
263
285
|
if (css) {
|
|
264
286
|
componentCSS.set(doc.tagName, css);
|
|
287
|
+
collectAssetRefs(css, assetMounts, assetRefs);
|
|
265
288
|
writeFileSync(resolve(componentOutDir, `${doc.tagName}.css`), css, "utf8");
|
|
266
289
|
fileCount += 1;
|
|
267
290
|
}
|
|
@@ -325,6 +348,7 @@ export async function buildSite(
|
|
|
325
348
|
formatRegistry,
|
|
326
349
|
registry,
|
|
327
350
|
rewriteSidecarSrc,
|
|
351
|
+
assetMounts,
|
|
328
352
|
);
|
|
329
353
|
|
|
330
354
|
// Determine which component tags are fully static (for script omission)
|
|
@@ -345,6 +369,10 @@ export async function buildSite(
|
|
|
345
369
|
);
|
|
346
370
|
}
|
|
347
371
|
|
|
372
|
+
// Mounted assets this page actually references — scanned from the finished HTML so a
|
|
373
|
+
// Reference is caught wherever it came from (markdown image, hand-authored page, $head).
|
|
374
|
+
collectAssetRefs(result.html, assetMounts, assetRefs);
|
|
375
|
+
|
|
348
376
|
// Determine output path
|
|
349
377
|
const outPath = routeToOutputPath(route.urlPattern, outDir, trailingSlash);
|
|
350
378
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
@@ -568,7 +596,19 @@ export async function buildSite(
|
|
|
568
596
|
console.warn("sitemap.xml skipped — set `url` in project.json to enable sitemap generation.");
|
|
569
597
|
}
|
|
570
598
|
|
|
571
|
-
// ── 7c. Copy
|
|
599
|
+
// ── 7c. Copy referenced mounted assets (extensions.md §8.5) ─────────────
|
|
600
|
+
// Only files the compiled output references are copied, so a content source's entry files
|
|
601
|
+
// Never land in dist. Runs before the public/ copy, which keeps shadowing everything.
|
|
602
|
+
if (assetRefs.size > 0) {
|
|
603
|
+
const { copied, missing } = copyMountedAssets(assetRefs, assetMounts, outDir);
|
|
604
|
+
fileCount += copied;
|
|
605
|
+
log(`Copying ${copied} mounted asset(s)...`);
|
|
606
|
+
for (const url of missing) {
|
|
607
|
+
console.warn(`Referenced asset not found: ${url}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── 7c(ii). Copy public/ assets ─────────────────────────────────────────
|
|
572
612
|
if (existsSync(publicDir)) {
|
|
573
613
|
log("Copying public/ assets...");
|
|
574
614
|
cpSync(publicDir, outDir, { recursive: true });
|
|
@@ -630,6 +670,7 @@ async function compilePage(
|
|
|
630
670
|
formatRegistry?: FormatRegistry,
|
|
631
671
|
registry?: ExtensionRegistry,
|
|
632
672
|
rewriteSidecarSrc?: (specifier: string, docDir: string | null) => string,
|
|
673
|
+
assetMounts: readonly AssetMount[] = [],
|
|
633
674
|
) {
|
|
634
675
|
// Load the raw page document (.json natively, other formats via the registry)
|
|
635
676
|
const pageDoc = await readPageDocument(route.sourcePath as string, formatRegistry);
|
|
@@ -744,6 +785,7 @@ async function compilePage(
|
|
|
744
785
|
projectRoot,
|
|
745
786
|
imageCache,
|
|
746
787
|
imageMetaCache ?? undefined,
|
|
788
|
+
assetMounts,
|
|
747
789
|
);
|
|
748
790
|
}
|
|
749
791
|
|