@glossarist/concept-browser 0.7.117 → 0.7.118

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.117",
3
+ "version": "0.7.118",
4
4
  "description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -420,7 +420,7 @@ async function buildActivityTurtle(input) {
420
420
  }
421
421
 
422
422
  // scripts/lib/vocab-turtle.ts
423
- import { readFileSync as readFileSync2 } from "node:fs";
423
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
424
424
  import { join, dirname as dirname2 } from "node:path";
425
425
  import { fileURLToPath as fileURLToPath2 } from "node:url";
426
426
  import {
@@ -430,9 +430,21 @@ import {
430
430
  PREFIXES as PREFIXES2
431
431
  } from "glossarist/rdf";
432
432
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
433
- var VOCAB_PATH = join(__dirname2, "..", "..", "data", "glossarist-vocab.json");
434
- function loadVocab(path4 = VOCAB_PATH) {
435
- return JSON.parse(readFileSync2(path4, "utf8"));
433
+ function findVocabPath() {
434
+ const candidates = [
435
+ join(__dirname2, "..", "..", "data", "glossarist-vocab.json"),
436
+ join(__dirname2, "..", "data", "glossarist-vocab.json")
437
+ ];
438
+ for (const p of candidates) {
439
+ if (existsSync2(p)) return p;
440
+ }
441
+ throw new Error(
442
+ `glossarist-vocab.json not found. Tried:
443
+ ${candidates.map((p) => " " + p).join("\n")}`
444
+ );
445
+ }
446
+ function loadVocab() {
447
+ return JSON.parse(readFileSync2(findVocabPath(), "utf8"));
436
448
  }
437
449
  function writerPrefixes3() {
438
450
  const out = {};
@@ -1090,6 +1102,9 @@ function buildPatternIndex(datasets, registerCache2) {
1090
1102
  function buildRefMaps(config2, registerCache2) {
1091
1103
  const refPrefixMap = {};
1092
1104
  const patternIndex = buildPatternIndex(config2.datasets, registerCache2);
1105
+ for (const ds of config2.datasets) {
1106
+ if (ds.ref) refPrefixMap[ds.ref] = ds.id;
1107
+ }
1093
1108
  for (const route of config2.routing || []) {
1094
1109
  if (route.uri && route.uri.includes("iec") && route.uri.includes("60050")) {
1095
1110
  const mapped = route.targetDataset;
@@ -1316,6 +1331,13 @@ function yamlToJsonLd(conceptYaml, register, refMaps2) {
1316
1331
  if (r.ref.id) ref["gl:id"] = r.ref.id;
1317
1332
  if (r.ref.text) ref["gl:text"] = r.ref.text;
1318
1333
  rel["gl:ref"] = ref;
1334
+ if (r.ref.source && r.ref.id && refMaps2?.refPrefixMap?.[r.ref.source]) {
1335
+ const datasetId = refMaps2.refPrefixMap[r.ref.source];
1336
+ rel["gl:target"] = buildConceptUri(refMaps2.uriBase, datasetId, r.ref.id);
1337
+ }
1338
+ }
1339
+ if (r.target) {
1340
+ rel["gl:target"] = r.target;
1319
1341
  }
1320
1342
  return rel;
1321
1343
  });
@@ -221,6 +221,14 @@ function buildRefMaps(config, registerCache) {
221
221
  const refPrefixMap = {};
222
222
  const patternIndex = buildPatternIndex(config.datasets, registerCache);
223
223
 
224
+ // Auto-map each dataset's `ref` field to its dataset ID.
225
+ // This enables cross-dataset link resolution: a concept in cie-2020
226
+ // with ref: { source: "CIE S 017:2011", id: "17-245" } resolves to
227
+ // cie-2011/concept/17-245 because cie-2011 has ref: "CIE S 017:2011".
228
+ for (const ds of config.datasets) {
229
+ if (ds.ref) refPrefixMap[ds.ref] = ds.id;
230
+ }
231
+
224
232
  for (const route of config.routing || []) {
225
233
  if (route.uri && route.uri.includes('iec') && route.uri.includes('60050')) {
226
234
  const mapped = route.targetDataset;
@@ -508,6 +516,18 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
508
516
  if (r.ref.id) ref['gl:id'] = r.ref.id;
509
517
  if (r.ref.text) ref['gl:text'] = r.ref.text;
510
518
  rel['gl:ref'] = ref;
519
+
520
+ // Resolve cross-dataset refs to gl:target URIs.
521
+ // refPrefixMap maps source labels (e.g. "CIE S 017:2011") to
522
+ // dataset IDs (e.g. "cie-2011"). When the source matches a
523
+ // co-deployed dataset, we can build a navigable concept URI.
524
+ if (r.ref.source && r.ref.id && refMaps?.refPrefixMap?.[r.ref.source]) {
525
+ const datasetId = refMaps.refPrefixMap[r.ref.source];
526
+ rel['gl:target'] = buildConceptUri(refMaps.uriBase, datasetId, r.ref.id);
527
+ }
528
+ }
529
+ if (r.target) {
530
+ rel['gl:target'] = r.target;
511
531
  }
512
532
  return rel;
513
533
  });
@@ -7,7 +7,7 @@
7
7
  // to absolute IRIs, then re-rendered via glossarist-js's n3 writer with
8
8
  // the canonical prefix map ).
9
9
 
10
- import { readFileSync } from 'node:fs';
10
+ import { readFileSync, existsSync } from 'node:fs';
11
11
  import { join, dirname } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import {
@@ -18,10 +18,25 @@ import {
18
18
  } from 'glossarist/rdf';
19
19
 
20
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const VOCAB_PATH = join(__dirname, '..', '..', 'data', 'glossarist-vocab.json');
22
21
 
23
- function loadVocab(path = VOCAB_PATH) {
24
- return JSON.parse(readFileSync(path, 'utf8'));
22
+ // When running from source (tsx), this file is at scripts/lib/ and
23
+ // the data dir is two levels up. When bundled by esbuild into
24
+ // scripts/*.js, __dirname is scripts/ and the data dir is one level up.
25
+ function findVocabPath(): string {
26
+ const candidates = [
27
+ join(__dirname, '..', '..', 'data', 'glossarist-vocab.json'),
28
+ join(__dirname, '..', 'data', 'glossarist-vocab.json'),
29
+ ];
30
+ for (const p of candidates) {
31
+ if (existsSync(p)) return p;
32
+ }
33
+ throw new Error(
34
+ `glossarist-vocab.json not found. Tried:\n${candidates.map(p => ' ' + p).join('\n')}`,
35
+ );
36
+ }
37
+
38
+ function loadVocab() {
39
+ return JSON.parse(readFileSync(findVocabPath(), 'utf8'));
25
40
  }
26
41
 
27
42
  // n3's writer prefixes must omit the trailing # or / — those go in
@@ -0,0 +1,98 @@
1
+ /**
2
+ * End-to-end consumer simulation test.
3
+ *
4
+ * Simulates what a real consumer experiences: creates a temp "consumer
5
+ * project" with the compiled package in node_modules, then runs scripts
6
+ * with plain node. Catches packaging bugs that dev-mode tests miss.
7
+ */
8
+
9
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
10
+ import { spawnSync } from 'node:child_process';
11
+ import {
12
+ mkdirSync, writeFileSync, cpSync, rmSync, existsSync, readFileSync, readdirSync,
13
+ } from 'node:fs';
14
+ import { resolve, join, dirname } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const ROOT = resolve(__dirname, '..', '..');
19
+ const SCRIPTS_DIR = join(ROOT, 'scripts');
20
+ const CONSUMER_DIR = join(ROOT, '.tmp-consumer-test');
21
+
22
+ describe('end-to-end consumer simulation — compiled package works under plain node', () => {
23
+ beforeAll(() => {
24
+ const build = spawnSync('node', [join(ROOT, 'scripts', 'build-scripts.mjs')], {
25
+ encoding: 'utf8', cwd: ROOT,
26
+ });
27
+ expect(build.status).toBe(0);
28
+
29
+ rmSync(CONSUMER_DIR, { recursive: true, force: true });
30
+ const pkgDest = join(CONSUMER_DIR, 'node_modules', '@glossarist', 'concept-browser');
31
+ mkdirSync(pkgDest, { recursive: true });
32
+ cpSync(join(ROOT, 'scripts'), join(pkgDest, 'scripts'), { recursive: true });
33
+ cpSync(join(ROOT, 'data'), join(pkgDest, 'data'), { recursive: true });
34
+ cpSync(join(ROOT, 'cli'), join(pkgDest, 'cli'), { recursive: true });
35
+ writeFileSync(join(pkgDest, 'package.json'), JSON.stringify({
36
+ name: '@glossarist/concept-browser', version: '0.0.0-test', type: 'module',
37
+ bin: { 'concept-browser': './cli/index.mjs' },
38
+ }));
39
+ }, 60000);
40
+
41
+ afterAll(() => {
42
+ rmSync(CONSUMER_DIR, { recursive: true, force: true });
43
+ });
44
+
45
+ it('compiled scripts can find package data files from node_modules location', () => {
46
+ const scriptDir = join(CONSUMER_DIR, 'node_modules', '@glossarist', 'concept-browser', 'scripts');
47
+ const pathFromBundled = join(scriptDir, '..', 'data', 'glossarist-vocab.json');
48
+ expect(existsSync(pathFromBundled)).toBe(true);
49
+ });
50
+
51
+ it('compiled scripts list matches source scripts (no missing outputs)', () => {
52
+ const sourceTs = readdirSync(SCRIPTS_DIR)
53
+ .filter(f => f.endsWith('.ts') && !f.includes('__tests__') && f !== 'smoke.ts')
54
+ .map(f => f.replace(/\.ts$/, ''));
55
+ const compiledJs = readdirSync(SCRIPTS_DIR)
56
+ .filter(f => f.endsWith('.js'))
57
+ .map(f => f.replace(/\.js$/, ''));
58
+ for (const name of sourceTs) {
59
+ expect(compiledJs).toContain(name);
60
+ }
61
+ });
62
+
63
+ it('validate-yaml.js validates against the shipped schema from node_modules', () => {
64
+ const consumerPkg = join(CONSUMER_DIR, 'node_modules', '@glossarist', 'concept-browser');
65
+ const scriptPath = join(consumerPkg, 'scripts', 'validate-yaml.js');
66
+ const schemaPath = join(consumerPkg, 'data', 'concept-model', 'shapes', 'glossarist.concept.yaml.schema.json');
67
+ const code = [
68
+ 'import { validateYaml } from ' + JSON.stringify(scriptPath) + ';',
69
+ 'import { readFileSync } from "fs";',
70
+ 'const schema = JSON.parse(readFileSync(' + JSON.stringify(schemaPath) + ', "utf8"));',
71
+ 'const r = validateYaml("termid: test", schema);',
72
+ 'console.log("valid=" + r.valid);',
73
+ ].join('\n');
74
+ const result = spawnSync('node', ['--input-type=module', '-e', code], {
75
+ encoding: 'utf8', cwd: ROOT,
76
+ });
77
+ expect(result.status).toBe(0);
78
+ expect(result.stdout).toContain('valid=true');
79
+ });
80
+
81
+ it('no compiled script references a devDependency-only package', () => {
82
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
83
+ const deps = new Set(Object.keys(pkg.dependencies || {}));
84
+ const devDeps = new Set(Object.keys(pkg.devDependencies || {}));
85
+ const violations: string[] = [];
86
+ for (const file of readdirSync(SCRIPTS_DIR).filter(f => f.endsWith('.js'))) {
87
+ const src = readFileSync(join(SCRIPTS_DIR, file), 'utf8');
88
+ const importRe = new RegExp('(?:from\\s+|import\\s*\\(\\s*)["\']([^"\'./@][^"\']*)["\']', 'g');
89
+ let m;
90
+ while ((m = importRe.exec(src)) !== null) {
91
+ const name = m[1].startsWith('@') ? m[1].split('/').slice(0, 2).join('/') : m[1].split('/')[0];
92
+ if (name.startsWith('node:') || name === 'node') continue;
93
+ if (devDeps.has(name) && !deps.has(name)) violations.push(file + ' -> ' + name);
94
+ }
95
+ }
96
+ expect(violations).toEqual([]);
97
+ });
98
+ });
@@ -97,4 +97,14 @@ describe('published-package contract — compiled .js scripts load under plain n
97
97
  }
98
98
  }
99
99
  });
100
+
101
+ it('compiled scripts can find package data files (glossarist-vocab.json path regression)', () => {
102
+ // When esbuild bundles scripts/lib/vocab-turtle.ts into scripts/generate-data.js,
103
+ // __dirname changes from scripts/lib/ to scripts/. The path to data/ must
104
+ // resolve correctly in both modes. This test verifies the fallback logic.
105
+ const dataDir = join(ROOT, 'data');
106
+ expect(existsSync(join(dataDir, 'glossarist-vocab.json'))).toBe(true);
107
+ expect(existsSync(join(dataDir, 'colors.json'))).toBe(true);
108
+ expect(existsSync(join(dataDir, 'concept-model', 'shapes', 'glossarist.shacl.ttl'))).toBe(true);
109
+ });
100
110
  });