@jxsuite/compiler 8.3.0 → 8.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/dist/cli.js +168 -167
- package/dist/compiler.js +82 -82
- package/package.json +7 -7
- package/src/cli.ts +1 -1
- package/src/site/image-cache.ts +8 -3
- package/src/site/image-optimizer.ts +20 -1
- package/src/site/image-transform.ts +19 -5
- package/src/site/site-loader.ts +8 -7
- package/src/site/validate-command.ts +2 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.4.0",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"upgrade": "bun update --latest && bun install"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@jxsuite/create": "^1.
|
|
56
|
-
"@jxsuite/runtime": "^6.2.
|
|
57
|
-
"@jxsuite/schema": "^5.
|
|
58
|
-
"@jxsuite/site": "^2.
|
|
55
|
+
"@jxsuite/create": "^1.6.0",
|
|
56
|
+
"@jxsuite/runtime": "^6.2.1",
|
|
57
|
+
"@jxsuite/schema": "^5.4.0",
|
|
58
|
+
"@jxsuite/site": "^2.5.0",
|
|
59
59
|
"@vue/reactivity": "3.5.44",
|
|
60
60
|
"esbuild": "^0.28.2",
|
|
61
61
|
"lit-html": "^3.3.3",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@happy-dom/global-registrator": "^21.0.4",
|
|
66
|
-
"@jxsuite/connector": "^0.8.
|
|
67
|
-
"@jxsuite/parser": "^4.3.
|
|
66
|
+
"@jxsuite/connector": "^0.8.6",
|
|
67
|
+
"@jxsuite/parser": "^4.3.2",
|
|
68
68
|
"@jxsuite/test-support": "^0.0.0",
|
|
69
69
|
"@types/mdast": "^4.0.4"
|
|
70
70
|
}
|
package/src/cli.ts
CHANGED
|
@@ -29,7 +29,7 @@ Commands:
|
|
|
29
29
|
preview [root] Serve an already-built dist/ directory
|
|
30
30
|
schema [root] Generate project.schema.json + document.schema.json from project.json#/extensions
|
|
31
31
|
validate [root] Validate the whole project: project.json, documents, classes, and fragments
|
|
32
|
-
convert <paths…> Move documents between .json and .jxn, repairing
|
|
32
|
+
convert <paths…> Move documents and the project file between .json and .jxn, repairing references
|
|
33
33
|
db push [root] Sync the data section's tables to their connections (additive-only)
|
|
34
34
|
|
|
35
35
|
Options:
|
package/src/site/image-cache.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { basename, resolve } from "node:path";
|
|
10
|
+
import { basename, extname, resolve } from "node:path";
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { configHash, contentHash } from "./image-optimizer.ts";
|
|
@@ -112,14 +112,19 @@ export function getImageCacheDir(projectRoot: string): string {
|
|
|
112
112
|
// ─── Cache key ───────────────────────────────────────────────────────────────
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Build a cache key from source file content and
|
|
115
|
+
* Build a cache key from source file content, config, and the file's name.
|
|
116
|
+
*
|
|
117
|
+
* The name is part of the key because it is part of the output: variants are named after the source
|
|
118
|
+
* file (`variantFilename`). Keyed on content alone, a renamed photo found its old entry and kept
|
|
119
|
+
* the old variant names, so renaming a file to fix its URLs changed nothing until the cache was
|
|
120
|
+
* deleted by hand. The old entry is untouched by the build that renames it, so pruning removes it.
|
|
116
121
|
*
|
|
117
122
|
* @param {string} srcPath - Absolute path to source image
|
|
118
123
|
* @param {ImageConfig} config
|
|
119
124
|
* @returns {string}
|
|
120
125
|
*/
|
|
121
126
|
export function cacheKey(srcPath: string, config: ImageConfig) {
|
|
122
|
-
return `${contentHash(srcPath)}:${configHash(config)}`;
|
|
127
|
+
return `${contentHash(srcPath)}:${configHash(config)}:${basename(srcPath, extname(srcPath))}`;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
// ─── Load / save ─────────────────────────────────────────────────────────────
|
|
@@ -263,6 +263,25 @@ export async function processImage(srcPath: string, cacheImgDir: string, config:
|
|
|
263
263
|
*/
|
|
264
264
|
export function buildSrcset(variants: ImageVariant[], format: string) {
|
|
265
265
|
return variants
|
|
266
|
-
.flatMap((v) => (v.format === format ? [`${v.outputPath} ${v.width}w`] : []))
|
|
266
|
+
.flatMap((v) => (v.format === format ? [`${variantUrl(v.outputPath)} ${v.width}w`] : []))
|
|
267
267
|
.join(", ");
|
|
268
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The URL of an emitted variant: its output path with each segment percent-encoded.
|
|
272
|
+
*
|
|
273
|
+
* A variant is named after its source file, and a source file may be named anything. Written raw, a
|
|
274
|
+
* name with a space breaks a `srcset`, which splits candidates on whitespace, so `Designer, White,
|
|
275
|
+
* Black.jpg` became a candidate for `…/Designer,` and a descriptor of `White,`. A comma is legal
|
|
276
|
+
* inside a candidate URL but not at its end, and is encoded too, so no file name can end a
|
|
277
|
+
* candidate early. The file on disk keeps its name: a server decodes the path back to it.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} outputPath - A variant's `outputPath`, as written to disk
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
export function variantUrl(outputPath: string) {
|
|
283
|
+
return outputPath
|
|
284
|
+
.split("/")
|
|
285
|
+
.map((segment) => encodeURIComponent(segment))
|
|
286
|
+
.join("/");
|
|
287
|
+
}
|
|
@@ -15,12 +15,12 @@ import { existsSync } from "node:fs";
|
|
|
15
15
|
import { basename, extname, resolve } from "node:path";
|
|
16
16
|
import {
|
|
17
17
|
buildSrcset,
|
|
18
|
-
configHash,
|
|
19
18
|
contentHash,
|
|
20
19
|
getImageMetadata,
|
|
21
20
|
processImage,
|
|
21
|
+
variantUrl,
|
|
22
22
|
} from "./image-optimizer.ts";
|
|
23
|
-
import { getCached, getImageCacheDir, setCached } from "./image-cache.ts";
|
|
23
|
+
import { cacheKey, getCached, getImageCacheDir, setCached } from "./image-cache.ts";
|
|
24
24
|
import { imgLoadingAttrs } from "./img-loading.ts";
|
|
25
25
|
import { resolveAssetUrl } from "@jxsuite/schema/asset-paths";
|
|
26
26
|
import { isNodeObject, isNonNullObject, isString } from "@jxsuite/schema/guards";
|
|
@@ -96,17 +96,31 @@ function buildCloudflareSrcset(
|
|
|
96
96
|
const quality = config.quality.webp ?? 80;
|
|
97
97
|
const separator = src.startsWith("/") ? "" : "/";
|
|
98
98
|
const suffix = hash ? `?v=${hash}` : "";
|
|
99
|
+
const path = srcsetSafe(src);
|
|
99
100
|
return config.widths
|
|
100
101
|
.filter((w) => w <= originalWidth)
|
|
101
102
|
.toSorted((a, b) => a - b)
|
|
102
103
|
.map(
|
|
103
104
|
(w) =>
|
|
104
105
|
`/cdn-cgi/image/width=${w},quality=${quality},fit=scale-down,format=auto` +
|
|
105
|
-
`${separator}${
|
|
106
|
+
`${separator}${path}${suffix} ${w}w`,
|
|
106
107
|
)
|
|
107
108
|
.join(", ");
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* An authored `src` made safe to stand as a `srcset` candidate: whitespace and commas encoded,
|
|
113
|
+
* everything else as written. A `src` attribute tolerates a space, but a `srcset` splits on it.
|
|
114
|
+
* Only those characters are touched, because an author's `src` may already be percent-encoded and
|
|
115
|
+
* encoding it whole would encode its `%` a second time.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} src
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
function srcsetSafe(src: string) {
|
|
121
|
+
return src.replaceAll(/[\s,]/g, (char) => encodeURIComponent(char));
|
|
122
|
+
}
|
|
123
|
+
|
|
110
124
|
/**
|
|
111
125
|
* @param {string} absoluteSrc
|
|
112
126
|
* @param {string} src
|
|
@@ -122,7 +136,7 @@ async function resolveManifest(
|
|
|
122
136
|
projectRoot: string,
|
|
123
137
|
cache: CacheManifest,
|
|
124
138
|
) {
|
|
125
|
-
const key =
|
|
139
|
+
const key = cacheKey(absoluteSrc, config);
|
|
126
140
|
const cached = getCached(cache, key);
|
|
127
141
|
|
|
128
142
|
if (cached) {
|
|
@@ -471,7 +485,7 @@ function fallbackVariant(variants: ImageVariant[], formats: readonly string[]):
|
|
|
471
485
|
best = variant;
|
|
472
486
|
}
|
|
473
487
|
}
|
|
474
|
-
return best
|
|
488
|
+
return best && variantUrl(best.outputPath);
|
|
475
489
|
}
|
|
476
490
|
|
|
477
491
|
/**
|
package/src/site/site-loader.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import type { ImageConfig, JxDocumentValue, ProjectConfig } from "@jxsuite/schema/types";
|
|
10
|
-
import { errorMessage
|
|
11
|
-
import {
|
|
10
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
11
|
+
import { parseProjectSource } from "@jxsuite/schema/project-file";
|
|
12
|
+
import { basename, resolve } from "node:path";
|
|
12
13
|
import { projectFilePath } from "@jxsuite/schema/project-file/node";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -65,24 +66,24 @@ const DEFAULTS = {
|
|
|
65
66
|
} satisfies Partial<ResolvedProjectConfig>;
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
|
-
* Load and validate project.json from a project root.
|
|
69
|
+
* Load and validate the project file, `project.jxn` or `project.json`, from a project root.
|
|
69
70
|
*
|
|
70
71
|
* @param {string} projectRoot - Absolute path to the project directory
|
|
71
72
|
* @returns {{ config: ProjectConfig; configPath: string; projectRoot: string }}
|
|
72
|
-
* @throws {Error} If
|
|
73
|
+
* @throws {Error} If there is no project file, there are two, or it does not read
|
|
73
74
|
*/
|
|
74
75
|
export function loadProjectConfig(projectRoot: string) {
|
|
75
76
|
const configPath = projectFilePath(projectRoot);
|
|
76
77
|
|
|
77
78
|
if (!existsSync(configPath)) {
|
|
78
|
-
throw new Error(`project.
|
|
79
|
+
throw new Error(`No project.jxn or project.json in ${projectRoot}`);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
let raw: ProjectConfig;
|
|
82
83
|
try {
|
|
83
|
-
raw =
|
|
84
|
+
raw = parseProjectSource(readFileSync(configPath, "utf8"), configPath);
|
|
84
85
|
} catch (error) {
|
|
85
|
-
throw new Error(`Invalid
|
|
86
|
+
throw new Error(`Invalid ${basename(configPath)}: ${errorMessage(error)}`, { cause: error });
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
// Deep merge with defaults. The `...raw` spread carries every author-declared key through
|
|
@@ -33,7 +33,6 @@ import type { JsonObject, JsonValue, JxElement } from "@jxsuite/schema/types";
|
|
|
33
33
|
import { buildProjectExtensionRegistry } from "./format-host.ts";
|
|
34
34
|
import { loadProjectConfig } from "./site-loader.ts";
|
|
35
35
|
import { readBundledProjectSchemas } from "./schema-command.ts";
|
|
36
|
-
import { PROJECT_FILE_NAME } from "@jxsuite/schema/project-file";
|
|
37
36
|
|
|
38
37
|
/** Directories that hold Jx documents validated against the document schema. */
|
|
39
38
|
const DOCUMENT_DIRS = ["components", "pages", "layouts"] as const;
|
|
@@ -174,11 +173,11 @@ export async function validateProjectTree(
|
|
|
174
173
|
return { checked, issues, lint, valid: false };
|
|
175
174
|
}
|
|
176
175
|
|
|
177
|
-
// 2. project
|
|
176
|
+
// 2. The project file against the generated entry schema.
|
|
178
177
|
const projectResult = await validateProjectFile(root);
|
|
179
178
|
checked += 1;
|
|
180
179
|
if (!projectResult.valid) {
|
|
181
|
-
issues.push({ errors: projectResult.errors ?? [], file:
|
|
180
|
+
issues.push({ errors: projectResult.errors ?? [], file: projectResult.file });
|
|
182
181
|
}
|
|
183
182
|
|
|
184
183
|
const { addFormats, Ajv } = await loadAjv();
|