@terrazzo/plugin-tailwind 2.0.0-beta.4 → 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,6 +4,11 @@
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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
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.
@@ -14,11 +17,18 @@ interface TailwindPluginOptions {
14
17
  template?: string;
15
18
  /** @see https://tailwindcss.com/docs/theme */
16
19
  theme: Record<string, unknown>;
17
- /** Array of mapping variants to DTCG modes. */
18
- modeVariants?: {
19
- variant: string;
20
- mode: string;
21
- }[];
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
+ };
22
32
  }
23
33
  /** Flatten an arbitrarily-nested object */
24
34
  declare function flattenThemeObj(themeObj: Record<string, unknown>): {
@@ -31,8 +41,7 @@ declare const TERRAZZO_SLOT = "@terrazzo-slot;";
31
41
  declare function applyTemplate(template: string, generatedTheme: string): string;
32
42
  //#endregion
33
43
  //#region src/index.d.ts
34
- declare const FORMAT_ID = "tailwind";
35
44
  declare function pluginTailwind(options: TailwindPluginOptions): Plugin;
36
45
  //#endregion
37
- export { FILE_HEADER, FORMAT_ID, TERRAZZO_SLOT, TailwindPluginOptions, applyTemplate, buildFileHeader, pluginTailwind as default, flattenThemeObj };
46
+ export { FILE_HEADER, FORMAT_ID, PLUGIN_NAME, ResolverInput, TERRAZZO_SLOT, TailwindPluginOptions, applyTemplate, buildFileHeader, pluginTailwind as default, flattenThemeObj };
38
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;EAOa;;;;EAFb,QAAA;EAIA;EAFA,KAAA,EAAO,MAAA;EAE2B;EAAlC,YAAA;IAAiB,OAAA;IAAiB,IAAA;EAAA;AAAA;;iBAIpB,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;;;cClDnC,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,9 +1,12 @@
1
- import fs from "node:fs";
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { FORMAT_ID as FORMAT_ID$1 } from "@terrazzo/plugin-css";
4
5
  import { makeCSSVar } from "@terrazzo/token-tools/css";
5
6
 
6
7
  //#region src/lib.ts
8
+ const FORMAT_ID = "tailwind";
9
+ const PLUGIN_NAME = "@terrazzo/plugin-tailwind";
7
10
  /** Flatten an arbitrarily-nested object */
8
11
  function flattenThemeObj(themeObj) {
9
12
  const result = [];
@@ -35,78 +38,98 @@ function buildFileHeader(templatePath) {
35
38
  const TERRAZZO_SLOT = "@terrazzo-slot;";
36
39
  function applyTemplate(template, generatedTheme) {
37
40
  if (!template.includes(TERRAZZO_SLOT)) throw new Error(`Template must contain "${TERRAZZO_SLOT}" directive`);
38
- return template.replace(/@terrazzo-slot;[\r\n]+/, `${generatedTheme.trimEnd()}\n\n`);
41
+ return template.replace(/@terrazzo-slot;(\r?\n)+/, `${generatedTheme.trimEnd()}\n\n`);
39
42
  }
40
43
 
41
44
  //#endregion
42
45
  //#region src/index.ts
43
- const FORMAT_ID = "tailwind";
46
+ const DEFAULT_THEME = ".";
44
47
  function pluginTailwind(options) {
45
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
+ };
46
56
  let cwd;
57
+ const msg = {
58
+ group: "plugin",
59
+ label: PLUGIN_NAME
60
+ };
47
61
  return {
48
- name: "@terrazzo/plugin-tailwind",
62
+ name: PLUGIN_NAME,
49
63
  enforce: "post",
50
- config(config) {
51
- 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.");
52
- 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
+ });
53
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
+ });
54
82
  },
