@khaledeltohamy14/token-pipeline-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,355 @@
1
+ const fs = require('fs');
2
+
3
+ function readJSON(filePath) {
4
+ if (!fs.existsSync(filePath)) {
5
+ throw new Error(`❌ File not found: ${filePath}`);
6
+ }
7
+
8
+ try {
9
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
10
+ } catch (error) {
11
+ throw new Error(
12
+ `❌ Invalid JSON: ${filePath}\n${error.message}`
13
+ );
14
+ }
15
+ }
16
+
17
+ function ensureDir(dir) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+
21
+ function isObject(value) {
22
+ return value !== null &&
23
+ typeof value === 'object' &&
24
+ !Array.isArray(value);
25
+ }
26
+
27
+ function isToken(value) {
28
+ return isObject(value) &&
29
+ ('$value' in value || 'value' in value);
30
+ }
31
+
32
+ function getTokenValue(token) {
33
+ return '$value' in token
34
+ ? token.$value
35
+ : token.value;
36
+ }
37
+
38
+ function getTokenType(token) {
39
+ return token.$type || token.type;
40
+ }
41
+
42
+ function isReference(value) {
43
+ return typeof value === 'string' &&
44
+ value.startsWith('{') &&
45
+ value.endsWith('}');
46
+ }
47
+
48
+ function cleanReference(value) {
49
+ return value
50
+ .replace(/^\{/, '')
51
+ .replace(/\}$/, '')
52
+ .trim();
53
+ }
54
+
55
+ /*
56
+ |--------------------------------------------------------------------------
57
+ | Dart naming
58
+ |--------------------------------------------------------------------------
59
+ |
60
+ | JSON naming stays untouched.
61
+ |
62
+ | Dart:
63
+ |
64
+ | red_500 -> red500
65
+ | card_background -> cardBackground
66
+ | textfieldcomponent_padding -> textfieldComponentPadding
67
+ | topRightButtonsComponent -> topRightButtonsComponent
68
+ | toprightbuttonscomponent -> topRightButtonsComponent
69
+ |
70
+ |--------------------------------------------------------------------------
71
+ */
72
+
73
+ const KNOWN_COMPOUND_NAMES = {
74
+ toprightbuttons: 'topRightButtons',
75
+ textfield: 'textfield',
76
+ button: 'button',
77
+ radio: 'radio',
78
+ card: 'card',
79
+ };
80
+
81
+ function normalizeCompoundWord(word) {
82
+ const lower = word.toLowerCase();
83
+
84
+ /*
85
+ * Handle:
86
+ *
87
+ * textfieldcomponent
88
+ * buttoncomponent
89
+ * radiocomponent
90
+ * cardcomponent
91
+ * toprightbuttonscomponent
92
+ */
93
+ if (lower.endsWith('component')) {
94
+ const base = lower.substring(
95
+ 0,
96
+ lower.length - 'component'.length
97
+ );
98
+
99
+ const normalizedBase =
100
+ KNOWN_COMPOUND_NAMES[base] || base;
101
+
102
+ return `${normalizedBase}Component`;
103
+ }
104
+
105
+ return KNOWN_COMPOUND_NAMES[lower] || lower;
106
+ }
107
+
108
+ function splitIdentifier(value) {
109
+ return String(value)
110
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
111
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
112
+ .replace(/[_./\\\-]+/g, ' ')
113
+ .trim()
114
+ .split(/\s+/)
115
+ .filter(Boolean);
116
+ }
117
+
118
+ function toDartIdentifier(value) {
119
+ const words = splitIdentifier(value);
120
+
121
+ if (!words.length) {
122
+ return '_token';
123
+ }
124
+
125
+ /*
126
+ * First normalize compound component words.
127
+ */
128
+ const normalizedWords = [];
129
+
130
+ for (finalWord of words) {
131
+ const normalized = normalizeCompoundWord(finalWord);
132
+
133
+ /*
134
+ * A word such as:
135
+ *
136
+ * topRightButtonsComponent
137
+ *
138
+ * can already contain camel casing.
139
+ */
140
+ const nested = normalized
141
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
142
+ .split(/\s+/);
143
+
144
+ normalizedWords.push(...nested);
145
+ }
146
+
147
+ const first = normalizedWords[0].toLowerCase();
148
+
149
+ const rest = normalizedWords
150
+ .slice(1)
151
+ .map(word => {
152
+ const lower = word.toLowerCase();
153
+
154
+ return lower.charAt(0).toUpperCase() +
155
+ lower.slice(1);
156
+ })
157
+ .join('');
158
+
159
+ let result = first + rest;
160
+
161
+ if (/^\d/.test(result)) {
162
+ result = `_${result}`;
163
+ }
164
+
165
+ return result;
166
+ }
167
+
168
+ /*
169
+ * Same semantic key for duplicate detection.
170
+ *
171
+ * buttonComponent
172
+ * buttoncomponent
173
+ *
174
+ * => buttoncomponent
175
+ */
176
+ function canonicalTokenKey(value) {
177
+ return String(value)
178
+ .replace(/[^a-zA-Z0-9]/g, '')
179
+ .toLowerCase();
180
+ }
181
+
182
+ const toSafeName = toDartIdentifier;
183
+
184
+ function flattenTokens(
185
+ object,
186
+ prefix = '',
187
+ result = {}
188
+ ) {
189
+ for (const [key, value] of Object.entries(object || {})) {
190
+ const currentPath =
191
+ prefix
192
+ ? `${prefix}.${key}`
193
+ : key;
194
+
195
+ if (isToken(value)) {
196
+ result[currentPath] = value;
197
+ } else if (isObject(value)) {
198
+ flattenTokens(
199
+ value,
200
+ currentPath,
201
+ result
202
+ );
203
+ }
204
+ }
205
+
206
+ return result;
207
+ }
208
+
209
+ function colorValue(value) {
210
+ if (
211
+ typeof value !== 'string' ||
212
+ !value.startsWith('#')
213
+ ) {
214
+ return null;
215
+ }
216
+
217
+ let hex = value
218
+ .replace('#', '')
219
+ .toUpperCase();
220
+
221
+ if (hex.length === 3) {
222
+ hex = hex
223
+ .split('')
224
+ .map(x => x + x)
225
+ .join('');
226
+ }
227
+
228
+ if (hex.length === 6) {
229
+ return `Color(0xFF${hex})`;
230
+ }
231
+
232
+ if (hex.length === 8) {
233
+ return `Color(0x${hex})`;
234
+ }
235
+
236
+ return null;
237
+ }
238
+
239
+ function dimensionValue(value) {
240
+ if (typeof value !== 'string') {
241
+ return null;
242
+ }
243
+
244
+ const match = value.match(
245
+ /^(-?\d+(?:\.\d+)?)px$/
246
+ );
247
+
248
+ return match ? match[1] : null;
249
+ }
250
+
251
+ function dartType(type) {
252
+ const types = {
253
+ color: 'Color',
254
+ dimension: 'double',
255
+ number: 'double',
256
+ string: 'String',
257
+ boolean: 'bool',
258
+ };
259
+
260
+ if (!types[type]) {
261
+ throw new Error(
262
+ `Unsupported type: ${type}`
263
+ );
264
+ }
265
+
266
+ return types[type];
267
+ }
268
+
269
+ function dartLiteral(
270
+ value,
271
+ type,
272
+ tokenPath = 'token'
273
+ ) {
274
+ if (type === 'color') {
275
+ const color = colorValue(value);
276
+
277
+ if (!color) {
278
+ throw new Error(
279
+ `❌ Invalid color token: ${tokenPath}\n` +
280
+ `Value: ${value}`
281
+ );
282
+ }
283
+
284
+ return {
285
+ type: 'Color',
286
+ value: color,
287
+ };
288
+ }
289
+
290
+ if (type === 'dimension') {
291
+ const dimension =
292
+ dimensionValue(value);
293
+
294
+ if (dimension === null) {
295
+ throw new Error(
296
+ `❌ Invalid dimension token: ${tokenPath}\n` +
297
+ `Value: ${value}`
298
+ );
299
+ }
300
+
301
+ return {
302
+ type: 'double',
303
+ value: dimension,
304
+ };
305
+ }
306
+
307
+ if (type === 'number') {
308
+ return {
309
+ type: 'double',
310
+ value: String(value),
311
+ };
312
+ }
313
+
314
+ if (type === 'string') {
315
+ return {
316
+ type: 'String',
317
+ value: JSON.stringify(
318
+ String(value)
319
+ ),
320
+ };
321
+ }
322
+
323
+ if (type === 'boolean') {
324
+ return {
325
+ type: 'bool',
326
+ value: value
327
+ ? 'true'
328
+ : 'false',
329
+ };
330
+ }
331
+
332
+ throw new Error(
333
+ `❌ Unsupported token type: ${type}\n` +
334
+ `Token: ${tokenPath}`
335
+ );
336
+ }
337
+
338
+ module.exports = {
339
+ readJSON,
340
+ ensureDir,
341
+ isObject,
342
+ isToken,
343
+ getTokenValue,
344
+ getTokenType,
345
+ isReference,
346
+ cleanReference,
347
+ toSafeName,
348
+ toDartIdentifier,
349
+ canonicalTokenKey,
350
+ flattenTokens,
351
+ colorValue,
352
+ dimensionValue,
353
+ dartType,
354
+ dartLiteral,
355
+ };
package/index.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type TokenPipelineGeneratedFile = {
2
+ path: string;
3
+ content: string;
4
+ mode: '100644' | '100755';
5
+ };
6
+
7
+ export type GenerateTokenFilesOptions = {
8
+ document: Record<string, unknown>;
9
+ outputRoot?: string;
10
+ };
11
+
12
+ export type GenerateRepositoryFilesOptions = {
13
+ document: Record<string, unknown>;
14
+ outputRoot?: string;
15
+ tokenRoot?: string;
16
+ sourceTokenPath?: string;
17
+ };
18
+
19
+ export function splitTokenDocument(
20
+ document: Record<string, unknown>,
21
+ ): Record<string, unknown>;
22
+
23
+ export function splitTokenFiles(
24
+ document: Record<string, unknown>,
25
+ root?: string,
26
+ ): TokenPipelineGeneratedFile[];
27
+
28
+ export function generateTokenFiles(
29
+ options: GenerateTokenFilesOptions,
30
+ ): TokenPipelineGeneratedFile[];
31
+
32
+ export function generateRepositoryFiles(
33
+ options: GenerateRepositoryFilesOptions,
34
+ ): TokenPipelineGeneratedFile[];
package/index.js ADDED
@@ -0,0 +1,148 @@
1
+ const { splitTokenDocument, splitTokenFiles } = require('./split/split-token-document');
2
+ const { renderGlobalTokens } = require('./generators/global-generator');
3
+ const { renderAliasTokens } = require('./generators/alias-generator');
4
+ const { renderCategoryTokens } = require('./generators/category-generator');
5
+ const { renderComponentTokens } = require('./generators/component-generator');
6
+ const { renderBrandTokens } = require('./generators/brand-generator');
7
+ const { renderTheme, renderThemeTokens } = require('./generators/theme-generator');
8
+
9
+ const CATEGORY_CONFIG = [
10
+ ['spacing', 'SpacingTokens'],
11
+ ['typography', 'TypographyTokens'],
12
+ ['radius', 'RadiusTokens'],
13
+ ['shadow', 'ShadowTokens'],
14
+ ['dimensions', 'DimensionTokens'],
15
+ ];
16
+
17
+ function asRecord(value) {
18
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
19
+ }
20
+
21
+ function normalizeRelativePath(value) {
22
+ const normalized = String(value || '').trim().replace(/^\/+/, '').replace(/\\/g, '/');
23
+ if (!normalized || normalized.includes('..') || normalized.split('/').some((part) => !part)) {
24
+ throw new Error(`Invalid generated output path: ${value}`);
25
+ }
26
+ return normalized;
27
+ }
28
+
29
+ function joinPath(root, file) {
30
+ const cleanRoot = String(root || '').trim().replace(/^\/+|\/+$/g, '');
31
+ return cleanRoot ? `${cleanRoot}/${file}` : file;
32
+ }
33
+
34
+ function brandClassName(brandName) {
35
+ return String(brandName).split(/[_-]/).map((value) => value.charAt(0).toUpperCase() + value.slice(1)).join('') + 'Tokens';
36
+ }
37
+
38
+ function brandThemeClassName(brandName) {
39
+ return String(brandName).split(/[_-]/).map((value) => value.charAt(0).toUpperCase() + value.slice(1)).join('') + 'Theme';
40
+ }
41
+
42
+ function generateTokenFiles({
43
+ document,
44
+ outputRoot = 'packages/tokens/lib',
45
+ }) {
46
+ const source = asRecord(document);
47
+ const tokens = asRecord(source.tokens);
48
+ const globalTokens = asRecord(tokens.global);
49
+ const aliasTokens = asRecord(tokens.alias);
50
+ const componentTokens = asRecord(tokens.components);
51
+ const spacingTokens = asRecord(tokens.spacing);
52
+ const typographyTokens = asRecord(tokens.typography);
53
+ const radiusTokens = asRecord(tokens.radius);
54
+ const shadowTokens = asRecord(tokens.shadow);
55
+ const dimensionTokens = asRecord(tokens.dimensions);
56
+ const brands = asRecord(tokens.brands);
57
+
58
+ const root = normalizeRelativePath(outputRoot);
59
+ const files = [];
60
+ const add = (path, content) => files.push({ path: joinPath(root, path), content, mode: '100644' });
61
+
62
+ add('global/global_tokens.dart', renderGlobalTokens({ tokens: globalTokens }));
63
+ add('alias/alias_tokens.dart', renderAliasTokens({ tokens: aliasTokens, globalTokens }));
64
+
65
+ for (const [name, className] of CATEGORY_CONFIG) {
66
+ add(`${name}/${name}_tokens.dart`, renderCategoryTokens({
67
+ tokens: asRecord(tokens[name]),
68
+ globalTokens,
69
+ aliasTokens,
70
+ className,
71
+ }));
72
+ }
73
+
74
+ add('components/component_tokens.dart', renderComponentTokens({
75
+ tokens: componentTokens,
76
+ aliasTokens,
77
+ globalTokens,
78
+ spacingTokens,
79
+ radiusTokens,
80
+ dimensionTokens,
81
+ typographyTokens,
82
+ shadowTokens,
83
+ }));
84
+
85
+ const brandNames = Object.keys(brands).sort();
86
+ const brandMap = {};
87
+ for (const brandName of brandNames) {
88
+ const brandTokens = asRecord(brands[brandName]);
89
+ brandMap[brandName] = brandTokens;
90
+ add(`brands/${brandName}_tokens.dart`, renderBrandTokens({
91
+ brandTokens,
92
+ aliasTokens,
93
+ globalTokens,
94
+ className: brandClassName(brandName),
95
+ }));
96
+ add(`themes/${brandName}_theme.dart`, renderTheme({
97
+ aliasTokens,
98
+ globalTokens,
99
+ componentTokens,
100
+ spacingTokens,
101
+ radiusTokens,
102
+ dimensionTokens,
103
+ typographyTokens,
104
+ shadowTokens,
105
+ brandTokens,
106
+ className: brandThemeClassName(brandName),
107
+ }));
108
+ }
109
+
110
+ add('themes/theme_tokens.dart', renderThemeTokens({
111
+ aliasTokens,
112
+ globalTokens,
113
+ componentTokens,
114
+ spacingTokens,
115
+ radiusTokens,
116
+ dimensionTokens,
117
+ typographyTokens,
118
+ shadowTokens,
119
+ brands: brandMap,
120
+ }));
121
+
122
+ return files;
123
+ }
124
+
125
+ function generateRepositoryFiles({
126
+ document,
127
+ outputRoot = 'packages/tokens/lib',
128
+ tokenRoot = 'token_pipeline/tokens',
129
+ sourceTokenPath = 'token_pipeline/tokens.json',
130
+ }) {
131
+ const files = [];
132
+ const sourcePath = normalizeRelativePath(sourceTokenPath);
133
+ files.push({
134
+ path: sourcePath,
135
+ content: `${JSON.stringify(document, null, 2)}\n`,
136
+ mode: '100644',
137
+ });
138
+ files.push(...splitTokenFiles(document, tokenRoot));
139
+ files.push(...generateTokenFiles({ document, outputRoot }));
140
+ return files;
141
+ }
142
+
143
+ module.exports = {
144
+ splitTokenDocument,
145
+ splitTokenFiles,
146
+ generateTokenFiles,
147
+ generateRepositoryFiles,
148
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@khaledeltohamy14/token-pipeline-engine",
3
+ "version": "1.0.0",
4
+ "description": "Reusable Galaxy design token pipeline engine for splitting design tokens and generating token/theme files.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "commonjs",
8
+ "files": [
9
+ "index.js",
10
+ "index.d.ts",
11
+ "generators",
12
+ "split",
13
+ "README.md"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/YOUR_GITHUB_USERNAME/galaxy-token-pipeline-engine.git"
18
+ },
19
+ "keywords": [
20
+ "figma",
21
+ "design-tokens",
22
+ "flutter",
23
+ "dart",
24
+ "atomic-design",
25
+ "galaxy"
26
+ ],
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "scripts": {
32
+ "test": "node test/engine.test.js",
33
+ "pack:check": "npm pack --dry-run"
34
+ }
35
+ }
@@ -0,0 +1,58 @@
1
+ const CATEGORY_NAMES = [
2
+ 'global',
3
+ 'alias',
4
+ 'components',
5
+ 'spacing',
6
+ 'typography',
7
+ 'radius',
8
+ 'shadow',
9
+ 'dimensions',
10
+ ];
11
+
12
+ function asRecord(value) {
13
+ return value && typeof value === 'object' && !Array.isArray(value)
14
+ ? value
15
+ : {};
16
+ }
17
+
18
+ function splitTokenDocument(document) {
19
+ const source = asRecord(document);
20
+ const tokens = asRecord(source.tokens);
21
+ const result = {};
22
+
23
+ for (const category of CATEGORY_NAMES) {
24
+ result[category] = asRecord(tokens[category]);
25
+ }
26
+
27
+ result.brands = asRecord(tokens.brands);
28
+
29
+ return result;
30
+ }
31
+
32
+ function splitTokenFiles(document, root = 'token_pipeline/tokens') {
33
+ const split = splitTokenDocument(document);
34
+ const files = [];
35
+
36
+ for (const category of CATEGORY_NAMES) {
37
+ files.push({
38
+ path: `${root}/${category}.json`,
39
+ content: `${JSON.stringify(split[category], null, 2)}\n`,
40
+ mode: '100644',
41
+ });
42
+ }
43
+
44
+ for (const [brandName, brandTokens] of Object.entries(split.brands)) {
45
+ files.push({
46
+ path: `token_pipeline/brands/${brandName}.json`,
47
+ content: `${JSON.stringify(asRecord(brandTokens), null, 2)}\n`,
48
+ mode: '100644',
49
+ });
50
+ }
51
+
52
+ return files;
53
+ }
54
+
55
+ module.exports = {
56
+ splitTokenDocument,
57
+ splitTokenFiles,
58
+ };