From 3295ef6f2ebcc80d2287c102aa008a083ada1d85 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 23 Sep 2026 14:14:58 +0200 Subject: [PATCH 01/44] enable new output and root --- designsystemet.config.json | 2 +- packages/cli/bin/designsystemet.ts | 148 +++++++++++----------- packages/cli/src/schemas/schema-output.ts | 7 +- packages/cli/src/schemas/schema.ts | 2 +- 4 files changed, 80 insertions(+), 79 deletions(-) diff --git a/designsystemet.config.json b/designsystemet.config.json index 36950e80d8..af06a9830b 100644 --- a/designsystemet.config.json +++ b/designsystemet.config.json @@ -1,5 +1,5 @@ { - "$schema": "packages/cli/dist/config-full.schema.json", + "$schema": "packages/cli/dist/config.schema.json", "output": [{ "type": "css", "dir": "./packages/css/theme" }, "design-tokens"], "themes": { "designsystemet": { diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 0dc4134a25..a3c20a9675 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -35,19 +35,16 @@ const figletAscii = ` |___/ |___/ `; -program.name('designsystemet').description('CLI for working with Designsystemet').showHelpAfterError(); -program.hook('preAction', () => console.log(figletAscii)); + const DEFAULT_TOKENS_CREATE_DIR = './design-tokens'; const DEFAULT_TOKENS_BUILD_DIR = './design-tokens-build'; const DEFAULT_FONT = 'Inter'; const DEFAULT_THEME_NAME = 'theme'; -const DEFAULT_CONFIG_FILEPATH = 'designsystemet.config.json'; // Default config files to auto-detect when no --config is supplied, in order of precedence. const DEFAULT_CONFIG_FILEPATHS = ['designsystemet.config.json', 'designsystemet.config.jsonc']; +const DEFAULT_CONFIG_FILEPATH = DEFAULT_CONFIG_FILEPATHS[0]; -// Options shared by multiple commands. Factories because commander mutates Option instances, -// so each command needs its own. const configOption = () => new Option( '-c, --config ', @@ -57,73 +54,6 @@ const dryOption = (description = 'Dry run - no files will be written') => new Option('--dry [boolean]', description).argParser(parseBoolean).default(false); const verboseOption = () => new Option('--verbose', 'Enable verbose output').default(false); -function _makeConfigCommand() { - return createCommand('config') - .usage('designsystemet') - .description('Parses config file and run Designsystemet commands') - .addOption(configOption()) - .addOption(dryOption()) - .addOption(verboseOption()) - .action(async (opts) => { - const { verbose, dry } = opts; - - const { configFile, configFilePath } = await getConfigFile(opts.config); - - dsfs.init({ dry, verbose, outdir: path.dirname(configFilePath) }); - - if (!configFile) { - console.error(pc.redBright(`No config file found. Please create one at ${pc.blue(DEFAULT_CONFIG_FILEPATH)}.`)); - process.exit(1); - } - - const parsedConfig = parseConfig(configFile); - // Validate against the public schema first for a user-facing error on unsupported theme fields. - validateConfig(externalConfigSchema, parsedConfig); - const config = validateConfig(configSchema, parsedConfig); - - // Sort outputs so that design-tokens are generated before CSS, since CSS may depend on the design tokens being present. - const sortedOutput = R.sortBy((o) => (o.type === 'design-tokens' ? 0 : 1), config.output); - - for (const output of sortedOutput) { - const outDir = path.join(dsfs.outDir, output.dir); - - if (output.type === 'design-tokens') { - console.log(`\nšŸ± Generating design tokens in ${pc.green(output.dir)}...`); - - await createDesignTokens({ - themes: config.themes, - outDir: outDir, - clean: output.cleanDir, - }); - } - - if (output.type === 'css') { - console.log(`\nšŸ± Generating CSS in ${pc.green(output.dir)}...`); - - // Only generate create CSS if no `design-tokens` output is present and no `tokenDir` is explicitly set in the config file. Otherwise, build CSS from existing design tokens. - if (isOnlyCssOutput(config)) { - await createCss({ - themes: config.themes, - outDir: outDir, - clean: output.cleanDir, - verbose, - tailwind: output.experimental_tailwind, - }); - } else { - await buildCss({ - // Resolve the token directory relative to the config file, like output.dir, - // so it matches where a preceding design-tokens output wrote its files. - tokensDir: path.join(dsfs.outDir, output.tokenDir), - outDir, - clean: output.cleanDir, - verbose, - tailwind: output.experimental_tailwind, - }); - } - } - } - }); -} function makeTokenCommands() { const tokenCmd = createCommand('tokens'); @@ -243,9 +173,75 @@ function makeTokenCommands() { return tokenCmd; } +program.name('designsystemet').description('CLI for working with Designsystemet').showHelpAfterError(); +program.hook('preAction', () => console.log(figletAscii)); +program.version(pkg.version, '-v, --version', 'Display version number').helpOption('-h, --help', 'Display help'); + +program.description('Run Designsystemet') + .addOption(configOption()) + .addOption(dryOption()) + .addOption(verboseOption()) + .action(async (opts) => { + const { verbose, dry } = opts; + + const { configFile, configFilePath } = await getConfigFile(opts.config); + + dsfs.init({ dry, verbose, outdir: path.dirname(configFilePath) }); + + if (!configFile) { + console.error(pc.redBright(`No config file found. Please create one at ${pc.blue(DEFAULT_CONFIG_FILEPATH)}.`)); + process.exit(1); + } + + const parsedConfig = parseConfig(configFile); + // Validate against the public schema first for a user-facing error on unsupported theme fields. + validateConfig(externalConfigSchema, parsedConfig); + const config = validateConfig(configSchema, parsedConfig); + + // Sort outputs so that design-tokens are generated before CSS, since CSS may depend on the design tokens being present. + const sortedOutput = R.sortBy((o) => (o.type === 'design-tokens' ? 0 : 1), config.output); + + for (const output of sortedOutput) { + const outDir = path.join(dsfs.outDir, output.dir); + + if (output.type === 'design-tokens') { + console.log(`\nšŸ± Creating design tokens in ${pc.green(output.dir)}...`); + + await createDesignTokens({ + themes: config.themes, + outDir: outDir, + clean: output.cleanDir, + }); + } + + if (output.type === 'css') { + console.log(`\nšŸ± Creating CSS in ${pc.green(output.dir)}...`); + + // Only generate create CSS if no `design-tokens` output is present and no `tokenDir` is explicitly set in the config file. Otherwise, build CSS from existing design tokens. + if (isOnlyCssOutput(config)) { + await createCss({ + themes: config.themes, + outDir: outDir, + clean: output.cleanDir, + verbose, + tailwind: output.experimental_tailwind, + }); + } else { + await buildCss({ + // Resolve the token directory relative to the config file, like output.dir, + // so it matches where a preceding design-tokens output wrote its files. + tokensDir: path.join(dsfs.outDir, output.tokenDir), + outDir, + clean: output.cleanDir, + verbose, + tailwind: output.experimental_tailwind, + }); + } + } + } + }); + program.addCommand(makeTokenCommands()); -/** Disabling this for future testing and assessment */ -// program.addCommand(_makeConfigCommand(), { isDefault: true }); program .command('generate-config-from-tokens') @@ -314,7 +310,6 @@ program } }); -program.version(pkg.version, '-v, --version', 'Display version number').helpOption('-h, --help', 'Display help'); await program.parseAsync(process.argv); @@ -390,6 +385,9 @@ async function createDesignTokens({ await dsfs.cleanDir(outDir); } + console.log(`\nšŸ’¾ Writing design tokens to ${pc.green(outDir)}`); + + await dsfs.mkdir(outDir); await dsfs.writeFiles(files, outDir); diff --git a/packages/cli/src/schemas/schema-output.ts b/packages/cli/src/schemas/schema-output.ts index 51545aa9e5..cfef587691 100644 --- a/packages/cli/src/schemas/schema-output.ts +++ b/packages/cli/src/schemas/schema-output.ts @@ -27,11 +27,14 @@ const outputShorthandSchema = z const outputSchema = z .union([outputObjectSchema, outputShorthandSchema]) - .describe('An output file, either as an object or an output type using its default settings'); + .describe('An output file, either as an object or an output type using its default settings.'); /** The output settings of a config. `outDir` and `clean` are used by `tokens create`, `output` by the `config` command. */ export const outputConfigShape = { - output: z.array(outputSchema).prefault(['design-tokens', 'css']).describe('An array of output files'), + output: z + .array(outputSchema) + .prefault(['design-tokens', 'css']) + .describe('An array of output types. These are run in the order they are specified.'), outDir: z .string() .default('design-tokens') diff --git a/packages/cli/src/schemas/schema.ts b/packages/cli/src/schemas/schema.ts index d8c991b049..65f248a8b2 100644 --- a/packages/cli/src/schemas/schema.ts +++ b/packages/cli/src/schemas/schema.ts @@ -268,7 +268,7 @@ const externalThemeSchema = themeObjectSchema * Use this when exposing the schema externally (the public JSON schema, the theme builder and the Figma plugin); * use {@link configSchema} to validate a config in the CLI. */ -export const externalConfigSchema = configSchema.omit({ output: true }).extend({ +export const externalConfigSchema = configSchema.extend({ themes: z.record(z.string(), externalThemeSchema).superRefine(checkThemes).meta({ description: 'An object with one or more themes. Each property defines a theme, and the property name is used as the theme name. All themes must define the same color names.', From badbe08d2209b1dacf44674906777df766899430 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 23 Sep 2026 14:35:50 +0200 Subject: [PATCH 02/44] deprecate tokens commands --- packages/cli/bin/config.ts | 25 +++ packages/cli/bin/deprecated.ts | 161 +++++++++++++++++ packages/cli/bin/designsystemet.ts | 281 ++++++----------------------- packages/cli/bin/options.ts | 16 +- 4 files changed, 256 insertions(+), 227 deletions(-) create mode 100644 packages/cli/bin/deprecated.ts diff --git a/packages/cli/bin/config.ts b/packages/cli/bin/config.ts index a697e4b218..538336e251 100644 --- a/packages/cli/bin/config.ts +++ b/packages/cli/bin/config.ts @@ -13,6 +13,10 @@ import { getCliOption, getDefaultCliOption, getSuppliedCliOption, type OptionGet export { deprecatedCLIOptions } from '../src/schemas/helpers.ts'; +// Default config files to auto-detect when no --config is supplied, in order of precedence. +export const DEFAULT_CONFIG_FILEPATHS = ['designsystemet.config.json', 'designsystemet.config.jsonc']; +export const DEFAULT_CONFIG_FILEPATH = DEFAULT_CONFIG_FILEPATHS[0]; + export async function readConfigFile(configFilePath: string, allowFileNotFound = true): Promise { let configFile: string; @@ -116,3 +120,24 @@ export async function parseValidateAndOptsConfig( return validatedConfig; } + +export async function getConfigFile(userConfigFilePath: string | undefined) { + if (!R.isNil(userConfigFilePath)) { + // A config path was supplied explicitly. It's allowed to not exist only if it's one of the defaults. + const allowFileNotFound = DEFAULT_CONFIG_FILEPATHS.includes(userConfigFilePath); + const configFile = await readConfigFile(userConfigFilePath, allowFileNotFound); + + return { configFile, configFilePath: userConfigFilePath }; + } + + // No config path supplied: auto-detect the default config files (.json, then .jsonc). + for (const configFilePath of DEFAULT_CONFIG_FILEPATHS) { + const configFile = await readConfigFile(configFilePath, true); + if (configFile) { + return { configFile, configFilePath }; + } + } + + // None found - return empty config using the canonical default path for messaging. + return { configFile: '', configFilePath: DEFAULT_CONFIG_FILEPATH }; +} diff --git a/packages/cli/bin/deprecated.ts b/packages/cli/bin/deprecated.ts new file mode 100644 index 0000000000..beb668b11c --- /dev/null +++ b/packages/cli/bin/deprecated.ts @@ -0,0 +1,161 @@ +import { createCommand } from '@commander-js/extra-typings'; +import pc from 'picocolors'; +import { checkAutomigrate } from '../src/automigrate.ts'; +import { convertToHex } from '../src/colors/index.ts'; +import type { CssColor } from '../src/colors/types.ts'; +import type { ConfigSchema } from '../src/schemas/schema.ts'; +import { dsfs } from '../src/utils/filesystem.ts'; +import { deprecatedCLIOptions as cliOptions, getConfigFile, parseValidateAndOptsConfig } from './config.ts'; +import { configOption, dryOption, parseBoolean, verboseOption } from './options.ts'; + +export const DEFAULT_TOKENS_CREATE_DIR = './design-tokens'; +const DEFAULT_TOKENS_BUILD_DIR = './design-tokens-build'; +const DEFAULT_FONT = 'Inter'; +const DEFAULT_THEME_NAME = 'theme'; + +type TokenCommandDeps = { + createDesignTokens: (options: { themes: ConfigSchema['themes']; outDir: string; clean?: boolean }) => Promise; + buildCss: (options: { + tokensDir: string; + outDir: string; + clean?: boolean; + verbose?: boolean; + tailwind?: boolean; + }) => Promise; +}; + +/** + * @deprecated Use `designsystemet` with a config file instead. + */ +export function makeTokenCommands({ createDesignTokens, buildCss }: TokenCommandDeps) { + const tokenCmd = createCommand('tokens'); + + tokenCmd + .description(`[deprecated] use ${pc.blue('designsystemet')} with a config file instead`) + .hook('preAction', () => { + console.warn( + pc.yellow(`\n āš ļø The ${pc.bold('tokens')} commands are deprecated and will be removed in a future release. + \n āš ļø Please run ${pc.bold('designsystemet')} with a config file instead.`), + ); + }); + + tokenCmd + .command('build') + .description('[deprecated] Build Designsystemet tokens') + .option('-t, --tokens ', `Path to ${pc.blue('design-tokens')}`, DEFAULT_TOKENS_CREATE_DIR) + .option( + '-o, --out-dir ', + `Output directory for built ${pc.blue('design-tokens')}`, + DEFAULT_TOKENS_BUILD_DIR, + ) + .option(`--${cliOptions.clean} [boolean]`, 'Clean output directory before building tokens', parseBoolean, false) + .addOption(dryOption(`Dry run for built ${pc.blue('design-tokens')}`)) + .addOption(verboseOption()) + .addOption(configOption()) + .option('--experimental-tailwind', 'Generate Tailwind CSS classes for tokens', false) + .action(async (opts) => { + const { verbose, clean, dry, experimentalTailwind, tokens } = opts; + + // TODO - add outdir eqivalent to config option when parsing config, so that it can be set in the config file as well. buildDir? + + dsfs.init({ dry, outdir: opts.outDir, verbose }); + + await buildCss({ + tokensDir: tokens, + outDir: dsfs.outDir, + clean, + verbose, + tailwind: experimentalTailwind, + }); + }); + + tokenCmd + .command('create') + .description('[deprecated] Create Designsystemet tokens') + .addOption(configOption()) + .option(`--${cliOptions.clean} [boolean]`, 'Clean output directory before creating tokens', parseBoolean, false) + .addOption(dryOption(`Dry run for created ${pc.blue('design-tokens')}`)) + .option('--skip-check', 'Skip migration check', false) // TODO -- will be moved to global option in the future, since it applies to all commands, not just create + .option('-y, --yes', 'Skip user prompts', false) // TODO -- will be moved to global option in the future, since it applies to all commands, not just create + /** Deprecated options */ + .option( + `-m, --${cliOptions.theme.colors.main} `, + `Main colors (deprecated, use JSON config file instead)`, + parseColorValues, + ) + .option( + `-s, --${cliOptions.theme.colors.support} `, + `Support colors (deprecated, use JSON config file instead)`, + parseColorValues, + ) + .option( + `-n, --${cliOptions.theme.colors.neutral} `, + `Neutral hex color (deprecated, use JSON config file instead)`, + convertToHex, + ) + .option( + `-o, --${cliOptions.outDir} `, + `Output directory for created ${pc.blue('design-tokens')}`, + DEFAULT_TOKENS_CREATE_DIR, + ) + .option( + `-f, --${cliOptions.theme.typography.fontFamily} `, + `Font family (experimental, deprecated, use JSON config file instead)`, + DEFAULT_FONT, + ) + .option( + `-b, --${cliOptions.theme.borderRadius} `, + `Unitless base border-radius in px (deprecated, use JSON config file instead)`, + (radiusAsString) => Number(radiusAsString), + 4, + ) + .option('--theme ', 'Theme name (deprecated, use JSON config file instead)', DEFAULT_THEME_NAME) + .action(async (opts, cmd) => { + if ( + opts.mainColors || + opts.supportColors || + opts.neutralColor || + (opts.borderRadius && opts.borderRadius !== 4) || + (opts.theme && opts.theme !== DEFAULT_THEME_NAME) || + (opts.fontFamily && opts.fontFamily !== DEFAULT_FONT) + ) { + console.warn( + pc.yellow(`\n āš ļø Using CLI options for ${pc.bold(`colors, border radius, theme, or font family is deprecated`)} and will be removed in a future release. + \n āš ļø Please use a JSON config file instead.`), + ); + } + + if (opts.dry) { + console.log(`Performing dry run, no files will be written`); + } + const themeName = opts.theme; + + const { configFile, configFilePath } = await getConfigFile(opts.config); + + const updatedConfigFile = opts.skipCheck + ? configFile + : await checkAutomigrate(configFile, configFilePath, opts.yes); + + const config = await parseValidateAndOptsConfig(updatedConfigFile || configFile, { + theme: themeName, + cmd, + configFilePath, + }); + + dsfs.init({ dry: opts.dry, outdir: config.outDir }); + + await createDesignTokens({ + themes: config.themes, + outDir: dsfs.outDir, + clean: config.clean, + }); + }); + + return tokenCmd; +} + +function parseColorValues(value: string, previous: Record = {}): Record { + const [name, hex] = value.split(':'); + previous[name] = convertToHex(hex); + return previous; +} diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index a3c20a9675..2b59071dfa 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -1,12 +1,9 @@ #!/usr/bin/env node import path from 'node:path'; -import { Argument, createCommand, Option, program } from '@commander-js/extra-typings'; +import { Argument, program } from '@commander-js/extra-typings'; import pc from 'picocolors'; import * as R from 'ramda'; import pkg from '../package.json' with { type: 'json' }; -import { checkAutomigrate } from '../src/automigrate.ts'; -import { convertToHex } from '../src/colors/index.ts'; -import type { CssColor } from '../src/colors/types.ts'; import migrations from '../src/migrations/index.ts'; import { parseConfig, validateConfig } from '../src/schemas/helpers.ts'; import { @@ -22,7 +19,9 @@ import { generateConfigFromTokens } from '../src/tokens/generate-config.ts'; import type { OutputFile, Theme } from '../src/tokens/types.ts'; import { toColorNames } from '../src/tokens/utils.ts'; import { dsfs } from '../src/utils/filesystem.ts'; -import { deprecatedCLIOptions as cliOptions, parseValidateAndOptsConfig, readConfigFile } from './config.ts'; +import { DEFAULT_CONFIG_FILEPATH, getConfigFile } from './config.ts'; +import { DEFAULT_TOKENS_CREATE_DIR, makeTokenCommands } from './deprecated.ts'; +import { configOption, dryOption, verboseOption } from './options.ts'; const figletAscii = ` _____ _ _ _ @@ -35,213 +34,76 @@ const figletAscii = ` |___/ |___/ `; - - -const DEFAULT_TOKENS_CREATE_DIR = './design-tokens'; -const DEFAULT_TOKENS_BUILD_DIR = './design-tokens-build'; -const DEFAULT_FONT = 'Inter'; -const DEFAULT_THEME_NAME = 'theme'; -// Default config files to auto-detect when no --config is supplied, in order of precedence. -const DEFAULT_CONFIG_FILEPATHS = ['designsystemet.config.json', 'designsystemet.config.jsonc']; -const DEFAULT_CONFIG_FILEPATH = DEFAULT_CONFIG_FILEPATHS[0]; - -const configOption = () => - new Option( - '-c, --config ', - `Path to config file (auto-detects ${DEFAULT_CONFIG_FILEPATHS.map((p) => `"${p}"`).join(' or ')})`, - ); -const dryOption = (description = 'Dry run - no files will be written') => - new Option('--dry [boolean]', description).argParser(parseBoolean).default(false); -const verboseOption = () => new Option('--verbose', 'Enable verbose output').default(false); - - -function makeTokenCommands() { - const tokenCmd = createCommand('tokens'); - - tokenCmd - .command('build') - .description('Build Designsystemet tokens') - .option('-t, --tokens ', `Path to ${pc.blue('design-tokens')}`, DEFAULT_TOKENS_CREATE_DIR) - .option( - '-o, --out-dir ', - `Output directory for built ${pc.blue('design-tokens')}`, - DEFAULT_TOKENS_BUILD_DIR, - ) - .option(`--${cliOptions.clean} [boolean]`, 'Clean output directory before building tokens', parseBoolean, false) - .addOption(dryOption(`Dry run for built ${pc.blue('design-tokens')}`)) - .addOption(verboseOption()) - .addOption(configOption()) - .option('--experimental-tailwind', 'Generate Tailwind CSS classes for tokens', false) - .action(async (opts) => { - const { verbose, clean, dry, experimentalTailwind, tokens } = opts; - - // TODO - add outdir eqivalent to config option when parsing config, so that it can be set in the config file as well. buildDir? - - dsfs.init({ dry, outdir: opts.outDir, verbose }); - - await buildCss({ - tokensDir: tokens, - outDir: dsfs.outDir, - clean, - verbose, - tailwind: experimentalTailwind, - }); - }); - - tokenCmd - .command('create') - .description('Create Designsystemet tokens') - .addOption(configOption()) - .option(`--${cliOptions.clean} [boolean]`, 'Clean output directory before creating tokens', parseBoolean, false) - .addOption(dryOption(`Dry run for created ${pc.blue('design-tokens')}`)) - .option('--skip-check', 'Skip migration check', false) // TODO -- will be moved to global option in the future, since it applies to all commands, not just create - .option('-y, --yes', 'Skip user prompts', false) // TODO -- will be moved to global option in the future, since it applies to all commands, not just create - /** Deprecated options */ - .option( - `-m, --${cliOptions.theme.colors.main} `, - `Main colors (deprecated, use JSON config file instead)`, - parseColorValues, - ) - .option( - `-s, --${cliOptions.theme.colors.support} `, - `Support colors (deprecated, use JSON config file instead)`, - parseColorValues, - ) - .option( - `-n, --${cliOptions.theme.colors.neutral} `, - `Neutral hex color (deprecated, use JSON config file instead)`, - convertToHex, - ) - .option( - `-o, --${cliOptions.outDir} `, - `Output directory for created ${pc.blue('design-tokens')}`, - DEFAULT_TOKENS_CREATE_DIR, - ) - .option( - `-f, --${cliOptions.theme.typography.fontFamily} `, - `Font family (experimental, deprecated, use JSON config file instead)`, - DEFAULT_FONT, - ) - .option( - `-b, --${cliOptions.theme.borderRadius} `, - `Unitless base border-radius in px (deprecated, use JSON config file instead)`, - (radiusAsString) => Number(radiusAsString), - 4, - ) - .option('--theme ', 'Theme name (deprecated, use JSON config file instead)', DEFAULT_THEME_NAME) - .action(async (opts, cmd) => { - if ( - opts.mainColors || - opts.supportColors || - opts.neutralColor || - (opts.borderRadius && opts.borderRadius !== 4) || - (opts.theme && opts.theme !== DEFAULT_THEME_NAME) || - (opts.fontFamily && opts.fontFamily !== DEFAULT_FONT) - ) { - console.warn( - pc.yellow(`\n āš ļø Using CLI options for ${pc.bold(`colors, border radius, theme, or font family is deprecated`)} and will be removed in a future release. - \n āš ļø Please use a JSON config file instead.`), - ); - } - - if (opts.dry) { - console.log(`Performing dry run, no files will be written`); - } - const themeName = opts.theme; - - const { configFile, configFilePath } = await getConfigFile(opts.config); - - const updatedConfigFile = opts.skipCheck - ? configFile - : await checkAutomigrate(configFile, configFilePath, opts.yes); - - const config = await parseValidateAndOptsConfig(updatedConfigFile || configFile, { - theme: themeName, - cmd, - configFilePath, - }); - - dsfs.init({ dry: opts.dry, outdir: config.outDir }); - - await createDesignTokens({ - themes: config.themes, - outDir: dsfs.outDir, - clean: config.clean, - }); - }); - - return tokenCmd; -} - program.name('designsystemet').description('CLI for working with Designsystemet').showHelpAfterError(); program.hook('preAction', () => console.log(figletAscii)); program.version(pkg.version, '-v, --version', 'Display version number').helpOption('-h, --help', 'Display help'); -program.description('Run Designsystemet') - .addOption(configOption()) - .addOption(dryOption()) - .addOption(verboseOption()) - .action(async (opts) => { - const { verbose, dry } = opts; +program + .description('Run Designsystemet') + .addOption(configOption()) + .addOption(dryOption()) + .addOption(verboseOption()) + .action(async (opts) => { + const { verbose, dry } = opts; - const { configFile, configFilePath } = await getConfigFile(opts.config); + const { configFile, configFilePath } = await getConfigFile(opts.config); - dsfs.init({ dry, verbose, outdir: path.dirname(configFilePath) }); + dsfs.init({ dry, verbose, outdir: path.dirname(configFilePath) }); - if (!configFile) { - console.error(pc.redBright(`No config file found. Please create one at ${pc.blue(DEFAULT_CONFIG_FILEPATH)}.`)); - process.exit(1); - } + if (!configFile) { + console.error(pc.redBright(`No config file found. Please create one at ${pc.blue(DEFAULT_CONFIG_FILEPATH)}.`)); + process.exit(1); + } - const parsedConfig = parseConfig(configFile); - // Validate against the public schema first for a user-facing error on unsupported theme fields. - validateConfig(externalConfigSchema, parsedConfig); - const config = validateConfig(configSchema, parsedConfig); + const parsedConfig = parseConfig(configFile); + // Validate against the public schema first for a user-facing error on unsupported theme fields. + validateConfig(externalConfigSchema, parsedConfig); + const config = validateConfig(configSchema, parsedConfig); - // Sort outputs so that design-tokens are generated before CSS, since CSS may depend on the design tokens being present. - const sortedOutput = R.sortBy((o) => (o.type === 'design-tokens' ? 0 : 1), config.output); + // Sort outputs so that design-tokens are generated before CSS, since CSS may depend on the design tokens being present. + const sortedOutput = R.sortBy((o) => (o.type === 'design-tokens' ? 0 : 1), config.output); - for (const output of sortedOutput) { - const outDir = path.join(dsfs.outDir, output.dir); + for (const output of sortedOutput) { + const outDir = path.join(dsfs.outDir, output.dir); - if (output.type === 'design-tokens') { - console.log(`\nšŸ± Creating design tokens in ${pc.green(output.dir)}...`); + if (output.type === 'design-tokens') { + console.log(`\nšŸ± Creating design tokens in ${pc.green(output.dir)}...`); - await createDesignTokens({ + await createDesignTokens({ + themes: config.themes, + outDir: outDir, + clean: output.cleanDir, + }); + } + + if (output.type === 'css') { + console.log(`\nšŸ± Creating CSS in ${pc.green(output.dir)}...`); + + // Only generate create CSS if no `design-tokens` output is present and no `tokenDir` is explicitly set in the config file. Otherwise, build CSS from existing design tokens. + if (isOnlyCssOutput(config)) { + await createCss({ themes: config.themes, outDir: outDir, clean: output.cleanDir, + verbose, + tailwind: output.experimental_tailwind, + }); + } else { + await buildCss({ + // Resolve the token directory relative to the config file, like output.dir, + // so it matches where a preceding design-tokens output wrote its files. + tokensDir: path.join(dsfs.outDir, output.tokenDir), + outDir, + clean: output.cleanDir, + verbose, + tailwind: output.experimental_tailwind, }); - } - - if (output.type === 'css') { - console.log(`\nšŸ± Creating CSS in ${pc.green(output.dir)}...`); - - // Only generate create CSS if no `design-tokens` output is present and no `tokenDir` is explicitly set in the config file. Otherwise, build CSS from existing design tokens. - if (isOnlyCssOutput(config)) { - await createCss({ - themes: config.themes, - outDir: outDir, - clean: output.cleanDir, - verbose, - tailwind: output.experimental_tailwind, - }); - } else { - await buildCss({ - // Resolve the token directory relative to the config file, like output.dir, - // so it matches where a preceding design-tokens output wrote its files. - tokensDir: path.join(dsfs.outDir, output.tokenDir), - outDir, - clean: output.cleanDir, - verbose, - tailwind: output.experimental_tailwind, - }); - } } } - }); + } + }); -program.addCommand(makeTokenCommands()); +program.addCommand(makeTokenCommands({ createDesignTokens, buildCss })); program .command('generate-config-from-tokens') @@ -310,40 +172,8 @@ program } }); - await program.parseAsync(process.argv); -function parseColorValues(value: string, previous: Record = {}): Record { - const [name, hex] = value.split(':'); - previous[name] = convertToHex(hex); - return previous; -} - -function parseBoolean(value: string | boolean): boolean { - return value === 'true' || value === true; -} - -async function getConfigFile(userConfigFilePath: string | undefined) { - if (!R.isNil(userConfigFilePath)) { - // A config path was supplied explicitly. It's allowed to not exist only if it's one of the defaults. - const allowFileNotFound = DEFAULT_CONFIG_FILEPATHS.includes(userConfigFilePath); - const configFile = await readConfigFile(userConfigFilePath, allowFileNotFound); - - return { configFile, configFilePath: userConfigFilePath }; - } - - // No config path supplied: auto-detect the default config files (.json, then .jsonc). - for (const configFilePath of DEFAULT_CONFIG_FILEPATHS) { - const configFile = await readConfigFile(configFilePath, true); - if (configFile) { - return { configFile, configFilePath }; - } - } - - // None found - return empty config using the canonical default path for messaging. - return { configFile: '', configFilePath: DEFAULT_CONFIG_FILEPATH }; -} - /** * Creates design token files for the given themes and writes them to `outDir`. * Shared by `tokens create` and the `config` command's `design-tokens` output. @@ -387,7 +217,6 @@ async function createDesignTokens({ console.log(`\nšŸ’¾ Writing design tokens to ${pc.green(outDir)}`); - await dsfs.mkdir(outDir); await dsfs.writeFiles(files, outDir); diff --git a/packages/cli/bin/options.ts b/packages/cli/bin/options.ts index 199abb64e5..e5bea11d5a 100644 --- a/packages/cli/bin/options.ts +++ b/packages/cli/bin/options.ts @@ -1,4 +1,5 @@ -import type { Command, OptionValueSource, OptionValues } from '@commander-js/extra-typings'; +import { type Command, Option, type OptionValueSource, type OptionValues } from '@commander-js/extra-typings'; +import { DEFAULT_CONFIG_FILEPATHS } from './config.ts'; const getOptionIfMatchingSource = (...sources: OptionValueSource[]) => @@ -32,3 +33,16 @@ export const getDefaultCliOption = getOptionIfMatchingSource('default'); * for the option as defined in the {@link Command} */ export const getCliOption = getOptionIfMatchingSource('cli', 'default'); + +export const configOption = () => + new Option( + '-c, --config ', + `Path to config file (auto-detects ${DEFAULT_CONFIG_FILEPATHS.map((p) => `"${p}"`).join(' or ')})`, + ); +export const dryOption = (description = 'Dry run - no files will be written') => + new Option('--dry [boolean]', description).argParser(parseBoolean).default(false); +export const verboseOption = () => new Option('--verbose', 'Enable verbose output').default(false); + +export function parseBoolean(value: string | boolean): boolean { + return value === 'true' || value === true; +} From 11cdec1f5c2b344b9c995e1d7f5d321a747212d9 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 23 Sep 2026 14:39:07 +0200 Subject: [PATCH 03/44] deprecate clean and outDir in schema --- packages/cli/bin/config.ts | 2 + packages/cli/bin/designsystemet.ts | 2 + packages/cli/src/schemas/schema-output.ts | 57 +++++++++++------------ 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/cli/bin/config.ts b/packages/cli/bin/config.ts index 538336e251..9376f22f2b 100644 --- a/packages/cli/bin/config.ts +++ b/packages/cli/bin/config.ts @@ -8,6 +8,7 @@ import { type ExternalConfigSchema, externalConfigSchema, } from '../src/schemas/schema.ts'; +import { warnDeprecatedFields } from '../src/schemas/schema-output.ts'; import { dsfs } from '../src/utils/filesystem.ts'; import { getCliOption, getDefaultCliOption, getSuppliedCliOption, type OptionGetter } from './options.ts'; @@ -56,6 +57,7 @@ export async function parseValidateAndOptsConfig( try { configParsed = parseConfig(configFile); + warnDeprecatedFields(configParsed); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred while parsing config file'; console.error(pc.redBright(`Failed parsing config file at ${pc.red(configFilePath)}`)); diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 2b59071dfa..9deef4262b 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -12,6 +12,7 @@ import { type ExternalConfigSchemaInput, externalConfigSchema, } from '../src/schemas/schema.ts'; +import { warnDeprecatedFields } from '../src/schemas/schema-output.ts'; import { buildTokens } from '../src/tokens/build.ts'; import { createTokens, getTokenSetDimensions, systemTokenToFiles, tokenSetsToFiles } from '../src/tokens/create.ts'; import { formatThemeCSS } from '../src/tokens/format.ts'; @@ -56,6 +57,7 @@ program } const parsedConfig = parseConfig(configFile); + warnDeprecatedFields(parsedConfig); // Validate against the public schema first for a user-facing error on unsupported theme fields. validateConfig(externalConfigSchema, parsedConfig); const config = validateConfig(configSchema, parsedConfig); diff --git a/packages/cli/src/schemas/schema-output.ts b/packages/cli/src/schemas/schema-output.ts index cfef587691..71cf6d7f5f 100644 --- a/packages/cli/src/schemas/schema-output.ts +++ b/packages/cli/src/schemas/schema-output.ts @@ -1,3 +1,4 @@ +import pc from 'picocolors'; import { z } from 'zod'; const designTokensOutputSchema = z.object({ @@ -29,46 +30,40 @@ const outputSchema = z .union([outputObjectSchema, outputShorthandSchema]) .describe('An output file, either as an object or an output type using its default settings.'); +/** Fields superseded by `output`. Kept so existing config files and `tokens create` keep working. */ +const deprecatedFields = ['outDir', 'clean'] as const; + /** The output settings of a config. `outDir` and `clean` are used by `tokens create`, `output` by the `config` command. */ export const outputConfigShape = { output: z .array(outputSchema) .prefault(['design-tokens', 'css']) .describe('An array of output types. These are run in the order they are specified.'), - outDir: z - .string() - .default('design-tokens') - .meta({ description: 'Path to the output directory for the created design tokens' }), + /** @deprecated Use `output[].dir` instead. */ + outDir: z.string().default('design-tokens').meta({ + deprecated: true, + description: 'Deprecated: use `output[].dir` instead. Path to the output directory for the created design tokens', + }), + /** @deprecated Use `output[].cleanDir` instead. */ clean: z .boolean() .default(false) - .meta({ description: 'Delete the output directory before building or creating tokens' }) + .meta({ + deprecated: true, + description: 'Deprecated: use `output[].cleanDir` instead. Delete the output directory before creating tokens', + }) .optional(), }; -// /** Fields superseded by `output`. Kept so existing config files keep validating. */ -// const deprecatedFields = ['outDir', 'clean'] as const; - -// /** The `output` field and the deprecated fields it superseded. */ -// export const outputConfigShape = { -// output: z.array(outputSchema).prefault(['design-tokens', 'css']).describe('An array of output files'), -// // No `.default()` on the deprecated fields: we need `undefined` when the user did not -// // set them, so we can warn only when they actually did. -// outDir: z.string().optional().meta({ -// deprecated: true, -// description: 'Deprecated: use `output[].dir` instead. Ignored when `output` is set.', -// }), -// clean: z.boolean().optional().meta({ -// deprecated: true, -// description: 'Deprecated: use `output[].cleanDir` instead. Ignored when `output` is set.', -// }), -// }; - -// // Non-fatal: warn about deprecated fields instead of failing validation. -// export const warnDeprecatedFields = (config: Partial>) => { -// for (const key of deprecatedFields) { -// if (config[key] !== undefined) { -// console.warn(pc.yellow(`āš ļø "${key}" is deprecated and ignored; use "output" instead.`)); -// } -// } -// }; +/** Non-fatal: warn about deprecated fields set in a config file instead of failing validation. */ +export const warnDeprecatedFields = (config: Partial>) => { + for (const key of deprecatedFields) { + if (config[key] !== undefined) { + console.warn( + pc.yellow( + `āš ļø Config field "${key}" is deprecated and will be removed in a future release; use "output" instead.`, + ), + ); + } + } +}; From 0774e66fd0cb1c3e591195268f2e97dca1d8c4c2 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 23 Sep 2026 14:44:38 +0200 Subject: [PATCH 04/44] changeset --- .changeset/honest-pans-lose.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/honest-pans-lose.md diff --git a/.changeset/honest-pans-lose.md b/.changeset/honest-pans-lose.md new file mode 100644 index 0000000000..bb8ea4337b --- /dev/null +++ b/.changeset/honest-pans-lose.md @@ -0,0 +1,5 @@ +--- +"@digdir/designsystemet": minor +--- + +**CLI:** New `output` field for defining outputs in config From 422c509a0bad5c2c52c62b57cb9c44ec4f09538f Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 23 Sep 2026 14:46:29 +0200 Subject: [PATCH 05/44] update snapshot --- .../schemas/__snapshots__/config.schema.json | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/schemas/__snapshots__/config.schema.json b/packages/cli/src/schemas/__snapshots__/config.schema.json index cc3187b039..7649069b7d 100644 --- a/packages/cli/src/schemas/__snapshots__/config.schema.json +++ b/packages/cli/src/schemas/__snapshots__/config.schema.json @@ -2,14 +2,103 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { + "output": { + "description": "An array of output types. These are run in the order they are specified.", + "default": [ + "design-tokens", + "css" + ], + "type": "array", + "items": { + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "design-tokens", + "description": "The type of output file" + }, + "dir": { + "default": "design-tokens", + "description": "The output directory", + "type": "string" + }, + "cleanDir": { + "default": true, + "description": "Whether to clean the output directory before generating files", + "type": "boolean" + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "css", + "description": "The type of output file" + }, + "dir": { + "default": "design-tokens-build", + "description": "The output directory", + "type": "string" + }, + "cleanDir": { + "default": true, + "description": "Whether to clean the output directory before generating files", + "type": "boolean" + }, + "tokenDir": { + "default": "design-tokens", + "description": "The directory containing the design tokens", + "type": "string" + }, + "banner": { + "default": "", + "description": "A banner to include at the top of the CSS file", + "type": "string" + }, + "experimental_tailwind": { + "default": true, + "description": "Whether to enable experimental Tailwind support", + "type": "boolean" + } + }, + "required": [ + "type" + ] + } + ], + "description": "An object representing an output file" + }, + { + "type": "string", + "enum": [ + "design-tokens", + "css" + ], + "description": "An output type using its default settings" + } + ], + "description": "An output file, either as an object or an output type using its default settings." + } + }, "outDir": { "default": "design-tokens", - "description": "Path to the output directory for the created design tokens", + "deprecated": true, + "description": "Deprecated: use `output[].dir` instead. Path to the output directory for the created design tokens", "type": "string" }, "clean": { "default": false, - "description": "Delete the output directory before building or creating tokens", + "deprecated": true, + "description": "Deprecated: use `output[].cleanDir` instead. Delete the output directory before creating tokens", "type": "boolean" }, "themes": { From 6891cf36367f1a3ec995d63ed74970ca2a6eda1f Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 24 Sep 2026 09:09:01 +0200 Subject: [PATCH 06/44] add automigration --- packages/cli/bin/designsystemet.ts | 9 +- packages/cli/src/automigrate.ts | 7 +- packages/cli/src/migrations/index.ts | 2 + .../src/migrations/new-output-field.test.ts | 47 +++++++ .../cli/src/migrations/new-output-field.ts | 125 ++++++++++++++++++ 5 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/migrations/new-output-field.test.ts create mode 100644 packages/cli/src/migrations/new-output-field.ts diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 9deef4262b..3afe596e03 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -4,6 +4,7 @@ import { Argument, program } from '@commander-js/extra-typings'; import pc from 'picocolors'; import * as R from 'ramda'; import pkg from '../package.json' with { type: 'json' }; +import { checkAutomigrate } from '../src/automigrate.ts'; import migrations from '../src/migrations/index.ts'; import { parseConfig, validateConfig } from '../src/schemas/helpers.ts'; import { @@ -44,6 +45,8 @@ program .addOption(configOption()) .addOption(dryOption()) .addOption(verboseOption()) + .option('--skip-check', 'Skip migration check', false) + .option('-y, --yes', 'Skip user prompts', false) .action(async (opts) => { const { verbose, dry } = opts; @@ -56,7 +59,11 @@ program process.exit(1); } - const parsedConfig = parseConfig(configFile); + const updatedConfigFile = opts.skipCheck + ? configFile + : await checkAutomigrate(configFile, configFilePath, opts.yes); + + const parsedConfig = parseConfig(updatedConfigFile); warnDeprecatedFields(parsedConfig); // Validate against the public schema first for a user-facing error on unsupported theme fields. validateConfig(externalConfigSchema, parsedConfig); diff --git a/packages/cli/src/automigrate.ts b/packages/cli/src/automigrate.ts index d738a8b2b7..4912c356fb 100644 --- a/packages/cli/src/automigrate.ts +++ b/packages/cli/src/automigrate.ts @@ -4,10 +4,6 @@ import { automigrations } from './migrations/index.ts'; import { dsfs } from './utils/filesystem.ts'; export const checkAutomigrate = async (configFile: string, configFilePath: string, yes: boolean) => { - if (!configFile) { - return null; - } - let migratedConfigFile = null; const eligibleMigrations = Object.values(automigrations).filter((migration) => { try { return migration.check(configFile); @@ -16,9 +12,10 @@ export const checkAutomigrate = async (configFile: string, configFilePath: strin } }); if (eligibleMigrations.length === 0) { - return null; + return configFile; } + let migratedConfigFile = configFile; for (const migration of eligibleMigrations) { console.log(pc.red(`\n āœ‹ Automigration detected \n`)); console.log( diff --git a/packages/cli/src/migrations/index.ts b/packages/cli/src/migrations/index.ts index 1a1265cb29..78d6314308 100644 --- a/packages/cli/src/migrations/index.ts +++ b/packages/cli/src/migrations/index.ts @@ -1,9 +1,11 @@ import betaToV1 from './beta-to-v1.ts'; import colorRenameNext49 from './color-rename-next49.ts'; import flattenColorCategories from './flatten-color-categories.ts'; +import newOutputField from './new-output-field.ts'; export const automigrations = { colorCategoryFlattening: flattenColorCategories, + newOutputField, }; export default { diff --git a/packages/cli/src/migrations/new-output-field.test.ts b/packages/cli/src/migrations/new-output-field.test.ts new file mode 100644 index 0000000000..29f05bcadc --- /dev/null +++ b/packages/cli/src/migrations/new-output-field.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { parseJsonc } from '../schemas/helpers.ts'; +import migration, { migrateToOutputField } from './new-output-field.ts'; + +describe('new output field migration', () => { + it('only applies to configs with deprecated fields', () => { + expect(migration.check('{ "outDir": "tokens" }')).toBe(true); + expect(migration.check('{ "clean": false }')).toBe(true); + expect(migration.check('{ "output": ["css"] }')).toBe(false); + }); + + it('replaces outDir and clean with output, preserving comments', () => { + const config = `{ + "outDir": "tokens", + "clean": true, + // my themes + "themes": {} +}`; + const migrated = migrateToOutputField(config); + + expect(migrated).toContain('// my themes'); + expect(parseJsonc(migrated)).toEqual({ + themes: {}, + output: [ + { type: 'design-tokens', dir: 'tokens' }, + { type: 'css', tokenDir: 'tokens' }, + ], + }); + }); + + it('does not define output when the deprecated fields have default values', () => { + expect(parseJsonc(migrateToOutputField('{ "outDir": "./design-tokens", "clean": false }'))).toEqual({}); + expect(parseJsonc(migrateToOutputField('{ "clean": true }'))).toEqual({}); + }); + + it('only removes deprecated fields when output is already set', () => { + const migrated = migrateToOutputField('{ "outDir": "tokens", "output": ["css"] }'); + + expect(parseJsonc(migrated)).toEqual({ output: ['css'] }); + }); + + it('leaves the config unchanged when declined', () => { + const config = '{ "outDir": "tokens" }'; + + expect(migration.no(config)).toBe(config); + }); +}); diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts new file mode 100644 index 0000000000..22bfd6d28c --- /dev/null +++ b/packages/cli/src/migrations/new-output-field.ts @@ -0,0 +1,125 @@ +// biome-ignore-all lint/suspicious/noExplicitAny: the deprecated fields are no longer in the schema types, so we need to use any here +import path from 'node:path'; +import { applyEdits, findNodeAtLocation, modify, parseTree } from 'jsonc-parser'; +import pc from 'picocolors'; +import { parseJsonc } from '../schemas/helpers.ts'; +import { outputConfigShape } from '../schemas/schema-output.ts'; + +const formattingOptions = { insertSpaces: true, tabSize: 2 } as const; + +const deprecatedFields = ['outDir', 'clean'] as const; + +type Automigrate = { + name: string; + check: (config: string) => boolean; + message: string; + yes: (config: string) => string; + no: (config: string) => string; +}; + +/** + * Removes a top-level property and its comma, leaving surrounding comments and formatting intact. + * `modify(text, [key], undefined)` removes everything up to the next property, including comments. + */ +const removeProperty = (text: string, key: string): string => { + const tree = parseTree(text); + const property = tree && findNodeAtLocation(tree, [key])?.parent; + if (!property) { + return text; + } + + let start = property.offset; + let end = property.offset + property.length; + + const trailingComma = /^\s*,/.exec(text.slice(end)); + if (trailingComma) { + end += trailingComma[0].length; + } else { + // Last property: remove the comma before it instead. + const leadingComma = /,\s*$/.exec(text.slice(0, start)); + if (leadingComma) { + start -= leadingComma[0].length; + } + } + + // Remove the whole line when the property is on its own line. + const lineStart = text.lastIndexOf('\n', start - 1) + 1; + if (trailingComma && /^[ \t]*$/.test(text.slice(lineStart, start)) && text[end] === '\n') { + start = lineStart; + end += 1; + } + + return text.slice(0, start) + text.slice(end); +}; + +const hasDeprecatedFields = (config: string): boolean => { + const currentConfig = parseJsonc(config); + + return deprecatedFields.some((key) => key in currentConfig); +}; + +const defaultOutDir = outputConfigShape.outDir.parse(undefined); + +/** + * Builds an `output` equivalent to the deprecated `outDir` field. + * Returns `undefined` when `outDir` has its default value, since the default `output` covers it. + * + * `clean` never needs to be carried over: its default is covered by the default `output`, + * and `clean: true` matches the default `cleanDir`. + */ +const toOutput = (outDir: string | undefined) => { + if (outDir === undefined || path.posix.normalize(outDir) === path.posix.normalize(defaultOutDir)) { + return undefined; + } + + return [ + { type: 'design-tokens', dir: outDir }, + // CSS is built from the design tokens, so it must read them from the same directory. + { type: 'css', tokenDir: outDir }, + ]; +}; + +export const migrateToOutputField = (config: string): string => { + const currentConfig = parseJsonc(config); + + // Apply targeted edits to the original text instead of re-serializing the whole + // config, so comments, formatting and trailing commas are preserved. + let configText = config; + + // If `output` is already set, the deprecated fields are ignored and can simply be removed. + if (!currentConfig.output) { + const output = toOutput(currentConfig.outDir); + if (output) { + configText = applyEdits(configText, modify(configText, ['output'], output, { formattingOptions })); + } + } + + for (const key of deprecatedFields) { + configText = removeProperty(configText, key); + } + + return configText; +}; + +const migration: Automigrate = { + name: 'New output field', + check: hasDeprecatedFields, + message: `Your config file uses the deprecated ${pc.yellow('outDir')} and ${pc.yellow('clean')} fields. \nThis migration will replace them with an ${pc.yellow('output')} field.\n`, + yes: (config: string): string => { + const migratedConfig = migrateToOutputField(config); + console.log( + pc.green( + `\nConfig file successfully migrated, you now only need to run ${pc.blue('designsystemet')} to generate outputs`, + ), + ); + + return migratedConfig; + }, + no: (config: string): string => { + // The deprecated fields still validate, so the config can be used as-is. + console.log(pc.yellow('\nUsing existing config file but migration was skipped.\n')); + return config; + }, +}; + +export default migration; From 7464c331133435ed361f57b1dc972e15134058ab Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 24 Sep 2026 09:45:39 +0200 Subject: [PATCH 07/44] wording --- packages/cli/src/migrations/new-output-field.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 22bfd6d28c..9cab9a43e6 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -104,14 +104,17 @@ export const migrateToOutputField = (config: string): string => { const migration: Automigrate = { name: 'New output field', check: hasDeprecatedFields, - message: `Your config file uses the deprecated ${pc.yellow('outDir')} and ${pc.yellow('clean')} fields. \nThis migration will replace them with an ${pc.yellow('output')} field.\n`, + message: `Your config file uses the deprecated ${pc.yellow('outDir')} and ${pc.yellow('clean')} fields. \nThis migration will replace them with a new ${pc.blue('output')} field if necessary.\n`, yes: (config: string): string => { const migratedConfig = migrateToOutputField(config); - console.log( - pc.green( - `\nConfig file successfully migrated, you now only need to run ${pc.blue('designsystemet')} to generate outputs`, - ), - ); + console.log(pc.green(`\nConfig file successfully migrated.`)); + if (typeof JSON.parse(migratedConfig).output === 'undefined') { + console.log( + pc.green( + `\nNo new output field was added because the deprecated fields matched outputs default values and does not need to be added explicitly.`, + ), + ); + } return migratedConfig; }, From d7975952f0f24f0abad154413856301114efa3e9 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 24 Sep 2026 09:45:56 +0200 Subject: [PATCH 08/44] changeset about changed clean --- .changeset/sour-turtles-begin.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/sour-turtles-begin.md diff --git a/.changeset/sour-turtles-begin.md b/.changeset/sour-turtles-begin.md new file mode 100644 index 0000000000..840ea1eec8 --- /dev/null +++ b/.changeset/sour-turtles-begin.md @@ -0,0 +1,5 @@ +--- +"@digdir/designsystemet": minor +--- + +**CLI** New `output[]` will clean `outDir` folders by default. From d3be736e2fa5b2a52034d52f76b14243d7dd0378 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 24 Sep 2026 09:53:13 +0200 Subject: [PATCH 09/44] make sure output comes at the top or behind $schema --- packages/cli/src/migrations/new-output-field.test.ts | 8 ++++++++ packages/cli/src/migrations/new-output-field.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/migrations/new-output-field.test.ts b/packages/cli/src/migrations/new-output-field.test.ts index 29f05bcadc..ca470c29f4 100644 --- a/packages/cli/src/migrations/new-output-field.test.ts +++ b/packages/cli/src/migrations/new-output-field.test.ts @@ -28,6 +28,14 @@ describe('new output field migration', () => { }); }); + it('places output after $schema if present, otherwise at the top', () => { + const withSchema = migrateToOutputField('{ "themes": {}, "$schema": "schema.json", "outDir": "tokens" }'); + expect(Object.keys(parseJsonc(withSchema))).toEqual(['themes', '$schema', 'output']); + + const withoutSchema = migrateToOutputField('{ "themes": {}, "outDir": "tokens" }'); + expect(Object.keys(parseJsonc(withoutSchema))).toEqual(['output', 'themes']); + }); + it('does not define output when the deprecated fields have default values', () => { expect(parseJsonc(migrateToOutputField('{ "outDir": "./design-tokens", "clean": false }'))).toEqual({}); expect(parseJsonc(migrateToOutputField('{ "clean": true }'))).toEqual({}); diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 9cab9a43e6..68e8b3c963 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -90,7 +90,14 @@ export const migrateToOutputField = (config: string): string => { if (!currentConfig.output) { const output = toOutput(currentConfig.outDir); if (output) { - configText = applyEdits(configText, modify(configText, ['output'], output, { formattingOptions })); + configText = applyEdits( + configText, + modify(configText, ['output'], output, { + formattingOptions, + // Place `output` right after `$schema` if present, otherwise at the top. + getInsertionIndex: (properties) => properties.indexOf('$schema') + 1, + }), + ); } } From 0c916359f0c6e3dc2315f4a9373234630ac9f01e Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 24 Sep 2026 11:02:55 +0200 Subject: [PATCH 10/44] update docs --- .../fundamentals/en/code/cli-config.mdx | 86 +++++++++++++++++-- .../fundamentals/en/start-here/own-theme.mdx | 18 +--- .../fundamentals/en/theme/multiple-themes.mdx | 12 ++- .../fundamentals/no/code/cli-config.mdx | 86 +++++++++++++++++-- .../fundamentals/no/start-here/own-theme.mdx | 18 +--- .../fundamentals/no/theme/multiple-themes.mdx | 12 ++- packages/cli/README.md | 79 ++++++++++------- 7 files changed, 232 insertions(+), 79 deletions(-) diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index 2a859f97fe..aeb4bd1c3d 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -9,9 +9,6 @@ published: true order: 40 --- - -Note that config is experimental. The config schema may change at any time, and will be communicated in the changelog. - Using a config file with the CLI gives you more control over your colours and makes it easier to update and maintain them. @@ -21,6 +18,7 @@ The config file can be named whatever you want, but if you use `designsystemet.c You can place it anywhere in your project. If you've placed your file in a different location or with a different name, you can use `--config ` in CLI commands. +Both `designsystemet.config.json` and `designsystemet.config.jsonc` are detected automatically, and both may contain comments and trailing commas. ```json { @@ -41,14 +39,87 @@ If you've placed your file in a different location or with a different name, you Above, we have defined a theme called `my-theme` with some colours and border radius. Each colour is defined directly under `colors`, where the key is the colour's name and the value is a hex code. +## Running the CLI + +Run the CLI in the same folder as your config file: + +```bash +npx @digdir/designsystemet@latest +``` + +This creates design tokens and CSS for all themes, based on the [`output`](#output) field in the config file. + +| Option | Description | +| ------ | ----------- | +| `-c, --config ` | Path to the config file. | +| `--dry` | Dry run, no files will be written. | +| `--verbose` | Enable verbose output. | +| `--skip-check` | Skip the automatic migration check of the config file. | +| `-y, --yes` | Skip prompts, for example when migrating the config file. | + + +The `tokens create` and `tokens build` commands are deprecated and will be removed in a future release. Use `designsystemet` with a config file instead. + + ## Structure | Name | Type | Required | Description | | ---- | ---- | ------- | ----------- | | $schema | String | No | Path to JSON schema for validation. Recommended: `node_modules/@digdir/designsystemet/dist/config.schema.json`. | +| output | Array | No | What the CLI should create, and where. Defaults to `["design-tokens", "css"]`. See [Output](#output). | | themes | Object | Yes | Contains all themes you want to define. Each key is the name of the theme. | -| outDir | String | Yes | The folder where design tokens should be saved. | -| clean | Boolean | No | Delete the output directory before creating tokens. Useful for removing deprecated files. | +| outDir | String | No | **Deprecated:** use `output[].dir` instead. | +| clean | Boolean | No | **Deprecated:** use `output[].cleanDir` instead. | + +### Output + +`output` is a list of what the CLI should create. Each item is either the name of an output type, which uses its default settings, or an object with custom settings. +Design tokens are always created before CSS, since CSS is built from the design tokens. + +All paths are relative to the config file. + +```json +{ + "output": [ + { "type": "design-tokens", "dir": "./design-tokens" }, + { "type": "css", "dir": "./css", "tokenDir": "./design-tokens" } + ] +} +``` + +#### design-tokens + +| Name | Type | Required | Default | Description | +| ---- | ---- | ------- | ------- | ----------- | +| type | `"design-tokens"` | Yes | | The output type. | +| dir | String | No | `design-tokens` | The folder where design tokens should be saved. | +| cleanDir | Boolean | No | `true` | Delete the folder before creating design tokens. Removes files that are no longer in use. | + +#### css + +| Name | Type | Required | Default | Description | +| ---- | ---- | ------- | ------- | ----------- | +| type | `"css"` | Yes | | The output type. | +| dir | String | No | `design-tokens-build` | The folder where CSS should be saved. | +| cleanDir | Boolean | No | `true` | Delete the folder before creating CSS. | +| tokenDir | String | No | `design-tokens` | The folder containing the design tokens to build CSS from. Should match `dir` of the `design-tokens` output. | +| experimental_tailwind | Boolean | No | `true` | Also generate Tailwind CSS classes. Experimental. | + +### Migrating from outDir and clean + +`outDir` and `clean` are replaced by `output`. When you run the CLI with a config file that uses them, it offers to migrate the file for you. +If you'd rather do it manually, replace `"outDir": ""` with: + +```json +{ + "output": [ + { "type": "design-tokens", "dir": "" }, + { "type": "css", "tokenDir": "" } + ] +} +``` + +`clean` can be removed, since `cleanDir` is `true` by default. ### Themes @@ -170,7 +241,10 @@ The `severity` override allows you to customise the colours used for severity, w ```json { "$schema": "node_modules/@digdir/designsystemet/dist/config.schema.json", - "outDir": "./design-tokens", + "output": [ + { "type": "design-tokens", "dir": "./design-tokens" }, + { "type": "css", "dir": "./design-tokens-build", "tokenDir": "./design-tokens" } + ], "themes": { "my-theme": { "colors": { diff --git a/apps/www/app/content/fundamentals/en/start-here/own-theme.mdx b/apps/www/app/content/fundamentals/en/start-here/own-theme.mdx index 9a05ea88d2..a502478975 100644 --- a/apps/www/app/content/fundamentals/en/start-here/own-theme.mdx +++ b/apps/www/app/content/fundamentals/en/start-here/own-theme.mdx @@ -23,8 +23,6 @@ We recommend that both a developer and a designer take part in the process when ```json { - "outDir": "./design-tokens", - "clean": true, "themes": { "theme": { "colors": { @@ -45,9 +43,9 @@ We recommend that both a developer and a designer take part in the process when 5. Run the following command in the terminal: ```bash -npx @digdir/designsystemet@latest tokens create --config designsystemet.config.json +npx @digdir/designsystemet@latest ``` -This will generate Design Tokens based on the configuration file and save them in a folder called `design-tokens`. Push the changes to your repository. +This will generate Design Tokens based on the configuration file and save them in a folder called `design-tokens`, and CSS in a folder called `design-tokens-build`. You can change the folders with the [`output`](/en/fundamentals/code/cli-config#output) field in the configuration file. Push the changes to your repository. 6. Fetch the component library from [Figma Community (figma.com)](https://www.figma.com/community/file/1322138390374166141/designsystemet-core-ui-kit) (Click ā€œOpen in Figmaā€) into your organisation that has at least a Pro licence. Note that this will be a copy of the component library without any connection to the main file. @@ -70,7 +68,7 @@ We recommend *not* pushing changes to Design Tokens from Tokens Studio, as this ```bash -npx @digdir/designsystemet@latest tokens create --config designsystemet.config.json +npx @digdir/designsystemet@latest --config designsystemet.config.json ``` **Remember to run this command every time you make changes to the configuration file.** @@ -116,8 +114,6 @@ If you want additional themes, you can generate a new theme in the Theme Builder ```json { - "outDir": "./design-tokens", - "clean": true, "themes": { "theme-one": { "colors": { @@ -144,13 +140,7 @@ When the Design Tokens are updated in code, you can easily fetch the new values -10. To generate CSS from your theme, run the following command: - -```bash -npx @digdir/designsystemet@latest tokens build --config designsystemet.config.json -``` - -Remember that this must also be done whenever the tokens are updated. +10. The same command also generates CSS from your theme, in the `design-tokens-build` folder. This CSS is what you use in code. ### Icons in Figma To get icons working in Figma, they need to be connected to a library stored in your organisation. See the guide ["Get icons working in Figma"](/en/fundamentals/theme/icons#get-icons-working-in-figma). diff --git a/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx b/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx index 542227d0f8..cfe6637774 100644 --- a/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx +++ b/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx @@ -57,7 +57,10 @@ It is common to have to do it this way if you have a repository that collects al ```json { - "outDir": "./some-org-dt", + "output": [ + { "type": "design-tokens", "dir": "./some-org-dt" }, + { "type": "css", "dir": "./some-org-css", "tokenDir": "./some-org-dt" } + ], "themes": { "some-org": { "colors": { @@ -73,7 +76,10 @@ It is common to have to do it this way if you have a repository that collects al ```json { - "outDir": "./other-org-dt", + "output": [ + { "type": "design-tokens", "dir": "./other-org-dt" }, + { "type": "css", "dir": "./other-org-css", "tokenDir": "./other-org-dt" } + ], "themes": { "other-org": { "colors": { @@ -88,5 +94,5 @@ It is common to have to do it this way if you have a repository that collects al ``` -Note that we have different `outDir`s in these two config files, so that the design tokens for each theme are placed in their own folder. +Note that we have different `output` folders in these two config files, so that the design tokens and CSS for each theme are placed in their own folders. diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index 337fc7e082..a529866a74 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -10,9 +10,6 @@ order: 40 search_terms: konfigurasjon, tokens --- - -Merk at config er eksperimentelt. Config skjema kan endre seg nĆ„r som helst, og vil bli kommunisert i changelog. - ƅ bruke config fil med CLI gjer at du kan ha meir kontroll over fargane dine, og kan lettare oppdatere og vedlikehalde dei. @@ -22,6 +19,7 @@ Config fila kan heite det du vil, men bruker du `designsystemet.config.json` vil Du kan legge den kor du vil i prosjektet ditt. Har du plassert fila di ein anna plass, eller med eit anna navn, kan du bruke `--config ` i CLI-kommandoar. +BĆ„de `designsystemet.config.json` og `designsystemet.config.jsonc` blir funne automatisk, og begge kan innehalde kommentarar og avsluttande komma. ```json { @@ -42,14 +40,87 @@ Har du plassert fila di ein anna plass, eller med eit anna navn, kan du bruke `- Over har me definert eit tema som heiter `my-theme` med nokre fargar og border radius. Kvar farge er definert direkte under `colors`, der nĆøkkelen er namnet pĆ„ fargen og verdien er ein hexkode. +## KĆøyre CLI + +KĆøyr CLI-et i same mappe som config fila: + +```bash +npx @digdir/designsystemet@latest +``` + +Dette lagar design tokens og CSS for alle tema, basert pĆ„ [`output`](#output)-feltet i config fila. + +| Val | Forklaring | +| --- | ----------- | +| `-c, --config ` | Sti til config fila. | +| `--dry` | PrĆøvekĆøyring, ingen filer blir skrivne. | +| `--verbose` | Vis meir utfyllande logg. | +| `--skip-check` | Hopp over automatisk migreringssjekk av config fila. | +| `-y, --yes` | Hopp over spĆørsmĆ„l, til dĆømes ved migrering av config fila. | + + +Kommandoane `tokens create` og `tokens build` er utdaterte og blir fjerna i ein framtidig versjon. Bruk `designsystemet` med ei config fil i staden. + + ## Struktur | Namn | Type | PĆ„krevd | Forklaring | | ---- | ---- | ------- | ----------- | | $schema | Streng | Nei | Sti til JSON schema for validering. Anbefalt: `node_modules/@digdir/designsystemet/dist/config.schema.json`. | +| output | Liste | Nei | Kva CLI-et skal lage, og kvar. Standard er `["design-tokens", "css"]`. SjĆ„ [Output](#output). | | themes | Objekt | Ja | Inneheld alle tema du vil definere. Ny nĆøkkel er namnet pĆ„ temaet. | -| outDir | Streng | Ja | Mappa der design tokens skal lagrast. | -| clean | Boolean | Nei | Slett utdata-mappa fĆør du lagar tokens. Nyttig for Ć„ fjerne utdaterte filer. | +| outDir | Streng | Nei | **Utdatert:** bruk `output[].dir` i staden. | +| clean | Boolean | Nei | **Utdatert:** bruk `output[].cleanDir` i staden. | + +### Output + +`output` er ei liste over kva CLI-et skal lage. Kvart element er anten namnet pĆ„ ein output-type, som brukar standardinnstillingane, eller eit objekt med eigne innstillingar. +Design tokens blir alltid laga fĆør CSS, sidan CSS blir bygd frĆ„ design tokens. + +Alle stiar er relative til config fila. + +```json +{ + "output": [ + { "type": "design-tokens", "dir": "./design-tokens" }, + { "type": "css", "dir": "./css", "tokenDir": "./design-tokens" } + ] +} +``` + +#### design-tokens + +| Namn | Type | PĆ„krevd | Standard | Forklaring | +| ---- | ---- | ------- | -------- | ----------- | +| type | `"design-tokens"` | Ja | | Output-typen. | +| dir | Streng | Nei | `design-tokens` | Mappa der design tokens skal lagrast. | +| cleanDir | Boolean | Nei | `true` | Slett mappa fĆør design tokens blir laga. Fjernar filer som ikkje lenger er i bruk. | + +#### css + +| Namn | Type | PĆ„krevd | Standard | Forklaring | +| ---- | ---- | ------- | -------- | ----------- | +| type | `"css"` | Ja | | Output-typen. | +| dir | Streng | Nei | `design-tokens-build` | Mappa der CSS skal lagrast. | +| cleanDir | Boolean | Nei | `true` | Slett mappa fĆør CSS blir laga. | +| tokenDir | Streng | Nei | `design-tokens` | Mappa med design tokens som CSS skal byggast frĆ„. BĆør vere lik `dir` i `design-tokens`-outputen. | +| experimental_tailwind | Boolean | Nei | `true` | Lag òg Tailwind CSS-klassar. Eksperimentelt. | + +### Migrere frĆ„ outDir og clean + +`outDir` og `clean` er erstatta av `output`. NĆ„r du kĆøyrer CLI-et med ei config fil som brukar dei, tilbyr det Ć„ migrere fila for deg. +Vil du heller gjere det manuelt, byt ut `"outDir": ""` med: + +```json +{ + "output": [ + { "type": "design-tokens", "dir": "" }, + { "type": "css", "tokenDir": "" } + ] +} +``` + +`clean` kan fjernast, sidan `cleanDir` er `true` som standard. ### Themes @@ -161,7 +232,10 @@ Overstyringen av `severity` lar deg tilpasse fargane som blir brukt for severity ```json { "$schema": "node_modules/@digdir/designsystemet/dist/config.schema.json", - "outDir": "./design-tokens", + "output": [ + { "type": "design-tokens", "dir": "./design-tokens" }, + { "type": "css", "dir": "./design-tokens-build", "tokenDir": "./design-tokens" } + ], "themes": { "my-theme": { "colors": { diff --git a/apps/www/app/content/fundamentals/no/start-here/own-theme.mdx b/apps/www/app/content/fundamentals/no/start-here/own-theme.mdx index 69bec274a1..9d9a551f79 100644 --- a/apps/www/app/content/fundamentals/no/start-here/own-theme.mdx +++ b/apps/www/app/content/fundamentals/no/start-here/own-theme.mdx @@ -25,8 +25,6 @@ Config filen mĆ„ vƦre lagret som en `json` fil, vi anbfaler f.eks `designsystem ```json { - "outDir": "./design-tokens", - "clean": true, "themes": { "theme": { "colors": { @@ -48,9 +46,9 @@ Config filen mĆ„ vƦre lagret som en `json` fil, vi anbfaler f.eks `designsystem 5. KjĆør denne kommandoen i terminalen: ```bash -npx @digdir/designsystemet@latest tokens create --config designsystemet.config.json +npx @digdir/designsystemet@latest ``` -Dette vil generere Design Tokens basert pĆ„ config-fila og lagre dem i en mappe som heter `design-tokens`. Push endringene til ditt repo. +Dette vil generere Design Tokens basert pĆ„ config-fila og lagre dem i en mappe som heter `design-tokens`, og CSS i en mappe som heter `design-tokens-build`. Du kan endre mappene med [`output`](/no/fundamentals/code/cli-config#output)-feltet i config-fila. Push endringene til ditt repo. 6. Hent komponentbiblioteket fra [Figma Community (figma.com)](https://www.figma.com/community/file/1322138390374166141/designsystemet-core-ui-kit) (Trykk "Open in Figma") til din organisasjon som har mimimum pro-lisens. Merk at dette blir en kopi av komponentbiblioteket uten noen kobling mot hovedfilen. @@ -72,7 +70,7 @@ Vi anbefaler Ć„ *ikke* pushe endringer i Design Tokens via Tokens Studio, da det 8. GĆ„ til [Temabyggeren](https://theme.designsystemet.no) og generer fargeskalaer ut fra dine profilfarger. Klikk "Ta i bruk tema". Kopier config-filen og lim inn i `designsystemet.config.json`-fila i ditt repo. KjĆør kommandoen under og push endringene. ```bash -npx @digdir/designsystemet@latest tokens create --config designsystemet.config.json +npx @digdir/designsystemet@latest --config designsystemet.config.json ``` **Husk at du mĆ„ kjĆøre denne kommandoen hver gang du gjĆør endringer i config-filen.** @@ -117,8 +115,6 @@ NĆ„ skal du se alle komponentene med dine egne profilfarger i Figma. ```json { - "outDir": "./design-tokens", - "clean": true, "themes": { "theme-one": { "colors": { @@ -144,13 +140,7 @@ NĆ„ skal du se alle komponentene med dine egne profilfarger i Figma. NĆ„r Design Tokens oppdateres i koden, kan du enkelt hente de nye verdiene inn i Figma ved Ć„ bruke "Pull"-knappen i Tokens Studio igjen. -10. For Ć„ generere CSS fra temaet ditt, kjĆør denne kommandoen: - -```bash -npx @digdir/designsystemet@latest tokens build --config designsystemet.config.json -``` - -Husk at dette mĆ„ gjĆøres nĆ„r tokens blir oppdatert ogsĆ„. +10. Den samme kommandoen genererer ogsĆ„ CSS fra temaet ditt, i mappen `design-tokens-build`. Det er denne CSS-en du bruker i koden. ### Ikoner i Figma For Ć„ fĆ„ ikoner til Ć„ fungere i Figma, mĆ„ de kobles til et bibliotek som er lagret pĆ„ deres organisasjon. Se guiden ["FĆ„ ikoner til Ć„ fungere i Figma"](/no/fundamentals/theme/icons#fĆ„-ikoner-til-Ć„-fungere-i-figma). \ No newline at end of file diff --git a/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx b/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx index 639bd5e241..74d7ad34e2 100644 --- a/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx +++ b/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx @@ -58,7 +58,10 @@ Det er vanleg Ć„ mĆ„tte gjere det pĆ„ denne mĆ„ten dersom du har eit repository ```json { - "outDir": "./some-org-dt", + "output": [ + { "type": "design-tokens", "dir": "./some-org-dt" }, + { "type": "css", "dir": "./some-org-css", "tokenDir": "./some-org-dt" } + ], "themes": { "some-org": { "colors": { @@ -74,7 +77,10 @@ Det er vanleg Ć„ mĆ„tte gjere det pĆ„ denne mĆ„ten dersom du har eit repository ```json { - "outDir": "./other-org-dt", + "output": [ + { "type": "design-tokens", "dir": "./other-org-dt" }, + { "type": "css", "dir": "./other-org-css", "tokenDir": "./other-org-dt" } + ], "themes": { "other-org": { "colors": { @@ -89,5 +95,5 @@ Det er vanleg Ć„ mĆ„tte gjere det pĆ„ denne mĆ„ten dersom du har eit repository ``` -Legg merke til at me har ulik `outDir` i desse to config filene, slik at design tokens for kvart tema blir lagt i kvar si mappe. +Legg merke til at me har ulike `output`-mapper i desse to config filene, slik at design tokens og CSS for kvart tema blir lagt i kvar sine mapper. diff --git a/packages/cli/README.md b/packages/cli/README.md index b9eae5f2e6..980999c720 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -9,55 +9,49 @@ Read the Designsystemet [README](https://github.com/digdir/designsystemet) to ge ## Usage -### Create tokens - -Use `npx @digdir/designsystemet tokens create ` to create design tokens for use with Designsystemet. +Use `npx @digdir/designsystemet` to create design tokens and CSS for use with Designsystemet, based on a [config file](#using-a-config-file). This allows you to define themes including custom colors, font-family, and border-radius. We recommend using the [Designsystemet theme builder](https://theme.designsystemet.no/) for generating a valid config. -#### Update tokens +| Option | Description | +| ------ | ----------- | +| `-c, --config ` | Path to config file (auto-detects `designsystemet.config.json` or `designsystemet.config.jsonc`) | +| `--dry` | Dry run - no files will be written | +| `--verbose` | Enable verbose output | +| `--skip-check` | Skip migration check | +| `-y, --yes` | Skip user prompts | + +> āš ļø **DEPRECATED** āš ļø +> The `tokens create` and `tokens build` commands are deprecated and will be removed in a future release. +> Use `designsystemet` with a config file instead. -Whenever a new version of the CLI is released, or you have done changes, we recommend to update design tokens with the `--clean` option to potentially remove any changes deprecated files or unneeded files. +#### Update tokens and CSS -To update design tokens, re-run `npx @digdir/designsystemet tokens create --clean`. -If a [config file](#using-a-config-file) you can also re-run with `"clean": true`. +Whenever a new version of the CLI is released, or you have done changes, re-run `npx @digdir/designsystemet`. +Output directories are cleaned by default (`cleanDir`), which removes any deprecated or unneeded files. > āš ļø **WARNING** āš ļø > The design tokens created by this tool are considered an implementation detail, and is subject > to change at any time without being considered a breaking change. We **only** support customisations -> done through the CLI options and config. Direct editing of the design tokens are **not** supported. -> -> Since tokens may be added or removed at any time, it is necessary to routinely re-run this -> command when upgrading the libraries. This will remove any direct edits to the design tokens. - -### Build CSS from tokens - -Use `npx @digdir/designsystemet tokens build ` to build CSS from design tokens generated in the previous step. - -> āš ļø **WARNING** āš ļø -> The CSS files from created by this tool are considered build artifacts. They should **not** be +> done through the config. Direct editing of the design tokens are **not** supported. +> +> The CSS files created by this tool are considered build artifacts. They should **not** be > edited directly. While the CSS will not change unexpectedly, new variables may be added at any -> time. Therefore, it is necessary to routinely re-run this command when upgrading the libraries. -> This will remove any direct edits to the CSS. - -#### Update built CSS - -Whenever a new version of the CLI is released, or you have done changes, we recommend to build a new set of CSS from design tokens with the `--clean` option to potentially remove any changes deprecated files or unneeded files. - +> time. +> +> Therefore, it is necessary to routinely re-run this command when upgrading the libraries. +> This will remove any direct edits to the design tokens and CSS. ### Using a config file > āš ļø **WARNING** āš ļø > The typography feature is experimental. The config schema may change at any time. - -The `tokens create` command supports a config file. It will auto-detect a `designsystemet.config.json` or `designsystemet.config.jsonc` file in the current directory. You can also use the `--config ` option to supply a different config name and location. +The CLI will auto-detect a `designsystemet.config.json` or `designsystemet.config.jsonc` file in the current directory. You can also use the `--config ` option to supply a different config name and location. Both `.json` and `.jsonc` files may contain comments and trailing commas (JSONC). -The main advantage of using a config file is for automation in scenarios with multiple themes. - To get started, use this template for a `designsystemet.config.json` file: ```jsonc @@ -72,7 +66,6 @@ In editors which support JSON Schema, the `$schema` will then give you editor h ```jsonc { "$schema": "./node_modules/@digdir/designsystemet/dist/config.schema.json", - "outDir": "../path/to/design-tokens", "themes": { "theme": { "colors": { @@ -84,13 +77,33 @@ In editors which support JSON Schema, the `$schema` will then give you editor h } } ``` -To generate new design tokens and CSS files, you would then run. +To generate new design tokens and CSS files, you would then run: ``` -npx @digdir/designsystemet tokens create -npx @digdir/designsystemet tokens build +npx @digdir/designsystemet ``` +This creates design tokens in `design-tokens` and CSS in `design-tokens-build`, relative to the config file. + +#### Output + +Use `output` to choose what is created and where. Each item is either an output type (`"design-tokens"` or `"css"`) using its default settings, or an object: + +```jsonc +{ + "output": [ + // defaults: dir "design-tokens", cleanDir true + { "type": "design-tokens", "dir": "../path/to/design-tokens" }, + // defaults: dir "design-tokens-build", tokenDir "design-tokens", cleanDir true + { "type": "css", "dir": "../path/to/css", "tokenDir": "../path/to/design-tokens" }, + ], +} +``` + +Design tokens are always created before CSS. `tokenDir` should match the `dir` of the `design-tokens` output. + +The `outDir` and `clean` fields are deprecated in favour of `output`. The CLI will offer to migrate your config file automatically. + #### Complex config example Have a look at the `*.config.json` files under the `packages/cli` in the Github repo for more complex examples. From 25e2a44f8574ab725048d7be10978e80596b76ab Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Fri, 25 Sep 2026 08:42:32 +0200 Subject: [PATCH 11/44] fixed edge-case with isOnlyCssOutput and added docs about css only --- .../fundamentals/en/code/cli-config.mdx | 2 ++ .../fundamentals/no/code/cli-config.mdx | 2 ++ packages/cli/README.md | 2 ++ packages/cli/bin/designsystemet.ts | 22 +++++++++++++------ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index aeb4bd1c3d..1ac1e2d4c7 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -102,6 +102,8 @@ All paths are relative to the config file. | type | `"css"` | Yes | | The output type. | | dir | String | No | `design-tokens-build` | The folder where CSS should be saved. | | cleanDir | Boolean | No | `true` | Delete the folder before creating CSS. | + +If you only need CSS, you can use `"output": ["css"]` without `tokenDir`. The CSS is then created directly from the themes, without saving any design tokens. | tokenDir | String | No | `design-tokens` | The folder containing the design tokens to build CSS from. Should match `dir` of the `design-tokens` output. | | experimental_tailwind | Boolean | No | `true` | Also generate Tailwind CSS classes. Experimental. | diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index a529866a74..a2d95dd8a9 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -103,6 +103,8 @@ Alle stiar er relative til config fila. | type | `"css"` | Ja | | Output-typen. | | dir | Streng | Nei | `design-tokens-build` | Mappa der CSS skal lagrast. | | cleanDir | Boolean | Nei | `true` | Slett mappa fĆør CSS blir laga. | + +Treng du berre CSS, kan du bruke `"output": ["css"]` utan `tokenDir`. CSS-en blir dĆ„ laga direkte frĆ„ tema, utan at design tokens blir lagra. | tokenDir | Streng | Nei | `design-tokens` | Mappa med design tokens som CSS skal byggast frĆ„. BĆør vere lik `dir` i `design-tokens`-outputen. | | experimental_tailwind | Boolean | Nei | `true` | Lag òg Tailwind CSS-klassar. Eksperimentelt. | diff --git a/packages/cli/README.md b/packages/cli/README.md index 980999c720..eaffbce9b4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -102,6 +102,8 @@ Use `output` to choose what is created and where. Each item is either an output Design tokens are always created before CSS. `tokenDir` should match the `dir` of the `design-tokens` output. +If you only need CSS, use `"output": ["css"]` without `tokenDir`. The CSS is then created directly from the themes, without writing any design tokens. + The `outDir` and `clean` fields are deprecated in favour of `output`. The CLI will offer to migrate your config file automatically. #### Complex config example diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 3afe596e03..0c0cee275c 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -89,7 +89,7 @@ program console.log(`\nšŸ± Creating CSS in ${pc.green(output.dir)}...`); // Only generate create CSS if no `design-tokens` output is present and no `tokenDir` is explicitly set in the config file. Otherwise, build CSS from existing design tokens. - if (isOnlyCssOutput(config)) { + if (isOnlyCssOutput(parsedConfig)) { await createCss({ themes: config.themes, outDir: outDir, @@ -308,12 +308,20 @@ async function createCss({ console.log(`\nāœ… Finished creating CSS`); } -function isOnlyCssOutput(config: ConfigSchema): boolean { - // Can be defined using either the shorthand or object syntax, so check for both. - const hasDesignTokensOutput = - config.output.find((o) => o.type === 'design-tokens') || - config.output.find((o) => o === ('design-tokens' as unknown as ConfigSchema['output'][number])); - const hasCSSTokensDir = config.output.find((o) => o.type === 'css')?.tokenDir; +/** Checks the config file as written, since validation adds defaults such as `tokenDir`. */ +function isOnlyCssOutput(config: ExternalConfigSchemaInput): boolean { + // No `output` means the default outputs, which include design tokens. + if (!config.output) { + return false; + } + + // Outputs can be defined using either the shorthand or object syntax, so check for both. + const hasDesignTokensOutput = config.output.some( + (o) => o === 'design-tokens' || (typeof o === 'object' && o.type === 'design-tokens'), + ); + const hasCSSTokensDir = config.output.some( + (o) => typeof o === 'object' && o.type === 'css' && o.tokenDir !== undefined, + ); return !hasDesignTokensOutput && !hasCSSTokensDir; } From 48d792cc132b0ea4912eea6fb4ab6ca1d1412043 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 28 Sep 2026 08:43:55 +0200 Subject: [PATCH 12/44] tweak docs --- .changeset/honest-pans-lose.md | 2 +- apps/www/app/content/fundamentals/en/code/cli-config.mdx | 3 +++ apps/www/app/content/fundamentals/no/code/cli-config.mdx | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/honest-pans-lose.md b/.changeset/honest-pans-lose.md index bb8ea4337b..c9f2731fd0 100644 --- a/.changeset/honest-pans-lose.md +++ b/.changeset/honest-pans-lose.md @@ -2,4 +2,4 @@ "@digdir/designsystemet": minor --- -**CLI:** New `output` field for defining outputs in config +**CLI:** New `output` field for defining outputs in config diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index 1ac1e2d4c7..e67aa145a8 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -9,6 +9,9 @@ published: true order: 40 --- + +Note that config is experimental. The config schema may change at any time, and will be communicated in the changelog. + Using a config file with the CLI gives you more control over your colours and makes it easier to update and maintain them. diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index a2d95dd8a9..fae3cbe5ff 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -10,6 +10,9 @@ order: 40 search_terms: konfigurasjon, tokens --- + +Merk at config er eksperimentelt. Config skjema kan endre seg nĆ„r som helst, og vil bli kommunisert i changelog. + ƅ bruke config fil med CLI gjer at du kan ha meir kontroll over fargane dine, og kan lettare oppdatere og vedlikehalde dei. From 197a2c5ff3ab199557425ad7d8cd8cc861bc5441 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 28 Sep 2026 09:46:59 +0200 Subject: [PATCH 13/44] update theme-builder --- .../token-modal/use-token-modal.ts | 21 ++++++++++--------- packages/cli/src/internal.ts | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/themebuilder/app/_components/token-modal/use-token-modal.ts b/apps/themebuilder/app/_components/token-modal/use-token-modal.ts index 5d679b8529..60200db619 100644 --- a/apps/themebuilder/app/_components/token-modal/use-token-modal.ts +++ b/apps/themebuilder/app/_components/token-modal/use-token-modal.ts @@ -1,6 +1,7 @@ -import type { - CssColor, - ExternalConfigSchemaInput, +import { + type CssColor, + defaultBorderRadius, + type ExternalConfigSchemaInput, } from '@digdir/designsystemet/internal'; import pkg from '@digdir/designsystemet/package.json'; import { useState } from 'react'; @@ -43,19 +44,17 @@ export const useTokenModal = () => { }, {} as Record, ), - borderRadius: baseBorderRadius, - typography: { - fontFamily: 'Inter', - }, + ...(baseBorderRadius !== defaultBorderRadius && { + borderRadius: baseBorderRadius, + }), }; const packageWithTag = `@digdir/designsystemet${isProduction ? '@latest' : '@next'}`; - const configBuildSnippet = `npx ${packageWithTag} tokens create --config designsystemet.config.json\nnpx ${packageWithTag} tokens build --config designsystemet.config.json`; + const configBuildSnippet = `npx ${packageWithTag}`; const configSnippet = { $schema: `https://designsystemet.no/schemas/config/${pkg.version}.json`, - outDir: './design-tokens', themes: { [name]: { colors: theme.colors, @@ -72,7 +71,9 @@ export const useTokenModal = () => { }, } : {}), - borderRadius: theme.borderRadius, + ...(theme.borderRadius !== undefined && { + borderRadius: theme.borderRadius, + }), }, }, }; diff --git a/packages/cli/src/internal.ts b/packages/cli/src/internal.ts index 4d20821f81..7cc2b5e31b 100644 --- a/packages/cli/src/internal.ts +++ b/packages/cli/src/internal.ts @@ -29,7 +29,7 @@ export { toFigmaCollections, } from './figma/collections.ts'; export { figmaVariableScopes, figmaVariableType } from './figma/scopes.ts'; -export { severityColors } from './schemas/defaults.ts'; +export { defaultBorderRadius, severityColors } from './schemas/defaults.ts'; export { parseConfig, validateConfig } from './schemas/helpers.ts'; export { type ConfigSchema, From aac9c4d02d56d18968493c9e0d043ba4856df52e Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 28 Sep 2026 12:07:04 +0200 Subject: [PATCH 14/44] remove banner --- packages/cli/src/schemas/__snapshots__/config.schema.json | 5 ----- packages/cli/src/schemas/schema-output.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/packages/cli/src/schemas/__snapshots__/config.schema.json b/packages/cli/src/schemas/__snapshots__/config.schema.json index 7649069b7d..5f95ba72bd 100644 --- a/packages/cli/src/schemas/__snapshots__/config.schema.json +++ b/packages/cli/src/schemas/__snapshots__/config.schema.json @@ -59,11 +59,6 @@ "description": "The directory containing the design tokens", "type": "string" }, - "banner": { - "default": "", - "description": "A banner to include at the top of the CSS file", - "type": "string" - }, "experimental_tailwind": { "default": true, "description": "Whether to enable experimental Tailwind support", diff --git a/packages/cli/src/schemas/schema-output.ts b/packages/cli/src/schemas/schema-output.ts index 71cf6d7f5f..6129ca3d1d 100644 --- a/packages/cli/src/schemas/schema-output.ts +++ b/packages/cli/src/schemas/schema-output.ts @@ -12,7 +12,6 @@ const cssOutputSchema = z.object({ dir: z.string().default('design-tokens-build').describe('The output directory'), cleanDir: z.boolean().default(true).describe('Whether to clean the output directory before generating files'), tokenDir: z.string().default('design-tokens').describe('The directory containing the design tokens'), - banner: z.string().default('').describe('A banner to include at the top of the CSS file'), experimental_tailwind: z.boolean().default(true).describe('Whether to enable experimental Tailwind support'), }); From aae27e68cdb7b087712539a31072d9eeaa0b170e Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 28 Sep 2026 14:53:32 +0200 Subject: [PATCH 15/44] wording --- packages/cli/bin/designsystemet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 0c0cee275c..ffb5f65e2b 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -46,7 +46,7 @@ program .addOption(dryOption()) .addOption(verboseOption()) .option('--skip-check', 'Skip migration check', false) - .option('-y, --yes', 'Skip user prompts', false) + .option('-y, --yes', 'Skip migration prompts and auto accept', false) .action(async (opts) => { const { verbose, dry } = opts; From 7a36bad25bc362d4519ca41d4e130c8f0b751e73 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 28 Sep 2026 14:53:39 +0200 Subject: [PATCH 16/44] skip-check for test --- packages/cli/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 857f7510b4..97eb82f24d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,8 +37,8 @@ "build:json-schema": "pnpm node ./src/scripts/create-json-schema.ts && pnpm node ./src/scripts/create-full-json-schema.ts && pnpm node ./src/scripts/create-example-config.ts", "types": "tsc --noEmit", "test:unit": "vitest run", - "test:tokens-create-options": "pnpm run designsystemet tokens create -m dominant:\"#007682\" -n \"#003333\" -b 99 -o ./temp/options/design-tokens --theme options --clean", - "test:tokens-create-config": "pnpm run designsystemet tokens create --config ./tests/test-tokens.config.jsonc", + "test:tokens-create-options": "pnpm run designsystemet tokens create -m dominant:\"#007682\" -n \"#003333\" -b 99 -o ./temp/options/design-tokens --theme options --clean --skip-check", + "test:tokens-create-config": "pnpm run designsystemet tokens create --config ./tests/test-tokens.config.jsonc --skip-check", "test:tokens-build-options": "pnpm run designsystemet tokens build -t ./temp/options/design-tokens -o ./temp/options/build --clean --experimental-tailwind", "test:tokens-build-config": "pnpm run designsystemet tokens build -t ./tests/config/design-tokens -o ./tests/config/build --clean --experimental-tailwind", "test:tokens-build-config:inspect": "pnpm run designsystemet:inspect tokens build -t ./tests/config/design-tokens -o ./tests/config/build --clean", From 617e1bbc81007bad882dab4b43550a1321e80705 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 28 Sep 2026 16:04:05 +0200 Subject: [PATCH 17/44] fix some options not being passed down --- packages/cli/bin/designsystemet.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index ffb5f65e2b..9ea3096bbd 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -36,7 +36,13 @@ const figletAscii = ` |___/ |___/ `; -program.name('designsystemet').description('CLI for working with Designsystemet').showHelpAfterError(); +program + .name('designsystemet') + .description('CLI for working with Designsystemet') + .showHelpAfterError() + // The root command and its subcommands share option names (e.g. --config, --skip-check), + // so only parse root options before the subcommand, leaving the rest to the subcommand. + .enablePositionalOptions(); program.hook('preAction', () => console.log(figletAscii)); program.version(pkg.version, '-v, --version', 'Display version number').helpOption('-h, --help', 'Display help'); From 6631a20f0a26505a9f968f2d3d98fead62b5d659 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Mon, 28 Sep 2026 16:04:13 +0200 Subject: [PATCH 18/44] run test --- packages/cli/tests/config/build/colors.d.ts | 2 +- packages/cli/tests/config/build/other-org.css | 4 ++-- packages/cli/tests/config/build/some-org.css | 4 ++-- packages/cli/tests/config/build/types.d.ts | 2 +- packages/cli/tests/config/design-tokens/$designsystemet.jsonc | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/cli/tests/config/build/colors.d.ts b/packages/cli/tests/config/build/colors.d.ts index 5336820273..b6ba694959 100644 --- a/packages/cli/tests/config/build/colors.d.ts +++ b/packages/cli/tests/config/build/colors.d.ts @@ -1,5 +1,5 @@ /* @deprecated: This file will be removed in a future release. Use types.d.ts instead */ -/* build: v1.22.0 */ +/* build: v1.23.0 */ import type {} from '@digdir/designsystemet-types'; // Augment types based on theme diff --git a/packages/cli/tests/config/build/other-org.css b/packages/cli/tests/config/build/other-org.css index 2d8a7860dd..2a451a7f57 100644 --- a/packages/cli/tests/config/build/other-org.css +++ b/packages/cli/tests/config/build/other-org.css @@ -1,8 +1,8 @@ @charset "UTF-8"; @layer ds.theme.color-scheme, ds.theme.color, ds.theme.forced-colors; /* -build: v1.22.0 -design-tokens: v1.22.0 +build: v1.23.0 +design-tokens: v1.23.0 */ @layer ds.theme.size-mode { diff --git a/packages/cli/tests/config/build/some-org.css b/packages/cli/tests/config/build/some-org.css index 214d67cd7a..6ce4f0546b 100644 --- a/packages/cli/tests/config/build/some-org.css +++ b/packages/cli/tests/config/build/some-org.css @@ -1,8 +1,8 @@ @charset "UTF-8"; @layer ds.theme.color-scheme, ds.theme.color, ds.theme.forced-colors; /* -build: v1.22.0 -design-tokens: v1.22.0 +build: v1.23.0 +design-tokens: v1.23.0 */ @layer ds.theme.size-mode { diff --git a/packages/cli/tests/config/build/types.d.ts b/packages/cli/tests/config/build/types.d.ts index e022e9d461..b433bc9584 100644 --- a/packages/cli/tests/config/build/types.d.ts +++ b/packages/cli/tests/config/build/types.d.ts @@ -1,4 +1,4 @@ -/* build: v1.22.0 */ +/* build: v1.23.0 */ import type {} from '@digdir/designsystemet-types'; // Augment types based on theme diff --git a/packages/cli/tests/config/design-tokens/$designsystemet.jsonc b/packages/cli/tests/config/design-tokens/$designsystemet.jsonc index db04db9041..d7503e3bb0 100644 --- a/packages/cli/tests/config/design-tokens/$designsystemet.jsonc +++ b/packages/cli/tests/config/design-tokens/$designsystemet.jsonc @@ -1,4 +1,4 @@ { "name": "@digdir/designsystemet", - "version": "1.22.0" + "version": "1.23.0" } \ No newline at end of file From 5ec61095a8438d52bd20c4dd2d512f14bee0b29f Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 29 Sep 2026 14:00:35 +0200 Subject: [PATCH 19/44] made themes optional --- packages/cli/bin/config.ts | 15 +++++++++++++++ packages/cli/bin/deprecated.ts | 13 +++++++++---- packages/cli/bin/designsystemet.ts | 12 ++++++------ .../docs/designsystemet.config.defaults.json | 1 - packages/cli/src/figma/scopes.test.ts | 2 +- .../schemas/__snapshots__/config.schema.json | 5 +---- packages/cli/src/schemas/schema.ts | 18 +++++++++++++----- .../cli/src/scripts/update-preview-tokens.ts | 4 ++++ packages/cli/src/tokens/css-variables.test.ts | 2 +- packages/cli/src/tokens/generate-config.ts | 5 +++-- 10 files changed, 53 insertions(+), 24 deletions(-) diff --git a/packages/cli/bin/config.ts b/packages/cli/bin/config.ts index 9376f22f2b..36f6413d28 100644 --- a/packages/cli/bin/config.ts +++ b/packages/cli/bin/config.ts @@ -4,6 +4,7 @@ import * as R from 'ramda'; import { parseConfig, validateConfig } from '../src/schemas/helpers.ts'; import { type ConfigSchema, + type ConfigSchemaThemes, configSchema, type ExternalConfigSchema, externalConfigSchema, @@ -14,6 +15,20 @@ import { getCliOption, getDefaultCliOption, getSuppliedCliOption, type OptionGet export { deprecatedCLIOptions } from '../src/schemas/helpers.ts'; +/** Returns the themes of a validated config, or exits with an error if there are none. */ +export function requireThemes(config: Pick): ConfigSchemaThemes { + if (!config.themes) { + console.error( + pc.redBright( + `No themes found in config file. Add ${pc.blue('themes')}, or set ${pc.blue('tokenDir')} on the ${pc.blue('css')} output to build CSS from existing design tokens.`, + ), + ); + process.exit(1); + } + + return config.themes; +} + // Default config files to auto-detect when no --config is supplied, in order of precedence. export const DEFAULT_CONFIG_FILEPATHS = ['designsystemet.config.json', 'designsystemet.config.jsonc']; export const DEFAULT_CONFIG_FILEPATH = DEFAULT_CONFIG_FILEPATHS[0]; diff --git a/packages/cli/bin/deprecated.ts b/packages/cli/bin/deprecated.ts index beb668b11c..66c64e9f60 100644 --- a/packages/cli/bin/deprecated.ts +++ b/packages/cli/bin/deprecated.ts @@ -3,9 +3,14 @@ import pc from 'picocolors'; import { checkAutomigrate } from '../src/automigrate.ts'; import { convertToHex } from '../src/colors/index.ts'; import type { CssColor } from '../src/colors/types.ts'; -import type { ConfigSchema } from '../src/schemas/schema.ts'; +import type { ConfigSchemaThemes } from '../src/schemas/schema.ts'; import { dsfs } from '../src/utils/filesystem.ts'; -import { deprecatedCLIOptions as cliOptions, getConfigFile, parseValidateAndOptsConfig } from './config.ts'; +import { + deprecatedCLIOptions as cliOptions, + getConfigFile, + parseValidateAndOptsConfig, + requireThemes, +} from './config.ts'; import { configOption, dryOption, parseBoolean, verboseOption } from './options.ts'; export const DEFAULT_TOKENS_CREATE_DIR = './design-tokens'; @@ -14,7 +19,7 @@ const DEFAULT_FONT = 'Inter'; const DEFAULT_THEME_NAME = 'theme'; type TokenCommandDeps = { - createDesignTokens: (options: { themes: ConfigSchema['themes']; outDir: string; clean?: boolean }) => Promise; + createDesignTokens: (options: { themes: ConfigSchemaThemes; outDir: string; clean?: boolean }) => Promise; buildCss: (options: { tokensDir: string; outDir: string; @@ -145,7 +150,7 @@ export function makeTokenCommands({ createDesignTokens, buildCss }: TokenCommand dsfs.init({ dry: opts.dry, outdir: config.outDir }); await createDesignTokens({ - themes: config.themes, + themes: requireThemes(config), outDir: dsfs.outDir, clean: config.clean, }); diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 9ea3096bbd..b0c9f88ac6 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -8,7 +8,7 @@ import { checkAutomigrate } from '../src/automigrate.ts'; import migrations from '../src/migrations/index.ts'; import { parseConfig, validateConfig } from '../src/schemas/helpers.ts'; import { - type ConfigSchema, + type ConfigSchemaThemes, configSchema, type ExternalConfigSchemaInput, externalConfigSchema, @@ -21,7 +21,7 @@ import { generateConfigFromTokens } from '../src/tokens/generate-config.ts'; import type { OutputFile, Theme } from '../src/tokens/types.ts'; import { toColorNames } from '../src/tokens/utils.ts'; import { dsfs } from '../src/utils/filesystem.ts'; -import { DEFAULT_CONFIG_FILEPATH, getConfigFile } from './config.ts'; +import { DEFAULT_CONFIG_FILEPATH, getConfigFile, requireThemes } from './config.ts'; import { DEFAULT_TOKENS_CREATE_DIR, makeTokenCommands } from './deprecated.ts'; import { configOption, dryOption, verboseOption } from './options.ts'; @@ -85,7 +85,7 @@ program console.log(`\nšŸ± Creating design tokens in ${pc.green(output.dir)}...`); await createDesignTokens({ - themes: config.themes, + themes: requireThemes(config), outDir: outDir, clean: output.cleanDir, }); @@ -97,7 +97,7 @@ program // Only generate create CSS if no `design-tokens` output is present and no `tokenDir` is explicitly set in the config file. Otherwise, build CSS from existing design tokens. if (isOnlyCssOutput(parsedConfig)) { await createCss({ - themes: config.themes, + themes: requireThemes(config), outDir: outDir, clean: output.cleanDir, verbose, @@ -198,7 +198,7 @@ async function createDesignTokens({ outDir, clean, }: { - themes: ConfigSchema['themes']; + themes: ConfigSchemaThemes; outDir: string; clean?: boolean; }) { @@ -280,7 +280,7 @@ async function createCss({ verbose, tailwind, }: { - themes: ConfigSchema['themes']; + themes: ConfigSchemaThemes; outDir: string; clean?: boolean; verbose: boolean; diff --git a/packages/cli/docs/designsystemet.config.defaults.json b/packages/cli/docs/designsystemet.config.defaults.json index 13115dfbf5..0d67b3bf8e 100644 --- a/packages/cli/docs/designsystemet.config.defaults.json +++ b/packages/cli/docs/designsystemet.config.defaults.json @@ -10,7 +10,6 @@ "dir": "design-tokens-build", "cleanDir": true, "tokenDir": "design-tokens", - "banner": "", "experimental_tailwind": true } ], diff --git a/packages/cli/src/figma/scopes.test.ts b/packages/cli/src/figma/scopes.test.ts index 838b4f36e8..857863bba6 100644 --- a/packages/cli/src/figma/scopes.test.ts +++ b/packages/cli/src/figma/scopes.test.ts @@ -61,7 +61,7 @@ describe('figmaVariableScopes covers every generated token', () => { const config = configSchema.parse({ themes: { [themeName]: { colors: { neutral: '#444444', brand: '#0062BA' } } }, }); - const theme = { name: themeName, ...config.themes[themeName] } as Theme; + const theme = { name: themeName, ...config.themes?.[themeName] } as Theme; const dimensions = getTokenSetDimensions(theme); const { tokenSets } = await createTokens(theme, dimensions); const $themes = await generate$Themes(dimensions, [themeName], toColorNames(theme.colors)); diff --git a/packages/cli/src/schemas/__snapshots__/config.schema.json b/packages/cli/src/schemas/__snapshots__/config.schema.json index 5f95ba72bd..c1c386bf5c 100644 --- a/packages/cli/src/schemas/__snapshots__/config.schema.json +++ b/packages/cli/src/schemas/__snapshots__/config.schema.json @@ -270,8 +270,5 @@ }, "description": "An object with one or more themes. Each property defines a theme, and the property name is used as the theme name. All themes must define the same color names." } - }, - "required": [ - "themes" - ] + } } diff --git a/packages/cli/src/schemas/schema.ts b/packages/cli/src/schemas/schema.ts index 65f248a8b2..4f418a3391 100644 --- a/packages/cli/src/schemas/schema.ts +++ b/packages/cli/src/schemas/schema.ts @@ -221,7 +221,8 @@ export const themesSchema = z .meta({ description: 'An object with one or more themes. Each property defines a theme, and the property name is used as the theme name. All themes must define the same color names, size configuration, shadows, border widths, opacities, border-radius step names and typography sets.', - }); + }) + .optional(); export type ConfigSchemaTheme = z.infer; /** The pre-validation shape of a theme, i.e. what users write: defaulted fields are optional. */ @@ -235,6 +236,9 @@ export const configSchema = z.object({ export type ConfigSchema = z.infer; +/** The themes of a config. `themes` is optional, since it's only needed by outputs that are created from themes. */ +export type ConfigSchemaThemes = NonNullable; + export type ConfigSchemaInput = z.input; /** @@ -269,10 +273,14 @@ const externalThemeSchema = themeObjectSchema * use {@link configSchema} to validate a config in the CLI. */ export const externalConfigSchema = configSchema.extend({ - themes: z.record(z.string(), externalThemeSchema).superRefine(checkThemes).meta({ - description: - 'An object with one or more themes. Each property defines a theme, and the property name is used as the theme name. All themes must define the same color names.', - }), + themes: z + .record(z.string(), externalThemeSchema) + .superRefine(checkThemes) + .meta({ + description: + 'An object with one or more themes. Each property defines a theme, and the property name is used as the theme name. All themes must define the same color names.', + }) + .optional(), }); export type ExternalConfigSchema = z.infer; diff --git a/packages/cli/src/scripts/update-preview-tokens.ts b/packages/cli/src/scripts/update-preview-tokens.ts index 1df421ae44..ed71fd6e4b 100644 --- a/packages/cli/src/scripts/update-preview-tokens.ts +++ b/packages/cli/src/scripts/update-preview-tokens.ts @@ -104,6 +104,10 @@ const formatTheme = async (themeConfig: Theme) => { // Parse the config through the schema so defaults (typography, borderRadius, size) are applied. const { themes } = validateConfig(configSchema, config); +if (!themes) { + throw new Error('No themes found in designsystemet.config.json'); +} + formatTheme({ name: 'test', borderRadius: themes.designsystemet.borderRadius, diff --git a/packages/cli/src/tokens/css-variables.test.ts b/packages/cli/src/tokens/css-variables.test.ts index d01ff9cad5..9157e6cf23 100644 --- a/packages/cli/src/tokens/css-variables.test.ts +++ b/packages/cli/src/tokens/css-variables.test.ts @@ -79,7 +79,7 @@ describe('cssVariableName matches the tokens build output', () => { const config = configSchema.parse({ themes: { [themeName]: { colors: { neutral: '#444444', brand: '#0062BA' } } }, }); - const theme = { name: themeName, ...config.themes[themeName] } as Theme; + const theme = { name: themeName, ...config.themes?.[themeName] } as Theme; const files = await formatThemeCSS(theme, { verbose: false, tailwind: false }); const css = files.map((file) => file.output).join('\n'); diff --git a/packages/cli/src/tokens/generate-config.ts b/packages/cli/src/tokens/generate-config.ts index 75f2b06ecc..828dbf592a 100644 --- a/packages/cli/src/tokens/generate-config.ts +++ b/packages/cli/src/tokens/generate-config.ts @@ -210,9 +210,10 @@ export async function generateConfigFromTokens(options: GenerateConfigOptions): console.log(`\nFound ${pc.green(String(themes.length))} theme(s): ${themes.map((t) => pc.cyan(t)).join(', ')}`); // Generate config for each theme + const configThemes: NonNullable = {}; const config: ExternalConfigSchemaInput = { outDir: tokensDir, - themes: {}, + themes: configThemes, }; for (const themeName of themes) { @@ -234,7 +235,7 @@ export async function generateConfigFromTokens(options: GenerateConfigOptions): const borderRadius = extractBorderRadius(themeConfig); const fontFamily = extractFontFamily(themeConfig) ?? extractFontFamilyFromPrimitives(typographyConfig, themeName); - config.themes[themeName] = { + configThemes[themeName] = { colors, borderRadius, typography: fontFamily ? { fontFamily } : undefined, From 1cb8213bb519d21fab75cbaf8d0d577df206eb0d Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 29 Sep 2026 14:02:44 +0200 Subject: [PATCH 20/44] update generate config from tokens --- packages/cli/src/migrations/new-output-field.ts | 4 ++-- packages/cli/src/tokens/generate-config.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 68e8b3c963..718e539808 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -67,7 +67,7 @@ const defaultOutDir = outputConfigShape.outDir.parse(undefined); * `clean` never needs to be carried over: its default is covered by the default `output`, * and `clean: true` matches the default `cleanDir`. */ -const toOutput = (outDir: string | undefined) => { +export const toOutput = (outDir: string | undefined) => { if (outDir === undefined || path.posix.normalize(outDir) === path.posix.normalize(defaultOutDir)) { return undefined; } @@ -76,7 +76,7 @@ const toOutput = (outDir: string | undefined) => { { type: 'design-tokens', dir: outDir }, // CSS is built from the design tokens, so it must read them from the same directory. { type: 'css', tokenDir: outDir }, - ]; + ] as const; }; export const migrateToOutputField = (config: string): string => { diff --git a/packages/cli/src/tokens/generate-config.ts b/packages/cli/src/tokens/generate-config.ts index 828dbf592a..c4b2a5345e 100644 --- a/packages/cli/src/tokens/generate-config.ts +++ b/packages/cli/src/tokens/generate-config.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import pc from 'picocolors'; import type { CssColor } from '../colors/types.ts'; +import { toOutput } from '../migrations/new-output-field.ts'; import type { ExternalConfigSchemaInput } from '../schemas/schema.ts'; import { dsfs } from '../utils/filesystem.ts'; @@ -189,6 +190,7 @@ function extractColors(themeTokens: TokenObject, themeName: string): Record { - const { tokensDir } = options; + const { tokensDir, outFile } = options; console.log(`\nReading tokens from ${pc.blue(tokensDir)}`); @@ -210,9 +212,15 @@ export async function generateConfigFromTokens(options: GenerateConfigOptions): console.log(`\nFound ${pc.green(String(themes.length))} theme(s): ${themes.map((t) => pc.cyan(t)).join(', ')}`); // Generate config for each theme + // Paths in a config are relative to the config file, and always use forward slashes so the config works on any OS. + const configDir = outFile ? path.dirname(path.resolve(outFile)) : process.cwd(); + const relativeTokensDir = (path.relative(configDir, path.resolve(tokensDir)) || '.').split(path.sep).join('/'); + const configThemes: NonNullable = {}; + const output = toOutput(relativeTokensDir); const config: ExternalConfigSchemaInput = { - outDir: tokensDir, + // Omitted when the tokens are in the default directory, since the default `output` covers it. + ...(output && { output: [...output] }), themes: configThemes, }; From b219b2db1dcbc5592728d92611066c55ce14e5bd Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Tue, 29 Sep 2026 15:32:50 +0200 Subject: [PATCH 21/44] fix use of themes thats optional --- .../_components/token-modal/use-token-modal.ts | 2 +- apps/themebuilder/app/_utils/config-to-url.ts | 2 +- plugins/designsystemet/src/plugin/code.ts | 16 +++++++++------- plugins/designsystemet/src/ui/app.tsx | 2 +- plugins/designsystemet/src/ui/preview-view.tsx | 7 ++++--- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/themebuilder/app/_components/token-modal/use-token-modal.ts b/apps/themebuilder/app/_components/token-modal/use-token-modal.ts index 60200db619..6e7640c8ff 100644 --- a/apps/themebuilder/app/_components/token-modal/use-token-modal.ts +++ b/apps/themebuilder/app/_components/token-modal/use-token-modal.ts @@ -36,7 +36,7 @@ export const useTokenModal = () => { } }); - const theme: ExternalConfigSchemaInput['themes'][string] = { + const theme: NonNullable[string] = { colors: colors.reduce( (acc, color) => { acc[color.name] = color.colors.light['base-default']?.hex || '#'; diff --git a/apps/themebuilder/app/_utils/config-to-url.ts b/apps/themebuilder/app/_utils/config-to-url.ts index 708ad75de3..201edf0bdb 100644 --- a/apps/themebuilder/app/_utils/config-to-url.ts +++ b/apps/themebuilder/app/_utils/config-to-url.ts @@ -6,7 +6,7 @@ const QUERY_SEPARATOR = ' '; * Converts a theme config object to a themebuilder URL with query parameters */ export function configThemeToUrl( - theme: ConfigSchema['themes']['default'], + theme: NonNullable[string], lang = 'no', ): string { const params = new URLSearchParams(); diff --git a/plugins/designsystemet/src/plugin/code.ts b/plugins/designsystemet/src/plugin/code.ts index 427edb47a0..50ed730897 100644 --- a/plugins/designsystemet/src/plugin/code.ts +++ b/plugins/designsystemet/src/plugin/code.ts @@ -62,17 +62,19 @@ figma.ui.onmessage = async (msg: FigmaMessages) => { externalConfig, ); - themeNames = Object.keys(config.themes ?? {}); + // `themes` is optional in the config, but the plugin creates everything from themes. + const { themes } = config; + if (!themes || Object.keys(themes).length === 0) { + throw new Error('The config must define at least one theme.'); + } + + themeNames = Object.keys(themes); // The dimensions come from the first theme, mirroring the CLI: size modes and // typography sets are expected to be the same across themes. - const tokenSetDimensions = getTokenSetDimensions( - config.themes[themeNames[0]], - ); + const tokenSetDimensions = getTokenSetDimensions(themes[themeNames[0]]); - for (const [themeName, themeConfig] of Object.entries( - config.themes, - ) as [string, ConfigSchema['themes'][string]][]) { + for (const [themeName, themeConfig] of Object.entries(themes)) { const themeTokens = await createTokens( { name: themeName, diff --git a/plugins/designsystemet/src/ui/app.tsx b/plugins/designsystemet/src/ui/app.tsx index 7caccfc704..a878da32d1 100644 --- a/plugins/designsystemet/src/ui/app.tsx +++ b/plugins/designsystemet/src/ui/app.tsx @@ -37,7 +37,7 @@ function reducer(state: UiState, action: Action): UiState { return { ...state, config: action.config, - selectedTheme: Object.keys(action.config.themes)[0] ?? null, + selectedTheme: Object.keys(action.config.themes ?? {})[0] ?? null, selectedScheme: action.scheme, notification: action.notification, }; diff --git a/plugins/designsystemet/src/ui/preview-view.tsx b/plugins/designsystemet/src/ui/preview-view.tsx index e6f041074e..ac176c52df 100644 --- a/plugins/designsystemet/src/ui/preview-view.tsx +++ b/plugins/designsystemet/src/ui/preview-view.tsx @@ -16,7 +16,7 @@ type PreviewViewProps = { onSelectScheme: (scheme: string) => void; }; -type ThemeConfig = ConfigSchema['themes'][string]; +type ThemeConfig = NonNullable[string]; // Pascal case to match the Figma variable modes ('Light'/'Dark'). const COLOR_SCHEME_OPTIONS = ['Light', 'Dark']; @@ -32,9 +32,10 @@ export function PreviewView({ onSelectTheme, onSelectScheme, }: PreviewViewProps): React.JSX.Element { - const themeNames = Object.keys(config.themes); + const themes = config.themes ?? {}; + const themeNames = Object.keys(themes); const themeName = pickOption(themeNames, selectedTheme); - const theme = themeName ? config.themes[themeName] : null; + const theme = themeName ? themes[themeName] : null; const scheme = selectedScheme.toLowerCase() as ColorScheme; return ( From 6b0d73120fcb7205f25e7d5979638c128643e906 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 30 Sep 2026 08:51:16 +0200 Subject: [PATCH 22/44] more changelogs --- .changeset/clear-buckets-shout.md | 5 +++++ .changeset/solid-turkeys-buy.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/clear-buckets-shout.md create mode 100644 .changeset/solid-turkeys-buy.md diff --git a/.changeset/clear-buckets-shout.md b/.changeset/clear-buckets-shout.md new file mode 100644 index 0000000000..6b5058ede0 --- /dev/null +++ b/.changeset/clear-buckets-shout.md @@ -0,0 +1,5 @@ +--- +"@digdir/designsystemet": patch +--- + +**Deprecated:** Commands `token create` and `token build`. Use `designsystemet` with new `output` field in `designsystemet.config.json` to configure outputs. diff --git a/.changeset/solid-turkeys-buy.md b/.changeset/solid-turkeys-buy.md new file mode 100644 index 0000000000..0b133a1755 --- /dev/null +++ b/.changeset/solid-turkeys-buy.md @@ -0,0 +1,5 @@ +--- +"@digdir/designsystemet": patch +--- + +**Deprecated:** `outDir` and `clean` are deprecated in the Config schema, and replaced by `output[].dir` and `output[].cleanDir` for the respective output type. From 2281794ed907bfc578d35b5fa298158f02845829 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 30 Sep 2026 09:20:11 +0200 Subject: [PATCH 23/44] update docs --- .../fundamentals/en/code/cli-config.mdx | 28 ++++++++++++++----- .../fundamentals/no/code/cli-config.mdx | 28 +++++++++++++++---- packages/cli/README.md | 19 +++++++++++-- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index e67aa145a8..f4433dcb83 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -69,15 +69,15 @@ The `tokens create` and `tokens build` commands are deprecated and will be remov | Name | Type | Required | Description | | ---- | ---- | ------- | ----------- | | $schema | String | No | Path to JSON schema for validation. Recommended: `node_modules/@digdir/designsystemet/dist/config.schema.json`. | -| output | Array | No | What the CLI should create, and where. Defaults to `["design-tokens", "css"]`. See [Output](#output). | -| themes | Object | Yes | Contains all themes you want to define. Each key is the name of the theme. | +| output | Array | No | What the CLI should create, and where. Defaults to `["design-tokens", "css", "types"]`. See [Output](#output). | +| themes | Object | No | Contains all themes you want to define. Each key is the name of the theme. Required by outputs that are created from themes, see [Building without themes](#building-without-themes). | | outDir | String | No | **Deprecated:** use `output[].dir` instead. | | clean | Boolean | No | **Deprecated:** use `output[].cleanDir` instead. | ### Output `output` is a list of what the CLI should create. Each item is either the name of an output type, which uses its default settings, or an object with custom settings. -Design tokens are always created before CSS, since CSS is built from the design tokens. +Outputs are always created in the order `design-tokens`, `css`, whatever order they are listed in. When CSS is built from design tokens (with `tokenDir`), the design tokens must be created first. All paths are relative to the config file. @@ -106,9 +106,23 @@ All paths are relative to the config file. | dir | String | No | `design-tokens-build` | The folder where CSS should be saved. | | cleanDir | Boolean | No | `true` | Delete the folder before creating CSS. | -If you only need CSS, you can use `"output": ["css"]` without `tokenDir`. The CSS is then created directly from the themes, without saving any design tokens. -| tokenDir | String | No | `design-tokens` | The folder containing the design tokens to build CSS from. Should match `dir` of the `design-tokens` output. | -| experimental_tailwind | Boolean | No | `true` | Also generate Tailwind CSS classes. Experimental. | +#### Without design tokens + +If you only need CSS, you can use for example `"output": ["css"]` without `tokenDir`. They are then created directly from the themes, without saving any design tokens. + +#### Building without themes + +`themes` is only needed by outputs that are created from themes. If you already have design tokens, you can leave out `themes` and build from them by setting `tokenDir`: + +```json +{ + "output": [ + { "type": "css", "tokenDir": "./design-tokens" }, + ] +} +``` + +If an output needs themes and there are none, the CLI stops with an error. ### Migrating from outDir and clean @@ -119,7 +133,7 @@ If you'd rather do it manually, replace `"outDir": ""` with: { "output": [ { "type": "design-tokens", "dir": "" }, - { "type": "css", "tokenDir": "" } + { "type": "css", "tokenDir": "" }, ] } ``` diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index fae3cbe5ff..24f876c672 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -71,14 +71,15 @@ Kommandoane `tokens create` og `tokens build` er utdaterte og blir fjerna i ein | ---- | ---- | ------- | ----------- | | $schema | Streng | Nei | Sti til JSON schema for validering. Anbefalt: `node_modules/@digdir/designsystemet/dist/config.schema.json`. | | output | Liste | Nei | Kva CLI-et skal lage, og kvar. Standard er `["design-tokens", "css"]`. SjĆ„ [Output](#output). | -| themes | Objekt | Ja | Inneheld alle tema du vil definere. Ny nĆøkkel er namnet pĆ„ temaet. | +| themes | Objekt | Nei | Inneheld alle tema du vil definere. Kvar nĆøkkel er namnet pĆ„ temaet. PĆ„krevd for outputar som blir laga frĆ„ tema, sjĆ„ [Bygge utan tema](#bygge-utan-tema). | | outDir | Streng | Nei | **Utdatert:** bruk `output[].dir` i staden. | | clean | Boolean | Nei | **Utdatert:** bruk `output[].cleanDir` i staden. | ### Output `output` er ei liste over kva CLI-et skal lage. Kvart element er anten namnet pĆ„ ein output-type, som brukar standardinnstillingane, eller eit objekt med eigne innstillingar. -Design tokens blir alltid laga fĆør CSS, sidan CSS blir bygd frĆ„ design tokens. + +Outputane blir alltid laga i rekkjefĆølgja `design-tokens`, `css`, same kva rekkjefĆølgje dei stĆ„r i. NĆ„r CSS-en blir bygd frĆ„ design tokens (med `tokenDir`), mĆ„ design tokens vere laga fĆørst. Alle stiar er relative til config fila. @@ -107,9 +108,23 @@ Alle stiar er relative til config fila. | dir | Streng | Nei | `design-tokens-build` | Mappa der CSS skal lagrast. | | cleanDir | Boolean | Nei | `true` | Slett mappa fĆør CSS blir laga. | -Treng du berre CSS, kan du bruke `"output": ["css"]` utan `tokenDir`. CSS-en blir dĆ„ laga direkte frĆ„ tema, utan at design tokens blir lagra. -| tokenDir | Streng | Nei | `design-tokens` | Mappa med design tokens som CSS skal byggast frĆ„. BĆør vere lik `dir` i `design-tokens`-outputen. | -| experimental_tailwind | Boolean | Nei | `true` | Lag òg Tailwind CSS-klassar. Eksperimentelt. | +#### Utan design tokens + +Treng du berre CSS, kan du bruke til dĆømes `"output": ["css"]` utan `tokenDir`. Dei blir dĆ„ laga direkte frĆ„ tema, utan at design tokens blir lagra. + +#### Bygge utan tema + +`themes` trengst berre for outputar som blir laga frĆ„ tema. Har du alt design tokens, kan du slĆøyfe `themes` og byggje frĆ„ dei ved Ć„ setje `tokenDir`: + +```json +{ + "output": [ + { "type": "css", "tokenDir": "./design-tokens" } + ] +} +``` + +Treng ein output tema og det ikkje finst nokon, stoppar CLI-et med ein feil. ### Migrere frĆ„ outDir og clean @@ -120,7 +135,8 @@ Vil du heller gjere det manuelt, byt ut `"outDir": ""` med: { "output": [ { "type": "design-tokens", "dir": "" }, - { "type": "css", "tokenDir": "" } + { "type": "css", "tokenDir": "" }, + { "type": "types", "tokenDir": "" } ] } ``` diff --git a/packages/cli/README.md b/packages/cli/README.md index eaffbce9b4..e107b4b62e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -9,7 +9,7 @@ Read the Designsystemet [README](https://github.com/digdir/designsystemet) to ge ## Usage -Use `npx @digdir/designsystemet` to create design tokens and CSS for use with Designsystemet, based on a [config file](#using-a-config-file). +Use `npx @digdir/designsystemet` to create design tokens, CSS and type declarations for use with Designsystemet, based on a [config file](#using-a-config-file). This allows you to define themes including custom colors, font-family, and border-radius. We recommend using the [Designsystemet theme builder](https://theme.designsystemet.no/) for generating a valid config. @@ -94,7 +94,7 @@ Use `output` to choose what is created and where. Each item is either an output "output": [ // defaults: dir "design-tokens", cleanDir true { "type": "design-tokens", "dir": "../path/to/design-tokens" }, - // defaults: dir "design-tokens-build", tokenDir "design-tokens", cleanDir true + // defaults: dir "design-tokens-build", tokenDir "design-tokens", cleanDir true, tailwind false { "type": "css", "dir": "../path/to/css", "tokenDir": "../path/to/design-tokens" }, ], } @@ -104,6 +104,17 @@ Design tokens are always created before CSS. `tokenDir` should match the `dir` o If you only need CSS, use `"output": ["css"]` without `tokenDir`. The CSS is then created directly from the themes, without writing any design tokens. +`themes` is only needed by outputs that are created from themes. To build CSS and types from existing design tokens, leave out `themes` and set `tokenDir`: + +```jsonc +{ + "output": [ + { "type": "css", "tokenDir": "./design-tokens" }, + { "type": "types", "tokenDir": "./design-tokens" }, + ], +} +``` + The `outDir` and `clean` fields are deprecated in favour of `output`. The CLI will offer to migrate your config file automatically. #### Complex config example @@ -115,5 +126,7 @@ Have a look at the `*.config.json` files under the `packages/cli` in the Github You can get a minimal config file, meaning without overrides, generated from existing design tokens using the following command: ```sh -npx @digdir/designsystemet generate-config-from-tokens --dir +npx @digdir/designsystemet generate-config-from-tokens --dir --out ``` + +`--out` defaults to `designsystemet.config.json`. The generated config uses `output`, with the design tokens directory written relative to the config file. From df6b2592f5233f9829a12ae7adce0cc786ca8a1e Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Wed, 30 Sep 2026 09:37:16 +0200 Subject: [PATCH 24/44] docs: align CLI docs with this layer Remove references to the types and tailwind options, which come later in the stack, document tokenDir on the css output, and fix invalid JSON in examples. Co-Authored-By: Claude Opus 5.5 --- apps/www/app/content/fundamentals/en/code/cli-config.mdx | 9 +++++---- apps/www/app/content/fundamentals/no/code/cli-config.mdx | 6 +++--- packages/cli/README.md | 7 +++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index f4433dcb83..bb98582335 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -69,7 +69,7 @@ The `tokens create` and `tokens build` commands are deprecated and will be remov | Name | Type | Required | Description | | ---- | ---- | ------- | ----------- | | $schema | String | No | Path to JSON schema for validation. Recommended: `node_modules/@digdir/designsystemet/dist/config.schema.json`. | -| output | Array | No | What the CLI should create, and where. Defaults to `["design-tokens", "css", "types"]`. See [Output](#output). | +| output | Array | No | What the CLI should create, and where. Defaults to `["design-tokens", "css"]`. See [Output](#output). | | themes | Object | No | Contains all themes you want to define. Each key is the name of the theme. Required by outputs that are created from themes, see [Building without themes](#building-without-themes). | | outDir | String | No | **Deprecated:** use `output[].dir` instead. | | clean | Boolean | No | **Deprecated:** use `output[].cleanDir` instead. | @@ -105,10 +105,11 @@ All paths are relative to the config file. | type | `"css"` | Yes | | The output type. | | dir | String | No | `design-tokens-build` | The folder where CSS should be saved. | | cleanDir | Boolean | No | `true` | Delete the folder before creating CSS. | +| tokenDir | String | No | `design-tokens` | The folder containing the design tokens to build CSS from. Should match `dir` of the `design-tokens` output. | #### Without design tokens -If you only need CSS, you can use for example `"output": ["css"]` without `tokenDir`. They are then created directly from the themes, without saving any design tokens. +If you only need CSS, you can use for example `"output": ["css"]` without `tokenDir`. It is then created directly from the themes, without saving any design tokens. #### Building without themes @@ -117,7 +118,7 @@ If you only need CSS, you can use for example `"output": ["css"]` without `token ```json { "output": [ - { "type": "css", "tokenDir": "./design-tokens" }, + { "type": "css", "tokenDir": "./design-tokens" } ] } ``` @@ -133,7 +134,7 @@ If you'd rather do it manually, replace `"outDir": ""` with: { "output": [ { "type": "design-tokens", "dir": "" }, - { "type": "css", "tokenDir": "" }, + { "type": "css", "tokenDir": "" } ] } ``` diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index 24f876c672..30fafeb4b8 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -107,10 +107,11 @@ Alle stiar er relative til config fila. | type | `"css"` | Ja | | Output-typen. | | dir | Streng | Nei | `design-tokens-build` | Mappa der CSS skal lagrast. | | cleanDir | Boolean | Nei | `true` | Slett mappa fĆør CSS blir laga. | +| tokenDir | Streng | Nei | `design-tokens` | Mappa med design tokens som CSS skal byggast frĆ„. BĆør vere lik `dir` i `design-tokens`-outputen. | #### Utan design tokens -Treng du berre CSS, kan du bruke til dĆømes `"output": ["css"]` utan `tokenDir`. Dei blir dĆ„ laga direkte frĆ„ tema, utan at design tokens blir lagra. +Treng du berre CSS, kan du bruke til dĆømes `"output": ["css"]` utan `tokenDir`. CSS-en blir dĆ„ laga direkte frĆ„ tema, utan at design tokens blir lagra. #### Bygge utan tema @@ -135,8 +136,7 @@ Vil du heller gjere det manuelt, byt ut `"outDir": ""` med: { "output": [ { "type": "design-tokens", "dir": "" }, - { "type": "css", "tokenDir": "" }, - { "type": "types", "tokenDir": "" } + { "type": "css", "tokenDir": "" } ] } ``` diff --git a/packages/cli/README.md b/packages/cli/README.md index e107b4b62e..c02734ac27 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -9,7 +9,7 @@ Read the Designsystemet [README](https://github.com/digdir/designsystemet) to ge ## Usage -Use `npx @digdir/designsystemet` to create design tokens, CSS and type declarations for use with Designsystemet, based on a [config file](#using-a-config-file). +Use `npx @digdir/designsystemet` to create design tokens and CSS for use with Designsystemet, based on a [config file](#using-a-config-file). This allows you to define themes including custom colors, font-family, and border-radius. We recommend using the [Designsystemet theme builder](https://theme.designsystemet.no/) for generating a valid config. @@ -94,7 +94,7 @@ Use `output` to choose what is created and where. Each item is either an output "output": [ // defaults: dir "design-tokens", cleanDir true { "type": "design-tokens", "dir": "../path/to/design-tokens" }, - // defaults: dir "design-tokens-build", tokenDir "design-tokens", cleanDir true, tailwind false + // defaults: dir "design-tokens-build", tokenDir "design-tokens", cleanDir true { "type": "css", "dir": "../path/to/css", "tokenDir": "../path/to/design-tokens" }, ], } @@ -104,13 +104,12 @@ Design tokens are always created before CSS. `tokenDir` should match the `dir` o If you only need CSS, use `"output": ["css"]` without `tokenDir`. The CSS is then created directly from the themes, without writing any design tokens. -`themes` is only needed by outputs that are created from themes. To build CSS and types from existing design tokens, leave out `themes` and set `tokenDir`: +`themes` is only needed by outputs that are created from themes. To build CSS from existing design tokens, leave out `themes` and set `tokenDir`: ```jsonc { "output": [ { "type": "css", "tokenDir": "./design-tokens" }, - { "type": "types", "tokenDir": "./design-tokens" }, ], } ``` From 694409ea19e787ace1f9e56f61011a29148ef7ac Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 09:12:25 +0200 Subject: [PATCH 25/44] fix jsonc parse in migration --- .../src/migrations/new-output-field.test.ts | 23 ++++++++++++++++++- .../cli/src/migrations/new-output-field.ts | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/migrations/new-output-field.test.ts b/packages/cli/src/migrations/new-output-field.test.ts index ca470c29f4..067ab65718 100644 --- a/packages/cli/src/migrations/new-output-field.test.ts +++ b/packages/cli/src/migrations/new-output-field.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { parseJsonc } from '../schemas/helpers.ts'; import migration, { migrateToOutputField } from './new-output-field.ts'; @@ -47,6 +47,27 @@ describe('new output field migration', () => { expect(parseJsonc(migrated)).toEqual({ output: ['css'] }); }); + it('migrates JSONC configs with comments and trailing commas', () => { + vi.spyOn(console, 'log').mockImplementation(() => {}); + const config = `{ + // my output + "outDir": "tokens", + "themes": {}, +}`; + + expect(parseJsonc(migration.yes(config))).toEqual({ + themes: {}, + output: [ + { type: 'design-tokens', dir: 'tokens' }, + { type: 'css', tokenDir: 'tokens' }, + ], + }); + expect(parseJsonc(migration.yes('{ /* default */ "outDir": "design-tokens", "themes": {} }'))).toEqual({ + themes: {}, + }); + vi.restoreAllMocks(); + }); + it('leaves the config unchanged when declined', () => { const config = '{ "outDir": "tokens" }'; diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 718e539808..05498663af 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -115,7 +115,7 @@ const migration: Automigrate = { yes: (config: string): string => { const migratedConfig = migrateToOutputField(config); console.log(pc.green(`\nConfig file successfully migrated.`)); - if (typeof JSON.parse(migratedConfig).output === 'undefined') { + if (typeof parseJsonc(migratedConfig).output === 'undefined') { console.log( pc.green( `\nNo new output field was added because the deprecated fields matched outputs default values and does not need to be added explicitly.`, From 532833c15d660481a969b56c2938b4ecb7faa4a4 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 09:20:03 +0200 Subject: [PATCH 26/44] fix potential wrong outDir in migration --- packages/cli/src/automigrate.ts | 2 +- .../migrations/flatten-color-categories.ts | 2 +- .../src/migrations/new-output-field.test.ts | 35 +++++++++++++++++++ .../cli/src/migrations/new-output-field.ts | 34 +++++++++++++++--- 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/automigrate.ts b/packages/cli/src/automigrate.ts index 4912c356fb..4241561686 100644 --- a/packages/cli/src/automigrate.ts +++ b/packages/cli/src/automigrate.ts @@ -35,7 +35,7 @@ export const checkAutomigrate = async (configFile: string, configFilePath: strin if (!answer) { migratedConfigFile = migration.no(migratedConfigFile ?? configFile); } else { - migratedConfigFile = migration.yes(migratedConfigFile ?? configFile); + migratedConfigFile = migration.yes(migratedConfigFile ?? configFile, { configFilePath }); await dsfs.writeFile(configFilePath, migratedConfigFile); } } diff --git a/packages/cli/src/migrations/flatten-color-categories.ts b/packages/cli/src/migrations/flatten-color-categories.ts index 5c7c4a890c..239e3ee533 100644 --- a/packages/cli/src/migrations/flatten-color-categories.ts +++ b/packages/cli/src/migrations/flatten-color-categories.ts @@ -19,7 +19,7 @@ type Automigrate = { name: string; check: (config: any) => boolean; message: string; - yes: (config: string) => string; + yes: (config: string, context?: { configFilePath?: string }) => string; no: (config: string) => string; }; diff --git a/packages/cli/src/migrations/new-output-field.test.ts b/packages/cli/src/migrations/new-output-field.test.ts index 067ab65718..f4663aff24 100644 --- a/packages/cli/src/migrations/new-output-field.test.ts +++ b/packages/cli/src/migrations/new-output-field.test.ts @@ -68,6 +68,41 @@ describe('new output field migration', () => { vi.restoreAllMocks(); }); + describe('when the config file is not in the directory the CLI was run from', () => { + const context = { configFilePath: 'configs/designsystemet.config.json', cwd: '/project' }; + + it('rewrites outDir relative to the config file', () => { + expect(parseJsonc(migrateToOutputField('{ "outDir": "tokens" }', context))).toEqual({ + output: [ + { type: 'design-tokens', dir: '../tokens' }, + { type: 'css', tokenDir: '../tokens' }, + ], + }); + }); + + it('keeps the default outDir pointing to the same directory', () => { + expect(parseJsonc(migrateToOutputField('{ "clean": true }', context))).toEqual({ + output: [ + { type: 'design-tokens', dir: '../design-tokens' }, + { type: 'css', tokenDir: '../design-tokens' }, + ], + }); + }); + + it('rewrites an absolute outDir relative to the config file', () => { + expect(parseJsonc(migrateToOutputField('{ "outDir": "/project/configs/tokens" }', context))).toEqual({ + output: [ + { type: 'design-tokens', dir: 'tokens' }, + { type: 'css', tokenDir: 'tokens' }, + ], + }); + }); + + it('does not define output when outDir resolves to the default directory next to the config file', () => { + expect(parseJsonc(migrateToOutputField('{ "outDir": "configs/design-tokens" }', context))).toEqual({}); + }); + }); + it('leaves the config unchanged when declined', () => { const config = '{ "outDir": "tokens" }'; diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 05498663af..742a895cc5 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -9,11 +9,18 @@ const formattingOptions = { insertSpaces: true, tabSize: 2 } as const; const deprecatedFields = ['outDir', 'clean'] as const; +type MigrationContext = { + /** Path of the config file being migrated. */ + configFilePath?: string; + /** Directory the CLI was run from. Defaults to `process.cwd()`. */ + cwd?: string; +}; + type Automigrate = { name: string; check: (config: string) => boolean; message: string; - yes: (config: string) => string; + yes: (config: string, context?: MigrationContext) => string; no: (config: string) => string; }; @@ -79,7 +86,23 @@ export const toOutput = (outDir: string | undefined) => { ] as const; }; -export const migrateToOutputField = (config: string): string => { +/** + * `outDir` was resolved from the directory the CLI was run from, while `output` paths are resolved from the + * config file's directory. Rewrites `outDir` so it points to the same directory when resolved from the config file. + * Paths always use forward slashes, so the config works on any OS. + */ +const toConfigRelative = (outDir: string, { configFilePath, cwd = process.cwd() }: MigrationContext): string => { + if (!configFilePath) { + return outDir; + } + + const configDir = path.dirname(path.resolve(cwd, configFilePath)); + const relative = path.relative(configDir, path.resolve(cwd, outDir)) || '.'; + + return relative.split(path.sep).join('/'); +}; + +export const migrateToOutputField = (config: string, context: MigrationContext = {}): string => { const currentConfig = parseJsonc(config); // Apply targeted edits to the original text instead of re-serializing the whole @@ -88,7 +111,8 @@ export const migrateToOutputField = (config: string): string => { // If `output` is already set, the deprecated fields are ignored and can simply be removed. if (!currentConfig.output) { - const output = toOutput(currentConfig.outDir); + // A missing `outDir` meant the default directory, relative to where the CLI was run from. + const output = toOutput(toConfigRelative(currentConfig.outDir ?? defaultOutDir, context)); if (output) { configText = applyEdits( configText, @@ -112,8 +136,8 @@ const migration: Automigrate = { name: 'New output field', check: hasDeprecatedFields, message: `Your config file uses the deprecated ${pc.yellow('outDir')} and ${pc.yellow('clean')} fields. \nThis migration will replace them with a new ${pc.blue('output')} field if necessary.\n`, - yes: (config: string): string => { - const migratedConfig = migrateToOutputField(config); + yes: (config: string, context?: MigrationContext): string => { + const migratedConfig = migrateToOutputField(config, context); console.log(pc.green(`\nConfig file successfully migrated.`)); if (typeof parseJsonc(migratedConfig).output === 'undefined') { console.log( From 634af36b80146009960c6a1409f906e5e8b65798 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 09:35:44 +0200 Subject: [PATCH 27/44] make tokenDir optional so it works better in isolation. simplified check --- .../fundamentals/en/code/cli-config.mdx | 2 +- .../fundamentals/no/code/cli-config.mdx | 2 +- packages/cli/README.md | 4 +-- packages/cli/bin/designsystemet.ts | 28 +++++-------------- .../docs/designsystemet.config.defaults.json | 1 - .../schemas/__snapshots__/config.schema.json | 3 +- packages/cli/src/schemas/schema-output.ts | 7 ++++- 7 files changed, 18 insertions(+), 29 deletions(-) diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index bb98582335..e261b11ed7 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -105,7 +105,7 @@ All paths are relative to the config file. | type | `"css"` | Yes | | The output type. | | dir | String | No | `design-tokens-build` | The folder where CSS should be saved. | | cleanDir | Boolean | No | `true` | Delete the folder before creating CSS. | -| tokenDir | String | No | `design-tokens` | The folder containing the design tokens to build CSS from. Should match `dir` of the `design-tokens` output. | +| tokenDir | String | No | `dir` of the `design-tokens` output | The folder containing the design tokens to build CSS from. If neither is set, CSS is created directly from the themes. | #### Without design tokens diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index 30fafeb4b8..107936cbad 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -107,7 +107,7 @@ Alle stiar er relative til config fila. | type | `"css"` | Ja | | Output-typen. | | dir | Streng | Nei | `design-tokens-build` | Mappa der CSS skal lagrast. | | cleanDir | Boolean | Nei | `true` | Slett mappa fĆør CSS blir laga. | -| tokenDir | Streng | Nei | `design-tokens` | Mappa med design tokens som CSS skal byggast frĆ„. BĆør vere lik `dir` i `design-tokens`-outputen. | +| tokenDir | Streng | Nei | `dir` i `design-tokens`-outputen | Mappa med design tokens som CSS skal byggast frĆ„. Er ingen av dei sette, blir CSS-en laga direkte frĆ„ tema. | #### Utan design tokens diff --git a/packages/cli/README.md b/packages/cli/README.md index c02734ac27..f6d9169562 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -94,13 +94,13 @@ Use `output` to choose what is created and where. Each item is either an output "output": [ // defaults: dir "design-tokens", cleanDir true { "type": "design-tokens", "dir": "../path/to/design-tokens" }, - // defaults: dir "design-tokens-build", tokenDir "design-tokens", cleanDir true + // defaults: dir "design-tokens-build", tokenDir from the design-tokens output, cleanDir true { "type": "css", "dir": "../path/to/css", "tokenDir": "../path/to/design-tokens" }, ], } ``` -Design tokens are always created before CSS. `tokenDir` should match the `dir` of the `design-tokens` output. +Design tokens are always created before CSS. If `tokenDir` is not set, CSS is built from the `dir` of the `design-tokens` output. If you only need CSS, use `"output": ["css"]` without `tokenDir`. The CSS is then created directly from the themes, without writing any design tokens. diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index b0c9f88ac6..7fb888a1a0 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -77,6 +77,7 @@ program // Sort outputs so that design-tokens are generated before CSS, since CSS may depend on the design tokens being present. const sortedOutput = R.sortBy((o) => (o.type === 'design-tokens' ? 0 : 1), config.output); + const designTokensOutput = config.output.find((o) => o.type === 'design-tokens'); for (const output of sortedOutput) { const outDir = path.join(dsfs.outDir, output.dir); @@ -94,8 +95,11 @@ program if (output.type === 'css') { console.log(`\nšŸ± Creating CSS in ${pc.green(output.dir)}...`); - // Only generate create CSS if no `design-tokens` output is present and no `tokenDir` is explicitly set in the config file. Otherwise, build CSS from existing design tokens. - if (isOnlyCssOutput(parsedConfig)) { + // Build CSS from `tokenDir`, or else from the design tokens created by the `design-tokens` output. + // With neither, there are no design tokens to build from, so CSS is created directly from the themes. + const tokenDir = output.tokenDir ?? designTokensOutput?.dir; + + if (tokenDir === undefined) { await createCss({ themes: requireThemes(config), outDir: outDir, @@ -107,7 +111,7 @@ program await buildCss({ // Resolve the token directory relative to the config file, like output.dir, // so it matches where a preceding design-tokens output wrote its files. - tokensDir: path.join(dsfs.outDir, output.tokenDir), + tokensDir: path.join(dsfs.outDir, tokenDir), outDir, clean: output.cleanDir, verbose, @@ -313,21 +317,3 @@ async function createCss({ console.log(`\nāœ… Finished creating CSS`); } - -/** Checks the config file as written, since validation adds defaults such as `tokenDir`. */ -function isOnlyCssOutput(config: ExternalConfigSchemaInput): boolean { - // No `output` means the default outputs, which include design tokens. - if (!config.output) { - return false; - } - - // Outputs can be defined using either the shorthand or object syntax, so check for both. - const hasDesignTokensOutput = config.output.some( - (o) => o === 'design-tokens' || (typeof o === 'object' && o.type === 'design-tokens'), - ); - const hasCSSTokensDir = config.output.some( - (o) => typeof o === 'object' && o.type === 'css' && o.tokenDir !== undefined, - ); - - return !hasDesignTokensOutput && !hasCSSTokensDir; -} diff --git a/packages/cli/docs/designsystemet.config.defaults.json b/packages/cli/docs/designsystemet.config.defaults.json index 0d67b3bf8e..836245972a 100644 --- a/packages/cli/docs/designsystemet.config.defaults.json +++ b/packages/cli/docs/designsystemet.config.defaults.json @@ -9,7 +9,6 @@ "type": "css", "dir": "design-tokens-build", "cleanDir": true, - "tokenDir": "design-tokens", "experimental_tailwind": true } ], diff --git a/packages/cli/src/schemas/__snapshots__/config.schema.json b/packages/cli/src/schemas/__snapshots__/config.schema.json index c1c386bf5c..d4c11acb6c 100644 --- a/packages/cli/src/schemas/__snapshots__/config.schema.json +++ b/packages/cli/src/schemas/__snapshots__/config.schema.json @@ -55,8 +55,7 @@ "type": "boolean" }, "tokenDir": { - "default": "design-tokens", - "description": "The directory containing the design tokens", + "description": "The directory containing the design tokens to build CSS from. Defaults to `dir` of the `design-tokens` output. If neither is set, CSS is created directly from the themes", "type": "string" }, "experimental_tailwind": { diff --git a/packages/cli/src/schemas/schema-output.ts b/packages/cli/src/schemas/schema-output.ts index 6129ca3d1d..b776868a88 100644 --- a/packages/cli/src/schemas/schema-output.ts +++ b/packages/cli/src/schemas/schema-output.ts @@ -11,7 +11,12 @@ const cssOutputSchema = z.object({ type: z.literal('css').describe('The type of output file'), dir: z.string().default('design-tokens-build').describe('The output directory'), cleanDir: z.boolean().default(true).describe('Whether to clean the output directory before generating files'), - tokenDir: z.string().default('design-tokens').describe('The directory containing the design tokens'), + tokenDir: z + .string() + .optional() + .describe( + 'The directory containing the design tokens to build CSS from. Defaults to `dir` of the `design-tokens` output. If neither is set, CSS is created directly from the themes', + ), experimental_tailwind: z.boolean().default(true).describe('Whether to enable experimental Tailwind support'), }); From c15ad3aeb1554ee4403e506b6d2e0db754428290 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 09:47:05 +0200 Subject: [PATCH 28/44] do clean once instead of each output step --- packages/cli/bin/designsystemet.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 7fb888a1a0..63b39ab066 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -79,6 +79,15 @@ program const sortedOutput = R.sortBy((o) => (o.type === 'design-tokens' ? 0 : 1), config.output); const designTokensOutput = config.output.find((o) => o.type === 'design-tokens'); + // Clean every output directory once, before any output is created. Cleaning as part of each output would + // delete what earlier outputs wrote when they share a directory, which makes the outputs depend on their order. + const dirsToClean = R.uniq( + config.output.filter((o) => 'cleanDir' in o && o.cleanDir).map((o) => path.join(dsfs.outDir, o.dir)), + ); + for (const dir of dirsToClean) { + await dsfs.cleanDir(dir); + } + for (const output of sortedOutput) { const outDir = path.join(dsfs.outDir, output.dir); @@ -88,7 +97,6 @@ program await createDesignTokens({ themes: requireThemes(config), outDir: outDir, - clean: output.cleanDir, }); } @@ -103,7 +111,6 @@ program await createCss({ themes: requireThemes(config), outDir: outDir, - clean: output.cleanDir, verbose, tailwind: output.experimental_tailwind, }); @@ -113,7 +120,6 @@ program // so it matches where a preceding design-tokens output wrote its files. tokensDir: path.join(dsfs.outDir, tokenDir), outDir, - clean: output.cleanDir, verbose, tailwind: output.experimental_tailwind, }); @@ -280,20 +286,14 @@ async function buildCss({ async function createCss({ themes, outDir, - clean, verbose, tailwind, }: { themes: ConfigSchemaThemes; outDir: string; - clean?: boolean; verbose: boolean; tailwind: boolean; }) { - if (clean) { - await dsfs.cleanDir(outDir); - } - const themeNames = Object.keys(themes); if (themeNames.length > 0) { console.log(`Using themes from config file: ${pc.blue(themeNames.join(', '))}`); @@ -306,10 +306,6 @@ async function createCss({ files.push(...themeCSSFiles); } - if (clean) { - await dsfs.cleanDir(outDir); - } - console.log(`\nšŸ’¾ Writing CSS to ${pc.green(outDir)}`); await dsfs.mkdir(outDir); From ff6294f492e9ab109dfa761c77ff55c3d7576bbc Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 10:36:57 +0200 Subject: [PATCH 29/44] rename tokenDir to tokensDir --- .../content/fundamentals/en/code/cli-config.mdx | 16 ++++++++-------- .../fundamentals/en/theme/multiple-themes.mdx | 4 ++-- .../content/fundamentals/no/code/cli-config.mdx | 16 ++++++++-------- .../fundamentals/no/theme/multiple-themes.mdx | 4 ++-- packages/cli/README.md | 12 ++++++------ packages/cli/bin/config.ts | 2 +- packages/cli/bin/designsystemet.ts | 8 ++++---- .../cli/src/migrations/new-output-field.test.ts | 10 +++++----- packages/cli/src/migrations/new-output-field.ts | 2 +- .../src/schemas/__snapshots__/config.schema.json | 2 +- packages/cli/src/schemas/schema-output.ts | 2 +- 11 files changed, 39 insertions(+), 39 deletions(-) diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index e261b11ed7..27c86a6d7b 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -77,7 +77,7 @@ The `tokens create` and `tokens build` commands are deprecated and will be remov ### Output `output` is a list of what the CLI should create. Each item is either the name of an output type, which uses its default settings, or an object with custom settings. -Outputs are always created in the order `design-tokens`, `css`, whatever order they are listed in. When CSS is built from design tokens (with `tokenDir`), the design tokens must be created first. +Outputs are always created in the order `design-tokens`, `css`, whatever order they are listed in. When CSS is built from design tokens (with `tokensDir`), the design tokens must be created first. All paths are relative to the config file. @@ -85,7 +85,7 @@ All paths are relative to the config file. { "output": [ { "type": "design-tokens", "dir": "./design-tokens" }, - { "type": "css", "dir": "./css", "tokenDir": "./design-tokens" } + { "type": "css", "dir": "./css", "tokensDir": "./design-tokens" } ] } ``` @@ -105,20 +105,20 @@ All paths are relative to the config file. | type | `"css"` | Yes | | The output type. | | dir | String | No | `design-tokens-build` | The folder where CSS should be saved. | | cleanDir | Boolean | No | `true` | Delete the folder before creating CSS. | -| tokenDir | String | No | `dir` of the `design-tokens` output | The folder containing the design tokens to build CSS from. If neither is set, CSS is created directly from the themes. | +| tokensDir | String | No | `dir` of the `design-tokens` output | The folder containing the design tokens to build CSS from. If neither is set, CSS is created directly from the themes. | #### Without design tokens -If you only need CSS, you can use for example `"output": ["css"]` without `tokenDir`. It is then created directly from the themes, without saving any design tokens. +If you only need CSS, you can use for example `"output": ["css"]` without `tokensDir`. It is then created directly from the themes, without saving any design tokens. #### Building without themes -`themes` is only needed by outputs that are created from themes. If you already have design tokens, you can leave out `themes` and build from them by setting `tokenDir`: +`themes` is only needed by outputs that are created from themes. If you already have design tokens, you can leave out `themes` and build from them by setting `tokensDir`: ```json { "output": [ - { "type": "css", "tokenDir": "./design-tokens" } + { "type": "css", "tokensDir": "./design-tokens" } ] } ``` @@ -134,7 +134,7 @@ If you'd rather do it manually, replace `"outDir": ""` with: { "output": [ { "type": "design-tokens", "dir": "" }, - { "type": "css", "tokenDir": "" } + { "type": "css", "tokensDir": "" } ] } ``` @@ -263,7 +263,7 @@ The `severity` override allows you to customise the colours used for severity, w "$schema": "node_modules/@digdir/designsystemet/dist/config.schema.json", "output": [ { "type": "design-tokens", "dir": "./design-tokens" }, - { "type": "css", "dir": "./design-tokens-build", "tokenDir": "./design-tokens" } + { "type": "css", "dir": "./design-tokens-build", "tokensDir": "./design-tokens" } ], "themes": { "my-theme": { diff --git a/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx b/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx index cfe6637774..9e34806260 100644 --- a/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx +++ b/apps/www/app/content/fundamentals/en/theme/multiple-themes.mdx @@ -59,7 +59,7 @@ It is common to have to do it this way if you have a repository that collects al { "output": [ { "type": "design-tokens", "dir": "./some-org-dt" }, - { "type": "css", "dir": "./some-org-css", "tokenDir": "./some-org-dt" } + { "type": "css", "dir": "./some-org-css", "tokensDir": "./some-org-dt" } ], "themes": { "some-org": { @@ -78,7 +78,7 @@ It is common to have to do it this way if you have a repository that collects al { "output": [ { "type": "design-tokens", "dir": "./other-org-dt" }, - { "type": "css", "dir": "./other-org-css", "tokenDir": "./other-org-dt" } + { "type": "css", "dir": "./other-org-css", "tokensDir": "./other-org-dt" } ], "themes": { "other-org": { diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index 107936cbad..b8b8f08a6e 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -79,7 +79,7 @@ Kommandoane `tokens create` og `tokens build` er utdaterte og blir fjerna i ein `output` er ei liste over kva CLI-et skal lage. Kvart element er anten namnet pĆ„ ein output-type, som brukar standardinnstillingane, eller eit objekt med eigne innstillingar. -Outputane blir alltid laga i rekkjefĆølgja `design-tokens`, `css`, same kva rekkjefĆølgje dei stĆ„r i. NĆ„r CSS-en blir bygd frĆ„ design tokens (med `tokenDir`), mĆ„ design tokens vere laga fĆørst. +Outputane blir alltid laga i rekkjefĆølgja `design-tokens`, `css`, same kva rekkjefĆølgje dei stĆ„r i. NĆ„r CSS-en blir bygd frĆ„ design tokens (med `tokensDir`), mĆ„ design tokens vere laga fĆørst. Alle stiar er relative til config fila. @@ -87,7 +87,7 @@ Alle stiar er relative til config fila. { "output": [ { "type": "design-tokens", "dir": "./design-tokens" }, - { "type": "css", "dir": "./css", "tokenDir": "./design-tokens" } + { "type": "css", "dir": "./css", "tokensDir": "./design-tokens" } ] } ``` @@ -107,20 +107,20 @@ Alle stiar er relative til config fila. | type | `"css"` | Ja | | Output-typen. | | dir | Streng | Nei | `design-tokens-build` | Mappa der CSS skal lagrast. | | cleanDir | Boolean | Nei | `true` | Slett mappa fĆør CSS blir laga. | -| tokenDir | Streng | Nei | `dir` i `design-tokens`-outputen | Mappa med design tokens som CSS skal byggast frĆ„. Er ingen av dei sette, blir CSS-en laga direkte frĆ„ tema. | +| tokensDir | Streng | Nei | `dir` i `design-tokens`-outputen | Mappa med design tokens som CSS skal byggast frĆ„. Er ingen av dei sette, blir CSS-en laga direkte frĆ„ tema. | #### Utan design tokens -Treng du berre CSS, kan du bruke til dĆømes `"output": ["css"]` utan `tokenDir`. CSS-en blir dĆ„ laga direkte frĆ„ tema, utan at design tokens blir lagra. +Treng du berre CSS, kan du bruke til dĆømes `"output": ["css"]` utan `tokensDir`. CSS-en blir dĆ„ laga direkte frĆ„ tema, utan at design tokens blir lagra. #### Bygge utan tema -`themes` trengst berre for outputar som blir laga frĆ„ tema. Har du alt design tokens, kan du slĆøyfe `themes` og byggje frĆ„ dei ved Ć„ setje `tokenDir`: +`themes` trengst berre for outputar som blir laga frĆ„ tema. Har du alt design tokens, kan du slĆøyfe `themes` og byggje frĆ„ dei ved Ć„ setje `tokensDir`: ```json { "output": [ - { "type": "css", "tokenDir": "./design-tokens" } + { "type": "css", "tokensDir": "./design-tokens" } ] } ``` @@ -136,7 +136,7 @@ Vil du heller gjere det manuelt, byt ut `"outDir": ""` med: { "output": [ { "type": "design-tokens", "dir": "" }, - { "type": "css", "tokenDir": "" } + { "type": "css", "tokensDir": "" } ] } ``` @@ -255,7 +255,7 @@ Overstyringen av `severity` lar deg tilpasse fargane som blir brukt for severity "$schema": "node_modules/@digdir/designsystemet/dist/config.schema.json", "output": [ { "type": "design-tokens", "dir": "./design-tokens" }, - { "type": "css", "dir": "./design-tokens-build", "tokenDir": "./design-tokens" } + { "type": "css", "dir": "./design-tokens-build", "tokensDir": "./design-tokens" } ], "themes": { "my-theme": { diff --git a/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx b/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx index 74d7ad34e2..806c0e3038 100644 --- a/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx +++ b/apps/www/app/content/fundamentals/no/theme/multiple-themes.mdx @@ -60,7 +60,7 @@ Det er vanleg Ć„ mĆ„tte gjere det pĆ„ denne mĆ„ten dersom du har eit repository { "output": [ { "type": "design-tokens", "dir": "./some-org-dt" }, - { "type": "css", "dir": "./some-org-css", "tokenDir": "./some-org-dt" } + { "type": "css", "dir": "./some-org-css", "tokensDir": "./some-org-dt" } ], "themes": { "some-org": { @@ -79,7 +79,7 @@ Det er vanleg Ć„ mĆ„tte gjere det pĆ„ denne mĆ„ten dersom du har eit repository { "output": [ { "type": "design-tokens", "dir": "./other-org-dt" }, - { "type": "css", "dir": "./other-org-css", "tokenDir": "./other-org-dt" } + { "type": "css", "dir": "./other-org-css", "tokensDir": "./other-org-dt" } ], "themes": { "other-org": { diff --git a/packages/cli/README.md b/packages/cli/README.md index f6d9169562..a5364a7bc4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -94,22 +94,22 @@ Use `output` to choose what is created and where. Each item is either an output "output": [ // defaults: dir "design-tokens", cleanDir true { "type": "design-tokens", "dir": "../path/to/design-tokens" }, - // defaults: dir "design-tokens-build", tokenDir from the design-tokens output, cleanDir true - { "type": "css", "dir": "../path/to/css", "tokenDir": "../path/to/design-tokens" }, + // defaults: dir "design-tokens-build", tokensDir from the design-tokens output, cleanDir true + { "type": "css", "dir": "../path/to/css", "tokensDir": "../path/to/design-tokens" }, ], } ``` -Design tokens are always created before CSS. If `tokenDir` is not set, CSS is built from the `dir` of the `design-tokens` output. +Design tokens are always created before CSS. If `tokensDir` is not set, CSS is built from the `dir` of the `design-tokens` output. -If you only need CSS, use `"output": ["css"]` without `tokenDir`. The CSS is then created directly from the themes, without writing any design tokens. +If you only need CSS, use `"output": ["css"]` without `tokensDir`. The CSS is then created directly from the themes, without writing any design tokens. -`themes` is only needed by outputs that are created from themes. To build CSS from existing design tokens, leave out `themes` and set `tokenDir`: +`themes` is only needed by outputs that are created from themes. To build CSS from existing design tokens, leave out `themes` and set `tokensDir`: ```jsonc { "output": [ - { "type": "css", "tokenDir": "./design-tokens" }, + { "type": "css", "tokensDir": "./design-tokens" }, ], } ``` diff --git a/packages/cli/bin/config.ts b/packages/cli/bin/config.ts index 36f6413d28..ee36d44ea2 100644 --- a/packages/cli/bin/config.ts +++ b/packages/cli/bin/config.ts @@ -20,7 +20,7 @@ export function requireThemes(config: Pick): ConfigSchem if (!config.themes) { console.error( pc.redBright( - `No themes found in config file. Add ${pc.blue('themes')}, or set ${pc.blue('tokenDir')} on the ${pc.blue('css')} output to build CSS from existing design tokens.`, + `No themes found in config file. Add ${pc.blue('themes')}, or set ${pc.blue('tokensDir')} on the ${pc.blue('css')} output to build CSS from existing design tokens.`, ), ); process.exit(1); diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 63b39ab066..a691fee90c 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -103,11 +103,11 @@ program if (output.type === 'css') { console.log(`\nšŸ± Creating CSS in ${pc.green(output.dir)}...`); - // Build CSS from `tokenDir`, or else from the design tokens created by the `design-tokens` output. + // Build CSS from `tokensDir`, or else from the design tokens created by the `design-tokens` output. // With neither, there are no design tokens to build from, so CSS is created directly from the themes. - const tokenDir = output.tokenDir ?? designTokensOutput?.dir; + const tokensDir = output.tokensDir ?? designTokensOutput?.dir; - if (tokenDir === undefined) { + if (tokensDir === undefined) { await createCss({ themes: requireThemes(config), outDir: outDir, @@ -118,7 +118,7 @@ program await buildCss({ // Resolve the token directory relative to the config file, like output.dir, // so it matches where a preceding design-tokens output wrote its files. - tokensDir: path.join(dsfs.outDir, tokenDir), + tokensDir: path.join(dsfs.outDir, tokensDir), outDir, verbose, tailwind: output.experimental_tailwind, diff --git a/packages/cli/src/migrations/new-output-field.test.ts b/packages/cli/src/migrations/new-output-field.test.ts index f4663aff24..0ae6c6ac80 100644 --- a/packages/cli/src/migrations/new-output-field.test.ts +++ b/packages/cli/src/migrations/new-output-field.test.ts @@ -23,7 +23,7 @@ describe('new output field migration', () => { themes: {}, output: [ { type: 'design-tokens', dir: 'tokens' }, - { type: 'css', tokenDir: 'tokens' }, + { type: 'css', tokensDir: 'tokens' }, ], }); }); @@ -59,7 +59,7 @@ describe('new output field migration', () => { themes: {}, output: [ { type: 'design-tokens', dir: 'tokens' }, - { type: 'css', tokenDir: 'tokens' }, + { type: 'css', tokensDir: 'tokens' }, ], }); expect(parseJsonc(migration.yes('{ /* default */ "outDir": "design-tokens", "themes": {} }'))).toEqual({ @@ -75,7 +75,7 @@ describe('new output field migration', () => { expect(parseJsonc(migrateToOutputField('{ "outDir": "tokens" }', context))).toEqual({ output: [ { type: 'design-tokens', dir: '../tokens' }, - { type: 'css', tokenDir: '../tokens' }, + { type: 'css', tokensDir: '../tokens' }, ], }); }); @@ -84,7 +84,7 @@ describe('new output field migration', () => { expect(parseJsonc(migrateToOutputField('{ "clean": true }', context))).toEqual({ output: [ { type: 'design-tokens', dir: '../design-tokens' }, - { type: 'css', tokenDir: '../design-tokens' }, + { type: 'css', tokensDir: '../design-tokens' }, ], }); }); @@ -93,7 +93,7 @@ describe('new output field migration', () => { expect(parseJsonc(migrateToOutputField('{ "outDir": "/project/configs/tokens" }', context))).toEqual({ output: [ { type: 'design-tokens', dir: 'tokens' }, - { type: 'css', tokenDir: 'tokens' }, + { type: 'css', tokensDir: 'tokens' }, ], }); }); diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 742a895cc5..99bd847818 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -82,7 +82,7 @@ export const toOutput = (outDir: string | undefined) => { return [ { type: 'design-tokens', dir: outDir }, // CSS is built from the design tokens, so it must read them from the same directory. - { type: 'css', tokenDir: outDir }, + { type: 'css', tokensDir: outDir }, ] as const; }; diff --git a/packages/cli/src/schemas/__snapshots__/config.schema.json b/packages/cli/src/schemas/__snapshots__/config.schema.json index d4c11acb6c..895c0ac100 100644 --- a/packages/cli/src/schemas/__snapshots__/config.schema.json +++ b/packages/cli/src/schemas/__snapshots__/config.schema.json @@ -54,7 +54,7 @@ "description": "Whether to clean the output directory before generating files", "type": "boolean" }, - "tokenDir": { + "tokensDir": { "description": "The directory containing the design tokens to build CSS from. Defaults to `dir` of the `design-tokens` output. If neither is set, CSS is created directly from the themes", "type": "string" }, diff --git a/packages/cli/src/schemas/schema-output.ts b/packages/cli/src/schemas/schema-output.ts index b776868a88..24e1025524 100644 --- a/packages/cli/src/schemas/schema-output.ts +++ b/packages/cli/src/schemas/schema-output.ts @@ -11,7 +11,7 @@ const cssOutputSchema = z.object({ type: z.literal('css').describe('The type of output file'), dir: z.string().default('design-tokens-build').describe('The output directory'), cleanDir: z.boolean().default(true).describe('Whether to clean the output directory before generating files'), - tokenDir: z + tokensDir: z .string() .optional() .describe( From 35675b41e0b39d1f5d6c71e55f2cb9d7bfa257be Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 10:53:47 +0200 Subject: [PATCH 30/44] reject config from tokens if config file is placed inside design-tokens output --- .../cli/src/tokens/generate-config.test.ts | 22 +++++++++++++ packages/cli/src/tokens/generate-config.ts | 32 ++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/tokens/generate-config.test.ts diff --git a/packages/cli/src/tokens/generate-config.test.ts b/packages/cli/src/tokens/generate-config.test.ts new file mode 100644 index 0000000000..d3c01ca0a7 --- /dev/null +++ b/packages/cli/src/tokens/generate-config.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { toConfigTokensDir } from './generate-config.ts'; + +describe('toConfigTokensDir', () => { + it('returns the tokens directory relative to the config directory', () => { + expect(toConfigTokensDir('/project/design-tokens', '/project')).toBe('design-tokens'); + expect(toConfigTokensDir('/project/tokens', '/project/configs')).toBe('../tokens'); + expect(toConfigTokensDir('/project/src/design-tokens', '/project')).toBe('src/design-tokens'); + }); + + it('allows a sibling directory whose name starts with the tokens directory name', () => { + expect(toConfigTokensDir('/project/tokens', '/project/tokens-config')).toBe('../tokens'); + }); + + // The generated `design-tokens` output would point to `.` or `..`, which `cleanDir` would delete. + it.each([ + ['the tokens directory', '/project/design-tokens'], + ['a subdirectory of the tokens directory', '/project/design-tokens/configs'], + ])('rejects a config file in %s', (_, configDir) => { + expect(() => toConfigTokensDir('/project/design-tokens', configDir)).toThrow(/inside the design tokens directory/); + }); +}); diff --git a/packages/cli/src/tokens/generate-config.ts b/packages/cli/src/tokens/generate-config.ts index c4b2a5345e..1cffbdd9aa 100644 --- a/packages/cli/src/tokens/generate-config.ts +++ b/packages/cli/src/tokens/generate-config.ts @@ -194,12 +194,40 @@ type GenerateConfigOptions = { outFile?: string; }; +/** + * Returns `tokensDir` relative to `configDir`, as paths are written in a config: relative to the config file, + * with forward slashes so the config works on any OS. + * + * Throws when `configDir` is the tokens directory or inside it. The generated `design-tokens` output would then + * point to the config's own directory or a parent of it, which `cleanDir` deletes before creating design tokens. + */ +export const toConfigTokensDir = (tokensDir: string, configDir: string): string => { + const absoluteTokensDir = path.resolve(tokensDir); + const configFromTokensDir = path.relative(absoluteTokensDir, path.resolve(configDir)); + const configIsInsideTokensDir = + configFromTokensDir !== '..' && + !configFromTokensDir.startsWith(`..${path.sep}`) && + !path.isAbsolute(configFromTokensDir); + + if (configIsInsideTokensDir) { + throw new Error( + `The config file can't be placed inside the design tokens directory ${pc.blue(absoluteTokensDir)}, since running the config would delete that directory. Use ${pc.blue('--out')} to place the config file outside it.`, + ); + } + + return path.relative(path.resolve(configDir), absoluteTokensDir).split(path.sep).join('/'); +}; + /** * Generates a config file from existing design tokens */ export async function generateConfigFromTokens(options: GenerateConfigOptions): Promise { const { tokensDir, outFile } = options; + // Check the paths before reading any tokens, so an unsafe `--out` fails right away. + const configDir = outFile ? path.dirname(path.resolve(outFile)) : process.cwd(); + const relativeTokensDir = toConfigTokensDir(tokensDir, configDir); + console.log(`\nReading tokens from ${pc.blue(tokensDir)}`); // Discover themes @@ -212,10 +240,6 @@ export async function generateConfigFromTokens(options: GenerateConfigOptions): console.log(`\nFound ${pc.green(String(themes.length))} theme(s): ${themes.map((t) => pc.cyan(t)).join(', ')}`); // Generate config for each theme - // Paths in a config are relative to the config file, and always use forward slashes so the config works on any OS. - const configDir = outFile ? path.dirname(path.resolve(outFile)) : process.cwd(); - const relativeTokensDir = (path.relative(configDir, path.resolve(tokensDir)) || '.').split(path.sep).join('/'); - const configThemes: NonNullable = {}; const output = toOutput(relativeTokensDir); const config: ExternalConfigSchemaInput = { From 3b0d2f8bb9bff71bd2cfff0f886a3fa233bf2350 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 10:58:11 +0200 Subject: [PATCH 31/44] added guard if users try to output and clean same dir as config --- packages/cli/bin/designsystemet.ts | 14 +++++++++++++ packages/cli/src/tokens/generate-config.ts | 8 ++------ packages/cli/src/utils/paths.test.ts | 23 ++++++++++++++++++++++ packages/cli/src/utils/paths.ts | 8 ++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 packages/cli/src/utils/paths.test.ts create mode 100644 packages/cli/src/utils/paths.ts diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index a691fee90c..3cb33ad3c7 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -21,6 +21,7 @@ import { generateConfigFromTokens } from '../src/tokens/generate-config.ts'; import type { OutputFile, Theme } from '../src/tokens/types.ts'; import { toColorNames } from '../src/tokens/utils.ts'; import { dsfs } from '../src/utils/filesystem.ts'; +import { isSameOrInside } from '../src/utils/paths.ts'; import { DEFAULT_CONFIG_FILEPATH, getConfigFile, requireThemes } from './config.ts'; import { DEFAULT_TOKENS_CREATE_DIR, makeTokenCommands } from './deprecated.ts'; import { configOption, dryOption, verboseOption } from './options.ts'; @@ -84,6 +85,19 @@ program const dirsToClean = R.uniq( config.output.filter((o) => 'cleanDir' in o && o.cleanDir).map((o) => path.join(dsfs.outDir, o.dir)), ); + + // Check every directory before cleaning any, so nothing is deleted when one of them is unsafe. + const configDir = path.dirname(path.resolve(configFilePath)); + const unsafeDir = dirsToClean.find((dir) => isSameOrInside(configDir, dir)); + if (unsafeDir) { + console.error( + pc.redBright( + `Output directory ${pc.blue(path.relative(configDir, unsafeDir) || '.')} contains the config file, so cleaning it would delete the config file. Use another ${pc.blue('dir')}, or set ${pc.blue('cleanDir')} to ${pc.blue('false')} for that output.`, + ), + ); + process.exit(1); + } + for (const dir of dirsToClean) { await dsfs.cleanDir(dir); } diff --git a/packages/cli/src/tokens/generate-config.ts b/packages/cli/src/tokens/generate-config.ts index 1cffbdd9aa..7959eeacdb 100644 --- a/packages/cli/src/tokens/generate-config.ts +++ b/packages/cli/src/tokens/generate-config.ts @@ -4,6 +4,7 @@ import type { CssColor } from '../colors/types.ts'; import { toOutput } from '../migrations/new-output-field.ts'; import type { ExternalConfigSchemaInput } from '../schemas/schema.ts'; import { dsfs } from '../utils/filesystem.ts'; +import { isSameOrInside } from '../utils/paths.ts'; type TokenValue = { $type: string; @@ -203,13 +204,8 @@ type GenerateConfigOptions = { */ export const toConfigTokensDir = (tokensDir: string, configDir: string): string => { const absoluteTokensDir = path.resolve(tokensDir); - const configFromTokensDir = path.relative(absoluteTokensDir, path.resolve(configDir)); - const configIsInsideTokensDir = - configFromTokensDir !== '..' && - !configFromTokensDir.startsWith(`..${path.sep}`) && - !path.isAbsolute(configFromTokensDir); - if (configIsInsideTokensDir) { + if (isSameOrInside(configDir, absoluteTokensDir)) { throw new Error( `The config file can't be placed inside the design tokens directory ${pc.blue(absoluteTokensDir)}, since running the config would delete that directory. Use ${pc.blue('--out')} to place the config file outside it.`, ); diff --git a/packages/cli/src/utils/paths.test.ts b/packages/cli/src/utils/paths.test.ts new file mode 100644 index 0000000000..a6bb7613c6 --- /dev/null +++ b/packages/cli/src/utils/paths.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; +import { isSameOrInside } from './paths.ts'; + +describe('isSameOrInside', () => { + it('is true for the directory itself and anything inside it', () => { + expect(isSameOrInside('/project', '/project')).toBe(true); + expect(isSameOrInside('/project/configs', '/project')).toBe(true); + expect(isSameOrInside('/project/a/b', '/project')).toBe(true); + }); + + it('is false for parents and siblings', () => { + expect(isSameOrInside('/project', '/project/configs')).toBe(false); + expect(isSameOrInside('/project/other', '/project/configs')).toBe(false); + }); + + it('does not treat a sibling whose name starts with the directory name as inside it', () => { + expect(isSameOrInside('/project/tokens-config', '/project/tokens')).toBe(false); + }); + + it('resolves relative paths', () => { + expect(isSameOrInside('configs/..', '.')).toBe(true); + }); +}); diff --git a/packages/cli/src/utils/paths.ts b/packages/cli/src/utils/paths.ts new file mode 100644 index 0000000000..efad33e50b --- /dev/null +++ b/packages/cli/src/utils/paths.ts @@ -0,0 +1,8 @@ +import path from 'node:path'; + +/** Whether `dir` is `parent` itself or somewhere inside it. */ +export const isSameOrInside = (dir: string, parent: string): boolean => { + const fromParent = path.relative(path.resolve(parent), path.resolve(dir)); + + return fromParent !== '..' && !fromParent.startsWith(`..${path.sep}`) && !path.isAbsolute(fromParent); +}; From 361f7d3d3672b4da756111a3075908f76a6de3dd Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 11:04:46 +0200 Subject: [PATCH 32/44] add guard incase someone has same folder in dir and tokensDir --- packages/cli/bin/designsystemet.ts | 49 +++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 3cb33ad3c7..a2f8e124e7 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -87,14 +87,17 @@ program ); // Check every directory before cleaning any, so nothing is deleted when one of them is unsafe. - const configDir = path.dirname(path.resolve(configFilePath)); - const unsafeDir = dirsToClean.find((dir) => isSameOrInside(configDir, dir)); - if (unsafeDir) { - console.error( - pc.redBright( - `Output directory ${pc.blue(path.relative(configDir, unsafeDir) || '.')} contains the config file, so cleaning it would delete the config file. Use another ${pc.blue('dir')}, or set ${pc.blue('cleanDir')} to ${pc.blue('false')} for that output.`, - ), - ); + const unsafeCleanError = findUnsafeClean({ + dirsToClean, + configDir: path.dirname(path.resolve(configFilePath)), + // Existing design tokens that `css` outputs build from. Tokens in the `design-tokens` output's directory + // are created again in this run, so cleaning them is safe. + inputDirs: config.output + .flatMap((o) => (o.type === 'css' && o.tokensDir !== undefined ? [path.join(dsfs.outDir, o.tokensDir)] : [])) + .filter((dir) => !designTokensOutput || dir !== path.join(dsfs.outDir, designTokensOutput.dir)), + }); + if (unsafeCleanError) { + console.error(pc.redBright(unsafeCleanError)); process.exit(1); } @@ -327,3 +330,33 @@ async function createCss({ console.log(`\nāœ… Finished creating CSS`); } + +/** + * Returns an error message when cleaning `dirsToClean` would delete something the run needs: the config file, + * or existing design tokens in `inputDirs` that an output builds from. + */ +function findUnsafeClean({ + dirsToClean, + configDir, + inputDirs, +}: { + dirsToClean: string[]; + configDir: string; + inputDirs: string[]; +}): string | undefined { + const toConfigRelative = (dir: string) => pc.blue(path.relative(configDir, dir) || '.'); + const fix = `Use another ${pc.blue('dir')}, or set ${pc.blue('cleanDir')} to ${pc.blue('false')} for that output.`; + + for (const dir of dirsToClean) { + if (isSameOrInside(configDir, dir)) { + return `Output directory ${toConfigRelative(dir)} contains the config file, so cleaning it would delete the config file. ${fix}`; + } + + const inputDir = inputDirs.find((input) => isSameOrInside(input, dir)); + if (inputDir) { + return `Output directory ${toConfigRelative(dir)} contains the design tokens in ${toConfigRelative(inputDir)}, so cleaning it would delete them before they are used. ${fix}`; + } + } + + return undefined; +} From ce6713d118ea725115d93206d29573ca96e1db87 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 11:32:31 +0200 Subject: [PATCH 33/44] make sure clean:"false" is carried over in migration --- .../fundamentals/en/code/cli-config.mdx | 2 +- .../fundamentals/no/code/cli-config.mdx | 2 +- .../src/migrations/new-output-field.test.ts | 25 ++++++++++++++++- .../cli/src/migrations/new-output-field.ts | 28 ++++++++++++------- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/apps/www/app/content/fundamentals/en/code/cli-config.mdx b/apps/www/app/content/fundamentals/en/code/cli-config.mdx index 27c86a6d7b..5842183b7f 100644 --- a/apps/www/app/content/fundamentals/en/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/en/code/cli-config.mdx @@ -139,7 +139,7 @@ If you'd rather do it manually, replace `"outDir": ""` with: } ``` -`clean` can be removed, since `cleanDir` is `true` by default. +`clean` can be removed, since `cleanDir` is `true` by default. If you had `"clean": false`, add `"cleanDir": false` to each output instead, so the folders are not deleted. ### Themes diff --git a/apps/www/app/content/fundamentals/no/code/cli-config.mdx b/apps/www/app/content/fundamentals/no/code/cli-config.mdx index b8b8f08a6e..bd7de15b84 100644 --- a/apps/www/app/content/fundamentals/no/code/cli-config.mdx +++ b/apps/www/app/content/fundamentals/no/code/cli-config.mdx @@ -141,7 +141,7 @@ Vil du heller gjere det manuelt, byt ut `"outDir": ""` med: } ``` -`clean` kan fjernast, sidan `cleanDir` er `true` som standard. +`clean` kan fjernast, sidan `cleanDir` er `true` som standard. Hadde du `"clean": false`, legg du i staden til `"cleanDir": false` pĆ„ kvar output, slik at mappene ikkje blir sletta. ### Themes diff --git a/packages/cli/src/migrations/new-output-field.test.ts b/packages/cli/src/migrations/new-output-field.test.ts index 0ae6c6ac80..dfe5e8b357 100644 --- a/packages/cli/src/migrations/new-output-field.test.ts +++ b/packages/cli/src/migrations/new-output-field.test.ts @@ -37,10 +37,33 @@ describe('new output field migration', () => { }); it('does not define output when the deprecated fields have default values', () => { - expect(parseJsonc(migrateToOutputField('{ "outDir": "./design-tokens", "clean": false }'))).toEqual({}); + expect(parseJsonc(migrateToOutputField('{ "outDir": "./design-tokens" }'))).toEqual({}); + expect(parseJsonc(migrateToOutputField('{ "outDir": "./design-tokens", "clean": true }'))).toEqual({}); expect(parseJsonc(migrateToOutputField('{ "clean": true }'))).toEqual({}); }); + // `cleanDir` defaults to `true`, so dropping `clean: false` would delete directories the user opted out of cleaning. + it('carries an explicit clean: false over as cleanDir: false on every output', () => { + expect(parseJsonc(migrateToOutputField('{ "outDir": "tokens", "clean": false }'))).toEqual({ + output: [ + { type: 'design-tokens', dir: 'tokens', cleanDir: false }, + { type: 'css', tokensDir: 'tokens', cleanDir: false }, + ], + }); + }); + + it('defines output for clean: false even when outDir has its default value', () => { + const expected = { + output: [ + { type: 'design-tokens', cleanDir: false }, + { type: 'css', cleanDir: false }, + ], + }; + + expect(parseJsonc(migrateToOutputField('{ "clean": false }'))).toEqual(expected); + expect(parseJsonc(migrateToOutputField('{ "outDir": "./design-tokens", "clean": false }'))).toEqual(expected); + }); + it('only removes deprecated fields when output is already set', () => { const migrated = migrateToOutputField('{ "outDir": "tokens", "output": ["css"] }'); diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 99bd847818..f4ad39e0fb 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -68,22 +68,28 @@ const hasDeprecatedFields = (config: string): boolean => { const defaultOutDir = outputConfigShape.outDir.parse(undefined); /** - * Builds an `output` equivalent to the deprecated `outDir` field. - * Returns `undefined` when `outDir` has its default value, since the default `output` covers it. + * Builds an `output` equivalent to the deprecated `outDir` and `clean` fields. + * Returns `undefined` when both have default behaviour, since the default `output` covers it. * - * `clean` never needs to be carried over: its default is covered by the default `output`, - * and `clean: true` matches the default `cleanDir`. + * `cleanDir` defaults to `true`, so an explicit `clean: false` is carried over as `cleanDir: false` on every output. + * Otherwise the migrated config would delete output directories the user opted out of cleaning. + * A missing `clean` or `clean: true` uses the new default. */ -export const toOutput = (outDir: string | undefined) => { - if (outDir === undefined || path.posix.normalize(outDir) === path.posix.normalize(defaultOutDir)) { +export const toOutput = (outDir: string | undefined, { clean }: { clean?: boolean } = {}) => { + const isDefaultDir = outDir === undefined || path.posix.normalize(outDir) === path.posix.normalize(defaultOutDir); + const keepFiles = clean === false; + + if (isDefaultDir && !keepFiles) { return undefined; } + const cleanDir = keepFiles ? { cleanDir: false } : {}; + return [ - { type: 'design-tokens', dir: outDir }, + { type: 'design-tokens' as const, ...(!isDefaultDir && { dir: outDir }), ...cleanDir }, // CSS is built from the design tokens, so it must read them from the same directory. - { type: 'css', tokensDir: outDir }, - ] as const; + { type: 'css' as const, ...(!isDefaultDir && { tokensDir: outDir }), ...cleanDir }, + ]; }; /** @@ -112,7 +118,9 @@ export const migrateToOutputField = (config: string, context: MigrationContext = // If `output` is already set, the deprecated fields are ignored and can simply be removed. if (!currentConfig.output) { // A missing `outDir` meant the default directory, relative to where the CLI was run from. - const output = toOutput(toConfigRelative(currentConfig.outDir ?? defaultOutDir, context)); + const output = toOutput(toConfigRelative(currentConfig.outDir ?? defaultOutDir, context), { + clean: currentConfig.clean, + }); if (output) { configText = applyEdits( configText, From 734a9ec1287c6471a14a38ccb677f81ac2d51be6 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 11:53:49 +0200 Subject: [PATCH 34/44] add automigration tests and fix chained designsystem runs don't overwrite config if later migration says yes when first said no --- packages/cli/src/automigrate.test.ts | 65 ++++++++ packages/cli/src/automigrate.ts | 16 +- summary.md | 214 +++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 5 deletions(-) create mode 100644 packages/cli/src/automigrate.test.ts create mode 100644 summary.md diff --git a/packages/cli/src/automigrate.test.ts b/packages/cli/src/automigrate.test.ts new file mode 100644 index 0000000000..77491ae433 --- /dev/null +++ b/packages/cli/src/automigrate.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { checkAutomigrate } from './automigrate.ts'; +import { parseJsonc } from './schemas/helpers.ts'; +import { dsfs } from './utils/filesystem.ts'; + +const confirm = vi.hoisted(() => vi.fn<() => Promise>()); +vi.mock('@inquirer/confirm', () => ({ default: confirm })); + +type ParsedConfig = { outDir?: string; output?: unknown; themes: { theme: { colors: Record } } }; + +// Eligible for both automigrations: the old color categories and the deprecated `outDir`. +const config = JSON.stringify({ + outDir: 'tokens', + themes: { theme: { colors: { main: { accent: '#0062BA' }, support: {}, neutral: '#1E2B3C' } } }, +}); + +describe('checkAutomigrate', () => { + let written: string[]; + + beforeEach(() => { + vi.spyOn(console, 'log').mockImplementation(() => {}); + written = []; + vi.spyOn(dsfs, 'writeFile').mockImplementation(async (_path, content) => { + written.push(String(content)); + }); + confirm.mockReset(); + }); + + it('does not write a declined migration when a later migration is accepted', async () => { + // Decline flattening the color categories, accept the new output field. + confirm.mockResolvedValueOnce(false).mockResolvedValueOnce(true); + + const runtimeConfig = parseJsonc(await checkAutomigrate(config, 'designsystemet.config.json', false)); + const writtenConfig = parseJsonc(written.at(-1) ?? ''); + + // The file keeps the declined color categories, but gets the accepted `output`. + expect(writtenConfig.themes.theme.colors).toEqual({ main: { accent: '#0062BA' }, support: {}, neutral: '#1E2B3C' }); + expect(writtenConfig.output).toBeDefined(); + expect(writtenConfig.outDir).toBeUndefined(); + + // This run still uses the flattened colors, plus the accepted `output`. + expect(runtimeConfig.themes.theme.colors).toEqual({ accent: '#0062BA', neutral: '#1E2B3C' }); + expect(runtimeConfig.output).toEqual(writtenConfig.output); + }); + + it('writes every accepted migration', async () => { + confirm.mockResolvedValue(true); + + const runtimeConfig = await checkAutomigrate(config, 'designsystemet.config.json', false); + + expect(written.at(-1)).toBe(runtimeConfig); + expect(parseJsonc(runtimeConfig).themes.theme.colors).toEqual({ + accent: '#0062BA', + neutral: '#1E2B3C', + }); + }); + + it('writes nothing when every migration is declined', async () => { + confirm.mockResolvedValue(false); + + await checkAutomigrate(config, 'designsystemet.config.json', false); + + expect(written).toEqual([]); + }); +}); diff --git a/packages/cli/src/automigrate.ts b/packages/cli/src/automigrate.ts index 4241561686..5e1a70a97e 100644 --- a/packages/cli/src/automigrate.ts +++ b/packages/cli/src/automigrate.ts @@ -15,7 +15,10 @@ export const checkAutomigrate = async (configFile: string, configFilePath: strin return configFile; } - let migratedConfigFile = configFile; + // Declined migrations may still transform the config for this run, e.g. to keep it compatible with the schema. + // Those changes must never reach the file, so the text written to disk is tracked separately from the text used now. + let persistedConfig = configFile; + let runtimeConfig = configFile; for (const migration of eligibleMigrations) { console.log(pc.red(`\n āœ‹ Automigration detected \n`)); console.log( @@ -33,11 +36,14 @@ export const checkAutomigrate = async (configFile: string, configFilePath: strin } if (!answer) { - migratedConfigFile = migration.no(migratedConfigFile ?? configFile); + runtimeConfig = migration.no(runtimeConfig); } else { - migratedConfigFile = migration.yes(migratedConfigFile ?? configFile, { configFilePath }); - await dsfs.writeFile(configFilePath, migratedConfigFile); + const migrated = migration.yes(persistedConfig, { configFilePath }); + // Only migrate the runtime text separately when a declined migration has made it differ from the file. + runtimeConfig = runtimeConfig === persistedConfig ? migrated : migration.yes(runtimeConfig, { configFilePath }); + persistedConfig = migrated; + await dsfs.writeFile(configFilePath, persistedConfig); } } - return migratedConfigFile; + return runtimeConfig; }; diff --git a/summary.md b/summary.md new file mode 100644 index 0000000000..7899f9a725 --- /dev/null +++ b/summary.md @@ -0,0 +1,214 @@ +resolves #5191 +## Summary + +### Current way to run CLI +When we started with the CLI we didn't have experience or much of a plan on how it would be used. This resulted in a patchwork of features, implemented as the need came up, which is cumbersome to use and not very thought out. + +- You need to run two commands to generate css file + - `designsystemet tokens create && designsystemet tokens build -t ./design-tokens -o ./design-tokens-build` +- Users need to look up what command to run or forget to run the latter command for CSS file. +- `tokens create` uses `designsystemet.config.json` for creating tokens with fields like `outDir` and `clean` tied only to create. +- `tokens build` _does not use_ `designsystemet.config.json`, instead you need to pass terminal arguments such as `--out-dir`, `--clean`, `--tokens` to configure how it runs. +- no option for turning off `types.d.ts` generation, or in general confusing configuration +- continuing in the pattern of `tokens create` and `tokens build` is not very scalable, as we can generate CSS without design-tokens. + + +### New way to run CLI + +Declarative approach with everything that needs to be run defined in a config schema, `designsystemet.config.json`. + +New `output` field which lets users decide which output they want to create/generate for designsystemet. Just executing the bin/command, `designsystemet`, creates everything defined in `output`, so users can pick and choose which outputs they want created for their designsystem. + +This is inspired with how CLI for bundlers, `tsdown`, `rolldown`, `rollup` etc. so should be more familiar. + +This feature is also part of stabilising the config schema so we can remove the experimental we have today under config readme and page. + +**`tokens create` and `tokens build` still work, but are deprecated. `outDir` and `clean` are deprecated in favour of `output`, and the CLI offers to migrate existing config files automatically.** + + + +### Stack + +- #5416 – new `output` field and root command (this PR) +- #5438 – `tailwind` option on the `css` output, with Tailwind v3 and v4 support +- #5443 – default severity colors are added during config validation +- #5444 – new `types` output for TypeScript declarations +- #5451 – missing `--text-*` variables in the Tailwind theme file + +## Preview + +### Before + +```jsonc +{ + "outDir": "./design-tokens", // only used by `tokens create` + "clean": true, + "themes": { + "my-theme": { + "colors": { + "accent": "#0062BA", + "neutral": "#1E2B3C" + } + } + } +} +``` + +```bash +npx @digdir/designsystemet tokens create --config designsystemet.config.json +npx @digdir/designsystemet tokens build -t ./design-tokens -o ./design-tokens-build --experimental-tailwind +``` + +### After + +```jsonc +{ + "themes": { + "my-theme": { + "colors": { + "accent": "#0062BA", + "neutral": "#1E2B3C" + } + } + } +} +``` + +```bash +npx @digdir/designsystemet +``` + +With no `output` field, you get the defaults, `["design-tokens", "css", "types"]` once the whole stack is merged (`["design-tokens", "css"]` in this PR alone, as `types` arrives in #5444). + +**This is primarily to match today's expected output when running `tokens create` and `tokens build`. I expect us to adjust what default output will be in the future.** + +The config file is auto-detected (`designsystemet.config.json`, then `designsystemet.config.jsonc`), or passed with `-c, --config `. All paths in `output` are relative to the config file. + + +## How `output` works + +`output` is a list of what to create. Each item is either an output type using its defaults, or an object with custom settings: + +```jsonc +{ + "output": [ + "design-tokens", // output type with default values + + // object with configured output options for css file + { + "type": "css", + "dir": "./css", + "tokensDir": "./design-tokens", + "tailwind": "v4" + }, + ] +} +``` + +- **Cleaning is on by default:** `cleanDir` defaults to `true`, so files that are no longer generated are removed. This was opt-in before (`clean: false`). Every output directory is cleaned once, before any output runs, so outputs can share a directory without deleting each other's files. The CLI refuses to clean a directory that contains the config file, or existing design tokens that `css` or `types` build from (`tokensDir`), and stops before deleting anything. +- **Order:** `design-tokens` always runs first, whatever order the outputs are listed in, since CSS and types can be built from the tokens. +- **`tokensDir`:** `css` and `types` build from `tokensDir`, or else from the `dir` of the `design-tokens` output. This is decided per output, so one `css` output can build from existing tokens while another is created from the themes. +- **Without design tokens:** `"output": ["css", "types"]` with no `tokensDir` creates CSS and types directly from the themes, without writing any design tokens to disk. +- **Without themes:** `themes` is only needed by outputs that are created from themes. A config with only a `css` output and a `tokensDir` builds CSS from existing design tokens and can leave `themes` out: + + ```json + { + "output": [{ "type": "css", "tokensDir": "./design-tokens" }] + } + ``` + +If an output needs themes and there are none, the CLI stops with an error saying so. + +### Inject design-tokens + +We know some users today have scripts to manipulate design-tokens before building. Usually adding colors or adjusting size scale. This is still possible with `output` by using two configs and running `designsystemet` twice with each config. + +`create-tokens.json` +```jsonc +{ + "output": ["design-tokens"], + "themes": {} // your theme configuration +} +``` + +`build-tokens.json` +```jsonc +{ + // only output is needed as we build css from design-tokens + "output": [{ + "type": "css", + "tokensDir": "./design-tokens", + }] +} +``` + +run +``` +designsystemet --config create-tokens.json && +node inject-script.js && +designsystemet --config build-tokens.json +``` + +### Tailwind (#5438) + +The `css` output has a `tailwind` option that replaces `--experimental-tailwind`. By default (`false`) no Tailwind file is generated. Set it to the Tailwind version you use to also get a `.tailwind.css`: + +- `"v4"` uses `@theme inline`, so Tailwind utilities reference the `--ds-*` variables directly. `data-color`, `data-color-scheme` and `data-size` then also apply to utilities, at any depth in the DOM. +- `"v3"` generates the same file as before. The deprecated `tokens build --experimental-tailwind` keeps generating v3. + +Both versions also get `--text-sm`, `--text-md` and `--text-lg` mapped to the body font sizes in #5451. These were never generated because of a typo in the token name matching. + +### Types (#5444) + +Type declarations (`types.d.ts`) now have their own `types` output instead of always being written with the CSS. These augment `@digdir/designsystemet-types` with the theme's color names. + +> [!NOTE] +> The `css` output no longer writes type declarations. If you set `output` yourself, add `"types"` to keep getting them. The default `output`, and configs migrated from `outDir`, already include it. + +## Migrating existing configs + +When the CLI finds `outDir` or `clean`, it offers to migrate the config file: + +``` + āœ‹ Automigration detected +Config file designsystemet.config.json is eligible for migration: New output field +Your config file uses the deprecated outDir and clean fields. +This migration will replace them with a new output field if necessary. +? Do you want to migrate? (Y/n) +``` + +`"outDir": "./tokens", "clean": true` becomes: + +```json +{ + "output": [ + { "type": "design-tokens", "dir": "./tokens" }, + { "type": "css", "tokensDir": "./tokens" }, + { "type": "types", "tokensDir": "./tokens" } + ] +} +``` + +- **Default values:** if `outDir` already has its default value and `clean` isn't `false`, the migration just removes the old fields and doesn't add an `output`. +- **`clean: false`:** an explicit `"clean": false` is carried over as `"cleanDir": false` on every output that is cleaned, so folders the user opted out of cleaning are not deleted. This adds an `output` even when `outDir` has its default value. +- **Paths:** `outDir` was resolved from the directory the CLI ran in, but `output` paths are resolved from the config file. When the config isn't in that directory, the migration rewrites `outDir` so it still points to the same place. +- **Formatting:** the migration edits the file in place, so comments and formatting are kept. `output` is placed right after `$schema`. +- **Declining:** the config still works as before. `outDir` and `clean` still validate and print a deprecation warning. + +`generate-config-from-tokens` uses the same mapping, so a generated config gets the same `output` as a migrated one. + +## Deprecations + +- **`tokens create` / `tokens build`:** these now print a deprecation warning and are marked `[deprecated]` in `--help`. They're kept for backwards compatibility and will be removed in a future release. +- **`outDir` / `clean`:** marked `deprecated` in the Config schema, and replaced by `output[].dir` / `output[].cleanDir`. +- **`experimental_tailwind`:** replaced by `tailwind` on the `css` output. + +## Other changes + +- **`generate-config-from-tokens`:** the generated config now uses `output` instead of the deprecated `outDir`, with the tokens directory written relative to the config file (`--out`) rather than as an absolute path. If the tokens are in the default `design-tokens` directory next to the config, no `output` is written and the defaults apply. It refuses an `--out` inside the tokens directory, since running that config would clean the directory it's in. +- **`themes` is optional:** `themes` is no longer required by the config schema or the published JSON schema. An empty `"themes": {}` is still rejected. +- **Severity color defaults (#5443):** the default `info`, `success`, `warning` and `danger` colors are now added during config validation, instead of in several places in the token generation. User-defined severity colors keep their value, and all severity colors are placed last. `neutral` is still required. +- **Root config:** the repo's `designsystemet.config.json` sets `"tailwind": "v3"` on its `css` output, so running the new command keeps the published `packages/css/theme/designsystemet.tailwind.css` identical to what `build:theme` generates. +- **Theme builder:** the "use theme" modal now shows `npx @digdir/designsystemet` as the build command. The config snippet no longer includes `outDir`, the hard-coded `typography`, or `borderRadius` when it's the default. +- **Removed `banner`:** the `banner` option on the `css` output was never used by the CLI, so it's gone from the schema. +- **Docs:** the `cli-config`, `own-theme`, `multiple-themes` and `css` pages (en and no), the CLI README and the CSS README are updated to use the new command, `output` and the `tailwind` option. \ No newline at end of file From 07c0cced2eb118160fd603da1df778c8eac7fda2 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 13:10:21 +0200 Subject: [PATCH 35/44] update tokens create to not automigrate but warning about running `designsystemet` to migrate, so tokens create does not break after first migrate --- packages/cli/bin/deprecated.ts | 14 +++++++++++++- packages/cli/src/automigrate.test.ts | 14 ++++++++++++++ packages/cli/src/automigrate.ts | 27 +++++++++++++++++++-------- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/cli/bin/deprecated.ts b/packages/cli/bin/deprecated.ts index 66c64e9f60..33b781dc76 100644 --- a/packages/cli/bin/deprecated.ts +++ b/packages/cli/bin/deprecated.ts @@ -3,6 +3,7 @@ import pc from 'picocolors'; import { checkAutomigrate } from '../src/automigrate.ts'; import { convertToHex } from '../src/colors/index.ts'; import type { CssColor } from '../src/colors/types.ts'; +import { automigrations } from '../src/migrations/index.ts'; import type { ConfigSchemaThemes } from '../src/schemas/schema.ts'; import { dsfs } from '../src/utils/filesystem.ts'; import { @@ -137,9 +138,20 @@ export function makeTokenCommands({ createDesignTokens, buildCss }: TokenCommand const { configFile, configFilePath } = await getConfigFile(opts.config); + if (!opts.skipCheck && automigrations.newOutputField.check(configFile)) { + console.warn( + pc.yellow( + `\n${pc.bold('outDir')} and ${pc.bold('clean')} are deprecated. Run ${pc.blue('designsystemet')} to migrate your config file to ${pc.bold('output')}.\n`, + ), + ); + } + const updatedConfigFile = opts.skipCheck ? configFile - : await checkAutomigrate(configFile, configFilePath, opts.yes); + : await checkAutomigrate(configFile, configFilePath, opts.yes, { + // `tokens create` only reads `outDir` and `clean`, so migrating them to `output` would make it ignore them. + exclude: ['newOutputField'], + }); const config = await parseValidateAndOptsConfig(updatedConfigFile || configFile, { theme: themeName, diff --git a/packages/cli/src/automigrate.test.ts b/packages/cli/src/automigrate.test.ts index 77491ae433..48a4864a6b 100644 --- a/packages/cli/src/automigrate.test.ts +++ b/packages/cli/src/automigrate.test.ts @@ -55,6 +55,20 @@ describe('checkAutomigrate', () => { }); }); + it('does not offer excluded migrations', async () => { + confirm.mockResolvedValue(true); + + const runtimeConfig = parseJsonc( + await checkAutomigrate(config, 'designsystemet.config.json', false, { exclude: ['newOutputField'] }), + ); + + // Only the color categories migration was offered, so `outDir` is kept and no `output` is added. + expect(confirm).toHaveBeenCalledTimes(1); + expect(runtimeConfig.outDir).toBe('tokens'); + expect(runtimeConfig.output).toBeUndefined(); + expect(runtimeConfig.themes.theme.colors).toEqual({ accent: '#0062BA', neutral: '#1E2B3C' }); + }); + it('writes nothing when every migration is declined', async () => { confirm.mockResolvedValue(false); diff --git a/packages/cli/src/automigrate.ts b/packages/cli/src/automigrate.ts index 5e1a70a97e..0af3458f01 100644 --- a/packages/cli/src/automigrate.ts +++ b/packages/cli/src/automigrate.ts @@ -3,14 +3,25 @@ import pc from 'picocolors'; import { automigrations } from './migrations/index.ts'; import { dsfs } from './utils/filesystem.ts'; -export const checkAutomigrate = async (configFile: string, configFilePath: string, yes: boolean) => { - const eligibleMigrations = Object.values(automigrations).filter((migration) => { - try { - return migration.check(configFile); - } catch { - return false; - } - }); +type AutomigrationName = keyof typeof automigrations; + +export const checkAutomigrate = async ( + configFile: string, + configFilePath: string, + yes: boolean, + /** Migrations not to offer, e.g. ones whose result the calling command can't use. */ + { exclude = [] }: { exclude?: AutomigrationName[] } = {}, +) => { + const eligibleMigrations = (Object.keys(automigrations) as AutomigrationName[]) + .filter((name) => !exclude.includes(name)) + .map((name) => automigrations[name]) + .filter((migration) => { + try { + return migration.check(configFile); + } catch { + return false; + } + }); if (eligibleMigrations.length === 0) { return configFile; } From 42f93e7d5c4e5f33ef079204492f583bd5f2c561 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 13:13:32 +0200 Subject: [PATCH 36/44] remove local summary --- summary.md | 214 ----------------------------------------------------- 1 file changed, 214 deletions(-) delete mode 100644 summary.md diff --git a/summary.md b/summary.md deleted file mode 100644 index 7899f9a725..0000000000 --- a/summary.md +++ /dev/null @@ -1,214 +0,0 @@ -resolves #5191 -## Summary - -### Current way to run CLI -When we started with the CLI we didn't have experience or much of a plan on how it would be used. This resulted in a patchwork of features, implemented as the need came up, which is cumbersome to use and not very thought out. - -- You need to run two commands to generate css file - - `designsystemet tokens create && designsystemet tokens build -t ./design-tokens -o ./design-tokens-build` -- Users need to look up what command to run or forget to run the latter command for CSS file. -- `tokens create` uses `designsystemet.config.json` for creating tokens with fields like `outDir` and `clean` tied only to create. -- `tokens build` _does not use_ `designsystemet.config.json`, instead you need to pass terminal arguments such as `--out-dir`, `--clean`, `--tokens` to configure how it runs. -- no option for turning off `types.d.ts` generation, or in general confusing configuration -- continuing in the pattern of `tokens create` and `tokens build` is not very scalable, as we can generate CSS without design-tokens. - - -### New way to run CLI - -Declarative approach with everything that needs to be run defined in a config schema, `designsystemet.config.json`. - -New `output` field which lets users decide which output they want to create/generate for designsystemet. Just executing the bin/command, `designsystemet`, creates everything defined in `output`, so users can pick and choose which outputs they want created for their designsystem. - -This is inspired with how CLI for bundlers, `tsdown`, `rolldown`, `rollup` etc. so should be more familiar. - -This feature is also part of stabilising the config schema so we can remove the experimental we have today under config readme and page. - -**`tokens create` and `tokens build` still work, but are deprecated. `outDir` and `clean` are deprecated in favour of `output`, and the CLI offers to migrate existing config files automatically.** - - - -### Stack - -- #5416 – new `output` field and root command (this PR) -- #5438 – `tailwind` option on the `css` output, with Tailwind v3 and v4 support -- #5443 – default severity colors are added during config validation -- #5444 – new `types` output for TypeScript declarations -- #5451 – missing `--text-*` variables in the Tailwind theme file - -## Preview - -### Before - -```jsonc -{ - "outDir": "./design-tokens", // only used by `tokens create` - "clean": true, - "themes": { - "my-theme": { - "colors": { - "accent": "#0062BA", - "neutral": "#1E2B3C" - } - } - } -} -``` - -```bash -npx @digdir/designsystemet tokens create --config designsystemet.config.json -npx @digdir/designsystemet tokens build -t ./design-tokens -o ./design-tokens-build --experimental-tailwind -``` - -### After - -```jsonc -{ - "themes": { - "my-theme": { - "colors": { - "accent": "#0062BA", - "neutral": "#1E2B3C" - } - } - } -} -``` - -```bash -npx @digdir/designsystemet -``` - -With no `output` field, you get the defaults, `["design-tokens", "css", "types"]` once the whole stack is merged (`["design-tokens", "css"]` in this PR alone, as `types` arrives in #5444). - -**This is primarily to match today's expected output when running `tokens create` and `tokens build`. I expect us to adjust what default output will be in the future.** - -The config file is auto-detected (`designsystemet.config.json`, then `designsystemet.config.jsonc`), or passed with `-c, --config `. All paths in `output` are relative to the config file. - - -## How `output` works - -`output` is a list of what to create. Each item is either an output type using its defaults, or an object with custom settings: - -```jsonc -{ - "output": [ - "design-tokens", // output type with default values - - // object with configured output options for css file - { - "type": "css", - "dir": "./css", - "tokensDir": "./design-tokens", - "tailwind": "v4" - }, - ] -} -``` - -- **Cleaning is on by default:** `cleanDir` defaults to `true`, so files that are no longer generated are removed. This was opt-in before (`clean: false`). Every output directory is cleaned once, before any output runs, so outputs can share a directory without deleting each other's files. The CLI refuses to clean a directory that contains the config file, or existing design tokens that `css` or `types` build from (`tokensDir`), and stops before deleting anything. -- **Order:** `design-tokens` always runs first, whatever order the outputs are listed in, since CSS and types can be built from the tokens. -- **`tokensDir`:** `css` and `types` build from `tokensDir`, or else from the `dir` of the `design-tokens` output. This is decided per output, so one `css` output can build from existing tokens while another is created from the themes. -- **Without design tokens:** `"output": ["css", "types"]` with no `tokensDir` creates CSS and types directly from the themes, without writing any design tokens to disk. -- **Without themes:** `themes` is only needed by outputs that are created from themes. A config with only a `css` output and a `tokensDir` builds CSS from existing design tokens and can leave `themes` out: - - ```json - { - "output": [{ "type": "css", "tokensDir": "./design-tokens" }] - } - ``` - -If an output needs themes and there are none, the CLI stops with an error saying so. - -### Inject design-tokens - -We know some users today have scripts to manipulate design-tokens before building. Usually adding colors or adjusting size scale. This is still possible with `output` by using two configs and running `designsystemet` twice with each config. - -`create-tokens.json` -```jsonc -{ - "output": ["design-tokens"], - "themes": {} // your theme configuration -} -``` - -`build-tokens.json` -```jsonc -{ - // only output is needed as we build css from design-tokens - "output": [{ - "type": "css", - "tokensDir": "./design-tokens", - }] -} -``` - -run -``` -designsystemet --config create-tokens.json && -node inject-script.js && -designsystemet --config build-tokens.json -``` - -### Tailwind (#5438) - -The `css` output has a `tailwind` option that replaces `--experimental-tailwind`. By default (`false`) no Tailwind file is generated. Set it to the Tailwind version you use to also get a `.tailwind.css`: - -- `"v4"` uses `@theme inline`, so Tailwind utilities reference the `--ds-*` variables directly. `data-color`, `data-color-scheme` and `data-size` then also apply to utilities, at any depth in the DOM. -- `"v3"` generates the same file as before. The deprecated `tokens build --experimental-tailwind` keeps generating v3. - -Both versions also get `--text-sm`, `--text-md` and `--text-lg` mapped to the body font sizes in #5451. These were never generated because of a typo in the token name matching. - -### Types (#5444) - -Type declarations (`types.d.ts`) now have their own `types` output instead of always being written with the CSS. These augment `@digdir/designsystemet-types` with the theme's color names. - -> [!NOTE] -> The `css` output no longer writes type declarations. If you set `output` yourself, add `"types"` to keep getting them. The default `output`, and configs migrated from `outDir`, already include it. - -## Migrating existing configs - -When the CLI finds `outDir` or `clean`, it offers to migrate the config file: - -``` - āœ‹ Automigration detected -Config file designsystemet.config.json is eligible for migration: New output field -Your config file uses the deprecated outDir and clean fields. -This migration will replace them with a new output field if necessary. -? Do you want to migrate? (Y/n) -``` - -`"outDir": "./tokens", "clean": true` becomes: - -```json -{ - "output": [ - { "type": "design-tokens", "dir": "./tokens" }, - { "type": "css", "tokensDir": "./tokens" }, - { "type": "types", "tokensDir": "./tokens" } - ] -} -``` - -- **Default values:** if `outDir` already has its default value and `clean` isn't `false`, the migration just removes the old fields and doesn't add an `output`. -- **`clean: false`:** an explicit `"clean": false` is carried over as `"cleanDir": false` on every output that is cleaned, so folders the user opted out of cleaning are not deleted. This adds an `output` even when `outDir` has its default value. -- **Paths:** `outDir` was resolved from the directory the CLI ran in, but `output` paths are resolved from the config file. When the config isn't in that directory, the migration rewrites `outDir` so it still points to the same place. -- **Formatting:** the migration edits the file in place, so comments and formatting are kept. `output` is placed right after `$schema`. -- **Declining:** the config still works as before. `outDir` and `clean` still validate and print a deprecation warning. - -`generate-config-from-tokens` uses the same mapping, so a generated config gets the same `output` as a migrated one. - -## Deprecations - -- **`tokens create` / `tokens build`:** these now print a deprecation warning and are marked `[deprecated]` in `--help`. They're kept for backwards compatibility and will be removed in a future release. -- **`outDir` / `clean`:** marked `deprecated` in the Config schema, and replaced by `output[].dir` / `output[].cleanDir`. -- **`experimental_tailwind`:** replaced by `tailwind` on the `css` output. - -## Other changes - -- **`generate-config-from-tokens`:** the generated config now uses `output` instead of the deprecated `outDir`, with the tokens directory written relative to the config file (`--out`) rather than as an absolute path. If the tokens are in the default `design-tokens` directory next to the config, no `output` is written and the defaults apply. It refuses an `--out` inside the tokens directory, since running that config would clean the directory it's in. -- **`themes` is optional:** `themes` is no longer required by the config schema or the published JSON schema. An empty `"themes": {}` is still rejected. -- **Severity color defaults (#5443):** the default `info`, `success`, `warning` and `danger` colors are now added during config validation, instead of in several places in the token generation. User-defined severity colors keep their value, and all severity colors are placed last. `neutral` is still required. -- **Root config:** the repo's `designsystemet.config.json` sets `"tailwind": "v3"` on its `css` output, so running the new command keeps the published `packages/css/theme/designsystemet.tailwind.css` identical to what `build:theme` generates. -- **Theme builder:** the "use theme" modal now shows `npx @digdir/designsystemet` as the build command. The config snippet no longer includes `outDir`, the hard-coded `typography`, or `borderRadius` when it's the default. -- **Removed `banner`:** the `banner` option on the `css` output was never used by the CLI, so it's gone from the schema. -- **Docs:** the `cli-config`, `own-theme`, `multiple-themes` and `css` pages (en and no), the CLI README and the CSS README are updated to use the new command, `output` and the `tailwind` option. \ No newline at end of file From 5955830f2a8b5574b4711c0c2c4ee03978c12fd9 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 13:38:59 +0200 Subject: [PATCH 37/44] makes sure themes is defined for the outputs that needs it --- packages/cli/bin/designsystemet.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index a2f8e124e7..7feca36200 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -80,6 +80,15 @@ program const sortedOutput = R.sortBy((o) => (o.type === 'design-tokens' ? 0 : 1), config.output); const designTokensOutput = config.output.find((o) => o.type === 'design-tokens'); + // Outputs created from themes can't run without them. Check this before cleaning, so nothing is deleted when + // themes are missing. Same rule as below: design tokens, and CSS with no design tokens to build from. + const needsThemes = config.output.some( + (o) => o.type === 'design-tokens' || (o.type === 'css' && (o.tokensDir ?? designTokensOutput?.dir) === undefined), + ); + if (needsThemes) { + requireThemes(config); + } + // Clean every output directory once, before any output is created. Cleaning as part of each output would // delete what earlier outputs wrote when they share a directory, which makes the outputs depend on their order. const dirsToClean = R.uniq( From dd8802544f5b0c63b7f6f30f0da2c326d9a62c24 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 13:41:27 +0200 Subject: [PATCH 38/44] make sure tokens create does not crash without config --- packages/cli/bin/deprecated.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/cli/bin/deprecated.ts b/packages/cli/bin/deprecated.ts index 33b781dc76..a32d1b64ba 100644 --- a/packages/cli/bin/deprecated.ts +++ b/packages/cli/bin/deprecated.ts @@ -138,7 +138,7 @@ export function makeTokenCommands({ createDesignTokens, buildCss }: TokenCommand const { configFile, configFilePath } = await getConfigFile(opts.config); - if (!opts.skipCheck && automigrations.newOutputField.check(configFile)) { + if (!opts.skipCheck && usesDeprecatedOutputFields(configFile)) { console.warn( pc.yellow( `\n${pc.bold('outDir')} and ${pc.bold('clean')} are deprecated. Run ${pc.blue('designsystemet')} to migrate your config file to ${pc.bold('output')}.\n`, @@ -176,3 +176,20 @@ function parseColorValues(value: string, previous: Record = {} previous[name] = convertToHex(hex); return previous; } + +/** + * Whether the config file uses the deprecated `outDir` or `clean` fields. False without a config file, since + * `tokens create` can run on CLI options alone, and for a config file that can't be parsed, which is reported + * when the config is validated. + */ +function usesDeprecatedOutputFields(configFile: string): boolean { + if (!configFile) { + return false; + } + + try { + return automigrations.newOutputField.check(configFile); + } catch { + return false; + } +} From 998ef5b0c20938a440a7e9897486b2a95ab10779 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 14:27:56 +0200 Subject: [PATCH 39/44] abort run if not migrated. --- packages/cli/bin/designsystemet.ts | 13 +++++++++++-- packages/cli/src/migrations/new-output-field.ts | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 7feca36200..a8e9f508f9 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -13,7 +13,6 @@ import { type ExternalConfigSchemaInput, externalConfigSchema, } from '../src/schemas/schema.ts'; -import { warnDeprecatedFields } from '../src/schemas/schema-output.ts'; import { buildTokens } from '../src/tokens/build.ts'; import { createTokens, getTokenSetDimensions, systemTokenToFiles, tokenSetsToFiles } from '../src/tokens/create.ts'; import { formatThemeCSS } from '../src/tokens/format.ts'; @@ -71,7 +70,17 @@ program : await checkAutomigrate(configFile, configFilePath, opts.yes); const parsedConfig = parseConfig(updatedConfigFile); - warnDeprecatedFields(parsedConfig); + + // This command only reads `output`. If `outDir` or `clean` are still in the config, because the migration was + // declined or skipped, stop before anything is cleaned or written instead of silently ignoring them. + if (parsedConfig.outDir !== undefined || parsedConfig.clean !== undefined) { + console.error( + pc.redBright( + `${pc.blue('outDir')} and ${pc.blue('clean')} are not supported by ${pc.blue('designsystemet')}. Run it again and accept the migration, or replace them with ${pc.blue('output')}. To keep using them, run ${pc.blue('designsystemet tokens create')} instead.`, + ), + ); + process.exit(1); + } // Validate against the public schema first for a user-facing error on unsupported theme fields. validateConfig(externalConfigSchema, parsedConfig); const config = validateConfig(configSchema, parsedConfig); diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index f4ad39e0fb..447a3034c0 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -158,8 +158,9 @@ const migration: Automigrate = { return migratedConfig; }, no: (config: string): string => { - // The deprecated fields still validate, so the config can be used as-is. - console.log(pc.yellow('\nUsing existing config file but migration was skipped.\n')); + // The file is left as-is. `designsystemet` stops when `outDir` or `clean` are still set, while `tokens create` + // keeps reading them. + console.log(pc.yellow('\nMigration was skipped.\n')); return config; }, }; From 410f027df346f7858619be392805ca9d99be655b Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 14:29:49 +0200 Subject: [PATCH 40/44] updated changeset about default clean --- .changeset/sour-turtles-begin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/sour-turtles-begin.md b/.changeset/sour-turtles-begin.md index 840ea1eec8..e279944702 100644 --- a/.changeset/sour-turtles-begin.md +++ b/.changeset/sour-turtles-begin.md @@ -2,4 +2,4 @@ "@digdir/designsystemet": minor --- -**CLI** New `output[]` will clean `outDir` folders by default. +**CLI:** Each output in the new `output` field cleans its `dir` before generating files. Set `cleanDir` to `false` on an output to keep existing files. From 9511183077cb92ea5e08933a20a99095aba5eba3 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 14:56:04 +0200 Subject: [PATCH 41/44] makes sure removing a deprecated field does not break comments --- .../src/migrations/new-output-field.test.ts | 13 ++++ .../cli/src/migrations/new-output-field.ts | 65 ++++++++++++++----- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/migrations/new-output-field.test.ts b/packages/cli/src/migrations/new-output-field.test.ts index dfe5e8b357..ee401c7989 100644 --- a/packages/cli/src/migrations/new-output-field.test.ts +++ b/packages/cli/src/migrations/new-output-field.test.ts @@ -28,6 +28,19 @@ describe('new output field migration', () => { }); }); + // A comment between a value and its comma used to hide the comma, leaving invalid JSON behind. + it.each([ + ['a block comment before the comma', '{ "outDir": "design-tokens" /* note */, "themes": {} }'], + ['a line comment after the comma', '{\n "clean": true, // note\n "themes": {}\n}'], + ['a line comment before the comma', '{\n "clean": true // note\n ,"themes": {}\n}'], + ['a comment after the last property', '{ "themes": {}, "clean": true /* last */ }'], + ])('removes the deprecated fields and keeps the config valid with %s', (_, config) => { + const migrated = migrateToOutputField(config); + + expect(parseJsonc(migrated)).toEqual({ themes: {} }); + expect(migrated).toMatch(/note|last/); + }); + it('places output after $schema if present, otherwise at the top', () => { const withSchema = migrateToOutputField('{ "themes": {}, "$schema": "schema.json", "outDir": "tokens" }'); expect(Object.keys(parseJsonc(withSchema))).toEqual(['themes', '$schema', 'output']); diff --git a/packages/cli/src/migrations/new-output-field.ts b/packages/cli/src/migrations/new-output-field.ts index 447a3034c0..dc952d104a 100644 --- a/packages/cli/src/migrations/new-output-field.ts +++ b/packages/cli/src/migrations/new-output-field.ts @@ -1,6 +1,6 @@ // biome-ignore-all lint/suspicious/noExplicitAny: the deprecated fields are no longer in the schema types, so we need to use any here import path from 'node:path'; -import { applyEdits, findNodeAtLocation, modify, parseTree } from 'jsonc-parser'; +import { applyEdits, createScanner, findNodeAtLocation, modify, parseTree } from 'jsonc-parser'; import pc from 'picocolors'; import { parseJsonc } from '../schemas/helpers.ts'; import { outputConfigShape } from '../schemas/schema-output.ts'; @@ -24,39 +24,70 @@ type Automigrate = { no: (config: string) => string; }; +/** + * The scanner's token kind for a comma. `SyntaxKind` is a `const enum`, which can't be imported with `isolatedModules`, + * so it's read from the scanner instead. + */ +const COMMA_TOKEN = createScanner(',').scan(); + +/** Returns the offset of the next token from `offset` that isn't whitespace or a comment, and that token. */ +const nextToken = (text: string, offset: number) => { + const scanner = createScanner(text, true); + scanner.setPosition(offset); + const token = scanner.scan(); + + return { token, offset: scanner.getTokenOffset() }; +}; + /** * Removes a top-level property and its comma, leaving surrounding comments and formatting intact. * `modify(text, [key], undefined)` removes everything up to the next property, including comments. + * + * Comments between the value and its comma are kept, so the comma is found with the JSONC scanner + * rather than by looking for whitespace only. */ const removeProperty = (text: string, key: string): string => { const tree = parseTree(text); const property = tree && findNodeAtLocation(tree, [key])?.parent; - if (!property) { + if (!property || !tree.children) { return text; } - let start = property.offset; - let end = property.offset + property.length; + const propertyStart = property.offset; + const propertyEnd = property.offset + property.length; + + // Ranges to remove: the property itself, and one comma next to it. + const removals: [number, number][] = [[propertyStart, propertyEnd]]; - const trailingComma = /^\s*,/.exec(text.slice(end)); - if (trailingComma) { - end += trailingComma[0].length; + const after = nextToken(text, propertyEnd); + const hasTrailingComma = after.token === COMMA_TOKEN; + if (hasTrailingComma) { + removals.push([after.offset, after.offset + 1]); } else { - // Last property: remove the comma before it instead. - const leadingComma = /,\s*$/.exec(text.slice(0, start)); - if (leadingComma) { - start -= leadingComma[0].length; + // Last property: remove the comma after the previous property instead. + const index = tree.children.indexOf(property); + const previous = tree.children[index - 1]; + if (previous) { + const comma = nextToken(text, previous.offset + previous.length); + if (comma.token === COMMA_TOKEN) { + removals.push([comma.offset, comma.offset + 1]); + } } } - // Remove the whole line when the property is on its own line. - const lineStart = text.lastIndexOf('\n', start - 1) + 1; - if (trailingComma && /^[ \t]*$/.test(text.slice(lineStart, start)) && text[end] === '\n') { - start = lineStart; - end += 1; + let result = text; + for (const [start, end] of removals.sort(([a], [b]) => b - a)) { + result = result.slice(0, start) + result.slice(end); + } + + // Remove the line the property was on when nothing but whitespace is left on it. + const lineStart = result.lastIndexOf('\n', propertyStart - 1) + 1; + const lineEnd = result.indexOf('\n', propertyStart); + if (lineEnd !== -1 && lineStart > 0 && /^[ \t]*$/.test(result.slice(lineStart, lineEnd))) { + result = result.slice(0, lineStart) + result.slice(lineEnd + 1); } - return text.slice(0, start) + text.slice(end); + return result; }; const hasDeprecatedFields = (config: string): boolean => { From 7db3413fcfb028de31345bc7900e64f5a0f6b197 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 15:01:19 +0200 Subject: [PATCH 42/44] more guards for conflicting outputs and cleaning --- packages/cli/bin/designsystemet.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index a8e9f508f9..90254c52e9 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -105,14 +105,19 @@ program ); // Check every directory before cleaning any, so nothing is deleted when one of them is unsafe. + // Paths are resolved, so differently written paths to the same directory (`tokens`, `./tokens/`) match. + const resolveDir = (dir: string) => path.resolve(dsfs.outDir, dir); + const regeneratedDirs = config.output.flatMap((o) => (o.type === 'design-tokens' ? [resolveDir(o.dir)] : [])); const unsafeCleanError = findUnsafeClean({ dirsToClean, configDir: path.dirname(path.resolve(configFilePath)), - // Existing design tokens that `css` outputs build from. Tokens in the `design-tokens` output's directory + // Existing design tokens that `css` outputs build from. Tokens in a `design-tokens` output's directory // are created again in this run, so cleaning them is safe. inputDirs: config.output - .flatMap((o) => (o.type === 'css' && o.tokensDir !== undefined ? [path.join(dsfs.outDir, o.tokensDir)] : [])) - .filter((dir) => !designTokensOutput || dir !== path.join(dsfs.outDir, designTokensOutput.dir)), + .flatMap((o) => (o.type === 'css' && o.tokensDir !== undefined ? [resolveDir(o.tokensDir)] : [])) + .filter((dir) => !regeneratedDirs.includes(dir)), + // Directories of outputs that keep their existing files, which another output's cleaning must not delete. + keptDirs: config.output.flatMap((o) => ('cleanDir' in o && o.cleanDir === false ? [resolveDir(o.dir)] : [])), }); if (unsafeCleanError) { console.error(pc.redBright(unsafeCleanError)); @@ -350,17 +355,19 @@ async function createCss({ } /** - * Returns an error message when cleaning `dirsToClean` would delete something the run needs: the config file, - * or existing design tokens in `inputDirs` that an output builds from. + * Returns an error message when cleaning `dirsToClean` would delete something it shouldn't: the config file, + * existing design tokens in `inputDirs` that an output builds from, or files in `keptDirs` that an output keeps. */ function findUnsafeClean({ dirsToClean, configDir, inputDirs, + keptDirs, }: { dirsToClean: string[]; configDir: string; inputDirs: string[]; + keptDirs: string[]; }): string | undefined { const toConfigRelative = (dir: string) => pc.blue(path.relative(configDir, dir) || '.'); const fix = `Use another ${pc.blue('dir')}, or set ${pc.blue('cleanDir')} to ${pc.blue('false')} for that output.`; @@ -374,6 +381,11 @@ function findUnsafeClean({ if (inputDir) { return `Output directory ${toConfigRelative(dir)} contains the design tokens in ${toConfigRelative(inputDir)}, so cleaning it would delete them before they are used. ${fix}`; } + + const keptDir = keptDirs.find((kept) => isSameOrInside(kept, dir)); + if (keptDir) { + return `Output directory ${toConfigRelative(dir)} contains ${toConfigRelative(keptDir)}, which has ${pc.blue('cleanDir')} set to ${pc.blue('false')}, so cleaning it would delete files that should be kept. Use separate directories, or the same ${pc.blue('cleanDir')} for both outputs.`; + } } return undefined; From 2c8d41af4cf6ab42bb060cb8429be193480fc159 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 15:02:10 +0200 Subject: [PATCH 43/44] update comment --- packages/cli/src/schemas/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/schemas/schema.ts b/packages/cli/src/schemas/schema.ts index 4f418a3391..75b0749e54 100644 --- a/packages/cli/src/schemas/schema.ts +++ b/packages/cli/src/schemas/schema.ts @@ -268,7 +268,7 @@ const externalThemeSchema = themeObjectSchema .meta({ description: 'An object defining a theme. The property name holding the object becomes the theme name.' }); /** - * The public config: {@link configSchema} without `output`, and with themes restricted to the public keys. + * The public config: {@link configSchema}, including `output`, with themes restricted to the public keys. * Use this when exposing the schema externally (the public JSON schema, the theme builder and the Figma plugin); * use {@link configSchema} to validate a config in the CLI. */ From fc4f52e73623cb632691fa9816756fdba7223721 Mon Sep 17 00:00:00 2001 From: Michael Marszalek Date: Thu, 1 Oct 2026 15:18:10 +0200 Subject: [PATCH 44/44] build from themes instead of design-tokens if dry run --- packages/cli/bin/designsystemet.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/bin/designsystemet.ts b/packages/cli/bin/designsystemet.ts index 90254c52e9..eb4aa277c8 100644 --- a/packages/cli/bin/designsystemet.ts +++ b/packages/cli/bin/designsystemet.ts @@ -146,8 +146,11 @@ program // Build CSS from `tokensDir`, or else from the design tokens created by the `design-tokens` output. // With neither, there are no design tokens to build from, so CSS is created directly from the themes. const tokensDir = output.tokensDir ?? designTokensOutput?.dir; + // A dry run doesn't write the design tokens this run creates, so they can't be read back from disk. + // Create the CSS from the themes instead, which gives the same result. + const tokensNotWritten = dry && tokensDir !== undefined && regeneratedDirs.includes(resolveDir(tokensDir)); - if (tokensDir === undefined) { + if (tokensDir === undefined || tokensNotWritten) { await createCss({ themes: requireThemes(config), outDir: outDir,