@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,179 @@
1
+ import { isDtcgColorValue } from '../color-value.js';
2
+ const FONT_WEIGHTS = new Set([
3
+ 'black',
4
+ 'bold',
5
+ 'book',
6
+ 'demi-bold',
7
+ 'extra-black',
8
+ 'extra-bold',
9
+ 'extra-light',
10
+ 'hairline',
11
+ 'heavy',
12
+ 'light',
13
+ 'medium',
14
+ 'normal',
15
+ 'regular',
16
+ 'semi-bold',
17
+ 'thin',
18
+ 'ultra-black',
19
+ 'ultra-bold',
20
+ 'ultra-light',
21
+ ]);
22
+ const STROKE_STYLES = new Set([
23
+ 'dashed',
24
+ 'dotted',
25
+ 'double',
26
+ 'groove',
27
+ 'inset',
28
+ 'outset',
29
+ 'ridge',
30
+ 'solid',
31
+ ]);
32
+ // Terrazzo accepts additional types and CSS units; this gate keeps the final DTCG boundary strict.
33
+ export function validateTokenValue(type, value) {
34
+ switch (type) {
35
+ case 'color':
36
+ return isDtcgColorValue(value);
37
+ case 'dimension':
38
+ return isDimension(value);
39
+ case 'duration':
40
+ return isDuration(value);
41
+ case 'number':
42
+ return isFiniteNumber(value);
43
+ case 'fontFamily':
44
+ return isFontFamily(value);
45
+ case 'fontWeight':
46
+ return isFontWeight(value);
47
+ case 'cubicBezier':
48
+ return isCubicBezier(value);
49
+ case 'strokeStyle':
50
+ return isStrokeStyle(value);
51
+ case 'border':
52
+ return isBorder(value);
53
+ case 'transition':
54
+ return isTransition(value);
55
+ case 'shadow':
56
+ return isShadow(value);
57
+ case 'gradient':
58
+ return isGradient(value);
59
+ case 'typography':
60
+ return isTypography(value);
61
+ default:
62
+ return false;
63
+ }
64
+ }
65
+ export function validateAuthoredColorValues(type, value) {
66
+ if (isReference(value))
67
+ return true;
68
+ if (type === 'color') {
69
+ return containsReference(value) || isDtcgColorValue(value);
70
+ }
71
+ if (type === 'border' && isObject(value) && 'color' in value) {
72
+ return isAuthoredColor(value.color);
73
+ }
74
+ if (type === 'gradient' && Array.isArray(value)) {
75
+ return value.every(stop => !isObject(stop) || !('color' in stop) || isAuthoredColor(stop.color));
76
+ }
77
+ if (type === 'shadow') {
78
+ const layers = Array.isArray(value) ? value : [value];
79
+ return layers.every(layer => !isObject(layer) || !('color' in layer) || isAuthoredColor(layer.color));
80
+ }
81
+ return true;
82
+ }
83
+ function isObject(value) {
84
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
85
+ }
86
+ function isFiniteNumber(value) {
87
+ return typeof value === 'number' && Number.isFinite(value);
88
+ }
89
+ function isAuthoredColor(value) {
90
+ return isReference(value) || containsReference(value) || isDtcgColorValue(value);
91
+ }
92
+ function isReference(value) {
93
+ return typeof value === 'string' && /^\{[^{}]+\}$/.test(value)
94
+ || isObject(value) && typeof value.$ref === 'string';
95
+ }
96
+ function containsReference(value) {
97
+ if (isReference(value))
98
+ return true;
99
+ if (Array.isArray(value))
100
+ return value.some(containsReference);
101
+ if (isObject(value))
102
+ return Object.values(value).some(containsReference);
103
+ return false;
104
+ }
105
+ function isDimension(value) {
106
+ return isObject(value)
107
+ && isFiniteNumber(value.value)
108
+ && ['px', 'rem'].includes(value.unit);
109
+ }
110
+ function isDuration(value) {
111
+ return isObject(value)
112
+ && isFiniteNumber(value.value)
113
+ && ['ms', 's'].includes(value.unit);
114
+ }
115
+ function isFontFamily(value) {
116
+ return typeof value === 'string'
117
+ || Array.isArray(value)
118
+ && value.length > 0
119
+ && value.every(name => typeof name === 'string');
120
+ }
121
+ function isFontWeight(value) {
122
+ return isFiniteNumber(value) && value >= 1 && value <= 1000
123
+ || typeof value === 'string' && FONT_WEIGHTS.has(value);
124
+ }
125
+ function isCubicBezier(value) {
126
+ return Array.isArray(value)
127
+ && value.length === 4
128
+ && value.every(isFiniteNumber)
129
+ && value[0] >= 0
130
+ && value[0] <= 1
131
+ && value[2] >= 0
132
+ && value[2] <= 1;
133
+ }
134
+ function isStrokeStyle(value) {
135
+ if (typeof value === 'string') {
136
+ return STROKE_STYLES.has(value);
137
+ }
138
+ return isObject(value)
139
+ && Array.isArray(value.dashArray)
140
+ && value.dashArray.every(isDimension)
141
+ && ['round', 'butt', 'square'].includes(value.lineCap);
142
+ }
143
+ function isBorder(value) {
144
+ return isObject(value)
145
+ && isDimension(value.width)
146
+ && isDtcgColorValue(value.color)
147
+ && isStrokeStyle(value.style);
148
+ }
149
+ function isTransition(value) {
150
+ return isObject(value)
151
+ && isDuration(value.duration)
152
+ && isDuration(value.delay)
153
+ && isCubicBezier(value.timingFunction);
154
+ }
155
+ function isShadow(value) {
156
+ if (Array.isArray(value)) {
157
+ return value.every(layer => !Array.isArray(layer) && isShadow(layer));
158
+ }
159
+ return isObject(value)
160
+ && isDtcgColorValue(value.color)
161
+ && ['offsetX', 'offsetY', 'blur', 'spread'].every(key => isDimension(value[key]))
162
+ && (value.inset === undefined || typeof value.inset === 'boolean');
163
+ }
164
+ function isGradient(value) {
165
+ return Array.isArray(value) && value.every(stop => (isObject(stop)
166
+ && isDtcgColorValue(stop.color)
167
+ && isFiniteNumber(stop.position)
168
+ && stop.position >= 0
169
+ && stop.position <= 1));
170
+ }
171
+ function isTypography(value) {
172
+ return isObject(value)
173
+ && isFontFamily(value.fontFamily)
174
+ && isDimension(value.fontSize)
175
+ && isFontWeight(value.fontWeight)
176
+ && isDimension(value.letterSpacing)
177
+ && isFiniteNumber(value.lineHeight)
178
+ && Object.keys(value).every(key => (['fontFamily', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight'].includes(key)));
179
+ }
@@ -0,0 +1,31 @@
1
+ import type { TokenNormalized } from '@terrazzo/parser';
2
+ import type { TokenPermutation } from './compiler.js';
3
+ interface ModifierProjection {
4
+ name: string;
5
+ contexts?: Record<string, unknown>;
6
+ default?: string;
7
+ }
8
+ export declare function serializeTokenCss(token: TokenNormalized, path: string, tokensSet: Record<string, TokenNormalized>, input: Record<string, string>): {
9
+ [k: string]: string;
10
+ };
11
+ export declare function createPermutationSelector(input: Record<string, string>): string;
12
+ export declare function buildWebProjection(permutations: TokenPermutation[], cssNames: Iterable<string>, defaultInput: Record<string, string>, modifiers: ModifierProjection[]): {
13
+ tokensCss: string;
14
+ tailwindCss: string;
15
+ modes: {
16
+ version: number;
17
+ default: string;
18
+ defaultInput: Record<string, string>;
19
+ modifiers: {
20
+ name: string;
21
+ contexts: string[];
22
+ default: string | null;
23
+ }[];
24
+ permutations: {
25
+ input: Record<string, string>;
26
+ selector: string;
27
+ }[];
28
+ };
29
+ };
30
+ export {};
31
+ //# sourceMappingURL=web-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-projection.d.ts","sourceRoot":"","sources":["../../src/compilation/web-projection.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAoBpE,UAAU,kBAAkB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAC7B,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAC1C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;;EA2DhC;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,UAStE;AAED,wBAAgB,kBAAkB,CAC9B,YAAY,EAAE,gBAAgB,EAAE,EAChC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC1B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,SAAS,EAAE,kBAAkB,EAAE;;;;;;;;;;;;;;;;;EAgClC"}
@@ -0,0 +1,136 @@
1
+ import { tokenToColor } from '@terrazzo/token-tools';
2
+ import { transformCSSValue, transformDimension, transformShadowLayer, transformStrokeStyle, } from '@terrazzo/token-tools/css';
3
+ import { serialize } from 'colorjs.io/fn';
4
+ const GENERIC_FONT_FAMILIES = new Set([
5
+ 'cursive',
6
+ 'emoji',
7
+ 'fangsong',
8
+ 'fantasy',
9
+ 'math',
10
+ 'monospace',
11
+ 'sans-serif',
12
+ 'serif',
13
+ 'system-ui',
14
+ 'ui-monospace',
15
+ 'ui-rounded',
16
+ 'ui-sans-serif',
17
+ 'ui-serif',
18
+ ]);
19
+ const TAILWIND_NAMESPACE = /^--(color|spacing|font|text|font-weight|tracking|leading|breakpoint|container|radius|shadow|inset-shadow|drop-shadow|blur|perspective|aspect|ease|animate)-/;
20
+ export function serializeTokenCss(token, path, tokensSet, input) {
21
+ const name = `--${path.replace(/\.\$root$/, '').replaceAll('.', '-')}`;
22
+ const cssOptions = { tokensSet, permutation: input };
23
+ const color = (value) => (serialize(tokenToColor(value), { precision: 15 }));
24
+ // Preserve native color spaces; Terrazzo's gamut fallback uses incompatible Color.js IDs.
25
+ let value;
26
+ switch (token.$type) {
27
+ case 'color':
28
+ value = color(token.$value);
29
+ break;
30
+ case 'border':
31
+ value = [
32
+ transformDimension({ $value: token.$value.width }, cssOptions),
33
+ transformStrokeStyle({ $value: token.$value.style }, cssOptions),
34
+ color(token.$value.color),
35
+ ].join(' ');
36
+ break;
37
+ case 'shadow':
38
+ value = token.$value.map(layer => transformShadowLayer(layer, { ...cssOptions, colorValue: color(layer.color) })).join(', ');
39
+ break;
40
+ case 'gradient':
41
+ value = token.$value.map(stop => (`${color(stop.color)} ${stop.position * 100}%`)).join(', ');
42
+ break;
43
+ default:
44
+ value = transformCSSValue({ ...token, aliasChain: [], partialAliasOf: undefined }, cssOptions);
45
+ }
46
+ if (token.$type === 'fontFamily') {
47
+ value = serializeFontFamily(token.$value);
48
+ }
49
+ if (token.$type === 'typography' && typeof value === 'object') {
50
+ value['font-family'] = serializeFontFamily(token.$value.fontFamily);
51
+ }
52
+ const entries = typeof value === 'string' ? [['', value]] : Object.entries(value);
53
+ return Object.fromEntries(entries.map(([suffix, propertyValue]) => [
54
+ `${name}${suffix ? `-${suffix}` : ''}`,
55
+ propertyValue,
56
+ ]));
57
+ }
58
+ export function createPermutationSelector(input) {
59
+ const attributes = Object.entries(input).map(([axis, value]) => {
60
+ const name = axis === 'mode'
61
+ ? 'data-theme'
62
+ : `data-token-${cssIdentifier(axis)}`;
63
+ return `[${name}=${cssString(value)}]`;
64
+ });
65
+ return `html${attributes.join('')}`;
66
+ }
67
+ export function buildWebProjection(permutations, cssNames, defaultInput, modifiers) {
68
+ const defaults = Object.assign({}, ...permutations[0].tokens.map(token => token.css));
69
+ const overrides = permutations.map(permutation => (buildPermutationOverrides(permutation, defaults))).join('');
70
+ const tailwind = Object.fromEntries([...cssNames]
71
+ .filter(name => TAILWIND_NAMESPACE.test(name))
72
+ .map(name => [name, `var(${cssIdentifier(name)})`]));
73
+ return {
74
+ tokensCss: cssBlock(':root', defaults) + overrides,
75
+ tailwindCss: Object.keys(tailwind).length > 0
76
+ ? cssBlock('@theme inline', tailwind)
77
+ : '',
78
+ modes: {
79
+ version: 1,
80
+ default: defaultInput.mode ?? null,
81
+ defaultInput,
82
+ modifiers: modifiers.map(modifier => ({
83
+ name: modifier.name,
84
+ contexts: Object.keys(modifier.contexts),
85
+ default: modifier.default ?? null,
86
+ })),
87
+ permutations: permutations.map(({ input, selector }) => ({ input, selector })),
88
+ },
89
+ };
90
+ }
91
+ function cssIdentifier(value) {
92
+ return [...value].map(character => {
93
+ const codePoint = character.codePointAt(0);
94
+ return /[a-zA-Z0-9_-]/.test(character) || codePoint >= 128
95
+ ? character
96
+ : `\\${codePoint.toString(16)} `;
97
+ }).join('');
98
+ }
99
+ function cssString(value) {
100
+ const escaped = [...value].map(character => {
101
+ const codePoint = character.codePointAt(0);
102
+ return character === '"' || character === '\\' || codePoint < 32 || codePoint === 127
103
+ ? `\\${codePoint.toString(16)} `
104
+ : character;
105
+ }).join('');
106
+ return `"${escaped}"`;
107
+ }
108
+ function serializeFontFamily(value) {
109
+ const names = Array.isArray(value) ? value : [value];
110
+ return names.map(name => (GENERIC_FONT_FAMILIES.has(name) ? name : cssString(name))).join(', ');
111
+ }
112
+ function buildPermutationOverrides(permutation, defaults) {
113
+ const values = {};
114
+ for (const token of permutation.tokens) {
115
+ for (const [name, value] of Object.entries(token.css)) {
116
+ if (defaults[name] !== value) {
117
+ values[name] = value;
118
+ }
119
+ }
120
+ }
121
+ for (const name of Object.keys(defaults)) {
122
+ if (!permutation.tokens.some(token => name in token.css)) {
123
+ values[name] = 'initial';
124
+ }
125
+ }
126
+ return Object.keys(values).length > 0
127
+ ? `\n${cssBlock(permutation.selector, values)}`
128
+ : '';
129
+ }
130
+ function cssBlock(selector, values) {
131
+ const declarations = Object.entries(values)
132
+ .sort(([left], [right]) => left.localeCompare(right))
133
+ .map(([name, value]) => ` ${cssIdentifier(name)}: ${value};`)
134
+ .join('\n');
135
+ return `${selector} {\n${declarations}\n}\n`;
136
+ }
@@ -0,0 +1,6 @@
1
+ import { type TokenCompilation, type TokenCompilationOptions } from './compilation/compiler.js';
2
+ export declare function readTokenDependency(root: string, path: string): Promise<string | null>;
3
+ export declare function withTokenCompilationLock<T>(options: TokenCompilationOptions, work: (generate: (expectedRevision?: string) => Promise<TokenCompilation>) => Promise<T>): Promise<T>;
4
+ /** Compile and replace generated files under the same lock used by token transactions. */
5
+ export declare function buildProjectTokens(options: TokenCompilationOptions): Promise<TokenCompilation>;
6
+ //# sourceMappingURL=generation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generation.d.ts","sourceRoot":"","sources":["../src/generation.ts"],"names":[],"mappings":"AAKA,OAAO,EAAwB,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAYtH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,0BAYnE;AAED,wBAAsB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,gBAAgB,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAoIxL;AAED,0FAA0F;AAC1F,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEpG"}
@@ -0,0 +1,186 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lstat, mkdir, readFile, readlink, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { compileProjectTokens } from './compilation/compiler.js';
6
+ async function readOptional(path) {
7
+ try {
8
+ return await readFile(path, 'utf8');
9
+ }
10
+ catch (error) {
11
+ if (error.code === 'ENOENT')
12
+ return null;
13
+ throw error;
14
+ }
15
+ }
16
+ export async function readTokenDependency(root, path) {
17
+ try {
18
+ root = await realpath(root);
19
+ const actual = await realpath(resolve(root, path));
20
+ const rel = relative(root, actual);
21
+ if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`))
22
+ return null;
23
+ return await readOptional(actual);
24
+ }
25
+ catch (error) {
26
+ if (error.code === 'ENOENT')
27
+ return null;
28
+ throw error;
29
+ }
30
+ }
31
+ export async function withTokenCompilationLock(options, work) {
32
+ const root = await realpath(options.workspaceRoot);
33
+ const directory = resolve(root, dirname(options.entry ?? '.reforma/tokens/tokens.resolver.json'), '.generated');
34
+ // Check every existing ancestor before creating output or following a symlink.
35
+ for (let path = directory; path !== root; path = dirname(path)) {
36
+ const rel = relative(root, path);
37
+ if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`))
38
+ throw new Error('Output escapes the workspace.');
39
+ try {
40
+ if ((await lstat(path)).isSymbolicLink())
41
+ throw new Error(`Output ancestor is a symlink: ${path}`);
42
+ }
43
+ catch (error) {
44
+ if (error.code !== 'ENOENT')
45
+ throw error;
46
+ }
47
+ }
48
+ await mkdir(directory, { recursive: true });
49
+ // Shared filesystem protocol for CLI and sandbox writers; all generated writes hold this lock.
50
+ const lock = join(directory, '.compile.lock');
51
+ const namespace = await readlink('/proc/self/ns/pid').catch(() => null);
52
+ const deadline = Date.now() + 10_000;
53
+ while (true) {
54
+ if (Date.now() > deadline)
55
+ throw new Error('Timed out waiting for the token compilation lock.');
56
+ try {
57
+ await mkdir(lock);
58
+ break;
59
+ }
60
+ catch (error) {
61
+ if (error.code !== 'EEXIST')
62
+ throw error;
63
+ const identity = await lstat(lock).catch(() => undefined);
64
+ if (!identity)
65
+ continue;
66
+ if (!identity.isDirectory() || identity.isSymbolicLink())
67
+ throw new Error('The token compilation lock must be a directory.');
68
+ try {
69
+ const owner = JSON.parse(await readFile(join(lock, 'owner.json'), 'utf8'));
70
+ // Sandbox children have their own PID namespace; their PIDs are not controller PIDs.
71
+ if (owner.namespace && owner.namespace !== namespace) {
72
+ await delay(40);
73
+ continue;
74
+ }
75
+ process.kill(owner.pid, 0);
76
+ }
77
+ catch (error) {
78
+ const code = error.code;
79
+ if (code === 'ESRCH') {
80
+ const claim = join(lock, '.reclaim');
81
+ try {
82
+ await mkdir(claim);
83
+ }
84
+ catch (error) {
85
+ if (['EEXIST', 'ENOENT'].includes(error.code ?? ''))
86
+ continue;
87
+ throw error;
88
+ }
89
+ const current = await stat(lock);
90
+ if (current.ino !== identity.ino || current.dev !== identity.dev) {
91
+ await rm(claim, { recursive: true });
92
+ continue;
93
+ }
94
+ const abandoned = `${lock}.${randomUUID()}`;
95
+ await rename(lock, abandoned);
96
+ await rm(abandoned, { recursive: true });
97
+ continue;
98
+ }
99
+ if (code !== 'ENOENT' && code !== 'EPERM')
100
+ throw error;
101
+ }
102
+ await delay(40);
103
+ }
104
+ }
105
+ try {
106
+ const ownerFile = join(lock, `owner-${randomUUID()}.json`);
107
+ await writeFile(ownerFile, JSON.stringify({ pid: process.pid, namespace }));
108
+ await rename(ownerFile, join(lock, 'owner.json'));
109
+ return await work(async (expectedRevision) => {
110
+ let stage;
111
+ try {
112
+ let result = await compileProjectTokens(options);
113
+ for (let attempt = 0; result.ok && attempt < 5; attempt++) {
114
+ const unchanged = await Promise.all(result.dependencies.map(async (file) => {
115
+ const text = await readTokenDependency(root, file.path);
116
+ return (text === null ? null : createHash('sha256').update(text).digest('hex')) === file.hash;
117
+ }));
118
+ if (unchanged.every(Boolean))
119
+ break;
120
+ if (attempt === 4)
121
+ throw new Error('Token sources keep changing; generation was not replaced.');
122
+ result = await compileProjectTokens(options);
123
+ }
124
+ if (expectedRevision && result.revision !== expectedRevision) {
125
+ throw Object.assign(new Error('Token sources changed before generation.'), { code: 'REVISION_CONFLICT' });
126
+ }
127
+ if (!result.ok || !result.output)
128
+ return result;
129
+ const output = {
130
+ 'tokens.css': result.output.tokensCss,
131
+ 'tailwind.css': result.output.tailwindCss,
132
+ 'modes.json': result.output.modesJson,
133
+ };
134
+ const changes = [];
135
+ for (const [name, text] of Object.entries(output)) {
136
+ const destination = join(directory, name);
137
+ if ((await lstat(destination).catch((error) => {
138
+ if (error.code === 'ENOENT')
139
+ return null;
140
+ throw error;
141
+ }))?.isSymbolicLink())
142
+ throw new Error(`Generated output is a symlink: ${name}`);
143
+ const previous = await readOptional(destination);
144
+ if (previous !== text)
145
+ changes.push({ name, text, previous });
146
+ }
147
+ if (!changes.length)
148
+ return result;
149
+ stage = join(directory, `.stage-${randomUUID()}`);
150
+ await mkdir(stage);
151
+ for (const { name, text } of changes)
152
+ await writeFile(join(stage, name), text);
153
+ const replaced = [];
154
+ try {
155
+ for (const change of changes) {
156
+ await rename(join(stage, change.name), join(directory, change.name));
157
+ replaced.push(change);
158
+ }
159
+ }
160
+ catch (error) {
161
+ for (const { name, previous } of replaced.reverse()) {
162
+ if (previous === null)
163
+ await rm(join(directory, name), { force: true });
164
+ else {
165
+ await writeFile(join(stage, name), previous);
166
+ await rename(join(stage, name), join(directory, name));
167
+ }
168
+ }
169
+ throw error;
170
+ }
171
+ return result;
172
+ }
173
+ finally {
174
+ if (stage)
175
+ await rm(stage, { recursive: true, force: true });
176
+ }
177
+ });
178
+ }
179
+ finally {
180
+ await rm(lock, { recursive: true, force: true });
181
+ }
182
+ }
183
+ /** Compile and replace generated files under the same lock used by token transactions. */
184
+ export async function buildProjectTokens(options) {
185
+ return withTokenCompilationLock(options, (generate) => generate());
186
+ }
@@ -0,0 +1,5 @@
1
+ export declare function encodeJsonPointer(parts: string[]): string;
2
+ export declare function decodeJsonPointer(value: string): string[];
3
+ export declare function readJsonPointer(root: unknown, parts: string[]): any;
4
+ export declare function encodeJsonPointerPart(part: string): string;
5
+ //# sourceMappingURL=json-pointer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-pointer.d.ts","sourceRoot":"","sources":["../src/json-pointer.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,UAMhD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,YAY9C;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAiB7D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,UAEjD"}
@@ -0,0 +1,36 @@
1
+ export function encodeJsonPointer(parts) {
2
+ if (parts.length === 0) {
3
+ return '';
4
+ }
5
+ return '/' + parts.map(encodeJsonPointerPart).join('/');
6
+ }
7
+ export function decodeJsonPointer(value) {
8
+ const fragment = value.startsWith('#') ? decodeURIComponent(value.slice(1)) : value;
9
+ if (fragment === '') {
10
+ return [];
11
+ }
12
+ if (!fragment.startsWith('/') || /~(?![01])/.test(fragment)) {
13
+ throw new Error(`Invalid JSON Pointer: ${value}`);
14
+ }
15
+ return fragment.slice(1).split('/').map(decodeJsonPointerPart);
16
+ }
17
+ export function readJsonPointer(root, parts) {
18
+ let value = root;
19
+ for (const part of parts) {
20
+ const missing = !value
21
+ || typeof value !== 'object'
22
+ || !Object.hasOwn(value, part)
23
+ || Array.isArray(value) && !/^(0|[1-9]\d*)$/.test(part);
24
+ if (missing) {
25
+ throw new Error(`Missing JSON Pointer: ${encodeJsonPointer(parts)}`);
26
+ }
27
+ value = value[part];
28
+ }
29
+ return value;
30
+ }
31
+ export function encodeJsonPointerPart(part) {
32
+ return part.replaceAll('~', '~0').replaceAll('/', '~1');
33
+ }
34
+ function decodeJsonPointerPart(part) {
35
+ return part.replaceAll('~1', '/').replaceAll('~0', '~');
36
+ }
@@ -0,0 +1,20 @@
1
+ import { type TokenFileChange } from './types.js';
2
+ import type { TokenCompilation, TokenCompilationOptions } from '../compilation/compiler.js';
3
+ export declare class TokenMutationDocuments {
4
+ private readonly workspaceRoot;
5
+ readonly entryFile: string;
6
+ private readonly current;
7
+ private readonly original;
8
+ private constructor();
9
+ static load(options: Omit<TokenCompilationOptions, 'documents'>, compilation: TokenCompilation): Promise<TokenMutationDocuments>;
10
+ get values(): Readonly<Record<string, string>>;
11
+ get files(): string[];
12
+ has(file: string): boolean;
13
+ parse(file: string): any;
14
+ read(file: string, path: string[]): any;
15
+ create(file: string, text?: string): void;
16
+ edit(file: string, path: Array<string | number>, value: unknown): void;
17
+ assertAvailable(file: string): Promise<void>;
18
+ changes(): TokenFileChange[];
19
+ }
20
+ //# sourceMappingURL=documents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/mutation/documents.ts"],"names":[],"mappings":"AAKA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAE5F,qBAAa,sBAAsB;IAI3B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAElD,OAAO,eAMN;IAED,OAAa,IAAI,CACb,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,WAAW,CAAC,EACnD,WAAW,EAAE,gBAAgB,mCA0BhC;IAED,IAAI,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAE7C;IAED,IAAI,KAAK,aAER;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,WAEf;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAEvB;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAIhC;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAS,QAEjC;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,QAY9D;IAEK,eAAe,CAAC,IAAI,EAAE,MAAM,iBAQjC;IAED,OAAO,IAAI,eAAe,EAAE,CAQ3B;CACJ"}