@glossarist/concept-browser 0.7.115 → 0.7.116
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 +5 -1
- package/scripts/bridge-to-astro.js +132 -0
- package/scripts/build-edges.js +480 -0
- package/scripts/build-scripts.mjs +22 -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/smoke.js +211 -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 +84 -0
- package/src/__tests__/scripts-syntax.test.ts +2 -2
- package/src/data/ontology-schema.json +1336 -183
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/migrate-v1-to-v3.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
function isV1PartitiveRelation(rel) {
|
|
7
|
+
return rel && Array.isArray(rel.parts);
|
|
8
|
+
}
|
|
9
|
+
function migrateRelation(rel) {
|
|
10
|
+
if (!isV1PartitiveRelation(rel)) return null;
|
|
11
|
+
return {
|
|
12
|
+
comprehensive: rel.comprehensive,
|
|
13
|
+
completeness: rel.enumeration === "open" ? "partial" : rel.enumeration === "closed" || rel.enumeration == null ? "complete" : rel.enumeration,
|
|
14
|
+
partitives: (rel.parts ?? []).map((p) => ({
|
|
15
|
+
ref: p,
|
|
16
|
+
certainty: "confirmed"
|
|
17
|
+
})),
|
|
18
|
+
...rel.content ? { content: rel.content } : {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function migrateConceptData(data) {
|
|
22
|
+
if (!data?.partitive_relations?.length) return null;
|
|
23
|
+
const migrated = data.partitive_relations.map(migrateRelation).filter((r) => r !== null);
|
|
24
|
+
if (migrated.length === 0) return null;
|
|
25
|
+
return { ...data, partitive_relations: migrated };
|
|
26
|
+
}
|
|
27
|
+
function isAlreadyMigrated(content) {
|
|
28
|
+
return Boolean(content?.data?.partitive_relations?.some?.((rel) => Array.isArray(rel.partitives)));
|
|
29
|
+
}
|
|
30
|
+
function walkFiles(input) {
|
|
31
|
+
const stat = fs.statSync(input);
|
|
32
|
+
if (stat.isFile()) return [input];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const entry of fs.readdirSync(input, { withFileTypes: true })) {
|
|
35
|
+
const full = path.join(input, entry.name);
|
|
36
|
+
if (entry.isDirectory()) out.push(...walkFiles(full));
|
|
37
|
+
else if (entry.isFile() && /\.(ya?ml)$/i.test(entry.name)) out.push(full);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function loadYamlModule() {
|
|
42
|
+
try {
|
|
43
|
+
return await import("js-yaml");
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function outputPath(input, file, outRoot) {
|
|
49
|
+
const stat = fs.statSync(input);
|
|
50
|
+
if (stat.isFile()) return outRoot;
|
|
51
|
+
const rel = path.relative(input, file);
|
|
52
|
+
return path.join(outRoot, rel);
|
|
53
|
+
}
|
|
54
|
+
async function migrateFile(input, file, options, yaml) {
|
|
55
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
56
|
+
const parsed = yaml ? yaml.load(raw) : JSON.parse(raw);
|
|
57
|
+
if (!parsed?.data) return { changed: false };
|
|
58
|
+
if (isAlreadyMigrated(parsed)) return { changed: false, skipped: true };
|
|
59
|
+
const migrated = migrateConceptData(parsed.data);
|
|
60
|
+
if (!migrated) return { changed: false };
|
|
61
|
+
parsed.data = { ...parsed.data, ...migrated };
|
|
62
|
+
const out = outputPath(input, file, options.out);
|
|
63
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
64
|
+
const outText = yaml ? yaml.dump(parsed) : JSON.stringify(parsed, null, 2);
|
|
65
|
+
if (!options.dryRun) fs.writeFileSync(out, outText);
|
|
66
|
+
return { changed: true, out, in: file };
|
|
67
|
+
}
|
|
68
|
+
async function main() {
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
const opts = { in: null, out: null, dryRun: false };
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
const a = args[i];
|
|
73
|
+
if (a === "--in") opts.in = args[++i];
|
|
74
|
+
else if (a === "--out") opts.out = args[++i];
|
|
75
|
+
else if (a === "--dry-run") opts.dryRun = true;
|
|
76
|
+
else if (!opts.in) opts.in = a;
|
|
77
|
+
else if (!opts.out) opts.out = a;
|
|
78
|
+
}
|
|
79
|
+
if (!opts.in) {
|
|
80
|
+
console.error("Usage: node scripts/migrate-v1-to-v3.mjs --in <dir|file> [--out <dir|file>] [--dry-run]");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (!opts.out) opts.out = opts.dryRun ? opts.in : `${opts.in}.v3`;
|
|
84
|
+
const yaml = await loadYamlModule();
|
|
85
|
+
if (!yaml) {
|
|
86
|
+
console.error("js-yaml is required; install via npm install js-yaml");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const files = walkFiles(opts.in);
|
|
90
|
+
let changed = 0, skipped = 0, total = 0;
|
|
91
|
+
for (const f of files) {
|
|
92
|
+
total++;
|
|
93
|
+
const res = await migrateFile(opts.in, f, opts, yaml);
|
|
94
|
+
if (res.changed) {
|
|
95
|
+
changed++;
|
|
96
|
+
console.log(`\u2713 ${res.in} \u2192 ${res.out}`);
|
|
97
|
+
} else if (res.skipped) {
|
|
98
|
+
skipped++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log(`
|
|
102
|
+
${changed} migrated, ${skipped} skipped (already v3), ${total - changed - skipped} unchanged.`);
|
|
103
|
+
if (opts.dryRun) console.log("(dry-run \u2014 no files written)");
|
|
104
|
+
}
|
|
105
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
106
|
+
main();
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
isAlreadyMigrated,
|
|
110
|
+
isV1PartitiveRelation,
|
|
111
|
+
migrateConceptData,
|
|
112
|
+
migrateRelation
|
|
113
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/normalize-yaml.ts
|
|
4
|
+
import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { join, relative, extname, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
var ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
var YAML_EXTS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
9
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
|
|
10
|
+
function walk(dir) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const entry of readdirSync(dir)) {
|
|
13
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
14
|
+
const full = join(dir, entry);
|
|
15
|
+
const st = statSync(full);
|
|
16
|
+
if (st.isDirectory()) {
|
|
17
|
+
out.push(...walk(full));
|
|
18
|
+
} else if (YAML_EXTS.has(extname(entry))) {
|
|
19
|
+
out.push(full);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function normalizeYaml({ root = ROOT, check = false, paths = [] } = {}) {
|
|
25
|
+
const searchDirs = paths.length > 0 ? paths.map((p) => join(root, p)) : [join(root, ".datasets")];
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const dir of searchDirs) {
|
|
28
|
+
try {
|
|
29
|
+
statSync(dir);
|
|
30
|
+
} catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
files.push(...walk(dir));
|
|
34
|
+
}
|
|
35
|
+
files.sort();
|
|
36
|
+
let checked = 0;
|
|
37
|
+
let nonNfc = 0;
|
|
38
|
+
const fixed = [];
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const original = readFileSync(file, "utf8");
|
|
41
|
+
checked++;
|
|
42
|
+
if (!isNfc(original)) {
|
|
43
|
+
nonNfc++;
|
|
44
|
+
if (!check) {
|
|
45
|
+
writeFileSync(file, original.normalize("NFC"), "utf8");
|
|
46
|
+
fixed.push(relative(root, file));
|
|
47
|
+
} else {
|
|
48
|
+
fixed.push(relative(root, file));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { checked, nonNfc, fixed, check };
|
|
53
|
+
}
|
|
54
|
+
function isNfc(s) {
|
|
55
|
+
return s === s.normalize("NFC");
|
|
56
|
+
}
|
|
57
|
+
function main() {
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
const check = args.includes("--check");
|
|
60
|
+
const paths = args.filter((a) => !a.startsWith("-") && a !== "normalize");
|
|
61
|
+
const { checked, nonNfc, fixed } = normalizeYaml({ check, paths });
|
|
62
|
+
if (check) {
|
|
63
|
+
if (nonNfc === 0) {
|
|
64
|
+
process.stdout.write(`NFC OK: ${checked} file(s) checked, all normalized
|
|
65
|
+
`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
process.stderr.write(`NFC check failed: ${nonNfc} of ${checked} file(s) are not NFC-normalized
|
|
69
|
+
|
|
70
|
+
`);
|
|
71
|
+
for (const f of fixed) {
|
|
72
|
+
process.stderr.write(` ${f}
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
} else {
|
|
77
|
+
if (nonNfc === 0) {
|
|
78
|
+
process.stdout.write(`NFC OK: ${checked} file(s) checked, all already normalized
|
|
79
|
+
`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(`Normalized ${nonNfc} of ${checked} file(s)
|
|
83
|
+
`);
|
|
84
|
+
for (const f of fixed) {
|
|
85
|
+
process.stdout.write(` ${f}
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var isDirectEntry = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
91
|
+
if (isDirectEntry) {
|
|
92
|
+
main();
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
normalizeYaml
|
|
96
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/process-about-pages.ts
|
|
4
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync, readdirSync, statSync, realpathSync } from "node:fs";
|
|
5
|
+
import { join, basename, extname, resolve as resolve2 } from "node:path";
|
|
6
|
+
import { cwd } from "node:process";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
|
+
|
|
9
|
+
// scripts/load-site-config.ts
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { resolve, dirname } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import yaml from "js-yaml";
|
|
14
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
var projectRoot = resolve(__dirname, "..");
|
|
16
|
+
var SNAKE_TO_CAMEL = {
|
|
17
|
+
// Site-level
|
|
18
|
+
uri_base: "uriBase",
|
|
19
|
+
base_path: "basePath",
|
|
20
|
+
ui_languages: "uiLanguages",
|
|
21
|
+
dataset_groups: "datasetGroups",
|
|
22
|
+
dataset_translations: "datasetTranslations",
|
|
23
|
+
primary_color: "primaryColor",
|
|
24
|
+
dark_color: "darkColor",
|
|
25
|
+
owner_name: "ownerName",
|
|
26
|
+
owner_url: "ownerUrl",
|
|
27
|
+
powered_by: "poweredBy",
|
|
28
|
+
footer_nav: "footerNav",
|
|
29
|
+
footer_logo: "footerLogo",
|
|
30
|
+
news_dir: "newsDir",
|
|
31
|
+
// Dataset-level
|
|
32
|
+
gcr_package: "gcrPackage",
|
|
33
|
+
source_repo: "sourceRepo",
|
|
34
|
+
local_path: "localPath",
|
|
35
|
+
local_light: "localLight",
|
|
36
|
+
local_dark: "localDark",
|
|
37
|
+
uri_aliases: "uriAliases",
|
|
38
|
+
language_order: "languageOrder",
|
|
39
|
+
ref_aliases: "refAliases",
|
|
40
|
+
urn_aliases: "urnAliases",
|
|
41
|
+
existing_site_url: "existingSiteUrl",
|
|
42
|
+
edition_status: "editionStatus",
|
|
43
|
+
dataset_uri: "datasetUri",
|
|
44
|
+
schema_version: "schemaVersion"
|
|
45
|
+
};
|
|
46
|
+
function normalizeKey(key) {
|
|
47
|
+
if (typeof key !== "string") return key;
|
|
48
|
+
if (Object.prototype.hasOwnProperty.call(SNAKE_TO_CAMEL, key)) {
|
|
49
|
+
return SNAKE_TO_CAMEL[key];
|
|
50
|
+
}
|
|
51
|
+
if (key.includes("_") && /^[a-z][a-z0-9_]*$/.test(key)) {
|
|
52
|
+
return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
53
|
+
}
|
|
54
|
+
return key;
|
|
55
|
+
}
|
|
56
|
+
function normalizeObject(obj) {
|
|
57
|
+
if (Array.isArray(obj)) return obj.map(normalizeObject);
|
|
58
|
+
if (obj && typeof obj === "object") {
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
61
|
+
out[normalizeKey(k)] = normalizeObject(v);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
67
|
+
function findConfigFile(args = []) {
|
|
68
|
+
if (process.env.SITE_CONFIG) {
|
|
69
|
+
return resolve(process.env.SITE_CONFIG);
|
|
70
|
+
}
|
|
71
|
+
for (const dir of [process.cwd(), projectRoot]) {
|
|
72
|
+
const p = resolve(dir, "site-config.yml");
|
|
73
|
+
if (existsSync(p)) return p;
|
|
74
|
+
}
|
|
75
|
+
throw new Error("No site config found. Set SITE_CONFIG or create site-config.yml");
|
|
76
|
+
}
|
|
77
|
+
function loadSiteConfig(args = []) {
|
|
78
|
+
const configPath = findConfigFile(args);
|
|
79
|
+
const raw = yaml.load(readFileSync(configPath, "utf-8"));
|
|
80
|
+
const config = normalizeObject(raw);
|
|
81
|
+
return { config, configPath };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// scripts/process-about-pages.ts
|
|
85
|
+
function loadSiteConfigForAbout() {
|
|
86
|
+
try {
|
|
87
|
+
const { config } = loadSiteConfig([]);
|
|
88
|
+
return config;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function roots() {
|
|
94
|
+
const ROOT = cwd();
|
|
95
|
+
return {
|
|
96
|
+
ROOT,
|
|
97
|
+
PUBLIC_PAGES: join(ROOT, "public", "pages"),
|
|
98
|
+
DATASETS_DIR: join(ROOT, ".datasets"),
|
|
99
|
+
GROUPS_CONTENT_DIR: join(ROOT, "site-content", "groups")
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function renderMarkdown(text) {
|
|
103
|
+
const lines = text.split("\n");
|
|
104
|
+
const html = [];
|
|
105
|
+
let inList = false;
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (!trimmed) {
|
|
109
|
+
if (inList) {
|
|
110
|
+
html.push("</ul>");
|
|
111
|
+
inList = false;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (/^#{1}\s/.test(trimmed)) {
|
|
116
|
+
if (inList) {
|
|
117
|
+
html.push("</ul>");
|
|
118
|
+
inList = false;
|
|
119
|
+
}
|
|
120
|
+
html.push(`<h1>${inline(trimmed.replace(/^#\s/, ""))}</h1>`);
|
|
121
|
+
} else if (/^#{2}\s/.test(trimmed)) {
|
|
122
|
+
if (inList) {
|
|
123
|
+
html.push("</ul>");
|
|
124
|
+
inList = false;
|
|
125
|
+
}
|
|
126
|
+
html.push(`<h2>${inline(trimmed.replace(/^##\s/, ""))}</h2>`);
|
|
127
|
+
} else if (/^#{3}\s/.test(trimmed)) {
|
|
128
|
+
if (inList) {
|
|
129
|
+
html.push("</ul>");
|
|
130
|
+
inList = false;
|
|
131
|
+
}
|
|
132
|
+
html.push(`<h3>${inline(trimmed.replace(/^###\s/, ""))}</h3>`);
|
|
133
|
+
} else if (/^[-*]\s/.test(trimmed)) {
|
|
134
|
+
if (!inList) {
|
|
135
|
+
html.push("<ul>");
|
|
136
|
+
inList = true;
|
|
137
|
+
}
|
|
138
|
+
html.push(`<li>${inline(trimmed.replace(/^[-*]\s/, ""))}</li>`);
|
|
139
|
+
} else {
|
|
140
|
+
if (inList) {
|
|
141
|
+
html.push("</ul>");
|
|
142
|
+
inList = false;
|
|
143
|
+
}
|
|
144
|
+
html.push(`<p>${inline(trimmed)}</p>`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (inList) html.push("</ul>");
|
|
148
|
+
return html.join("\n");
|
|
149
|
+
}
|
|
150
|
+
function inline(text) {
|
|
151
|
+
return text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>').replace(/`(.+?)`/g, "<code>$1</code>");
|
|
152
|
+
}
|
|
153
|
+
function processAboutDir(sourceDir, outputPrefix, publicPagesDir) {
|
|
154
|
+
if (!existsSync2(sourceDir)) return 0;
|
|
155
|
+
let count = 0;
|
|
156
|
+
for (const file of readdirSync(sourceDir)) {
|
|
157
|
+
const fullPath = join(sourceDir, file);
|
|
158
|
+
if (!statSync(fullPath).isFile()) continue;
|
|
159
|
+
const ext = extname(file);
|
|
160
|
+
if (ext !== ".md" && ext !== ".adoc" && ext !== ".html") continue;
|
|
161
|
+
const base = basename(file, ext);
|
|
162
|
+
const langMatch = base.match(/^about\.(\w+)$/);
|
|
163
|
+
const lang = langMatch ? langMatch[1] : "eng";
|
|
164
|
+
const outputName = lang === "eng" ? `${outputPrefix}.json` : `${outputPrefix}.${lang}.json`;
|
|
165
|
+
const raw = readFileSync2(fullPath, "utf8");
|
|
166
|
+
let html;
|
|
167
|
+
let title;
|
|
168
|
+
if (ext === ".html") {
|
|
169
|
+
html = raw;
|
|
170
|
+
const titleMatch = raw.match(/<h1[^>]*>(.+?)<\/h1>/);
|
|
171
|
+
title = titleMatch ? titleMatch[1] : "About";
|
|
172
|
+
} else if (ext === ".md") {
|
|
173
|
+
html = renderMarkdown(raw);
|
|
174
|
+
const titleMatch = raw.match(/^#\s+(.+)$/m);
|
|
175
|
+
title = titleMatch ? titleMatch[1] : "About";
|
|
176
|
+
} else {
|
|
177
|
+
html = `<p>${raw.split("\n").filter((l) => l.trim() && !l.startsWith("=")).join("</p>\n<p>")}</p>`;
|
|
178
|
+
const titleMatch = raw.match(/^=\s+(.+)$/m);
|
|
179
|
+
title = titleMatch ? titleMatch[1] : "About";
|
|
180
|
+
}
|
|
181
|
+
const output = { title, html };
|
|
182
|
+
mkdirSync(publicPagesDir, { recursive: true });
|
|
183
|
+
writeFileSync(join(publicPagesDir, outputName), JSON.stringify(output));
|
|
184
|
+
count++;
|
|
185
|
+
console.log(` Compiled: ${file} \u2192 public/pages/${outputName}`);
|
|
186
|
+
}
|
|
187
|
+
return count;
|
|
188
|
+
}
|
|
189
|
+
function main() {
|
|
190
|
+
const { ROOT, PUBLIC_PAGES, DATASETS_DIR, GROUPS_CONTENT_DIR } = roots();
|
|
191
|
+
mkdirSync(PUBLIC_PAGES, { recursive: true });
|
|
192
|
+
let total = 0;
|
|
193
|
+
const siteConfig = loadSiteConfigForAbout();
|
|
194
|
+
const localPathMap = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const ds of siteConfig?.datasets || []) {
|
|
196
|
+
if (ds.id && ds.localPath) {
|
|
197
|
+
localPathMap.set(ds.id, resolve2(ROOT, ds.localPath));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
201
|
+
if (existsSync2(DATASETS_DIR)) {
|
|
202
|
+
for (const dsId of readdirSync(DATASETS_DIR)) {
|
|
203
|
+
seenIds.add(dsId);
|
|
204
|
+
const aboutDir = join(DATASETS_DIR, dsId, "about");
|
|
205
|
+
if (existsSync2(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
206
|
+
console.log(`Processing dataset: ${dsId}`);
|
|
207
|
+
total += processAboutDir(aboutDir, `dataset-${dsId}-about`, PUBLIC_PAGES);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const [dsId, dsPath] of localPathMap) {
|
|
212
|
+
if (seenIds.has(dsId)) continue;
|
|
213
|
+
const aboutDir = join(dsPath, "about");
|
|
214
|
+
if (existsSync2(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
215
|
+
console.log(`Processing dataset (localPath): ${dsId}`);
|
|
216
|
+
total += processAboutDir(aboutDir, `dataset-${dsId}-about`, PUBLIC_PAGES);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (existsSync2(GROUPS_CONTENT_DIR)) {
|
|
220
|
+
for (const groupId of readdirSync(GROUPS_CONTENT_DIR)) {
|
|
221
|
+
const aboutDir = join(GROUPS_CONTENT_DIR, groupId, "about");
|
|
222
|
+
if (existsSync2(aboutDir) && statSync(aboutDir).isDirectory()) {
|
|
223
|
+
console.log(`Processing group: ${groupId}`);
|
|
224
|
+
total += processAboutDir(aboutDir, `group-${groupId}-about`, PUBLIC_PAGES);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
console.log(total > 0 ? `
|
|
229
|
+
Compiled ${total} about page(s).` : "\nNo about pages found.");
|
|
230
|
+
}
|
|
231
|
+
var isDirectInvocation = process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath2(import.meta.url));
|
|
232
|
+
if (isDirectInvocation) {
|
|
233
|
+
main();
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
loadSiteConfigForAbout,
|
|
237
|
+
main,
|
|
238
|
+
processAboutDir,
|
|
239
|
+
renderMarkdown,
|
|
240
|
+
roots
|
|
241
|
+
};
|
package/scripts/smoke.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/smoke.ts
|
|
4
|
+
import { chromium } from "playwright";
|
|
5
|
+
import { createServer } from "http";
|
|
6
|
+
import { statSync, createReadStream } from "fs";
|
|
7
|
+
import { extname, join, normalize, resolve as pathResolve } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
10
|
+
var DIST = pathResolve(__dirname, "..", "dist");
|
|
11
|
+
var MIME = {
|
|
12
|
+
".html": "text/html; charset=utf-8",
|
|
13
|
+
".js": "application/javascript; charset=utf-8",
|
|
14
|
+
"": "application/javascript; charset=utf-8",
|
|
15
|
+
".css": "text/css; charset=utf-8",
|
|
16
|
+
".json": "application/json; charset=utf-8",
|
|
17
|
+
".svg": "image/svg+xml",
|
|
18
|
+
".png": "image/png",
|
|
19
|
+
".jpg": "image/jpeg",
|
|
20
|
+
".ico": "image/x-icon",
|
|
21
|
+
".webp": "image/webp",
|
|
22
|
+
".woff": "font/woff",
|
|
23
|
+
".woff2": "font/woff2",
|
|
24
|
+
".ttf": "font/ttf",
|
|
25
|
+
".map": "application/json; charset=utf-8",
|
|
26
|
+
".xml": "application/xml; charset=utf-8",
|
|
27
|
+
".txt": "text/plain; charset=utf-8",
|
|
28
|
+
".webmanifest": "application/manifest+json; charset=utf-8"
|
|
29
|
+
};
|
|
30
|
+
function serveDist(root) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const server = createServer((req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
35
|
+
let filePath = join(root, urlPath);
|
|
36
|
+
if (!extname(filePath)) {
|
|
37
|
+
if (statSafe(filePath)?.isFile()) {
|
|
38
|
+
return sendFile(res, filePath);
|
|
39
|
+
}
|
|
40
|
+
const candidates = [
|
|
41
|
+
filePath + ".html",
|
|
42
|
+
join(filePath, "index.html"),
|
|
43
|
+
join(root, "200.html"),
|
|
44
|
+
join(root, "index.html")
|
|
45
|
+
];
|
|
46
|
+
for (const c of candidates) {
|
|
47
|
+
if (statSafe(c)?.isFile()) {
|
|
48
|
+
return sendFile(res, c);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
res.writeHead(404);
|
|
52
|
+
return res.end("Not found");
|
|
53
|
+
}
|
|
54
|
+
if (statSafe(filePath)?.isFile()) {
|
|
55
|
+
return sendFile(res, filePath);
|
|
56
|
+
}
|
|
57
|
+
res.writeHead(404);
|
|
58
|
+
return res.end("Not found");
|
|
59
|
+
} catch (e) {
|
|
60
|
+
res.writeHead(500);
|
|
61
|
+
res.end(String(e ?? "server error"));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
server.listen(0, "127.0.0.1", () => {
|
|
65
|
+
const addr = server.address();
|
|
66
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
67
|
+
resolve({ server, origin: `http://127.0.0.1:${port}` });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function statSafe(p) {
|
|
72
|
+
try {
|
|
73
|
+
return statSync(p);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function sendFile(res, filePath) {
|
|
79
|
+
const safe = normalize(filePath);
|
|
80
|
+
const ct = MIME[extname(safe)] ?? "application/octet-stream";
|
|
81
|
+
res.writeHead(200, { "Content-Type": ct });
|
|
82
|
+
createReadStream(safe).pipe(res);
|
|
83
|
+
}
|
|
84
|
+
var PAGE_PATHS = [
|
|
85
|
+
"/",
|
|
86
|
+
"/learn/",
|
|
87
|
+
"/learn/relationships/",
|
|
88
|
+
"/learn/designations/",
|
|
89
|
+
"/learn/statuses/"
|
|
90
|
+
];
|
|
91
|
+
var CONSUMER_DATA_FILES = [
|
|
92
|
+
"/datasets.json",
|
|
93
|
+
"/site-config.json",
|
|
94
|
+
"/data/cross-ref-index.json"
|
|
95
|
+
];
|
|
96
|
+
function isConsumerDataFile(url) {
|
|
97
|
+
try {
|
|
98
|
+
const u = new URL(url);
|
|
99
|
+
return CONSUMER_DATA_FILES.some((f) => u.pathname === f || u.pathname.startsWith("/data/"));
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function probePage(browser, origin, path) {
|
|
105
|
+
const url = new URL(path, origin).toString();
|
|
106
|
+
const page = await browser.newPage();
|
|
107
|
+
const errors = [];
|
|
108
|
+
const failedRequests = [];
|
|
109
|
+
page.on("pageerror", (e) => {
|
|
110
|
+
errors.push({ kind: "pageerror", message: e.message, stack: e.stack ?? "" });
|
|
111
|
+
});
|
|
112
|
+
page.on("console", (msg) => {
|
|
113
|
+
if (msg.type() !== "error") return;
|
|
114
|
+
const text = msg.text();
|
|
115
|
+
if (/Failed to load resource/i.test(text)) return;
|
|
116
|
+
if (/fonts\.g(oogle|static)\.com/.test(text)) return;
|
|
117
|
+
errors.push({ kind: "console.error", message: text });
|
|
118
|
+
});
|
|
119
|
+
page.on("requestfailed", (req) => {
|
|
120
|
+
const u = req.url();
|
|
121
|
+
if (u.startsWith(origin) && !isConsumerDataFile(u)) {
|
|
122
|
+
failedRequests.push({ url: u, failure: req.failure()?.errorText ?? "failed" });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
page.on("response", (resp) => {
|
|
126
|
+
const u = resp.url();
|
|
127
|
+
if (u.startsWith(origin) && resp.status() >= 400 && !isConsumerDataFile(u)) {
|
|
128
|
+
failedRequests.push({ url: u, status: resp.status() });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
const resp = await page.goto(url, { waitUntil: "networkidle", timeout: 15e3 });
|
|
133
|
+
if (resp && resp.status() >= 400) {
|
|
134
|
+
return { path, url, status: resp.status(), bodyLen: 0, errors, failedRequests, skipped: path !== "/" };
|
|
135
|
+
}
|
|
136
|
+
await page.waitForTimeout(500);
|
|
137
|
+
const bodyLen = await page.evaluate(() => document.body?.innerText?.length ?? 0);
|
|
138
|
+
return { path, url, status: resp?.status() ?? 0, bodyLen, errors, failedRequests, skipped: false };
|
|
139
|
+
} catch (e) {
|
|
140
|
+
errors.push({ kind: "navigation", message: String(e) });
|
|
141
|
+
return { path, url, status: 0, bodyLen: 0, errors, failedRequests, skipped: false };
|
|
142
|
+
} finally {
|
|
143
|
+
await page.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function formatFindings(results) {
|
|
147
|
+
const lines = [];
|
|
148
|
+
for (const r of results) {
|
|
149
|
+
if (r.skipped) {
|
|
150
|
+
lines.push(`
|
|
151
|
+
${r.path} (HTTP ${r.status} \u2014 skipped, not in this build)`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (r.errors.length === 0 && r.failedRequests.length === 0 && r.bodyLen > 0) continue;
|
|
155
|
+
lines.push(`
|
|
156
|
+
${r.path} (HTTP ${r.status}, body ${r.bodyLen} chars)`);
|
|
157
|
+
for (const e of r.errors) {
|
|
158
|
+
lines.push(` [${e.kind}] ${e.message}`);
|
|
159
|
+
}
|
|
160
|
+
for (const f of r.failedRequests) {
|
|
161
|
+
lines.push(` [request] ${f.status ?? f.failure} ${f.url}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return lines.join("\n");
|
|
165
|
+
}
|
|
166
|
+
async function ensureDist() {
|
|
167
|
+
if (!statSafe(DIST)?.isDirectory()) {
|
|
168
|
+
console.error(`smoke: dist/ not found at ${DIST}`);
|
|
169
|
+
console.error(` run \`npm run build\` first, or use \`npm run build:full\`.`);
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
if (!statSafe(join(DIST, "index.html"))?.isFile()) {
|
|
173
|
+
console.error(`smoke: dist/index.html missing \u2014 build did not complete.`);
|
|
174
|
+
process.exit(2);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function main() {
|
|
178
|
+
await ensureDist();
|
|
179
|
+
const { server, origin } = await serveDist(DIST);
|
|
180
|
+
let exitCode = 0;
|
|
181
|
+
const browser = await chromium.launch({ headless: true });
|
|
182
|
+
try {
|
|
183
|
+
const results = [];
|
|
184
|
+
for (const path of PAGE_PATHS) {
|
|
185
|
+
const r = await probePage(browser, origin, path);
|
|
186
|
+
results.push(r);
|
|
187
|
+
}
|
|
188
|
+
const anyFail = results.some(
|
|
189
|
+
(r) => !r.skipped && (r.errors.length > 0 || r.failedRequests.length > 0 || r.bodyLen === 0)
|
|
190
|
+
);
|
|
191
|
+
const findings = formatFindings(results);
|
|
192
|
+
if (anyFail) {
|
|
193
|
+
exitCode = 1;
|
|
194
|
+
console.error(`
|
|
195
|
+
FAIL smoke test found problems:${findings}
|
|
196
|
+
`);
|
|
197
|
+
} else {
|
|
198
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
199
|
+
console.log(`
|
|
200
|
+
OK smoke test passed (${results.length - skipped} pages, ${skipped} skipped, 0 errors).`);
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
await browser.close();
|
|
204
|
+
server.close();
|
|
205
|
+
}
|
|
206
|
+
process.exit(exitCode);
|
|
207
|
+
}
|
|
208
|
+
main().catch((e) => {
|
|
209
|
+
console.error("smoke: fatal error", e);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
});
|