@advantacode/brander 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # AdvantaCode Brander
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@advantacode/brander)](https://www.npmjs.com/package/@advantacode/brander)
4
+
3
5
  AdvantaCode Brander is a design token generator that produces consistent branding tokens for modern web applications.
4
6
 
5
7
  It converts a simple configuration into reusable outputs for multiple platforms including:
@@ -16,13 +18,13 @@ AdvantaCode Brander uses OKLCH color space to generate perceptually consistent c
16
18
 
17
19
  ```bash
18
20
  npm install -D @advantacode/brander
19
- npx --package @advantacode/brander advantacode-brander setup --out src/brander --style src/style.css
21
+ npx --package @advantacode/brander advantacode-brander setup --style src/style.css
20
22
  ```
21
23
 
22
24
  This creates `brand.config.ts`, adds a `brand:generate` script, patches your stylesheet imports, and prepares the token output folder.
23
25
 
24
26
  During setup, Brander creates `brand.css` next to your main stylesheet, writes token/theme imports there, and adds a single `@import './brand.css';` to your main stylesheet.
25
- The generated `brand:generate` script uses `advantacode-brander generate` and includes any setup generation flags (`--out`, `--format`, `--theme`, `--prefix`) so repeat runs keep writing to the same target.
27
+ Brander stores project paths like `project.outDir` and `project.styleFile` in `brand.config.*` so your `brand:generate` script can stay minimal.
26
28
 
27
29
  AdvantaCode Brander generates design tokens and framework adapters from a single brand configuration file. It allows applications, design systems, and design tools to share a consistent source of truth for colors and semantic tokens.
28
30
 
@@ -77,16 +79,17 @@ advantacode-brander --out src/tokens
77
79
  advantacode-brander --format css,tailwind,figma
78
80
  advantacode-brander --theme dark
79
81
  advantacode-brander --prefix ac
80
- advantacode-brander setup --out src/brander --style src/style.css
82
+ advantacode-brander setup --style src/style.css
81
83
  advantacode-brander init --out src/brander
82
84
  ```
83
85
 
84
- Supported flags:
86
+ Supported flags (CLI overrides `brand.config.*`):
85
87
 
86
- * `--out <dir>` writes generated files to a custom folder instead of `dist/brander`
88
+ * `--out <dir>` writes generated files to a custom folder instead of `dist/brander` (or `project.outDir`)
87
89
  * `--format <list>` limits output to specific formats: `all`, `css`, `json`, `typescript` or `ts`, `scss`, `tailwind`, `bootstrap`, `figma`
88
- * `--theme <value>` limits theme CSS output to `light`, `dark`, or `both`
89
- * `--prefix <value>` applies a CSS variable prefix like `ac`, producing variables such as `--ac-primary`
90
+ * `--theme <value>` limits theme CSS output to `light`, `dark`, or `both` (or `theme`)
91
+ * `--prefix <value>` applies a CSS variable prefix like `ac`, producing variables such as `--ac-primary` (or `css.prefix`)
92
+ * `--style <path>` refreshes `brand.css` and the main stylesheet import during normal generation (or `project.styleFile`)
90
93
  * `--version`, `-v` prints the installed package version
91
94
  * `--help`, `-h` prints the CLI help text
92
95
 
@@ -104,6 +107,12 @@ Example:
104
107
  ```ts
105
108
  export default {
106
109
  name: process.env.COMPANY_NAME || "My Company",
110
+ project: {
111
+ outDir: "src/assets/brand",
112
+ styleFile: "src/styles.css"
113
+ },
114
+
115
+ adapters: ["tailwind"],
107
116
  css: {
108
117
  prefix: process.env.CSS_PREFIX ?? ""
109
118
  },
@@ -117,6 +126,19 @@ export default {
117
126
  success: process.env.SUCCESS_COLOR || "green-500",
118
127
  warning: process.env.WARNING_COLOR || "yellow-500",
119
128
  danger: process.env.DANGER_COLOR || "red-500"
129
+ },
130
+
131
+ typography: {
132
+ fontSans: "Inter",
133
+ fontMono: "JetBrains Mono"
134
+ },
135
+
136
+ spacing: {
137
+ xs: "0.25rem",
138
+ sm: "0.5rem",
139
+ md: "1rem",
140
+ lg: "1.5rem",
141
+ xl: "2rem"
120
142
  }
121
143
  };
122
144
  ```
@@ -148,7 +170,7 @@ DANGER_COLOR=red-500
148
170
 
149
171
  ## Generated Outputs
150
172
 
151
- Running the CLI with no flags generates all formats into `dist/brander` and writes both light and dark theme CSS.
173
+ If neither CLI `--format` nor `formats` / `adapters` are set in `brand.config.*`, running the CLI with no flags generates all formats into `dist/brander` and writes both light and dark theme CSS.
152
174
 
153
175
  ```text
154
176
  dist/
@@ -175,6 +197,8 @@ Example generated `tokens.css`:
175
197
  :root {
176
198
  --primary-500: oklch(0.65 0.2 45);
177
199
  --neutral-50: oklch(0.97 0.02 95);
200
+ --space-md: 1rem;
201
+ --font-sans: "Inter", sans-serif;
178
202
  }
179
203
  ```
180
204
 
@@ -280,6 +304,18 @@ text-danger
280
304
  border-secondary
281
305
  ```
282
306
 
307
+ CommonJS Tailwind config:
308
+
309
+ ```js
310
+ const brandPreset = require("./src/assets/brander/adapters/tailwind.preset");
311
+
312
+ module.exports = {
313
+ presets: [brandPreset]
314
+ };
315
+ ```
316
+
317
+ This step should stay documented rather than auto-patched for now. Unlike stylesheet imports, Tailwind config shape varies across projects between CJS, ESM, TS, Vite plugins, and newer Tailwind entrypoints, so automatic mutation is riskier and easier to get wrong.
318
+
283
319
  ## Bootstrap / SCSS Frameworks
284
320
 
285
321
  Generated file:
@@ -359,6 +395,8 @@ AdvantaCode Brander is maintained under a closed governance model.
359
395
 
360
396
  Issues and feature requests are welcome, but pull requests may not be accepted.
361
397
 
398
+ Maintainer release and publishing workflow documentation is in [docs/TECH_OVERVIEW.md](docs/TECH_OVERVIEW.md).
399
+
362
400
  See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for details.
363
401
 
364
402
  ## Trademark Notice
@@ -25,6 +25,25 @@ function renderPrimitiveTokens(tokenModel, variableOptions) {
25
25
  css += ` ${getVariableName(`${colorName}-${step}`, variableOptions)}: ${scale[step]};\n`;
26
26
  }
27
27
  }
28
+ if (tokenModel.spacing && Object.keys(tokenModel.spacing).length > 0) {
29
+ css += "\n";
30
+ for (const [spaceName, spaceToken] of Object.entries(tokenModel.spacing)) {
31
+ css += ` ${getVariableName(`space-${spaceName}`, variableOptions)}: ${spaceToken.value};\n`;
32
+ }
33
+ }
34
+ if (tokenModel.typography) {
35
+ const fontSans = tokenModel.typography.fontSans?.value;
36
+ const fontMono = tokenModel.typography.fontMono?.value;
37
+ if (fontSans || fontMono) {
38
+ css += "\n";
39
+ }
40
+ if (fontSans) {
41
+ css += ` ${getVariableName("font-sans", variableOptions)}: ${fontSans};\n`;
42
+ }
43
+ if (fontMono) {
44
+ css += ` ${getVariableName("font-mono", variableOptions)}: ${fontMono};\n`;
45
+ }
46
+ }
28
47
  css += "}\n";
