@terrazzo/cli 0.7.4 → 0.8.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/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/bin/cli.js +1 -8
- package/dist/index.d.ts +148 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +723 -26
- package/dist/index.js.map +1 -1
- package/package.json +8 -13
- package/dist/build.d.ts +0 -16
- package/dist/build.d.ts.map +0 -1
- package/dist/build.js +0 -93
- package/dist/build.js.map +0 -1
- package/dist/check.d.ts +0 -10
- package/dist/check.d.ts.map +0 -1
- package/dist/check.js +0 -24
- package/dist/check.js.map +0 -1
- package/dist/help.d.ts +0 -3
- package/dist/help.d.ts.map +0 -1
- package/dist/help.js +0 -19
- package/dist/help.js.map +0 -1
- package/dist/init.d.ts +0 -6
- package/dist/init.d.ts.map +0 -1
- package/dist/init.js +0 -224
- package/dist/init.js.map +0 -1
- package/dist/lab.d.ts +0 -11
- package/dist/lab.d.ts.map +0 -1
- package/dist/lab.js +0 -74
- package/dist/lab.js.map +0 -1
- package/dist/normalize.d.ts +0 -7
- package/dist/normalize.d.ts.map +0 -1
- package/dist/normalize.js +0 -77
- package/dist/normalize.js.map +0 -1
- package/dist/shared.d.ts +0 -48
- package/dist/shared.d.ts.map +0 -1
- package/dist/shared.js +0 -190
- package/dist/shared.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -6
- package/dist/version.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,29 +1,726 @@
|
|
|
1
|
-
import { createRequire } from
|
|
2
|
-
import { pathToFileURL } from
|
|
3
|
-
import { defineConfig as
|
|
4
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { build, defineConfig as defineConfig$1, getObjMembers, parse, traverse } from "@terrazzo/parser";
|
|
4
|
+
import fs, { createReadStream, createWriteStream } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import chokidar from "chokidar";
|
|
8
|
+
import yamlToMomoa from "yaml-to-momoa";
|
|
9
|
+
import { exec } from "node:child_process";
|
|
10
|
+
import { confirm, intro, multiselect, outro, select, spinner } from "@clack/prompts";
|
|
11
|
+
import { isAlias, pluralize } from "@terrazzo/token-tools";
|
|
12
|
+
import { detect } from "detect-package-manager";
|
|
13
|
+
import { generate } from "escodegen";
|
|
14
|
+
import { parseModule } from "meriyah";
|
|
15
|
+
import { readdir } from "node:fs/promises";
|
|
16
|
+
import { Readable, Writable } from "node:stream";
|
|
17
|
+
import { serve } from "@hono/node-server";
|
|
18
|
+
import mime from "mime";
|
|
19
|
+
import { parse as parse$1, print } from "@humanwhocodes/momoa";
|
|
20
|
+
|
|
21
|
+
//#region src/shared.ts
|
|
22
|
+
const cwd = new URL(`${pathToFileURL(process.cwd())}/`);
|
|
23
|
+
const DEFAULT_CONFIG_PATH = new URL("./terrazzo.config.mjs", cwd);
|
|
24
|
+
const DEFAULT_TOKENS_PATH = new URL("./tokens.json", cwd);
|
|
25
|
+
const GREEN_CHECK = pc.green("✔");
|
|
26
|
+
/** Load config */
|
|
27
|
+
async function loadConfig({ cmd, flags, logger }) {
|
|
28
|
+
try {
|
|
29
|
+
let config = {
|
|
30
|
+
tokens: [DEFAULT_TOKENS_PATH],
|
|
31
|
+
outDir: new URL("./tokens/", cwd),
|
|
32
|
+
plugins: [],
|
|
33
|
+
lint: {
|
|
34
|
+
build: { enabled: true },
|
|
35
|
+
rules: {}
|
|
36
|
+
},
|
|
37
|
+
ignore: {
|
|
38
|
+
tokens: [],
|
|
39
|
+
deprecated: false
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
let configPath;
|
|
43
|
+
if (typeof flags.config === "string") {
|
|
44
|
+
if (flags.config === "") {
|
|
45
|
+
logger.error({
|
|
46
|
+
group: "config",
|
|
47
|
+
message: "Missing path after --config flag"
|
|
48
|
+
});
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
configPath = resolveConfig(flags.config);
|
|
52
|
+
}
|
|
53
|
+
const resolvedConfigPath = resolveConfig(configPath);
|
|
54
|
+
if (resolvedConfigPath) try {
|
|
55
|
+
const mod = await import(resolvedConfigPath);
|
|
56
|
+
if (!mod.default) throw new Error(`No default export found in ${path.relative(cwd.href, resolvedConfigPath)}. See https://terrazzo.dev/docs/cli for instructions.`);
|
|
57
|
+
config = defineConfig$1(mod.default, {
|
|
58
|
+
cwd,
|
|
59
|
+
logger
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.error({
|
|
63
|
+
group: "config",
|
|
64
|
+
message: err.message || err
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else if (cmd !== "init" && cmd !== "check") logger.error({
|
|
68
|
+
group: "config",
|
|
69
|
+
message: "No config file found. Create one with `npx terrazzo init`."
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
config,
|
|
73
|
+
configPath: resolvedConfigPath
|
|
74
|
+
};
|
|
75
|
+
} catch (err) {
|
|
76
|
+
printError(err.message);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** load tokens */
|
|
81
|
+
async function loadTokens(tokenPaths, { logger }) {
|
|
82
|
+
try {
|
|
83
|
+
const allTokens = [];
|
|
84
|
+
if (!Array.isArray(tokenPaths)) logger.error({
|
|
85
|
+
group: "config",
|
|
86
|
+
message: `loadTokens: Expected array, received ${typeof tokenPaths}`
|
|
87
|
+
});
|
|
88
|
+
if (tokenPaths.length === 1 && tokenPaths[0].href === DEFAULT_TOKENS_PATH.href) {
|
|
89
|
+
if (!fs.existsSync(tokenPaths[0])) {
|
|
90
|
+
const yamlPath = new URL("./tokens.yaml", cwd);
|
|
91
|
+
if (fs.existsSync(yamlPath)) tokenPaths[0] = yamlPath;
|
|
92
|
+
else {
|
|
93
|
+
logger.error({
|
|
94
|
+
group: "config",
|
|
95
|
+
message: `Could not locate ${path.relative(cwd.href, tokenPaths[0].href)}. To create one, run \`npx tz init\`.`
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (let i = 0; i < tokenPaths.length; i++) {
|
|
102
|
+
const filename = tokenPaths[i];
|
|
103
|
+
if (!(filename instanceof URL)) {
|
|
104
|
+
logger.error({
|
|
105
|
+
group: "config",
|
|
106
|
+
message: `Expected URL, received ${filename}`,
|
|
107
|
+
label: `loadTokens[${i}]`
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
} else if (filename.protocol === "http:" || filename.protocol === "https:") try {
|
|
111
|
+
if (filename.host === "figma.com" || filename.host === "www.figma.com") {
|
|
112
|
+
const [_, fileKeyword, fileKey] = filename.pathname.split("/");
|
|
113
|
+
if (fileKeyword !== "file" || !fileKey) logger.error({
|
|
114
|
+
group: "config",
|
|
115
|
+
message: `Unexpected Figma URL. Expected "https://www.figma.com/file/:file_key/:file_name?…", received "${filename.href}"`
|
|
116
|
+
});
|
|
117
|
+
const headers = new Headers({
|
|
118
|
+
Accept: "*/*",
|
|
119
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0"
|
|
120
|
+
});
|
|
121
|
+
if (process.env.FIGMA_ACCESS_TOKEN) headers.set("X-FIGMA-TOKEN", process.env.FIGMA_ACCESS_TOKEN);
|
|
122
|
+
else logger.warn({
|
|
123
|
+
group: "config",
|
|
124
|
+
message: "FIGMA_ACCESS_TOKEN not set"
|
|
125
|
+
});
|
|
126
|
+
const res$1 = await fetch(`https://api.figma.com/v1/files/${fileKey}/variables/local`, {
|
|
127
|
+
method: "GET",
|
|
128
|
+
headers
|
|
129
|
+
});
|
|
130
|
+
if (res$1.ok) allTokens.push({
|
|
131
|
+
filename,
|
|
132
|
+
src: await res$1.text()
|
|
133
|
+
});
|
|
134
|
+
const message = res$1.status !== 404 ? JSON.stringify(await res$1.json(), void 0, 2) : "";
|
|
135
|
+
logger.error({
|
|
136
|
+
group: "config",
|
|
137
|
+
message: `Figma responded with ${res$1.status}${message ? `:\n${message}` : ""}`
|
|
138
|
+
});
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
const res = await fetch(filename, {
|
|
142
|
+
method: "GET",
|
|
143
|
+
headers: {
|
|
144
|
+
Accept: "*/*",
|
|
145
|
+
"User-Agent": "Mozilla/5.0 Gecko/20100101 Firefox/123.0"
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
allTokens.push({
|
|
149
|
+
filename,
|
|
150
|
+
src: await res.text()
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
logger.error({
|
|
154
|
+
group: "config",
|
|
155
|
+
message: `${filename.href}: ${err}`
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
else if (fs.existsSync(filename)) allTokens.push({
|
|
160
|
+
filename,
|
|
161
|
+
src: fs.readFileSync(filename, "utf8")
|
|
162
|
+
});
|
|
163
|
+
else {
|
|
164
|
+
logger.error({
|
|
165
|
+
group: "config",
|
|
166
|
+
message: `Could not locate ${path.relative(cwd.href, filename.href)}. To create one, run \`npx tz init\`.`
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return allTokens;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
printError(err.message);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Print error */
|
|
178
|
+
function printError(message) {
|
|
179
|
+
console.error(pc.red(`✗ ${message}`));
|
|
180
|
+
}
|
|
181
|
+
/** Print success */
|
|
182
|
+
function printSuccess(message, startTime) {
|
|
183
|
+
console.log(`${GREEN_CHECK} ${message}${startTime ? ` ${time(startTime)}` : ""}`);
|
|
184
|
+
}
|
|
185
|
+
/** Resolve config */
|
|
186
|
+
function resolveConfig(filename) {
|
|
187
|
+
if (filename) {
|
|
188
|
+
const configPath = new URL(filename, cwd);
|
|
189
|
+
if (fs.existsSync(configPath)) return configPath.href;
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
return [
|
|
193
|
+
".js",
|
|
194
|
+
".mjs",
|
|
195
|
+
".cjs"
|
|
196
|
+
].map((ext) => new URL(`./terrazzo.config${ext}`, cwd)).find((configPath) => fs.existsSync(configPath))?.href;
|
|
197
|
+
}
|
|
198
|
+
/** Resolve tokens.json path (for lint command) */
|
|
199
|
+
function resolveTokenPath(filename, { logger }) {
|
|
200
|
+
const tokensPath = new URL(filename, cwd);
|
|
201
|
+
if (!fs.existsSync(tokensPath)) logger.error({
|
|
202
|
+
group: "config",
|
|
203
|
+
message: `Could not locate ${filename}. Does the file exist?`
|
|
204
|
+
});
|
|
205
|
+
else if (!fs.statSync(tokensPath).isFile()) logger.error({
|
|
206
|
+
group: "config",
|
|
207
|
+
message: `Expected JSON or YAML file, received ${filename}.`
|
|
208
|
+
});
|
|
209
|
+
return tokensPath;
|
|
210
|
+
}
|
|
211
|
+
/** Print time elapsed */
|
|
212
|
+
function time(start) {
|
|
213
|
+
const diff = performance.now() - start;
|
|
214
|
+
return pc.dim(diff < 750 ? `${Math.round(diff)}ms` : `${(diff / 1e3).toFixed(1)}s`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/build.ts
|
|
219
|
+
/** tz build */
|
|
220
|
+
async function buildCmd({ config, configPath, flags, logger }) {
|
|
221
|
+
try {
|
|
222
|
+
const startTime = performance.now();
|
|
223
|
+
if (!Array.isArray(config.plugins) || !config.plugins.length) logger.error({
|
|
224
|
+
group: "config",
|
|
225
|
+
message: `No plugins defined! Add some in ${configPath || "terrazzo.config.js"}`
|
|
226
|
+
});
|
|
227
|
+
let rawSchemas = await loadTokens(config.tokens, { logger });
|
|
228
|
+
if (!rawSchemas) {
|
|
229
|
+
logger.error({
|
|
230
|
+
group: "config",
|
|
231
|
+
message: `Error loading ${path.relative(fileURLToPath(cwd), fileURLToPath(config.tokens[0] || DEFAULT_TOKENS_PATH))}`
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let { tokens, sources } = await parse(rawSchemas, {
|
|
236
|
+
config,
|
|
237
|
+
logger,
|
|
238
|
+
yamlToMomoa
|
|
239
|
+
});
|
|
240
|
+
let result = await build(tokens, {
|
|
241
|
+
sources,
|
|
242
|
+
config,
|
|
243
|
+
logger
|
|
244
|
+
});
|
|
245
|
+
writeFiles(result, {
|
|
246
|
+
config,
|
|
247
|
+
logger
|
|
248
|
+
});
|
|
249
|
+
if (flags.watch) {
|
|
250
|
+
const dt = new Intl.DateTimeFormat("en-us", {
|
|
251
|
+
hour: "2-digit",
|
|
252
|
+
minute: "2-digit"
|
|
253
|
+
});
|
|
254
|
+
async function rebuild({ messageBefore, messageAfter } = {}) {
|
|
255
|
+
try {
|
|
256
|
+
if (messageBefore) logger.info({
|
|
257
|
+
group: "plugin",
|
|
258
|
+
label: "watch",
|
|
259
|
+
message: messageBefore
|
|
260
|
+
});
|
|
261
|
+
rawSchemas = await loadTokens(config.tokens, { logger });
|
|
262
|
+
if (!rawSchemas) throw new Error(`Error loading ${path.relative(fileURLToPath(cwd), fileURLToPath(config.tokens[0] || DEFAULT_TOKENS_PATH))}`);
|
|
263
|
+
const parseResult = await parse(rawSchemas, {
|
|
264
|
+
config,
|
|
265
|
+
logger,
|
|
266
|
+
yamlToMomoa
|
|
267
|
+
});
|
|
268
|
+
tokens = parseResult.tokens;
|
|
269
|
+
sources = parseResult.sources;
|
|
270
|
+
result = await build(tokens, {
|
|
271
|
+
sources,
|
|
272
|
+
config,
|
|
273
|
+
logger
|
|
274
|
+
});
|
|
275
|
+
if (messageAfter) logger.info({
|
|
276
|
+
group: "plugin",
|
|
277
|
+
label: "watch",
|
|
278
|
+
message: messageAfter
|
|
279
|
+
});
|
|
280
|
+
writeFiles(result, {
|
|
281
|
+
config,
|
|
282
|
+
logger
|
|
283
|
+
});
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(pc.red(`✗ ${err.message || err}`));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const tokenWatcher = chokidar.watch(config.tokens.map((filename) => fileURLToPath(filename)));
|
|
289
|
+
tokenWatcher.on("change", async (filename) => {
|
|
290
|
+
await rebuild({ messageBefore: `${pc.dim(dt.format(/* @__PURE__ */ new Date()))} ${pc.green("tz")}} ${pc.yellow(filename)} updated ${GREEN_CHECK}` });
|
|
291
|
+
});
|
|
292
|
+
const configWatcher = chokidar.watch(resolveConfig(configPath));
|
|
293
|
+
configWatcher.on("change", async () => {
|
|
294
|
+
await rebuild({ messageBefore: `${pc.dim(dt.format(/* @__PURE__ */ new Date()))} ${pc.green("tz")} ${pc.yellow("Config updated. Reloading…")}` });
|
|
295
|
+
});
|
|
296
|
+
await new Promise(() => {});
|
|
297
|
+
} else printSuccess(`${Object.keys(tokens).length} token${Object.keys(tokens).length !== 1 ? "s" : ""} built`, startTime);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
printError(err.message);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** Write files */
|
|
304
|
+
function writeFiles(result, { config, logger }) {
|
|
305
|
+
for (const { filename, contents } of result.outputFiles) {
|
|
306
|
+
const output = new URL(filename, config.outDir);
|
|
307
|
+
fs.mkdirSync(new URL(".", output), { recursive: true });
|
|
308
|
+
fs.writeFileSync(output, contents);
|
|
309
|
+
logger.debug({
|
|
310
|
+
group: "parser",
|
|
311
|
+
label: "buildEnd",
|
|
312
|
+
message: `Wrote file ${fileURLToPath(output)}`
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/check.ts
|
|
319
|
+
/** tz check */
|
|
320
|
+
async function checkCmd({ config, logger, positionals }) {
|
|
321
|
+
try {
|
|
322
|
+
const startTime = performance.now();
|
|
323
|
+
const tokenPaths = positionals.slice(1).length ? positionals.slice(1).map((tokenPath) => resolveTokenPath(tokenPath, { logger })) : config.tokens;
|
|
324
|
+
const sources = await loadTokens(tokenPaths, { logger });
|
|
325
|
+
if (!sources?.length) {
|
|
326
|
+
logger.error({
|
|
327
|
+
group: "config",
|
|
328
|
+
message: "Couldn’t find any tokens. Run `npx tz init` to create some."
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await parse(sources, {
|
|
333
|
+
config,
|
|
334
|
+
continueOnError: true,
|
|
335
|
+
logger,
|
|
336
|
+
yamlToMomoa
|
|
337
|
+
});
|
|
338
|
+
printSuccess("No errors", startTime);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
printError(err.message);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/help.ts
|
|
347
|
+
/** Show help */
|
|
348
|
+
function helpCmd() {
|
|
349
|
+
console.log(`tz
|
|
350
|
+
[commands]
|
|
351
|
+
build Build token artifacts from tokens.json
|
|
352
|
+
--watch, -w Watch tokens.json for changes and recompile
|
|
353
|
+
--no-lint Disable linters running on build
|
|
354
|
+
check [path] Check tokens.json for errors and run linters
|
|
355
|
+
lint [path] (alias of check)
|
|
356
|
+
init Create a starter tokens.json file
|
|
357
|
+
lab Manage your tokens with a web interface
|
|
358
|
+
|
|
359
|
+
[options]
|
|
360
|
+
--help Show this message
|
|
361
|
+
--config, -c Path to config (default: ./terrazzo.config.js)
|
|
362
|
+
--quiet Suppress warnings
|
|
363
|
+
`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/init.ts
|
|
368
|
+
const INSTALL_COMMAND = {
|
|
369
|
+
npm: "install -D --silent",
|
|
370
|
+
yarn: "add -D --silent",
|
|
371
|
+
pnpm: "add -D --silent",
|
|
372
|
+
bun: "install -D --silent"
|
|
373
|
+
};
|
|
374
|
+
const DTCG_ROOT_URL = "https://raw.githubusercontent.com/terrazzoapp/dtcg-examples/refs/heads/main/";
|
|
375
|
+
const DESIGN_SYSTEMS = {
|
|
376
|
+
"adobe-spectrum": {
|
|
377
|
+
name: "Spectrum",
|
|
378
|
+
author: "Adobe",
|
|
379
|
+
tokens: ["adobe-spectrum.json"]
|
|
380
|
+
},
|
|
381
|
+
"apple-hig": {
|
|
382
|
+
name: "Human Interface Guidelines",
|
|
383
|
+
author: "Apple",
|
|
384
|
+
tokens: ["apple-hig.json"]
|
|
385
|
+
},
|
|
386
|
+
"figma-sds": {
|
|
387
|
+
name: "Simple Design System",
|
|
388
|
+
author: "Figma",
|
|
389
|
+
tokens: ["figma-sds.json"]
|
|
390
|
+
},
|
|
391
|
+
"github-primer": {
|
|
392
|
+
name: "Primer",
|
|
393
|
+
author: "GitHub",
|
|
394
|
+
tokens: ["github-primer.json"]
|
|
395
|
+
},
|
|
396
|
+
"ibm-carbon": {
|
|
397
|
+
name: "Carbon",
|
|
398
|
+
author: "IBM",
|
|
399
|
+
tokens: ["ibm-carbon.json"]
|
|
400
|
+
},
|
|
401
|
+
"microsoft-fluent": {
|
|
402
|
+
name: "Fluent",
|
|
403
|
+
author: "Microsoft",
|
|
404
|
+
tokens: ["microsoft-fluent.json"]
|
|
405
|
+
},
|
|
406
|
+
radix: {
|
|
407
|
+
name: "Radix",
|
|
408
|
+
author: "Radix",
|
|
409
|
+
tokens: ["radix.json"]
|
|
410
|
+
},
|
|
411
|
+
"salesforce-lightning": {
|
|
412
|
+
name: "Lightning",
|
|
413
|
+
author: "Salesforce",
|
|
414
|
+
tokens: ["salesforce-lightning.json"]
|
|
415
|
+
},
|
|
416
|
+
"shopify-polaris": {
|
|
417
|
+
name: "Polaris",
|
|
418
|
+
author: "Shopify",
|
|
419
|
+
tokens: ["shopify-polaris.json"]
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
async function initCmd({ logger }) {
|
|
423
|
+
try {
|
|
424
|
+
intro("⛋ Welcome to Terrazzo");
|
|
425
|
+
const packageManager = await detect({ cwd: fileURLToPath(cwd) });
|
|
426
|
+
const { config, configPath } = await loadConfig({
|
|
427
|
+
cmd: "init",
|
|
428
|
+
flags: {},
|
|
429
|
+
logger
|
|
430
|
+
});
|
|
431
|
+
const relConfigPath = configPath ? path.relative(fileURLToPath(cwd), fileURLToPath(new URL(configPath))) : void 0;
|
|
432
|
+
let tokensPath = config.tokens[0];
|
|
433
|
+
let startFromDS = !(tokensPath && fs.existsSync(tokensPath));
|
|
434
|
+
if (tokensPath && fs.existsSync(tokensPath)) {
|
|
435
|
+
if (await confirm({ message: `Found tokens at ${path.relative(fileURLToPath(cwd), fileURLToPath(tokensPath))}. Overwrite with a new design system?` })) startFromDS = true;
|
|
436
|
+
} else tokensPath = DEFAULT_TOKENS_PATH;
|
|
437
|
+
if (startFromDS) {
|
|
438
|
+
const ds = DESIGN_SYSTEMS[await select({
|
|
439
|
+
message: "Start from existing design system?",
|
|
440
|
+
options: [...Object.entries(DESIGN_SYSTEMS).map(([id, { author, name }]) => ({
|
|
441
|
+
value: id,
|
|
442
|
+
label: `${author} ${name}`
|
|
443
|
+
})), {
|
|
444
|
+
value: "none",
|
|
445
|
+
label: "None"
|
|
446
|
+
}]
|
|
447
|
+
})];
|
|
448
|
+
if (ds) {
|
|
449
|
+
const s = spinner();
|
|
450
|
+
s.start("Downloading");
|
|
451
|
+
const tokenSource = await fetch(new URL(ds.tokens[0], DTCG_ROOT_URL)).then((res) => res.text());
|
|
452
|
+
fs.writeFileSync(tokensPath, tokenSource);
|
|
453
|
+
s.stop("Download complete");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const existingPlugins = config.plugins.map((p) => p.name);
|
|
457
|
+
const pluginSelection = await multiselect({
|
|
458
|
+
message: "Install plugins?",
|
|
459
|
+
options: [
|
|
460
|
+
{
|
|
461
|
+
value: "@terrazzo/plugin-css",
|
|
462
|
+
label: "CSS"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
value: "@terrazzo/plugin-js",
|
|
466
|
+
label: "JS + TS"
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
value: "@terrazzo/plugin-sass",
|
|
470
|
+
label: "Sass"
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
value: "@terrazzo/plugin-tailwind",
|
|
474
|
+
label: "Tailwind"
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
required: false
|
|
478
|
+
});
|
|
479
|
+
const newPlugins = Array.isArray(pluginSelection) ? pluginSelection.filter((p) => !existingPlugins.includes(p)) : [];
|
|
480
|
+
if (newPlugins?.length) {
|
|
481
|
+
const plugins = newPlugins.map((p) => ({
|
|
482
|
+
specifier: p.replace("@terrazzo/plugin-", ""),
|
|
483
|
+
package: p
|
|
484
|
+
}));
|
|
485
|
+
const pluginCount = `${newPlugins.length} ${pluralize(newPlugins.length, "plugin", "plugins")}`;
|
|
486
|
+
const s = spinner();
|
|
487
|
+
s.start(`Installing ${pluginCount}`);
|
|
488
|
+
await new Promise((resolve, reject) => {
|
|
489
|
+
const subprocess = exec([
|
|
490
|
+
packageManager,
|
|
491
|
+
INSTALL_COMMAND[packageManager],
|
|
492
|
+
newPlugins.join(" ")
|
|
493
|
+
].join(" "), { cwd });
|
|
494
|
+
subprocess.on("error", reject);
|
|
495
|
+
subprocess.on("exit", resolve);
|
|
496
|
+
});
|
|
497
|
+
s.message("Updating config");
|
|
498
|
+
if (configPath) {
|
|
499
|
+
const ast = parseModule(fs.readFileSync(configPath, "utf8"));
|
|
500
|
+
const astExport = ast.body.find((node) => node.type === "ExportDefaultDeclaration");
|
|
501
|
+
ast.body.push(...plugins.map((p) => ({
|
|
502
|
+
type: "ImportDeclaration",
|
|
503
|
+
source: {
|
|
504
|
+
type: "Literal",
|
|
505
|
+
value: p.package
|
|
506
|
+
},
|
|
507
|
+
specifiers: [{
|
|
508
|
+
type: "ImportDefaultSpecifier",
|
|
509
|
+
local: {
|
|
510
|
+
type: "Identifier",
|
|
511
|
+
name: p.specifier
|
|
512
|
+
}
|
|
513
|
+
}],
|
|
514
|
+
attributes: []
|
|
515
|
+
})));
|
|
516
|
+
if (!astExport) {
|
|
517
|
+
logger.error({
|
|
518
|
+
group: "config",
|
|
519
|
+
message: `SyntaxError: ${relConfigPath} does not have default export.`
|
|
520
|
+
});
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const astConfig = astExport.declaration.type === "CallExpression" ? astExport.declaration.arguments[0] : astExport.declaration;
|
|
524
|
+
if (astConfig.type !== "ObjectExpression") {
|
|
525
|
+
logger.error({
|
|
526
|
+
group: "config",
|
|
527
|
+
message: `Config: expected object default export, received ${astConfig.type}`
|
|
528
|
+
});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const pluginsArray = astConfig.properties.find((property) => property.type === "Property" && property.key.type === "Identifier" && property.key.name === "plugins")?.value;
|
|
532
|
+
const pluginsAst = plugins.map((p) => ({
|
|
533
|
+
type: "CallExpression",
|
|
534
|
+
callee: {
|
|
535
|
+
type: "Identifier",
|
|
536
|
+
name: p.specifier
|
|
537
|
+
},
|
|
538
|
+
arguments: []
|
|
539
|
+
}));
|
|
540
|
+
if (pluginsArray) pluginsArray.elements.push(...pluginsAst);
|
|
541
|
+
else astConfig.properties.push({
|
|
542
|
+
type: "Property",
|
|
543
|
+
key: {
|
|
544
|
+
type: "Identifier",
|
|
545
|
+
name: "plugins"
|
|
546
|
+
},
|
|
547
|
+
value: {
|
|
548
|
+
type: "ArrayExpression",
|
|
549
|
+
elements: pluginsAst
|
|
550
|
+
},
|
|
551
|
+
kind: "init",
|
|
552
|
+
computed: false,
|
|
553
|
+
method: false,
|
|
554
|
+
shorthand: false
|
|
555
|
+
});
|
|
556
|
+
fs.writeFileSync(configPath, generate(ast, { format: {
|
|
557
|
+
indent: { style: " " },
|
|
558
|
+
quotes: "single",
|
|
559
|
+
semicolons: true
|
|
560
|
+
} }));
|
|
561
|
+
} else fs.writeFileSync(DEFAULT_CONFIG_PATH, `import { defineConfig } from '@terrazzo/cli';
|
|
562
|
+
${plugins.map((p) => `import ${p.specifier} from '${p.package}';`).join("\n")}
|
|
563
|
+
export default defineConfig({
|
|
564
|
+
tokens: ['./tokens.json'],
|
|
565
|
+
plugins: [
|
|
566
|
+
${plugins.map((p) => `${p.specifier}(),`).join("\n ")}
|
|
567
|
+
],
|
|
568
|
+
outDir: './dist/',
|
|
569
|
+
lint: {
|
|
570
|
+
/** @see https://terrazzo.app/docs/cli/lint */
|
|
571
|
+
},
|
|
572
|
+
});`);
|
|
573
|
+
s.stop(`Installed ${pluginCount}`);
|
|
574
|
+
}
|
|
575
|
+
outro("⛋ Done! 🎉");
|
|
576
|
+
} catch (err) {
|
|
577
|
+
printError(err.message);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/lab.ts
|
|
584
|
+
async function labCmd({ config, logger }) {
|
|
585
|
+
/** TODO: handle multiple files */
|
|
586
|
+
const [tokenFileUrl] = config.tokens;
|
|
587
|
+
const staticFiles = /* @__PURE__ */ new Set();
|
|
588
|
+
const dirEntries = await readdir(fileURLToPath(import.meta.resolve("./lab")), {
|
|
589
|
+
withFileTypes: true,
|
|
590
|
+
recursive: true
|
|
591
|
+
});
|
|
592
|
+
for (const entry of dirEntries) {
|
|
593
|
+
if (entry.isFile() === false) continue;
|
|
594
|
+
const absolutePath = `${entry.parentPath.replaceAll("\\", "/")}/${entry.name}`;
|
|
595
|
+
staticFiles.add(absolutePath.replace(fileURLToPath(import.meta.resolve("./lab")).replaceAll("\\", "/"), ""));
|
|
596
|
+
}
|
|
597
|
+
const server = serve({
|
|
598
|
+
port: 9e3,
|
|
599
|
+
overrideGlobalObjects: false,
|
|
600
|
+
async fetch(request) {
|
|
601
|
+
const url = new URL(request.url);
|
|
602
|
+
const pathname = url.pathname;
|
|
603
|
+
if (pathname === "/") return new Response(Readable.toWeb(createReadStream(fileURLToPath(import.meta.resolve("./lab/index.html")))), { headers: { "Content-Type": "text/html" } });
|
|
604
|
+
if (pathname === "/api/tokens") {
|
|
605
|
+
if (request.method === "GET") return new Response(Readable.toWeb(createReadStream(tokenFileUrl)), { headers: {
|
|
606
|
+
"Content-Type": "application/json",
|
|
607
|
+
"Cache-Control": "no-cache"
|
|
608
|
+
} });
|
|
609
|
+
else if (request.method === "POST" && request.body) {
|
|
610
|
+
await request.body.pipeTo(Writable.toWeb(createWriteStream(tokenFileUrl)));
|
|
611
|
+
return new Response(JSON.stringify({ success: true }), { headers: { "Content-Type": "application/json" } });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (staticFiles.has(pathname)) return new Response(Readable.toWeb(createReadStream(fileURLToPath(import.meta.resolve(`./lab${pathname}`)))), { headers: { "Content-Type": mime.getType(pathname) ?? "application/octet-stream" } });
|
|
615
|
+
return new Response("Not found", { status: 404 });
|
|
616
|
+
}
|
|
617
|
+
}, (info) => {
|
|
618
|
+
logger.info({
|
|
619
|
+
group: "server",
|
|
620
|
+
message: `Token Lab running at http://${info.address === "::" ? "localhost" : info.address}:${info.port}`
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
/**
|
|
624
|
+
* The cli entrypoint is going to manually exit the process after labCmd returns.
|
|
625
|
+
*/
|
|
626
|
+
await new Promise((resolve, reject) => {
|
|
627
|
+
server.on("close", resolve);
|
|
628
|
+
server.on("error", reject);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region src/normalize.ts
|
|
634
|
+
function findMember(name) {
|
|
635
|
+
return function(member) {
|
|
636
|
+
return member.name.type === "String" && member.name.value === name;
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async function normalizeCmd(filename, { logger, output }) {
|
|
640
|
+
try {
|
|
641
|
+
if (!filename) {
|
|
642
|
+
logger.error({
|
|
643
|
+
group: "config",
|
|
644
|
+
message: "Expected input: `tz normalize <tokens.json> -o output.json`"
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const sourceLoc = new URL(filename, cwd);
|
|
649
|
+
if (!fs.existsSync(sourceLoc)) logger.error({
|
|
650
|
+
group: "config",
|
|
651
|
+
message: `Couldn’t find ${path.relative(cwd.href, sourceLoc.href)}. Does it exist?`
|
|
652
|
+
});
|
|
653
|
+
const sourceData = fs.readFileSync(sourceLoc, "utf8");
|
|
654
|
+
const document = parse$1(sourceData);
|
|
655
|
+
const { tokens } = await parse([{
|
|
656
|
+
src: sourceData,
|
|
657
|
+
filename: sourceLoc
|
|
658
|
+
}], {
|
|
659
|
+
config: defineConfig$1({}, { cwd }),
|
|
660
|
+
logger
|
|
661
|
+
});
|
|
662
|
+
traverse(document, { enter(node, _parent, nodePath) {
|
|
663
|
+
const token = tokens[nodePath.join(".")];
|
|
664
|
+
if (!token || token.aliasOf || node.type !== "Member" || node.value.type !== "Object") return;
|
|
665
|
+
const $valueI = node.value.members.findIndex(findMember("$value"));
|
|
666
|
+
switch (token.$type) {
|
|
667
|
+
case "color":
|
|
668
|
+
case "dimension":
|
|
669
|
+
case "duration": {
|
|
670
|
+
if (node.value.members[$valueI].value.type === "String") {
|
|
671
|
+
const newValueContainer = parse$1(JSON.stringify({ $value: token.$value })).body;
|
|
672
|
+
const newValueNode = newValueContainer.members.find(findMember("$value"));
|
|
673
|
+
node.value.members[$valueI] = newValueNode;
|
|
674
|
+
const { $extensions } = getObjMembers(node.value);
|
|
675
|
+
if ($extensions?.type === "Object") {
|
|
676
|
+
const { mode } = getObjMembers($extensions);
|
|
677
|
+
if (mode?.type === "Object") for (let i = 0; i < mode.members.length; i++) {
|
|
678
|
+
const modeName = mode.members[i].name.value;
|
|
679
|
+
const modeValue = token.mode[modeName];
|
|
680
|
+
if (typeof modeValue === "string" && isAlias(modeValue)) continue;
|
|
681
|
+
const newModeValueContainer = parse$1(JSON.stringify({ [modeName]: token.mode[modeName].$value })).body;
|
|
682
|
+
const newModeValueNode = newModeValueContainer.members.find(findMember(modeName));
|
|
683
|
+
mode.members[i] = newModeValueNode;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} });
|
|
691
|
+
const outputLoc = new URL(output, cwd);
|
|
692
|
+
fs.mkdirSync(new URL(".", outputLoc), { recursive: true });
|
|
693
|
+
fs.writeFileSync(outputLoc, print(document, { indent: 2 }));
|
|
694
|
+
} catch (err) {
|
|
695
|
+
printError(err.message);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/version.ts
|
|
702
|
+
function versionCmd() {
|
|
703
|
+
const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
704
|
+
console.log(version);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/index.ts
|
|
5
709
|
const require = createRequire(cwd);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
catch (err) {
|
|
19
|
-
console.error(err);
|
|
20
|
-
// this will throw an error if Node couldn’t automatically resolve it,
|
|
21
|
-
// which will be true for many token paths. We don’t need to surface
|
|
22
|
-
// that error; it’ll get its own error down the line if it’s a bad path.
|
|
23
|
-
return tokenPath;
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
return defineConfigCore(normalizedConfig, { cwd });
|
|
710
|
+
function defineConfig(config) {
|
|
711
|
+
const normalizedConfig = { ...config };
|
|
712
|
+
if (typeof normalizedConfig.tokens === "string" || Array.isArray(normalizedConfig.tokens)) normalizedConfig.tokens = (Array.isArray(normalizedConfig.tokens) ? normalizedConfig.tokens : [normalizedConfig.tokens]).map((tokenPath) => {
|
|
713
|
+
if (tokenPath.startsWith(".") || /^(https?|file):\/\//i.test(tokenPath)) return tokenPath;
|
|
714
|
+
try {
|
|
715
|
+
return pathToFileURL(require.resolve(tokenPath));
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error(err);
|
|
718
|
+
return tokenPath;
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
return defineConfig$1(normalizedConfig, { cwd });
|
|
28
722
|
}
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
export { DEFAULT_CONFIG_PATH, DEFAULT_TOKENS_PATH, GREEN_CHECK, buildCmd, checkCmd, cwd, defineConfig, helpCmd, initCmd, labCmd, loadConfig, loadTokens, normalizeCmd, printError, printSuccess, resolveConfig, resolveTokenPath, time, versionCmd, writeFiles };
|
|
29
726
|
//# sourceMappingURL=index.js.map
|