@navecss/tokens 0.1.0 → 0.2.0
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/README.md +1 -1
- package/dist/core-contract.json +1 -1
- package/dist/lib/bin.js +35 -6
- package/dist/lib/core-version.d.ts +5 -1
- package/dist/lib/core-version.js +7 -1
- package/dist/lib/facade.d.ts +5 -0
- package/dist/lib/facade.js +17 -11
- package/dist/lib/formats.js +27 -6
- package/dist/lib/out-path-conflict.d.ts +3 -0
- package/dist/lib/out-path-conflict.js +7 -0
- package/dist/lib/read-manifest.d.ts +2 -0
- package/dist/lib/read-manifest.js +17 -0
- package/dist/lib/reader.d.ts +6 -0
- package/dist/lib/reader.js +41 -41
- package/dist/lib/reserved-keys.d.ts +1 -0
- package/dist/lib/reserved-keys.js +18 -0
- package/dist/lib/resolve-value.d.ts +8 -0
- package/dist/lib/resolve-value.js +56 -0
- package/dist/lib/theming/comment-open-tracker.js +230 -9
- package/dist/lib/theming/core-contract.js +127 -4
- package/dist/lib/theming/guard-message-probes.js +18 -1
- package/dist/lib/theming/neutral.js +1 -1
- package/dist/lib/theming/seed-channel-split.js +8 -3
- package/dist/lib/validate-report.d.ts +1 -0
- package/dist/lib/validate-report.js +7 -3
- package/dist/lib/validate.js +17 -3
- package/dist/palette-record.json +52 -52
- package/dist/tokens.css +33 -33
- package/dist/tokens.d.ts +46 -2
- package/dist/tokens.js +5 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Every custom property the stylesheet declares is listed in [TOKENS.md](./TOKENS.
|
|
|
46
46
|
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
47
|
| `@navecss/tokens` | JS entry point — the typed constants described under `@navecss/tokens/js` below. Not a CSS entry: `@import url('@navecss/tokens/css')` is the stylesheet |
|
|
48
48
|
| `@navecss/tokens/css` | The generated stylesheet — `@import url('@navecss/tokens/css')` |
|
|
49
|
-
| `@navecss/tokens/js` | Typed ESM token constants — the DTCG 2025.10 token set; every `--nave-color-*` custom property ships in the stylesheet above and not through this export,
|
|
49
|
+
| `@navecss/tokens/js` | Typed ESM token constants — the DTCG 2025.10 token set; every `--nave-color-*` custom property (the `--nave-color-tint` seed included) ships as a value in the stylesheet above and not through this export, but its NAME is typed here as `ColorPropertyName`, exported alongside `TokenName`/`TokenValue` and disjoint from them |
|
|
50
50
|
| `@navecss/tokens/breakpoints` | Breakpoint values, em conversions, media strings |
|
|
51
51
|
| `@navecss/tokens/core-contract` | The shipped core-contract manifest, as JSON, for a token layer you generate yourself and load through `@navecss/core/no-tokens`: the `--nave-*` custom-property names `@navecss/core` reads, generated from that package's own source rather than listed by hand, and not the colour namespace alone. Import it with `with { type: 'json' }`. A clean check against it means those names are present rather than that your token set is complete; for what else it does and does not establish, read the header comment in `@navecss/core/no-tokens`. A validator reading the manifest directly should refuse a `formatVersion` higher than it understands, rather than mis-reading it. |
|
|
52
52
|
| `@navecss/tokens/build` | The token build as a JavaScript API: `build` and `validate`, the two operations the `navecss-tokens` binary wraps |
|
package/dist/core-contract.json
CHANGED
package/dist/lib/bin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import { build, DuplicateTokenNameRefusal, MissingContractTokensError, SeedIngestRefusal, TokenCollisionRefusal, UsageError, validate, } from "./facade.js";
|
|
3
|
+
import { build, DuplicateTokenNameRefusal, formatVersionSkewFact, MissingContractTokensError, SeedIngestRefusal, TokenCollisionRefusal, UsageError, validate, } from "./facade.js";
|
|
4
4
|
function normalizationQualifier(seedNormalization) {
|
|
5
5
|
switch (seedNormalization) {
|
|
6
6
|
case 'chroma-reduced': {
|
|
@@ -20,11 +20,21 @@ function normalizationQualifier(seedNormalization) {
|
|
|
20
20
|
const USAGE = 'Usage:\n' +
|
|
21
21
|
' navecss-tokens build --seed=<colour> --out=<dir> [--source=<file>] [--overrides=<file>]\n' +
|
|
22
22
|
' navecss-tokens validate --source=<file>';
|
|
23
|
+
function assertNoDuplicateFlags(tokens) {
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const token of tokens) {
|
|
26
|
+
if (token.kind !== 'option' || token.name === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
if (seen.has(token.name)) {
|
|
29
|
+
throw new UsageError(`--${token.name} was given more than once; each flag accepts exactly one value.\n${USAGE}`);
|
|
30
|
+
}
|
|
31
|
+
seen.add(token.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
23
34
|
function parseBuildArgs(args) {
|
|
24
35
|
let values;
|
|
25
36
|
try {
|
|
26
|
-
|
|
27
|
-
({ values } = parseArgs({
|
|
37
|
+
const parsed = parseArgs({
|
|
28
38
|
args,
|
|
29
39
|
options: {
|
|
30
40
|
out: { type: 'string' },
|
|
@@ -33,9 +43,14 @@ function parseBuildArgs(args) {
|
|
|
33
43
|
source: { type: 'string' },
|
|
34
44
|
},
|
|
35
45
|
strict: true,
|
|
36
|
-
|
|
46
|
+
tokens: true,
|
|
47
|
+
});
|
|
48
|
+
assertNoDuplicateFlags(parsed.tokens);
|
|
49
|
+
values = parsed.values;
|
|
37
50
|
}
|
|
38
51
|
catch (error) {
|
|
52
|
+
if (error instanceof UsageError)
|
|
53
|
+
throw error;
|
|
39
54
|
throw new UsageError(error.message);
|
|
40
55
|
}
|
|
41
56
|
if (!values.seed)
|
|
@@ -52,10 +67,18 @@ function parseBuildArgs(args) {
|
|
|
52
67
|
function parseValidateArgs(args) {
|
|
53
68
|
let values;
|
|
54
69
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
const parsed = parseArgs({
|
|
71
|
+
args,
|
|
72
|
+
options: { source: { type: 'string' } },
|
|
73
|
+
strict: true,
|
|
74
|
+
tokens: true,
|
|
75
|
+
});
|
|
76
|
+
assertNoDuplicateFlags(parsed.tokens);
|
|
77
|
+
values = parsed.values;
|
|
57
78
|
}
|
|
58
79
|
catch (error) {
|
|
80
|
+
if (error instanceof UsageError)
|
|
81
|
+
throw error;
|
|
59
82
|
throw new UsageError(error.message);
|
|
60
83
|
}
|
|
61
84
|
if (!values.source)
|
|
@@ -78,6 +101,12 @@ async function main() {
|
|
|
78
101
|
}
|
|
79
102
|
const result = await build(parseBuildArgs(rest));
|
|
80
103
|
console.log(`Built from seed ${result.resolvedSeed}${normalizationQualifier(result.seedNormalization)}: ${result.files.join(', ')}`);
|
|
104
|
+
if (result.versionSkew) {
|
|
105
|
+
const { producerName, ...skew } = result.versionSkew;
|
|
106
|
+
console.error(`Version skew: ${formatVersionSkewFact(producerName, skew)}. The generated file may ` +
|
|
107
|
+
`not carry every custom-property name the installed ${producerName} reads. Reinstall ` +
|
|
108
|
+
`matching versions, then build again.`);
|
|
109
|
+
}
|
|
81
110
|
return 0;
|
|
82
111
|
}
|
|
83
112
|
if (subcommand === 'validate') {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CoreContractManifest, ManifestVersionSkew } from './theming/core-contract.ts';
|
|
1
2
|
export type CoreVersionProbe = {
|
|
2
3
|
status: 'resolved';
|
|
3
4
|
version: string;
|
|
@@ -9,4 +10,7 @@ export type CoreVersionProbe = {
|
|
|
9
10
|
message: string;
|
|
10
11
|
status: 'unreadable';
|
|
11
12
|
};
|
|
12
|
-
export declare function
|
|
13
|
+
export declare function detectVersionSkew(manifest: CoreContractManifest): Promise<{
|
|
14
|
+
probe: CoreVersionProbe;
|
|
15
|
+
skew: ManifestVersionSkew | undefined;
|
|
16
|
+
}>;
|
package/dist/lib/core-version.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { checkManifestVersionSkew } from "./theming/core-contract.js";
|
|
3
4
|
function canResolveCoreDefaultEntry() {
|
|
4
5
|
try {
|
|
5
6
|
import.meta.resolve('@navecss/core');
|
|
@@ -9,7 +10,7 @@ function canResolveCoreDefaultEntry() {
|
|
|
9
10
|
return false;
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
+
async function resolveInstalledCoreVersion() {
|
|
13
14
|
let resolved;
|
|
14
15
|
try {
|
|
15
16
|
resolved = import.meta.resolve('@navecss/core/package.json');
|
|
@@ -38,3 +39,8 @@ export async function resolveInstalledCoreVersion() {
|
|
|
38
39
|
return { message: error.message, status: 'unreadable' };
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
export async function detectVersionSkew(manifest) {
|
|
43
|
+
const probe = await resolveInstalledCoreVersion();
|
|
44
|
+
const skew = probe.status === 'resolved' ? checkManifestVersionSkew(manifest, probe.version) : undefined;
|
|
45
|
+
return { probe, skew };
|
|
46
|
+
}
|
package/dist/lib/facade.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { SeedNormalization } from './theming/color-math.ts';
|
|
2
|
+
import type { ManifestVersionSkew } from './theming/core-contract.ts';
|
|
2
3
|
export { DuplicateTokenNameRefusal, MissingContractTokensError, TokenCollisionRefusal, UsageError, } from './errors.ts';
|
|
3
4
|
export type { SeedNormalization } from './theming/color-math.ts';
|
|
4
5
|
export type { PerStepOverrides } from './theming/pipeline.ts';
|
|
5
6
|
export { SeedIngestRefusal } from './theming/seed-ingest.ts';
|
|
7
|
+
export { formatVersionSkewFact } from './validate-report.ts';
|
|
6
8
|
export interface TokensBuildOptions {
|
|
7
9
|
seed: string;
|
|
8
10
|
source?: string;
|
|
@@ -14,6 +16,9 @@ export interface TokensBuildResult {
|
|
|
14
16
|
seedNormalization: SeedNormalization;
|
|
15
17
|
files: string[];
|
|
16
18
|
collidingNames: string[];
|
|
19
|
+
versionSkew?: (ManifestVersionSkew & {
|
|
20
|
+
producerName: string;
|
|
21
|
+
}) | undefined;
|
|
17
22
|
}
|
|
18
23
|
export declare function build(options: TokensBuildOptions): Promise<TokensBuildResult>;
|
|
19
24
|
export interface TokensValidateOptions {
|
package/dist/lib/facade.js
CHANGED
|
@@ -3,14 +3,16 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { writeOutputs } from "./builder.js";
|
|
5
5
|
import { detectCollidingNames, refuseOnCollision } from "./collision.js";
|
|
6
|
-
import {
|
|
6
|
+
import { detectVersionSkew } from "./core-version.js";
|
|
7
7
|
import { composeDtcgOutputs } from "./dtcg-outputs.js";
|
|
8
8
|
import { MissingContractTokensError, UsageError } from "./errors.js";
|
|
9
|
+
import { isOutPathConflict } from "./out-path-conflict.js";
|
|
9
10
|
import { readOverrides } from "./overrides.js";
|
|
10
11
|
import { findPackageRoot } from "./package-root.js";
|
|
12
|
+
import { readManifest } from "./read-manifest.js";
|
|
11
13
|
import { formatOklch } from "./theming/color-math.js";
|
|
12
14
|
import { composeConsumerBuild } from "./theming/consumer-build.js";
|
|
13
|
-
import {
|
|
15
|
+
import { validateAgainstManifest } from "./theming/core-contract.js";
|
|
14
16
|
import { shippedThemingPropertyNames } from "./theming/emit.js";
|
|
15
17
|
import { parseSeed } from "./theming/seed-ingest.js";
|
|
16
18
|
import { SHIPPED_SEEDS } from "./theming/shipped-seeds.js";
|
|
@@ -18,11 +20,8 @@ import { checkManifestFormatSupport, formatValidateReport } from "./validate-rep
|
|
|
18
20
|
import { computeMissing, detectSourceKind, namesFromSource, splitMissingBySupply, } from "./validate.js";
|
|
19
21
|
export { DuplicateTokenNameRefusal, MissingContractTokensError, TokenCollisionRefusal, UsageError, } from "./errors.js";
|
|
20
22
|
export { SeedIngestRefusal } from "./theming/seed-ingest.js";
|
|
23
|
+
export { formatVersionSkewFact } from "./validate-report.js";
|
|
21
24
|
const PACKAGE_ROOT = findPackageRoot(import.meta.url);
|
|
22
|
-
async function readManifest() {
|
|
23
|
-
const raw = await readFile(path.join(PACKAGE_ROOT, 'dist', 'core-contract.json'), 'utf8');
|
|
24
|
-
return JSON.parse(raw);
|
|
25
|
-
}
|
|
26
25
|
export async function build(options) {
|
|
27
26
|
const sourcePath = options.source ?? path.join(PACKAGE_ROOT, 'tokens.json');
|
|
28
27
|
if (!existsSync(sourcePath)) {
|
|
@@ -37,6 +36,7 @@ export async function build(options) {
|
|
|
37
36
|
const missing = validateAgainstManifest(manifest, emittedNames);
|
|
38
37
|
if (missing.length > 0)
|
|
39
38
|
throw new MissingContractTokensError(missing);
|
|
39
|
+
const { skew } = await detectVersionSkew(manifest);
|
|
40
40
|
const theming = composeConsumerBuild({
|
|
41
41
|
seedInputs: { primary },
|
|
42
42
|
seeds: {
|
|
@@ -55,7 +55,15 @@ export async function build(options) {
|
|
|
55
55
|
? { ...file, content: `${file.content}\n${theming.css}` }
|
|
56
56
|
: file);
|
|
57
57
|
const allFiles = [...merged, ...theming.files];
|
|
58
|
-
|
|
58
|
+
try {
|
|
59
|
+
await writeOutputs(allFiles);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (isOutPathConflict(error)) {
|
|
63
|
+
throw new UsageError(`could not write to --out="${options.outDir}": ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
59
67
|
return {
|
|
60
68
|
resolvedSeed: formatOklch(theming.primaryRecord.usedSeed),
|
|
61
69
|
seedNormalization: theming.primaryRecord.normalization,
|
|
@@ -63,6 +71,7 @@ export async function build(options) {
|
|
|
63
71
|
.map((file) => path.relative(options.outDir, file.destination))
|
|
64
72
|
.toSorted((a, b) => a.localeCompare(b)),
|
|
65
73
|
collidingNames,
|
|
74
|
+
versionSkew: skew === undefined ? undefined : { ...skew, producerName: manifest.producer.name },
|
|
66
75
|
};
|
|
67
76
|
}
|
|
68
77
|
export async function validate(options) {
|
|
@@ -77,10 +86,7 @@ export async function validate(options) {
|
|
|
77
86
|
const content = await readFile(options.source, 'utf8');
|
|
78
87
|
const notDeclared = computeMissing(manifest, kind, content, { path: options.source });
|
|
79
88
|
const { missing, supplied } = splitMissingBySupply(kind, notDeclared);
|
|
80
|
-
const coreProbe = await
|
|
81
|
-
const versionSkew = coreProbe.status === 'resolved'
|
|
82
|
-
? checkManifestVersionSkew(manifest, coreProbe.version)
|
|
83
|
-
: undefined;
|
|
89
|
+
const { probe: coreProbe, skew: versionSkew } = await detectVersionSkew(manifest);
|
|
84
90
|
const formatRefusal = checkManifestFormatSupport(manifest);
|
|
85
91
|
const output = formatValidateReport(manifest, missing, versionSkew, { ...coreProbe, supplied });
|
|
86
92
|
let exitCode;
|
package/dist/lib/formats.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shippedThemingPropertyNames } from "./theming/emit.js";
|
|
1
2
|
const HEADER = '/* Nave Design System — generated, do not edit */';
|
|
2
3
|
const LAYER_ORDER_STATEMENT = '@layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;';
|
|
3
4
|
const PREFIX = '--nave-';
|
|
@@ -15,8 +16,10 @@ const isPublic = (token) => !token.path.includes('_primitive') && token.path[0]
|
|
|
15
16
|
const isReducibleDuration = (token) => token.type === 'duration' &&
|
|
16
17
|
token.path[0] === 'motion' &&
|
|
17
18
|
token.name !== 'motion-duration-instant';
|
|
19
|
+
const isZIndexShaped = (token) => token.type === 'number' && token.path[0] === 'layer';
|
|
20
|
+
const atPropertySyntaxFor = (token) => isZIndexShaped(token) ? '<integer>' : AT_PROPERTY_SYNTAX[token.type];
|
|
18
21
|
function isRegistrable(token) {
|
|
19
|
-
const syntax =
|
|
22
|
+
const syntax = atPropertySyntaxFor(token);
|
|
20
23
|
if (syntax === undefined)
|
|
21
24
|
return false;
|
|
22
25
|
if (syntax !== '<length>')
|
|
@@ -26,7 +29,7 @@ function isRegistrable(token) {
|
|
|
26
29
|
function buildAtProperty(token) {
|
|
27
30
|
return [
|
|
28
31
|
` @property ${PREFIX}${token.name} {`,
|
|
29
|
-
` syntax: '${
|
|
32
|
+
` syntax: '${atPropertySyntaxFor(token)}';`,
|
|
30
33
|
` inherits: true;`,
|
|
31
34
|
` initial-value: ${token.value};`,
|
|
32
35
|
` }`,
|
|
@@ -93,8 +96,11 @@ export function formatJsTokens(tokens) {
|
|
|
93
96
|
'',
|
|
94
97
|
'/** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).',
|
|
95
98
|
' * Keys are CSS custom property names. The semantic colour layer',
|
|
96
|
-
' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`',
|
|
97
|
-
' *
|
|
99
|
+
' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css` and is',
|
|
100
|
+
' * not part of this object; its property NAMES are typed separately as',
|
|
101
|
+
' * `ColorPropertyName`, and its VALUES are reachable only through CSS (e.g.',
|
|
102
|
+
' * `var(--nave-color-action-primary)`): the browser resolves each one for the',
|
|
103
|
+
' * active colour scheme, and the neutral-derived ones also follow the tint.',
|
|
98
104
|
' */',
|
|
99
105
|
'export const tokens = {',
|
|
100
106
|
entries,
|
|
@@ -107,13 +113,20 @@ export function formatTsDeclarations(tokens) {
|
|
|
107
113
|
.filter((t) => isPublic(t))
|
|
108
114
|
.map((t) => ` '${PREFIX}${t.name}': ${typeof t.value === 'number' ? 'number' : 'string'}`)
|
|
109
115
|
.join('\n');
|
|
116
|
+
const colorPropertyNameMembers = [...shippedThemingPropertyNames()]
|
|
117
|
+
.toSorted((a, b) => a.localeCompare(b))
|
|
118
|
+
.map((name) => ` | '${name}'`)
|
|
119
|
+
.join('\n');
|
|
110
120
|
return [
|
|
111
121
|
HEADER,
|
|
112
122
|
'',
|
|
113
123
|
'/** The DTCG 2025.10 token set as a typed const object (e.g. `--nave-font-size-md`).',
|
|
114
124
|
' * Keys are CSS custom property names. The semantic colour layer',
|
|
115
|
-
' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css`',
|
|
116
|
-
' *
|
|
125
|
+
' * (`--nave-color-*`) ships as CSS custom properties in `dist/tokens.css` and is',
|
|
126
|
+
' * not part of this object; its property NAMES are typed separately as',
|
|
127
|
+
' * `ColorPropertyName`, and its VALUES are reachable only through CSS (e.g.',
|
|
128
|
+
' * `var(--nave-color-action-primary)`): the browser resolves each one for the',
|
|
129
|
+
' * active colour scheme, and the neutral-derived ones also follow the tint.',
|
|
117
130
|
' */',
|
|
118
131
|
'export declare const tokens: {',
|
|
119
132
|
keys,
|
|
@@ -122,6 +135,14 @@ export function formatTsDeclarations(tokens) {
|
|
|
122
135
|
'export declare type TokenName = keyof typeof tokens',
|
|
123
136
|
'export declare type TokenValue = typeof tokens[TokenName]',
|
|
124
137
|
'',
|
|
138
|
+
'/** Every `--nave-color-*` custom property the semantic colour layer emits: names',
|
|
139
|
+
' * only, no value (see the doc comment above `tokens` for where the values live).',
|
|
140
|
+
' * Disjoint from `TokenName` — a consumer who wants both writes',
|
|
141
|
+
' * `TokenName | ColorPropertyName`.',
|
|
142
|
+
' */',
|
|
143
|
+
'export declare type ColorPropertyName =',
|
|
144
|
+
colorPropertyNameMembers,
|
|
145
|
+
'',
|
|
125
146
|
].join('\n');
|
|
126
147
|
}
|
|
127
148
|
function buildBreakpointMap(tokens) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { UsageError } from "./errors.js";
|
|
4
|
+
import { findPackageRoot } from "./package-root.js";
|
|
5
|
+
const PACKAGE_ROOT = findPackageRoot(import.meta.url);
|
|
6
|
+
export async function readManifest() {
|
|
7
|
+
let raw;
|
|
8
|
+
try {
|
|
9
|
+
raw = await readFile(path.join(PACKAGE_ROOT, 'dist', 'core-contract.json'), 'utf8');
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
throw new UsageError(`could not read this package's own dist/core-contract.json: ${error.message}. ` +
|
|
13
|
+
'This is a broken install of @navecss/tokens, not something your build/validate ' +
|
|
14
|
+
'arguments can fix — reinstall the package.');
|
|
15
|
+
}
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
package/dist/lib/reader.d.ts
CHANGED
|
@@ -5,4 +5,10 @@ export interface FlatToken {
|
|
|
5
5
|
readonly type: string;
|
|
6
6
|
readonly value: string | number;
|
|
7
7
|
}
|
|
8
|
+
export interface RawToken {
|
|
9
|
+
path: string[];
|
|
10
|
+
ref?: string;
|
|
11
|
+
type: string;
|
|
12
|
+
rawValue: unknown;
|
|
13
|
+
}
|
|
8
14
|
export declare function readTokens(source: unknown, sourceName?: string): FlatToken[];
|
package/dist/lib/reader.js
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
import { renderTokenValue } from "./composite-value.js";
|
|
2
1
|
import { classifyMalformed, formatUnreadableNodesRefusal } from "./dtcg-malformed.js";
|
|
3
2
|
import { classifyDraftShape, refuseTypeReason } from "./dtcg-shape.js";
|
|
4
3
|
import { DtcgShapeRefusal } from "./errors.js";
|
|
5
|
-
import { refuseBothReferenceForms
|
|
4
|
+
import { refuseBothReferenceForms } from "./json-pointer.js";
|
|
5
|
+
import { refuseUnsupportedReservedKey } from "./reserved-keys.js";
|
|
6
|
+
import { resolveValue } from "./resolve-value.js";
|
|
6
7
|
import { nameFromPath } from "./token-name.js";
|
|
7
8
|
export { kebabName } from "./token-name.js";
|
|
8
|
-
const
|
|
9
|
+
const RESERVED_METADATA_KEYS = new Set([
|
|
10
|
+
'$deprecated',
|
|
11
|
+
'$description',
|
|
12
|
+
'$extensions',
|
|
13
|
+
'$ref',
|
|
14
|
+
'$type',
|
|
15
|
+
'$value',
|
|
16
|
+
]);
|
|
9
17
|
function isPlainObject(value) {
|
|
10
18
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
19
|
}
|
|
20
|
+
function assertRecognisedDollarKey(key, at) {
|
|
21
|
+
if (RESERVED_METADATA_KEYS.has(key))
|
|
22
|
+
return;
|
|
23
|
+
refuseUnsupportedReservedKey(key, at);
|
|
24
|
+
throw new TypeError(`DTCG 2025.10 reader: "${at}" starts with "$", which DTCG 2025.10 ` +
|
|
25
|
+
'reserves for metadata — it is not a valid token or group name');
|
|
26
|
+
}
|
|
27
|
+
function assertTokenDollarKeys(node, path) {
|
|
28
|
+
for (const key of Object.keys(node)) {
|
|
29
|
+
if (key.startsWith('$'))
|
|
30
|
+
assertRecognisedDollarKey(key, [...path, key].join('.'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
12
33
|
function readTokenNode(node, path, inheritedType) {
|
|
13
34
|
const hasValue = '$value' in node;
|
|
14
35
|
const hasRef = typeof node.$ref === 'string';
|
|
@@ -17,6 +38,13 @@ function readTokenNode(node, path, inheritedType) {
|
|
|
17
38
|
const at = path.join('.');
|
|
18
39
|
if (hasValue && hasRef)
|
|
19
40
|
refuseBothReferenceForms(at);
|
|
41
|
+
assertTokenDollarKeys(node, path);
|
|
42
|
+
const childKeys = Object.keys(node).filter((key) => !key.startsWith('$'));
|
|
43
|
+
if (childKeys.length > 0) {
|
|
44
|
+
throw new TypeError(`DTCG 2025.10 reader: "${at}" carries both a $value and child token(s) (${childKeys.join(', ')}) ` +
|
|
45
|
+
'— a node is either a token (a $value/$ref leaf) or a group (children only), never both; ' +
|
|
46
|
+
'give the children their own group, separate from this token');
|
|
47
|
+
}
|
|
20
48
|
const type = typeof node.$type === 'string' ? node.$type : inheritedType;
|
|
21
49
|
if (type === undefined) {
|
|
22
50
|
throw new TypeError(`DTCG 2025.10 reader: no resolvable $type for token "${at}"`);
|
|
@@ -41,8 +69,16 @@ function collectRawTokens(node, path, inheritedType, out) {
|
|
|
41
69
|
}
|
|
42
70
|
const groupType = typeof node.$type === 'string' ? node.$type : inheritedType;
|
|
43
71
|
for (const [key, child] of Object.entries(node)) {
|
|
44
|
-
if (key.startsWith('$'))
|
|
72
|
+
if (key.startsWith('$')) {
|
|
73
|
+
assertRecognisedDollarKey(key, [...path, key].join('.'));
|
|
45
74
|
continue;
|
|
75
|
+
}
|
|
76
|
+
if (key.includes('.')) {
|
|
77
|
+
throw new TypeError(`DTCG 2025.10 reader: "${[...path, key].join('.')}" contains a literal "." in "${key}" ` +
|
|
78
|
+
"— dotted keys are ambiguous against this reader's own dotted alias paths " +
|
|
79
|
+
'(a group "a" -> "b" -> "c" and a group "a" with a key "b.c" both name the path ' +
|
|
80
|
+
'"a.b.c"), and the format has no other separator to disambiguate them');
|
|
81
|
+
}
|
|
46
82
|
collectRawTokens(child, [...path, key], groupType, out);
|
|
47
83
|
}
|
|
48
84
|
}
|
|
@@ -81,43 +117,6 @@ function refuseUnreadableNodes(raw, sourceName) {
|
|
|
81
117
|
throw new DtcgShapeRefusal(formatUnreadableNodesRefusal(drafts, malformed, sourceName), drafts);
|
|
82
118
|
}
|
|
83
119
|
}
|
|
84
|
-
function resolveReference(key, targetPath, context) {
|
|
85
|
-
if (context.visiting.has(key)) {
|
|
86
|
-
throw new TypeError(`DTCG 2025.10 reader: reference cycle detected at "${key}" -> "${targetPath}"`);
|
|
87
|
-
}
|
|
88
|
-
const target = context.byPath.get(targetPath);
|
|
89
|
-
if (!target) {
|
|
90
|
-
throw new TypeError(`DTCG 2025.10 reader: "${key}" references unresolved target "${targetPath}"`);
|
|
91
|
-
}
|
|
92
|
-
context.visiting.add(key);
|
|
93
|
-
const value = resolveValue(target, context);
|
|
94
|
-
context.visiting.delete(key);
|
|
95
|
-
return value;
|
|
96
|
-
}
|
|
97
|
-
function resolveValue(token, context) {
|
|
98
|
-
const key = token.path.join('.');
|
|
99
|
-
const cached = context.resolved.get(key);
|
|
100
|
-
if (cached !== undefined)
|
|
101
|
-
return cached;
|
|
102
|
-
let value;
|
|
103
|
-
if (token.ref !== undefined) {
|
|
104
|
-
const targetPath = resolveTokenPointer(token.ref, key, (path) => context.byPath.has(path));
|
|
105
|
-
value = resolveReference(key, targetPath, context);
|
|
106
|
-
}
|
|
107
|
-
else if (typeof token.rawValue === 'string' && REF_RE.test(token.rawValue)) {
|
|
108
|
-
value = resolveReference(key, REF_RE.exec(token.rawValue)[1], context);
|
|
109
|
-
}
|
|
110
|
-
else if (typeof token.rawValue === 'string' &&
|
|
111
|
-
(token.rawValue.includes('{') || token.rawValue.includes('}'))) {
|
|
112
|
-
throw new TypeError(`DTCG 2025.10 reader: "${key}" embeds a reference inside a larger string ("${token.rawValue}") — ` +
|
|
113
|
-
'only a whole-value alias (curly braces around one dotted path, nothing else) is supported');
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
value = renderTokenValue(token.type, token.rawValue, key);
|
|
117
|
-
}
|
|
118
|
-
context.resolved.set(key, value);
|
|
119
|
-
return value;
|
|
120
|
-
}
|
|
121
120
|
export function readTokens(source, sourceName) {
|
|
122
121
|
const raw = [];
|
|
123
122
|
collectRawTokens(source, [], undefined, raw);
|
|
@@ -126,6 +125,7 @@ export function readTokens(source, sourceName) {
|
|
|
126
125
|
byPath: new Map(raw.map((t) => [t.path.join('.'), t])),
|
|
127
126
|
resolved: new Map(),
|
|
128
127
|
visiting: new Set(),
|
|
128
|
+
depth: 0,
|
|
129
129
|
};
|
|
130
130
|
return raw.map((token) => ({
|
|
131
131
|
path: token.path,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function refuseUnsupportedReservedKey(key: string, at: string): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const UNSUPPORTED_RESERVED_KEYS = [
|
|
2
|
+
{
|
|
3
|
+
key: '$root',
|
|
4
|
+
reason: 'DTCG 2025.10 §6.2 "Root Tokens in Groups". Move its $value to a sibling token with an ' +
|
|
5
|
+
'ordinary name (for example "base") and alias that instead: {group.base}.',
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
key: '$extends',
|
|
9
|
+
reason: 'DTCG 2025.10 §6.4 "Extending Groups". Copy the extended group\'s tokens into this group, ' +
|
|
10
|
+
'or alias them individually with {group.token}.',
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
export function refuseUnsupportedReservedKey(key, at) {
|
|
14
|
+
const found = UNSUPPORTED_RESERVED_KEYS.find((entry) => entry.key === key);
|
|
15
|
+
if (found === undefined)
|
|
16
|
+
return;
|
|
17
|
+
throw new TypeError(`DTCG 2025.10 reader: "${at}" uses "${key}", which this reader does not support yet (${found.reason})`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RawToken } from './reader.ts';
|
|
2
|
+
export interface ResolveContext {
|
|
3
|
+
byPath: ReadonlyMap<string, RawToken>;
|
|
4
|
+
resolved: Map<string, string | number>;
|
|
5
|
+
visiting: Set<string>;
|
|
6
|
+
depth: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveValue(token: RawToken, context: ResolveContext): string | number;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { renderTokenValue } from "./composite-value.js";
|
|
2
|
+
import { resolveTokenPointer } from "./json-pointer.js";
|
|
3
|
+
const REF_RE = /^\{([^{}]+)\}$/;
|
|
4
|
+
const MAX_ALIAS_CHAIN_DEPTH = 1000;
|
|
5
|
+
function resolveReference(key, targetPath, context) {
|
|
6
|
+
if (context.visiting.has(key)) {
|
|
7
|
+
throw new TypeError(`DTCG 2025.10 reader: reference cycle detected at "${key}" -> "${targetPath}"`);
|
|
8
|
+
}
|
|
9
|
+
const target = context.byPath.get(targetPath);
|
|
10
|
+
if (!target) {
|
|
11
|
+
throw new TypeError(`DTCG 2025.10 reader: "${key}" references unresolved target "${targetPath}"`);
|
|
12
|
+
}
|
|
13
|
+
if (context.depth >= MAX_ALIAS_CHAIN_DEPTH) {
|
|
14
|
+
throw new TypeError(`DTCG 2025.10 reader: "${key}" -> "${targetPath}" exceeds the maximum alias chain depth ` +
|
|
15
|
+
`(${MAX_ALIAS_CHAIN_DEPTH}); this reads as a generated or malformed source rather than an ` +
|
|
16
|
+
'authored one');
|
|
17
|
+
}
|
|
18
|
+
context.visiting.add(key);
|
|
19
|
+
context.depth += 1;
|
|
20
|
+
const value = resolveValue(target, context);
|
|
21
|
+
context.depth -= 1;
|
|
22
|
+
context.visiting.delete(key);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function refuseUnsafeStringValue(key, rawValue) {
|
|
26
|
+
if (rawValue.includes(';') || rawValue.includes('/*')) {
|
|
27
|
+
throw new TypeError(`DTCG 2025.10 reader: "${key}" carries a ";" or a comment opener ("/*") in its value ` +
|
|
28
|
+
`("${rawValue}") — a raw string value is spliced directly into generated CSS, and ` +
|
|
29
|
+
'either would splice a second declaration in beside it');
|
|
30
|
+
}
|
|
31
|
+
if (rawValue.includes('{') || rawValue.includes('}')) {
|
|
32
|
+
throw new TypeError(`DTCG 2025.10 reader: "${key}" embeds a reference inside a larger string ("${rawValue}") — ` +
|
|
33
|
+
'only a whole-value alias (curly braces around one dotted path, nothing else) is supported');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function resolveValue(token, context) {
|
|
37
|
+
const key = token.path.join('.');
|
|
38
|
+
const cached = context.resolved.get(key);
|
|
39
|
+
if (cached !== undefined)
|
|
40
|
+
return cached;
|
|
41
|
+
let value;
|
|
42
|
+
if (token.ref !== undefined) {
|
|
43
|
+
const targetPath = resolveTokenPointer(token.ref, key, (path) => context.byPath.has(path));
|
|
44
|
+
value = resolveReference(key, targetPath, context);
|
|
45
|
+
}
|
|
46
|
+
else if (typeof token.rawValue === 'string' && REF_RE.test(token.rawValue)) {
|
|
47
|
+
value = resolveReference(key, REF_RE.exec(token.rawValue)[1], context);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (typeof token.rawValue === 'string')
|
|
51
|
+
refuseUnsafeStringValue(key, token.rawValue);
|
|
52
|
+
value = renderTokenValue(token.type, token.rawValue, key);
|
|
53
|
+
}
|
|
54
|
+
context.resolved.set(key, value);
|
|
55
|
+
return value;
|
|
56
|
+
}
|