29
48
  return css;
30
49
  }
@@ -38,6 +38,25 @@ function renderTokensScss(tokenModel, variableOptions) {
38
38
  scss += `${getSassVariableName(`${colorName}-${step}`, variableOptions)}: ${scale[step]};\n`;
39
39
  }
40
40
  }
41
+ if (tokenModel.spacing && Object.keys(tokenModel.spacing).length > 0) {
42
+ scss += "\n";
43
+ for (const [spaceName, spaceToken] of Object.entries(tokenModel.spacing)) {
44
+ scss += `${getSassVariableName(`space-${spaceName}`, variableOptions)}: ${spaceToken.value};\n`;
45
+ }
46
+ }
47
+ if (tokenModel.typography) {
48
+ const fontSans = tokenModel.typography.fontSans?.value;
49
+ const fontMono = tokenModel.typography.fontMono?.value;
50
+ if (fontSans || fontMono) {
51
+ scss += "\n";
52
+ }
53
+ if (fontSans) {
54
+ scss += `${getSassVariableName("font-sans", variableOptions)}: ${fontSans};\n`;
55
+ }
56
+ if (fontMono) {
57
+ scss += `${getSassVariableName("font-mono", variableOptions)}: ${fontMono};\n`;
58
+ }
59
+ }
41
60
  scss += "\n";