55
- async transform({ getTransforms, setTransform }) {
56
- const variants = [{
57
- variant: ".",
58
- mode: "."
59
- }, ...options?.modeVariants ?? []];
60
- for (const { variant, mode } of variants) {
61
- 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);
62
87
  for (const { path, value } of flatTheme) {
63
- const tokens = getTransforms({
88
+ const variantTokens = getTransforms({
89
+ ...query,
64
90
  format: FORMAT_ID$1,
65
- id: value,
66
- mode
91
+ id: value
92
+ });
93
+ if (!variantTokens.length) logger.warn({
94
+ ...msg,
95
+ message: `${value} matched 0 tokens`
67
96
  });
68
- for (const token of tokens) {
97
+ for (const token of variantTokens) {
69
98
  let relName = token.id.split(".").at(-1);
70
99
  for (const subgroup of [...Array.isArray(value) ? value : [value]]) {
71
100
  const match = subgroup.replace(/\*.*/, "");
72
101
  relName = token.id.replace(match, "");
73
102
  }
74
103
  setTransform(token.id, {
104
+ ...query,
75
105
  format: FORMAT_ID,
76
106
  localID: makeCSSVar(`${path.join("-")}-${relName.replace(/\./g, "-")}`),
77
- value: typeof token.value === "object" ? token.value["."] : token.value,
78
- mode: variant
107
+ value: typeof token.value === "object" ? token.value["."] : token.value
79
108
  });
80
109
  }
81
110
  }
82
111
  }
83
112
  },
84
113
  async build({ getTransforms, outputFile }) {
85
- const themeOutput = [];
86
- const variants = { ".": [] };
87
- for (const token of getTransforms({
88
- format: FORMAT_ID,
89
- mode: "*"
90
- })) {
91
- const { localID, value, mode } = token;
92
- if (!variants[mode]) variants[mode] = [];
93
- 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";
94
123
  }
95
- for (const [variant, values] of Object.entries(variants)) {
96
- themeOutput.push(variant === "." ? "@theme {" : `@variant ${variant} {`);
97
- for (const value of values) themeOutput.push(` ${value}`);
98
- themeOutput.push("}", "");
99
- }
100
- const generatedTheme = themeOutput.join("\n");
101
- let finalOutput;
124
+ let finalOutput = "";
102
125
  if (options.template) {
103
126
  const templateUrl = new URL(options.template, cwd);
104
- finalOutput = applyTemplate(fs.readFileSync(fileURLToPath(templateUrl), "utf8"), generatedTheme);
105
- } else finalOutput = [
106
- "@import \"tailwindcss\";",
107
- "",
108
- generatedTheme
109
- ].join("\n");
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;
132
+ }
110
133
  outputFile(filename, [
111
134
  buildFileHeader(options.template),
112
135
  "",
@@ -115,7 +138,15 @@ function pluginTailwind(options) {
115
138
  }
116
139
  };
117
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
+ }
118
149
 
119
150
  //#endregion
120
- export { FILE_HEADER, FORMAT_ID, TERRAZZO_SLOT, applyTemplate, buildFileHeader, pluginTailwind as default, flattenThemeObj };
151
+ export { FILE_HEADER, FORMAT_ID, PLUGIN_NAME, TERRAZZO_SLOT, applyTemplate, buildFileHeader, pluginTailwind as default, flattenThemeObj };
121
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 /**\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 /** 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\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 fs from 'node:fs';\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 { applyTemplate, buildFileHeader, 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 ?? 'tailwind-theme.css';\n let cwd: URL;\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 // store cwd for template resolution (parent of outDir)\n cwd = new URL('./', config.outDir);\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 themeOutput: string[] = [];\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 themeOutput.push(variant === '.' ? '@theme {' : `@variant ${variant} {`);\n for (const value of values) {\n themeOutput.push(` ${value}`);\n }\n themeOutput.push('}', '');\n }\n const generatedTheme = themeOutput.join('\\n');\n\n // build the output combining theme and template\n let finalOutput: string;\n if (options.template) {\n const templateUrl = new URL(options.template, cwd);\n const templateContent = fs.readFileSync(fileURLToPath(templateUrl), 'utf8');\n finalOutput = applyTemplate(templateContent, generatedTheme);\n } else {\n finalOutput = ['@import \"tailwindcss\";', '', generatedTheme].join('\\n');\n }\n\n const header = buildFileHeader(options.template);\n outputFile(filename, [header, '', finalOutput].join('\\n'));\n },\n };\n}\n"],"mappings":"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"}
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.4",
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.4",
32
- "@terrazzo/parser": "^2.0.0-beta.4",
33
- "@terrazzo/plugin-css": "^2.0.0-beta.4",
34
- "@terrazzo/token-tools": "^2.0.0-beta.4"
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.4"
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.4",
42
- "@terrazzo/parser": "^2.0.0-beta.4",
43
- "@terrazzo/plugin-css": "^2.0.0-beta.4"
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 .",