@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.
- package/README.md +174 -0
- package/dist/authoring.d.ts +42 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +161 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +142 -0
- package/dist/color-value.d.ts +13 -0
- package/dist/color-value.d.ts.map +1 -0
- package/dist/color-value.js +71 -0
- package/dist/compilation/compiler.d.ts +61 -0
- package/dist/compilation/compiler.d.ts.map +1 -0
- package/dist/compilation/compiler.js +254 -0
- package/dist/compilation/definitions.d.ts +17 -0
- package/dist/compilation/definitions.d.ts.map +1 -0
- package/dist/compilation/definitions.js +103 -0
- package/dist/compilation/scope-validation.d.ts +12 -0
- package/dist/compilation/scope-validation.d.ts.map +1 -0
- package/dist/compilation/scope-validation.js +105 -0
- package/dist/compilation/validation.d.ts +4 -0
- package/dist/compilation/validation.d.ts.map +1 -0
- package/dist/compilation/validation.js +179 -0
- package/dist/compilation/web-projection.d.ts +31 -0
- package/dist/compilation/web-projection.d.ts.map +1 -0
- package/dist/compilation/web-projection.js +136 -0
- package/dist/generation.d.ts +6 -0
- package/dist/generation.d.ts.map +1 -0
- package/dist/generation.js +186 -0
- package/dist/json-pointer.d.ts +5 -0
- package/dist/json-pointer.d.ts.map +1 -0
- package/dist/json-pointer.js +36 -0
- package/dist/mutation/documents.d.ts +20 -0
- package/dist/mutation/documents.d.ts.map +1 -0
- package/dist/mutation/documents.js +77 -0
- package/dist/mutation/layout.d.ts +17 -0
- package/dist/mutation/layout.d.ts.map +1 -0
- package/dist/mutation/layout.js +43 -0
- package/dist/mutation/mode-operations.d.ts +15 -0
- package/dist/mutation/mode-operations.d.ts.map +1 -0
- package/dist/mutation/mode-operations.js +124 -0
- package/dist/mutation/mutations.d.ts +11 -0
- package/dist/mutation/mutations.d.ts.map +1 -0
- package/dist/mutation/mutations.js +41 -0
- package/dist/mutation/planner.d.ts +11 -0
- package/dist/mutation/planner.d.ts.map +1 -0
- package/dist/mutation/planner.js +67 -0
- package/dist/mutation/reference-renamer.d.ts +25 -0
- package/dist/mutation/reference-renamer.d.ts.map +1 -0
- package/dist/mutation/reference-renamer.js +186 -0
- package/dist/mutation/token-operations.d.ts +31 -0
- package/dist/mutation/token-operations.d.ts.map +1 -0
- package/dist/mutation/token-operations.js +309 -0
- package/dist/mutation/types.d.ts +78 -0
- package/dist/mutation/types.d.ts.map +1 -0
- package/dist/mutation/types.js +9 -0
- package/dist/mutation/validation.d.ts +4 -0
- package/dist/mutation/validation.d.ts.map +1 -0
- package/dist/mutation/validation.js +48 -0
- package/dist/source/composer.d.ts +17 -0
- package/dist/source/composer.d.ts.map +1 -0
- package/dist/source/composer.js +226 -0
- package/dist/source/documents.d.ts +18 -0
- package/dist/source/documents.d.ts.map +1 -0
- package/dist/source/documents.js +68 -0
- package/dist/source/load.d.ts +8 -0
- package/dist/source/load.d.ts.map +1 -0
- package/dist/source/load.js +13 -0
- package/dist/source/resolver.d.ts +14 -0
- package/dist/source/resolver.d.ts.map +1 -0
- package/dist/source/resolver.js +180 -0
- package/dist/source/values.d.ts +17 -0
- package/dist/source/values.d.ts.map +1 -0
- package/dist/source/values.js +21 -0
- package/package.json +66 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { DtcgTokenType, TokenAuthoring, TokenEditingProfile, TokenGroup } from '../authoring.js';
|
|
2
|
+
export { DTCG_TOKEN_TYPES } from '../authoring.js';
|
|
3
|
+
export type { DtcgTokenType } from '../authoring.js';
|
|
4
|
+
export interface TokenDiagnostic {
|
|
5
|
+
severity: 'error' | 'warning';
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
pointer?: string;
|
|
10
|
+
input?: Record<string, string>;
|
|
11
|
+
expectedType?: DtcgTokenType;
|
|
12
|
+
actualType?: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface TokenDependency {
|
|
15
|
+
path: string;
|
|
16
|
+
hash: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface TokenSource {
|
|
19
|
+
file: string;
|
|
20
|
+
pointer: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ProjectToken {
|
|
23
|
+
path: string;
|
|
24
|
+
type: DtcgTokenType;
|
|
25
|
+
value: unknown;
|
|
26
|
+
css: Record<string, string>;
|
|
27
|
+
source: TokenSource;
|
|
28
|
+
description?: string;
|
|
29
|
+
deprecated?: boolean | string;
|
|
30
|
+
authoring?: TokenAuthoring;
|
|
31
|
+
}
|
|
32
|
+
export interface TokenPermutation {
|
|
33
|
+
input: Record<string, string>;
|
|
34
|
+
selector: string;
|
|
35
|
+
tokens: ProjectToken[];
|
|
36
|
+
groups?: TokenGroup[];
|
|
37
|
+
}
|
|
38
|
+
export interface TokenCompilationOptions {
|
|
39
|
+
workspaceRoot: string;
|
|
40
|
+
entry?: string;
|
|
41
|
+
/** Required values for modifiers without a default. */
|
|
42
|
+
input?: Record<string, string>;
|
|
43
|
+
/** In-memory candidate files, keyed by workspace-relative path. Never persisted by compilation. */
|
|
44
|
+
documents?: Readonly<Record<string, string>>;
|
|
45
|
+
}
|
|
46
|
+
export interface TokenCompilation {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
diagnostics: TokenDiagnostic[];
|
|
49
|
+
dependencies: TokenDependency[];
|
|
50
|
+
revision: string;
|
|
51
|
+
permutations: TokenPermutation[];
|
|
52
|
+
editing?: TokenEditingProfile;
|
|
53
|
+
output?: {
|
|
54
|
+
tokensCss: string;
|
|
55
|
+
tailwindCss: string;
|
|
56
|
+
modesJson: string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Compile canonical project files without changing source or generated output. */
|
|
60
|
+
export declare function compileProjectTokens(options: TokenCompilationOptions): Promise<TokenCompilation>;
|
|
61
|
+
//# sourceMappingURL=compiler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../src/compilation/compiler.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEtG,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B,SAAS,CAAC,EAAE,cAAc,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,mGAAmG;IACnG,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,MAAM,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1E;AAUD,mFAAmF;AACnF,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiStG"}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { defineConfig, Logger, parse } from '@terrazzo/parser';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { describeTokenAuthoring, getTokenEditingProfile } from '../authoring.js';
|
|
7
|
+
import { loadTokenSources } from '../source/load.js';
|
|
8
|
+
import { collectTokenDefinitions, createTerrazzoSource, fromTerrazzoTokenValue } from './definitions.js';
|
|
9
|
+
import { validateComposedTokenScopes, validateTokenScopes } from './scope-validation.js';
|
|
10
|
+
import { validateAuthoredColorValues, validateTokenValue } from './validation.js';
|
|
11
|
+
import { buildWebProjection, createPermutationSelector, serializeTokenCss, } from './web-projection.js';
|
|
12
|
+
export { DTCG_TOKEN_TYPES } from '../authoring.js';
|
|
13
|
+
function hash(value) {
|
|
14
|
+
return createHash('sha256').update(value).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
function compareNames(left, right) {
|
|
17
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
18
|
+
}
|
|
19
|
+
/** Compile canonical project files without changing source or generated output. */
|
|
20
|
+
export async function compileProjectTokens(options) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
const files = new Map();
|
|
23
|
+
const documents = new Map();
|
|
24
|
+
const reads = new Map();
|
|
25
|
+
const permutations = [];
|
|
26
|
+
let editing;
|
|
27
|
+
let projection;
|
|
28
|
+
const root = await realpath(options.workspaceRoot).catch(() => resolve(options.workspaceRoot));
|
|
29
|
+
const entry = pathToFileURL(resolve(root, options.entry ?? '.reforma/tokens/tokens.resolver.json'));
|
|
30
|
+
const local = (filename) => relative(root, filename).split(sep).join('/');
|
|
31
|
+
const fail = (code, message, source) => {
|
|
32
|
+
diagnostics.push({ severity: 'error', code, message, ...source });
|
|
33
|
+
throw new Error(message);
|
|
34
|
+
};
|
|
35
|
+
const load = async (url) => {
|
|
36
|
+
if (url.protocol !== 'file:' || url.host || url.search) {
|
|
37
|
+
fail('REFERENCE_REMOTE', `Only local workspace references are supported: ${url.href}`);
|
|
38
|
+
}
|
|
39
|
+
const filename = fileURLToPath(url);
|
|
40
|
+
const path = local(filename);
|
|
41
|
+
if (path === '..' || path.startsWith('../') || isAbsolute(path)) {
|
|
42
|
+
fail('REFERENCE_OUTSIDE_WORKSPACE', `Reference escapes the workspace: ${path}`);
|
|
43
|
+
}
|
|
44
|
+
if (!reads.has(path)) {
|
|
45
|
+
const reading = (async () => {
|
|
46
|
+
files.set(path, null);
|
|
47
|
+
const actualFilename = await realpath(filename).catch((error) => {
|
|
48
|
+
if (error.code === 'ENOENT'
|
|
49
|
+
&& Object.hasOwn(options.documents ?? {}, path)) {
|
|
50
|
+
return filename;
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
54
|
+
const actual = local(actualFilename);
|
|
55
|
+
if (actual === '..' || actual.startsWith('../') || isAbsolute(actual)) {
|
|
56
|
+
fail('REFERENCE_OUTSIDE_WORKSPACE', `Symlink escapes the workspace: ${path}`);
|
|
57
|
+
}
|
|
58
|
+
const src = Object.hasOwn(options.documents ?? {}, path)
|
|
59
|
+
? options.documents[path]
|
|
60
|
+
: await readFile(actualFilename, 'utf8');
|
|
61
|
+
files.set(path, src);
|
|
62
|
+
try {
|
|
63
|
+
documents.set(path, JSON.parse(src));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
fail('INVALID_JSON', 'Expected strict JSON.', { file: path, pointer: '' });
|
|
67
|
+
}
|
|
68
|
+
return src;
|
|
69
|
+
})();
|
|
70
|
+
reads.set(path, reading);
|
|
71
|
+
}
|
|
72
|
+
return reads.get(path);
|
|
73
|
+
};
|
|
74
|
+
let activeInput;
|
|
75
|
+
let tokenPaths = [];
|
|
76
|
+
const logger = new Logger({ level: 'silent' });
|
|
77
|
+
logger.error = (...entries) => {
|
|
78
|
+
for (const log of entries) {
|
|
79
|
+
diagnostics.push({
|
|
80
|
+
severity: 'error',
|
|
81
|
+
code: 'DTCG_INVALID',
|
|
82
|
+
message: log.message.replace(/token(\d+)/g, (id, index) => tokenPaths[Number(index)] ?? id),
|
|
83
|
+
file: local(fileURLToPath(entry)),
|
|
84
|
+
input: activeInput,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
throw new Error(entries[0]?.message);
|
|
88
|
+
};
|
|
89
|
+
logger.warn = (...entries) => {
|
|
90
|
+
for (const log of entries) {
|
|
91
|
+
diagnostics.push({
|
|
92
|
+
severity: 'warning',
|
|
93
|
+
code: 'DTCG_WARNING',
|
|
94
|
+
message: log.message.replace(/token(\d+)/g, (id, index) => tokenPaths[Number(index)] ?? id),
|
|
95
|
+
file: local(fileURLToPath(entry)),
|
|
96
|
+
input: activeInput,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
try {
|
|
101
|
+
const source = await loadTokenSources(entry, async (url) => {
|
|
102
|
+
await load(url);
|
|
103
|
+
return documents.get(local(fileURLToPath(url)));
|
|
104
|
+
}, url => local(fileURLToPath(url)), diagnostics);
|
|
105
|
+
editing = getTokenEditingProfile({ workspaceRoot: root, entry: local(fileURLToPath(entry)) }, documents);
|
|
106
|
+
const modifiers = source.items.filter((item) => item.type === 'modifier');
|
|
107
|
+
const defaultInput = Object.create(null);
|
|
108
|
+
for (const modifier of modifiers) {
|
|
109
|
+
const value = options.input?.[modifier.name] ?? modifier.default;
|
|
110
|
+
if (typeof value !== 'string' || !Object.hasOwn(modifier.contexts, value)) {
|
|
111
|
+
fail('INPUT_INVALID', `Select a valid context for modifier ${modifier.name}.`);
|
|
112
|
+
}
|
|
113
|
+
defaultInput[modifier.name] = value;
|
|
114
|
+
}
|
|
115
|
+
for (const name of Object.keys(options.input ?? {})) {
|
|
116
|
+
if (!modifiers.some(modifier => modifier.name === name)) {
|
|
117
|
+
fail('INPUT_INVALID', `Unknown modifier ${name}.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let inputs = [{}];
|
|
121
|
+
for (const modifier of modifiers) {
|
|
122
|
+
const contexts = Object.keys(modifier.contexts);
|
|
123
|
+
if (inputs.length * contexts.length > 1000) {
|
|
124
|
+
fail('PERMUTATION_LIMIT', 'At most 1000 permutations can be compiled.');
|
|
125
|
+
}
|
|
126
|
+
inputs = inputs.flatMap(input => contexts.map(context => ({
|
|
127
|
+
...input,
|
|
128
|
+
[modifier.name]: context,
|
|
129
|
+
})));
|
|
130
|
+
}
|
|
131
|
+
const isDefault = (input) => (Object.keys(defaultInput).every(key => input[key] === defaultInput[key]));
|
|
132
|
+
inputs.sort((left, right) => Number(isDefault(right)) - Number(isDefault(left)));
|
|
133
|
+
const managedProfile = editing.supported ? editing : undefined;
|
|
134
|
+
const managedBase = managedProfile ? await source.compose({ mode: managedProfile.defaultMode }) : undefined;
|
|
135
|
+
if (editing.supported && managedBase) {
|
|
136
|
+
const issues = validateTokenScopes(documents, editing, managedBase);
|
|
137
|
+
diagnostics.push(...issues);
|
|
138
|
+
if (issues.length > 0) {
|
|
139
|
+
editing = { supported: false, reason: { code: 'INVALID_SCOPE', message: issues[0].message } };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const names = new Map();
|
|
143
|
+
for (const input of inputs) {
|
|
144
|
+
activeInput = input;
|
|
145
|
+
const composed = await source.compose(input);
|
|
146
|
+
if (managedBase && managedProfile) {
|
|
147
|
+
const issues = validateComposedTokenScopes(composed, managedBase, input, documents, managedProfile, root);
|
|
148
|
+
diagnostics.push(...issues);
|
|
149
|
+
if (issues.length > 0) {
|
|
150
|
+
editing = { supported: false, reason: { code: 'INVALID_SCOPE', message: issues[0].message } };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Neutral internal IDs let Terrazzo parse legal names such as constructor and __proto__.
|
|
154
|
+
const { definitions, groups } = collectTokenDefinitions(composed, documents);
|
|
155
|
+
for (const definition of definitions) {
|
|
156
|
+
if (!validateAuthoredColorValues(definition.token.$type, definition.token.$value)) {
|
|
157
|
+
fail('DTCG_VALUE', `Invalid authored DTCG ${definition.token.$type} color value for ${definition.path}.`, definition.token.$extensions?.['ai.reforma.compiler']);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
tokenPaths = definitions.map(definition => definition.path);
|
|
161
|
+
const parserSource = createTerrazzoSource(definitions);
|
|
162
|
+
const config = defineConfig({}, { cwd: pathToFileURL(`${root}/`), logger });
|
|
163
|
+
config.lint.rules['core/consistent-naming'] = ['off', {}];
|
|
164
|
+
// Terrazzo still models missing color channels as null and only permits them in hue positions.
|
|
165
|
+
// The DTCG 2025.10 contract permits the "none" keyword in every component; validate it below.
|
|
166
|
+
config.lint.rules['core/valid-color'] = ['off', {}];
|
|
167
|
+
const { tokens: tokensSet } = await parse([{ filename: entry, src: parserSource }], { config, logger, req: load });
|
|
168
|
+
const tokens = [];
|
|
169
|
+
for (const token of Object.values(tokensSet)) {
|
|
170
|
+
const path = tokenPaths[Number(token.id.slice(5))];
|
|
171
|
+
const value = fromTerrazzoTokenValue(token.$type, token.$value);
|
|
172
|
+
if (!validateTokenValue(token.$type, value)) {
|
|
173
|
+
fail('DTCG_VALUE', `Invalid final DTCG ${token.$type} value for ${path}.`, token.$extensions?.['ai.reforma.compiler']);
|
|
174
|
+
}
|
|
175
|
+
const css = serializeTokenCss(token, path, tokensSet, input);
|
|
176
|
+
for (const key of Object.keys(css)) {
|
|
177
|
+
if (names.has(key) && names.get(key) !== path) {
|
|
178
|
+
fail('CSS_COLLISION', `${path} and ${names.get(key)} both map to ${key}.`);
|
|
179
|
+
}
|
|
180
|
+
names.set(key, path);
|
|
181
|
+
}
|
|
182
|
+
const resolved = definitions[Number(token.id.slice(5))].token;
|
|
183
|
+
const result = {
|
|
184
|
+
path,
|
|
185
|
+
type: token.$type,
|
|
186
|
+
value,
|
|
187
|
+
css,
|
|
188
|
+
source: token.$extensions?.['ai.reforma.compiler'],
|
|
189
|
+
description: resolved.$description,
|
|
190
|
+
deprecated: resolved.$deprecated ?? false,
|
|
191
|
+
};
|
|
192
|
+
result.authoring = describeTokenAuthoring(result, documents, input.mode, editing);
|
|
193
|
+
tokens.push(result);
|
|
194
|
+
}
|
|
195
|
+
const selector = createPermutationSelector(input);
|
|
196
|
+
permutations.push({ input, selector, tokens, groups });
|
|
197
|
+
}
|
|
198
|
+
if (!diagnostics.some(diagnostic => diagnostic.severity === 'error')) {
|
|
199
|
+
projection = buildWebProjection(permutations, names.keys(), defaultInput, modifiers);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (!diagnostics.some(diagnostic => diagnostic.severity === 'error')) {
|
|
204
|
+
diagnostics.push({
|
|
205
|
+
severity: 'error',
|
|
206
|
+
code: 'COMPILE_FAILED',
|
|
207
|
+
message: error instanceof Error ? error.message : String(error),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const dependencies = [...files]
|
|
212
|
+
.sort(([left], [right]) => compareNames(left, right))
|
|
213
|
+
.map(([path, text]) => ({
|
|
214
|
+
path,
|
|
215
|
+
hash: text === null ? null : hash(text),
|
|
216
|
+
}));
|
|
217
|
+
if (editing?.supported && diagnostics.some(diagnostic => diagnostic.severity === 'error')) {
|
|
218
|
+
editing = { supported: false, reason: { code: 'INVALID_SOURCE', message: 'Fix token diagnostics before structured editing.' } };
|
|
219
|
+
}
|
|
220
|
+
if (editing && !editing.supported) {
|
|
221
|
+
for (const permutation of permutations) {
|
|
222
|
+
for (const token of permutation.tokens) {
|
|
223
|
+
token.authoring = describeTokenAuthoring(token, documents, permutation.input.mode, editing);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const input = Object.fromEntries(Object.entries(options.input ?? {})
|
|
228
|
+
.sort(([left], [right]) => compareNames(left, right)));
|
|
229
|
+
const revision = hash(JSON.stringify({
|
|
230
|
+
entry: local(fileURLToPath(entry)),
|
|
231
|
+
input,
|
|
232
|
+
dependencies,
|
|
233
|
+
}));
|
|
234
|
+
const output = projection && {
|
|
235
|
+
tokensCss: projection.tokensCss,
|
|
236
|
+
tailwindCss: projection.tailwindCss,
|
|
237
|
+
modesJson: JSON.stringify({ ...projection.modes, revision, dependencies }, null, 2) + '\n',
|
|
238
|
+
};
|
|
239
|
+
const uniqueDiagnostics = [
|
|
240
|
+
...new Map(diagnostics.map(diagnostic => [
|
|
241
|
+
JSON.stringify(diagnostic),
|
|
242
|
+
diagnostic,
|
|
243
|
+
])).values(),
|
|
244
|
+
];
|
|
245
|
+
return {
|
|
246
|
+
ok: !diagnostics.some(diagnostic => diagnostic.severity === 'error'),
|
|
247
|
+
diagnostics: uniqueDiagnostics,
|
|
248
|
+
dependencies,
|
|
249
|
+
revision,
|
|
250
|
+
permutations,
|
|
251
|
+
...(editing ? { editing } : {}),
|
|
252
|
+
...(output ? { output } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type TokenGroup } from '../authoring.js';
|
|
2
|
+
interface TokenDefinition {
|
|
3
|
+
path: string;
|
|
4
|
+
token: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
export declare function collectTokenDefinitions(composed: Record<string, any>, documents: ReadonlyMap<string, Record<string, any>>): {
|
|
7
|
+
definitions: TokenDefinition[];
|
|
8
|
+
groups: TokenGroup[];
|
|
9
|
+
};
|
|
10
|
+
export declare function createTerrazzoSource(definitions: TokenDefinition[]): {
|
|
11
|
+
[k: string]: {
|
|
12
|
+
$value: unknown;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function fromTerrazzoTokenValue(type: string, value: unknown): unknown;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=definitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/compilation/definitions.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2C,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI3F,UAAU,eAAe;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,wBAAgB,uBAAuB,CACnC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;;;EAuCtD;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,eAAe,EAAE;;;;EA6BlE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5E"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readTokenDefinition } from '../authoring.js';
|
|
2
|
+
export function collectTokenDefinitions(composed, documents) {
|
|
3
|
+
const definitions = [];
|
|
4
|
+
const groups = [];
|
|
5
|
+
const collect = (group, path = [], inheritedType, inheritedDeprecated) => {
|
|
6
|
+
const type = group.$type ?? inheritedType;
|
|
7
|
+
const deprecated = group.$deprecated ?? inheritedDeprecated;
|
|
8
|
+
if ('$value' in group) {
|
|
9
|
+
definitions.push({
|
|
10
|
+
path: path.join('.'),
|
|
11
|
+
token: {
|
|
12
|
+
...group,
|
|
13
|
+
$type: type,
|
|
14
|
+
$deprecated: deprecated,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
groups.push(describeGroup(group, path, type, deprecated, documents));
|
|
20
|
+
for (const [key, child] of Object.entries(group)) {
|
|
21
|
+
if (!key.startsWith('$') || key === '$root') {
|
|
22
|
+
collect(child, [...path, key], type, deprecated);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
collect(composed);
|
|
27
|
+
definitions.sort((left, right) => left.path.localeCompare(right.path));
|
|
28
|
+
return { definitions, groups };
|
|
29
|
+
}
|
|
30
|
+
export function createTerrazzoSource(definitions) {
|
|
31
|
+
const aliases = new Map(definitions.map((definition, index) => [definition.path, `token${index}`]));
|
|
32
|
+
const rebaseAliases = (value) => {
|
|
33
|
+
if (typeof value === 'string' && /^\{[^{}]+\}$/.test(value)) {
|
|
34
|
+
return `{${aliases.get(value.slice(1, -1))}}`;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value.map(rebaseAliases);
|
|
38
|
+
}
|
|
39
|
+
if (value && typeof value === 'object') {
|
|
40
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, rebaseAliases(child)]));
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
return Object.fromEntries(definitions.map((definition, index) => [
|
|
45
|
+
`token${index}`,
|
|
46
|
+
{
|
|
47
|
+
...definition.token,
|
|
48
|
+
$value: rebaseAliases(toTerrazzoTokenValue(definition.token.$type, definition.token.$value)),
|
|
49
|
+
},
|
|
50
|
+
]));
|
|
51
|
+
}
|
|
52
|
+
export function fromTerrazzoTokenValue(type, value) {
|
|
53
|
+
return mapMissingColorComponents(type, value, null, 'none');
|
|
54
|
+
}
|
|
55
|
+
function toTerrazzoTokenValue(type, value) {
|
|
56
|
+
return mapMissingColorComponents(type, value, 'none', null);
|
|
57
|
+
}
|
|
58
|
+
function mapMissingColorComponents(type, value, from, to) {
|
|
59
|
+
if (typeof value === 'string' || !value || typeof value !== 'object') {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
if (type === 'color') {
|
|
63
|
+
return mapColor(value, from, to);
|
|
64
|
+
}
|
|
65
|
+
if (type === 'border') {
|
|
66
|
+
return { ...value, color: mapColor(value.color, from, to) };
|
|
67
|
+
}
|
|
68
|
+
if (type === 'gradient' && Array.isArray(value)) {
|
|
69
|
+
return value.map(stop => (stop && typeof stop === 'object'
|
|
70
|
+
? { ...stop, color: mapColor(stop.color, from, to) }
|
|
71
|
+
: stop));
|
|
72
|
+
}
|
|
73
|
+
if (type === 'shadow') {
|
|
74
|
+
const layers = Array.isArray(value) ? value : [value];
|
|
75
|
+
const mapped = layers.map(layer => (layer && typeof layer === 'object'
|
|
76
|
+
? { ...layer, color: mapColor(layer.color, from, to) }
|
|
77
|
+
: layer));
|
|
78
|
+
return Array.isArray(value) ? mapped : mapped[0];
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function mapColor(value, from, to) {
|
|
83
|
+
if (typeof value === 'string' || !value || typeof value !== 'object' || Array.isArray(value)) {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
const color = value;
|
|
87
|
+
return Array.isArray(color.components)
|
|
88
|
+
? { ...color, components: color.components.map(component => component === from ? to : component) }
|
|
89
|
+
: value;
|
|
90
|
+
}
|
|
91
|
+
function describeGroup(group, path, type, deprecated, documents) {
|
|
92
|
+
const source = group.$extensions?.['ai.reforma.compiler'] ?? null;
|
|
93
|
+
const original = source ? readTokenDefinition(source, documents) : {};
|
|
94
|
+
const definition = Object.fromEntries(Object.entries(original).filter(([key]) => key.startsWith('$') && key !== '$root'));
|
|
95
|
+
return {
|
|
96
|
+
path: path.join('.'),
|
|
97
|
+
source,
|
|
98
|
+
definition,
|
|
99
|
+
type,
|
|
100
|
+
description: group.$description,
|
|
101
|
+
deprecated: deprecated ?? false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DtcgTokenType, TokenEditingProfile } from '../authoring.js';
|
|
2
|
+
import type { TokenDiagnostic } from './compiler.js';
|
|
3
|
+
/** Check inherited group types and paths after $extends and mode composition. */
|
|
4
|
+
export declare function validateComposedTokenScopes(composed: Record<string, any>, base: Record<string, any>, input: Record<string, string>, documents: ReadonlyMap<string, Record<string, any>>, profile: Extract<TokenEditingProfile, {
|
|
5
|
+
supported: true;
|
|
6
|
+
}>, workspaceRoot: string): TokenDiagnostic[];
|
|
7
|
+
/** Validate authored declarations before composition can hide a mode's type changes. */
|
|
8
|
+
export declare function validateTokenScopes(documents: ReadonlyMap<string, Record<string, any>>, profile: Extract<TokenEditingProfile, {
|
|
9
|
+
supported: true;
|
|
10
|
+
}>, base: Record<string, any>): TokenDiagnostic[];
|
|
11
|
+
export declare function isDtcgTokenType(type: unknown): type is DtcgTokenType;
|
|
12
|
+
//# sourceMappingURL=scope-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scope-validation.d.ts","sourceRoot":"","sources":["../../src/compilation/scope-validation.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,iFAAiF;AACjF,wBAAgB,2BAA2B,CACvC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EACnD,OAAO,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,EAC1D,aAAa,EAAE,MAAM,GACtB,eAAe,EAAE,CAgDnB;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EACnD,OAAO,EAAE,OAAO,CAAC,mBAAmB,EAAE;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,EAC1D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,eAAe,EAAE,CA6CnB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,aAAa,CAEpE"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import { DTCG_TOKEN_TYPES, readTokenDefinition } from '../authoring.js';
|
|
4
|
+
import { decodeJsonPointer, encodeJsonPointer, readJsonPointer } from '../json-pointer.js';
|
|
5
|
+
/** Check inherited group types and paths after $extends and mode composition. */
|
|
6
|
+
export function validateComposedTokenScopes(composed, base, input, documents, profile, workspaceRoot) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
const visit = (node, baseNode, path, scopeType) => {
|
|
9
|
+
const type = node.$type ?? scopeType;
|
|
10
|
+
const expectedType = path.length === 1 ? baseNode?.$type : scopeType;
|
|
11
|
+
const source = node.$extensions?.['ai.reforma.compiler'];
|
|
12
|
+
if (path.length > 0 && type !== expectedType) {
|
|
13
|
+
diagnostics.push({ severity: 'error', code: 'SCOPE_TYPE_MISMATCH', ...source, input,
|
|
14
|
+
expectedType, actualType: type,
|
|
15
|
+
message: `${path.join('.')}: Expected scope type ${expectedType}; received ${type}.` });
|
|
16
|
+
}
|
|
17
|
+
if (!baseNode || ('$value' in node) !== ('$value' in baseNode)) {
|
|
18
|
+
diagnostics.push({ severity: 'error', code: 'SCOPE_BASE_PATH_REQUIRED', ...source, input,
|
|
19
|
+
message: `${path.join('.')}: The base must own every mode path of the same kind.` });
|
|
20
|
+
}
|
|
21
|
+
if ('$value' in node) {
|
|
22
|
+
const value = readTokenDefinition(source, documents).$value;
|
|
23
|
+
const reference = value && typeof value === 'object' ? value.$ref : undefined;
|
|
24
|
+
if (typeof reference === 'string') {
|
|
25
|
+
const owner = pathToFileURL(resolve(workspaceRoot, source.file));
|
|
26
|
+
const url = new URL(reference, owner);
|
|
27
|
+
const targetPath = decodeJsonPointer(url.hash);
|
|
28
|
+
if (targetPath.at(-1) === '$value') {
|
|
29
|
+
const file = relative(workspaceRoot, fileURLToPath(url)).split(sep).join('/');
|
|
30
|
+
const target = reference.startsWith('#') ? composed : documents.get(file);
|
|
31
|
+
let group = target;
|
|
32
|
+
let targetType = target?.$type ?? (Object.values(profile.modeFiles).includes(file) ? base[targetPath[0]]?.$type : undefined);
|
|
33
|
+
for (const part of targetPath.slice(0, -1)) {
|
|
34
|
+
group = readJsonPointer(group, [part]);
|
|
35
|
+
targetType = group.$type ?? targetType;
|
|
36
|
+
}
|
|
37
|
+
if (targetType !== undefined && targetType !== expectedType) {
|
|
38
|
+
diagnostics.push({ severity: 'error', code: 'SCOPE_ALIAS_TYPE_MISMATCH', ...source, input,
|
|
39
|
+
expectedType, actualType: targetType,
|
|
40
|
+
message: `${path.join('.')}: A whole-token value reference must use scope type ${expectedType}; received ${targetType}.` });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const [key, child] of Object.entries(node)) {
|
|
47
|
+
if (!key.startsWith('$') || key === '$root') {
|
|
48
|
+
visit(child, baseNode && Object.hasOwn(baseNode, key) ? baseNode[key] : undefined, [...path, key], expectedType);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
visit(composed, base, []);
|
|
53
|
+
return diagnostics;
|
|
54
|
+
}
|
|
55
|
+
/** Validate authored declarations before composition can hide a mode's type changes. */
|
|
56
|
+
export function validateTokenScopes(documents, profile, base) {
|
|
57
|
+
const diagnostics = [];
|
|
58
|
+
for (const file of new Set([profile.baseFile, ...Object.values(profile.modeFiles)])) {
|
|
59
|
+
if (!file)
|
|
60
|
+
continue;
|
|
61
|
+
const mode = file !== profile.baseFile;
|
|
62
|
+
const visit = (node, baseNode, path, scopeType) => {
|
|
63
|
+
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
64
|
+
return;
|
|
65
|
+
const token = Object.hasOwn(node, '$value');
|
|
66
|
+
const scope = path.length === 1 && !token;
|
|
67
|
+
const expectedType = scope ? baseNode?.$type : scopeType;
|
|
68
|
+
const report = (code, message) => diagnostics.push({
|
|
69
|
+
severity: 'error', code, file, pointer: encodeJsonPointer(path),
|
|
70
|
+
message: `${path.join('.') || '<root>'}: ${message}`,
|
|
71
|
+
...(expectedType ? { expectedType } : {}),
|
|
72
|
+
...(Object.hasOwn(node, '$type') ? { actualType: node.$type } : {}),
|
|
73
|
+
});
|
|
74
|
+
const name = path.at(-1);
|
|
75
|
+
if (name !== undefined && (!name.trim() || name !== name.trim())) {
|
|
76
|
+
report('SCOPE_INVALID_NAME', 'Use a nonempty name without outer whitespace.');
|
|
77
|
+
}
|
|
78
|
+
if (token && path.length < 2) {
|
|
79
|
+
report('SCOPE_REQUIRED', 'Tokens must belong to a typed top-level scope.');
|
|
80
|
+
}
|
|
81
|
+
if (scope && !mode && !isDtcgTokenType(node.$type)) {
|
|
82
|
+
report('SCOPE_TYPE_REQUIRED', 'A scope must declare one standard DTCG $type.');
|
|
83
|
+
}
|
|
84
|
+
if (Object.hasOwn(node, '$type') && (mode || token || path.length === 0
|
|
85
|
+
|| !scope && node.$type !== scopeType)) {
|
|
86
|
+
report('SCOPE_TYPE_DECLARATION', `Expected inherited ${expectedType ?? 'scope'} type; authored $type ${JSON.stringify(node.$type)} is not allowed here.`);
|
|
87
|
+
}
|
|
88
|
+
if (mode && (!baseNode || token !== Object.hasOwn(baseNode, '$value'))) {
|
|
89
|
+
report('SCOPE_BASE_PATH_REQUIRED', 'Mode sources may only override existing base paths of the same kind.');
|
|
90
|
+
}
|
|
91
|
+
if (token)
|
|
92
|
+
return;
|
|
93
|
+
for (const [key, child] of Object.entries(node)) {
|
|
94
|
+
if (!key.startsWith('$') || key === '$root') {
|
|
95
|
+
visit(child, baseNode && Object.hasOwn(baseNode, key) ? baseNode[key] : undefined, [...path, key], expectedType);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
visit(documents.get(file), base, []);
|
|
100
|
+
}
|
|
101
|
+
return diagnostics;
|
|
102
|
+
}
|
|
103
|
+
export function isDtcgTokenType(type) {
|
|
104
|
+
return DTCG_TOKEN_TYPES.some(candidate => candidate === type);
|
|
105
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DtcgTokenType } from '../authoring.js';
|
|
2
|
+
export declare function validateTokenValue(type: string, value: unknown): type is DtcgTokenType;
|
|
3
|
+
export declare function validateAuthoredColorValues(type: string, value: unknown): boolean;
|
|
4
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/compilation/validation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAqCrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,IAAI,aAAa,CA+BtF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAkBjF"}
|