42
61
  for (const semanticTokenName of semanticTokenNames) {
43
62
  scss += `${getSassVariableName(semanticTokenName, variableOptions)}: ${tokenModel.color.semantic.light[semanticTokenName].value};\n`;
@@ -4,15 +4,36 @@ import { semanticTokenNames } from "../engine/semantics.js";
4
4
  import { getVariableReference } from "./variables.js";
5
5
  export function writeTailwindAdapter(outputDir, tokenModel, variableOptions) {
6
6
  const adaptersDir = path.join(outputDir, "adapters");
7
- let preset = `export default {\n theme: {\n extend: {\n colors: {\n`;
7
+ let preset = `export default {\n theme: {\n extend: {\n`;
8
8
  fs.mkdirSync(adaptersDir, { recursive: true });
9
+ preset += " colors: {\n";
9
10
  for (const semanticTokenName of semanticTokenNames) {
10
11
  if (!tokenModel.color.semantic.light[semanticTokenName]) {
11
12
  continue;
12
13
  }
13
14
  preset += ` "${semanticTokenName}": "${getVariableReference(semanticTokenName, variableOptions)}",\n`;
14
15
  }
15
- preset += " }\n }\n }\n};\n";
16
+ preset += " },\n";
17
+ if (tokenModel.spacing && Object.keys(tokenModel.spacing).length > 0) {
18
+ preset += " spacing: {\n";
19
+ for (const spaceName of Object.keys(tokenModel.spacing)) {
20
+ preset += ` "${spaceName}": "${getVariableReference(`space-${spaceName}`, variableOptions)}",\n`;
21
+ }
22
+ preset += " },\n";
23
+ }
24
+ const fontSans = tokenModel.typography?.fontSans?.value;
25
+ const fontMono = tokenModel.typography?.fontMono?.value;
26
+ if (fontSans || fontMono) {
27
+ preset += " fontFamily: {\n";
28
+ if (fontSans) {
29
+ preset += ` "sans": ["${getVariableReference("font-sans", variableOptions)}"],\n`;
30
+ }
31
+ if (fontMono) {
32
+ preset += ` "mono": ["${getVariableReference("font-mono", variableOptions)}"],\n`;
33
+ }
34
+ preset += " },\n";
35
+ }
36
+ preset += " }\n }\n};\n";
16
37
  fs.writeFileSync(path.join(adaptersDir, "tailwind.preset.ts"), preset);
17
38
  return ["adapters/tailwind.preset.ts"];
18
39
  }
@@ -1,10 +1,10 @@
1
1
  import { generatePrimitivePalettes } from "./palette.js";
2
2
  import { buildThemeReferences, resolveThemeValues } from "./semantics.js";
3
- export function createTokenModel(baseColors) {
3
+ export function createTokenModel(baseColors, extras) {
4
4
  const primitivePalettes = generatePrimitivePalettes(baseColors);
5
5
  const themeReferences = buildThemeReferences(primitivePalettes);
6
6
  const themeValues = resolveThemeValues(primitivePalettes, themeReferences);
7
- return {
7
+ const tokenModel = {
8
8
  color: {
9
9
  primitive: primitivePalettes,
10
10
  semantic: {
@@ -13,6 +13,29 @@ export function createTokenModel(baseColors) {
13
13
  }
14
14
  }
15
15
  };
16
+ if (extras?.typography) {
17
+ const typography = {};
18
+ const fontSans = extras.typography.fontSans;
19
+ const fontMono = extras.typography.fontMono;
20
+ if (typeof fontSans === "string" && fontSans.trim()) {
21
+ typography.fontSans = { value: fontSans.trim() };
22
+ }
23
+ if (typeof fontMono === "string" && fontMono.trim()) {
24
+ typography.fontMono = { value: fontMono.trim() };
25
+ }
26
+ if (Object.keys(typography).length > 0) {
27
+ tokenModel.typography = typography;
28
+ }
29
+ }
30
+ if (extras?.spacing) {
31
+ const entries = Object.entries(extras.spacing).filter(([, value]) => typeof value === "string" && value.trim());
32
+ if (entries.length > 0) {
33
+ tokenModel.spacing = Object.fromEntries(entries
34
+ .sort(([left], [right]) => left.localeCompare(right))
35
+ .map(([key, value]) => [key, { value: value.trim() }]));
36
+ }
37
+ }
38
+ return tokenModel;
16
39
  }
17
40
  function mapThemeTokens(references, values) {
18
41
  return Object.fromEntries(Object.entries(references).map(([tokenName, reference]) => [
@@ -11,6 +11,7 @@ import { writeTypeScriptArtifacts } from "./adapters/typescript.js";
11
11
  import { normalizeVariablePrefix } from "./adapters/variables.js";
12
12
  import { baseColorNames, resolveBaseColors } from "./engine/color-parser.js";
13
13
  import { createTokenModel } from "./engine/themes.js";
14
+ import { syncStyleImports } from "./style-imports.js";
14
15
  export const supportedFormats = [
15
16
  "all",
16
17
  "css",
@@ -22,15 +23,18 @@ export const supportedFormats = [
22
23
  "figma"
23
24
  ];
24
25
  export async function generateTokens(options = {}) {
25
- const defaultOutputDir = resolveDefaultOutputDir();
26
- const outputDir = options.outputDir ? path.resolve(process.cwd(), options.outputDir) : defaultOutputDir;
27
- const theme = options.theme ?? "both";
28
- const formats = resolveFormats(options.formats);
29
26
  loadDotEnv({ path: path.resolve(process.cwd(), ".env"), quiet: true });
30
27
  const brandConfig = await loadBrandConfig();
28
+ const defaultOutputDir = resolveDefaultOutputDir();
29
+ const outputDir = resolveOutputDir(options.outputDir, brandConfig.project?.outDir, defaultOutputDir);
30
+ const theme = options.theme ?? brandConfig.theme ?? "both";
31
+ const formats = resolveFormats(options.formats ?? brandConfig.formats ?? resolveFormatsFromAdapters(brandConfig.adapters));
31
32
  const prefix = resolveCssPrefix(options.prefix, brandConfig.css?.prefix, process.env.CSS_PREFIX);
32
33
  const baseColors = resolveBaseColors(brandConfig.colors ?? {});
33
- const tokenModel = createTokenModel(baseColors);
34
+ const tokenModel = createTokenModel(baseColors, {
35
+ typography: resolveTypographyConfig(brandConfig.typography),
36
+ spacing: brandConfig.spacing
37
+ });
34
38
  fs.mkdirSync(outputDir, { recursive: true });
35
39
  if (outputDir === defaultOutputDir) {
36
40
  removeLegacyGeneratedFiles();
@@ -69,6 +73,10 @@ export async function generateTokens(options = {}) {
69
73
  writtenArtifacts.push(...writeTypeScriptArtifacts(outputDir, tokenModel, metadata));
70
74
  }
71
75
  writtenArtifacts.push(...writeMetadataJson(outputDir, metadata));
76
+ const stylePath = options.stylePath ?? brandConfig.project?.styleFile;
77
+ if (stylePath && formats.has("css")) {
78
+ syncStyleImports(stylePath, outputDir, theme);
79
+ }
72
80
  console.log(`✔ AdvantaCode tokens generated in ${path.relative(process.cwd(), outputDir) || "."}!`);
73
81
  }
74
82
  async function loadBrandConfig() {
@@ -117,6 +125,67 @@ function parseBrandConfig(rawConfig, configPath) {
117
125
  }
118
126
  parsedConfig.name = config.name;
119
127
  }
128
+ if ("project" in config && config.project !== undefined) {
129
+ if (typeof config.project !== "object" || config.project === null || Array.isArray(config.project)) {
130
+ throw new Error(`Expected "project" in ${path.basename(configPath)} to be an object.`);
131
+ }
132
+ const projectConfig = config.project;
133
+ const parsedProjectConfig = {};
134
+ if ("outDir" in projectConfig && projectConfig.outDir !== undefined) {
135
+ if (typeof projectConfig.outDir !== "string") {
136
+ throw new Error(`Expected "project.outDir" in ${path.basename(configPath)} to be a string.`);
137
+ }
138
+ parsedProjectConfig.outDir = projectConfig.outDir;
139
+ }
140
+ if ("styleFile" in projectConfig && projectConfig.styleFile !== undefined) {
141
+ if (typeof projectConfig.styleFile !== "string") {
142
+ throw new Error(`Expected "project.styleFile" in ${path.basename(configPath)} to be a string.`);
143
+ }
144
+ parsedProjectConfig.styleFile = projectConfig.styleFile;
145
+ }
146
+ parsedConfig.project = parsedProjectConfig;
147
+ }
148
+ if ("adapters" in config && config.adapters !== undefined) {
149
+ if (!Array.isArray(config.adapters)) {
150
+ throw new Error(`Expected "adapters" in ${path.basename(configPath)} to be an array.`);
151
+ }
152
+ const parsedAdapters = [];
153
+ for (const adapter of config.adapters) {
154
+ if (typeof adapter !== "string") {
155
+ throw new Error(`Expected "adapters" entries in ${path.basename(configPath)} to be strings.`);
156
+ }
157
+ if (!["tailwind", "bootstrap", "figma"].includes(adapter)) {
158
+ throw new Error(`Unsupported adapter "${adapter}" in ${path.basename(configPath)}.`);
159
+ }
160
+ parsedAdapters.push(adapter);
161
+ }
162
+ parsedConfig.adapters = parsedAdapters;
163
+ }
164
+ if ("formats" in config && config.formats !== undefined) {
165
+ if (!Array.isArray(config.formats)) {
166
+ throw new Error(`Expected "formats" in ${path.basename(configPath)} to be an array.`);
167
+ }
168
+ const parsedFormats = [];
169
+ for (const format of config.formats) {
170
+ if (typeof format !== "string") {
171
+ throw new Error(`Expected "formats" entries in ${path.basename(configPath)} to be strings.`);
172
+ }
173
+ if (!supportedFormats.includes(format)) {
174
+ throw new Error(`Unknown format "${format}" in ${path.basename(configPath)}.`);
175
+ }
176
+ parsedFormats.push(format);
177
+ }
178
+ parsedConfig.formats = parsedFormats;
179
+ }
180
+ if ("theme" in config && config.theme !== undefined) {
181
+ if (typeof config.theme !== "string") {
182
+ throw new Error(`Expected "theme" in ${path.basename(configPath)} to be a string.`);
183
+ }
184
+ if (!["light", "dark", "both"].includes(config.theme)) {
185
+ throw new Error(`Invalid "theme" in ${path.basename(configPath)}. Use "light", "dark", or "both".`);
186
+ }
187
+ parsedConfig.theme = config.theme;
188
+ }
120
189
  if ("colors" in config && config.colors !== undefined) {
121
190
  if (typeof config.colors !== "object" || config.colors === null || Array.isArray(config.colors)) {
122
191
  throw new Error(`Expected "colors" in ${path.basename(configPath)} to be an object.`);
@@ -148,8 +217,92 @@ function parseBrandConfig(rawConfig, configPath) {
148
217
  }
149
218
  parsedConfig.css = parsedCssConfig;
150
219
  }
220
+ if ("typography" in config && config.typography !== undefined) {
221
+ if (typeof config.typography !== "object" || config.typography === null || Array.isArray(config.typography)) {
222
+ throw new Error(`Expected "typography" in ${path.basename(configPath)} to be an object.`);
223
+ }
224
+ const typographyConfig = config.typography;
225
+ const parsedTypographyConfig = {};
226
+ if ("fontSans" in typographyConfig && typographyConfig.fontSans !== undefined) {
227
+ if (typeof typographyConfig.fontSans !== "string") {
228
+ throw new Error(`Expected "typography.fontSans" in ${path.basename(configPath)} to be a string.`);
229
+ }
230
+ parsedTypographyConfig.fontSans = typographyConfig.fontSans;
231
+ }
232
+ if ("fontMono" in typographyConfig && typographyConfig.fontMono !== undefined) {
233
+ if (typeof typographyConfig.fontMono !== "string") {
234
+ throw new Error(`Expected "typography.fontMono" in ${path.basename(configPath)} to be a string.`);
235
+ }
236
+ parsedTypographyConfig.fontMono = typographyConfig.fontMono;
237
+ }
238
+ parsedConfig.typography = parsedTypographyConfig;
239
+ }
240
+ if ("spacing" in config && config.spacing !== undefined) {
241
+ if (typeof config.spacing !== "object" || config.spacing === null || Array.isArray(config.spacing)) {
242
+ throw new Error(`Expected "spacing" in ${path.basename(configPath)} to be an object.`);
243
+ }
244
+ const spacingEntries = Object.entries(config.spacing);
245
+ const parsedSpacing = {};
246
+ for (const [spaceName, spaceValue] of spacingEntries) {
247
+ if (!isSafeTokenKey(spaceName)) {
248
+ throw new Error(`Unsupported spacing token "${spaceName}" in ${path.basename(configPath)}. Use letters, numbers, ".", "_", or "-".`);
249
+ }
250
+ if (typeof spaceValue !== "string") {
251
+ throw new Error(`Expected spacing "${spaceName}" in ${path.basename(configPath)} to be a string.`);
252
+ }
253
+ parsedSpacing[spaceName] = spaceValue;
254
+ }
255
+ parsedConfig.spacing = parsedSpacing;
256
+ }
151
257
  return parsedConfig;
152
258
  }
259
+ function isSafeTokenKey(value) {
260
+ return /^[A-Za-z0-9._-]+$/.test(value);
261
+ }
262
+ const genericFontFamilyKeywords = new Set([
263
+ "serif",
264
+ "sans-serif",
265
+ "monospace",
266
+ "cursive",
267
+ "fantasy",
268
+ "system-ui",
269
+ "ui-serif",
270
+ "ui-sans-serif",
271
+ "ui-monospace",
272
+ "ui-rounded",
273
+ "emoji",
274
+ "math",
275
+ "fangsong"
276
+ ]);
277
+ function resolveTypographyConfig(config) {
278
+ if (!config) {
279
+ return undefined;
280
+ }
281
+ const resolved = {};
282
+ if (typeof config.fontSans === "string" && config.fontSans.trim()) {
283
+ resolved.fontSans = normalizeFontStack(config.fontSans, "sans-serif");
284
+ }
285
+ if (typeof config.fontMono === "string" && config.fontMono.trim()) {
286
+ resolved.fontMono = normalizeFontStack(config.fontMono, "monospace");
287
+ }
288
+ return Object.keys(resolved).length > 0 ? resolved : undefined;
289
+ }
290
+ function normalizeFontStack(fontValue, fallback) {
291
+ const trimmed = fontValue.trim();
292
+ if (!trimmed) {
293
+ return trimmed;
294
+ }
295
+ if (trimmed.includes(",")) {
296
+ return trimmed;
297
+ }
298
+ if (genericFontFamilyKeywords.has(trimmed)) {
299
+ return trimmed;
300
+ }
301
+ return `"${escapeCssString(trimmed)}", ${fallback}`;
302
+ }
303
+ function escapeCssString(value) {
304
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
305
+ }
153
306
  function resolveFormats(formats) {
154
307
  const resolvedFormats = new Set();
155
308
  if (!formats || formats.length === 0 || formats.includes("all")) {
@@ -170,6 +323,16 @@ function resolveFormats(formats) {
170
323
  }
171
324
  return resolvedFormats;
172
325
  }
326
+ function resolveFormatsFromAdapters(adapters) {
327
+ if (!adapters || adapters.length === 0) {
328
+ return undefined;
329
+ }
330
+ return ["css", ...adapters];
331
+ }
332
+ function resolveOutputDir(cliOutDir, configOutDir, defaultOutputDir) {
333
+ const resolvedOutDir = cliOutDir ?? configOutDir;
334
+ return resolvedOutDir ? path.resolve(process.cwd(), resolvedOutDir) : defaultOutputDir;
335
+ }
173
336
  function removeLegacyGeneratedFiles() {
174
337
  const legacyFiles = [
175
338
  path.resolve(process.cwd(), "dist", "tokens.css"),
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ export async function runCli(args) {
14
14
  return 0;
15
15
  }
16
16
  if (command === 'generate') {
17
- await generateTokens(parseGenerateArgs(commandArgs));
17
+ await runGenerateCommand(parseGenerateArgs(commandArgs));
18
18
  return 0;
19
19
  }
20
20
  await setupProject(parseSetupArgs(command, commandArgs));
@@ -44,6 +44,9 @@ function resolveCommand(args) {
44
44
  }
45
45
  throw new Error(`Unknown command "${firstArg}". Use --help to see supported commands.`);
46
46
  }
47
+ async function runGenerateCommand(options) {
48
+ await generateTokens(options);
49
+ }
47
50
  function parseGenerateArgs(args) {
48
51
  const options = {};
49
52
  for (let index = 0; index < args.length; index += 1) {
@@ -74,6 +77,11 @@ function parseGenerateArgs(args) {
74
77
  index += 1;
75
78
  continue;
76
79
  }
80
+ if (arg === "--style") {
81
+ options.stylePath = getNextArgValue(arg, args, index);
82
+ index += 1;
83
+ continue;
84
+ }
77
85
  if (arg.startsWith("-")) {
78
86
  throw new Error(`Unknown option "${arg}". Use --help to see supported flags.`);
79
87
  }
@@ -161,11 +169,11 @@ Generation options:
161
169
  --theme <value> Theme CSS output: light, dark, or both (default: both)
162
170
  --prefix <value> CSS variable prefix. Use "" or omit for no prefix
163
171
 
164
- Examples:
165
- advantacode-brander ${command}
166
- advantacode-brander ${command} --out src/brander
167
- advantacode-brander ${command} --style src/style.css
168
- advantacode-brander ${command} --skip-imports --skip-generate
172
+ Examples:
173
+ advantacode-brander ${command}
174
+ advantacode-brander ${command} --out src/brander
175
+ advantacode-brander ${command} --style src/style.css
176
+ advantacode-brander ${command} --skip-imports --skip-generate
169
177
  `;
170
178
  }
171
179
  return `AdvantaCode Brander
@@ -186,11 +194,13 @@ Options:
186
194
  --format <list> Comma-separated formats: all, css, json, typescript|ts, scss, tailwind, bootstrap, figma
187
195
  --theme <value> Theme CSS output: light, dark, or both (default: both)
188
196
  --prefix <value> CSS variable prefix. Use "" or omit for no prefix
197
+ --style <path> Main stylesheet file to patch with a brand.css import
189
198
 
190
- Examples:
191
- advantacode-brander
192
- advantacode-brander --out src/tokens
193
- advantacode-brander setup --out src/brander --style src/style.css
194
- advantacode-brander init --out resources/brander --skip-imports
199
+ Examples:
200
+ advantacode-brander
201
+ advantacode-brander --out src/tokens
202
+ advantacode-brander --style src/style.css
203
+ advantacode-brander setup --style src/style.css
204
+ advantacode-brander init --out resources/brander --skip-imports
195
205
  `;
196
206
  }
package/dist/setup.js CHANGED
@@ -1,32 +1,33 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { generateTokens } from "./generate-tokens.js";
4
+ import { ensureStyleImports, resolveStylePath, syncStyleImports as syncStyleImportsImpl } from "./style-imports.js";
4
5
  const defaultSetupOutputDir = path.join("src", "brander");
5
- const brandStylesheetFileName = "brand.css";
6
- const defaultStyleCandidates = [
7
- path.join("src", "style.css"),
8
- path.join("src", "main.css"),
9
- path.join("src", "index.css"),
10
- path.join("src", "app.css"),
11
- path.join("resources", "css", "app.css")
12
- ];
13
6
  export async function setupProject(options) {
14
7
  const resolvedOutputDir = options.outputDir ?? defaultSetupOutputDir;
15
8
  const scriptName = options.scriptName ?? "brand:generate";
16
9
  const notes = [];
10
+ const resolvedStylePath = !options.skipImports ? resolveStylePath(options.stylePath) : undefined;
11
+ const configFormats = options.formats && options.formats.length > 0 ? options.formats : undefined;
17
12
  if (!options.skipConfig) {
18
- const configResult = ensureBrandConfig();
13
+ const configResult = ensureBrandConfig({
14
+ outputDir: resolvedOutputDir,
15
+ styleFile: resolvedStylePath ? normalizeConfigPath(path.relative(process.cwd(), resolvedStylePath)) : undefined,
16
+ formats: configFormats,
17
+ adapters: !configFormats ? ["tailwind"] : undefined,
18
+ prefix: options.prefix,
19
+ theme: options.theme
20
+ });
19
21
  notes.push(configResult.message);
20
22
  }
21
23
  if (!options.skipScript) {
22
24
  const scriptResult = ensurePackageScript(scriptName, buildGenerateCommand({
23
- ...options,
24
- outputDir: resolvedOutputDir
25
+ ...options
25
26
  }));
26
27
  notes.push(scriptResult.message);
27
28
  }
28
29
  if (!options.skipImports) {
29
- const styleResult = ensureStyleImports(options.stylePath, resolvedOutputDir);
30
+ const styleResult = ensureStyleImports(resolvedStylePath ? normalizeConfigPath(path.relative(process.cwd(), resolvedStylePath)) : undefined, resolvedOutputDir, options.theme ?? "both");
30
31
  notes.push(styleResult.message);
31
32
  }
32
33
  if (!options.skipGenerate) {
@@ -43,13 +44,8 @@ export async function setupProject(options) {
43
44
  console.log(` - ${note}`);
44
45
  }
45
46
  }
46
- function ensureBrandConfig() {
47
- const configPath = path.resolve(process.cwd(), "brand.config.js");
48
- if (fs.existsSync(configPath)) {
49
- return { message: "Kept existing brand.config.js." };
50
- }
51
- fs.writeFileSync(configPath, getDefaultBrandConfigTemplate());
52
- return { message: "Created brand.config.js." };
47
+ export function syncStyleImports(stylePath, outputDir) {
48
+ return syncStyleImportsImpl(stylePath, outputDir);
53
49
  }
54
50
  function ensurePackageScript(scriptName, command) {
55
51
  const packageJsonPath = path.resolve(process.cwd(), "package.json");
@@ -71,114 +67,79 @@ function ensurePackageScript(scriptName, command) {
71
67
  fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
72
68
  return { message: `Added "${scriptName}" script to package.json.` };
73
69
  }
74
- function ensureStyleImports(stylePath, outputDir) {
75
- const resolvedStylePath = resolveStylePath(stylePath);
76
- if (!resolvedStylePath) {
77
- return {
78
- message: "Skipped stylesheet imports because no stylesheet was found. Use --style <path> to target a file explicitly."
79
- };
80
- }
81
- const brandStylesheetPath = path.join(path.dirname(resolvedStylePath), brandStylesheetFileName);
82
- const brandStylesheetImports = [
83
- buildImportLine(brandStylesheetPath, path.join(outputDir, "tokens.css")),
84
- buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "light.css")),
85
- buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "dark.css"))
86
- ];
87
- const brandStylesheetContents = `${brandStylesheetImports.join("\n")}\n`;
88
- const hasBrandStylesheet = fs.existsSync(brandStylesheetPath);
89
- const existingBrandStylesheet = hasBrandStylesheet ? fs.readFileSync(brandStylesheetPath, "utf8") : "";
90
- if (existingBrandStylesheet !== brandStylesheetContents) {
91
- fs.writeFileSync(brandStylesheetPath, brandStylesheetContents);
92
- }
93
- const styleFileContents = fs.readFileSync(resolvedStylePath, "utf8");
94
- const legacyTokenImports = [
95
- buildImportLine(resolvedStylePath, path.join(outputDir, "tokens.css")),
96
- buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "light.css")),
97
- buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "dark.css"))
98
- ];
99
- const brandImportLine = buildImportLine(resolvedStylePath, brandStylesheetPath);
100
- const styleLineEnding = styleFileContents.includes("\r\n") ? "\r\n" : "\n";
101
- const styleLines = styleFileContents.split(/\r?\n/);
102
- const legacyImportCandidates = new Set();
103
- for (const importLine of legacyTokenImports) {
104
- legacyImportCandidates.add(importLine);
105
- legacyImportCandidates.add(importLine.replace(/'/g, '"'));
106
- }
107
- let nextStyleLines = styleLines.filter((line) => !legacyImportCandidates.has(line.trim()));
108
- const hasBrandImport = nextStyleLines.some((line) => line.trim() === brandImportLine || line.trim() === brandImportLine.replace(/'/g, '"'));
109
- if (!hasBrandImport) {
110
- nextStyleLines = [brandImportLine, ...nextStyleLines];
111
- }
112
- while (nextStyleLines[0] === "") {
113
- nextStyleLines = nextStyleLines.slice(1);
114
- }
115
- const nextStyleContents = `${nextStyleLines.join(styleLineEnding)}${styleLineEnding}`;
116
- if (nextStyleContents !== styleFileContents) {
117
- fs.writeFileSync(resolvedStylePath, nextStyleContents);
118
- }
119
- const brandStylesheetStatus = hasBrandStylesheet ? "Updated" : "Created";
120
- const mainStylesheetStatus = nextStyleContents === styleFileContents ? "Kept" : "Updated";
121
- return {
122
- message: `${brandStylesheetStatus} ${path.relative(process.cwd(), brandStylesheetPath)} and ${mainStylesheetStatus.toLowerCase()} ${path.relative(process.cwd(), resolvedStylePath)} to import it.`
123
- };
70
+ function normalizeConfigPath(value) {
71
+ return value.replace(/\\/g, "/");
124
72
  }
125
- function resolveStylePath(stylePath) {
126
- if (stylePath) {
127
- const candidatePath = path.resolve(process.cwd(), stylePath);
128
- if (!fs.existsSync(candidatePath)) {
129
- throw new Error(`Unable to find stylesheet "${stylePath}".`);
130
- }
131
- return candidatePath;
132
- }
133
- const existingCandidates = defaultStyleCandidates
134
- .map((candidate) => path.resolve(process.cwd(), candidate))
135
- .filter((candidatePath) => fs.existsSync(candidatePath));
136
- if (existingCandidates.length === 1) {
137
- return existingCandidates[0];
138
- }
139
- if (existingCandidates.length > 1) {
140
- throw new Error(`Multiple stylesheet candidates were found: ${existingCandidates
141
- .map((candidatePath) => path.relative(process.cwd(), candidatePath))
142
- .join(", ")}. Use --style <path> to choose one.`);
143
- }
144
- return undefined;
73
+ function buildGenerateCommand(options) {
74
+ void options;
75
+ return "advantacode-brander";
145
76
  }
146
- function buildImportLine(stylePath, targetPath) {
147
- const relativeImportPath = path.relative(path.dirname(stylePath), path.resolve(process.cwd(), targetPath)).replace(/\\/g, "/");
148
- const normalizedImportPath = relativeImportPath.startsWith(".") ? relativeImportPath : `./${relativeImportPath}`;
149
- return `@import '${normalizedImportPath}';`;
77
+ function ensureBrandConfig(options) {
78
+ const existingConfigPath = findExistingBrandConfigPath();
79
+ if (existingConfigPath) {
80
+ return { message: `Kept existing ${path.basename(existingConfigPath)}.` };
81
+ }
82
+ const configPath = path.resolve(process.cwd(), "brand.config.ts");
83
+ fs.writeFileSync(configPath, getDefaultBrandConfigTemplate(options));
84
+ return { message: `Created ${path.basename(configPath)}.` };
150
85
  }
151
- function buildGenerateCommand(options) {
152
- const commandParts = ["advantacode-brander", "generate"];
153
- const outputDir = options.outputDir ?? defaultSetupOutputDir;
154
- commandParts.push("--out", outputDir);
155
- if (options.formats && options.formats.length > 0) {
156
- commandParts.push("--format", options.formats.join(","));
157
- }
158
- if (options.theme && options.theme !== "both") {
159
- commandParts.push("--theme", options.theme);
160
- }
161
- if (options.prefix) {
162
- commandParts.push("--prefix", options.prefix);
86
+ function findExistingBrandConfigPath() {
87
+ const candidateFiles = [
88
+ "brand.config.ts",
89
+ "brand.config.mts",
90
+ "brand.config.cts",
91
+ "brand.config.js",
92
+ "brand.config.mjs",
93
+ "brand.config.cjs"
94
+ ];
95
+ for (const candidateFile of candidateFiles) {
96
+ const candidatePath = path.resolve(process.cwd(), candidateFile);
97
+ if (fs.existsSync(candidatePath)) {
98
+ return candidatePath;
99
+ }
163
100
  }
164
- return commandParts.join(" ");
101
+ return undefined;
165
102
  }
166
- function getDefaultBrandConfigTemplate() {
167
- return `export default {
168
- name: process.env.COMPANY_NAME || "My Company",
169
- css: {
170
- prefix: process.env.CSS_PREFIX ?? ""
171
- },
172
- colors: {
173
- primary: process.env.PRIMARY_COLOR || "amber-500",
174
- secondary: process.env.SECONDARY_COLOR || "zinc-700",
175
- neutral: process.env.NEUTRAL_COLOR || process.env.SECONDARY_COLOR || "zinc-700",
176
- accent: process.env.ACCENT_COLOR || "amber-400",
177
- info: process.env.INFO_COLOR || "sky-500",
178
- success: process.env.SUCCESS_COLOR || "green-500",
179
- warning: process.env.WARNING_COLOR || "yellow-500",
180
- danger: process.env.DANGER_COLOR || "red-500"
181
- }
182
- };
103
+ function getDefaultBrandConfigTemplate(options) {
104
+ const outDir = options?.outputDir ?? defaultSetupOutputDir;
105
+ const adapters = options?.adapters && options.adapters.length > 0 ? options.adapters : undefined;
106
+ const formats = options?.formats && options.formats.length > 0 ? options.formats : undefined;
107
+ const prefix = options?.prefix ? JSON.stringify(options.prefix) : "process.env.CSS_PREFIX ?? \"\"";
108
+ const themeLine = options?.theme && options.theme !== "both" ? ` theme: ${JSON.stringify(options.theme)},\n` : "";
109
+ const projectLines = [
110
+ ` project: {`,
111
+ ` outDir: ${JSON.stringify(outDir)},`,
112
+ ...(options?.styleFile ? [` styleFile: ${JSON.stringify(options.styleFile)},`] : []),
113
+ ` },`
114
+ ].join("\n");
115
+ return `export default {
116
+ name: process.env.COMPANY_NAME || "My Company",
117
+ ${projectLines}
118
+
119
+ ${themeLine}${formats ? ` formats: ${JSON.stringify(formats)},\n` : ""}${adapters ? ` adapters: ${JSON.stringify(adapters)},\n` : ""} css: {
120
+ prefix: ${prefix}
121
+ },
122
+ colors: {
123
+ primary: process.env.PRIMARY_COLOR || "amber-500",
124
+ secondary: process.env.SECONDARY_COLOR || "zinc-700",
125
+ neutral: process.env.NEUTRAL_COLOR || process.env.SECONDARY_COLOR || "zinc-700",
126
+ accent: process.env.ACCENT_COLOR || "amber-400",
127
+ info: process.env.INFO_COLOR || "sky-500",
128
+ success: process.env.SUCCESS_COLOR || "green-500",
129
+ warning: process.env.WARNING_COLOR || "yellow-500",
130
+ danger: process.env.DANGER_COLOR || "red-500"
131
+ },
132
+ typography: {
133
+ fontSans: "Inter",
134
+ fontMono: "JetBrains Mono"
135
+ },
136
+ spacing: {
137
+ xs: "0.25rem",
138
+ sm: "0.5rem",
139
+ md: "1rem",
140
+ lg: "1.5rem",
141
+ xl: "2rem"
142
+ }
143
+ };
183
144
  `;
184
145
  }
@@ -0,0 +1,94 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ const brandStylesheetFileName = "brand.css";
4
+ const defaultStyleCandidates = [
5
+ path.join("src", "style.css"),
6
+ path.join("src", "main.css"),
7
+ path.join("src", "index.css"),
8
+ path.join("src", "app.css"),
9
+ path.join("resources", "css", "app.css")
10
+ ];
11
+ export function syncStyleImports(stylePath, outputDir, theme = "both") {
12
+ return ensureStyleImports(stylePath, outputDir, theme);
13
+ }
14
+ export function ensureStyleImports(stylePath, outputDir, theme = "both") {
15
+ const resolvedStylePath = resolveStylePath(stylePath);
16
+ if (!resolvedStylePath) {
17
+ return {
18
+ message: "Skipped stylesheet imports because no stylesheet was found. Use --style <path> to target a file explicitly."
19
+ };
20
+ }
21
+ const brandStylesheetPath = path.join(path.dirname(resolvedStylePath), brandStylesheetFileName);
22
+ const brandStylesheetImports = [buildImportLine(brandStylesheetPath, path.join(outputDir, "tokens.css"))];
23
+ if (theme === "light" || theme === "both") {
24
+ brandStylesheetImports.push(buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "light.css")));
25
+ }
26
+ if (theme === "dark" || theme === "both") {
27
+ brandStylesheetImports.push(buildImportLine(brandStylesheetPath, path.join(outputDir, "themes", "dark.css")));
28
+ }
29
+ const brandStylesheetContents = `${brandStylesheetImports.join("\n")}\n`;
30
+ const hasBrandStylesheet = fs.existsSync(brandStylesheetPath);
31
+ const existingBrandStylesheet = hasBrandStylesheet ? fs.readFileSync(brandStylesheetPath, "utf8") : "";
32
+ if (existingBrandStylesheet !== brandStylesheetContents) {
33
+ fs.writeFileSync(brandStylesheetPath, brandStylesheetContents);
34
+ }
35
+ const styleFileContents = fs.readFileSync(resolvedStylePath, "utf8");
36
+ const legacyTokenImports = [
37
+ buildImportLine(resolvedStylePath, path.join(outputDir, "tokens.css")),
38
+ buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "light.css")),
39
+ buildImportLine(resolvedStylePath, path.join(outputDir, "themes", "dark.css"))
40
+ ];
41
+ const brandImportLine = buildImportLine(resolvedStylePath, brandStylesheetPath);
42
+ const styleLineEnding = styleFileContents.includes("\r\n") ? "\r\n" : "\n";
43
+ const styleLines = styleFileContents.split(/\r?\n/);
44
+ const legacyImportCandidates = new Set();
45
+ for (const importLine of legacyTokenImports) {
46
+ legacyImportCandidates.add(importLine);
47
+ legacyImportCandidates.add(importLine.replace(/'/g, '"'));
48
+ }
49
+ let nextStyleLines = styleLines.filter((line) => !legacyImportCandidates.has(line.trim()));
50
+ const hasBrandImport = nextStyleLines.some((line) => line.trim() === brandImportLine || line.trim() === brandImportLine.replace(/'/g, '"'));
51
+ if (!hasBrandImport) {
52
+ nextStyleLines = [brandImportLine, ...nextStyleLines];
53
+ }
54
+ while (nextStyleLines[0] === "") {
55
+ nextStyleLines = nextStyleLines.slice(1);
56
+ }
57
+ const nextStyleContents = `${nextStyleLines.join(styleLineEnding)}${styleLineEnding}`;
58
+ if (nextStyleContents !== styleFileContents) {
59
+ fs.writeFileSync(resolvedStylePath, nextStyleContents);
60
+ }
61
+ const brandStylesheetStatus = hasBrandStylesheet ? "Updated" : "Created";
62
+ const mainStylesheetStatus = nextStyleContents === styleFileContents ? "Kept" : "Updated";
63
+ return {
64
+ message: `${brandStylesheetStatus} ${path.relative(process.cwd(), brandStylesheetPath)} and ${mainStylesheetStatus.toLowerCase()} ${path.relative(process.cwd(), resolvedStylePath)} to import it.`
65
+ };
66
+ }
67
+ export function resolveStylePath(stylePath) {
68
+ if (stylePath) {
69
+ const candidatePath = path.resolve(process.cwd(), stylePath);
70
+ if (!fs.existsSync(candidatePath)) {
71
+ throw new Error(`Unable to find stylesheet "${stylePath}".`);
72
+ }
73
+ return candidatePath;
74
+ }
75
+ const existingCandidates = defaultStyleCandidates
76
+ .map((candidate) => path.resolve(process.cwd(), candidate))
77
+ .filter((candidatePath) => fs.existsSync(candidatePath));
78
+ if (existingCandidates.length === 1) {
79
+ return existingCandidates[0];
80
+ }
81
+ if (existingCandidates.length > 1) {
82
+ throw new Error(`Multiple stylesheet candidates were found: ${existingCandidates
83
+ .map((candidatePath) => path.relative(process.cwd(), candidatePath))
84
+ .join(", ")}. Use --style <path> to choose one.`);
85
+ }
86
+ return undefined;
87
+ }
88
+ function buildImportLine(stylePath, targetPath) {
89
+ const relativeImportPath = path
90
+ .relative(path.dirname(stylePath), path.resolve(process.cwd(), targetPath))
91
+ .replace(/\\/g, "/");
92
+ const normalizedImportPath = relativeImportPath.startsWith(".") ? relativeImportPath : `./${relativeImportPath}`;
93
+ return `@import '${normalizedImportPath}';`;
94
+ }
@@ -34,6 +34,10 @@ For internal architecture and technical details, see:
34
34
 
35
35
  [TECH_OVERVIEW.md](TECH_OVERVIEW.md)
36
36
 
37
+ ## Changelog
38
+
39
+ User-facing changes should be recorded in `CHANGELOG.md` under the `[Unreleased]` section.
40
+
37
41
  ## Branding and Trademarks
38
42
 
39
43
  Use of the AdvantaCode name, logos, package names, domains, and other brand identifiers is governed by the trademark policy.
@@ -308,12 +308,18 @@ Type definitions for Culori.
308
308
  brand.config.ts
309
309
  ```
310
310
 
311
- Defines the user-provided color inputs.
311
+ Defines the user-provided project settings and design tokens.
312
+ Also supports `project` output settings, optional `adapters` / `formats`, and optional `typography` (fonts) + `spacing` scales.
312
313
 
313
314
  Example:
314
315
 
315
316
  ```ts
316
317
  export default {
318
+ project: {
319
+ outDir: "src/assets/brand",
320
+ styleFile: "src/styles.css"
321
+ },
322
+ adapters: ["tailwind"],
317
323
  css: {
318
324
  prefix: ""
319
325
  },
@@ -321,6 +327,17 @@ export default {
321
327
  primary: "amber-500",
322
328
  secondary: "zinc-700",
323
329
  info: "sky-500"
330
+ },
331
+ typography: {
332
+ fontSans: "Inter",
333
+ fontMono: "JetBrains Mono"
334
+ },
335
+ spacing: {
336
+ xs: "0.25rem",
337
+ sm: "0.5rem",
338
+ md: "1rem",
339
+ lg: "1.5rem",
340
+ xl: "2rem"
324
341
  }
325
342
  };
326
343
  ```
@@ -452,9 +469,25 @@ Example:
452
469
  export default {
453
470
  name: "My Company",
454
471
 
472
+ project: {
473
+ outDir: "src/assets/brand",
474
+ styleFile: "src/styles.css"
475
+ },
476
+ adapters: ["tailwind"],
455
477
  colors: {
456
478
  primary: "amber-500",
457
479
  secondary: "zinc-700"
480
+ },
481
+ typography: {
482
+ fontSans: "Inter",
483
+ fontMono: "JetBrains Mono"
484
+ },
485
+ spacing: {
486
+ xs: "0.25rem",
487
+ sm: "0.5rem",
488
+ md: "1rem",
489
+ lg: "1.5rem",
490
+ xl: "2rem"
458
491
  }
459
492
  }
460
493
  ```
@@ -520,6 +553,27 @@ The current test coverage includes:
520
553
 
521
554
  ---
522
555
 
556
+ # Release and Publishing
557
+
558
+ For v1.0.0, keep release mechanics intentionally simple and rely on a short manual checklist.
559
+
560
+ Preflight checks:
561
+
562
+ ```
563
+ npm run release:check
564
+ ```
565
+
566
+ Recommended flow:
567
+
568
+ * update `CHANGELOG.md`
569
+ * run `npm run release:check`
570
+ * bump version with `npm version patch|minor|major`
571
+ * cut the changelog section with `npm run changelog:cut` (uses the current `package.json` version)
572
+ * publish with `npm publish`
573
+ * push the commit + tag
574
+
575
+ ---
576
+
523
577
  # Run Token Generator
524
578
 
525
579
  For local repository development, you can execute the generator directly with:
@@ -554,6 +608,40 @@ Why `devDependency`:
554
608
  * Brander is a build-time code generation tool
555
609
  * the consuming app needs the generated token files at runtime, not the generator itself
556
610
 
611
+ ## Compatibility Test Matrix (v1.0.0)
612
+
613
+ Keep v1.0.0 validation lean and focus on the integration points Brander actually owns:
614
+
615
+ * config loading (`brand.config.ts` / `brand.config.js`)
616
+ * writing outputs to `project.outDir`
617
+ * CSS variable/theme correctness
618
+ * `brand.css` wiring via `project.styleFile` (when applicable)
619
+ * Tailwind preset adapter import + usage
620
+
621
+ **Minimal matrix (recommended for v1):**
622
+
623
+ 1. Plain Vite + Tailwind (framework-agnostic baseline)
624
+ 2. Next.js + Tailwind (SSR + App Router + CSS import realities)
625
+ 3. Nuxt + Tailwind (Vue SSR ecosystem)
626
+ 4. One Vite SPA framework: Vue + Tailwind *or* React + Tailwind (framework independence)
627
+
628
+ **Optional (adds credibility, not required for v1):**
629
+
630
+ * Astro + Tailwind (marketing sites)
631
+ * Laravel + Vite + Tailwind (monolith stack)
632
+ * SvelteKit + Tailwind (non-React/Vue framework)
633
+
634
+ **Notes:**
635
+
636
+ * For Next.js/Nuxt/Astro you may prefer to skip `setup` import patching and wire `brand.css` manually in the framework’s global stylesheet entrypoint; still validate `generate` and token consumption.
637
+ * Vite + Next.js covers the majority of real-world build tooling for Brander’s outputs.
638
+
639
+ Automation (optional):
640
+
641
+ * GitHub Actions workflow: `.github/workflows/consumer-matrix.yml`
642
+ * Script used by the workflow: `scripts/consumer-smoke.sh`
643
+ * The workflow packs the current commit (`npm pack`) and installs Brander from that tarball in each consumer project to avoid npm-registry drift.
644
+
557
645
  Build and pack Brander:
558
646
 
559
647
  ```
@@ -573,7 +661,7 @@ Add a script to the app:
573
661
  ```json
574
662
  {
575
663
  "scripts": {
576
- "brand:generate": "advantacode-brander --out src/brander --format css,json,typescript --theme both"
664
+ "brand:generate": "advantacode-brander"
577
665
  }
578
666
  }
579
667
  ```
@@ -581,7 +669,7 @@ Add a script to the app:
581
669
  Or let Brander do that setup explicitly:
582
670
 
583
671
  ```
584
- npx --package @advantacode/brander advantacode-brander setup --out src/brander --style src/style.css
672
+ npx --package @advantacode/brander advantacode-brander setup --style src/style.css
585
673
  ```
586
674
 
587
675
  That command:
@@ -709,24 +797,32 @@ The project compiles TypeScript using:
709
797
 
710
798
  # CLI Entry Point
711
799
 
712
- The source CLI entry point lives at:
800
+ The source executable wrapper lives at:
713
801
 
714
802
  ```
715
- src/index.ts
803
+ src/cli-wrapper.ts
716
804
  ```
717
805
 
718
- It uses this executable header during development:
806
+ It uses this executable header:
719
807
 
720
808
  ```ts
721
- #!/usr/bin/env -S node --import tsx/esm
809
+ #!/usr/bin/env node
810
+ ```
811
+
812
+ CLI parsing and command behavior live in:
813
+
814
+ ```
815
+ src/index.ts
722
816
  ```
723
817
 
724
- The published binary entry in `package.json` points to the compiled output:
818
+ TypeScript config support is enabled at runtime by dynamically loading `tsx/esm` when a `brand.config.ts` is detected.
819
+
820
+ The published binary entry in `package.json` points to the compiled wrapper:
725
821
 
726
822
  ```json
727
823
  {
728
824
  "bin": {
729
- "advantacode-brander": "./dist/index.js"
825
+ "advantacode-brander": "./dist/cli-wrapper.js"
730
826
  }
731
827
  }
732
828
  ```
package/package.json CHANGED
@@ -1,60 +1,64 @@
1
- {
2
- "name": "@advantacode/brander",
3
- "version": "0.2.0",
4
- "description": "AdvantaCode Design System Brand Generator",
5
- "type": "module",
6
- "files": [
7
- "dist/**/*.js",
8
- "docs/*.md",
9
- "README.md",
10
- "LICENSE"
11
- ],
12
- "bin": {
13
- "advantacode-brander": "./dist/cli-wrapper.js"
14
- },
15
- "scripts": {
16
- "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
17
- "build": "npm run clean && tsc && chmod +x dist/cli-wrapper.js",
18
- "cli": "node --import tsx/esm dist/cli-wrapper.js",
19
- "lint": "eslint --max-warnings=0 src test",
20
- "pretest": "npm run build",
21
- "test": "node --import tsx/esm --test",
22
- "release:check": "npm run lint && npm test && npm pack --dry-run",
23
- "tokens": "tsx src/generate-tokens.ts",
24
- "prepack": "npm run build",
25
- "brand:generate": "advantacode-brander --out src/brander",
26
- "brand:test": "node ./dist/cli-wrapper.js --out src/brander"
27
- },
28
- "license": "MIT",
29
- "author": "Anthony Penn",
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/advantacode/advantacode-brander.git"
33
- },
34
- "homepage": "https://github.com/advantacode/advantacode-brander#readme",
35
- "bugs": {
36
- "url": "https://github.com/advantacode/advantacode-brander/issues"
37
- },
38
- "keywords": [
39
- "design-tokens",
40
- "branding",
41
- "oklch",
42
- "tailwind",
43
- "cli"
44
- ],
45
- "engines": {
46
- "node": ">=20.0.0"
47
- },
48
- "devDependencies": {
49
- "@types/node": "^25.3.3",
50
- "@typescript-eslint/eslint-plugin": "^8.56.1",
51
- "@typescript-eslint/parser": "^8.56.1",
52
- "eslint": "^8.0.0",
53
- "typescript": "^5.3.0"
54
- },
55
- "dependencies": {
56
- "culori": "^4.0.2",
57
- "dotenv": "^17.3.1",
58
- "tsx": "^4.21.0"
59
- }
60
- }
1
+ {
2
+ "name": "@advantacode/brander",
3
+ "version": "0.3.0",
4
+ "description": "AdvantaCode Design System Brand Generator",
5
+ "type": "module",
6
+ "files": [
7
+ "dist/**/*.js",
8
+ "docs/*.md",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "bin": {
13
+ "advantacode-brander": "./dist/cli-wrapper.js"
14
+ },
15
+ "scripts": {
16
+ "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
17
+ "build": "npm run clean && tsc && chmod +x dist/cli-wrapper.js",
18
+ "cli": "node --import tsx/esm dist/cli-wrapper.js",
19
+ "lint": "eslint --max-warnings=0 src test",
20
+ "pretest": "npm run build",
21
+ "test": "node --import tsx/esm --test",
22
+ "release:check": "npm run lint && npm test && npm pack --dry-run",
23
+ "tokens": "tsx src/generate-tokens.ts",
24
+ "prepack": "npm run build",
25
+ "changelog:cut": "node scripts/cut-changelog.mjs",
26
+ "brand:generate": "advantacode-brander",
27
+ "brand:test": "node ./dist/cli-wrapper.js"
28
+ },
29
+ "license": "MIT",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "author": "Anthony Penn",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/advantacode/advantacode-brander.git"
37
+ },
38
+ "homepage": "https://github.com/advantacode/advantacode-brander#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/advantacode/advantacode-brander/issues"
41
+ },
42
+ "keywords": [
43
+ "design-tokens",
44
+ "branding",
45
+ "oklch",
46
+ "tailwind",
47
+ "cli"
48
+ ],
49
+ "engines": {
50
+ "node": ">=20.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^25.3.3",
54
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
55
+ "@typescript-eslint/parser": "^8.56.1",
56
+ "eslint": "^8.0.0",
57
+ "typescript": "^5.3.0"
58
+ },
59
+ "dependencies": {
60
+ "culori": "^4.0.2",
61
+ "dotenv": "^17.3.1",
62
+ "tsx": "^4.21.0"
63
+ }
64
+ }