@glossarist/concept-browser 0.7.115 → 0.7.117
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/cli/index.mjs +12 -15
- package/package.json +8 -4
- package/scripts/bridge-to-astro.js +132 -0
- package/scripts/build-edges.js +480 -0
- package/scripts/build-scripts.mjs +31 -0
- package/scripts/check-syntax.js +81 -0
- package/scripts/doctor.js +278 -0
- package/scripts/extract-source-refs.js +24 -0
- package/scripts/fetch-datasets.js +278 -0
- package/scripts/generate-404.js +14 -0
- package/scripts/generate-data.js +2220 -0
- package/scripts/generate-ontology-data.js +139 -0
- package/scripts/generate-ontology-schema.js +472 -0
- package/scripts/load-site-config.js +83 -0
- package/scripts/migrate-v1-to-v3.js +113 -0
- package/scripts/normalize-yaml.js +96 -0
- package/scripts/process-about-pages.js +241 -0
- package/scripts/sync-concept-model.js +63 -0
- package/scripts/validate-shacl.js +152 -0
- package/scripts/validate-yaml.js +132 -0
- package/src/__tests__/published-package-contract.test.ts +100 -0
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/data/ontology-schema.json +1336 -183
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/doctor.ts
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { resolve, dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
var PKG_ROOT = resolve(__dirname, "..");
|
|
10
|
+
var NODE_MIN_MAJOR = 18;
|
|
11
|
+
function safe(id, check) {
|
|
12
|
+
const wrapped = async (ctx) => {
|
|
13
|
+
try {
|
|
14
|
+
return await check(ctx);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return {
|
|
17
|
+
id: "unexpected",
|
|
18
|
+
label: "Unexpected error",
|
|
19
|
+
status: "fail",
|
|
20
|
+
detail: err?.message ?? String(err)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(wrapped, "name", { value: id });
|
|
25
|
+
return wrapped;
|
|
26
|
+
}
|
|
27
|
+
function nodeVersionCheck() {
|
|
28
|
+
const major = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
29
|
+
return {
|
|
30
|
+
id: "node-version",
|
|
31
|
+
label: `Node.js v${process.versions.node}`,
|
|
32
|
+
status: major >= NODE_MIN_MAJOR ? "pass" : "fail",
|
|
33
|
+
detail: major < NODE_MIN_MAJOR ? `requires >= v${NODE_MIN_MAJOR}` : void 0,
|
|
34
|
+
hint: major < NODE_MIN_MAJOR ? "upgrade via nvm or your package manager" : void 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function packageDepsCheck(ctx) {
|
|
38
|
+
const nm = resolve(ctx.pkgRoot, "node_modules");
|
|
39
|
+
if (!existsSync(nm)) {
|
|
40
|
+
return {
|
|
41
|
+
id: "package-deps",
|
|
42
|
+
label: "Dependencies installed",
|
|
43
|
+
status: "fail",
|
|
44
|
+
detail: "node_modules missing",
|
|
45
|
+
hint: "run `npm install`"
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const required = ["vue", "vue-router", "pinia", "vite", "n3", "glossarist", "@rdfjs/dataset"];
|
|
49
|
+
const missing = required.filter((p) => !existsSync(join(nm, p)));
|
|
50
|
+
if (missing.length) {
|
|
51
|
+
return {
|
|
52
|
+
id: "package-deps",
|
|
53
|
+
label: "Dependencies installed",
|
|
54
|
+
status: "fail",
|
|
55
|
+
detail: `missing: ${missing.join(", ")}`,
|
|
56
|
+
hint: "run `npm install`"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { id: "package-deps", label: "Dependencies installed", status: "pass" };
|
|
60
|
+
}
|
|
61
|
+
function datasetsYmlCheck(ctx) {
|
|
62
|
+
const path = resolve(ctx.cwd, "datasets.yml");
|
|
63
|
+
if (!existsSync(path)) {
|
|
64
|
+
return {
|
|
65
|
+
id: "datasets-yml",
|
|
66
|
+
label: "datasets.yml present",
|
|
67
|
+
status: "warn",
|
|
68
|
+
detail: "file not found at project root",
|
|
69
|
+
hint: "datasets.yml registers every dataset the browser serves"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (ctx.datasetsYmlError) {
|
|
73
|
+
return {
|
|
74
|
+
id: "datasets-yml",
|
|
75
|
+
label: "datasets.yml parses",
|
|
76
|
+
status: "fail",
|
|
77
|
+
detail: ctx.datasetsYmlError,
|
|
78
|
+
hint: "fix the YAML syntax error"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const datasets = ctx.datasetsYml?.datasets;
|
|
82
|
+
if (!Array.isArray(datasets) || datasets.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
id: "datasets-yml",
|
|
85
|
+
label: "datasets.yml lists datasets",
|
|
86
|
+
status: "warn",
|
|
87
|
+
detail: "`datasets` key is empty or missing",
|
|
88
|
+
hint: "add at least one dataset entry; see docs/adding-a-dataset.md"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
id: "datasets-yml",
|
|
93
|
+
label: `datasets.yml lists ${datasets.length} dataset(s)`,
|
|
94
|
+
status: "pass"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function datasetsFetchedCheck(ctx) {
|
|
98
|
+
const datasets = ctx.datasetsYml?.datasets ?? [];
|
|
99
|
+
if (!datasets.length) {
|
|
100
|
+
return { id: "datasets-fetched", label: "Source datasets fetched", status: "warn", detail: "no datasets registered" };
|
|
101
|
+
}
|
|
102
|
+
const dotDatasets = resolve(ctx.cwd, ".datasets");
|
|
103
|
+
const missing = datasets.filter((d) => !existsSync(join(dotDatasets, d.id, "concepts"))).map((d) => d.id);
|
|
104
|
+
if (missing.length) {
|
|
105
|
+
return {
|
|
106
|
+
id: "datasets-fetched",
|
|
107
|
+
label: "Source datasets fetched",
|
|
108
|
+
status: "fail",
|
|
109
|
+
detail: `.datasets/ missing: ${missing.join(", ")}`,
|
|
110
|
+
hint: "run `npm run fetch-datasets`"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { id: "datasets-fetched", label: "Source datasets fetched", status: "pass" };
|
|
114
|
+
}
|
|
115
|
+
function datasetsGeneratedCheck(ctx) {
|
|
116
|
+
const datasets = ctx.datasetsYml?.datasets ?? [];
|
|
117
|
+
if (!datasets.length) {
|
|
118
|
+
return { id: "datasets-generated", label: "Generated data present", status: "warn", detail: "no datasets registered" };
|
|
119
|
+
}
|
|
120
|
+
const publicData = resolve(ctx.cwd, "public", "data");
|
|
121
|
+
const missing = datasets.filter((d) => !existsSync(join(publicData, d.id, "manifest.json"))).map((d) => d.id);
|
|
122
|
+
if (missing.length) {
|
|
123
|
+
return {
|
|
124
|
+
id: "datasets-generated",
|
|
125
|
+
label: "Generated data present",
|
|
126
|
+
status: "fail",
|
|
127
|
+
detail: `public/data/ missing manifest for: ${missing.join(", ")}`,
|
|
128
|
+
hint: "run `npm run generate-data`"
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { id: "datasets-generated", label: "Generated data present", status: "pass" };
|
|
132
|
+
}
|
|
133
|
+
function publicDatasetsJsonCheck(ctx) {
|
|
134
|
+
const path = resolve(ctx.cwd, "public", "datasets.json");
|
|
135
|
+
if (!existsSync(path)) {
|
|
136
|
+
return {
|
|
137
|
+
id: "public-datasets-json",
|
|
138
|
+
label: "public/datasets.json present",
|
|
139
|
+
status: "fail",
|
|
140
|
+
hint: "run `npm run generate-data`"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const doc = JSON.parse(readFileSync(path, "utf8"));
|
|
145
|
+
if (!Array.isArray(doc) || doc.length === 0) {
|
|
146
|
+
return {
|
|
147
|
+
id: "public-datasets-json",
|
|
148
|
+
label: "public/datasets.json non-empty",
|
|
149
|
+
status: "warn",
|
|
150
|
+
detail: "datasets list is empty"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { id: "public-datasets-json", label: "public/datasets.json parses", status: "pass" };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return {
|
|
156
|
+
id: "public-datasets-json",
|
|
157
|
+
label: "public/datasets.json parses",
|
|
158
|
+
status: "fail",
|
|
159
|
+
detail: err.message
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function shaclShapesCheck(ctx) {
|
|
164
|
+
const shapesPath = resolve(ctx.pkgRoot, "data", "concept-model", "shapes", "glossarist.shacl.ttl");
|
|
165
|
+
if (!existsSync(shapesPath)) {
|
|
166
|
+
return {
|
|
167
|
+
id: "shacl-shapes",
|
|
168
|
+
label: "SHACL shapes present",
|
|
169
|
+
status: "fail",
|
|
170
|
+
hint: "run `npm run sync:model` to vendor shapes from concept-model"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const ttl = readFileSync(shapesPath, "utf8");
|
|
174
|
+
const looksLikeShapes = /sh:\s*(NodeShape|PropertyShape|targetClass)/.test(ttl) || /a\s+sh:NodeShape/.test(ttl);
|
|
175
|
+
if (!looksLikeShapes) {
|
|
176
|
+
return {
|
|
177
|
+
id: "shacl-shapes",
|
|
178
|
+
label: "SHACL shapes well-formed",
|
|
179
|
+
status: "fail",
|
|
180
|
+
detail: "file exists but does not declare any sh:NodeShape",
|
|
181
|
+
hint: "re-sync from glossarist/concept-model"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { id: "shacl-shapes", label: "SHACL shapes present", status: "pass" };
|
|
185
|
+
}
|
|
186
|
+
function jsonldContextCheck(ctx) {
|
|
187
|
+
const ctxPath = resolve(ctx.pkgRoot, "data", "concept-model", "glossarist.context.jsonld");
|
|
188
|
+
if (!existsSync(ctxPath)) {
|
|
189
|
+
return {
|
|
190
|
+
id: "jsonld-context",
|
|
191
|
+
label: "JSON-LD context present",
|
|
192
|
+
status: "fail",
|
|
193
|
+
hint: "run `npm run sync:model` to vendor the context"
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const doc = JSON.parse(readFileSync(ctxPath, "utf8"));
|
|
198
|
+
if (!doc["@context"] || typeof doc["@context"] !== "object") {
|
|
199
|
+
return {
|
|
200
|
+
id: "jsonld-context",
|
|
201
|
+
label: "JSON-LD context well-formed",
|
|
202
|
+
status: "fail",
|
|
203
|
+
detail: "missing or invalid @context key"
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { id: "jsonld-context", label: "JSON-LD context present", status: "pass" };
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return {
|
|
209
|
+
id: "jsonld-context",
|
|
210
|
+
label: "JSON-LD context parses",
|
|
211
|
+
status: "fail",
|
|
212
|
+
detail: err.message
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
var CHECKS = [
|
|
217
|
+
safe("node-version", nodeVersionCheck),
|
|
218
|
+
safe("package-deps", packageDepsCheck),
|
|
219
|
+
safe("datasets-yml", datasetsYmlCheck),
|
|
220
|
+
safe("datasets-fetched", datasetsFetchedCheck),
|
|
221
|
+
safe("datasets-generated", datasetsGeneratedCheck),
|
|
222
|
+
safe("public-datasets-json", publicDatasetsJsonCheck),
|
|
223
|
+
safe("shacl-shapes", shaclShapesCheck),
|
|
224
|
+
safe("jsonld-context", jsonldContextCheck)
|
|
225
|
+
];
|
|
226
|
+
async function runDoctor(cwd = process.cwd()) {
|
|
227
|
+
const datasetsYmlPath = resolve(cwd, "datasets.yml");
|
|
228
|
+
let datasetsYml;
|
|
229
|
+
let datasetsYmlError;
|
|
230
|
+
if (existsSync(datasetsYmlPath)) {
|
|
231
|
+
try {
|
|
232
|
+
datasetsYml = yaml.load(readFileSync(datasetsYmlPath, "utf8"));
|
|
233
|
+
} catch (err) {
|
|
234
|
+
datasetsYmlError = err.message;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const ctx = { cwd, pkgRoot: PKG_ROOT, datasetsYml, datasetsYmlError };
|
|
238
|
+
const results = [];
|
|
239
|
+
for (const check of CHECKS) {
|
|
240
|
+
results.push(await check(ctx));
|
|
241
|
+
}
|
|
242
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
243
|
+
const exitCode = failed > 0 ? 1 : 0;
|
|
244
|
+
return { results, exitCode };
|
|
245
|
+
}
|
|
246
|
+
var STATUS_GLYPH = { pass: "\u2713", fail: "\u2717", warn: "!" };
|
|
247
|
+
function formatResults(results) {
|
|
248
|
+
const lines = [];
|
|
249
|
+
for (const r of results) {
|
|
250
|
+
lines.push(`${STATUS_GLYPH[r.status]} ${r.label}`);
|
|
251
|
+
if (r.detail) lines.push(` ${r.detail}`);
|
|
252
|
+
if (r.hint) lines.push(` \u2192 ${r.hint}`);
|
|
253
|
+
}
|
|
254
|
+
const pass = results.filter((r) => r.status === "pass").length;
|
|
255
|
+
const fail = results.filter((r) => r.status === "fail").length;
|
|
256
|
+
const warn = results.filter((r) => r.status === "warn").length;
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push(`${pass} passed, ${fail} failed, ${warn} warnings (${results.length} total)`);
|
|
259
|
+
return lines.join("\n");
|
|
260
|
+
}
|
|
261
|
+
async function main() {
|
|
262
|
+
const { results, exitCode } = await runDoctor();
|
|
263
|
+
console.log(formatResults(results));
|
|
264
|
+
process.exit(exitCode);
|
|
265
|
+
}
|
|
266
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
267
|
+
main().catch((err) => {
|
|
268
|
+
console.error(err);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
export {
|
|
273
|
+
CHECKS,
|
|
274
|
+
NODE_MIN_MAJOR,
|
|
275
|
+
formatResults,
|
|
276
|
+
main,
|
|
277
|
+
runDoctor
|
|
278
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// scripts/extract-source-refs.ts
|
|
2
|
+
function extractSourceRefs(concept, registerId) {
|
|
3
|
+
const refs = /* @__PURE__ */ new Set();
|
|
4
|
+
for (const src of concept["gl:source"] || []) {
|
|
5
|
+
const origin = src["gl:origin"];
|
|
6
|
+
if (origin) {
|
|
7
|
+
const ref = origin["gl:ref"];
|
|
8
|
+
if (ref?.["gl:source"]) refs.add(ref["gl:source"]);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const lc of Object.values(concept["gl:localizedConcept"] || {})) {
|
|
12
|
+
for (const src of lc["gl:source"] || []) {
|
|
13
|
+
const origin = src["gl:origin"];
|
|
14
|
+
if (origin) {
|
|
15
|
+
const ref = origin["gl:ref"];
|
|
16
|
+
if (ref?.["gl:source"]) refs.add(ref["gl:source"]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return [...refs].map((source) => ({ source, registerId }));
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
extractSourceRefs
|
|
24
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/fetch-datasets.ts
|
|
4
|
+
import fs2 from "fs";
|
|
5
|
+
import path2 from "path";
|
|
6
|
+
import JSZip from "jszip";
|
|
7
|
+
import { loadGcr } from "glossarist";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
|
+
|
|
11
|
+
// scripts/load-site-config.ts
|
|
12
|
+
import { readFileSync, existsSync } from "fs";
|
|
13
|
+
import { resolve, dirname } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import yaml from "js-yaml";
|
|
16
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
var projectRoot = resolve(__dirname, "..");
|
|
18
|
+
var SNAKE_TO_CAMEL = {
|
|
19
|
+
// Site-level
|
|
20
|
+
uri_base: "uriBase",
|
|
21
|
+
base_path: "basePath",
|
|
22
|
+
ui_languages: "uiLanguages",
|
|
23
|
+
dataset_groups: "datasetGroups",
|
|
24
|
+
dataset_translations: "datasetTranslations",
|
|
25
|
+
primary_color: "primaryColor",
|
|
26
|
+
dark_color: "darkColor",
|
|
27
|
+
owner_name: "ownerName",
|
|
28
|
+
owner_url: "ownerUrl",
|
|
29
|
+
powered_by: "poweredBy",
|
|
30
|
+
footer_nav: "footerNav",
|
|
31
|
+
footer_logo: "footerLogo",
|
|
32
|
+
news_dir: "newsDir",
|
|
33
|
+
// Dataset-level
|
|
34
|
+
gcr_package: "gcrPackage",
|
|
35
|
+
source_repo: "sourceRepo",
|
|
36
|
+
local_path: "localPath",
|
|
37
|
+
local_light: "localLight",
|
|
38
|
+
local_dark: "localDark",
|
|
39
|
+
uri_aliases: "uriAliases",
|
|
40
|
+
language_order: "languageOrder",
|
|
41
|
+
ref_aliases: "refAliases",
|
|
42
|
+
urn_aliases: "urnAliases",
|
|
43
|
+
existing_site_url: "existingSiteUrl",
|
|
44
|
+
edition_status: "editionStatus",
|
|
45
|
+
dataset_uri: "datasetUri",
|
|
46
|
+
schema_version: "schemaVersion"
|
|
47
|
+
};
|
|
48
|
+
function normalizeKey(key) {
|
|
49
|
+
if (typeof key !== "string") return key;
|
|
50
|
+
if (Object.prototype.hasOwnProperty.call(SNAKE_TO_CAMEL, key)) {
|
|
51
|
+
return SNAKE_TO_CAMEL[key];
|
|
52
|
+
}
|
|
53
|
+
if (key.includes("_") && /^[a-z][a-z0-9_]*$/.test(key)) {
|
|
54
|
+
return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
55
|
+
}
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
function normalizeObject(obj) {
|
|
59
|
+
if (Array.isArray(obj)) return obj.map(normalizeObject);
|
|
60
|
+
if (obj && typeof obj === "object") {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
63
|
+
out[normalizeKey(k)] = normalizeObject(v);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
return obj;
|
|
68
|
+
}
|
|
69
|
+
function findConfigFile(args = []) {
|
|
70
|
+
if (process.env.SITE_CONFIG) {
|
|
71
|
+
return resolve(process.env.SITE_CONFIG);
|
|
72
|
+
}
|
|
73
|
+
for (const dir of [process.cwd(), projectRoot]) {
|
|
74
|
+
const p = resolve(dir, "site-config.yml");
|
|
75
|
+
if (existsSync(p)) return p;
|
|
76
|
+
}
|
|
77
|
+
throw new Error("No site config found. Set SITE_CONFIG or create site-config.yml");
|
|
78
|
+
}
|
|
79
|
+
function loadSiteConfig(args = []) {
|
|
80
|
+
const configPath = findConfigFile(args);
|
|
81
|
+
const raw = yaml.load(readFileSync(configPath, "utf-8"));
|
|
82
|
+
const config = normalizeObject(raw);
|
|
83
|
+
return { config, configPath };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// scripts/lib/local-path-safety.ts
|
|
87
|
+
import fs from "fs";
|
|
88
|
+
import path from "path";
|
|
89
|
+
function physicalPath(p) {
|
|
90
|
+
if (fs.existsSync(p)) return fs.realpathSync(p);
|
|
91
|
+
const parent = path.dirname(p);
|
|
92
|
+
const parentReal = fs.existsSync(parent) ? fs.realpathSync(parent) : physicalPath(parent);
|
|
93
|
+
return path.join(parentReal, path.basename(p));
|
|
94
|
+
}
|
|
95
|
+
function assertLocalPathSafe(datasetId, localPath, { root = process.cwd(), datasetsDir } = {}) {
|
|
96
|
+
const datasetsRoot = datasetsDir || path.join(root, ".datasets");
|
|
97
|
+
const localResolved = path.resolve(root, localPath);
|
|
98
|
+
if (!fs.existsSync(localResolved)) {
|
|
99
|
+
throw new Error(`localPath for ${datasetId} does not exist: ${localResolved}`);
|
|
100
|
+
}
|
|
101
|
+
const localReal = fs.realpathSync(localResolved);
|
|
102
|
+
const stagedAbs = path.join(datasetsRoot, datasetId);
|
|
103
|
+
const stagedReal = physicalPath(stagedAbs);
|
|
104
|
+
if (localReal === stagedReal) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`localPath for ${datasetId} resolves to the same physical location as .datasets/${datasetId} (${localReal}). Refusing to operate \u2014 source and staging would clobber. Use a path outside .datasets/.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (localReal.startsWith(stagedReal + path.sep)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`localPath for ${datasetId} is nested inside .datasets/${datasetId}. Refusing to operate \u2014 staging operations would destroy source data. Use a path outside .datasets/.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (stagedReal.startsWith(localReal + path.sep)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`localPath for ${datasetId} contains .datasets/${datasetId}. Refusing to operate \u2014 staging operations would destroy source data. Use a path outside localPath.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return localReal;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// scripts/fetch-datasets.ts
|
|
123
|
+
var ROOT = process.cwd();
|
|
124
|
+
var DATASETS_DIR = path2.join(ROOT, ".datasets");
|
|
125
|
+
var GCR_DIR = path2.join(ROOT, ".gcr");
|
|
126
|
+
function matchUriPattern(uri, pattern) {
|
|
127
|
+
if (!pattern.endsWith("*")) return uri === pattern;
|
|
128
|
+
return uri.startsWith(pattern.slice(0, -1));
|
|
129
|
+
}
|
|
130
|
+
async function downloadGcr(url, destPath) {
|
|
131
|
+
fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
|
|
132
|
+
console.log(` Downloading ${url}...`);
|
|
133
|
+
const resp = await fetch(url);
|
|
134
|
+
if (!resp.ok) throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
|
|
135
|
+
const buf = Buffer.from(await resp.arrayBuffer());
|
|
136
|
+
fs2.writeFileSync(destPath, buf);
|
|
137
|
+
console.log(` Saved to ${destPath} (${(buf.length / 1024).toFixed(0)} KB)`);
|
|
138
|
+
}
|
|
139
|
+
async function extractGcr(gcrPath, targetDir) {
|
|
140
|
+
const targetAbs = path2.resolve(targetDir);
|
|
141
|
+
if (fs2.existsSync(targetAbs)) {
|
|
142
|
+
fs2.rmSync(targetAbs, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
fs2.mkdirSync(targetAbs, { recursive: true });
|
|
145
|
+
const buf = fs2.readFileSync(gcrPath);
|
|
146
|
+
const zip = await JSZip.loadAsync(buf);
|
|
147
|
+
const entries = Object.values(zip.files);
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
if (entry.dir) continue;
|
|
150
|
+
const dest = path2.resolve(targetAbs, entry.name);
|
|
151
|
+
if (dest !== targetAbs && !dest.startsWith(targetAbs + path2.sep)) {
|
|
152
|
+
throw new Error(`Refusing to extract entry outside target dir: ${entry.name}`);
|
|
153
|
+
}
|
|
154
|
+
fs2.mkdirSync(path2.dirname(dest), { recursive: true });
|
|
155
|
+
const content = await entry.async("nodebuffer");
|
|
156
|
+
fs2.writeFileSync(dest, content);
|
|
157
|
+
}
|
|
158
|
+
console.log(` Extracted to ${targetAbs}`);
|
|
159
|
+
}
|
|
160
|
+
async function readGcrMetadata(gcrPath) {
|
|
161
|
+
if (!fs2.existsSync(gcrPath)) return null;
|
|
162
|
+
try {
|
|
163
|
+
const buf = fs2.readFileSync(gcrPath);
|
|
164
|
+
const pkg = await loadGcr(buf);
|
|
165
|
+
const meta = await pkg.metadata();
|
|
166
|
+
return meta ? meta.toJSON() : null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function validateDependencies(config, gcrMetadata) {
|
|
172
|
+
const errors = [];
|
|
173
|
+
const allProvidedUris = [];
|
|
174
|
+
for (const ds of config.datasets) {
|
|
175
|
+
allProvidedUris.push(ds.uri);
|
|
176
|
+
if (ds.uriAliases) allProvidedUris.push(...ds.uriAliases);
|
|
177
|
+
}
|
|
178
|
+
const routingUris = (config.routing || []).map((r) => r.uri);
|
|
179
|
+
for (const ds of config.datasets) {
|
|
180
|
+
const meta = gcrMetadata[ds.id];
|
|
181
|
+
if (!meta?.dependencies?.length) continue;
|
|
182
|
+
for (const dep of meta.dependencies) {
|
|
183
|
+
const depUri = dep.uri;
|
|
184
|
+
const satisfied = [...allProvidedUris, ...routingUris].some((p) => matchUriPattern(depUri, p));
|
|
185
|
+
if (!satisfied) {
|
|
186
|
+
errors.push(`GCR '${ds.id}' depends on '${depUri}' (${dep.refCount} refs) but no provider or route configured`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return errors;
|
|
191
|
+
}
|
|
192
|
+
function cloneOrUpdate(sourceRepo, targetDir) {
|
|
193
|
+
const env = { ...process.env };
|
|
194
|
+
let repoUrl = sourceRepo;
|
|
195
|
+
if (env.GITHUB_TOKEN) {
|
|
196
|
+
repoUrl = sourceRepo.replace("https://", `https://x-access-token:${env.GITHUB_TOKEN}@`);
|
|
197
|
+
}
|
|
198
|
+
const targetAbs = path2.resolve(targetDir);
|
|
199
|
+
if (fs2.existsSync(path2.join(targetAbs, ".git"))) {
|
|
200
|
+
console.log(` Updating existing clone...`);
|
|
201
|
+
try {
|
|
202
|
+
execFileSync("git", ["fetch", "origin"], { cwd: targetAbs, stdio: "pipe", env });
|
|
203
|
+
execFileSync("git", ["reset", "--hard", "origin/HEAD"], { cwd: targetAbs, stdio: "pipe", env });
|
|
204
|
+
execFileSync("git", ["clean", "-fd"], { cwd: targetAbs, stdio: "pipe", env });
|
|
205
|
+
} catch {
|
|
206
|
+
console.warn(` git update failed, re-cloning`);
|
|
207
|
+
fs2.rmSync(targetAbs, { recursive: true, force: true });
|
|
208
|
+
execFileSync("git", ["clone", "--depth", "1", repoUrl, targetAbs], { stdio: "pipe", env });
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
fs2.mkdirSync(targetAbs, { recursive: true });
|
|
212
|
+
console.log(` Cloning ${sourceRepo}...`);
|
|
213
|
+
execFileSync("git", ["clone", "--depth", "1", repoUrl, targetAbs], { stdio: "pipe", env });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function main() {
|
|
217
|
+
console.log("Fetching glossarist datasets...\n");
|
|
218
|
+
const { config } = loadSiteConfig();
|
|
219
|
+
const gcrMetadata = {};
|
|
220
|
+
for (const ds of config.datasets) {
|
|
221
|
+
console.log(`${ds.id}:`);
|
|
222
|
+
const gcrPath = path2.join(GCR_DIR, `${ds.id}.gcr`);
|
|
223
|
+
const targetDir = path2.join(DATASETS_DIR, ds.id);
|
|
224
|
+
try {
|
|
225
|
+
if (fs2.existsSync(gcrPath)) {
|
|
226
|
+
console.log(` Using local .gcr/${ds.id}.gcr`);
|
|
227
|
+
await extractGcr(gcrPath, targetDir);
|
|
228
|
+
} else if (ds.gcrPackage) {
|
|
229
|
+
console.log(` Using GCR package: ${ds.gcrPackage}`);
|
|
230
|
+
try {
|
|
231
|
+
await downloadGcr(ds.gcrPackage, gcrPath);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
console.warn(` GCR download failed: ${e.message}`);
|
|
234
|
+
console.warn(` Skipping ${ds.id}`);
|
|
235
|
+
console.log();
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
await extractGcr(gcrPath, targetDir);
|
|
239
|
+
} else if (ds.localPath) {
|
|
240
|
+
const localResolved = assertLocalPathSafe(ds.id, ds.localPath);
|
|
241
|
+
console.log(` Using localPath in-place: ${localResolved}`);
|
|
242
|
+
} else if (ds.sourceRepo) {
|
|
243
|
+
cloneOrUpdate(ds.sourceRepo, targetDir);
|
|
244
|
+
} else {
|
|
245
|
+
console.warn(` No source configured, skipping`);
|
|
246
|
+
console.log();
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const meta = await readGcrMetadata(gcrPath);
|
|
250
|
+
if (meta) {
|
|
251
|
+
gcrMetadata[ds.id] = meta;
|
|
252
|
+
console.log(` ${meta.concept_count || "?"} concepts, ${meta.uri_prefix || "no uri"}`);
|
|
253
|
+
}
|
|
254
|
+
} catch (e) {
|
|
255
|
+
console.warn(` Failed: ${e.message}`);
|
|
256
|
+
console.warn(` Skipping ${ds.id}`);
|
|
257
|
+
}
|
|
258
|
+
console.log();
|
|
259
|
+
}
|
|
260
|
+
console.log("Validating dependencies...\n");
|
|
261
|
+
const errors = validateDependencies(config, gcrMetadata);
|
|
262
|
+
if (errors.length) {
|
|
263
|
+
console.error("Dependency validation FAILED:");
|
|
264
|
+
for (const err of errors) {
|
|
265
|
+
console.error(` \u2717 ${err}`);
|
|
266
|
+
}
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
console.log("All dependencies satisfied.\n");
|
|
270
|
+
console.log("Done.");
|
|
271
|
+
}
|
|
272
|
+
var isDirectInvocation = process.argv[1] && fs2.realpathSync(process.argv[1]) === fs2.realpathSync(fileURLToPath2(import.meta.url));
|
|
273
|
+
if (isDirectInvocation) {
|
|
274
|
+
await main();
|
|
275
|
+
}
|
|
276
|
+
export {
|
|
277
|
+
main
|
|
278
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// scripts/generate-404.ts
|
|
2
|
+
import { copyFileSync, existsSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
var dist = join(process.cwd(), "dist");
|
|
7
|
+
var indexHtml = join(dist, "index.html");
|
|
8
|
+
var notFoundHtml = join(dist, "404.html");
|
|
9
|
+
if (existsSync(indexHtml)) {
|
|
10
|
+
copyFileSync(indexHtml, notFoundHtml);
|
|
11
|
+
console.log("Created dist/404.html for SPA fallback");
|
|
12
|
+
} else {
|
|
13
|
+
console.warn("dist/index.html not found \u2014 skipping 404.html generation");
|
|
14
|
+
}
|