From f2a25e2e8300341a363d42654356dfb979a1bc35 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 5 Feb 2026 12:44:20 +0100 Subject: [PATCH 01/11] fix: dir in config are relative to config location --- packages/cli/bin/designsystemet.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index d5e236799c..ed201da394 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -51,9 +51,7 @@ function makeTokenCommands() { .option('--experimental-tailwind', 'Generate Tailwind CSS classes for tokens', false) .action(async (opts) => { console.log(figletAscii); - const { verbose, clean, dry, experimentalTailwind } = opts; - const tokensDir = typeof opts.tokens === 'string' ? opts.tokens : DEFAULT_TOKENS_CREATE_DIR; - const outDir = typeof opts.outDir === 'string' ? opts.outDir : './dist/tokens'; + const { verbose, clean, dry, experimentalTailwind, outDir, tokens } = opts; const { configFile, configPath } = await getConfigFile(opts.config); const config = await parseBuildConfig(configFile, { configPath }); @@ -66,7 +64,7 @@ function makeTokenCommands() { await cleanDir(outDir, dry); } - await buildTokens({ tokensDir, outDir, verbose, dry, tailwind: experimentalTailwind, ...config }); + await buildTokens({ tokensDir: tokens, outDir, verbose, dry, tailwind: experimentalTailwind, ...config }); return Promise.resolve(); }); From 3d7de0521421ef14f5b08501dff7c1c926215416 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Fri, 6 Feb 2026 16:15:03 +0100 Subject: [PATCH 02/11] making resolving working dir in a single place --- packages/cli/bin/config.ts | 7 ++-- packages/cli/bin/designsystemet.ts | 34 ++++++++++++++++---- packages/cli/configs/test-tokens.config.json | 2 +- packages/cli/src/tokens/create/write.ts | 14 ++++---- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/cli/bin/config.ts b/packages/cli/bin/config.ts index 1f43c54caa..8246229587 100644 --- a/packages/cli/bin/config.ts +++ b/packages/cli/bin/config.ts @@ -14,21 +14,20 @@ import { readFile } from '../src/utils.js'; import { getCliOption, getDefaultCliOption, getSuppliedCliOption, type OptionGetter } from './options.js'; export async function readConfigFile(configPath: string, allowFileNotFound = true): Promise { - const resolvedPath = path.resolve(process.cwd(), configPath); let configFile: string; try { - configFile = await readFile(resolvedPath, false, allowFileNotFound); + configFile = await readFile(configPath, false, allowFileNotFound); } catch (err) { if (allowFileNotFound) { return ''; } - console.error(pc.redBright(`Could not read config file at ${pc.blue(resolvedPath)}`)); + console.error(pc.redBright(`Could not read config file at ${pc.blue(configPath)}`)); throw err; } if (configFile) { - console.log(`Found config file: ${pc.green(resolvedPath)}`); + console.log(`Found config file: ${pc.green(configPath)}`); } return configFile; diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index ed201da394..fc599af7b3 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import path from 'node:path'; import { Argument, createCommand, program } from '@commander-js/extra-typings'; import pc from 'picocolors'; import * as R from 'ramda'; @@ -53,18 +54,30 @@ function makeTokenCommands() { console.log(figletAscii); const { verbose, clean, dry, experimentalTailwind, outDir, tokens } = opts; - const { configFile, configPath } = await getConfigFile(opts.config); + const configFilePath = opts.config; + const workingDir = configFilePath ? path.dirname(configFilePath) : process.cwd(); + + const { configFile, configPath } = await getConfigFile(configFilePath); const config = await parseBuildConfig(configFile, { configPath }); + const writeDir = path.resolve(workingDir, outDir); + if (dry) { console.log(`Performing dry run, no files will be written`); } if (clean) { - await cleanDir(outDir, dry); + await cleanDir(writeDir, dry); } - await buildTokens({ tokensDir: tokens, outDir, verbose, dry, tailwind: experimentalTailwind, ...config }); + await buildTokens({ + tokensDir: tokens, + outDir: writeDir, + verbose, + dry, + tailwind: experimentalTailwind, + ...config, + }); return Promise.resolve(); }); @@ -96,17 +109,23 @@ function makeTokenCommands() { if (opts.dry) { console.log(`Performing dry run, no files will be written`); } + const themeName = opts.theme; + const configFilePath = opts.config; + const workingDir = configFilePath ? path.dirname(configFilePath) : process.cwd(); - const { configFile, configPath } = await getConfigFile(opts.config); + const { configFile, configPath } = await getConfigFile(configFilePath); const config = await parseCreateConfig(configFile, { - theme: opts.theme, + theme: themeName, cmd, configPath, }); + const writeDir = path.resolve(workingDir, config.outDir); + if (config.clean) { - await cleanDir(config.outDir, opts.dry); + await cleanDir(writeDir, opts.dry); } + /* * Create and write tokens for each theme */ @@ -116,7 +135,7 @@ function makeTokenCommands() { const theme = { name, ...themeWithoutName } as Theme; const { tokenSets } = await createTokens(theme); - await writeTokens({ outDir: config.outDir, theme, dry: opts.dry, tokenSets }); + await writeTokens({ outDir: writeDir, theme, dry: opts.dry, tokenSets }); } } }); @@ -203,5 +222,6 @@ async function getConfigFile(config: string | undefined) { const allowFileNotFound = R.isNil(config) || config === DEFAULT_CONFIG_FILE; const configPath = config ?? DEFAULT_CONFIG_FILE; const configFile = await readConfigFile(configPath, allowFileNotFound); + return { configFile, configPath }; } diff --git a/packages/cli/configs/test-tokens.config.json b/packages/cli/configs/test-tokens.config.json index 5d1b5f9201..b04c9ebbec 100644 --- a/packages/cli/configs/test-tokens.config.json +++ b/packages/cli/configs/test-tokens.config.json @@ -1,6 +1,6 @@ { "$schema": "../dist/config.schema.json", - "outDir": "./temp/config/design-tokens", + "outDir": "../temp/config/design-tokens", "clean": true, "themes": { "some-org": { diff --git a/packages/cli/src/tokens/create/write.ts b/packages/cli/src/tokens/create/write.ts index 0eacb60015..5d2a186cba 100644 --- a/packages/cli/src/tokens/create/write.ts +++ b/packages/cli/src/tokens/create/write.ts @@ -25,14 +25,14 @@ export const writeTokens = async (options: WriteTokensOptions) => { theme: { name: themeName, colors }, dry, } = options; - const targetDir = path.resolve(process.cwd(), String(outDir)); - const $themesPath = path.join(targetDir, '$themes.json'); - const $metadataPath = path.join(targetDir, '$metadata.json'); - const $designsystemetPath = path.join(targetDir, '$designsystemet.jsonc'); + + const $themesPath = path.join(outDir, '$themes.json'); + const $metadataPath = path.join(outDir, '$metadata.json'); + const $designsystemetPath = path.join(outDir, '$designsystemet.jsonc'); let themeObjects: ThemeObject[] = []; const sizeModes: SizeModes[] = ['small', 'medium', 'large']; - await mkdir(targetDir, dry); + await mkdir(outDir, dry); try { // Fetch existing themes @@ -65,10 +65,10 @@ export const writeTokens = async (options: WriteTokensOptions) => { for (const [set, tokens] of tokenSets) { // Remove last part of the path to get the directory - const fileDir = path.join(targetDir, path.dirname(set)); + const fileDir = path.join(outDir, path.dirname(set)); await mkdir(fileDir, dry); - const filePath = path.join(targetDir, `${set}.json`); + const filePath = path.join(outDir, `${set}.json`); await writeFile(filePath, stringify(tokens), dry); } From d7829e2500dfd31aa17aefefb162fac5380e6ca2 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Fri, 6 Feb 2026 16:34:01 +0100 Subject: [PATCH 03/11] added filesystem class --- packages/cli/src/utils.ts | 99 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 0e93f35ae8..0c99590551 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -2,6 +2,105 @@ import type { CopyOptions } from 'node:fs'; import fs from 'node:fs/promises'; import pc from 'picocolors'; +/** + * An abstraction of Node's file system API which allows dry-running destructive operations + */ +export class FileSystem { + private dry: boolean; + workingDir: string = process.cwd(); + + constructor( + /** Dry-run destructive operations instead of actually performing them */ + dry = false, + ) { + this.dry = dry; + } + + /** + * Creates a directory if it does not already exist. + * + * @param dir - The path of the directory to create. + * + * @returns A promise that resolves when the operation is complete. + * If the directory already exists or `dry` is `true`, the promise resolves immediately. + */ + mkdir = async (dir: string) => { + if (this.dry) { + console.log(`${pc.blue('mkdir')} ${dir}`); + return Promise.resolve(); + } + + const exists = await fs + .access(dir, fs.constants.F_OK) + .then(() => true) + .catch(() => false); + + if (exists) { + return Promise.resolve(); + } + + return fs.mkdir(dir, { recursive: true }); + }; + + writeFile = async (path: string, data: string) => { + if (this.dry) { + console.log(`${pc.blue('writeFile')} ${path}`); + return Promise.resolve(); + } + + return fs.writeFile(path, data, { encoding: 'utf-8' }).catch((error) => { + console.error(pc.red(`Error writing file: ${path}`)); + console.error(pc.red(error)); + throw error; + }); + }; + + cp = async (src: string, dest: string, filter?: CopyOptions['filter']) => { + if (this.dry) { + console.log(`${pc.blue('cp')} ${src} ${dest}`); + return Promise.resolve(); + } + + return fs.cp(src, dest, { recursive: true, filter }); + }; + + copyFile = async (src: string, dest: string) => { + if (this.dry) { + console.log(`${pc.blue('copyFile')} ${src} to ${dest}`); + return Promise.resolve(); + } + + return fs.copyFile(src, dest); + }; + + cleanDir = async (dir: string) => { + if (this.dry) { + console.log(`${pc.blue('cleanDir')} ${dir}`); + return Promise.resolve(); + } + + console.log(`\n🔥 Cleaning dir ${pc.red(`${dir.trim()}`)} `); + + return fs.rm(dir, { recursive: true, force: true }); + }; + + readFile = async (path: string, allowFileNotFound?: boolean) => { + if (this.dry) { + console.log(`${pc.blue('readFile')} ${path}`); + return Promise.resolve(''); + } + + try { + return await fs.readFile(path, 'utf-8'); + } catch (error) { + if (allowFileNotFound && (error as NodeJS.ErrnoException).code === 'ENOENT') { + return ''; + } + throw error; + } + }; +} + /** * Creates a directory if it does not already exist. * From 24fe037750af5fe25478aefdf72f28d045dd3107 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 9 Feb 2026 10:04:38 +0100 Subject: [PATCH 04/11] introduce filesystem singleton for better file system handling in CLI --- packages/cli/bin/config.ts | 5 +- packages/cli/bin/designsystemet.ts | 19 +- .../cli/src/migrations/codemods/css/run.ts | 10 +- .../cli/src/scripts/update-preview-tokens.ts | 11 +- packages/cli/src/tokens/build.ts | 22 +- packages/cli/src/tokens/create/write.ts | 19 +- packages/cli/src/tokens/generate-config.ts | 11 +- packages/cli/src/tokens/process/platform.ts | 2 - packages/cli/src/utils.ts | 188 ------------------ packages/cli/src/utils/filesystem.ts | 132 ++++++++++++ 10 files changed, 171 insertions(+), 248 deletions(-) delete mode 100644 packages/cli/src/utils.ts create mode 100644 packages/cli/src/utils/filesystem.ts diff --git a/packages/cli/bin/config.ts b/packages/cli/bin/config.ts index 8246229587..1771befb5c 100644 --- a/packages/cli/bin/config.ts +++ b/packages/cli/bin/config.ts @@ -1,4 +1,3 @@ -import path from 'node:path'; import type { Command, OptionValues } from '@commander-js/extra-typings'; import pc from 'picocolors'; import * as R from 'ramda'; @@ -10,14 +9,14 @@ import { parseConfig, validateConfig, } from '../src/config.js'; -import { readFile } from '../src/utils.js'; +import fs from '../src/utils/filesystem.js'; import { getCliOption, getDefaultCliOption, getSuppliedCliOption, type OptionGetter } from './options.js'; export async function readConfigFile(configPath: string, allowFileNotFound = true): Promise { let configFile: string; try { - configFile = await readFile(configPath, false, allowFileNotFound); + configFile = await fs.readFile(configPath, allowFileNotFound); } catch (err) { if (allowFileNotFound) { return ''; diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index fc599af7b3..3b75ee8db6 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -11,7 +11,7 @@ import { writeTokens } from '../src/tokens/create/write.js'; import { cliOptions, createTokens } from '../src/tokens/create.js'; import { generateConfigFromTokens } from '../src/tokens/generate-config.js'; import type { Theme } from '../src/tokens/types.js'; -import { cleanDir } from '../src/utils.js'; +import fs from '../src/utils/filesystem.js'; import { parseBuildConfig, parseCreateConfig, readConfigFile } from './config.js'; export const figletAscii = ` @@ -60,21 +60,17 @@ function makeTokenCommands() { const { configFile, configPath } = await getConfigFile(configFilePath); const config = await parseBuildConfig(configFile, { configPath }); - const writeDir = path.resolve(workingDir, outDir); - - if (dry) { - console.log(`Performing dry run, no files will be written`); - } + const writeDir = path.resolve(workingDir, outDir); // TODO read output directory from config or CLI options + fs.init({ dry, writeDir }); if (clean) { - await cleanDir(writeDir, dry); + await fs.cleanDir(writeDir); } await buildTokens({ tokensDir: tokens, outDir: writeDir, verbose, - dry, tailwind: experimentalTailwind, ...config, }); @@ -119,11 +115,12 @@ function makeTokenCommands() { cmd, configPath, }); - const writeDir = path.resolve(workingDir, config.outDir); + fs.init({ dry: opts.dry, writeDir }); + if (config.clean) { - await cleanDir(writeDir, opts.dry); + await fs.cleanDir(writeDir); } /* @@ -135,7 +132,7 @@ function makeTokenCommands() { const theme = { name, ...themeWithoutName } as Theme; const { tokenSets } = await createTokens(theme); - await writeTokens({ outDir: writeDir, theme, dry: opts.dry, tokenSets }); + await writeTokens({ outDir: writeDir, theme, tokenSets }); } } }); diff --git a/packages/cli/src/migrations/codemods/css/run.ts b/packages/cli/src/migrations/codemods/css/run.ts index 482893cf1f..73b36210c0 100644 --- a/packages/cli/src/migrations/codemods/css/run.ts +++ b/packages/cli/src/migrations/codemods/css/run.ts @@ -1,9 +1,7 @@ -import fs from 'node:fs'; - import glob from 'fast-glob'; import type { AcceptedPlugin } from 'postcss'; import postcss from 'postcss'; -import { readFile } from '../../../utils.js'; +import fs from '../../../../src/utils/filesystem.js'; type CssCodemodProps = { plugins: AcceptedPlugin[]; @@ -25,10 +23,10 @@ export const runCssCodemod = async ({ plugins = [], globPattern = './**/*.css' } // console.log(`Skipping ${file}`); return; } - const contents = readFile(file).toString(); - const result = await processor.process(contents, { from: file }); + const contents = await fs.readFile(file); + const result = await processor.process(contents.toString(), { from: file }); - fs.writeFileSync(file, result.css); + await fs.writeFile(file, result.css); }); await Promise.all(filePromises); diff --git a/packages/cli/src/scripts/update-preview-tokens.ts b/packages/cli/src/scripts/update-preview-tokens.ts index 24615c5e6a..897d518656 100644 --- a/packages/cli/src/scripts/update-preview-tokens.ts +++ b/packages/cli/src/scripts/update-preview-tokens.ts @@ -7,11 +7,11 @@ import { createTokens } from '../tokens/create.js'; import { buildOptions, processPlatform } from '../tokens/process/platform.js'; import { processThemeObject } from '../tokens/process/utils/getMultidimensionalThemes.js'; import type { OutputFile, SizeModes, Theme } from '../tokens/types.js'; -import { cleanDir, mkdir, writeFile } from '../utils.js'; +import fs from '../utils/filesystem.js'; const OUTDIR = '../../internal/components/src/tokens/design-tokens'; -async function write(files: OutputFile[], outDir: string, dry?: boolean) { +async function write(files: OutputFile[], outDir: string) { for (const { destination, output } of files) { if (destination) { const filePath = path.join(outDir, destination); @@ -19,8 +19,8 @@ async function write(files: OutputFile[], outDir: string, dry?: boolean) { console.log(`Writing file: ${pc.green(filePath)}`); - await mkdir(fileDir, dry); - await writeFile(filePath, output, dry); + await fs.mkdir(fileDir); + await fs.writeFile(filePath, output); } } } @@ -55,7 +55,7 @@ export const formatTheme = async (themeConfig: Theme) => { buildTokenFormats: {}, }); - await cleanDir(OUTDIR, false); + await fs.cleanDir(OUTDIR); console.log( buildOptions?.buildTokenFormats @@ -103,7 +103,6 @@ export const formatTheme = async (themeConfig: Theme) => { }, ], OUTDIR, - false, ); } console.log(`\n✅ Finished building preview tokens for ${pc.blue('Designsystemet')}`); diff --git a/packages/cli/src/tokens/build.ts b/packages/cli/src/tokens/build.ts index 29c258e5e7..2618154cba 100644 --- a/packages/cli/src/tokens/build.ts +++ b/packages/cli/src/tokens/build.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import type { ThemeObject } from '@tokens-studio/types'; import pc from 'picocolors'; import * as R from 'ramda'; -import { mkdir, readFile, writeFile } from '../utils.js'; +import fs from '../utils/filesystem.js'; import { createTypeDeclarationFiles } from './process/output/declarations.js'; import { createTailwindCSSFiles } from './process/output/tailwind.js'; import { createThemeCSSFiles, defaultFileHeader } from './process/output/theme.js'; @@ -10,7 +10,7 @@ import { type BuildOptions, processPlatform } from './process/platform.js'; import { processThemeObject } from './process/utils/getMultidimensionalThemes.js'; import type { DesignsystemetObject, OutputFile } from './types.js'; -async function write(files: OutputFile[], outDir: string, dry?: boolean) { +async function write(files: OutputFile[], outDir: string) { for (const { destination, output } of files) { if (destination) { const filePath = path.join(outDir, destination); @@ -18,8 +18,8 @@ async function write(files: OutputFile[], outDir: string, dry?: boolean) { console.log(destination); - await mkdir(fileDir, dry); - await writeFile(filePath, output, dry); + await fs.mkdir(fileDir); + await fs.writeFile(filePath, output); } } } @@ -27,12 +27,12 @@ async function write(files: OutputFile[], outDir: string, dry?: boolean) { export const buildTokens = async (options: Omit) => { const outDir = path.resolve(options.outDir); const tokensDir = path.resolve(options.tokensDir); - const $themes = JSON.parse(await readFile(`${tokensDir}/$themes.json`)) as ThemeObject[]; + const $themes = JSON.parse(await fs.readFile(`${tokensDir}/$themes.json`)) as ThemeObject[]; const processed$themes = $themes.map(processThemeObject); let $designsystemet: DesignsystemetObject | undefined; try { - const $designsystemetContent = await readFile(`${tokensDir}/$designsystemet.jsonc`); + const $designsystemetContent = await fs.readFile(`${tokensDir}/$designsystemet.jsonc`); $designsystemet = JSON.parse($designsystemetContent) as DesignsystemetObject; } catch (_error) {} @@ -47,14 +47,6 @@ export const buildTokens = async (options: Omit JSON.stringify(data, null, 2); type WriteTokensOptions = { outDir: string; theme: Theme; - /** Dry run, no files will be written */ - dry?: boolean; tokenSets: TokenSets; }; @@ -23,7 +21,6 @@ export const writeTokens = async (options: WriteTokensOptions) => { outDir, tokenSets, theme: { name: themeName, colors }, - dry, } = options; const $themesPath = path.join(outDir, '$themes.json'); @@ -32,11 +29,11 @@ export const writeTokens = async (options: WriteTokensOptions) => { let themeObjects: ThemeObject[] = []; const sizeModes: SizeModes[] = ['small', 'medium', 'large']; - await mkdir(outDir, dry); + await fs.mkdir(outDir); try { // Fetch existing themes - const $themes = await readFile($themesPath); + const $themes = await fs.readFile($themesPath); if ($themes) { themeObjects = JSON.parse($themes) as ThemeObject[]; } @@ -59,17 +56,17 @@ export const writeTokens = async (options: WriteTokensOptions) => { const $metadata = generate$Metadata(['dark', 'light'], themes, colors, sizeModes); const $designsystemet = generate$Designsystemet(); - await writeFile($themesPath, stringify($themes), dry); - await writeFile($metadataPath, stringify($metadata), dry); - await writeFile($designsystemetPath, stringify($designsystemet), dry); + await fs.writeFile($themesPath, stringify($themes)); + await fs.writeFile($metadataPath, stringify($metadata)); + await fs.writeFile($designsystemetPath, stringify($designsystemet)); for (const [set, tokens] of tokenSets) { // Remove last part of the path to get the directory const fileDir = path.join(outDir, path.dirname(set)); - await mkdir(fileDir, dry); + await fs.mkdir(fileDir); const filePath = path.join(outDir, `${set}.json`); - await writeFile(filePath, stringify(tokens), dry); + await fs.writeFile(filePath, stringify(tokens)); } console.log(`Finished creating Designsystem design tokens in ${pc.green(outDir)} for theme ${pc.blue(themeName)}`); diff --git a/packages/cli/src/tokens/generate-config.ts b/packages/cli/src/tokens/generate-config.ts index f8d244d46a..25e29e5882 100644 --- a/packages/cli/src/tokens/generate-config.ts +++ b/packages/cli/src/tokens/generate-config.ts @@ -1,8 +1,8 @@ -import fs from 'node:fs/promises'; import path from 'node:path'; import pc from 'picocolors'; import type { CssColor } from '../colors/types.js'; import type { CreateConfigSchema } from '../config.js'; +import fs from '../utils/filesystem.js'; type TokenValue = { $type: string; @@ -18,7 +18,7 @@ type TokenObject = { */ async function readJsonFile(filePath: string): Promise { try { - const content = await fs.readFile(filePath, 'utf-8'); + const content = await fs.readFile(filePath); return JSON.parse(content) as TokenObject; } catch (err) { throw new Error(`Failed to read token file at ${filePath}: ${err instanceof Error ? err.message : String(err)}`); @@ -209,14 +209,13 @@ function categorizeColors( export type GenerateConfigOptions = { tokensDir: string; outFile?: string; - dry?: boolean; }; /** * Generates a config file from existing design tokens */ export async function generateConfigFromTokens(options: GenerateConfigOptions): Promise { - const { tokensDir, dry = false } = options; + const { tokensDir } = options; console.log(`\nReading tokens from ${pc.blue(tokensDir)}`); @@ -291,9 +290,9 @@ export async function generateConfigFromTokens(options: GenerateConfigOptions): } } - if (!dry && options.outFile) { + if (options.outFile) { const configJson = JSON.stringify(config, null, 2); - await fs.writeFile(options.outFile, configJson, 'utf-8'); + await fs.writeFile(options.outFile, configJson); console.log(); console.log(`\n✅ Config file written to ${pc.blue(options.outFile)}`); } diff --git a/packages/cli/src/tokens/process/platform.ts b/packages/cli/src/tokens/process/platform.ts index 22ea78ace4..b38924ca4a 100644 --- a/packages/cli/src/tokens/process/platform.ts +++ b/packages/cli/src/tokens/process/platform.ts @@ -16,8 +16,6 @@ type SharedOptions = { defaultSize?: string; /** Set the available size modes */ sizeModes?: string[]; - /** Dry run, no files will be written */ - dry?: boolean; /** Token Studio `$themes.json` content */ processed$themes: ProcessedThemeObject[]; /** Color groups */ diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts deleted file mode 100644 index 0c99590551..0000000000 --- a/packages/cli/src/utils.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { CopyOptions } from 'node:fs'; -import fs from 'node:fs/promises'; -import pc from 'picocolors'; - -/** - * An abstraction of Node's file system API which allows dry-running destructive operations - */ -export class FileSystem { - private dry: boolean; - workingDir: string = process.cwd(); - - constructor( - /** Dry-run destructive operations instead of actually performing them */ - dry = false, - ) { - this.dry = dry; - } - - /** - * Creates a directory if it does not already exist. - * - * @param dir - The path of the directory to create. - * - * @returns A promise that resolves when the operation is complete. - * If the directory already exists or `dry` is `true`, the promise resolves immediately. - */ - mkdir = async (dir: string) => { - if (this.dry) { - console.log(`${pc.blue('mkdir')} ${dir}`); - return Promise.resolve(); - } - - const exists = await fs - .access(dir, fs.constants.F_OK) - .then(() => true) - .catch(() => false); - - if (exists) { - return Promise.resolve(); - } - - return fs.mkdir(dir, { recursive: true }); - }; - - writeFile = async (path: string, data: string) => { - if (this.dry) { - console.log(`${pc.blue('writeFile')} ${path}`); - return Promise.resolve(); - } - - return fs.writeFile(path, data, { encoding: 'utf-8' }).catch((error) => { - console.error(pc.red(`Error writing file: ${path}`)); - console.error(pc.red(error)); - throw error; - }); - }; - - cp = async (src: string, dest: string, filter?: CopyOptions['filter']) => { - if (this.dry) { - console.log(`${pc.blue('cp')} ${src} ${dest}`); - return Promise.resolve(); - } - - return fs.cp(src, dest, { recursive: true, filter }); - }; - - copyFile = async (src: string, dest: string) => { - if (this.dry) { - console.log(`${pc.blue('copyFile')} ${src} to ${dest}`); - return Promise.resolve(); - } - - return fs.copyFile(src, dest); - }; - - cleanDir = async (dir: string) => { - if (this.dry) { - console.log(`${pc.blue('cleanDir')} ${dir}`); - return Promise.resolve(); - } - - console.log(`\n🔥 Cleaning dir ${pc.red(`${dir.trim()}`)} `); - - return fs.rm(dir, { recursive: true, force: true }); - }; - - readFile = async (path: string, allowFileNotFound?: boolean) => { - if (this.dry) { - console.log(`${pc.blue('readFile')} ${path}`); - return Promise.resolve(''); - } - - try { - return await fs.readFile(path, 'utf-8'); - } catch (error) { - if (allowFileNotFound && (error as NodeJS.ErrnoException).code === 'ENOENT') { - return ''; - } - throw error; - } - }; -} - -/** - * Creates a directory if it does not already exist. - * - * @param dir - The path of the directory to create. - * @param dry - Optional. If `true`, the function will log the operation - * without actually creating the directory. - * - * @returns A promise that resolves when the operation is complete. - * If the directory already exists or `dry` is `true`, the promise resolves immediately. - */ -export const mkdir = async (dir: string, dry?: boolean) => { - if (dry) { - console.log(`${pc.blue('mkdir')} ${dir}`); - return Promise.resolve(); - } - - const exists = await fs - .access(dir, fs.constants.F_OK) - .then(() => true) - .catch(() => false); - - if (exists) { - return Promise.resolve(); - } - - return fs.mkdir(dir, { recursive: true }); -}; - -export const writeFile = async (path: string, data: string, dry?: boolean) => { - if (dry) { - console.log(`${pc.blue('writeFile')} ${path}`); - return Promise.resolve(); - } - - return fs.writeFile(path, data, { encoding: 'utf-8' }).catch((error) => { - console.error(pc.red(`Error writing file: ${path}`)); - console.error(pc.red(error)); - throw error; - }); -}; - -export const cp = async (src: string, dest: string, dry?: boolean, filter?: CopyOptions['filter']) => { - if (dry) { - console.log(`${pc.blue('cp')} ${src} ${dest}`); - return Promise.resolve(); - } - - return fs.cp(src, dest, { recursive: true, filter }); -}; - -export const copyFile = async (src: string, dest: string, dry?: boolean) => { - if (dry) { - console.log(`${pc.blue('copyFile')} ${src} to ${dest}`); - return Promise.resolve(); - } - - return fs.copyFile(src, dest); -}; - -export const cleanDir = async (dir: string, dry?: boolean) => { - if (dry) { - console.log(`${pc.blue('cleanDir')} ${dir}`); - return Promise.resolve(); - } - - console.log(`\n🔥 Cleaning dir ${pc.red(`${dir.trim()}`)} `); - - return fs.rm(dir, { recursive: true, force: true }); -}; - -export const readFile = async (path: string, dry?: boolean, allowFileNotFound?: boolean) => { - if (dry) { - console.log(`${pc.blue('readFile')} ${path}`); - return Promise.resolve(''); - } - - try { - return await fs.readFile(path, 'utf-8'); - } catch (error) { - if (allowFileNotFound && (error as NodeJS.ErrnoException).code === 'ENOENT') { - return ''; - } - throw error; - } -}; diff --git a/packages/cli/src/utils/filesystem.ts b/packages/cli/src/utils/filesystem.ts new file mode 100644 index 0000000000..0af638dbd2 --- /dev/null +++ b/packages/cli/src/utils/filesystem.ts @@ -0,0 +1,132 @@ +import type { CopyOptions } from 'node:fs'; +import fs from 'node:fs/promises'; +import pc from 'picocolors'; + +class FileSystem { + private isInitialized = false; + private dry = false; + /** Resolved write directory */ + writeDir = process.cwd(); + + /** Initialize the file system */ + init({ dry, writeDir }: { dry?: boolean; writeDir?: string }) { + if (this.isInitialized) { + console.warn(pc.yellow('FileSystem is already initialized. Ignoring subsequent init call.')); + return; + } + + if (dry) { + console.log(pc.blue('Initializing FileSystem in dry-run mode. No files will be written.')); + } + + this.dry = dry ?? false; + this.writeDir = writeDir ?? process.cwd(); + this.isInitialized = true; + } + + /** + * Creates a directory if it does not already exist. + * + * @param dir - The path of the directory to create. + * + * @returns A promise that resolves when the operation is complete. + * If the directory already exists or `dry` is `true`, the promise resolves immediately. + */ + mkdir = async (dir: string) => { + if (this.dry) { + console.log(`${pc.blue('mkdir')} ${dir}`); + return Promise.resolve(); + } + + const exists = await fs + .access(dir, fs.constants.F_OK) + .then(() => true) + .catch(() => false); + + if (exists) { + return Promise.resolve(); + } + + return fs.mkdir(dir, { recursive: true }); + }; + + writeFile = async (path: string, data: string) => { + if (this.dry) { + console.log(`${pc.blue('writeFile')} ${path}`); + return Promise.resolve(); + } + + return fs.writeFile(path, data, { encoding: 'utf-8' }).catch((error) => { + console.error(pc.red(`Error writing file: ${path}`)); + console.error(pc.red(error)); + throw error; + }); + }; + + cp = async (src: string, dest: string, filter?: CopyOptions['filter']) => { + if (this.dry) { + console.log(`${pc.blue('cp')} ${src} ${dest}`); + return Promise.resolve(); + } + + return fs.cp(src, dest, { recursive: true, filter }); + }; + + copyFile = async (src: string, dest: string) => { + if (this.dry) { + console.log(`${pc.blue('copyFile')} ${src} to ${dest}`); + return Promise.resolve(); + } + + return fs.copyFile(src, dest); + }; + + cleanDir = async (dir: string) => { + if (this.dry) { + console.log(`${pc.blue('cleanDir')} ${dir}`); + return Promise.resolve(); + } + + console.log(`\n🔥 Cleaning dir ${pc.red(`${dir.trim()}`)} `); + + return fs.rm(dir, { recursive: true, force: true }); + }; + + readFile = async (path: string, allowFileNotFound?: boolean) => { + if (this.dry) { + console.log(`${pc.blue('readFile')} ${path}`); + return Promise.resolve(''); + } + + try { + return await fs.readFile(path, 'utf-8'); + } catch (error) { + if (allowFileNotFound && (error as NodeJS.ErrnoException).code === 'ENOENT') { + return ''; + } + throw error; + } + }; + readdir = async (path: string) => { + if (this.dry) { + console.log(`${pc.blue('readdir')} ${path}`); + return Promise.resolve([]); + } + + try { + return await fs.readdir(path); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return []; + } + throw error; + } + }; +} + +/** + * An abstraction of Node's file system API for how CLI should interact with Files system. + * + * Allows dry-running destructive operations, logging and store relevant file system state. + */ +export default new FileSystem(); From 266600288280abbe0d49830d2e7d5877b968a5cd Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 9 Feb 2026 16:34:48 +0100 Subject: [PATCH 05/11] minimizing file system interaction --- packages/cli/bin/designsystemet.ts | 37 +++++++++++++-------- packages/cli/src/tokens/build.ts | 35 ++++++++----------- packages/cli/src/tokens/create/write.ts | 2 -- packages/cli/src/tokens/process/platform.ts | 2 -- packages/cli/src/tokens/types.ts | 1 + 5 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 3b75ee8db6..8fc32fd96f 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -6,7 +6,7 @@ import * as R from 'ramda'; import { convertToHex } from '../src/colors/index.js'; import type { CssColor } from '../src/colors/types.js'; import migrations from '../src/migrations/index.js'; -import { buildTokens } from '../src/tokens/build.js'; +import { buildTokens, writeFiles } from '../src/tokens/build.js'; import { writeTokens } from '../src/tokens/create/write.js'; import { cliOptions, createTokens } from '../src/tokens/create.js'; import { generateConfigFromTokens } from '../src/tokens/generate-config.js'; @@ -52,7 +52,7 @@ function makeTokenCommands() { .option('--experimental-tailwind', 'Generate Tailwind CSS classes for tokens', false) .action(async (opts) => { console.log(figletAscii); - const { verbose, clean, dry, experimentalTailwind, outDir, tokens } = opts; + const { verbose, clean, dry, experimentalTailwind, tokens } = opts; const configFilePath = opts.config; const workingDir = configFilePath ? path.dirname(configFilePath) : process.cwd(); @@ -60,21 +60,27 @@ function makeTokenCommands() { const { configFile, configPath } = await getConfigFile(configFilePath); const config = await parseBuildConfig(configFile, { configPath }); - const writeDir = path.resolve(workingDir, outDir); // TODO read output directory from config or CLI options - fs.init({ dry, writeDir }); + const outDir = path.resolve(workingDir, opts.outDir); // TODO read output directory from config or CLI options + + fs.init({ dry }); if (clean) { - await fs.cleanDir(writeDir); + await fs.cleanDir(outDir); } - await buildTokens({ + const files = await buildTokens({ tokensDir: tokens, - outDir: writeDir, verbose, tailwind: experimentalTailwind, ...config, }); + console.log(`\n💾 Writing build to ${pc.green(outDir)}`); + + await writeFiles(files, outDir); + + console.log(`\n✅ Finished building tokens in ${pc.green(outDir)}`); + return Promise.resolve(); }); @@ -115,12 +121,12 @@ function makeTokenCommands() { cmd, configPath, }); - const writeDir = path.resolve(workingDir, config.outDir); - fs.init({ dry: opts.dry, writeDir }); + const outDir = path.resolve(workingDir, config.outDir); + fs.init({ dry: opts.dry }); if (config.clean) { - await fs.cleanDir(writeDir); + await fs.cleanDir(outDir); } /* @@ -132,9 +138,13 @@ function makeTokenCommands() { const theme = { name, ...themeWithoutName } as Theme; const { tokenSets } = await createTokens(theme); - await writeTokens({ outDir: writeDir, theme, tokenSets }); + await writeTokens({ outDir, theme, tokenSets }); } } + + console.log(`\n✅ Finished creating tokens in ${pc.green(outDir)} for theme: ${pc.blue(themeName)}`); + + return Promise.resolve(); }); return tokenCmd; @@ -154,11 +164,12 @@ program const tokensDir = typeof opts.dir === 'string' ? opts.dir : DEFAULT_TOKENS_CREATE_DIR; const outFile = typeof opts.out === 'string' ? opts.out : DEFAULT_CONFIG_FILE; + fs.init({ dry }); + try { const config = await generateConfigFromTokens({ tokensDir, - outFile: dry ? undefined : outFile, - dry, + outFile, }); if (dry) { diff --git a/packages/cli/src/tokens/build.ts b/packages/cli/src/tokens/build.ts index 2618154cba..621bdb7618 100644 --- a/packages/cli/src/tokens/build.ts +++ b/packages/cli/src/tokens/build.ts @@ -10,22 +10,7 @@ import { type BuildOptions, processPlatform } from './process/platform.js'; import { processThemeObject } from './process/utils/getMultidimensionalThemes.js'; import type { DesignsystemetObject, OutputFile } from './types.js'; -async function write(files: OutputFile[], outDir: string) { - for (const { destination, output } of files) { - if (destination) { - const filePath = path.join(outDir, destination); - const fileDir = path.dirname(filePath); - - console.log(destination); - - await fs.mkdir(fileDir); - await fs.writeFile(filePath, output); - } - } -} - export const buildTokens = async (options: Omit) => { - const outDir = path.resolve(options.outDir); const tokensDir = path.resolve(options.tokensDir); const $themes = JSON.parse(await fs.readFile(`${tokensDir}/$themes.json`)) as ThemeObject[]; const processed$themes = $themes.map(processThemeObject); @@ -40,7 +25,6 @@ export const buildTokens = async (options: Omit { const filePath = path.join(outDir, `${set}.json`); await fs.writeFile(filePath, stringify(tokens)); } - - console.log(`Finished creating Designsystem design tokens in ${pc.green(outDir)} for theme ${pc.blue(themeName)}`); }; diff --git a/packages/cli/src/tokens/process/platform.ts b/packages/cli/src/tokens/process/platform.ts index b38924ca4a..0872ef1a49 100644 --- a/packages/cli/src/tokens/process/platform.ts +++ b/packages/cli/src/tokens/process/platform.ts @@ -28,8 +28,6 @@ export type BuildOptions = { type: 'build'; /** Design tokens path */ tokensDir: string; - /** Output directory for built tokens */ - outDir: string; /** Tailwind CSS configuration */ tailwind?: boolean; } & SharedOptions; diff --git a/packages/cli/src/tokens/types.ts b/packages/cli/src/tokens/types.ts index 67a5db9919..fde2249900 100644 --- a/packages/cli/src/tokens/types.ts +++ b/packages/cli/src/tokens/types.ts @@ -70,6 +70,7 @@ export type BuildConfig = { export type SDConfigForThemePermutation = { permutation: ThemePermutation; config: SDConfig }; +/** This type is taken from Style Dictionary `formatPlatform` */ export type OutputFile = { output: string; destination: string; From 30e64ea087e740b2471cbc311c8b972ba79efa75 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 10 Feb 2026 09:26:44 +0100 Subject: [PATCH 06/11] conformed io using new fs --- packages/cli/bin/designsystemet.ts | 25 ++++++++++------ packages/cli/src/tokens/create/write.ts | 28 +++++++++--------- packages/cli/src/tokens/generate-config.ts | 7 ----- packages/cli/src/tokens/types.ts | 3 +- packages/cli/src/utils/filesystem.ts | 33 ++++++++++++++++------ 5 files changed, 56 insertions(+), 40 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 8fc32fd96f..ab6ea58c36 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -6,11 +6,11 @@ import * as R from 'ramda'; import { convertToHex } from '../src/colors/index.js'; import type { CssColor } from '../src/colors/types.js'; import migrations from '../src/migrations/index.js'; -import { buildTokens, writeFiles } from '../src/tokens/build.js'; +import { buildTokens } from '../src/tokens/build.js'; import { writeTokens } from '../src/tokens/create/write.js'; import { cliOptions, createTokens } from '../src/tokens/create.js'; import { generateConfigFromTokens } from '../src/tokens/generate-config.js'; -import type { Theme } from '../src/tokens/types.js'; +import type { OutputFile, Theme } from '../src/tokens/types.js'; import fs from '../src/utils/filesystem.js'; import { parseBuildConfig, parseCreateConfig, readConfigFile } from './config.js'; @@ -77,7 +77,7 @@ function makeTokenCommands() { console.log(`\n💾 Writing build to ${pc.green(outDir)}`); - await writeFiles(files, outDir); + await fs.writeFiles(files, outDir, true); console.log(`\n✅ Finished building tokens in ${pc.green(outDir)}`); @@ -129,19 +129,19 @@ function makeTokenCommands() { await fs.cleanDir(outDir); } - /* - * Create and write tokens for each theme - */ + let files: OutputFile[] = []; if (config.themes) { for (const [name, themeWithoutName] of Object.entries(config.themes)) { // Casting as missing properties should be validated by `getDefaultOrExplicitOption` to default values const theme = { name, ...themeWithoutName } as Theme; const { tokenSets } = await createTokens(theme); - await writeTokens({ outDir, theme, tokenSets }); + files = files.concat(await writeTokens({ outDir, theme, tokenSets })); } } + await fs.writeFiles(files, outDir); + console.log(`\n✅ Finished creating tokens in ${pc.green(outDir)} for theme: ${pc.blue(themeName)}`); return Promise.resolve(); @@ -161,8 +161,8 @@ program .action(async (opts) => { console.log(figletAscii); const { dry } = opts; - const tokensDir = typeof opts.dir === 'string' ? opts.dir : DEFAULT_TOKENS_CREATE_DIR; - const outFile = typeof opts.out === 'string' ? opts.out : DEFAULT_CONFIG_FILE; + const tokensDir = path.resolve(opts.dir); + const outFile = path.resolve(opts.out); fs.init({ dry }); @@ -177,6 +177,13 @@ program console.log('Generated config (dry run):'); console.log(JSON.stringify(config, null, 2)); } + + if (outFile) { + const configJson = JSON.stringify(config, null, 2); + await fs.writeFile(outFile, configJson); + console.log(); + console.log(`\n✅ Config file written to ${pc.blue(outFile)}`); + } } catch (error) { console.error(pc.redBright('Error generating config:')); console.error(error instanceof Error ? error.message : String(error)); diff --git a/packages/cli/src/tokens/create/write.ts b/packages/cli/src/tokens/create/write.ts index c2ce97af3d..178539d8f5 100644 --- a/packages/cli/src/tokens/create/write.ts +++ b/packages/cli/src/tokens/create/write.ts @@ -3,7 +3,7 @@ import type { ThemeObject } from '@tokens-studio/types'; import pc from 'picocolors'; import * as R from 'ramda'; import fs from '../../utils/filesystem.js'; -import type { SizeModes, Theme, TokenSets } from '../types.js'; +import type { OutputFile, SizeModes, Theme, TokenSets } from '../types.js'; import { generate$Designsystemet } from './generators/$designsystemet.js'; import { generate$Metadata } from './generators/$metadata.js'; import { generate$Themes } from './generators/$themes.js'; @@ -23,9 +23,9 @@ export const writeTokens = async (options: WriteTokensOptions) => { theme: { name: themeName, colors }, } = options; - const $themesPath = path.join(outDir, '$themes.json'); - const $metadataPath = path.join(outDir, '$metadata.json'); - const $designsystemetPath = path.join(outDir, '$designsystemet.jsonc'); + const $themesPath = '$themes.json'; + const $metadataPath = '$metadata.json'; + const $designsystemetPath = '$designsystemet.jsonc'; let themeObjects: ThemeObject[] = []; const sizeModes: SizeModes[] = ['small', 'medium', 'large']; @@ -33,7 +33,7 @@ export const writeTokens = async (options: WriteTokensOptions) => { try { // Fetch existing themes - const $themes = await fs.readFile($themesPath); + const $themes = await fs.readFile(path.join(outDir, $themesPath)); if ($themes) { themeObjects = JSON.parse($themes) as ThemeObject[]; } @@ -56,16 +56,16 @@ export const writeTokens = async (options: WriteTokensOptions) => { const $metadata = generate$Metadata(['dark', 'light'], themes, colors, sizeModes); const $designsystemet = generate$Designsystemet(); - await fs.writeFile($themesPath, stringify($themes)); - await fs.writeFile($metadataPath, stringify($metadata)); - await fs.writeFile($designsystemetPath, stringify($designsystemet)); + const files: OutputFile[] = []; - for (const [set, tokens] of tokenSets) { - // Remove last part of the path to get the directory - const fileDir = path.join(outDir, path.dirname(set)); - await fs.mkdir(fileDir); + files.push({ destination: $themesPath, output: stringify($themes) }); + files.push({ destination: $metadataPath, output: stringify($metadata) }); + files.push({ destination: $designsystemetPath, output: stringify($designsystemet) }); - const filePath = path.join(outDir, `${set}.json`); - await fs.writeFile(filePath, stringify(tokens)); + for (const [set, tokens] of tokenSets) { + const filePath = `${set}.json`; + files.push({ destination: filePath, output: stringify(tokens) }); } + + return files; }; diff --git a/packages/cli/src/tokens/generate-config.ts b/packages/cli/src/tokens/generate-config.ts index 25e29e5882..d7c3c84f81 100644 --- a/packages/cli/src/tokens/generate-config.ts +++ b/packages/cli/src/tokens/generate-config.ts @@ -290,12 +290,5 @@ export async function generateConfigFromTokens(options: GenerateConfigOptions): } } - if (options.outFile) { - const configJson = JSON.stringify(config, null, 2); - await fs.writeFile(options.outFile, configJson); - console.log(); - console.log(`\n✅ Config file written to ${pc.blue(options.outFile)}`); - } - return config; } diff --git a/packages/cli/src/tokens/types.ts b/packages/cli/src/tokens/types.ts index fde2249900..731f818cc7 100644 --- a/packages/cli/src/tokens/types.ts +++ b/packages/cli/src/tokens/types.ts @@ -70,7 +70,8 @@ export type BuildConfig = { export type SDConfigForThemePermutation = { permutation: ThemePermutation; config: SDConfig }; -/** This type is taken from Style Dictionary `formatPlatform` */ +/** This type is taken from Style Dictionary `formatPlatform`. + * Metadata for a file output */ export type OutputFile = { output: string; destination: string; diff --git a/packages/cli/src/utils/filesystem.ts b/packages/cli/src/utils/filesystem.ts index 0af638dbd2..48cbbb189a 100644 --- a/packages/cli/src/utils/filesystem.ts +++ b/packages/cli/src/utils/filesystem.ts @@ -1,6 +1,8 @@ -import type { CopyOptions } from 'node:fs'; +import type { CopyOptions, PathLike } from 'node:fs'; import fs from 'node:fs/promises'; +import path from 'node:path'; import pc from 'picocolors'; +import type { OutputFile } from '../tokens/types.js'; class FileSystem { private isInitialized = false; @@ -32,7 +34,7 @@ class FileSystem { * @returns A promise that resolves when the operation is complete. * If the directory already exists or `dry` is `true`, the promise resolves immediately. */ - mkdir = async (dir: string) => { + mkdir = async (dir: PathLike) => { if (this.dry) { console.log(`${pc.blue('mkdir')} ${dir}`); return Promise.resolve(); @@ -50,7 +52,7 @@ class FileSystem { return fs.mkdir(dir, { recursive: true }); }; - writeFile = async (path: string, data: string) => { + writeFile = async (path: PathLike, data: string) => { if (this.dry) { console.log(`${pc.blue('writeFile')} ${path}`); return Promise.resolve(); @@ -72,7 +74,7 @@ class FileSystem { return fs.cp(src, dest, { recursive: true, filter }); }; - copyFile = async (src: string, dest: string) => { + copyFile = async (src: PathLike, dest: PathLike) => { if (this.dry) { console.log(`${pc.blue('copyFile')} ${src} to ${dest}`); return Promise.resolve(); @@ -92,10 +94,9 @@ class FileSystem { return fs.rm(dir, { recursive: true, force: true }); }; - readFile = async (path: string, allowFileNotFound?: boolean) => { + readFile = async (path: PathLike, allowFileNotFound?: boolean) => { if (this.dry) { console.log(`${pc.blue('readFile')} ${path}`); - return Promise.resolve(''); } try { @@ -107,10 +108,9 @@ class FileSystem { throw error; } }; - readdir = async (path: string) => { + readdir = async (path: PathLike) => { if (this.dry) { console.log(`${pc.blue('readdir')} ${path}`); - return Promise.resolve([]); } try { @@ -122,10 +122,25 @@ class FileSystem { throw error; } }; + writeFiles = async (files: OutputFile[], outDir: string, log?: boolean) => { + for (const { destination: filename, output } of files) { + if (filename) { + const filePath = path.join(outDir, filename); + const fileDir = path.dirname(filePath); + + if (log) { + console.log(filename); + } + + await this.mkdir(fileDir); + await this.writeFile(filePath, output); + } + } + }; } /** - * An abstraction of Node's file system API for how CLI should interact with Files system. + * An abstraction of Node's file system API and helper functions for CLI interaction with the file system. * * Allows dry-running destructive operations, logging and store relevant file system state. */ From 411c82508a84899f8bf69ceb3c371d38bb181ceb Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 10 Feb 2026 09:42:39 +0100 Subject: [PATCH 07/11] cleaning up --- packages/cli/bin/designsystemet.ts | 23 +++++++++++------------ packages/cli/src/tokens/build.ts | 17 +---------------- packages/cli/src/utils/filesystem.ts | 14 +++++++++++--- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index ab6ea58c36..a07ccd4f8a 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -55,14 +55,13 @@ function makeTokenCommands() { const { verbose, clean, dry, experimentalTailwind, tokens } = opts; const configFilePath = opts.config; - const workingDir = configFilePath ? path.dirname(configFilePath) : process.cwd(); const { configFile, configPath } = await getConfigFile(configFilePath); const config = await parseBuildConfig(configFile, { configPath }); - const outDir = path.resolve(workingDir, opts.outDir); // TODO read output directory from config or CLI options + fs.init({ dry, configFilePath }); - fs.init({ dry }); + const outDir = fs.getOutdir(opts.outDir); // TODO read output directory from config or CLI options if (clean) { await fs.cleanDir(outDir); @@ -113,7 +112,6 @@ function makeTokenCommands() { } const themeName = opts.theme; const configFilePath = opts.config; - const workingDir = configFilePath ? path.dirname(configFilePath) : process.cwd(); const { configFile, configPath } = await getConfigFile(configFilePath); const config = await parseCreateConfig(configFile, { @@ -122,8 +120,9 @@ function makeTokenCommands() { configPath, }); - const outDir = path.resolve(workingDir, config.outDir); - fs.init({ dry: opts.dry }); + fs.init({ dry: opts.dry, configFilePath }); + + const outDir = fs.getOutdir(config.outDir); if (config.clean) { await fs.cleanDir(outDir); @@ -162,14 +161,14 @@ program console.log(figletAscii); const { dry } = opts; const tokensDir = path.resolve(opts.dir); - const outFile = path.resolve(opts.out); + const configFilePath = path.resolve(opts.out); - fs.init({ dry }); + fs.init({ dry, configFilePath }); try { const config = await generateConfigFromTokens({ tokensDir, - outFile, + outFile: configFilePath, }); if (dry) { @@ -178,11 +177,11 @@ program console.log(JSON.stringify(config, null, 2)); } - if (outFile) { + if (configFilePath) { const configJson = JSON.stringify(config, null, 2); - await fs.writeFile(outFile, configJson); + await fs.writeFile(configFilePath, configJson); console.log(); - console.log(`\n✅ Config file written to ${pc.blue(outFile)}`); + console.log(`\n✅ Config file written to ${pc.blue(configFilePath)}`); } } catch (error) { console.error(pc.redBright('Error generating config:')); diff --git a/packages/cli/src/tokens/build.ts b/packages/cli/src/tokens/build.ts index 621bdb7618..48019cd7f9 100644 --- a/packages/cli/src/tokens/build.ts +++ b/packages/cli/src/tokens/build.ts @@ -1,4 +1,3 @@ -import path from 'node:path'; import type { ThemeObject } from '@tokens-studio/types'; import pc from 'picocolors'; import * as R from 'ramda'; @@ -11,7 +10,7 @@ import { processThemeObject } from './process/utils/getMultidimensionalThemes.js import type { DesignsystemetObject, OutputFile } from './types.js'; export const buildTokens = async (options: Omit) => { - const tokensDir = path.resolve(options.tokensDir); + const tokensDir = options.tokensDir; const $themes = JSON.parse(await fs.readFile(`${tokensDir}/$themes.json`)) as ThemeObject[]; const processed$themes = $themes.map(processThemeObject); let $designsystemet: DesignsystemetObject | undefined; @@ -50,17 +49,3 @@ export const buildTokens = async (options: Omit Date: Tue, 10 Feb 2026 09:51:01 +0100 Subject: [PATCH 08/11] changeset --- .changeset/two-experts-punch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/two-experts-punch.md diff --git a/.changeset/two-experts-punch.md b/.changeset/two-experts-punch.md new file mode 100644 index 0000000000..baf56cb5fb --- /dev/null +++ b/.changeset/two-experts-punch.md @@ -0,0 +1,5 @@ +--- +"@digdir/designsystemet": patch +--- + +fix resolving of `outDir` in config not being relative to config file location From 6a0b30ee8458d1f0e0f38660389d6d9b1e6d8296 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 10 Feb 2026 09:55:11 +0100 Subject: [PATCH 09/11] rename --- packages/cli/bin/designsystemet.ts | 4 ++-- packages/cli/src/tokens/create/write.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index a07ccd4f8a..3e8786cc59 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -7,7 +7,7 @@ import { convertToHex } from '../src/colors/index.js'; import type { CssColor } from '../src/colors/types.js'; import migrations from '../src/migrations/index.js'; import { buildTokens } from '../src/tokens/build.js'; -import { writeTokens } from '../src/tokens/create/write.js'; +import { createTokenFiles } from '../src/tokens/create/write.js'; import { cliOptions, createTokens } from '../src/tokens/create.js'; import { generateConfigFromTokens } from '../src/tokens/generate-config.js'; import type { OutputFile, Theme } from '../src/tokens/types.js'; @@ -135,7 +135,7 @@ function makeTokenCommands() { const theme = { name, ...themeWithoutName } as Theme; const { tokenSets } = await createTokens(theme); - files = files.concat(await writeTokens({ outDir, theme, tokenSets })); + files = files.concat(await createTokenFiles({ outDir, theme, tokenSets })); } } diff --git a/packages/cli/src/tokens/create/write.ts b/packages/cli/src/tokens/create/write.ts index 178539d8f5..1a17efc8cb 100644 --- a/packages/cli/src/tokens/create/write.ts +++ b/packages/cli/src/tokens/create/write.ts @@ -10,13 +10,13 @@ import { generate$Themes } from './generators/$themes.js'; export const stringify = (data: unknown) => JSON.stringify(data, null, 2); -type WriteTokensOptions = { +type CreateTokenFilesOptions = { outDir: string; theme: Theme; tokenSets: TokenSets; }; -export const writeTokens = async (options: WriteTokensOptions) => { +export const createTokenFiles = async (options: CreateTokenFilesOptions) => { const { outDir, tokenSets, From cc5fcbf6fa5f0deefc85d6ba3edfddd81d07c934 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 10 Feb 2026 09:57:05 +0100 Subject: [PATCH 10/11] rename file --- packages/cli/bin/designsystemet.ts | 2 +- packages/cli/src/tokens/create/{write.ts => files.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/cli/src/tokens/create/{write.ts => files.ts} (100%) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 3e8786cc59..8d40b3c4e5 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -7,7 +7,7 @@ import { convertToHex } from '../src/colors/index.js'; import type { CssColor } from '../src/colors/types.js'; import migrations from '../src/migrations/index.js'; import { buildTokens } from '../src/tokens/build.js'; -import { createTokenFiles } from '../src/tokens/create/write.js'; +import { createTokenFiles } from '../src/tokens/create/files.js'; import { cliOptions, createTokens } from '../src/tokens/create.js'; import { generateConfigFromTokens } from '../src/tokens/generate-config.js'; import type { OutputFile, Theme } from '../src/tokens/types.js'; diff --git a/packages/cli/src/tokens/create/write.ts b/packages/cli/src/tokens/create/files.ts similarity index 100% rename from packages/cli/src/tokens/create/write.ts rename to packages/cli/src/tokens/create/files.ts From 8a6f5acc26f337d28bca0a3a43cd310ea7e133e3 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 10 Feb 2026 13:43:17 +0100 Subject: [PATCH 11/11] update preview tokens write --- .../cli/src/scripts/update-preview-tokens.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/scripts/update-preview-tokens.ts b/packages/cli/src/scripts/update-preview-tokens.ts index 897d518656..69a7b65f58 100644 --- a/packages/cli/src/scripts/update-preview-tokens.ts +++ b/packages/cli/src/scripts/update-preview-tokens.ts @@ -1,4 +1,3 @@ -import path from 'node:path'; import pc from 'picocolors'; import type { TransformedToken } from 'style-dictionary/types'; import config from './../../../../designsystemet.config.json' with { type: 'json' }; @@ -6,25 +5,11 @@ import { generate$Themes } from '../tokens/create/generators/$themes.js'; import { createTokens } from '../tokens/create.js'; import { buildOptions, processPlatform } from '../tokens/process/platform.js'; import { processThemeObject } from '../tokens/process/utils/getMultidimensionalThemes.js'; -import type { OutputFile, SizeModes, Theme } from '../tokens/types.js'; +import type { SizeModes, Theme } from '../tokens/types.js'; import fs from '../utils/filesystem.js'; const OUTDIR = '../../internal/components/src/tokens/design-tokens'; -async function write(files: OutputFile[], outDir: string) { - for (const { destination, output } of files) { - if (destination) { - const filePath = path.join(outDir, destination); - const fileDir = path.dirname(filePath); - - console.log(`Writing file: ${pc.green(filePath)}`); - - await fs.mkdir(fileDir); - await fs.writeFile(filePath, output); - } - } -} - const toPreviewToken = (tokens: { token: TransformedToken; formatted: string }[]): PreviewToken[] => tokens.map(({ token, formatted }) => { const [variable, value] = formatted.split(':'); @@ -95,7 +80,7 @@ export const formatTheme = async (themeConfig: Theme) => { console.log(`\n💾 Writing preview tokens`); for (const [type, tokens] of Object.entries(tokensGroupedByType)) { - write( + fs.writeFiles( [ { destination: `${type}.json`,