@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,77 @@
|
|
|
1
|
+
import { applyEdits, modify } from 'jsonc-parser';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { readTokenDependency } from '../generation.js';
|
|
5
|
+
import { TokenMutationError } from './types.js';
|
|
6
|
+
export class TokenMutationDocuments {
|
|
7
|
+
workspaceRoot;
|
|
8
|
+
entryFile;
|
|
9
|
+
current;
|
|
10
|
+
original;
|
|
11
|
+
constructor(workspaceRoot, entryFile, current) {
|
|
12
|
+
this.workspaceRoot = workspaceRoot;
|
|
13
|
+
this.entryFile = entryFile;
|
|
14
|
+
this.current = current;
|
|
15
|
+
this.original = { ...current };
|
|
16
|
+
}
|
|
17
|
+
static async load(options, compilation) {
|
|
18
|
+
const documents = Object.create(null);
|
|
19
|
+
for (const dependency of compilation.dependencies) {
|
|
20
|
+
const text = await readTokenDependency(options.workspaceRoot, dependency.path);
|
|
21
|
+
const hash = text === null ? null : createHash('sha256').update(text).digest('hex');
|
|
22
|
+
if (text === null || hash !== dependency.hash) {
|
|
23
|
+
throw new TokenMutationError('REVISION_CONFLICT', 'Token sources changed while preparing the edit.');
|
|
24
|
+
}
|
|
25
|
+
documents[dependency.path] = text;
|
|
26
|
+
}
|
|
27
|
+
const workspaceRoot = resolve(options.workspaceRoot);
|
|
28
|
+
const entryPath = resolve(workspaceRoot, options.entry ?? '.reforma/tokens/tokens.resolver.json');
|
|
29
|
+
const entryFile = relative(workspaceRoot, entryPath).split(sep).join('/');
|
|
30
|
+
return new TokenMutationDocuments(workspaceRoot, entryFile, documents);
|
|
31
|
+
}
|
|
32
|
+
get values() {
|
|
33
|
+
return this.current;
|
|
34
|
+
}
|
|
35
|
+
get files() {
|
|
36
|
+
return Object.keys(this.current);
|
|
37
|
+
}
|
|
38
|
+
has(file) {
|
|
39
|
+
return Object.hasOwn(this.current, file);
|
|
40
|
+
}
|
|
41
|
+
parse(file) {
|
|
42
|
+
return JSON.parse(this.current[file]);
|
|
43
|
+
}
|
|
44
|
+
read(file, path) {
|
|
45
|
+
return path.reduce((value, key) => (value && Object.hasOwn(value, key) ? value[key] : undefined), this.parse(file));
|
|
46
|
+
}
|
|
47
|
+
create(file, text = '{}\n') {
|
|
48
|
+
this.current[file] = text;
|
|
49
|
+
}
|
|
50
|
+
edit(file, path, value) {
|
|
51
|
+
const text = this.current[file];
|
|
52
|
+
const indent = text.match(/\n([\t ]+)"/)?.[1] ?? ' ';
|
|
53
|
+
const edits = modify(text, path, value, {
|
|
54
|
+
formattingOptions: {
|
|
55
|
+
insertSpaces: !indent.includes('\t'),
|
|
56
|
+
tabSize: indent.length,
|
|
57
|
+
eol: text.includes('\r\n') ? '\r\n' : '\n',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
this.current[file] = applyEdits(text, edits);
|
|
61
|
+
}
|
|
62
|
+
async assertAvailable(file) {
|
|
63
|
+
if (this.has(file)
|
|
64
|
+
|| await readTokenDependency(this.workspaceRoot, file) !== null) {
|
|
65
|
+
throw new TokenMutationError('PATH_EXISTS', `A document already exists at ${file}.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
changes() {
|
|
69
|
+
return Object.entries(this.current)
|
|
70
|
+
.filter(([file, text]) => this.original[file] !== text)
|
|
71
|
+
.map(([path, after]) => ({
|
|
72
|
+
path,
|
|
73
|
+
before: this.original[path] ?? null,
|
|
74
|
+
after,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TokenMutationDocuments } from './documents.js';
|
|
2
|
+
import type { TokenEditingProfile } from '../authoring.js';
|
|
3
|
+
export type SupportedEditingProfile = Extract<TokenEditingProfile, {
|
|
4
|
+
supported: true;
|
|
5
|
+
}>;
|
|
6
|
+
export declare class TokenMutationLayout {
|
|
7
|
+
readonly documents: TokenMutationDocuments;
|
|
8
|
+
readonly profile: SupportedEditingProfile;
|
|
9
|
+
readonly mode: any;
|
|
10
|
+
readonly modeFiles: Map<string, string>;
|
|
11
|
+
readonly modePath: Array<string | number>;
|
|
12
|
+
constructor(documents: TokenMutationDocuments, profile: SupportedEditingProfile);
|
|
13
|
+
assertModeExists(name: string, message?: string): void;
|
|
14
|
+
ensureModeFile(name: string): Promise<string>;
|
|
15
|
+
throwAmbiguous(): never;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/mutation/layout.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAGxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,mBAAmB,EAAE;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC;AAExF,qBAAa,mBAAmB;IAMxB,QAAQ,CAAC,SAAS,EAAE,sBAAsB;IAC1C,QAAQ,CAAC,OAAO,EAAE,uBAAuB;IAN7C,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IACnB,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;IAE1C,YACa,SAAS,EAAE,sBAAsB,EACjC,OAAO,EAAE,uBAAuB,EAc5C;IAED,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAyB,QAI9D;IAEK,cAAc,CAAC,IAAI,EAAE,MAAM,mBAqBhC;IAED,cAAc,IAAI,KAAK,CAKtB;CACJ"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { TokenMutationError } from './types.js';
|
|
3
|
+
export class TokenMutationLayout {
|
|
4
|
+
documents;
|
|
5
|
+
profile;
|
|
6
|
+
mode;
|
|
7
|
+
modeFiles;
|
|
8
|
+
modePath;
|
|
9
|
+
constructor(documents, profile) {
|
|
10
|
+
this.documents = documents;
|
|
11
|
+
this.profile = profile;
|
|
12
|
+
const resolver = documents.parse(documents.entryFile);
|
|
13
|
+
const modeOrder = resolver.resolutionOrder[1];
|
|
14
|
+
this.mode = modeOrder.$ref ? resolver.modifiers.mode : modeOrder;
|
|
15
|
+
this.modePath = modeOrder.$ref
|
|
16
|
+
? ['modifiers', 'mode']
|
|
17
|
+
: ['resolutionOrder', 1];
|
|
18
|
+
this.modeFiles = new Map(Object.entries(profile.modeFiles).filter((entry) => entry[1] !== null));
|
|
19
|
+
}
|
|
20
|
+
assertModeExists(name, message = 'Mode does not exist.') {
|
|
21
|
+
if (!Object.hasOwn(this.mode.contexts, name)) {
|
|
22
|
+
throw new TokenMutationError('MODE_NOT_FOUND', message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async ensureModeFile(name) {
|
|
26
|
+
const existing = this.modeFiles.get(name);
|
|
27
|
+
if (existing) {
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
const directory = dirname(this.documents.entryFile);
|
|
31
|
+
const encodedName = encodeURIComponent(name);
|
|
32
|
+
const filename = `${encodedName}.tokens.json`;
|
|
33
|
+
const file = directory === '.' ? filename : `${directory}/${filename}`;
|
|
34
|
+
await this.documents.assertAvailable(file);
|
|
35
|
+
this.documents.create(file);
|
|
36
|
+
this.modeFiles.set(name, file);
|
|
37
|
+
this.documents.edit(this.documents.entryFile, [...this.modePath, 'contexts', name], [{ $ref: `${encodeURIComponent(encodedName)}.tokens.json` }]);
|
|
38
|
+
return file;
|
|
39
|
+
}
|
|
40
|
+
throwAmbiguous() {
|
|
41
|
+
throw new TokenMutationError('AMBIGUOUS_SOURCE', 'Structured editing requires one base set followed by one mode modifier, with independent local documents and an empty default context. Edit this layout in JSON.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TokenMutationLayout } from './layout.js';
|
|
2
|
+
import type { TokenCompilation } from '../compilation/compiler.js';
|
|
3
|
+
export declare class TokenModeOperations {
|
|
4
|
+
private readonly layout;
|
|
5
|
+
private readonly compilation;
|
|
6
|
+
constructor(layout: TokenMutationLayout, compilation: TokenCompilation);
|
|
7
|
+
create(name: string): void;
|
|
8
|
+
rename(name: string, nextName: string): void;
|
|
9
|
+
reset(name: string): void;
|
|
10
|
+
delete(name: string): Promise<void>;
|
|
11
|
+
rebase(nextDefault: string): Promise<void>;
|
|
12
|
+
private collectDefinitions;
|
|
13
|
+
private clearTokenNodes;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=mode-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode-operations.d.ts","sourceRoot":"","sources":["../../src/mutation/mode-operations.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,qBAAa,mBAAmB;IAExB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAFhC,YACqB,MAAM,EAAE,mBAAmB,EAC3B,WAAW,EAAE,gBAAgB,EAC9C;IAEJ,MAAM,CAAC,IAAI,EAAE,MAAM,QAelB;IAED,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QA6BpC;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,QAsBjB;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,iBAuBxB;IAEK,MAAM,CAAC,WAAW,EAAE,MAAM,iBA8D/B;IAED,OAAO,CAAC,kBAAkB;IA4B1B,OAAO,CAAC,eAAe;CAoB1B"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { dirname, relative, sep } from 'node:path';
|
|
2
|
+
import { encodeJsonPointer } from '../json-pointer.js';
|
|
3
|
+
import { TokenMutationError } from './types.js';
|
|
4
|
+
export class TokenModeOperations {
|
|
5
|
+
layout;
|
|
6
|
+
compilation;
|
|
7
|
+
constructor(layout, compilation) {
|
|
8
|
+
this.layout = layout;
|
|
9
|
+
this.compilation = compilation;
|
|
10
|
+
}
|
|
11
|
+
create(name) {
|
|
12
|
+
const { documents, mode, modePath } = this.layout;
|
|
13
|
+
if (!name || Object.hasOwn(mode.contexts, name)) {
|
|
14
|
+
throw new TokenMutationError('MODE_EXISTS', 'Choose a new nonempty mode name.');
|
|
15
|
+
}
|
|
16
|
+
documents.edit(documents.entryFile, [...modePath, 'contexts', name], []);
|
|
17
|
+
}
|
|
18
|
+
rename(name, nextName) {
|
|
19
|
+
const { documents, mode, modePath } = this.layout;
|
|
20
|
+
this.layout.assertModeExists(name);
|
|
21
|
+
if (!nextName || Object.hasOwn(mode.contexts, nextName)) {
|
|
22
|
+
throw new TokenMutationError('MODE_EXISTS', 'Choose a new nonempty mode name.');
|
|
23
|
+
}
|
|
24
|
+
documents.edit(documents.entryFile, [...modePath, 'contexts', nextName], mode.contexts[name]);
|
|
25
|
+
documents.edit(documents.entryFile, [...modePath, 'contexts', name], undefined);
|
|
26
|
+
if (mode.default === name) {
|
|
27
|
+
documents.edit(documents.entryFile, [...modePath, 'default'], nextName);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
reset(name) {
|
|
31
|
+
const { documents, mode, modeFiles } = this.layout;
|
|
32
|
+
this.layout.assertModeExists(name);
|
|
33
|
+
if (name === mode.default) {
|
|
34
|
+
throw new TokenMutationError('DEFAULT_RESET', 'The base mode cannot be reset.');
|
|
35
|
+
}
|
|
36
|
+
const file = modeFiles.get(name);
|
|
37
|
+
if (!file) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const extensions = documents.read(file, [])?.$extensions;
|
|
41
|
+
documents.edit(file, [], extensions === undefined ? {} : { $extensions: extensions });
|
|
42
|
+
}
|
|
43
|
+
async delete(name) {
|
|
44
|
+
const { mode } = this.layout;
|
|
45
|
+
this.layout.assertModeExists(name);
|
|
46
|
+
if (name === mode.default) {
|
|
47
|
+
throw new TokenMutationError('DEFAULT_MODE', 'The default mode can be renamed but cannot be deleted.');
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(mode.contexts).length === 1) {
|
|
50
|
+
throw new TokenMutationError('LAST_MODE', 'At least one mode must remain.');
|
|
51
|
+
}
|
|
52
|
+
this.layout.documents.edit(this.layout.documents.entryFile, [...this.layout.modePath, 'contexts', name], undefined);
|
|
53
|
+
}
|
|
54
|
+
async rebase(nextDefault) {
|
|
55
|
+
const { documents, mode, modeFiles, modePath, profile, } = this.layout;
|
|
56
|
+
this.layout.assertModeExists(nextDefault, 'Replacement mode does not exist.');
|
|
57
|
+
if (nextDefault === mode.default) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const definitions = this.collectDefinitions();
|
|
61
|
+
const nextTokens = definitions.get(nextDefault);
|
|
62
|
+
const previousOverrideFile = modeFiles.get(nextDefault);
|
|
63
|
+
if (previousOverrideFile) {
|
|
64
|
+
modeFiles.set(mode.default, previousOverrideFile);
|
|
65
|
+
const reference = relative(dirname(documents.entryFile), previousOverrideFile).split(sep).map(encodeURIComponent).join('/');
|
|
66
|
+
documents.edit(documents.entryFile, [...modePath, 'contexts', mode.default], [{ $ref: reference }]);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
modeFiles.delete(mode.default);
|
|
70
|
+
}
|
|
71
|
+
for (const [modeName, tokens] of definitions) {
|
|
72
|
+
const file = modeName === nextDefault
|
|
73
|
+
? profile.baseFile
|
|
74
|
+
: await this.layout.ensureModeFile(modeName);
|
|
75
|
+
this.clearTokenNodes(file, documents.parse(file));
|
|
76
|
+
for (const [path, raw] of tokens) {
|
|
77
|
+
const inherited = nextTokens.get(path);
|
|
78
|
+
if (modeName === nextDefault || JSON.stringify(raw) !== JSON.stringify(inherited)) {
|
|
79
|
+
documents.edit(file, path.split('.'), raw);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
documents.edit(documents.entryFile, [...modePath, 'contexts', nextDefault], []);
|
|
84
|
+
documents.edit(documents.entryFile, [...modePath, 'default'], nextDefault);
|
|
85
|
+
}
|
|
86
|
+
collectDefinitions() {
|
|
87
|
+
const { documents, modeFiles } = this.layout;
|
|
88
|
+
const sourceFiles = [...modeFiles.values()];
|
|
89
|
+
const definitions = new Map();
|
|
90
|
+
for (const permutation of this.compilation.permutations) {
|
|
91
|
+
const tokens = new Map();
|
|
92
|
+
for (const token of permutation.tokens) {
|
|
93
|
+
const path = token.path.split('.');
|
|
94
|
+
const sourceOwned = token.source.pointer === encodeJsonPointer(path)
|
|
95
|
+
&& sourceFiles.includes(token.source.file);
|
|
96
|
+
if (!sourceOwned) {
|
|
97
|
+
this.layout.throwAmbiguous();
|
|
98
|
+
}
|
|
99
|
+
tokens.set(token.path, {
|
|
100
|
+
...documents.read(token.source.file, path),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
definitions.set(permutation.input.mode, tokens);
|
|
104
|
+
}
|
|
105
|
+
return definitions;
|
|
106
|
+
}
|
|
107
|
+
clearTokenNodes(file, node, path = []) {
|
|
108
|
+
if (!node || typeof node !== 'object') {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if ('$extends' in node) {
|
|
112
|
+
this.layout.throwAmbiguous();
|
|
113
|
+
}
|
|
114
|
+
if ('$value' in node) {
|
|
115
|
+
this.layout.documents.edit(file, path, undefined);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const [key, value] of Object.entries(node)) {
|
|
119
|
+
if (!key.startsWith('$') || key === '$root') {
|
|
120
|
+
this.clearTokenNodes(file, value, [...path, key]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TokenCompilationOptions } from '../compilation/compiler.js';
|
|
2
|
+
import type { TokenMutation } from './types.js';
|
|
3
|
+
export { TokenMutationError } from './types.js';
|
|
4
|
+
export type { TokenFileChange, TokenMutation } from './types.js';
|
|
5
|
+
/** Prepare and validate source edits without writing to the workspace. */
|
|
6
|
+
export declare function planTokenMutation(options: Omit<TokenCompilationOptions, 'documents'>, mutation: TokenMutation): Promise<{
|
|
7
|
+
compilation: import("../compilation/compiler.js").TokenCompilation;
|
|
8
|
+
changes: import("./types.js").TokenFileChange[];
|
|
9
|
+
revision: string;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=mutations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../../src/mutation/mutations.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEjE,0EAA0E;AAC1E,wBAAsB,iBAAiB,CACnC,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,WAAW,CAAC,EACnD,QAAQ,EAAE,aAAa;;;;GAqD1B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { compileProjectTokens } from '../compilation/compiler.js';
|
|
2
|
+
import { TokenMutationDocuments } from './documents.js';
|
|
3
|
+
import { TokenMutationPlanner } from './planner.js';
|
|
4
|
+
import { TokenMutationError } from './types.js';
|
|
5
|
+
import { validateMutationResult } from './validation.js';
|
|
6
|
+
export { TokenMutationError } from './types.js';
|
|
7
|
+
/** Prepare and validate source edits without writing to the workspace. */
|
|
8
|
+
export async function planTokenMutation(options, mutation) {
|
|
9
|
+
const current = await compileProjectTokens(options);
|
|
10
|
+
if (!current.ok) {
|
|
11
|
+
throw new TokenMutationError('INVALID_SOURCE', 'Fix token diagnostics before structured editing.', current);
|
|
12
|
+
}
|
|
13
|
+
if (!current.editing?.supported) {
|
|
14
|
+
const message = current.editing?.reason.message
|
|
15
|
+
?? 'The source layout is unavailable for editing.';
|
|
16
|
+
throw new TokenMutationError('AMBIGUOUS_SOURCE', message);
|
|
17
|
+
}
|
|
18
|
+
const documents = await TokenMutationDocuments.load(options, current);
|
|
19
|
+
const planner = new TokenMutationPlanner(options, current, documents, current.editing);
|
|
20
|
+
const changed = await planner.apply(mutation);
|
|
21
|
+
if (!changed) {
|
|
22
|
+
return {
|
|
23
|
+
compilation: current,
|
|
24
|
+
changes: [],
|
|
25
|
+
revision: current.revision,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const compilation = await compileProjectTokens({
|
|
29
|
+
...options,
|
|
30
|
+
documents: documents.values,
|
|
31
|
+
});
|
|
32
|
+
if (!compilation.ok) {
|
|
33
|
+
throw new TokenMutationError('INVALID_MUTATION', 'The edit does not compile in every mode.', compilation);
|
|
34
|
+
}
|
|
35
|
+
validateMutationResult(current, compilation, mutation);
|
|
36
|
+
return {
|
|
37
|
+
compilation,
|
|
38
|
+
changes: documents.changes(),
|
|
39
|
+
revision: current.revision,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type SupportedEditingProfile } from './layout.js';
|
|
2
|
+
import type { TokenCompilation, TokenCompilationOptions } from '../compilation/compiler.js';
|
|
3
|
+
import type { TokenMutationDocuments } from './documents.js';
|
|
4
|
+
import type { TokenMutation } from './types.js';
|
|
5
|
+
export declare class TokenMutationPlanner {
|
|
6
|
+
private readonly modes;
|
|
7
|
+
private readonly tokens;
|
|
8
|
+
constructor(options: Omit<TokenCompilationOptions, 'documents'>, compilation: TokenCompilation, documents: TokenMutationDocuments, profile: SupportedEditingProfile);
|
|
9
|
+
apply(mutation: TokenMutation): Promise<boolean>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=planner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/mutation/planner.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAKhF,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,qBAAa,oBAAoB;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IAEzC,YACI,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,WAAW,CAAC,EACnD,WAAW,EAAE,gBAAgB,EAC7B,SAAS,EAAE,sBAAsB,EACjC,OAAO,EAAE,uBAAuB,EAKnC;IAEK,KAAK,CAAC,QAAQ,EAAE,aAAa,oBAkDlC;CACJ"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isDtcgTokenType } from '../compilation/scope-validation.js';
|
|
2
|
+
import { TokenMutationLayout } from './layout.js';
|
|
3
|
+
import { TokenModeOperations } from './mode-operations.js';
|
|
4
|
+
import { TokenOperations, validateTokenPath } from './token-operations.js';
|
|
5
|
+
import { TokenMutationError } from './types.js';
|
|
6
|
+
export class TokenMutationPlanner {
|
|
7
|
+
modes;
|
|
8
|
+
tokens;
|
|
9
|
+
constructor(options, compilation, documents, profile) {
|
|
10
|
+
const layout = new TokenMutationLayout(documents, profile);
|
|
11
|
+
this.modes = new TokenModeOperations(layout, compilation);
|
|
12
|
+
this.tokens = new TokenOperations(options, compilation, layout);
|
|
13
|
+
}
|
|
14
|
+
async apply(mutation) {
|
|
15
|
+
const path = 'path' in mutation
|
|
16
|
+
? validateTokenPath(mutation.path)
|
|
17
|
+
: [];
|
|
18
|
+
switch (mutation.kind) {
|
|
19
|
+
case 'mode.create':
|
|
20
|
+
this.modes.create(mutation.mode);
|
|
21
|
+
return true;
|
|
22
|
+
case 'mode.default':
|
|
23
|
+
await this.modes.rebase(mutation.mode);
|
|
24
|
+
return true;
|
|
25
|
+
case 'mode.rename':
|
|
26
|
+
this.modes.rename(mutation.mode, mutation.to);
|
|
27
|
+
return true;
|
|
28
|
+
case 'mode.reset':
|
|
29
|
+
this.modes.reset(mutation.mode);
|
|
30
|
+
return true;
|
|
31
|
+
case 'mode.delete':
|
|
32
|
+
await this.modes.delete(mutation.mode);
|
|
33
|
+
return true;
|
|
34
|
+
case 'token.rename':
|
|
35
|
+
case 'group.rename':
|
|
36
|
+
this.tokens.rename(mutation, path);
|
|
37
|
+
return true;
|
|
38
|
+
case 'group.create':
|
|
39
|
+
this.tokens.createGroup(path);
|
|
40
|
+
return true;
|
|
41
|
+
case 'scope.create':
|
|
42
|
+
if (!isDtcgTokenType(mutation.type)) {
|
|
43
|
+
throw new TokenMutationError('INVALID_SCOPE', 'Choose a standard DTCG type.');
|
|
44
|
+
}
|
|
45
|
+
this.tokens.createGroup(path, mutation.type);
|
|
46
|
+
return true;
|
|
47
|
+
case 'group.delete':
|
|
48
|
+
this.tokens.deleteGroup(mutation, path);
|
|
49
|
+
return true;
|
|
50
|
+
case 'token.duplicate':
|
|
51
|
+
this.tokens.duplicate(path, validateTokenPath(mutation.to));
|
|
52
|
+
return true;
|
|
53
|
+
case 'token.delete':
|
|
54
|
+
this.tokens.deleteToken(mutation.path, path);
|
|
55
|
+
return true;
|
|
56
|
+
case 'token.create':
|
|
57
|
+
case 'token.reset':
|
|
58
|
+
case 'token.update':
|
|
59
|
+
return this.tokens.edit(mutation, path);
|
|
60
|
+
default:
|
|
61
|
+
return assertNever(mutation);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function assertNever(value) {
|
|
66
|
+
throw new TokenMutationError('INVALID_MUTATION', `Unsupported token mutation: ${JSON.stringify(value)}`);
|
|
67
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TokenMutationDocuments } from './documents.js';
|
|
2
|
+
import type { TokenCompilation, TokenCompilationOptions } from '../compilation/compiler.js';
|
|
3
|
+
import type { TokenMutation } from './types.js';
|
|
4
|
+
type RenameMutation = Extract<TokenMutation, {
|
|
5
|
+
kind: 'token.rename' | 'group.rename';
|
|
6
|
+
}>;
|
|
7
|
+
export declare class TokenReferenceRenamer {
|
|
8
|
+
private readonly options;
|
|
9
|
+
private readonly compilation;
|
|
10
|
+
private readonly documents;
|
|
11
|
+
private readonly modeFiles;
|
|
12
|
+
constructor(options: Omit<TokenCompilationOptions, 'documents'>, compilation: TokenCompilation, documents: TokenMutationDocuments, modeFiles: ReadonlyMap<string, string>);
|
|
13
|
+
rename(mutation: RenameMutation, sourcePath: string[], destinationPath: string[]): void;
|
|
14
|
+
private assertSource;
|
|
15
|
+
private moveDefinition;
|
|
16
|
+
private assertDestinationParents;
|
|
17
|
+
private findCompiledSource;
|
|
18
|
+
private assertScopeMove;
|
|
19
|
+
private rewriteReferences;
|
|
20
|
+
private rewriteDocumentReferences;
|
|
21
|
+
private renameReference;
|
|
22
|
+
private throwAmbiguous;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=reference-renamer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-renamer.d.ts","sourceRoot":"","sources":["../../src/mutation/reference-renamer.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,KAAK,cAAc,GAAG,OAAO,CAAC,aAAa,EAAE;IAAE,IAAI,EAAE,cAAc,GAAG,cAAc,CAAA;CAAE,CAAC,CAAC;AAExF,qBAAa,qBAAqB;IAE1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAJ9B,YACqB,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,WAAW,CAAC,EACnD,WAAW,EAAE,gBAAgB,EAC7B,SAAS,EAAE,sBAAsB,EACjC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACvD;IAEJ,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE,QAmC/E;IAED,OAAO,CAAC,YAAY;IAqBpB,OAAO,CAAC,cAAc;IAkBtB,OAAO,CAAC,wBAAwB;IAUhC,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,yBAAyB;IAwEjC,OAAO,CAAC,eAAe;IA0DvB,OAAO,CAAC,cAAc;CAMzB"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { relative, resolve, sep } from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import { decodeJsonPointer, encodeJsonPointerPart, } from '../json-pointer.js';
|
|
4
|
+
import { TokenMutationError } from './types.js';
|
|
5
|
+
export class TokenReferenceRenamer {
|
|
6
|
+
options;
|
|
7
|
+
compilation;
|
|
8
|
+
documents;
|
|
9
|
+
modeFiles;
|
|
10
|
+
constructor(options, compilation, documents, modeFiles) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.compilation = compilation;
|
|
13
|
+
this.documents = documents;
|
|
14
|
+
this.modeFiles = modeFiles;
|
|
15
|
+
}
|
|
16
|
+
rename(mutation, sourcePath, destinationPath) {
|
|
17
|
+
const group = mutation.kind === 'group.rename';
|
|
18
|
+
const files = [...this.modeFiles.values()].filter(file => (this.documents.read(file, sourcePath) !== undefined));
|
|
19
|
+
this.assertSource(files, sourcePath, group);
|
|
20
|
+
if (mutation.path === mutation.to) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (group && mutation.to.startsWith(`${mutation.path}.`)) {
|
|
24
|
+
throw new TokenMutationError('INVALID_PATH', 'A group cannot move into itself.');
|
|
25
|
+
}
|
|
26
|
+
if ([...this.modeFiles.values()].some(file => (this.documents.read(file, destinationPath) !== undefined))) {
|
|
27
|
+
throw new TokenMutationError('PATH_EXISTS', 'The destination already exists.');
|
|
28
|
+
}
|
|
29
|
+
this.assertScopeMove(sourcePath, destinationPath, group);
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
this.moveDefinition(file, sourcePath, destinationPath, mutation, group);
|
|
32
|
+
}
|
|
33
|
+
this.rewriteReferences(files, sourcePath, destinationPath);
|
|
34
|
+
}
|
|
35
|
+
assertSource(files, sourcePath, group) {
|
|
36
|
+
if (files.length === 0) {
|
|
37
|
+
throw new TokenMutationError(group ? 'GROUP_NOT_FOUND' : 'TOKEN_NOT_FOUND', 'The source path does not exist.');
|
|
38
|
+
}
|
|
39
|
+
const wrongKind = files.some(file => {
|
|
40
|
+
const source = this.documents.read(file, sourcePath);
|
|
41
|
+
return ('$value' in source) === group;
|
|
42
|
+
});
|
|
43
|
+
if (wrongKind) {
|
|
44
|
+
throw new TokenMutationError('INVALID_PATH', 'The source must match the requested token or group kind.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
moveDefinition(file, sourcePath, destinationPath, mutation, group) {
|
|
48
|
+
this.assertDestinationParents(file, destinationPath);
|
|
49
|
+
const moved = this.documents.read(file, sourcePath);
|
|
50
|
+
const original = this.findCompiledSource(file, mutation.path, group);
|
|
51
|
+
moved.$deprecated ??= original.deprecated;
|
|
52
|
+
this.documents.edit(file, destinationPath, moved);
|
|
53
|
+
this.documents.edit(file, sourcePath, undefined);
|
|
54
|
+
}
|
|
55
|
+
assertDestinationParents(file, destinationPath) {
|
|
56
|
+
for (let index = 1; index < destinationPath.length; index++) {
|
|
57
|
+
const parent = this.documents.read(file, destinationPath.slice(0, index));
|
|
58
|
+
if (parent && ('$value' in parent || '$extends' in parent)) {
|
|
59
|
+
this.throwAmbiguous();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
findCompiledSource(file, path, group) {
|
|
64
|
+
const permutation = this.compilation.permutations.find(candidate => (this.modeFiles.get(candidate.input.mode) === file));
|
|
65
|
+
const nodes = group ? permutation?.groups : permutation?.tokens;
|
|
66
|
+
const source = nodes?.find(node => node.path === path);
|
|
67
|
+
if (!source) {
|
|
68
|
+
this.throwAmbiguous();
|
|
69
|
+
}
|
|
70
|
+
return source;
|
|
71
|
+
}
|
|
72
|
+
assertScopeMove(source, destination, group) {
|
|
73
|
+
if (group && source.length === 1 && destination.length === 1)
|
|
74
|
+
return;
|
|
75
|
+
if (group && (source.length === 1 || destination.length === 1 || source[0] !== destination[0])) {
|
|
76
|
+
throw new TokenMutationError('INVALID_SCOPE', 'Nested groups must stay inside their owning scope.');
|
|
77
|
+
}
|
|
78
|
+
const groups = this.compilation.permutations[0]?.groups ?? [];
|
|
79
|
+
const from = groups.find(candidate => candidate.path === source[0]);
|
|
80
|
+
const to = groups.find(candidate => candidate.path === destination[0]);
|
|
81
|
+
const parent = groups.find(candidate => candidate.path === destination.slice(0, -1).join('.'));
|
|
82
|
+
if (destination.length < 2 || !to?.type || !parent) {
|
|
83
|
+
throw new TokenMutationError('INVALID_SCOPE', 'Choose an existing group inside a typed scope.');
|
|
84
|
+
}
|
|
85
|
+
if (from?.type !== to.type) {
|
|
86
|
+
throw new TokenMutationError('SCOPE_TYPE_MISMATCH', `Cannot move ${from?.type} tokens into a ${to.type} scope.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
rewriteReferences(sourceFiles, sourcePath, destinationPath) {
|
|
90
|
+
for (const file of this.documents.files) {
|
|
91
|
+
if (file === this.documents.entryFile) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
this.rewriteDocumentReferences(file, sourceFiles, sourcePath, destinationPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
rewriteDocumentReferences(file, sourceFiles, sourcePath, destinationPath) {
|
|
98
|
+
const rewriteReference = (value, pointer) => this.renameReference(value, file, pointer, sourceFiles, sourcePath, destinationPath);
|
|
99
|
+
const visitValue = (node, path) => {
|
|
100
|
+
if (typeof node === 'string') {
|
|
101
|
+
const renamed = rewriteReference(node, false);
|
|
102
|
+
if (renamed !== node) {
|
|
103
|
+
this.documents.edit(file, path, renamed);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!node || typeof node !== 'object') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const [key, value] of Object.entries(node)) {
|
|
111
|
+
if (key === '$extensions') {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const nextPath = [...path, Array.isArray(node) ? Number(key) : key];
|
|
115
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
116
|
+
const renamed = rewriteReference(value, true);
|
|
117
|
+
if (renamed !== value) {
|
|
118
|
+
this.documents.edit(file, nextPath, renamed);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
visitValue(value, nextPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const visitGroups = (node, path = []) => {
|
|
127
|
+
if (!node || typeof node !== 'object') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if ('$value' in node) {
|
|
131
|
+
visitValue(node.$value, [...path, '$value']);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const [key, value] of Object.entries(node)) {
|
|
135
|
+
if (key === '$extends' && typeof value === 'string') {
|
|
136
|
+
const renamed = rewriteReference(value, !value.startsWith('{'));
|
|
137
|
+
if (renamed !== value) {
|
|
138
|
+
this.documents.edit(file, [...path, key], renamed);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (!key.startsWith('$') || key === '$root') {
|
|
142
|
+
visitGroups(value, [...path, key]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
visitGroups(this.documents.parse(file));
|
|
147
|
+
}
|
|
148
|
+
renameReference(value, file, pointer, sourceFiles, sourcePath, destinationPath) {
|
|
149
|
+
const renamePath = (path) => {
|
|
150
|
+
const matches = sourcePath.every((part, index) => path[index] === part);
|
|
151
|
+
return matches
|
|
152
|
+
? [...destinationPath, ...path.slice(sourcePath.length)]
|
|
153
|
+
: path;
|
|
154
|
+
};
|
|
155
|
+
if (!pointer) {
|
|
156
|
+
if (!/^\{[^{}]+\}$/.test(value)) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
const path = value.slice(1, -1).split('.');
|
|
160
|
+
return `{${renamePath(path).join('.')}}`;
|
|
161
|
+
}
|
|
162
|
+
const url = new URL(value, pathToFileURL(resolve(this.options.workspaceRoot, file)));
|
|
163
|
+
const target = new URL(url);
|
|
164
|
+
target.hash = '';
|
|
165
|
+
const targetFile = relative(resolve(this.options.workspaceRoot), fileURLToPath(target)).split(sep).join('/');
|
|
166
|
+
if (!value.startsWith('#') && !sourceFiles.includes(targetFile)) {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
const fragment = decodeURIComponent(url.hash.slice(1));
|
|
170
|
+
if (!fragment.startsWith('/')) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
const path = decodeJsonPointer(fragment);
|
|
174
|
+
const renamed = renamePath(path);
|
|
175
|
+
if (renamed === path) {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
const encoded = renamed
|
|
179
|
+
.map(part => `/${encodeURIComponent(encodeJsonPointerPart(part))}`)
|
|
180
|
+
.join('');
|
|
181
|
+
return `${value.split('#')[0]}#${encoded}`;
|
|
182
|
+
}
|
|
183
|
+
throwAmbiguous() {
|
|
184
|
+
throw new TokenMutationError('AMBIGUOUS_SOURCE', 'Structured editing requires independent local token documents. Edit this layout in JSON.');
|
|
185
|
+
}
|
|
186
|
+
}
|