@terrazzo/plugin-tailwind 2.0.0-beta.3 → 2.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,12 +4,19 @@
4
4
 
5
5
  ### Minor Changes
6
6
 
7
+ - Fully supports DTCG 2025.10 and resolvers.
8
+
9
+ - ⚠️ Breaking change: plugin-tailwind now uses [Custom variants](https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually) which allow for closer 1:1 translation from tokens.
10
+ - This results in a config change from `modeVariations` → `customVariants`.
11
+
7
12
  - [#530](https://github.com/terrazzoapp/terrazzo/pull/530) [`370ed7b`](https://github.com/terrazzoapp/terrazzo/commit/370ed7b0f578a64824124145d7f4936536b37bb3) Thanks [@drwpow](https://github.com/drwpow)! - ⚠️ Breaking change: lint on plugins no longer runs on individual files, rather, the full set once merged.
8
13
 
9
14
  If your lint plugin is not using the `src` context value, no changes are needed. If it is, you’ll need to instead read from the `sources` array, and look up sources with a token’s `source.loc` filename manually. This change was because lint rules now run on all files in one pass, essentially.
10
15
 
11
16
  - [#530](https://github.com/terrazzoapp/terrazzo/pull/530) [`370ed7b`](https://github.com/terrazzoapp/terrazzo/commit/370ed7b0f578a64824124145d7f4936536b37bb3) Thanks [@drwpow](https://github.com/drwpow)! - ⚠️ [Plugin API] Minor breaking change: token.originalValue may be undefined for tokens created with $ref. This shouldn’t affect any tokens or plugins not using $refs. But going forward this value will be missing if the token was created dynamically via $ref.
12
17
 
18
+ - [#646](https://github.com/terrazzoapp/terrazzo/pull/646) [`fd9de2b`](https://github.com/terrazzoapp/terrazzo/commit/fd9de2b4a93f42e1f1bfd81cf4a63475b506488d) Thanks [@9rotama](https://github.com/9rotama)! - feat: add template option to plugin-tailwind
19
+
13
20
  ### Patch Changes
14
21
 
15
22
  - [#530](https://github.com/terrazzoapp/terrazzo/pull/530) [`370ed7b`](https://github.com/terrazzoapp/terrazzo/commit/370ed7b0f578a64824124145d7f4936536b37bb3) Thanks [@drwpow](https://github.com/drwpow)! - Validation moved to lint rules, which means token validation can be individually configured, and optionally extended.
package/dist/index.d.ts CHANGED
@@ -1,29 +1,47 @@
1
1
  import { Plugin } from "@terrazzo/parser";
2
2
 
3
3
  //#region src/lib.d.ts
4
+ declare const FORMAT_ID = "tailwind";
5
+ declare const PLUGIN_NAME = "@terrazzo/plugin-tailwind";
6
+ type ResolverInput = Record<string, string>;
4
7
  interface TailwindPluginOptions {
5
8
  /**
6
9
  * Filename to output.
7
10
  * @default "tailwind-theme.css"
8
11
  */
9
12
  filename?: string;
13
+ /**
14
+ * Path to a template file. The template should contain `@terrazzo-slot;`
15
+ * which will be replaced with the generated theme.
16
+ */
17
+ template?: string;
10
18
  /** @see https://tailwindcss.com/docs/theme */
11
- theme: Record<string, any>;
12
- /** Array of mapping variants to DTCG modes. */
13
- modeVariants?: {
14
- variant: string;
15
- mode: string;
16
- }[];
19
+ theme: Record<string, unknown>;
20
+ /** Default permutation */
21
+ defaultTheme?: ResolverInput;
22
+ /**
23
+ * Associate Tailwind custom variants with Resolver permutations
24
+ * @see https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually
25
+ */
26
+ customVariants?: {
27
+ [name: string]: {
28
+ /** The CSS selector to apply to this variant */selector: string; /** The resolver input to load for this custom variant */
29
+ input: ResolverInput;
30
+ };
31
+ };
17
32
  }
18
33
  /** Flatten an arbitrarily-nested object */
19
34
  declare function flattenThemeObj(themeObj: Record<string, unknown>): {
20
35
  path: string[];
21
36
  value: string | string[];
22
37
  }[];
38
+ declare const FILE_HEADER = "/* -------------------------------------------\n * Autogenerated by \u26CB Terrazzo. DO NOT EDIT!\n * ------------------------------------------- */";
39
+ declare function buildFileHeader(templatePath?: string): string;
40
+ declare const TERRAZZO_SLOT = "@terrazzo-slot;";
41
+ declare function applyTemplate(template: string, generatedTheme: string): string;
23
42
  //#endregion
24
43
  //#region src/index.d.ts
25
- declare const FORMAT_ID = "tailwind";
26
44
  declare function pluginTailwind(options: TailwindPluginOptions): Plugin;
27
45
  //#endregion
28
- export { FORMAT_ID, TailwindPluginOptions, pluginTailwind as default, flattenThemeObj };
46
+ export { FILE_HEADER, FORMAT_ID, PLUGIN_NAME, ResolverInput, TERRAZZO_SLOT, TailwindPluginOptions, applyTemplate, buildFileHeader, pluginTailwind as default, flattenThemeObj };
29
47
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/lib.ts","../src/index.ts"],"mappings":";;;UAAiB,qBAAA;;;AAAjB;;EAKE,QAAA;EAEa;EAAb,KAAA,EAAO,MAAA;EAAP;EAEA,YAAA;IAAiB,OAAA;IAAiB,IAAA;EAAA;AAAA;;iBAIpB,eAAA,CAAgB,QAAA,EAAU,MAAA;EAA4B,IAAA;EAAgB,KAAA;AAAA;;;cCRzE,SAAA;AAAA,iBAIW,cAAA,CAAe,OAAA,EAAS,qBAAA,GAAwB,MAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/lib.ts","../src/index.ts"],"mappings":";;;cAAa,SAAA;AAAA,cACA,WAAA;AAAA,KAED,aAAA,GAAgB,MAAA;AAAA,UAEX,qBAAA;EALK;;;;EAUpB,QAAA;EATsB;;;;EActB,QAAA;EAZuB;EAcvB,KAAA,EAAO,MAAA;EAdmB;EAgB1B,YAAA,GAAe,aAAA;EAdA;;;;EAmBf,cAAA;IAAA,CACG,IAAA;MAIqB,gDAFpB,QAAA,UAjBJ;MAmBI,KAAA,EAAO,aAAA;IAAA;EAAA;AAAA;;iBAMG,eAAA,CAAgB,QAAA,EAAU,MAAA;EAA4B,IAAA;EAAgB,KAAA;AAAA;AAAA,cAuBzE,WAAA;AAAA,iBAIG,eAAA,CAAgB,YAAA;AAAA,cAUnB,aAAA;AAAA,iBAEG,aAAA,CAAc,QAAA,UAAkB,cAAA;;;iBCvDxB,cAAA,CAAe,OAAA,EAAS,qBAAA,GAAwB,MAAA"}
package/dist/index.js CHANGED
@@ -1,7 +1,12 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
1
4
  import { FORMAT_ID as FORMAT_ID$1 } from "@terrazzo/plugin-css";
2
5
  import { makeCSSVar } from "@terrazzo/token-tools/css";
3
6
 
4
7
  //#region src/lib.ts
8
+ const FORMAT_ID = "tailwind";
9
+ const PLUGIN_NAME = "@terrazzo/plugin-tailwind";
5
10
  /** Flatten an arbitrarily-nested object */
6
11
  function flattenThemeObj(themeObj) {
7
12
  const result = [];
@@ -20,69 +25,128 @@ function flattenThemeObj(themeObj) {
20
25
  traverse(themeObj, []);
21
26
  return result;
22
27
  }
28
+ const FILE_HEADER = `/* -------------------------------------------
29
+ * Autogenerated by ⛋ Terrazzo. DO NOT EDIT!
30
+ * ------------------------------------------- */`;
31
+ function buildFileHeader(templatePath) {
32
+ if (templatePath) return `/* -------------------------------------------
33
+ * Autogenerated by ⛋ Terrazzo. DO NOT EDIT!
34
+ * template: ${templatePath}
35
+ * ------------------------------------------- */`;
36
+ return FILE_HEADER;
37
+ }
38
+ const TERRAZZO_SLOT = "@terrazzo-slot;";
39
+ function applyTemplate(template, generatedTheme) {
40
+ if (!template.includes(TERRAZZO_SLOT)) throw new Error(`Template must contain "${TERRAZZO_SLOT}" directive`);
41
+ return template.replace(/@terrazzo-slot;(\r?\n)+/, `${generatedTheme.trimEnd()}\n\n`);
42
+ }
23
43
 
24
44
  //#endregion
25
45
  //#region src/index.ts
26
- const FORMAT_ID = "tailwind";
46
+ const DEFAULT_THEME = ".";
27
47
  function pluginTailwind(options) {
28
- const filename = options?.filename ?? options?.fileName ?? "tailwind-theme.css";
48
+ const filename = options?.filename ?? "tailwind-theme.css";
49
+ const variations = {
50
+ [DEFAULT_THEME]: {
51
+ selector: "",
52
+ input: options.defaultTheme ?? { tzMode: "." }
53
+ },
54
+ ...options?.customVariants
55
+ };
56
+ let cwd;
57
+ const msg = {
58
+ group: "plugin",
59
+ label: PLUGIN_NAME
60
+ };
29
61
  return {
30
- name: "@terrazzo/plugin-tailwind",
62
+ name: PLUGIN_NAME,
31
63
  enforce: "post",
32
- config(config) {
33
- if (!config.plugins.some((p) => p.name === "@terrazzo/plugin-css")) throw new Error("@terrazzo/plugin-css missing! Please install and add to the plugins array to use the Tailwind plugin.");
34
- if (!options || !options.theme) throw new Error("Missing Tailwind `theme` option.");
64
+ config(config, { logger }) {
65
+ if (!config.plugins.some((p) => p.name === "@terrazzo/plugin-css")) logger.error({
66
+ ...msg,
67
+ message: "@terrazzo/plugin-css missing! Please install and add to the plugins array to use the Tailwind plugin."
68
+ });
69
+ if (!options || !options.theme) logger.error({
70
+ ...msg,
71
+ message: "Missing Tailwind `theme` option."
72
+ });
73
+ if (options && "modeVariants" in options) logger.error({
74
+ ...msg,
75
+ message: "Migrate \"modeVariants\" to \"variants\" in config (see docs)"
76
+ });
77
+ cwd = new URL("./", config.outDir);
78
+ if (options?.template && !fsSync.existsSync(new URL(options.template, cwd))) logger.error({
79
+ ...msg,
80
+ message: `Could not locate template ${fileURLToPath(new URL(options.template, cwd))}. Does the file exist?`
81
+ });
35
82
  },
36
- async transform({ getTransforms, setTransform }) {
37
- const variants = [{
38
- variant: ".",
39
- mode: "."
40
- }, ...options?.modeVariants ?? []];
41
- for (const { variant, mode } of variants) {
42
- const flatTheme = flattenThemeObj(options.theme);
83
+ async transform({ getTransforms, setTransform, context: { logger } }) {
84
+ const flatTheme = flattenThemeObj(options.theme);
85
+ for (const { input } of Object.values(variations)) {
86
+ const query = getTokenQuery(input);
43
87
  for (const { path, value } of flatTheme) {
44
- const tokens = getTransforms({
88
+ const variantTokens = getTransforms({
89
+ ...query,
45
90
  format: FORMAT_ID$1,
46
- id: value,
47
- mode
91
+ id: value
92
+ });
93
+ if (!variantTokens.length) logger.warn({
94
+ ...msg,
95
+ message: `${value} matched 0 tokens`
48
96
  });
49
- for (const token of tokens) {
97
+ for (const token of variantTokens) {
50
98
  let relName = token.id.split(".").at(-1);
51
99
  for (const subgroup of [...Array.isArray(value) ? value : [value]]) {
52
100
  const match = subgroup.replace(/\*.*/, "");
53
101
  relName = token.id.replace(match, "");
54
102
  }
55
103
  setTransform(token.id, {
104
+ ...query,
56
105
  format: FORMAT_ID,
57
106
  localID: makeCSSVar(`${path.join("-")}-${relName.replace(/\./g, "-")}`),
58
- value: typeof token.value === "object" ? token.value["."] : token.value,
59
- mode: variant
107
+ value: typeof token.value === "object" ? token.value["."] : token.value
60
108
  });
61
109
  }
62
110
  }
63
111
  }
64
112
  },
65
113
  async build({ getTransforms, outputFile }) {
66
- const output = ["@import \"tailwindcss\";", ""];
67
- const variants = { ".": [] };
68
- for (const token of getTransforms({
69
- format: FORMAT_ID,
70
- mode: "*"
71
- })) {
72
- const { localID, value, mode } = token;
73
- if (!variants[mode]) variants[mode] = [];
74
- variants[mode].push(`${localID}: ${value};`);
114
+ let generatedTheme = "";
115
+ for (const [variant, { input, selector }] of Object.entries(variations)) {
116
+ if (generatedTheme) generatedTheme += "\n";
117
+ generatedTheme += `${variant === DEFAULT_THEME ? "@theme" : `@custom-variant ${variant} (${selector})`} {\n`;
118
+ for (const token of getTransforms({
119
+ ...getTokenQuery(input),
120
+ format: FORMAT_ID
121
+ })) generatedTheme += ` ${token.localID}: ${token.value};\n`;
122
+ generatedTheme += "}\n";
75
123
  }
76
- for (const [variant, values] of Object.entries(variants)) {
77
- output.push(variant === "." ? "@theme {" : `@variant ${variant} {`);
78
- for (const value of values) output.push(` ${value}`);
79
- output.push("}", "");
124
+ let finalOutput = "";
125
+ if (options.template) {
126
+ const templateUrl = new URL(options.template, cwd);
127
+ const templateContent = await fs.readFile(fileURLToPath(templateUrl), "utf8");
128
+ finalOutput += applyTemplate(templateContent, generatedTheme);
129
+ } else {
130
+ finalOutput += "@import \"tailwindcss\";\n\n";
131
+ finalOutput += generatedTheme;
80
132
  }
81
- outputFile(filename, output.join("\n"));
133
+ outputFile(filename, [
134
+ buildFileHeader(options.template),
135
+ "",
136
+ finalOutput
137
+ ].join("\n"));
82
138
  }
83
139
  };
84
140
  }
141
+ /** Convert modes to inputs */
142
+ function isLegacyModes(input) {
143
+ return Object.keys(input).length === 0 && "tzMode" in input;
144
+ }
145
+ /** Build query for both resolvers and legacy modes */
146
+ function getTokenQuery(input) {
147
+ return isLegacyModes(input) ? { mode: input.tzMode ?? "." } : { input };
148
+ }
85
149
 
86
150
  //#endregion
87
- export { FORMAT_ID, pluginTailwind as default, flattenThemeObj };
151
+ export { FILE_HEADER, FORMAT_ID, PLUGIN_NAME, TERRAZZO_SLOT, applyTemplate, buildFileHeader, pluginTailwind as default, flattenThemeObj };
88
152
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["FORMAT_CSS"],"sources":["../src/lib.ts","../src/index.ts"],"sourcesContent":["export interface TailwindPluginOptions {\n /**\n * Filename to output.\n * @default \"tailwind-theme.css\"\n */\n filename?: string;\n /** @see https://tailwindcss.com/docs/theme */\n theme: Record<string, any>;\n /** Array of mapping variants to DTCG modes. */\n modeVariants?: { variant: string; mode: string }[];\n}\n\n/** Flatten an arbitrarily-nested object */\nexport function flattenThemeObj(themeObj: Record<string, unknown>): { path: string[]; value: string | string[] }[] {\n const result: { path: string[]; value: string | string[] }[] = [];\n\n function traverse(obj: Record<string, unknown>, path: string[]) {\n for (const [key, value] of Object.entries(obj)) {\n const newPath = [...path, key];\n if (typeof value === 'string' || Array.isArray(value)) {\n if (Array.isArray(value) && (value.length === 0 || value.some((v) => typeof v !== 'string'))) {\n throw new Error(\n `Invalid value at path \"${newPath.join('.')}\": expected a string or an array of strings, but got ${JSON.stringify(value)}`,\n );\n }\n result.push({ path: newPath, value });\n } else if (typeof value === 'object' && value !== null) {\n traverse(value as Record<string, unknown>, newPath);\n }\n }\n }\n\n traverse(themeObj, []);\n return result;\n}\n","import type { Plugin } from '@terrazzo/parser';\nimport { FORMAT_ID as FORMAT_CSS } from '@terrazzo/plugin-css';\nimport { makeCSSVar } from '@terrazzo/token-tools/css';\nimport { flattenThemeObj, type TailwindPluginOptions } from './lib.js';\n\nexport const FORMAT_ID = 'tailwind';\n\nexport * from './lib.js';\n\nexport default function pluginTailwind(options: TailwindPluginOptions): Plugin {\n const filename = options?.filename ?? (options as any)?.fileName ?? 'tailwind-theme.css';\n\n return {\n name: '@terrazzo/plugin-tailwind',\n enforce: 'post', // ensure this comes after @terrazzo/plugin-css\n config(config) {\n if (!config.plugins.some((p) => p.name === '@terrazzo/plugin-css')) {\n throw new Error(\n '@terrazzo/plugin-css missing! Please install and add to the plugins array to use the Tailwind plugin.',\n );\n }\n\n if (!options || !options.theme) {\n throw new Error('Missing Tailwind `theme` option.');\n }\n },\n async transform({ getTransforms, setTransform }) {\n const variants = [{ variant: '.', mode: '.' }, ...(options?.modeVariants ?? [])];\n for (const { variant, mode } of variants) {\n const flatTheme = flattenThemeObj(options.theme);\n for (const { path, value } of flatTheme) {\n const tokens = getTransforms({\n format: FORMAT_CSS,\n id: value,\n mode,\n });\n for (const token of tokens) {\n let relName = token.id.split('.').at(-1)!;\n for (const subgroup of [...(Array.isArray(value) ? value : [value])]) {\n const match = subgroup.replace(/\\*.*/, '');\n relName = token.id.replace(match, '');\n }\n setTransform(token.id, {\n format: FORMAT_ID,\n localID: makeCSSVar(`${path.join('-')}-${relName.replace(/\\./g, '-')}`),\n value: typeof token.value === 'object' ? token.value['.']! : token.value,\n mode: variant, // ! <- not the original mode!\n });\n }\n }\n }\n },\n async build({ getTransforms, outputFile }) {\n const output = ['@import \"tailwindcss\";', ''];\n\n const variants: Record<string, string[]> = { '.': [] };\n for (const token of getTransforms({ format: FORMAT_ID, mode: '*' })) {\n const { localID, value, mode } = token;\n if (!variants[mode]) {\n variants[mode] = [];\n }\n variants[mode].push(`${localID}: ${value};`);\n }\n for (const [variant, values] of Object.entries(variants)) {\n output.push(variant === '.' ? '@theme {' : `@variant ${variant} {`);\n for (const value of values) {\n output.push(` ${value}`);\n }\n output.push('}', '');\n }\n\n outputFile(filename, output.join('\\n'));\n },\n };\n}\n"],"mappings":";;;;;AAaA,SAAgB,gBAAgB,UAAmF;CACjH,MAAM,SAAyD,EAAE;CAEjE,SAAS,SAAS,KAA8B,MAAgB;AAC9D,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,EAAE;GAC9C,MAAM,UAAU,CAAC,GAAG,MAAM,IAAI;AAC9B,OAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,EAAE;AACrD,QAAI,MAAM,QAAQ,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM,MAAM,MAAM,OAAO,MAAM,SAAS,EACzF,OAAM,IAAI,MACR,0BAA0B,QAAQ,KAAK,IAAI,CAAC,uDAAuD,KAAK,UAAU,MAAM,GACzH;AAEH,WAAO,KAAK;KAAE,MAAM;KAAS;KAAO,CAAC;cAC5B,OAAO,UAAU,YAAY,UAAU,KAChD,UAAS,OAAkC,QAAQ;;;AAKzD,UAAS,UAAU,EAAE,CAAC;AACtB,QAAO;;;;;AC5BT,MAAa,YAAY;AAIzB,SAAwB,eAAe,SAAwC;CAC7E,MAAM,WAAW,SAAS,YAAa,SAAiB,YAAY;AAEpE,QAAO;EACL,MAAM;EACN,SAAS;EACT,OAAO,QAAQ;AACb,OAAI,CAAC,OAAO,QAAQ,MAAM,MAAM,EAAE,SAAS,uBAAuB,CAChE,OAAM,IAAI,MACR,wGACD;AAGH,OAAI,CAAC,WAAW,CAAC,QAAQ,MACvB,OAAM,IAAI,MAAM,mCAAmC;;EAGvD,MAAM,UAAU,EAAE,eAAe,gBAAgB;GAC/C,MAAM,WAAW,CAAC;IAAE,SAAS;IAAK,MAAM;IAAK,EAAE,GAAI,SAAS,gBAAgB,EAAE,CAAE;AAChF,QAAK,MAAM,EAAE,SAAS,UAAU,UAAU;IACxC,MAAM,YAAY,gBAAgB,QAAQ,MAAM;AAChD,SAAK,MAAM,EAAE,MAAM,WAAW,WAAW;KACvC,MAAM,SAAS,cAAc;MAC3B,QAAQA;MACR,IAAI;MACJ;MACD,CAAC;AACF,UAAK,MAAM,SAAS,QAAQ;MAC1B,IAAI,UAAU,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,GAAG;AACxC,WAAK,MAAM,YAAY,CAAC,GAAI,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAE,EAAE;OACpE,MAAM,QAAQ,SAAS,QAAQ,QAAQ,GAAG;AAC1C,iBAAU,MAAM,GAAG,QAAQ,OAAO,GAAG;;AAEvC,mBAAa,MAAM,IAAI;OACrB,QAAQ;OACR,SAAS,WAAW,GAAG,KAAK,KAAK,IAAI,CAAC,GAAG,QAAQ,QAAQ,OAAO,IAAI,GAAG;OACvE,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,MAAM,OAAQ,MAAM;OACnE,MAAM;OACP,CAAC;;;;;EAKV,MAAM,MAAM,EAAE,eAAe,cAAc;GACzC,MAAM,SAAS,CAAC,4BAA0B,GAAG;GAE7C,MAAM,WAAqC,EAAE,KAAK,EAAE,EAAE;AACtD,QAAK,MAAM,SAAS,cAAc;IAAE,QAAQ;IAAW,MAAM;IAAK,CAAC,EAAE;IACnE,MAAM,EAAE,SAAS,OAAO,SAAS;AACjC,QAAI,CAAC,SAAS,MACZ,UAAS,QAAQ,EAAE;AAErB,aAAS,MAAM,KAAK,GAAG,QAAQ,IAAI,MAAM,GAAG;;AAE9C,QAAK,MAAM,CAAC,SAAS,WAAW,OAAO,QAAQ,SAAS,EAAE;AACxD,WAAO,KAAK,YAAY,MAAM,aAAa,YAAY,QAAQ,IAAI;AACnE,SAAK,MAAM,SAAS,OAClB,QAAO,KAAK,KAAK,QAAQ;AAE3B,WAAO,KAAK,KAAK,GAAG;;AAGtB,cAAW,UAAU,OAAO,KAAK,KAAK,CAAC;;EAE1C"}
1
+ {"version":3,"file":"index.js","names":["FORMAT_CSS","FORMAT_TAILWIND"],"sources":["../src/lib.ts","../src/index.ts"],"sourcesContent":["export const FORMAT_ID = 'tailwind';\nexport const PLUGIN_NAME = '@terrazzo/plugin-tailwind';\n\nexport type ResolverInput = Record<string, string>;\n\nexport interface TailwindPluginOptions {\n /**\n * Filename to output.\n * @default \"tailwind-theme.css\"\n */\n filename?: string;\n /**\n * Path to a template file. The template should contain `@terrazzo-slot;`\n * which will be replaced with the generated theme.\n */\n template?: string;\n /** @see https://tailwindcss.com/docs/theme */\n theme: Record<string, unknown>;\n /** Default permutation */\n defaultTheme?: ResolverInput;\n /**\n * Associate Tailwind custom variants with Resolver permutations\n * @see https://tailwindcss.com/docs/dark-mode#toggling-dark-mode-manually\n */\n customVariants?: {\n [name: string]: {\n /** The CSS selector to apply to this variant */\n selector: string;\n /** The resolver input to load for this custom variant */\n input: ResolverInput;\n };\n };\n}\n\n/** Flatten an arbitrarily-nested object */\nexport function flattenThemeObj(themeObj: Record<string, unknown>): { path: string[]; value: string | string[] }[] {\n const result: { path: string[]; value: string | string[] }[] = [];\n\n function traverse(obj: Record<string, unknown>, path: string[]) {\n for (const [key, value] of Object.entries(obj)) {\n const newPath = [...path, key];\n if (typeof value === 'string' || Array.isArray(value)) {\n if (Array.isArray(value) && (value.length === 0 || value.some((v) => typeof v !== 'string'))) {\n throw new Error(\n `Invalid value at path \"${newPath.join('.')}\": expected a string or an array of strings, but got ${JSON.stringify(value)}`,\n );\n }\n result.push({ path: newPath, value });\n } else if (typeof value === 'object' && value !== null) {\n traverse(value as Record<string, unknown>, newPath);\n }\n }\n }\n\n traverse(themeObj, []);\n return result;\n}\n\nexport const FILE_HEADER = `/* -------------------------------------------\n * Autogenerated by ⛋ Terrazzo. DO NOT EDIT!\n * ------------------------------------------- */`;\n\nexport function buildFileHeader(templatePath?: string): string {\n if (templatePath) {\n return `/* -------------------------------------------\n * Autogenerated by ⛋ Terrazzo. DO NOT EDIT!\n * template: ${templatePath}\n * ------------------------------------------- */`;\n }\n return FILE_HEADER;\n}\n\nexport const TERRAZZO_SLOT = '@terrazzo-slot;';\n\nexport function applyTemplate(template: string, generatedTheme: string): string {\n if (!template.includes(TERRAZZO_SLOT)) {\n throw new Error(`Template must contain \"${TERRAZZO_SLOT}\" directive`);\n }\n // Replace slot and any following newlines (CRLF or LF) with theme + single newline\n return template.replace(/@terrazzo-slot;(\\r?\\n)+/, `${generatedTheme.trimEnd()}\\n\\n`);\n}\n","import fsSync from 'node:fs';\nimport fs from 'node:fs/promises';\nimport { fileURLToPath } from 'node:url';\nimport type { Plugin } from '@terrazzo/parser';\nimport { FORMAT_ID as FORMAT_CSS } from '@terrazzo/plugin-css';\nimport { makeCSSVar } from '@terrazzo/token-tools/css';\nimport {\n applyTemplate,\n buildFileHeader,\n FORMAT_ID as FORMAT_TAILWIND,\n flattenThemeObj,\n PLUGIN_NAME,\n type TailwindPluginOptions,\n} from './lib.js';\n\nexport * from './lib.js';\n\nconst DEFAULT_THEME = '.';\n\nexport default function pluginTailwind(options: TailwindPluginOptions): Plugin {\n const filename = options?.filename ?? 'tailwind-theme.css';\n const variations = {\n [DEFAULT_THEME]: { selector: '', input: options.defaultTheme ?? { tzMode: '.' } },\n ...options?.customVariants,\n };\n let cwd: URL;\n const msg = { group: 'plugin' as const, label: PLUGIN_NAME };\n\n return {\n name: PLUGIN_NAME,\n enforce: 'post', // ensure this comes after @terrazzo/plugin-css\n config(config, { logger }) {\n if (!config.plugins.some((p) => p.name === '@terrazzo/plugin-css')) {\n logger.error({\n ...msg,\n message:\n '@terrazzo/plugin-css missing! Please install and add to the plugins array to use the Tailwind plugin.',\n });\n }\n\n if (!options || !options.theme) {\n logger.error({ ...msg, message: 'Missing Tailwind `theme` option.' });\n }\n\n if (options && 'modeVariants' in options) {\n logger.error({ ...msg, message: 'Migrate \"modeVariants\" to \"variants\" in config (see docs)' });\n }\n\n // store cwd for template resolution (parent of outDir)\n cwd = new URL('./', config.outDir);\n if (options?.template && !fsSync.existsSync(new URL(options.template, cwd))) {\n logger.error({\n ...msg,\n message: `Could not locate template ${fileURLToPath(new URL(options.template, cwd))}. Does the file exist?`,\n });\n }\n },\n async transform({ getTransforms, setTransform, context: { logger } }) {\n const flatTheme = flattenThemeObj(options.theme);\n\n for (const { input } of Object.values(variations)) {\n const query = getTokenQuery(input);\n // Note: it’s important to remember that under-the-hood, getting/setting modes is NOT the same as having an { tzMode: value } input.\n // The former allows glob searching across all modes, the latter does not. Especially in the Tailwind plugin, confusing the two\n // will result in many dropped tokens.\n for (const { path, value } of flatTheme) {\n const variantTokens = getTransforms({ ...query, format: FORMAT_CSS, id: value });\n // Warn the user if they are trying to generate an empty Tailwind variant\n if (!variantTokens.length) {\n logger.warn({ ...msg, message: `${value} matched 0 tokens` });\n }\n\n for (const token of variantTokens) {\n let relName = token.id.split('.').at(-1)!;\n for (const subgroup of [...(Array.isArray(value) ? value : [value])]) {\n const match = subgroup.replace(/\\*.*/, '');\n relName = token.id.replace(match, '');\n }\n setTransform(token.id, {\n ...query,\n format: FORMAT_TAILWIND,\n localID: makeCSSVar(`${path.join('-')}-${relName.replace(/\\./g, '-')}`),\n value: typeof token.value === 'object' ? token.value['.']! : token.value,\n });\n }\n }\n }\n },\n async build({ getTransforms, outputFile }) {\n let generatedTheme = '';\n for (const [variant, { input, selector }] of Object.entries(variations)) {\n if (generatedTheme) {\n generatedTheme += '\\n'; // add extra line break if continuing\n }\n generatedTheme += `${variant === DEFAULT_THEME ? '@theme' : `@custom-variant ${variant} (${selector})`} {\\n`;\n for (const token of getTransforms({ ...getTokenQuery(input), format: FORMAT_TAILWIND })) {\n generatedTheme += ` ${token.localID}: ${token.value};\\n`;\n }\n generatedTheme += '}\\n';\n }\n\n // build the output combining theme and template\n let finalOutput = '';\n if (options.template) {\n const templateUrl = new URL(options.template, cwd);\n const templateContent = await fs.readFile(fileURLToPath(templateUrl), 'utf8');\n finalOutput += applyTemplate(templateContent, generatedTheme);\n } else {\n finalOutput += '@import \"tailwindcss\";\\n\\n';\n finalOutput += generatedTheme;\n }\n\n const header = buildFileHeader(options.template);\n outputFile(filename, [header, '', finalOutput].join('\\n'));\n },\n };\n}\n\n/** Convert modes to inputs */\nfunction isLegacyModes(input: Record<string, string>): boolean {\n return Object.keys(input).length === 0 && 'tzMode' in input;\n}\n\n/** Build query for both resolvers and legacy modes */\nfunction getTokenQuery(input: Record<string, string>) {\n return isLegacyModes(input) ? { mode: input.tzMode ?? '.' } : { input };\n}\n"],"mappings":";;;;;;;AAAA,MAAa,YAAY;AACzB,MAAa,cAAc;;AAkC3B,SAAgB,gBAAgB,UAAmF;CACjH,MAAM,SAAyD,EAAE;CAEjE,SAAS,SAAS,KAA8B,MAAgB;AAC9D,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,EAAE;GAC9C,MAAM,UAAU,CAAC,GAAG,MAAM,IAAI;AAC9B,OAAI,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,EAAE;AACrD,QAAI,MAAM,QAAQ,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM,MAAM,MAAM,OAAO,MAAM,SAAS,EACzF,OAAM,IAAI,MACR,0BAA0B,QAAQ,KAAK,IAAI,CAAC,uDAAuD,KAAK,UAAU,MAAM,GACzH;AAEH,WAAO,KAAK;KAAE,MAAM;KAAS;KAAO,CAAC;cAC5B,OAAO,UAAU,YAAY,UAAU,KAChD,UAAS,OAAkC,QAAQ;;;AAKzD,UAAS,UAAU,EAAE,CAAC;AACtB,QAAO;;AAGT,MAAa,cAAc;;;AAI3B,SAAgB,gBAAgB,cAA+B;AAC7D,KAAI,aACF,QAAO;;gBAEK,aAAa;;AAG3B,QAAO;;AAGT,MAAa,gBAAgB;AAE7B,SAAgB,cAAc,UAAkB,gBAAgC;AAC9E,KAAI,CAAC,SAAS,SAAS,cAAc,CACnC,OAAM,IAAI,MAAM,0BAA0B,cAAc,aAAa;AAGvE,QAAO,SAAS,QAAQ,2BAA2B,GAAG,eAAe,SAAS,CAAC,MAAM;;;;;AC9DvF,MAAM,gBAAgB;AAEtB,SAAwB,eAAe,SAAwC;CAC7E,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,aAAa;GAChB,gBAAgB;GAAE,UAAU;GAAI,OAAO,QAAQ,gBAAgB,EAAE,QAAQ,KAAK;GAAE;EACjF,GAAG,SAAS;EACb;CACD,IAAI;CACJ,MAAM,MAAM;EAAE,OAAO;EAAmB,OAAO;EAAa;AAE5D,QAAO;EACL,MAAM;EACN,SAAS;EACT,OAAO,QAAQ,EAAE,UAAU;AACzB,OAAI,CAAC,OAAO,QAAQ,MAAM,MAAM,EAAE,SAAS,uBAAuB,CAChE,QAAO,MAAM;IACX,GAAG;IACH,SACE;IACH,CAAC;AAGJ,OAAI,CAAC,WAAW,CAAC,QAAQ,MACvB,QAAO,MAAM;IAAE,GAAG;IAAK,SAAS;IAAoC,CAAC;AAGvE,OAAI,WAAW,kBAAkB,QAC/B,QAAO,MAAM;IAAE,GAAG;IAAK,SAAS;IAA6D,CAAC;AAIhG,SAAM,IAAI,IAAI,MAAM,OAAO,OAAO;AAClC,OAAI,SAAS,YAAY,CAAC,OAAO,WAAW,IAAI,IAAI,QAAQ,UAAU,IAAI,CAAC,CACzE,QAAO,MAAM;IACX,GAAG;IACH,SAAS,6BAA6B,cAAc,IAAI,IAAI,QAAQ,UAAU,IAAI,CAAC,CAAC;IACrF,CAAC;;EAGN,MAAM,UAAU,EAAE,eAAe,cAAc,SAAS,EAAE,YAAY;GACpE,MAAM,YAAY,gBAAgB,QAAQ,MAAM;AAEhD,QAAK,MAAM,EAAE,WAAW,OAAO,OAAO,WAAW,EAAE;IACjD,MAAM,QAAQ,cAAc,MAAM;AAIlC,SAAK,MAAM,EAAE,MAAM,WAAW,WAAW;KACvC,MAAM,gBAAgB,cAAc;MAAE,GAAG;MAAO,QAAQA;MAAY,IAAI;MAAO,CAAC;AAEhF,SAAI,CAAC,cAAc,OACjB,QAAO,KAAK;MAAE,GAAG;MAAK,SAAS,GAAG,MAAM;MAAoB,CAAC;AAG/D,UAAK,MAAM,SAAS,eAAe;MACjC,IAAI,UAAU,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,GAAG;AACxC,WAAK,MAAM,YAAY,CAAC,GAAI,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAE,EAAE;OACpE,MAAM,QAAQ,SAAS,QAAQ,QAAQ,GAAG;AAC1C,iBAAU,MAAM,GAAG,QAAQ,OAAO,GAAG;;AAEvC,mBAAa,MAAM,IAAI;OACrB,GAAG;OACH,QAAQC;OACR,SAAS,WAAW,GAAG,KAAK,KAAK,IAAI,CAAC,GAAG,QAAQ,QAAQ,OAAO,IAAI,GAAG;OACvE,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,MAAM,OAAQ,MAAM;OACpE,CAAC;;;;;EAKV,MAAM,MAAM,EAAE,eAAe,cAAc;GACzC,IAAI,iBAAiB;AACrB,QAAK,MAAM,CAAC,SAAS,EAAE,OAAO,eAAe,OAAO,QAAQ,WAAW,EAAE;AACvE,QAAI,eACF,mBAAkB;AAEpB,sBAAkB,GAAG,YAAY,gBAAgB,WAAW,mBAAmB,QAAQ,IAAI,SAAS,GAAG;AACvG,SAAK,MAAM,SAAS,cAAc;KAAE,GAAG,cAAc,MAAM;KAAE,QAAQA;KAAiB,CAAC,CACrF,mBAAkB,KAAK,MAAM,QAAQ,IAAI,MAAM,MAAM;AAEvD,sBAAkB;;GAIpB,IAAI,cAAc;AAClB,OAAI,QAAQ,UAAU;IACpB,MAAM,cAAc,IAAI,IAAI,QAAQ,UAAU,IAAI;IAClD,MAAM,kBAAkB,MAAM,GAAG,SAAS,cAAc,YAAY,EAAE,OAAO;AAC7E,mBAAe,cAAc,iBAAiB,eAAe;UACxD;AACL,mBAAe;AACf,mBAAe;;AAIjB,cAAW,UAAU;IADN,gBAAgB,QAAQ,SAAS;IAClB;IAAI;IAAY,CAAC,KAAK,KAAK,CAAC;;EAE7D;;;AAIH,SAAS,cAAc,OAAwC;AAC7D,QAAO,OAAO,KAAK,MAAM,CAAC,WAAW,KAAK,YAAY;;;AAIxD,SAAS,cAAc,OAA+B;AACpD,QAAO,cAAc,MAAM,GAAG,EAAE,MAAM,MAAM,UAAU,KAAK,GAAG,EAAE,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrazzo/plugin-tailwind",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.5",
4
4
  "description": "Generate Tailwind v4 theme using DTCG design tokens.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,19 +28,20 @@
28
28
  },
29
29
  "peerDependencies": {
30
30
  "tailwindcss": "^4.0.0",
31
- "@terrazzo/cli": "^2.0.0-beta.3",
32
- "@terrazzo/parser": "^2.0.0-beta.3",
33
- "@terrazzo/plugin-css": "^2.0.0-beta.3",
34
- "@terrazzo/token-tools": "^2.0.0-beta.3"
31
+ "@terrazzo/cli": "^2.0.0-beta.5",
32
+ "@terrazzo/parser": "^2.0.0-beta.5",
33
+ "@terrazzo/plugin-css": "^2.0.0-beta.5",
34
+ "@terrazzo/token-tools": "^2.0.0-beta.5"
35
35
  },
36
36
  "dependencies": {
37
- "@terrazzo/token-tools": "^2.0.0-beta.3"
37
+ "@terrazzo/token-tools": "^2.0.0-beta.5"
38
38
  },
39
39
  "devDependencies": {
40
- "tailwindcss": "^4.1.18",
41
- "@terrazzo/cli": "^2.0.0-beta.3",
42
- "@terrazzo/parser": "^2.0.0-beta.3",
43
- "@terrazzo/plugin-css": "^2.0.0-beta.3"
40
+ "dtcg-examples": "^1.0.3",
41
+ "tailwindcss": "^4.2.0",
42
+ "@terrazzo/cli": "^2.0.0-beta.5",
43
+ "@terrazzo/parser": "^2.0.0-beta.5",
44
+ "@terrazzo/plugin-css": "^2.0.0-beta.5"
44
45
  },
45
46
  "scripts": {
46
47
  "build": "rolldown -c && attw --profile esm-only --pack .",