halfcode-compiler.xnl 0.2.0 → 0.2.2

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.
@@ -1,180 +0,0 @@
1
- import { n as wordToString, t as parseXnl } from "./dist-Bkv7YeVi.js";
2
- import { copyFile, lstat, mkdir, readdir } from "node:fs/promises";
3
- import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
4
- //#region ../resource-mapping/src/index.ts
5
- function parseResourceMappings(source, uri = "vfs://./resource-mappings.xnl") {
6
- let root;
7
- try {
8
- const document = parseXnl(source);
9
- if (document.warnings?.length || document.nodes.length !== 1 || !isDataElement(document.nodes[0])) throw new Error("expected exactly one ResourceMappings data root without warnings");
10
- root = document.nodes[0];
11
- } catch (error) {
12
- throw new Error(`Invalid ResourceMappings at ${uri}: ${error instanceof Error ? error.message : String(error)}`);
13
- }
14
- if (root.tag !== "ResourceMappings") throw new Error(`Invalid ResourceMappings at ${uri}: root must be ResourceMappings`);
15
- const referenceTargets = /* @__PURE__ */ new Map();
16
- for (const entry of bodyElements(extension(root, "ReferenceTargets"), "ReferenceTarget")) {
17
- const kind = requiredXnlProperty(entry, "kind", uri);
18
- if (referenceTargets.has(kind)) throw new Error(`Invalid ResourceMappings at ${uri}: duplicate ReferenceTarget kind ${kind}`);
19
- referenceTargets.set(kind, normalizeOutputDirectory(requiredXnlProperty(entry, "target", uri), uri));
20
- }
21
- const callable = extension(root, "CallableArtifacts");
22
- const callableArtifactsTarget = normalizeOutputDirectory(callable ? requiredXnlProperty(callable, "target", uri) : "functions/", uri);
23
- const sources = bodyElements(extension(root, "SourceRoots"), "SourceRoot").map((entry) => {
24
- const id = wordToString(entry.id);
25
- if (!id || !/^[A-Z][A-Za-z0-9]*$/.test(id)) throw new Error(`Invalid ResourceMappings at ${uri}: SourceRoot #id must be a PascalCase segment`);
26
- const operations = (entry.body ?? []).filter(isDataElement).filter((operation) => operation.tag === "CopyDirectory" || operation.tag === "CopyFile").map((operation) => ({
27
- kind: operation.tag,
28
- from: normalizeRelativePath(requiredXnlProperty(operation, "from", uri), uri, "from"),
29
- to: normalizeRelativePath(requiredXnlProperty(operation, "to", uri), uri, "to")
30
- }));
31
- return {
32
- id,
33
- sourceRoot: requiredXnlProperty(entry, "sourceRoot", uri),
34
- operations
35
- };
36
- });
37
- if (new Set(sources.map((item) => item.id)).size !== sources.length) throw new Error(`Invalid ResourceMappings at ${uri}: SourceRoot ids must be unique`);
38
- return {
39
- referenceTargets,
40
- callableArtifactsTarget,
41
- sources,
42
- source: { uri }
43
- };
44
- }
45
- function extension(node, name) {
46
- const child = node.extend?.children[name];
47
- return child?.kind === "DataElement" ? child : void 0;
48
- }
49
- function bodyElements(node, tag) {
50
- return (node?.body ?? []).filter(isDataElement).filter((entry) => entry.tag === tag);
51
- }
52
- function requiredXnlProperty(node, name, uri) {
53
- const value = node.attributes?.[name];
54
- if (typeof value !== "string" || !value.trim()) throw new Error(`Invalid ResourceMappings at ${uri}: <${node.tag}> requires ${name}`);
55
- return value;
56
- }
57
- function isDataElement(value) {
58
- return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && value.kind === "DataElement";
59
- }
60
- async function planResourceMappings(mappings, options) {
61
- const roots = asRootMap(options.moduleRoots);
62
- const claims = [];
63
- const entries = [];
64
- const reserved = (options.reservedTargetPaths ?? []).map((path) => normalizeRelativePath(path, mappings.source.uri, "to"));
65
- for (const mapping of mappings.sources) {
66
- const sourceRootPath = resolveSourceRoot(mapping.sourceRoot, roots, options.defaultSourceRoot, mappings.source.uri);
67
- await assertDirectory(sourceRootPath, mappings.source.uri, mapping.sourceRoot);
68
- for (const operation of mapping.operations) {
69
- for (const prior of claims) if (pathsOverlap(prior.target, operation.to)) throw new Error(`ResourceMappings target collision between ${prior.mappingId}:${prior.from} -> ${prior.target} and ${mapping.id}:${operation.from} -> ${operation.to}`);
70
- claims.push({
71
- target: operation.to,
72
- mappingId: mapping.id,
73
- from: operation.from
74
- });
75
- const sourcePath = safeResolve(sourceRootPath, operation.from, mappings.source.uri);
76
- if (operation.kind === "CopyFile") {
77
- await assertFile(sourcePath, mappings.source.uri, operation.from);
78
- assertNotReserved(operation.to, reserved, mappings.source.uri);
79
- entries.push({
80
- sourcePath,
81
- targetRelativePath: operation.to,
82
- mappingId: mapping.id
83
- });
84
- continue;
85
- }
86
- await assertDirectory(sourcePath, mappings.source.uri, operation.from);
87
- for (const filePath of await listFiles(sourcePath, mappings.source.uri)) {
88
- const child = relative(sourcePath, filePath).split(sep).join("/");
89
- const targetRelativePath = posix.join(operation.to, child);
90
- assertNotReserved(targetRelativePath, reserved, mappings.source.uri);
91
- entries.push({
92
- sourcePath: filePath,
93
- targetRelativePath,
94
- mappingId: mapping.id
95
- });
96
- }
97
- }
98
- }
99
- const seenTargets = /* @__PURE__ */ new Map();
100
- for (const entry of entries) {
101
- const prior = seenTargets.get(entry.targetRelativePath);
102
- if (prior) throw new Error(`ResourceMappings duplicate target ${entry.targetRelativePath} from ${prior.sourcePath} and ${entry.sourcePath}`);
103
- seenTargets.set(entry.targetRelativePath, entry);
104
- }
105
- return { entries: entries.sort((a, b) => a.targetRelativePath.localeCompare(b.targetRelativePath)) };
106
- }
107
- async function applyResourceMappingPlan(plan, outputRoot) {
108
- const files = [];
109
- for (const entry of plan.entries) {
110
- const target = resolve(outputRoot, entry.targetRelativePath);
111
- if (!isWithinRoot(resolve(outputRoot), target)) throw new Error(`ResourceMappings output escapes target root: ${entry.targetRelativePath}`);
112
- await mkdir(dirname(target), { recursive: true });
113
- await copyFile(entry.sourcePath, target);
114
- files.push(entry.targetRelativePath);
115
- }
116
- return files;
117
- }
118
- function resolveSourceRoot(uri, roots, defaultSourceRoot, mappingUri) {
119
- const moduleMatch = /^vfs:\/\/module\/([A-Z][A-Za-z0-9]*)\/(.*)$/.exec(uri);
120
- if (moduleMatch) {
121
- const base = roots.get(moduleMatch[1]);
122
- if (!base) throw new Error(`Invalid ResourceMappings at ${mappingUri}: unknown module root ${moduleMatch[1]}`);
123
- return safeResolve(resolve(base), moduleMatch[2], mappingUri);
124
- }
125
- const localMatch = /^vfs:\/\/@\/(.*)$/.exec(uri);
126
- if (localMatch && defaultSourceRoot) return safeResolve(resolve(defaultSourceRoot), localMatch[1], mappingUri);
127
- throw new Error(`Invalid ResourceMappings at ${mappingUri}: unsupported sourceRoot ${uri}`);
128
- }
129
- async function listFiles(root, uri) {
130
- const files = [];
131
- for (const entry of (await readdir(root, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
132
- const path = join(root, entry.name);
133
- if (entry.isSymbolicLink()) throw new Error(`Invalid ResourceMappings at ${uri}: symbolic links are not supported: ${path}`);
134
- if (entry.isDirectory()) files.push(...await listFiles(path, uri));
135
- else if (entry.isFile()) files.push(path);
136
- else throw new Error(`Invalid ResourceMappings at ${uri}: unsupported source entry ${path}`);
137
- }
138
- return files;
139
- }
140
- async function assertDirectory(path, uri, label) {
141
- const info = await lstat(path).catch(() => void 0);
142
- if (!info || info.isSymbolicLink() || !info.isDirectory()) throw new Error(`Invalid ResourceMappings at ${uri}: directory source does not exist: ${label}`);
143
- }
144
- async function assertFile(path, uri, label) {
145
- const info = await lstat(path).catch(() => void 0);
146
- if (!info || info.isSymbolicLink() || !info.isFile()) throw new Error(`Invalid ResourceMappings at ${uri}: file source does not exist: ${label}`);
147
- }
148
- function safeResolve(root, path, uri) {
149
- const target = resolve(root, path);
150
- if (!isWithinRoot(root, target)) throw new Error(`Invalid ResourceMappings at ${uri}: path escapes source root: ${path}`);
151
- return target;
152
- }
153
- function isWithinRoot(root, target) {
154
- return target === root || target.startsWith(`${root}${sep}`);
155
- }
156
- function assertNotReserved(path, reserved, uri) {
157
- for (const item of reserved) if (pathsOverlap(path, item)) throw new Error(`Invalid ResourceMappings at ${uri}: target ${path} conflicts with generated target ${item}`);
158
- }
159
- function pathsOverlap(left, right) {
160
- return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
161
- }
162
- function normalizeRelativePath(value, uri, attribute) {
163
- if (!value.trim() || value.includes("\\") || value.startsWith("/") || isAbsolute(value) || value.split("/").includes("..")) throw new Error(`Invalid ResourceMappings at ${uri}: ${attribute} must be a safe relative path: ${value}`);
164
- const normalized = posix.normalize(value).replace(/^\.\//, "").replace(/\/$/, "");
165
- if (!normalized || normalized === "." || normalized.startsWith("../")) throw new Error(`Invalid ResourceMappings at ${uri}: ${attribute} must identify a non-root path: ${value}`);
166
- return normalized;
167
- }
168
- function normalizeOutputDirectory(value, uri) {
169
- return `${normalizeRelativePath(value, uri, "to")}/`;
170
- }
171
- function asRootMap(value) {
172
- return typeof value.get === "function" ? value : new Map(Object.entries(value));
173
- }
174
- const resourceMappingPackage = {
175
- role: "framework",
176
- area: "resource-mapping",
177
- owns: "declarative multi-root resource copy planning and collision preflight"
178
- };
179
- //#endregion
180
- export { resourceMappingPackage as i, parseResourceMappings as n, planResourceMappings as r, applyResourceMappingPlan as t };
@@ -1,494 +0,0 @@
1
- import { n as wordToString, t as parseXnl } from "./dist-Bkv7YeVi.js";
2
- import { readFile, readdir, realpath, stat } from "node:fs/promises";
3
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
- //#region ../resource-core/src/xnl-loader.ts
5
- async function buildXnlResourceTree(options) {
6
- const requestedRoot = resolve(options.rootDir);
7
- const context = {
8
- rootDir: await realpath(requestedRoot).catch(() => requestedRoot),
9
- diagnostics: [],
10
- kindDefinitions: /* @__PURE__ */ new Map(),
11
- resources: [],
12
- seenIdentities: /* @__PURE__ */ new Map()
13
- };
14
- const manifest = await loadManifest(resolve(context.rootDir, options.manifestPath), "manifest", context, true);
15
- if (!manifest || context.diagnostics.length > 0) return { diagnostics: context.diagnostics };
16
- const byKind = /* @__PURE__ */ new Map();
17
- for (const resource of context.resources) {
18
- const records = byKind.get(resource.kind) ?? [];
19
- records.push(resource);
20
- byKind.set(resource.kind, records);
21
- }
22
- return {
23
- diagnostics: [],
24
- tree: {
25
- manifest,
26
- registry: {
27
- byKind,
28
- kindDefinitions: context.kindDefinitions
29
- },
30
- diagnostics: []
31
- }
32
- };
33
- }
34
- async function loadManifest(filePath, sourceShape, context, isRoot) {
35
- const record = await loadXnlRecord(filePath, sourceShape, context);
36
- if (!record) return void 0;
37
- if (isRoot && record.kind !== "ResourcePackage") {
38
- context.diagnostics.push({
39
- code: "RESOURCE_XNL_ROOT_INVALID",
40
- location: record.documentUri,
41
- message: "The root manifest.xnl must contain one ResourcePackage root."
42
- });
43
- return;
44
- }
45
- const catalogs = readCatalogs(record, context.diagnostics);
46
- for (const catalog of catalogs.filter((item) => item.kind === "KindDefinition")) await loadKindDefinitions(catalog, dirname(filePath), context);
47
- for (const catalog of catalogs.filter((item) => item.kind !== "KindDefinition")) await loadCatalog(catalog, dirname(filePath), context);
48
- if (!isRoot) {
49
- validateIdentity(record, context);
50
- context.resources.push(record);
51
- }
52
- return record;
53
- }
54
- async function loadKindDefinitions(catalog, manifestDir, context) {
55
- const files = await catalogFiles(catalog, manifestDir, context);
56
- for (const file of files) {
57
- const record = await loadXnlRecord(file, catalog.shape, context);
58
- if (!record) continue;
59
- if (record.kind !== "KindDefinition") {
60
- context.diagnostics.push(kindMismatch(record.kind, "KindDefinition", record.documentUri));
61
- continue;
62
- }
63
- const definition = normalizeKindDefinition(record, context.diagnostics);
64
- if (!definition) continue;
65
- context.kindDefinitions.set(definition.resourceKind, definition);
66
- }
67
- }
68
- async function loadCatalog(catalog, manifestDir, context) {
69
- const definition = context.kindDefinitions.get(catalog.kind);
70
- if (!definition) {
71
- context.diagnostics.push({
72
- code: "KIND_DEFINITION_MISSING",
73
- location: catalog.location,
74
- message: `No KindDefinition is registered for catalog kind '${catalog.kind}'.`
75
- });
76
- return;
77
- }
78
- if (!definition.sourceShapes.includes(catalog.shape)) {
79
- context.diagnostics.push({
80
- code: "RESOURCE_SOURCE_SHAPE_NOT_ALLOWED",
81
- location: catalog.location,
82
- message: `Kind '${catalog.kind}' does not allow source shape '${catalog.shape}'.`
83
- });
84
- return;
85
- }
86
- const files = await catalogFiles(catalog, manifestDir, context);
87
- for (const file of files) {
88
- if (catalog.shape === "manifest") {
89
- const record = await loadXnlRecord(file, "manifest", context);
90
- if (!record) continue;
91
- if (record.kind !== catalog.kind) {
92
- context.diagnostics.push(kindMismatch(record.kind, catalog.kind, record.documentUri));
93
- continue;
94
- }
95
- validateResourceApiVersion(record, definition, context.diagnostics);
96
- await validateRequiredFiles(dirname(file), record, definition, context.diagnostics);
97
- validateIdentity(record, context);
98
- context.resources.push(record);
99
- const catalogs = readCatalogs(record, context.diagnostics);
100
- for (const nested of catalogs.filter((item) => item.kind !== "KindDefinition")) await loadCatalog(nested, dirname(file), context);
101
- continue;
102
- }
103
- if (definition.documentCardinality === "many" && catalog.shape !== "single-file") {
104
- context.diagnostics.push({
105
- code: "RESOURCE_DOCUMENT_CARDINALITY_UNSUPPORTED",
106
- location: catalog.location,
107
- message: `Kind '${catalog.kind}' allows multiple document roots only in a single-file catalog.`
108
- });
109
- continue;
110
- }
111
- const records = await loadXnlRecords(file, catalog.shape, context, definition.documentCardinality);
112
- for (const record of records) {
113
- if (record.kind !== catalog.kind) {
114
- context.diagnostics.push(kindMismatch(record.kind, catalog.kind, record.documentUri));
115
- continue;
116
- }
117
- validateResourceApiVersion(record, definition, context.diagnostics);
118
- await validateRequiredFiles(dirname(file), record, definition, context.diagnostics);
119
- validateIdentity(record, context);
120
- context.resources.push(record);
121
- }
122
- }
123
- }
124
- async function catalogFiles(catalog, manifestDir, context) {
125
- const root = await resolveCatalogDirectory(catalog.root, manifestDir, catalog.location, context);
126
- if (!root) return [];
127
- const entries = await readdir(root, { withFileTypes: true }).catch(() => void 0);
128
- if (!entries) {
129
- context.diagnostics.push({
130
- code: "RESOURCE_CATALOG_ROOT_MISSING",
131
- location: catalog.location,
132
- message: "Catalog root does not exist or is not readable."
133
- });
134
- return [];
135
- }
136
- if (catalog.shape === "single-file") return entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".xnl")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => join(root, entry.name));
137
- if (!catalog.entry || !isPlainEntry(catalog.entry)) {
138
- context.diagnostics.push({
139
- code: "RESOURCE_CATALOG_ENTRY_INVALID",
140
- location: catalog.location,
141
- message: "Directory and manifest catalogs require a plain XNL entry filename."
142
- });
143
- return [];
144
- }
145
- return entries.filter((entry) => entry.isDirectory()).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => join(root, entry.name, catalog.entry));
146
- }
147
- async function loadXnlRecord(filePath, sourceShape, context) {
148
- return (await loadXnlRecords(filePath, sourceShape, context, "one"))[0];
149
- }
150
- async function loadXnlRecords(filePath, sourceShape, context, documentCardinality) {
151
- const documentUri = documentUriFor(context.rootDir, filePath);
152
- const source = await readFile(filePath, "utf8").catch(() => void 0);
153
- if (source === void 0) {
154
- context.diagnostics.push({
155
- code: "RESOURCE_FILE_MISSING",
156
- location: documentUri,
157
- message: "Resource file does not exist."
158
- });
159
- return [];
160
- }
161
- let roots;
162
- try {
163
- const parsed = parseXnl(source, { textBlockStyle: true });
164
- if (parsed.warnings?.length) {
165
- context.diagnostics.push({
166
- code: "RESOURCE_XNL_WARNING",
167
- location: documentUri,
168
- message: parsed.warnings.map((warning) => warning.message).join("; ")
169
- });
170
- return [];
171
- }
172
- if (parsed.nodes.length === 0 || parsed.nodes.some((node) => !isDataElement(node)) || documentCardinality === "one" && parsed.nodes.length !== 1) {
173
- context.diagnostics.push({
174
- code: "RESOURCE_XNL_ROOT_INVALID",
175
- location: documentUri,
176
- message: documentCardinality === "one" ? "XNL resource documents must contain exactly one data-element root." : "XNL resource forests must contain one or more data-element roots."
177
- });
178
- return [];
179
- }
180
- roots = parsed.nodes;
181
- } catch (error) {
182
- context.diagnostics.push({
183
- code: "RESOURCE_XNL_SYNTAX",
184
- location: documentUri,
185
- message: `Invalid XNL resource document: ${error instanceof Error ? error.message : String(error)}`
186
- });
187
- return [];
188
- }
189
- return roots.flatMap((root) => {
190
- const record = resourceRecordFromRoot(root, filePath, sourceShape, context);
191
- return record ? [record] : [];
192
- });
193
- }
194
- function resourceRecordFromRoot(root, filePath, sourceShape, context) {
195
- const documentUri = documentUriFor(context.rootDir, filePath);
196
- const node = normalizeNode(root);
197
- const resourceId = node.resourceId;
198
- const apiVersion = asString(node.metadata.apiVersion);
199
- const lifecycle = asString(node.properties.lifecycle);
200
- const description = asString(node.properties.description) ?? node.subdomains.Description?.text;
201
- if (!resourceId) context.diagnostics.push({
202
- code: "RESOURCE_IDENTITY_MISSING",
203
- location: documentUri,
204
- message: "XNL resource roots must declare a #id."
205
- });
206
- if (!apiVersion) context.diagnostics.push({
207
- code: "RESOURCE_METADATA_FIELD_MISSING",
208
- location: documentUri,
209
- message: "XNL resources require metadata apiVersion."
210
- });
211
- if (!resourceId || !apiVersion) return void 0;
212
- const metadata = {
213
- apiVersion,
214
- ...lifecycle ? { lifecycle } : {},
215
- version: asString(node.metadata.version)
216
- };
217
- const logicalPath = relative(context.rootDir, filePath).split(sep).join("/");
218
- return {
219
- kind: node.tag,
220
- resourceId,
221
- fqn: resourceId,
222
- ...description ? { description } : {},
223
- metadata,
224
- sourceShape,
225
- logicalPath,
226
- documentUri,
227
- format: "xnl",
228
- node
229
- };
230
- }
231
- function normalizeNode(node) {
232
- const subdomains = {};
233
- if (node.kind === "DataElement" && node.extend) for (const name of node.extend.order) {
234
- const child = node.extend.children[name];
235
- if (child) subdomains[name] = normalizeNode(child);
236
- }
237
- return Object.freeze({
238
- tag: node.tag,
239
- resourceId: wordToString(node.id),
240
- metadata: Object.freeze(normalizeMap(node.metadata)),
241
- properties: Object.freeze(normalizeMap(node.attributes)),
242
- body: Object.freeze(node.kind === "DataElement" ? (node.body ?? []).map(normalizeValue) : []),
243
- subdomains: Object.freeze(subdomains),
244
- ...node.kind === "TextElement" ? { text: node.text ?? "" } : {}
245
- });
246
- }
247
- function normalizeMap(map) {
248
- return Object.fromEntries(Object.entries(map ?? {}).map(([key, value]) => [key, normalizeValue(value)]));
249
- }
250
- function normalizeValue(value) {
251
- if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
252
- if (Array.isArray(value)) return Object.freeze(value.map(normalizeValue));
253
- if (isElement(value)) return normalizeNode(value);
254
- if (isWord(value)) return wordToString(value) ?? "";
255
- if (isComment(value)) return value.value;
256
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, child]) => [key, normalizeValue(child)])));
257
- }
258
- function readCatalogs(record, diagnostics) {
259
- const catalogs = record.node.subdomains.Catalogs;
260
- if (!catalogs) return [];
261
- const out = [];
262
- for (const value of catalogs.body) {
263
- if (!isResourceNode(value) || value.tag !== "Catalog") continue;
264
- const id = value.resourceId;
265
- const kind = asString(value.properties.kind);
266
- const root = asString(value.properties.root);
267
- const entry = asString(value.properties.entry);
268
- const shapeValue = asString(value.properties.shape) ?? (entry ? "directory" : "single-file");
269
- const location = `${record.documentUri}#Catalog:${id ?? "unknown"}`;
270
- if (!id || !kind || !root || !isXnlShape(shapeValue)) {
271
- diagnostics.push({
272
- code: "RESOURCE_CATALOG_INVALID",
273
- location,
274
- message: "Catalog requires #id, kind, root and a valid shape."
275
- });
276
- continue;
277
- }
278
- out.push({
279
- id,
280
- kind,
281
- root,
282
- entry,
283
- shape: shapeValue,
284
- location
285
- });
286
- }
287
- return out;
288
- }
289
- function normalizeKindDefinition(record, diagnostics) {
290
- const resourceKind = asString(record.node.properties.resourceKind) ?? record.resourceId.split(".").at(-1);
291
- const shapes = record.node.properties.sourceShapes;
292
- const sourceShapes = Array.isArray(shapes) ? shapes.filter(isXnlShape) : [];
293
- const currentApiVersion = asString(record.node.properties.currentApiVersion);
294
- const versions = record.node.properties.supportedApiVersions;
295
- const supportedApiVersions = Array.isArray(versions) ? versions.filter((value) => typeof value === "string" && Boolean(value.trim())) : [];
296
- const cardinalityValue = asString(record.node.properties.documentCardinality) ?? "one";
297
- if (!resourceKind || sourceShapes.length === 0 || !currentApiVersion || supportedApiVersions.length === 0 || !isDocumentCardinality(cardinalityValue)) {
298
- diagnostics.push({
299
- code: "KIND_DEFINITION_INVALID",
300
- location: record.documentUri,
301
- message: "KindDefinition requires resourceKind (or an id suffix), sourceShapes, currentApiVersion, and supportedApiVersions."
302
- });
303
- return;
304
- }
305
- if (!supportedApiVersions.includes(currentApiVersion)) {
306
- diagnostics.push({
307
- code: "KIND_DEFINITION_VERSION_INVALID",
308
- location: record.documentUri,
309
- message: `Kind '${resourceKind}' currentApiVersion '${currentApiVersion}' must be included in supportedApiVersions.`
310
- });
311
- return;
312
- }
313
- if (cardinalityValue === "many" && sourceShapes.some((shape) => shape !== "single-file")) {
314
- diagnostics.push({
315
- code: "KIND_DEFINITION_CARDINALITY_INVALID",
316
- location: record.documentUri,
317
- message: `Kind '${resourceKind}' documentCardinality 'many' requires only the single-file source shape.`
318
- });
319
- return;
320
- }
321
- const requiredFiles = ((record.node.subdomains.DescriptorContract?.subdomains.RequiredFiles)?.body ?? []).filter(isResourceNode).filter((node) => node.tag === "File").map((node) => asString(node.properties.name)).filter((value) => Boolean(value));
322
- return {
323
- resourceId: record.resourceId,
324
- resourceKind,
325
- sourceShapes: Object.freeze([...sourceShapes]),
326
- requiredFiles: Object.freeze([...requiredFiles]),
327
- currentApiVersion,
328
- supportedApiVersions: Object.freeze([...new Set(supportedApiVersions)]),
329
- documentCardinality: cardinalityValue,
330
- documentUri: record.documentUri
331
- };
332
- }
333
- function validateResourceApiVersion(record, definition, diagnostics) {
334
- if (definition.supportedApiVersions.includes(record.metadata.apiVersion)) return;
335
- diagnostics.push({
336
- code: "RESOURCE_API_VERSION_UNSUPPORTED",
337
- location: record.documentUri,
338
- message: `Kind '${record.kind}' does not support apiVersion '${record.metadata.apiVersion}'.`,
339
- hint: `Current apiVersion is '${definition.currentApiVersion}'.`
340
- });
341
- }
342
- async function validateRequiredFiles(resourceDir, record, definition, diagnostics) {
343
- for (const name of definition.requiredFiles) {
344
- if (!isPlainEntry(name)) {
345
- diagnostics.push({
346
- code: "KIND_DEFINITION_REQUIRED_FILE_INVALID",
347
- location: record.documentUri,
348
- message: `Invalid required file '${name}'.`
349
- });
350
- continue;
351
- }
352
- if (!await stat(join(resourceDir, name)).then((info) => info.isFile()).catch(() => false)) diagnostics.push({
353
- code: "RESOURCE_REQUIRED_FILE_MISSING",
354
- location: `${record.documentUri}#${name}`,
355
- message: `Resource is missing required material '${name}'.`
356
- });
357
- }
358
- }
359
- async function resolveCatalogDirectory(uri, manifestDir, location, context) {
360
- const relativeMatch = /^vfs:\/\/(\.\/|@\/)(.*)$/.exec(uri);
361
- if (!relativeMatch || !uri.endsWith("/")) {
362
- context.diagnostics.push({
363
- code: "RESOURCE_REF_UNSUPPORTED",
364
- location,
365
- message: "Catalog roots must be directory vfs://./ or vfs://@/ references."
366
- });
367
- return;
368
- }
369
- const rawPath = relativeMatch[2];
370
- let decoded;
371
- try {
372
- decoded = decodeURIComponent(rawPath);
373
- } catch {
374
- context.diagnostics.push({
375
- code: "RESOURCE_REF_CONTAINMENT",
376
- location,
377
- message: "VFS reference contains invalid encoding."
378
- });
379
- return;
380
- }
381
- if (isAbsolute(decoded) || decoded.split(/[\\/]+/).includes("..") || /%2f|%5c/i.test(rawPath)) {
382
- context.diagnostics.push({
383
- code: "RESOURCE_REF_CONTAINMENT",
384
- location,
385
- message: "VFS reference escapes the owning package boundary."
386
- });
387
- return;
388
- }
389
- const base = relativeMatch[1] === "@/" ? context.rootDir : manifestDir;
390
- const boundary = await realpath(context.rootDir);
391
- const candidate = resolve(base, decoded);
392
- const resolved = await realpath(candidate).catch(() => candidate);
393
- if (resolved !== boundary && !resolved.startsWith(`${boundary}${sep}`)) {
394
- context.diagnostics.push({
395
- code: "RESOURCE_REF_CONTAINMENT",
396
- location,
397
- message: "VFS reference resolves outside the owning package boundary."
398
- });
399
- return;
400
- }
401
- return resolved;
402
- }
403
- function validateIdentity(record, context) {
404
- const prior = context.seenIdentities.get(record.resourceId);
405
- if (prior) {
406
- context.diagnostics.push({
407
- code: "RESOURCE_IDENTITY_DUPLICATE",
408
- location: record.documentUri,
409
- message: `Duplicate resource identity '${record.resourceId}'.`,
410
- hint: `First seen at ${prior}.`
411
- });
412
- return;
413
- }
414
- context.seenIdentities.set(record.resourceId, record.documentUri);
415
- }
416
- function documentUriFor(rootDir, filePath) {
417
- return `vfs://@/${relative(rootDir, filePath).split(sep).join("/")}`;
418
- }
419
- function kindMismatch(actual, expected, location) {
420
- return {
421
- code: "RESOURCE_KIND_MISMATCH",
422
- location,
423
- message: `Resource kind '${actual}' does not match catalog kind '${expected}'.`
424
- };
425
- }
426
- function isXnlShape(value) {
427
- return value === "single-file" || value === "directory" || value === "manifest";
428
- }
429
- function isDocumentCardinality(value) {
430
- return value === "one" || value === "many";
431
- }
432
- function isPlainEntry(value) {
433
- return Boolean(value) && !value.includes("/") && !value.includes("\\") && !value.includes("..");
434
- }
435
- function asString(value) {
436
- return typeof value === "string" && value.trim() ? value : void 0;
437
- }
438
- function isResourceNode(value) {
439
- return typeof value === "object" && value !== null && !Array.isArray(value) && "tag" in value;
440
- }
441
- function isDataElement(value) {
442
- return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && value.kind === "DataElement";
443
- }
444
- function isElement(value) {
445
- return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && (value.kind === "DataElement" || value.kind === "TextElement");
446
- }
447
- function isWord(value) {
448
- return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && value.kind === "Word";
449
- }
450
- function isComment(value) {
451
- return typeof value === "object" && value !== null && !Array.isArray(value) && "kind" in value && value.kind === "Comment";
452
- }
453
- //#endregion
454
- //#region ../resource-core/src/index.ts
455
- var ResourceValidationError = class extends Error {
456
- diagnostics;
457
- constructor(diagnostics) {
458
- super(`Resource validation failed with ${diagnostics.length} diagnostic(s)`);
459
- this.name = "ResourceValidationError";
460
- this.diagnostics = diagnostics;
461
- }
462
- };
463
- async function loadResourceTree(options) {
464
- const unsupported = unsupportedAuthorityDiagnostic(options);
465
- if (unsupported) throw new ResourceValidationError([unsupported]);
466
- const manifestPath = options.manifestPath ?? "manifest.xnl";
467
- const result = await buildXnlResourceTree({
468
- ...options,
469
- rootDir: resolve(options.rootDir),
470
- manifestPath
471
- });
472
- if (result.diagnostics.length > 0 || !result.tree) throw new ResourceValidationError(result.diagnostics);
473
- return result.tree;
474
- }
475
- async function validateResourceTree(options) {
476
- const unsupported = unsupportedAuthorityDiagnostic(options);
477
- if (unsupported) return [unsupported];
478
- const manifestPath = options.manifestPath ?? "manifest.xnl";
479
- return (await buildXnlResourceTree({
480
- ...options,
481
- rootDir: resolve(options.rootDir),
482
- manifestPath
483
- })).diagnostics;
484
- }
485
- function unsupportedAuthorityDiagnostic(options) {
486
- const manifestPath = options.manifestPath ?? "manifest.xnl";
487
- if (!manifestPath.toLowerCase().endsWith(".xnl")) return {
488
- code: "RESOURCE_AUTHORITY_FORMAT_UNSUPPORTED",
489
- location: `vfs://@/${manifestPath}`,
490
- message: "Resource package manifests must use XNL authority."
491
- };
492
- }
493
- //#endregion
494
- export { loadResourceTree as n, validateResourceTree as r, ResourceValidationError as t };