@jxsuite/compiler 5.1.2 → 6.0.1
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 +218 -346
- package/dist/compiler.js +130 -258
- package/package.json +11 -10
- package/src/cli.ts +12 -14
- package/src/compiler.ts +45 -23
- package/src/shadow.ts +9 -8
- package/src/shared.ts +693 -329
- package/src/site/asset-mounts.ts +22 -12
- package/src/site/bundler.ts +38 -12
- package/src/site/client-runtime.ts +31 -17
- package/src/site/csp.ts +20 -11
- package/src/site/db-push.ts +38 -24
- package/src/site/dev-command.ts +3 -3
- package/src/site/format-host.ts +56 -45
- package/src/site/headers-emitter.ts +7 -4
- package/src/site/i18n.ts +20 -19
- package/src/site/image-cache.ts +16 -3
- package/src/site/image-optimizer.ts +19 -5
- package/src/site/image-transform.ts +56 -60
- package/src/site/img-loading.ts +11 -5
- package/src/site/layout-resolver.ts +7 -6
- package/src/site/link-relations.ts +10 -6
- package/src/site/locale-negotiation.ts +1 -1
- package/src/site/pages-discovery.ts +65 -47
- package/src/site/preview-server.ts +22 -19
- package/src/site/project-sections.ts +16 -3
- package/src/site/prototype-resolver.ts +94 -42
- package/src/site/schema-command.ts +26 -13
- package/src/site/service-worker.ts +2 -2
- package/src/site/site-build.ts +421 -279
- package/src/site/site-loader.ts +6 -3
- package/src/site/static-host.ts +3 -3
- package/src/site/validate-command.ts +64 -34
- package/src/site/well-known.ts +57 -12
- package/src/targets/compile-class.ts +17 -5
- package/src/targets/compile-client.ts +156 -111
- package/src/targets/compile-element.ts +227 -198
- package/src/targets/compile-server.ts +11 -5
- package/src/targets/compile-static.ts +57 -63
- package/src/types.ts +5 -6
package/src/site/site-loader.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
-
import type { ImageConfig, ProjectConfig } from "@jxsuite/schema/types";
|
|
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
12
|
|
|
@@ -24,9 +24,10 @@ export interface ResolvedProjectConfig extends ProjectConfig {
|
|
|
24
24
|
adapter?: string;
|
|
25
25
|
outDir: string;
|
|
26
26
|
format: string;
|
|
27
|
-
|
|
27
|
+
/** `null` when project.json writes it so explicitly: the merge below keeps an author's null. */
|
|
28
|
+
trailingSlash: string | null;
|
|
28
29
|
provider?: string | null;
|
|
29
|
-
[key: string]:
|
|
30
|
+
[key: string]: JxDocumentValue | undefined;
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -92,6 +93,8 @@ export function loadProjectConfig(projectRoot: string) {
|
|
|
92
93
|
build: { ...DEFAULTS.build, ...raw.build },
|
|
93
94
|
defaults: { ...DEFAULTS.defaults, ...raw.defaults },
|
|
94
95
|
images: { ...DEFAULTS.images, ...raw.images },
|
|
96
|
+
// An explicit JSON `null` url means no site URL, which every consumer reads as "".
|
|
97
|
+
url: raw.url ?? DEFAULTS.url,
|
|
95
98
|
};
|
|
96
99
|
|
|
97
100
|
// Preserve arrays and objects that shouldn't be shallow-merged
|
package/src/site/static-host.ts
CHANGED
|
@@ -40,10 +40,10 @@ export function isNotFoundRoute(urlPattern: string): boolean {
|
|
|
40
40
|
* `/` either way, and any other setting leaves the route as written.
|
|
41
41
|
*
|
|
42
42
|
* @param {string} urlPattern - A concrete route (`/about`, `/kb/a/b`)
|
|
43
|
-
* @param {string} [trailingSlash] - `build.trailingSlash`
|
|
43
|
+
* @param {string | null} [trailingSlash] - `build.trailingSlash`
|
|
44
44
|
* @returns {string} The route as the host serves it
|
|
45
45
|
*/
|
|
46
|
-
export function servedRoute(urlPattern: string, trailingSlash?: string): string {
|
|
46
|
+
export function servedRoute(urlPattern: string, trailingSlash?: string | null): string {
|
|
47
47
|
if (urlPattern === "/") {
|
|
48
48
|
return "/";
|
|
49
49
|
}
|
|
@@ -93,7 +93,7 @@ export function htmlDeclaresNoindex(html: string): boolean {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** The `<head>` of a built page, or the whole string when it has no closing tag. */
|
|
96
|
-
function headOf(html: string)
|
|
96
|
+
function headOf(html: string) {
|
|
97
97
|
const end = html.search(/<\/head\s*>/i);
|
|
98
98
|
return { end, head: end === -1 ? html : html.slice(0, end) };
|
|
99
99
|
}
|
|
@@ -26,7 +26,8 @@ import { findA11yDefects } from "@jxsuite/schema/a11y";
|
|
|
26
26
|
import { findDialogDefects } from "@jxsuite/schema/dialogs";
|
|
27
27
|
import { findPopoverDefects, overlayScopeFor } from "@jxsuite/schema/overlays";
|
|
28
28
|
import type { OverlayScope } from "@jxsuite/schema/overlays";
|
|
29
|
-
import
|
|
29
|
+
import { isJsonObject, isNonNullObject, isString } from "@jxsuite/schema/guards";
|
|
30
|
+
import type { JsonObject, JsonValue, JxElement } from "@jxsuite/schema/types";
|
|
30
31
|
import { buildProjectExtensionRegistry } from "./format-host.ts";
|
|
31
32
|
import { loadProjectConfig } from "./site-loader.ts";
|
|
32
33
|
import { readBundledProjectSchemas } from "./schema-command.ts";
|
|
@@ -88,12 +89,14 @@ export interface ProjectTreeValidationResult {
|
|
|
88
89
|
|
|
89
90
|
// Minimal structural types for the optional `ajv` dependency (same pattern as validate-project).
|
|
90
91
|
interface AjvValidateFn {
|
|
91
|
-
(doc:
|
|
92
|
+
(doc: JsonValue): boolean;
|
|
92
93
|
errors?: unknown[] | null;
|
|
93
94
|
}
|
|
95
|
+
/** What `compile` receives: parsed JSON, or the class schema generateClassSchema builds in memory. */
|
|
96
|
+
type AjvSchemaInput = JsonValue | ReturnType<typeof generateClassSchema>;
|
|
94
97
|
interface AjvInstance {
|
|
95
|
-
compile: (schema:
|
|
96
|
-
addSchema: (schema:
|
|
98
|
+
compile: (schema: AjvSchemaInput) => AjvValidateFn;
|
|
99
|
+
addSchema: (schema: JsonValue) => AjvInstance;
|
|
97
100
|
}
|
|
98
101
|
type AjvCtor = new (opts: {
|
|
99
102
|
allErrors: boolean;
|
|
@@ -104,8 +107,18 @@ type AddFormatsFn = (ajv: AjvInstance) => void;
|
|
|
104
107
|
|
|
105
108
|
async function loadAjv(): Promise<{ Ajv: AjvCtor; addFormats: AddFormatsFn }> {
|
|
106
109
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
/*
|
|
111
|
+
* The schemas are JSON Schema 2020-12, so use the matching Ajv build.
|
|
112
|
+
* SAFETY: the default export of `ajv/dist/2020` is the Ajv2020 class. AjvCtor restates, in
|
|
113
|
+
* JSON terms, only the three constructor options, `compile`, `addSchema` and the validate
|
|
114
|
+
* function's call and `errors` used below; ajv reports a schema value it cannot use as a thrown
|
|
115
|
+
* error.
|
|
116
|
+
*/
|
|
117
|
+
const { default: Ajv } = (await import("ajv/dist/2020")) as { default: AjvCtor };
|
|
118
|
+
/*
|
|
119
|
+
* SAFETY: ajv-formats' default export is the plugin function that takes an Ajv instance and
|
|
120
|
+
* registers the formats on it; AddFormatsFn states only that call.
|
|
121
|
+
*/
|
|
109
122
|
// @ts-expect-error — resolved through @jxsuite/schema's dependency
|
|
110
123
|
const { default: addFormats } = (await import("ajv-formats")) as { default: AddFormatsFn };
|
|
111
124
|
return { addFormats, Ajv };
|
|
@@ -141,7 +154,7 @@ export async function validateProjectTree(
|
|
|
141
154
|
continue;
|
|
142
155
|
}
|
|
143
156
|
checked += 1;
|
|
144
|
-
const entry =
|
|
157
|
+
const entry = readJson(path);
|
|
145
158
|
const unresolvable = collectUnresolvableRefs(entry);
|
|
146
159
|
if (unresolvable.length > 0) {
|
|
147
160
|
issues.push({
|
|
@@ -180,6 +193,12 @@ export async function validateProjectTree(
|
|
|
180
193
|
const scope = overlayScopeFor(
|
|
181
194
|
walkClassFiles(root).flatMap((file) => {
|
|
182
195
|
try {
|
|
196
|
+
/*
|
|
197
|
+
* SAFETY: not schema-checked here; the class-schema pass below reports a malformed
|
|
198
|
+
* definition. overlayScopeFor tolerates any parsed JSON in this slot: it skips `null`,
|
|
199
|
+
* reads `tagName` through isString (a primitive or array has none, so it is skipped), and
|
|
200
|
+
* reads `observedAttributes` through Array.isArray.
|
|
201
|
+
*/
|
|
183
202
|
return [JSON.parse(readFileSync(file, "utf8")) as JxElement];
|
|
184
203
|
} catch {
|
|
185
204
|
// A malformed definition is reported by the class-schema pass below, not here.
|
|
@@ -193,9 +212,13 @@ export async function validateProjectTree(
|
|
|
193
212
|
continue; // Validated against the class schema below.
|
|
194
213
|
}
|
|
195
214
|
checked += 1;
|
|
196
|
-
const doc =
|
|
215
|
+
const doc = readJson(file);
|
|
197
216
|
if (validateDoc(doc)) {
|
|
198
|
-
|
|
217
|
+
/*
|
|
218
|
+
* Only a well-formed document is judged; a schema error is the report for the rest.
|
|
219
|
+
* SAFETY: validateDoc just accepted `doc` against the bundled document schema, which is
|
|
220
|
+
* the JSON Schema rendering of JxElement.
|
|
221
|
+
*/
|
|
199
222
|
lint.push(...lintDocument(doc as JxElement, relative(root, file), scope));
|
|
200
223
|
} else {
|
|
201
224
|
issues.push({ errors: validateDoc.errors ?? [], file: relative(root, file) });
|
|
@@ -209,7 +232,7 @@ export async function validateProjectTree(
|
|
|
209
232
|
const validateClass = classAjv.compile(generateClassSchema());
|
|
210
233
|
for (const file of walkClassFiles(root)) {
|
|
211
234
|
checked += 1;
|
|
212
|
-
const doc =
|
|
235
|
+
const doc = readJson(file);
|
|
213
236
|
if (!validateClass(doc)) {
|
|
214
237
|
issues.push({ errors: validateClass.errors ?? [], file: relative(root, file) });
|
|
215
238
|
}
|
|
@@ -230,11 +253,10 @@ export async function validateProjectTree(
|
|
|
230
253
|
addFormats(fragAjv);
|
|
231
254
|
for (const specifier of SHIPPED_RESOURCE_SPECIFIERS) {
|
|
232
255
|
const resourcePath = selfRequire.resolve(specifier);
|
|
233
|
-
|
|
234
|
-
fragAjv.addSchema(resource);
|
|
256
|
+
fragAjv.addSchema(readJson(resourcePath));
|
|
235
257
|
}
|
|
236
258
|
try {
|
|
237
|
-
fragAjv.compile(
|
|
259
|
+
fragAjv.compile(readJson(fragmentPath));
|
|
238
260
|
} catch (error) {
|
|
239
261
|
issues.push({
|
|
240
262
|
errors: [{ message: error instanceof Error ? error.message : String(error) }],
|
|
@@ -248,6 +270,14 @@ export async function validateProjectTree(
|
|
|
248
270
|
return { checked, issues, lint, valid: issues.length === 0 && !fatalLint };
|
|
249
271
|
}
|
|
250
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Read and parse a JSON file. `JSON.parse` without a reviver only ever produces JSON values, so the
|
|
275
|
+
* declared type is exact; what shape of JSON it is stays for the caller to check.
|
|
276
|
+
*/
|
|
277
|
+
function readJson(path: string): JsonValue {
|
|
278
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
279
|
+
}
|
|
280
|
+
|
|
251
281
|
/** The three lints over one document, as findings that name their file. */
|
|
252
282
|
function lintDocument(doc: JxElement, file: string, scope: OverlayScope): ProjectTreeLintFinding[] {
|
|
253
283
|
const findings: ProjectTreeLintFinding[] = [];
|
|
@@ -300,26 +330,29 @@ export function formatProjectTreeLint(result: ProjectTreeValidationResult): stri
|
|
|
300
330
|
* Walks only subschema keyword positions, since `examples`/`default`/`const`/`enum` hold instance
|
|
301
331
|
* data and Jx document examples legitimately contain `$ref` strings of their own.
|
|
302
332
|
*
|
|
303
|
-
* @param {
|
|
333
|
+
* @param {JsonValue} entry - Parsed committed entry document
|
|
304
334
|
* @returns {{ ref: string; reason: string }[]} One entry per offending `$ref`
|
|
305
335
|
*/
|
|
306
|
-
function collectUnresolvableRefs(
|
|
307
|
-
entry: Record<string, unknown>,
|
|
308
|
-
): { ref: string; reason: string }[] {
|
|
336
|
+
function collectUnresolvableRefs(entry: JsonValue): { ref: string; reason: string }[] {
|
|
309
337
|
const out: { ref: string; reason: string }[] = [];
|
|
310
|
-
const seen = new Set<
|
|
338
|
+
const seen = new Set<JsonObject>();
|
|
311
339
|
|
|
312
340
|
const resolvePointer = (pointer: string): boolean => {
|
|
313
|
-
let current:
|
|
341
|
+
let current: JsonValue | undefined = entry;
|
|
314
342
|
for (const raw of pointer.slice(1).split("/")) {
|
|
315
343
|
if (raw === "") {
|
|
316
344
|
continue;
|
|
317
345
|
}
|
|
318
346
|
const segment = raw.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
319
|
-
if (
|
|
347
|
+
if (!isNonNullObject(current)) {
|
|
320
348
|
return false;
|
|
321
349
|
}
|
|
322
|
-
|
|
350
|
+
/*
|
|
351
|
+
* SAFETY: `current` is a JSON object or array. Both are read by string key here, exactly as
|
|
352
|
+
* the original property access did (an array segment such as "0" is a string key too); the
|
|
353
|
+
* result is only compared with `undefined`, so JsonObject merely types the lookup.
|
|
354
|
+
*/
|
|
355
|
+
current = (current as JsonObject)[segment];
|
|
323
356
|
if (current === undefined) {
|
|
324
357
|
return false;
|
|
325
358
|
}
|
|
@@ -327,13 +360,13 @@ function collectUnresolvableRefs(
|
|
|
327
360
|
return true;
|
|
328
361
|
};
|
|
329
362
|
|
|
330
|
-
const walk = (node:
|
|
331
|
-
if (!
|
|
363
|
+
const walk = (node: JsonValue): void => {
|
|
364
|
+
if (!isJsonObject(node) || seen.has(node)) {
|
|
332
365
|
return;
|
|
333
366
|
}
|
|
334
367
|
seen.add(node);
|
|
335
368
|
const ref = node.$ref;
|
|
336
|
-
if (
|
|
369
|
+
if (isString(ref)) {
|
|
337
370
|
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(ref)) {
|
|
338
371
|
out.push({ reason: "is an absolute URI editors resolve over the network", ref });
|
|
339
372
|
} else if (!ref.startsWith("#")) {
|
|
@@ -352,7 +385,7 @@ function collectUnresolvableRefs(
|
|
|
352
385
|
for (const item of value) {
|
|
353
386
|
walk(item);
|
|
354
387
|
}
|
|
355
|
-
} else if (
|
|
388
|
+
} else if (isJsonObject(value)) {
|
|
356
389
|
// Map-valued keywords hold subschemas one level down; single-valued ones are the schema.
|
|
357
390
|
walk(value);
|
|
358
391
|
if (MAP_SUBSCHEMA_KEYWORDS.has(keyword)) {
|
|
@@ -367,11 +400,6 @@ function collectUnresolvableRefs(
|
|
|
367
400
|
return out;
|
|
368
401
|
}
|
|
369
402
|
|
|
370
|
-
/** True for a plain object (a candidate subschema node). */
|
|
371
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
372
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
403
|
/** Keywords whose values map names to subschemas. */
|
|
376
404
|
const MAP_SUBSCHEMA_KEYWORDS = new Set([
|
|
377
405
|
"$defs",
|
|
@@ -446,10 +474,12 @@ export function formatProjectTreeIssues(result: ProjectTreeValidationResult): st
|
|
|
446
474
|
for (const issue of result.issues) {
|
|
447
475
|
lines.push(`${issue.file}:`);
|
|
448
476
|
for (const error of issue.errors) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
477
|
+
/*
|
|
478
|
+
* An ajv error object or a `{ message }` record. Destructuring a primitive, an array or a
|
|
479
|
+
* null slot (via `{}`) yields neither member, so the fallback covers every non-object.
|
|
480
|
+
*/
|
|
481
|
+
const fields: JsonObject = isJsonObject(error) ? error : {};
|
|
482
|
+
const { instancePath, message } = fields;
|
|
453
483
|
lines.push(` - ${instancePath || "/"}: ${message ?? JSON.stringify(error)}`);
|
|
454
484
|
}
|
|
455
485
|
}
|
package/src/site/well-known.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { siteBasePath, withBase } from "@jxsuite/schema/asset-paths";
|
|
|
13
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
15
|
import { canonicalizeLocale } from "@jxsuite/schema/locale";
|
|
16
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
16
17
|
import type { ManifestConfig, ProjectConfig, SecurityTxtConfig } from "@jxsuite/schema/types";
|
|
17
18
|
|
|
18
19
|
/** The two sizes a browser wants before it will offer to install a site. */
|
|
@@ -24,6 +25,42 @@ export interface GeneratedFile {
|
|
|
24
25
|
content: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
type ManifestIcon = NonNullable<ManifestConfig["icons"]>[number];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The emitted `manifest.webmanifest`, member names as the W3C Web App Manifest spells them. Members
|
|
32
|
+
* are assigned in this order, which is the order they serialize in.
|
|
33
|
+
*/
|
|
34
|
+
interface WebAppManifest {
|
|
35
|
+
display: NonNullable<ManifestConfig["display"]>;
|
|
36
|
+
icons: ManifestIcon[];
|
|
37
|
+
name: string;
|
|
38
|
+
short_name: string;
|
|
39
|
+
start_url: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
theme_color?: string;
|
|
42
|
+
background_color?: string;
|
|
43
|
+
scope?: string;
|
|
44
|
+
orientation?: string;
|
|
45
|
+
lang?: string;
|
|
46
|
+
dir?: NonNullable<ManifestConfig["dir"]>;
|
|
47
|
+
categories?: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Add an optional manifest member only when the project set it, so an unset one is omitted rather
|
|
52
|
+
* than serialized. Members land in call order, which is the order they serialize in.
|
|
53
|
+
*/
|
|
54
|
+
function setIfDefined<K extends keyof WebAppManifest>(
|
|
55
|
+
manifest: WebAppManifest,
|
|
56
|
+
key: K,
|
|
57
|
+
value: WebAppManifest[K] | undefined,
|
|
58
|
+
): void {
|
|
59
|
+
if (value !== undefined) {
|
|
60
|
+
manifest[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
27
64
|
export interface WellKnownOutput {
|
|
28
65
|
files: GeneratedFile[];
|
|
29
66
|
errors: string[];
|
|
@@ -56,25 +93,29 @@ export function buildManifest(projectConfig: ProjectConfig): WellKnownOutput {
|
|
|
56
93
|
* `start_url` at the old root just opens the wrong page from the home screen.
|
|
57
94
|
*/
|
|
58
95
|
const base = siteBasePath(projectConfig.url);
|
|
59
|
-
const icons = [];
|
|
96
|
+
const icons: ManifestIcon[] = [];
|
|
60
97
|
for (const icon of config.icons ?? []) {
|
|
61
|
-
icons.push(
|
|
98
|
+
icons.push(isString(icon.src) ? { ...icon, src: withBase(base, icon.src) } : icon);
|
|
62
99
|
}
|
|
63
|
-
const manifest:
|
|
100
|
+
const manifest: WebAppManifest = {
|
|
64
101
|
display: config.display ?? "standalone",
|
|
65
102
|
icons,
|
|
66
103
|
name,
|
|
67
104
|
short_name: config.shortName ?? name,
|
|
68
105
|
start_url: withBase(base, config.startUrl ?? "/"),
|
|
69
|
-
...(config.description === undefined ? {} : { description: config.description }),
|
|
70
|
-
...(config.themeColor === undefined ? {} : { theme_color: config.themeColor }),
|
|
71
|
-
...(config.backgroundColor === undefined ? {} : { background_color: config.backgroundColor }),
|
|
72
|
-
...(config.scope === undefined ? {} : { scope: withBase(base, config.scope) }),
|
|
73
|
-
...(config.orientation === undefined ? {} : { orientation: config.orientation }),
|
|
74
|
-
...(config.lang === undefined ? {} : { lang: config.lang }),
|
|
75
|
-
...(config.dir === undefined ? {} : { dir: config.dir }),
|
|
76
|
-
...(config.categories === undefined ? {} : { categories: config.categories }),
|
|
77
106
|
};
|
|
107
|
+
setIfDefined(manifest, "description", config.description);
|
|
108
|
+
setIfDefined(manifest, "theme_color", config.themeColor);
|
|
109
|
+
setIfDefined(manifest, "background_color", config.backgroundColor);
|
|
110
|
+
setIfDefined(
|
|
111
|
+
manifest,
|
|
112
|
+
"scope",
|
|
113
|
+
config.scope === undefined ? undefined : withBase(base, config.scope),
|
|
114
|
+
);
|
|
115
|
+
setIfDefined(manifest, "orientation", config.orientation);
|
|
116
|
+
setIfDefined(manifest, "lang", config.lang);
|
|
117
|
+
setIfDefined(manifest, "dir", config.dir);
|
|
118
|
+
setIfDefined(manifest, "categories", config.categories);
|
|
78
119
|
|
|
79
120
|
/*
|
|
80
121
|
* A warning, not an error. The manifest is valid and useful without these — it still supplies
|
|
@@ -183,6 +224,10 @@ export function buildSecurityTxt(projectConfig: ProjectConfig, now: Date): WellK
|
|
|
183
224
|
const lines: string[] = [
|
|
184
225
|
"# Generated by @jxsuite/compiler — configure via securityTxt in project.json.",
|
|
185
226
|
...contact.map((value) => `Contact: ${value}`),
|
|
227
|
+
/*
|
|
228
|
+
* SAFETY: `config.expires === undefined` pushed an error above, and any error returned before
|
|
229
|
+
* this point, so `expires` is a string here.
|
|
230
|
+
*/
|
|
186
231
|
`Expires: ${new Date(config.expires as string).toISOString().replace(/\.\d{3}Z$/, "Z")}`,
|
|
187
232
|
...(config.encryption ?? []).map((value) => `Encryption: ${value}`),
|
|
188
233
|
...(config.acknowledgments ?? []).map((value) => `Acknowledgments: ${value}`),
|
|
@@ -215,7 +260,7 @@ export function writeWellKnown(
|
|
|
215
260
|
files: readonly GeneratedFile[],
|
|
216
261
|
outDir: string,
|
|
217
262
|
exists: (path: string) => boolean,
|
|
218
|
-
)
|
|
263
|
+
) {
|
|
219
264
|
let written = 0;
|
|
220
265
|
const skipped: string[] = [];
|
|
221
266
|
for (const file of files) {
|
|
@@ -8,18 +8,19 @@
|
|
|
8
8
|
import type {
|
|
9
9
|
JxClassCtorDef,
|
|
10
10
|
JxClassDef,
|
|
11
|
+
JxClassDefs,
|
|
11
12
|
JxClassMethodDef,
|
|
12
13
|
JxClassParamDef,
|
|
13
14
|
} from "@jxsuite/schema/types";
|
|
15
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Compile a .class.json schema to a JavaScript ES module string.
|
|
17
19
|
*
|
|
18
20
|
* @param {JxClassDef} classDef - Parsed .class.json content (must have $prototype: "Class")
|
|
19
|
-
* @param {Record<string, unknown>} [_opts]
|
|
20
21
|
* @returns {string} JavaScript module source code
|
|
21
22
|
*/
|
|
22
|
-
export function compileClassJson(classDef: JxClassDef
|
|
23
|
+
export function compileClassJson(classDef: JxClassDef) {
|
|
23
24
|
const className = classDef.title;
|
|
24
25
|
if (!className) {
|
|
25
26
|
throw new Error("compileClassJson: missing title (class name)");
|
|
@@ -30,7 +31,7 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
|
|
|
30
31
|
const fields = defs?.fields ?? {};
|
|
31
32
|
// JSON objects inherit Object.prototype.constructor — only an own object value counts.
|
|
32
33
|
const rawCtor = defs?.constructor;
|
|
33
|
-
const ctor =
|
|
34
|
+
const ctor = isOwnCtorDef(rawCtor) ? rawCtor : undefined;
|
|
34
35
|
const methods = defs?.methods ?? {};
|
|
35
36
|
|
|
36
37
|
const lines: string[] = [
|
|
@@ -153,6 +154,17 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
|
|
|
153
154
|
return lines.join("\n");
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Whether a `$defs.constructor` read is the document's own constructor definition rather than the
|
|
159
|
+
* `Object.prototype.constructor` function every JSON object inherits.
|
|
160
|
+
*
|
|
161
|
+
* @param {JxClassDefs["constructor"]} value
|
|
162
|
+
* @returns {boolean}
|
|
163
|
+
*/
|
|
164
|
+
function isOwnCtorDef(value: JxClassDefs["constructor"]): value is JxClassCtorDef {
|
|
165
|
+
return typeof value === "object";
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
/**
|
|
157
169
|
* Resolve the base class name from an extends value.
|
|
158
170
|
*
|
|
@@ -163,7 +175,7 @@ function resolveBaseClass(ext: JxClassDef["extends"]) {
|
|
|
163
175
|
if (!ext) {
|
|
164
176
|
return "Object";
|
|
165
177
|
}
|
|
166
|
-
if (
|
|
178
|
+
if (isString(ext)) {
|
|
167
179
|
return ext;
|
|
168
180
|
}
|
|
169
181
|
if (ext.$ref) {
|
|
@@ -186,7 +198,7 @@ function resolveParams(params: JxClassParamDef[]) {
|
|
|
186
198
|
return param.$ref.split("/").pop();
|
|
187
199
|
}
|
|
188
200
|
const name = param.identifier ?? param.name;
|
|
189
|
-
return
|
|
201
|
+
return isString(name) ? name : "arg";
|
|
190
202
|
})
|
|
191
203
|
.join(", ");
|
|
192
204
|
}
|