@reforma/project-tokens 0.0.1

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.
Files changed (74) hide show
  1. package/README.md +174 -0
  2. package/dist/authoring.d.ts +42 -0
  3. package/dist/authoring.d.ts.map +1 -0
  4. package/dist/authoring.js +161 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +142 -0
  8. package/dist/color-value.d.ts +13 -0
  9. package/dist/color-value.d.ts.map +1 -0
  10. package/dist/color-value.js +71 -0
  11. package/dist/compilation/compiler.d.ts +61 -0
  12. package/dist/compilation/compiler.d.ts.map +1 -0
  13. package/dist/compilation/compiler.js +254 -0
  14. package/dist/compilation/definitions.d.ts +17 -0
  15. package/dist/compilation/definitions.d.ts.map +1 -0
  16. package/dist/compilation/definitions.js +103 -0
  17. package/dist/compilation/scope-validation.d.ts +12 -0
  18. package/dist/compilation/scope-validation.d.ts.map +1 -0
  19. package/dist/compilation/scope-validation.js +105 -0
  20. package/dist/compilation/validation.d.ts +4 -0
  21. package/dist/compilation/validation.d.ts.map +1 -0
  22. package/dist/compilation/validation.js +179 -0
  23. package/dist/compilation/web-projection.d.ts +31 -0
  24. package/dist/compilation/web-projection.d.ts.map +1 -0
  25. package/dist/compilation/web-projection.js +136 -0
  26. package/dist/generation.d.ts +6 -0
  27. package/dist/generation.d.ts.map +1 -0
  28. package/dist/generation.js +186 -0
  29. package/dist/json-pointer.d.ts +5 -0
  30. package/dist/json-pointer.d.ts.map +1 -0
  31. package/dist/json-pointer.js +36 -0
  32. package/dist/mutation/documents.d.ts +20 -0
  33. package/dist/mutation/documents.d.ts.map +1 -0
  34. package/dist/mutation/documents.js +77 -0
  35. package/dist/mutation/layout.d.ts +17 -0
  36. package/dist/mutation/layout.d.ts.map +1 -0
  37. package/dist/mutation/layout.js +43 -0
  38. package/dist/mutation/mode-operations.d.ts +15 -0
  39. package/dist/mutation/mode-operations.d.ts.map +1 -0
  40. package/dist/mutation/mode-operations.js +124 -0
  41. package/dist/mutation/mutations.d.ts +11 -0
  42. package/dist/mutation/mutations.d.ts.map +1 -0
  43. package/dist/mutation/mutations.js +41 -0
  44. package/dist/mutation/planner.d.ts +11 -0
  45. package/dist/mutation/planner.d.ts.map +1 -0
  46. package/dist/mutation/planner.js +67 -0
  47. package/dist/mutation/reference-renamer.d.ts +25 -0
  48. package/dist/mutation/reference-renamer.d.ts.map +1 -0
  49. package/dist/mutation/reference-renamer.js +186 -0
  50. package/dist/mutation/token-operations.d.ts +31 -0
  51. package/dist/mutation/token-operations.d.ts.map +1 -0
  52. package/dist/mutation/token-operations.js +309 -0
  53. package/dist/mutation/types.d.ts +78 -0
  54. package/dist/mutation/types.d.ts.map +1 -0
  55. package/dist/mutation/types.js +9 -0
  56. package/dist/mutation/validation.d.ts +4 -0
  57. package/dist/mutation/validation.d.ts.map +1 -0
  58. package/dist/mutation/validation.js +48 -0
  59. package/dist/source/composer.d.ts +17 -0
  60. package/dist/source/composer.d.ts.map +1 -0
  61. package/dist/source/composer.js +226 -0
  62. package/dist/source/documents.d.ts +18 -0
  63. package/dist/source/documents.d.ts.map +1 -0
  64. package/dist/source/documents.js +68 -0
  65. package/dist/source/load.d.ts +8 -0
  66. package/dist/source/load.d.ts.map +1 -0
  67. package/dist/source/load.js +13 -0
  68. package/dist/source/resolver.d.ts +14 -0
  69. package/dist/source/resolver.d.ts.map +1 -0
  70. package/dist/source/resolver.js +180 -0
  71. package/dist/source/values.d.ts +17 -0
  72. package/dist/source/values.d.ts.map +1 -0
  73. package/dist/source/values.js +21 -0
  74. package/package.json +66 -0
