@jxsuite/compiler 8.1.0 → 8.3.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 +207 -159
- package/dist/compiler.js +125 -81
- package/package.json +7 -7
- package/src/cli.ts +21 -4
- package/src/compiler.ts +3 -3
- package/src/site/convert-command.ts +59 -0
- package/src/site/format-host.ts +3 -2
- package/src/site/layout-resolver.ts +4 -3
- package/src/site/pages-discovery.ts +9 -4
- package/src/site/project-sections.ts +6 -3
- package/src/site/schema-command.ts +2 -1
- package/src/site/site-build.ts +15 -9
- package/src/site/site-loader.ts +2 -1
- package/src/site/validate-command.ts +58 -6
- package/src/targets/compile-element.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.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.5.
|
|
56
|
-
"@jxsuite/runtime": "^6.0
|
|
57
|
-
"@jxsuite/schema": "^5.
|
|
58
|
-
"@jxsuite/site": "^2.
|
|
55
|
+
"@jxsuite/create": "^1.5.5",
|
|
56
|
+
"@jxsuite/runtime": "^6.2.0",
|
|
57
|
+
"@jxsuite/schema": "^5.3.0",
|
|
58
|
+
"@jxsuite/site": "^2.4.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.
|
|
66
|
+
"@jxsuite/connector": "^0.8.5",
|
|
67
|
+
"@jxsuite/parser": "^4.3.1",
|
|
68
68
|
"@jxsuite/test-support": "^0.0.0",
|
|
69
69
|
"@types/mdast": "^4.0.4"
|
|
70
70
|
}
|
package/src/cli.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Jx — Unified CLI for the Jx platform
|
|
4
4
|
*
|
|
5
|
-
* Commands: jx build [project-root] [--verbose] [--no-clean]
|
|
6
|
-
* [project-root]
|
|
5
|
+
* Commands: jx build [project-root] [--verbose] [--no-clean] [--include-held] jx schema
|
|
6
|
+
* [project-root] jx validate [project-root]
|
|
7
7
|
*
|
|
8
8
|
* @module jx-cli
|
|
9
9
|
*/
|
|
@@ -29,18 +29,34 @@ 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 every reference to them
|
|
32
33
|
db push [root] Sync the data section's tables to their connections (additive-only)
|
|
33
34
|
|
|
34
35
|
Options:
|
|
35
36
|
--verbose Print detailed build progress
|
|
36
37
|
--no-clean Don't clean outDir before building
|
|
38
|
+
--include-held Keep entries a content type's hold rules match, for a preview
|
|
37
39
|
--port <n> dev/preview: port to listen on (default 3000 / 4173)
|
|
38
40
|
--dry-run db push: print the statements without executing them
|
|
39
41
|
--connection db push: restrict to one connection name
|
|
40
|
-
--strict validate: fail on an overlay or accessibility lint error, not only a schema error
|
|
42
|
+
--strict validate: fail on an overlay or accessibility lint error, not only a schema error
|
|
43
|
+
--to <format> convert: jxn or json
|
|
44
|
+
--drop-comments convert: convert a .jxn file to .json even though it has comments`);
|
|
41
45
|
process.exit(0);
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
// Its positionals are paths and not a project root, so it does not join the split below.
|
|
49
|
+
if (command === "convert") {
|
|
50
|
+
try {
|
|
51
|
+
const { runConvert } = await import("./site/convert-command.ts");
|
|
52
|
+
runConvert(process.cwd(), args.slice(1));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(errorMessage(error));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
// `db push` is a two-word command; normalize it before the flag/positional split.
|
|
45
61
|
const isDb = command === "db";
|
|
46
62
|
const dbSubcommand = isDb ? args[1] : null;
|
|
@@ -110,12 +126,13 @@ Options:
|
|
|
110
126
|
if (command === "build") {
|
|
111
127
|
const verbose = flags.has("--verbose");
|
|
112
128
|
const clean = !flags.has("--no-clean");
|
|
129
|
+
const includeHeld = flags.has("--include-held");
|
|
113
130
|
|
|
114
131
|
console.log(`Building site from ${projectRoot}...`);
|
|
115
132
|
|
|
116
133
|
try {
|
|
117
134
|
const { buildSite } = await import("./site/site-build.ts");
|
|
118
|
-
const result = await buildSite(projectRoot, { clean, verbose });
|
|
135
|
+
const result = await buildSite(projectRoot, { clean, includeHeld, verbose });
|
|
119
136
|
|
|
120
137
|
if (result.errors.length > 0) {
|
|
121
138
|
console.error(`\nBuild completed with ${result.errors.length} error(s):`);
|
package/src/compiler.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { emitElementModule } from "./targets/compile-element.ts";
|
|
|
27
27
|
import { compileStaticPage } from "./targets/compile-static.ts";
|
|
28
28
|
import { compileClient } from "./targets/compile-client.ts";
|
|
29
29
|
import { isClassDef, isString } from "@jxsuite/schema/guards";
|
|
30
|
-
import {
|
|
30
|
+
import { isNativeDocumentPath, parseNativeDocument } from "@jxsuite/schema/jxn";
|
|
31
31
|
import type { JxDocument, JxStyle } from "@jxsuite/schema/types";
|
|
32
32
|
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
33
33
|
|
|
@@ -94,8 +94,8 @@ export async function compile(sourcePath: string | JxDocument, opts: CompileOpti
|
|
|
94
94
|
let raw: JxDocument;
|
|
95
95
|
if (isString(sourcePath)) {
|
|
96
96
|
const source = readFileSync(sourcePath, "utf8");
|
|
97
|
-
if (sourcePath
|
|
98
|
-
raw =
|
|
97
|
+
if (isNativeDocumentPath(sourcePath)) {
|
|
98
|
+
raw = parseNativeDocument(source, sourcePath);
|
|
99
99
|
} else {
|
|
100
100
|
const { extname } = await import("node:path");
|
|
101
101
|
const ext = extname(sourcePath).toLowerCase();
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert command: `jx convert` resolves the project's `@jxsuite/server/convert` and runs it under
|
|
3
|
+
* Bun, as `jx dev` does and for the same reasons. The compiler cannot depend on the server, and the
|
|
4
|
+
* refactor pass that repairs references ships as TypeScript this Node entry cannot load.
|
|
5
|
+
*
|
|
6
|
+
* @module convert-command
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import type { ChildProcess } from "node:child_process";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { ProcessLike } from "./dev-command.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the project's converter.
|
|
16
|
+
*
|
|
17
|
+
* @param projectRoot Absolute project root.
|
|
18
|
+
* @throws {Error} When the server package is not installed.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveConvertEntry(projectRoot: string): string {
|
|
21
|
+
try {
|
|
22
|
+
return createRequire(join(projectRoot, "package.json")).resolve("@jxsuite/server/convert");
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`@jxsuite/server is not installed in ${projectRoot}. ` +
|
|
26
|
+
`Add it to devDependencies (e.g. \`bun add -d @jxsuite/server\`) and retry.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Spawn the converter under Bun.
|
|
33
|
+
*
|
|
34
|
+
* @param projectRoot Absolute project root.
|
|
35
|
+
* @param argv The arguments after the command.
|
|
36
|
+
* @param spawnImpl Injectable for tests.
|
|
37
|
+
* @param proc Injectable for tests.
|
|
38
|
+
*/
|
|
39
|
+
export function runConvert(
|
|
40
|
+
projectRoot: string,
|
|
41
|
+
argv: readonly string[],
|
|
42
|
+
spawnImpl: typeof spawn = spawn,
|
|
43
|
+
proc: ProcessLike = process,
|
|
44
|
+
): ChildProcess {
|
|
45
|
+
const entry = resolveConvertEntry(projectRoot);
|
|
46
|
+
const child = spawnImpl("bun", [entry, "--root", projectRoot, ...argv], { stdio: "inherit" });
|
|
47
|
+
child.on("error", (error: NodeJS.ErrnoException) => {
|
|
48
|
+
console.error(
|
|
49
|
+
error.code === "ENOENT"
|
|
50
|
+
? "jx convert runs on Bun, which was not found on your PATH. Install it from https://bun.sh and retry."
|
|
51
|
+
: `Failed to start jx convert: ${error.message}`,
|
|
52
|
+
);
|
|
53
|
+
proc.exitCode = 1;
|
|
54
|
+
});
|
|
55
|
+
child.on("exit", (code) => {
|
|
56
|
+
proc.exitCode = code ?? 0;
|
|
57
|
+
});
|
|
58
|
+
return child;
|
|
59
|
+
}
|
package/src/site/format-host.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from "@jxsuite/schema/format-registry";
|
|
21
21
|
import { isJsonObject } from "@jxsuite/schema/guards";
|
|
22
22
|
import type { JsonObject, JsonValue, ProjectConfig } from "@jxsuite/schema/types";
|
|
23
|
+
import { projectFilePath } from "@jxsuite/schema/project-file/node";
|
|
23
24
|
|
|
24
25
|
export type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
25
26
|
export type { FormatRegistry, FormatEntry } from "@jxsuite/schema/format-registry";
|
|
@@ -133,7 +134,7 @@ export async function buildProjectExtensionRegistry(
|
|
|
133
134
|
return buildExtensionRegistry(
|
|
134
135
|
projectConfig?.extensions,
|
|
135
136
|
createNodeFormatIO(projectRoot),
|
|
136
|
-
|
|
137
|
+
projectFilePath(projectRoot),
|
|
137
138
|
);
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -176,7 +177,7 @@ export async function buildInstalledExtensionRegistry(
|
|
|
176
177
|
const registry = await buildExtensionRegistry(
|
|
177
178
|
present,
|
|
178
179
|
createNodeFormatIO(projectRoot),
|
|
179
|
-
|
|
180
|
+
projectFilePath(projectRoot),
|
|
180
181
|
);
|
|
181
182
|
return { missing, registry };
|
|
182
183
|
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
-
import {
|
|
12
|
+
import { parseNativeDocument } from "@jxsuite/schema/jxn";
|
|
13
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
13
14
|
import { resolve } from "node:path";
|
|
14
15
|
import { resolveLayout as resolveLayoutWith } from "@jxsuite/site/layout";
|
|
15
16
|
import type { JxDocument, ProjectConfig } from "@jxsuite/schema/types";
|
|
@@ -26,9 +27,9 @@ export function nodeLayoutLoader(projectRoot: string): (layoutRef: string) => Jx
|
|
|
26
27
|
throw new Error(`Layout not found: ${layoutRef} (resolved to ${layoutPath})`);
|
|
27
28
|
}
|
|
28
29
|
try {
|
|
29
|
-
return
|
|
30
|
+
return parseNativeDocument(readFileSync(layoutPath, "utf8"), layoutPath);
|
|
30
31
|
} catch (error) {
|
|
31
|
-
throw new Error(`Invalid layout
|
|
32
|
+
throw new Error(`Invalid layout at ${layoutPath}: ${errorMessage(error)}`, {
|
|
32
33
|
cause: error,
|
|
33
34
|
});
|
|
34
35
|
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
20
20
|
import { isJsonObject, isString } from "@jxsuite/schema/guards";
|
|
21
|
-
import {
|
|
21
|
+
import { isNativeDocumentPath, parseNativeDocument } from "@jxsuite/schema/jxn";
|
|
22
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
22
23
|
import { compareRoutes, fileToRoute as routeFromFileName } from "@jxsuite/site/routes";
|
|
23
24
|
import { pageCount, pageSlice, pageUrl, pagedSource, pagination } from "@jxsuite/site/pagination";
|
|
24
25
|
import type { PagePagination, PagedSource } from "@jxsuite/site/pagination";
|
|
@@ -80,8 +81,8 @@ export async function readPageDocument(
|
|
|
80
81
|
registry?: FormatRegistry,
|
|
81
82
|
): Promise<JxDocument> {
|
|
82
83
|
const source = readFileSync(filePath, "utf8");
|
|
83
|
-
if (filePath
|
|
84
|
-
return
|
|
84
|
+
if (isNativeDocumentPath(filePath)) {
|
|
85
|
+
return parseNativeDocument(source, filePath);
|
|
85
86
|
}
|
|
86
87
|
const ext = extname(filePath).toLowerCase();
|
|
87
88
|
const entry = registry?.byExtension(ext, "parse");
|
|
@@ -200,7 +201,11 @@ async function declaredTranslationKey(
|
|
|
200
201
|
*/
|
|
201
202
|
export async function discoverPages(pagesDir: string, registry?: FormatRegistry) {
|
|
202
203
|
const routes: Route[] = [];
|
|
203
|
-
const pageExtensions = new Set([
|
|
204
|
+
const pageExtensions = new Set([
|
|
205
|
+
".json",
|
|
206
|
+
".jxn",
|
|
207
|
+
...(registry?.documentExtensions("page") ?? []),
|
|
208
|
+
]);
|
|
204
209
|
await walkDir(pagesDir, pagesDir, routes, pageExtensions, registry);
|
|
205
210
|
|
|
206
211
|
// Static routes first, then dynamic, then catch-all (spec §4.4) — the shared ordering.
|
|
@@ -29,15 +29,17 @@ export type ProjectSections = Record<string, ProjectSectionData>;
|
|
|
29
29
|
* @param {string} projectRoot - Absolute path to the project root
|
|
30
30
|
* @param {ProjectConfig | undefined} projectConfig - Already-loaded project config
|
|
31
31
|
* @param {ExtensionRegistry} registry - The project's extension registry
|
|
32
|
-
* @param {{ lazy?: boolean }} [options] - `lazy`: the host serves one page
|
|
33
|
-
* may load its section on demand (specs/extensions.md §8). A build leaves
|
|
32
|
+
* @param {{ lazy?: boolean; includeHeld?: boolean }} [options] - `lazy`: the host serves one page
|
|
33
|
+
* at a time, so an owner may load its section on demand (specs/extensions.md §8). A build leaves
|
|
34
|
+
* it off. `includeHeld`: the host shows authors their work, so entries a project's `hold` rules
|
|
35
|
+
* match are kept, labelled (site-architecture.md §7.6). Left off, they are left out.
|
|
34
36
|
* @returns {Promise<ProjectSections>} Section key → projectData result
|
|
35
37
|
*/
|
|
36
38
|
export async function loadProjectSections(
|
|
37
39
|
projectRoot: string,
|
|
38
40
|
projectConfig: ProjectConfig | undefined,
|
|
39
41
|
registry: ExtensionRegistry,
|
|
40
|
-
options: { lazy?: boolean } = {},
|
|
42
|
+
options: { lazy?: boolean; includeHeld?: boolean } = {},
|
|
41
43
|
): Promise<ProjectSections> {
|
|
42
44
|
const sections: ProjectSections = {};
|
|
43
45
|
const io = createNodeFormatIO(projectRoot);
|
|
@@ -55,6 +57,7 @@ export async function loadProjectSections(
|
|
|
55
57
|
registry,
|
|
56
58
|
root: projectRoot,
|
|
57
59
|
...(options.lazy === true && { lazy: true }),
|
|
60
|
+
...(options.includeHeld === true && { includeHeld: true }),
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
return sections;
|
|
@@ -29,6 +29,7 @@ import { loadProjectConfig } from "./site-loader.ts";
|
|
|
29
29
|
import type { ExtensionInfo } from "@jxsuite/schema/extension-registry";
|
|
30
30
|
import type { SchemaJsonLoader } from "@jxsuite/schema/project-schemas";
|
|
31
31
|
import type { JsonObject, JsonValue } from "@jxsuite/schema/types";
|
|
32
|
+
import { projectFilePath } from "@jxsuite/schema/project-file/node";
|
|
32
33
|
|
|
33
34
|
const CORE_PROJECT_SPECIFIER = "@jxsuite/schema/schemas/project.core.schema.json";
|
|
34
35
|
const CORE_DOCUMENT_SPECIFIER = "@jxsuite/schema/schema.json";
|
|
@@ -95,7 +96,7 @@ export async function readBundledProjectSchemas(
|
|
|
95
96
|
) {
|
|
96
97
|
const { write = true } = options;
|
|
97
98
|
const root = resolve(projectRoot);
|
|
98
|
-
const projectJsonPath =
|
|
99
|
+
const projectJsonPath = projectFilePath(root);
|
|
99
100
|
const projectSchemaPath = resolve(root, "project.schema.json");
|
|
100
101
|
const documentSchemaPath = resolve(root, "document.schema.json");
|
|
101
102
|
|
package/src/site/site-build.ts
CHANGED
|
@@ -160,6 +160,7 @@ import type {
|
|
|
160
160
|
} from "@jxsuite/schema/types";
|
|
161
161
|
import type { JxScope } from "@jxsuite/runtime/types";
|
|
162
162
|
import type { CacheManifest } from "./image-cache.js";
|
|
163
|
+
import { projectFilePath } from "@jxsuite/schema/project-file/node";
|
|
163
164
|
|
|
164
165
|
declare module "@jxsuite/schema/format-registry" {
|
|
165
166
|
/**
|
|
@@ -188,6 +189,8 @@ const ADAPTER_REMEDY =
|
|
|
188
189
|
* @param {object} [options]
|
|
189
190
|
* @param {boolean} [options.clean] - Remove outDir before building
|
|
190
191
|
* @param {boolean} [options.verbose] - Log progress
|
|
192
|
+
* @param {boolean} [options.includeHeld] - Keep entries a content type's `hold` rules match, for a
|
|
193
|
+
* preview. Left off, a build leaves them out (site-architecture.md §7.6).
|
|
191
194
|
* @returns {Promise<{ routes: number; files: number; errors: string[] }>}
|
|
192
195
|
*/
|
|
193
196
|
export async function buildSite(
|
|
@@ -195,9 +198,10 @@ export async function buildSite(
|
|
|
195
198
|
options: {
|
|
196
199
|
clean?: boolean;
|
|
197
200
|
verbose?: boolean;
|
|
201
|
+
includeHeld?: boolean;
|
|
198
202
|
} = {},
|
|
199
203
|
) {
|
|
200
|
-
const { clean = true, verbose = false } = options;
|
|
204
|
+
const { clean = true, includeHeld = false, verbose = false } = options;
|
|
201
205
|
const errors: string[] = [];
|
|
202
206
|
const log = verbose ? console.log.bind(console) : () => {};
|
|
203
207
|
|
|
@@ -395,7 +399,7 @@ export async function buildSite(
|
|
|
395
399
|
|
|
396
400
|
// ── 3b. Load extension-contributed project sections ────────────────────
|
|
397
401
|
log("Loading project sections...");
|
|
398
|
-
const sections = await loadProjectSections(projectRoot, projectConfig, registry);
|
|
402
|
+
const sections = await loadProjectSections(projectRoot, projectConfig, registry, { includeHeld });
|
|
399
403
|
const sectionKeys = Object.keys(sections);
|
|
400
404
|
if (sectionKeys.length > 0) {
|
|
401
405
|
log(` Loaded ${sectionKeys.length} section(s): ${sectionKeys.join(", ")}`);
|
|
@@ -460,9 +464,12 @@ export async function buildSite(
|
|
|
460
464
|
if (existsSync(componentsDir)) {
|
|
461
465
|
log("Compiling components...");
|
|
462
466
|
const componentExtensions = [".json", ...formatRegistry.documentExtensions("component")];
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
467
|
+
/* Sorted, because this order is the order of the sheets in every page's `<head>`, and readdir
|
|
468
|
+
has none of its own: on ext4 it follows a hash of the name, so the same project built to
|
|
469
|
+
different bytes on different machines. */
|
|
470
|
+
const componentFiles = readdirSync(componentsDir)
|
|
471
|
+
.filter((f: string) => componentExtensions.some((ext) => f.endsWith(ext)))
|
|
472
|
+
.toSorted();
|
|
466
473
|
const componentOutDir = resolve(outDir, "components");
|
|
467
474
|
mkdirSync(componentOutDir, { recursive: true });
|
|
468
475
|
|
|
@@ -502,7 +509,7 @@ export async function buildSite(
|
|
|
502
509
|
resolveElementPath: (refPath: string, currentDir: string | null) => {
|
|
503
510
|
const depPath = currentDir ? resolve(currentDir, refPath) : resolve(refPath);
|
|
504
511
|
const depTag = componentTagByPath.get(depPath);
|
|
505
|
-
return depTag ? `./${depTag}.js` : refPath.replace(/\.json
|
|
512
|
+
return depTag ? `./${depTag}.js` : refPath.replace(/\.(?:json|jxn)$/i, ".js");
|
|
506
513
|
},
|
|
507
514
|
rewriteSrc: (specifier: string, docDir: string | null) =>
|
|
508
515
|
withBase(componentBase, rewriteSidecarSrc(specifier, docDir)),
|
|
@@ -599,7 +606,7 @@ export async function buildSite(
|
|
|
599
606
|
/* SAFETY: `ProjectConfig.state` is a plain record of state entries, and the collector reads
|
|
600
607
|
only a document's `state`, so a document carrying that one member is all it needs. */
|
|
601
608
|
{ state: projectConfig.state } as JxElement,
|
|
602
|
-
|
|
609
|
+
projectFilePath(projectRoot),
|
|
603
610
|
projectRoot,
|
|
604
611
|
),
|
|
605
612
|
);
|
|
@@ -1706,8 +1713,7 @@ async function compilePage(
|
|
|
1706
1713
|
|
|
1707
1714
|
return {
|
|
1708
1715
|
doc: layoutDoc,
|
|
1709
|
-
// A page opts out of the sitemap by setting `$sitemap: false
|
|
1710
|
-
// Until draft filtering lands in the build pipeline).
|
|
1716
|
+
// A page opts out of the sitemap by setting `$sitemap: false`.
|
|
1711
1717
|
excludeFromSitemap: pageDoc.$sitemap === false,
|
|
1712
1718
|
files: result.files,
|
|
1713
1719
|
html: result.html,
|
package/src/site/site-loader.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
9
9
|
import type { ImageConfig, JxDocumentValue, ProjectConfig } from "@jxsuite/schema/types";
|
|
10
10
|
import { errorMessage, parseProjectConfig } from "@jxsuite/schema/parse";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
|
+
import { projectFilePath } from "@jxsuite/schema/project-file/node";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Default project configuration. All properties are optional in project.json; these defaults fill
|
|
@@ -71,7 +72,7 @@ const DEFAULTS = {
|
|
|
71
72
|
* @throws {Error} If project.json is missing or invalid JSON
|
|
72
73
|
*/
|
|
73
74
|
export function loadProjectConfig(projectRoot: string) {
|
|
74
|
-
const configPath =
|
|
75
|
+
const configPath = projectFilePath(projectRoot);
|
|
75
76
|
|
|
76
77
|
if (!existsSync(configPath)) {
|
|
77
78
|
throw new Error(`project.json not found in ${projectRoot}`);
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
21
|
+
import { isJxnPath, isNativeDocumentPath, lintJxn, parseJxn } from "@jxsuite/schema/jxn";
|
|
22
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
21
23
|
import { createRequire } from "node:module";
|
|
22
24
|
import { join, relative, resolve } from "node:path";
|
|
23
25
|
import { generateClassSchema } from "@jxsuite/schema";
|
|
@@ -31,6 +33,7 @@ import type { JsonObject, JsonValue, JxElement } from "@jxsuite/schema/types";
|
|
|
31
33
|
import { buildProjectExtensionRegistry } from "./format-host.ts";
|
|
32
34
|
import { loadProjectConfig } from "./site-loader.ts";
|
|
33
35
|
import { readBundledProjectSchemas } from "./schema-command.ts";
|
|
36
|
+
import { PROJECT_FILE_NAME } from "@jxsuite/schema/project-file";
|
|
34
37
|
|
|
35
38
|
/** Directories that hold Jx documents validated against the document schema. */
|
|
36
39
|
const DOCUMENT_DIRS = ["components", "pages", "layouts"] as const;
|
|
@@ -63,8 +66,8 @@ export interface ProjectTreeLintFinding {
|
|
|
63
66
|
file: string;
|
|
64
67
|
/** The node's path inside the document. */
|
|
65
68
|
path: (string | number)[];
|
|
66
|
-
/** Which lint: `popover`, `dialog` or `
|
|
67
|
-
source: "popover" | "dialog" | "accessibility";
|
|
69
|
+
/** Which lint: `popover`, `dialog`, `accessibility`, or `notation` for a `.jxn` file's text. */
|
|
70
|
+
source: "popover" | "dialog" | "accessibility" | "notation";
|
|
68
71
|
rule: string;
|
|
69
72
|
message: string;
|
|
70
73
|
detail: string;
|
|
@@ -175,7 +178,7 @@ export async function validateProjectTree(
|
|
|
175
178
|
const projectResult = await validateProjectFile(root);
|
|
176
179
|
checked += 1;
|
|
177
180
|
if (!projectResult.valid) {
|
|
178
|
-
issues.push({ errors: projectResult.errors ?? [], file:
|
|
181
|
+
issues.push({ errors: projectResult.errors ?? [], file: PROJECT_FILE_NAME });
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
const { addFormats, Ajv } = await loadAjv();
|
|
@@ -212,7 +215,13 @@ export async function validateProjectTree(
|
|
|
212
215
|
continue; // Validated against the class schema below.
|
|
213
216
|
}
|
|
214
217
|
checked += 1;
|
|
215
|
-
const
|
|
218
|
+
const read = readDocumentFile(file, relative(root, file));
|
|
219
|
+
lint.push(...read.warnings);
|
|
220
|
+
if (read.failure !== null) {
|
|
221
|
+
issues.push(read.failure);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const doc = read.document;
|
|
216
225
|
if (validateDoc(doc)) {
|
|
217
226
|
/*
|
|
218
227
|
* Only a well-formed document is judged; a schema error is the report for the rest.
|
|
@@ -270,6 +279,49 @@ export async function validateProjectTree(
|
|
|
270
279
|
return { checked, issues, lint, valid: issues.length === 0 && !fatalLint };
|
|
271
280
|
}
|
|
272
281
|
|
|
282
|
+
/** A document file as the validator takes it: its value, or why it has none. */
|
|
283
|
+
interface ReadDocumentFile {
|
|
284
|
+
document: JsonValue;
|
|
285
|
+
failure: ProjectTreeIssue | null;
|
|
286
|
+
warnings: ProjectTreeLintFinding[];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Read a document file for checking. A `.jxn` file that does not read is a finding with a line and
|
|
291
|
+
* column, and not a stack trace, and an escape that changes what its code means is reported whether
|
|
292
|
+
* or not the rest of the file reads.
|
|
293
|
+
*/
|
|
294
|
+
function readDocumentFile(file: string, name: string): ReadDocumentFile {
|
|
295
|
+
if (!isJxnPath(file)) {
|
|
296
|
+
return { document: readJson(file), failure: null, warnings: [] };
|
|
297
|
+
}
|
|
298
|
+
const text = readFileSync(file, "utf8");
|
|
299
|
+
const warnings = lintJxn(text).map((warning): ProjectTreeLintFinding => ({
|
|
300
|
+
detail: warning.message,
|
|
301
|
+
file: name,
|
|
302
|
+
message: `line ${warning.line}, column ${warning.column}: ${warning.message}`,
|
|
303
|
+
path: [],
|
|
304
|
+
rule: "escape",
|
|
305
|
+
severity: "warn",
|
|
306
|
+
source: "notation",
|
|
307
|
+
}));
|
|
308
|
+
try {
|
|
309
|
+
return { document: readJxnValue(text), failure: null, warnings };
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return {
|
|
312
|
+
document: null,
|
|
313
|
+
failure: { errors: [{ message: errorMessage(error) }], file: name },
|
|
314
|
+
warnings,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** A `.jxn` file's document, as the JSON value it is. */
|
|
320
|
+
function readJxnValue(text: string): JsonValue {
|
|
321
|
+
const document = parseJxn(text);
|
|
322
|
+
return isJsonObject(document) ? document : null;
|
|
323
|
+
}
|
|
324
|
+
|
|
273
325
|
/**
|
|
274
326
|
* Read and parse a JSON file. `JSON.parse` without a reviver only ever produces JSON values, so the
|
|
275
327
|
* declared type is exact; what shape of JSON it is stays for the caller to check.
|
|
@@ -431,7 +483,7 @@ const SUBSCHEMA_KEYWORDS = new Set([
|
|
|
431
483
|
"unevaluatedProperties",
|
|
432
484
|
]);
|
|
433
485
|
|
|
434
|
-
/** Recursively list
|
|
486
|
+
/** Recursively list document files under a directory (absent directories yield nothing). */
|
|
435
487
|
function walkJsonFiles(dir: string): string[] {
|
|
436
488
|
if (!existsSync(dir)) {
|
|
437
489
|
return [];
|
|
@@ -441,7 +493,7 @@ function walkJsonFiles(dir: string): string[] {
|
|
|
441
493
|
const path = join(dir, name);
|
|
442
494
|
if (statSync(path).isDirectory()) {
|
|
443
495
|
out.push(...walkJsonFiles(path));
|
|
444
|
-
} else if (name
|
|
496
|
+
} else if (isNativeDocumentPath(name)) {
|
|
445
497
|
out.push(path);
|
|
446
498
|
}
|
|
447
499
|
}
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
isTagExpression,
|
|
50
50
|
paramNames,
|
|
51
51
|
} from "@jxsuite/schema/guards";
|
|
52
|
-
import {
|
|
52
|
+
import { isNativeDocumentPath, parseNativeDocument } from "@jxsuite/schema/jxn";
|
|
53
53
|
import { resolveShadowMode } from "../shadow.ts";
|
|
54
54
|
import type { ShadowMode } from "../shadow.ts";
|
|
55
55
|
import type { FormatRegistry } from "@jxsuite/schema/format-registry";
|
|
@@ -132,8 +132,8 @@ export async function compileElement(
|
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
134
134
|
visited.add(filePath);
|
|
135
|
-
if (filePath
|
|
136
|
-
doc =
|
|
135
|
+
if (isNativeDocumentPath(filePath)) {
|
|
136
|
+
doc = parseNativeDocument(readFileSync(filePath, "utf8"), filePath);
|
|
137
137
|
} else {
|
|
138
138
|
const { extname } = await import("node:path");
|
|
139
139
|
const ext = extname(filePath).toLowerCase();
|
|
@@ -179,7 +179,7 @@ export async function compileElement(
|
|
|
179
179
|
/** @type {string} */
|
|
180
180
|
const importPath = resolveElementPath
|
|
181
181
|
? resolveElementPath(refPath, currentDir)
|
|
182
|
-
: refPath.replace(/\.json
|
|
182
|
+
: refPath.replace(/\.(?:json|jxn)$/i, ".js");
|
|
183
183
|
elementImports.push(importPath);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -197,7 +197,7 @@ export async function compileElement(
|
|
|
197
197
|
resolveShadowMode(doc, opts.defaults),
|
|
198
198
|
opts.serverBaseUrl,
|
|
199
199
|
);
|
|
200
|
-
const outputPath = filePath ? filePath.replace(/\.json
|
|
200
|
+
const outputPath = filePath ? filePath.replace(/\.(?:json|jxn)$/i, ".js") : `${tagName}.js`;
|
|
201
201
|
files.push({ content: jsContent, path: outputPath, tagName });
|
|
202
202
|
}
|
|
203
203
|
|