@@ -0,0 +1,226 @@
1
+ import { decodeJsonPointer, encodeJsonPointer, readJsonPointer, } from '../json-pointer.js';
2
+ import { isObject, mergeSourceValues, } from './values.js';
3
+ export class TokenSourceComposer {
4
+ documents;
5
+ constructor(documents) {
6
+ this.documents = documents;
7
+ }
8
+ async compose(entry, items, input) {
9
+ let composed = Object.create(null);
10
+ for (const item of items) {
11
+ const sources = item.type === 'set'
12
+ ? item.sources
13
+ : item.contexts[input[item.name]];
14
+ for (const source of sources) {
15
+ composed = mergeSourceValues(composed, this.annotate(source));
16
+ }
17
+ }
18
+ composed = this.expandGroups(composed);
19
+ composed = await this.resolveValuePointers(composed, composed, entry);
20
+ this.validateAliases(composed, composed);
21
+ return this.expandArrayAliases(composed, composed);
22
+ }
23
+ annotate(node) {
24
+ const origin = this.documents.origin(node);
25
+ if (node.$extensions !== undefined && !isObject(node.$extensions)) {
26
+ this.documents.fail('EXTENSIONS_OBJECT', '$extensions must be an object.', node);
27
+ }
28
+ if ('$value' in node) {
29
+ return this.annotateToken(node, origin);
30
+ }
31
+ const result = Object.create(null);
32
+ result.$extensions = this.compilerSource(origin);
33
+ for (const [key, value] of Object.entries(node)) {
34
+ if (key === '$extensions') {
35
+ continue;
36
+ }
37
+ if (key.startsWith('$') && key !== '$root') {
38
+ if (!['$type', '$description', '$deprecated', '$extends'].includes(key)) {
39
+ this.documents.fail('TOKEN_NAME', `Unknown group property: ${key}`, node);
40
+ }
41
+ result[key] = value;
42
+ continue;
43
+ }
44
+ if (key !== '$root' && (key.startsWith('$') || /[.{}]/.test(key))) {
45
+ this.documents.fail('TOKEN_NAME', `Invalid token/group name: ${key}`, node);
46
+ }
47
+ if (!isObject(value)) {
48
+ this.documents.fail('TOKEN_GROUP', `Expected token or group at ${key}.`, node);
49
+ }
50
+ if (key === '$root' && !Object.hasOwn(value, '$value')) {
51
+ this.documents.fail('ROOT_TOKEN', '$root must be a token.', node);
52
+ }
53
+ result[key] = this.annotate(value);
54
+ }
55
+ return result;
56
+ }
57
+ annotateToken(node, origin) {
58
+ if (Object.keys(node).some(key => !key.startsWith('$')) || '$extends' in node) {
59
+ this.documents.fail('TOKEN_GROUP', 'A token cannot contain children or $extends.', node);
60
+ }
61
+ return {
62
+ ...node,
63
+ $value: this.rebaseValueReferences(node.$value, origin.url),
64
+ $extensions: this.compilerSource(origin),
65
+ };
66
+ }
67
+ rebaseValueReferences(value, owner) {
68
+ if (Array.isArray(value)) {
69
+ return value.map(child => this.rebaseValueReferences(child, owner));
70
+ }
71
+ if (!isObject(value)) {
72
+ return value;
73
+ }
74
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
75
+ key,
76
+ key === '$ref' && typeof child === 'string' && !child.startsWith('#')
77
+ ? new URL(child, owner).href
78
+ : this.rebaseValueReferences(child, owner),
79
+ ]));
80
+ }
81
+ compilerSource(origin) {
82
+ return {
83
+ 'ai.reforma.compiler': {
84
+ file: this.documents.formatFilename(origin.url),
85
+ pointer: encodeJsonPointer(origin.path),
86
+ },
87
+ };
88
+ }
89
+ expandGroups(composed) {
90
+ const expanded = new Map();
91
+ const expand = (group, path, chain) => {
92
+ const id = encodeJsonPointer(path);
93
+ if (chain.includes(id)) {
94
+ this.documents.fail('EXTENDS_CYCLE', `Circular group inheritance at ${id}.`);
95
+ }
96
+ const cached = expanded.get(id);
97
+ if (cached) {
98
+ return cached;
99
+ }
100
+ if ('$value' in group) {
101
+ this.documents.fail('EXTENDS_TOKEN', '$extends must reference a group.');
102
+ }
103
+ let result = group;
104
+ if ('$extends' in group) {
105
+ result = extendGroup(group, path, chain);
106
+ }
107
+ const children = { ...result };
108
+ for (const [key, child] of Object.entries(result)) {
109
+ if ((!key.startsWith('$') || key === '$root')
110
+ && isObject(child)
111
+ && !('$value' in child)) {
112
+ children[key] = expand(child, [...path, key], [...chain, id]);
113
+ }
114
+ }
115
+ expanded.set(id, children);
116
+ return children;
117
+ };
118
+ const extendGroup = (group, path, chain) => {
119
+ const reference = group.$extends;
120
+ if (typeof reference !== 'string') {
121
+ this.documents.fail('EXTENDS_INVALID', '$extends must be a group reference.');
122
+ }
123
+ const targetPath = reference.startsWith('{') && reference.endsWith('}')
124
+ ? reference.slice(1, -1).split('.')
125
+ : decodeJsonPointer(reference);
126
+ const target = readJsonPointer(composed, targetPath);
127
+ if (!isObject(target) || '$value' in target) {
128
+ this.documents.fail('EXTENDS_TOKEN', '$extends must reference a group.');
129
+ }
130
+ const { $extends: _, ...own } = group;
131
+ const id = encodeJsonPointer(path);
132
+ return mergeSourceValues(expand(target, targetPath, [...chain, id]), own);
133
+ };
134
+ return expand(composed, [], []);
135
+ }
136
+ async resolveValuePointers(value, document, owner, chain = []) {
137
+ if (Array.isArray(value)) {
138
+ return Promise.all(value.map(child => (this.resolveValuePointers(child, document, owner, chain))));
139
+ }
140
+ if (!isObject(value)) {
141
+ return value;
142
+ }
143
+ if ('$ref' in value) {
144
+ return this.resolveValuePointer(value, document, owner, chain);
145
+ }
146
+ const entries = await Promise.all(Object.entries(value).map(async ([key, child]) => [
147
+ key,
148
+ key === '$extensions'
149
+ ? child
150
+ : await this.resolveValuePointers(child, document, owner, chain),
151
+ ]));
152
+ return Object.fromEntries(entries);
153
+ }
154
+ async resolveValuePointer(value, document, owner, chain) {
155
+ if (typeof value.$ref !== 'string') {
156
+ this.documents.fail('REFERENCE_INVALID', '$ref must be a string.');
157
+ }
158
+ const url = new URL(value.$ref, owner);
159
+ const sameDocument = value.$ref.startsWith('#');
160
+ const targetDocument = sameDocument
161
+ ? document
162
+ : await this.documents.load(url);
163
+ const targetPath = decodeJsonPointer(url.hash);
164
+ const documentUrl = sameDocument ? owner.href : url.href.split('#')[0];
165
+ const id = `${documentUrl}#${encodeJsonPointer(targetPath)}`;
166
+ if (chain.includes(id)) {
167
+ this.documents.fail('REFERENCE_CYCLE', 'Circular value pointer.');
168
+ }
169
+ const target = readJsonPointer(targetDocument, targetPath);
170
+ const targetOwner = new URL(url);
171
+ targetOwner.hash = '';
172
+ const resolved = await this.resolveValuePointers(target, targetDocument, targetOwner, [...chain, id]);
173
+ const { $ref: _, ...overrides } = value;
174
+ if (Object.keys(overrides).length === 0) {
175
+ return resolved;
176
+ }
177
+ if (!isObject(resolved)) {
178
+ this.documents.fail('REFERENCE_OBJECT', 'Reference siblings require an object target.');
179
+ }
180
+ return {
181
+ ...resolved,
182
+ ...await this.resolveValuePointers(overrides, document, owner, chain),
183
+ };
184
+ }
185
+ validateAliases(value, composed) {
186
+ if (typeof value === 'string' && /^\{[^{}]+\}$/.test(value)) {
187
+ const target = readJsonPointer(composed, value.slice(1, -1).split('.'));
188
+ if (!isObject(target) || !('$value' in target)) {
189
+ this.documents.fail('ALIAS_GROUP', `Alias ${value} must name a token, including $root explicitly.`);
190
+ }
191
+ return;
192
+ }
193
+ if (!value || typeof value !== 'object') {
194
+ return;
195
+ }
196
+ for (const [key, child] of Object.entries(value)) {
197
+ if (!['$extensions', '$description', '$deprecated'].includes(key)) {
198
+ this.validateAliases(child, composed);
199
+ }
200
+ }
201
+ }
202
+ expandArrayAliases(value, composed, chain = []) {
203
+ if (Array.isArray(value)) {
204
+ return value.flatMap(item => {
205
+ if (typeof item === 'string' && /^\{[^{}]+\}$/.test(item)) {
206
+ if (chain.includes(item)) {
207
+ this.documents.fail('ALIAS_CYCLE', `Circular array alias ${item}.`);
208
+ }
209
+ const target = readJsonPointer(composed, item.slice(1, -1).split('.'));
210
+ const resolved = this.expandArrayAliases(target.$value, composed, [...chain, item]);
211
+ return Array.isArray(resolved) ? resolved : [resolved];
212
+ }
213
+ return [this.expandArrayAliases(item, composed, chain)];
214
+ });
215
+ }
216
+ if (isObject(value)) {
217
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
218
+ key,
219
+ key === '$extensions'
220
+ ? child
221
+ : this.expandArrayAliases(child, composed, chain),
222
+ ]));
223
+ }
224
+ return value;
225
+ }
226
+ }
@@ -0,0 +1,18 @@
1
+ import type { TokenDiagnostic } from '../compilation/compiler.js';
2
+ import type { FormatFilename, ObjectValue, ReadDocument, SourceOrigin } from './values.js';
3
+ export declare class TokenSourceDocuments {
4
+ private readonly read;
5
+ private readonly filename;
6
+ private readonly diagnostics;
7
+ private readonly documents;
8
+ private readonly origins;
9
+ constructor(read: ReadDocument, filename: FormatFilename, diagnostics: TokenDiagnostic[]);
10
+ load(url: URL): Promise<ObjectValue>;
11
+ origin(node: object): SourceOrigin;
12
+ setOrigin(node: object, origin: SourceOrigin): void;
13
+ formatFilename(url: URL): string;
14
+ warn(code: string, message: string): void;
15
+ fail(code: string, message: string, node?: object): never;
16
+ private recordOrigins;
17
+ }
18
+ //# sourceMappingURL=documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/source/documents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EACR,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACf,MAAM,aAAa,CAAC;AAErB,qBAAa,oBAAoB;IAKzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW;IANhC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuC;IAE/D,YACqB,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,eAAe,EAAE,EAC/C;IAEE,IAAI,CAAC,GAAG,EAAE,GAAG,wBAoBlB;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,gBAElB;IAED,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,QAE3C;IAED,cAAc,CAAC,GAAG,EAAE,GAAG,UAEtB;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAEjC;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAYxD;IAED,OAAO,CAAC,aAAa;CAgBxB"}
@@ -0,0 +1,68 @@
1
+ import { encodeJsonPointer } from '../json-pointer.js';
2
+ import { isObject } from './values.js';
3
+ export class TokenSourceDocuments {
4
+ read;
5
+ filename;
6
+ diagnostics;
7
+ documents = new Map();
8
+ origins = new WeakMap();
9
+ constructor(read, filename, diagnostics) {
10
+ this.read = read;
11
+ this.filename = filename;
12
+ this.diagnostics = diagnostics;
13
+ }
14
+ async load(url) {
15
+ const cleanUrl = new URL(url);
16
+ cleanUrl.hash = '';
17
+ const cached = this.documents.get(cleanUrl.href);
18
+ if (cached) {
19
+ return cached;
20
+ }
21
+ const document = await this.read(cleanUrl);
22
+ if (!isObject(document)) {
23
+ this.fail('DOCUMENT_OBJECT', 'Token and resolver documents must be JSON objects.');
24
+ }
25
+ this.recordOrigins(document, cleanUrl);
26
+ this.documents.set(cleanUrl.href, document);
27
+ return document;
28
+ }
29
+ origin(node) {
30
+ return this.origins.get(node);
31
+ }
32
+ setOrigin(node, origin) {
33
+ this.origins.set(node, origin);
34
+ }
35
+ formatFilename(url) {
36
+ return this.filename(url);
37
+ }
38
+ warn(code, message) {
39
+ this.diagnostics.push({ severity: 'warning', code, message });
40
+ }
41
+ fail(code, message, node) {
42
+ const origin = node && this.origins.get(node);
43
+ this.diagnostics.push({
44
+ severity: 'error',
45
+ code,
46
+ message,
47
+ ...(origin ? {
48
+ file: this.filename(origin.url),
49
+ pointer: encodeJsonPointer(origin.path),
50
+ } : {}),
51
+ });
52
+ throw new Error(message);
53
+ }
54
+ recordOrigins(value, url, path = []) {
55
+ if (!value || typeof value !== 'object') {
56
+ return;
57
+ }
58
+ this.origins.set(value, { url, path });
59
+ if (isObject(value) && '$value' in value) {
60
+ return;
61
+ }
62
+ for (const [key, child] of Object.entries(value)) {
63
+ if (key !== '$extensions') {
64
+ this.recordOrigins(child, url, [...path, key]);
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,8 @@
1
+ import type { TokenDiagnostic } from '../compilation/compiler.js';
2
+ import type { FormatFilename, ReadDocument } from './values.js';
3
+ /** Adapt resolver references and group inheritance before Terrazzo's value parser. */
4
+ export declare function loadTokenSources(entry: URL, read: ReadDocument, filename: FormatFilename, diagnostics: TokenDiagnostic[]): Promise<{
5
+ items: import("./values.js").SourceItem[];
6
+ compose: (input: Record<string, string>) => Promise<any>;
7
+ }>;
8
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/source/load.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhE,sFAAsF;AACtF,wBAAsB,gBAAgB,CAClC,KAAK,EAAE,GAAG,EACV,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,eAAe,EAAE;;qBAQT,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;GAE9C"}
@@ -0,0 +1,13 @@
1
+ import { TokenSourceComposer } from './composer.js';
2
+ import { TokenSourceDocuments } from './documents.js';
3
+ import { TokenSourceResolver } from './resolver.js';
4
+ /** Adapt resolver references and group inheritance before Terrazzo's value parser. */
5
+ export async function loadTokenSources(entry, read, filename, diagnostics) {
6
+ const documents = new TokenSourceDocuments(read, filename, diagnostics);
7
+ const items = await new TokenSourceResolver(documents).load(entry);
8
+ const composer = new TokenSourceComposer(documents);
9
+ return {
10
+ items,
11
+ compose: (input) => composer.compose(entry, items, input),
12
+ };
13
+ }
@@ -0,0 +1,14 @@
1
+ import { TokenSourceDocuments } from './documents.js';
2
+ import type { SourceItem } from './values.js';
3
+ export declare class TokenSourceResolver {
4
+ private readonly documents;
5
+ constructor(documents: TokenSourceDocuments);
6
+ load(entry: URL): Promise<SourceItem[]>;
7
+ private dereference;
8
+ private expandSources;
9
+ private validateResolver;
10
+ private normalizeItem;
11
+ private validateDefinitions;
12
+ private loadResolutionOrder;
13
+ }
14
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/source/resolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAGtD,OAAO,KAAK,EAAe,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D,qBAAa,mBAAmB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAAtC,YAA6B,SAAS,EAAE,oBAAoB,EAAI;IAE1D,IAAI,CAAC,KAAK,EAAE,GAAG,yBAMpB;YAEa,WAAW;YA8DX,aAAa;IAwC3B,OAAO,CAAC,gBAAgB;YAkDV,aAAa;YA4Eb,mBAAmB;YAanB,mBAAmB;CAwBpC"}
@@ -0,0 +1,180 @@
1
+ import { decodeJsonPointer, readJsonPointer } from '../json-pointer.js';
2
+ import { isObject } from './values.js';
3
+ export class TokenSourceResolver {
4
+ documents;
5
+ constructor(documents) {
6
+ this.documents = documents;
7
+ }
8
+ async load(entry) {
9
+ const root = await this.documents.load(entry);
10
+ this.validateResolver(root);
11
+ await this.validateDefinitions(root);
12
+ return this.loadResolutionOrder(root);
13
+ }
14
+ async dereference(node, allowModifier, chain = []) {
15
+ if (!isObject(node)) {
16
+ this.documents.fail('RESOLVER_OBJECT', 'Expected a resolver object.');
17
+ }
18
+ if (!Object.hasOwn(node, '$ref')) {
19
+ return node;
20
+ }
21
+ if (typeof node.$ref !== 'string') {
22
+ this.documents.fail('REFERENCE_INVALID', '$ref must be a string.', node);
23
+ }
24
+ const origin = this.documents.origin(node);
25
+ const url = new URL(node.$ref, origin.url);
26
+ const path = decodeJsonPointer(url.hash);
27
+ const referencesModifier = path[0] === 'modifiers' && !allowModifier;
28
+ if (path[0] === 'resolutionOrder' || referencesModifier) {
29
+ this.documents.fail('REFERENCE_FORBIDDEN', 'Only resolutionOrder may reference modifiers; inline entries cannot be referenced.', node);
30
+ }
31
+ if (chain.includes(url.href)) {
32
+ this.documents.fail('REFERENCE_CYCLE', 'Circular resolver reference.', node);
33
+ }
34
+ const document = await this.documents.load(url);
35
+ const target = readJsonPointer(document, path);
36
+ if (!isObject(target)) {
37
+ this.documents.fail('REFERENCE_OBJECT', 'Resolver reference must target an object.', node);
38
+ }
39
+ const resolved = await this.dereference(target, allowModifier, [...chain, url.href]);
40
+ // Resolver §4.2.2: sibling keys override shallowly, including arrays and objects.
41
+ const { $ref: _, ...overrides } = node;
42
+ const result = { ...resolved, ...overrides };
43
+ this.documents.setOrigin(result, this.documents.origin(target));
44
+ if (path.length === 2 && ['sets', 'modifiers'].includes(path[0])) {
45
+ result.name ??= path[1];
46
+ result.type ??= path[0] === 'sets' ? 'set' : 'modifier';
47
+ }
48
+ return result;
49
+ }
50
+ async expandSources(sources, chain = []) {
51
+ if (!Array.isArray(sources)) {
52
+ this.documents.fail('SOURCES_ARRAY', 'Token sources must be an array.');
53
+ }
54
+ const result = [];
55
+ for (const source of sources) {
56
+ if (!isObject(source)) {
57
+ this.documents.fail('SOURCE_OBJECT', 'A token source must be an object.');
58
+ }
59
+ if (chain.includes(source)) {
60
+ this.documents.fail('REFERENCE_CYCLE', 'Circular set reference.', source);
61
+ }
62
+ const expanded = await this.dereference(source, false);
63
+ if (expanded.type === 'modifier') {
64
+ this.documents.fail('REFERENCE_FORBIDDEN', 'A token source cannot reference a modifier.', source);
65
+ }
66
+ if (expanded.type === 'set') {
67
+ result.push(...await this.expandSources(expanded.sources, [...chain, source]));
68
+ }
69
+ else {
70
+ result.push(expanded);
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ validateResolver(root) {
76
+ if (root.version !== '2025.10' || !Array.isArray(root.resolutionOrder)) {
77
+ this.documents.fail('RESOLVER_VERSION', 'Expected a DTCG 2025.10 resolver with resolutionOrder.', root);
78
+ }
79
+ for (const key of ['name', 'description', '$schema']) {
80
+ if (root[key] !== undefined && typeof root[key] !== 'string') {
81
+ this.documents.fail('RESOLVER_PROPERTY', `${key} must be a string.`, root);
82
+ }
83
+ }
84
+ for (const key of ['sets', 'modifiers', '$defs', '$extensions']) {
85
+ if (root[key] !== undefined && !isObject(root[key])) {
86
+ this.documents.fail('RESOLVER_PROPERTY', `${key} must be an object.`, root);
87
+ }
88
+ }
89
+ const allowed = new Set([
90
+ '$defs',
91
+ '$extensions',
92
+ '$schema',
93
+ 'description',
94
+ 'modifiers',
95
+ 'name',
96
+ 'resolutionOrder',
97
+ 'sets',
98
+ 'version',
99
+ ]);
100
+ if (Object.keys(root).some(key => !allowed.has(key))) {
101
+ this.documents.fail('RESOLVER_PROPERTY', 'Unknown resolver property; use $extensions for vendor data.', root);
102
+ }
103
+ }
104
+ async normalizeItem(item) {
105
+ const invalidDescription = item.description !== undefined
106
+ && typeof item.description !== 'string';
107
+ const invalidExtensions = item.$extensions !== undefined
108
+ && !isObject(item.$extensions);
109
+ if (invalidDescription || invalidExtensions) {
110
+ this.documents.fail('RESOLVER_PROPERTY', 'Expected a string description and object $extensions.', item);
111
+ }
112
+ const allowed = [
113
+ '$defs',
114
+ '$extensions',
115
+ 'description',
116
+ 'name',
117
+ 'type',
118
+ ...(item.type === 'set' ? ['sources'] : ['contexts', 'default']),
119
+ ];
120
+ if (Object.keys(item).some(key => !allowed.includes(key))) {
121
+ this.documents.fail('RESOLVER_PROPERTY', 'Unknown set or modifier property.', item);
122
+ }
123
+ if (item.type === 'set') {
124
+ return {
125
+ name: item.name,
126
+ type: 'set',
127
+ sources: await this.expandSources(item.sources),
128
+ };
129
+ }
130
+ if (!isObject(item.contexts) || Object.keys(item.contexts).length === 0) {
131
+ this.documents.fail('CONTEXTS_EMPTY', 'A modifier must have at least one context.', item);
132
+ }
133
+ if (item.default !== undefined
134
+ && (typeof item.default !== 'string'
135
+ || !Object.hasOwn(item.contexts, item.default))) {
136
+ this.documents.fail('DEFAULT_INVALID', 'Modifier default must name an existing context.', item);
137
+ }
138
+ if (Object.keys(item.contexts).length === 1) {
139
+ this.documents.warn('SINGLE_CONTEXT', `Modifier ${item.name} has one context; DTCG recommends at least two.`);
140
+ }
141
+ const contexts = Object.create(null);
142
+ for (const [name, sources] of Object.entries(item.contexts)) {
143
+ contexts[name] = await this.expandSources(sources);
144
+ }
145
+ return {
146
+ name: item.name,
147
+ type: 'modifier',
148
+ contexts,
149
+ default: item.default,
150
+ };
151
+ }
152
+ async validateDefinitions(root) {
153
+ // Unused definitions still belong to the resolver and must be structurally valid.
154
+ for (const [section, type] of [
155
+ ['sets', 'set'],
156
+ ['modifiers', 'modifier'],
157
+ ]) {
158
+ for (const [name, raw] of Object.entries(root[section] ?? {})) {
159
+ const item = await this.dereference(raw, false);
160
+ await this.normalizeItem({ ...item, name, type });
161
+ }
162
+ }
163
+ }
164
+ async loadResolutionOrder(root) {
165
+ const items = [];
166
+ const names = new Set();
167
+ for (const raw of root.resolutionOrder) {
168
+ const item = await this.dereference(raw, true);
169
+ const invalid = typeof item.name !== 'string'
170
+ || !['set', 'modifier'].includes(item.type)
171
+ || names.has(item.name);
172
+ if (invalid) {
173
+ this.documents.fail('RESOLUTION_ORDER', 'Each resolutionOrder entry requires a unique name and set or modifier type.', raw);
174
+ }
175
+ names.add(item.name);
176
+ items.push(await this.normalizeItem(item));
177
+ }
178
+ return items;
179
+ }
180
+ }
@@ -0,0 +1,17 @@
1
+ export type ObjectValue = Record<string, any>;
2
+ export interface SourceItem {
3
+ name: string;
4
+ type: 'set' | 'modifier';
5
+ sources?: ObjectValue[];
6
+ contexts?: Record<string, ObjectValue[]>;
7
+ default?: string;
8
+ }
9
+ export interface SourceOrigin {
10
+ url: URL;
11
+ path: string[];
12
+ }
13
+ export type ReadDocument = (url: URL) => Promise<ObjectValue>;
14
+ export type FormatFilename = (url: URL) => string;
15
+ export declare function isObject(value: unknown): value is ObjectValue;
16
+ export declare function mergeSourceValues(base: ObjectValue, override: ObjectValue): ObjectValue;
17
+ //# sourceMappingURL=values.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"values.d.ts","sourceRoot":"","sources":["../../src/source/values.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE9C,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,UAAU,CAAC;IACzB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IACzB,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC;AAElD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAE7D;AAGD,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,WAAW,EACjB,QAAQ,EAAE,WAAW,GACtB,WAAW,CAmBb"}
@@ -0,0 +1,21 @@
1
+ export function isObject(value) {
2
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+ // Groups merge recursively; a later token replaces the complete earlier token.
5
+ export function mergeSourceValues(base, override) {
6
+ const result = { ...base };
7
+ for (const [key, value] of Object.entries(override)) {
8
+ const recursive = isObject(value)
9
+ && isObject(base[key])
10
+ && !('$value' in value)
11
+ && !('$value' in base[key])
12
+ && !key.startsWith('$');
13
+ Object.defineProperty(result, key, {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value: recursive ? mergeSourceValues(base[key], value) : value,
18
+ });
19
+ }
20
+ return result;
21
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@reforma/project-tokens",
3
+ "version": "0.0.1",
4
+ "description": "Portable DTCG 2025.10 compiler for project design tokens.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "author": "Reforma, Inc. <dev@reforma.ai>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/reforma-dev/reforma",
11
+ "directory": "packages/project-tokens"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/compilation/compiler.d.ts",
16
+ "default": "./dist/compilation/compiler.js"
17
+ },
18
+ "./mutations": {
19
+ "types": "./dist/mutation/mutations.d.ts",
20
+ "default": "./dist/mutation/mutations.js"
21
+ },
22
+ "./generation": {
23
+ "types": "./dist/generation.d.ts",
24
+ "default": "./dist/generation.js"
25
+ },
26
+ "./color-value": {
27
+ "types": "./dist/color-value.d.ts",
28
+ "default": "./dist/color-value.js"
29
+ }
30
+ },
31
+ "bin": {
32
+ "reforma-tokens": "./dist/cli.js"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md"
37
+ ],
38
+ "engines": {
39
+ "node": ">=24"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "dev:prepare": "bun run build",
47
+ "build:publish": "bun run build",
48
+ "test": "bun run build && vitest run --testTimeout=15000",
49
+ "typecheck": "tsc -p tsconfig.json --noEmit",
50
+ "lint": "eslint src scripts --fix",
51
+ "verify:project": "bun scripts/verify-project.ts"
52
+ },
53
+ "dependencies": {
54
+ "@terrazzo/parser": "2.7.1",
55
+ "@terrazzo/token-tools": "2.7.1",
56
+ "colorjs.io": "0.7.1",
57
+ "jsonc-parser": "3.3.1"
58
+ },
59
+ "devDependencies": {
60
+ "@reforma/config-typescript": "workspace:*",
61
+ "@reforma/eslint-config": "workspace:*",
62
+ "@types/node": "^24",
63
+ "typescript": "^7",
64
+ "vitest": "^5.0.1"
65
+ }
66
+ }