@ui5/webcomponents-tools 0.0.0-47cc17a26 → 0.0.0-49bade48d

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +327 -1
  2. package/assets-meta.js +6 -4
  3. package/bin/dev.js +9 -4
  4. package/bin/ui5nps.js +44 -8
  5. package/components-package/eslint.js +1 -1
  6. package/components-package/nps.js +23 -18
  7. package/icons-collection/nps.js +1 -1
  8. package/lib/amd-to-es6/index.js +3 -1
  9. package/lib/cem/cem.js +4 -0
  10. package/lib/cem/custom-elements-manifest.config.mjs +88 -4
  11. package/lib/cem/merge.mjs +220 -0
  12. package/lib/cem/schema-internal.json +41 -1
  13. package/lib/cem/schema.json +41 -1
  14. package/lib/cem/types-internal.d.ts +32 -2
  15. package/lib/cem/types.d.ts +32 -2
  16. package/lib/cem/utils.mjs +13 -3
  17. package/lib/cem/validate.js +7 -2
  18. package/lib/chokidar/chokidar.js +28 -0
  19. package/lib/copy-and-watch/index.js +5 -0
  20. package/lib/copy-list/index.js +3 -1
  21. package/lib/create-icons/index.js +18 -13
  22. package/lib/create-illustrations/index.js +46 -4
  23. package/lib/css-processors/css-processor-components.mjs +14 -3
  24. package/lib/css-processors/css-processor-themes.mjs +148 -24
  25. package/lib/css-processors/merge-light-dark.mjs +131 -0
  26. package/lib/css-processors/postcss-plugin.mjs +153 -0
  27. package/lib/css-processors/scope-variables.mjs +26 -1
  28. package/lib/css-processors/shared.mjs +7 -6
  29. package/lib/eslint/eslint.js +44 -0
  30. package/lib/generate-js-imports/illustrations.js +3 -1
  31. package/lib/generate-json-imports/i18n.js +3 -1
  32. package/lib/generate-json-imports/themes.js +6 -2
  33. package/lib/i18n/defaults.js +3 -1
  34. package/lib/i18n/toJSON.js +28 -4
  35. package/lib/test-runner/test-runner.js +56 -48
  36. package/lib/vite-bundler/vite-bundler.mjs +35 -0
  37. package/package.json +2 -4
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const extenalSchema = require('./schema.json');
6
6
  const internalSchema = require('./schema-internal.json');
7
7
 
8
+ const isVerbose = () => process.env.UI5_VERBOSE === "true";
8
9
 
9
10
  const validateFn = async () => {
10
11
  // Load your JSON data from the input file
@@ -49,7 +50,9 @@ const validateFn = async () => {
49
50
  // Validate the JSON data against the schema
50
51
  if (devMode) {
51
52
  if (validate(inputDataInternal)) {
52
- console.log('Internal custom element manifest is validated successfully');
53
+ if (isVerbose()) {
54
+ console.log('Internal custom element manifest is validated successfully');
55
+ }
53
56
  } else {
54
57
  console.log(validate.errors)
55
58
  throw new Error(`Validation of internal custom elements manifest failed: ${validate.errors}`);
@@ -61,7 +64,9 @@ const validateFn = async () => {
61
64
 
62
65
  // Validate the JSON data against the schema
63
66
  if (validate(inputDataExternal)) {
64
- console.log('Custom element manifest is validated successfully');
67
+ if (isVerbose()) {
68
+ console.log('Custom element manifest is validated successfully');
69
+ }
65
70
  fs.writeFileSync(inputFilePath, JSON.stringify(inputDataExternal, null, 2), 'utf8');
66
71
  fs.writeFileSync(inputFilePath.replace("custom-elements", "custom-elements-internal"), JSON.stringify(inputDataInternal, null, 2), 'utf8');
67
72
  } else if (devMode) {
@@ -0,0 +1,28 @@
1
+ const chokidar = require('chokidar');
2
+ const { exec } = require("child_process");
3
+
4
+ const main = async (argv) => {
5
+ if (argv.length < 4) {
6
+ console.error("Please provide a file pattern to watch and a command to execute on changes.");
7
+ console.error("<file-pattern> <command>");
8
+ process.exit(1);
9
+ }
10
+
11
+ const filePattern = argv[2];
12
+ const command = argv.slice(3).join(' ');
13
+
14
+ const watcher = new chokidar.FSWatcher();
15
+
16
+ watcher.add(filePattern);
17
+ watcher.unwatch("src/generated");
18
+
19
+ watcher.on('change', async () => {
20
+ exec(command);
21
+ });
22
+ };
23
+
24
+ if (require.main === module) {
25
+ main(process.argv)
26
+ }
27
+
28
+ exports._ui5mainFn = main;
@@ -42,6 +42,11 @@ const copyAndWatchFn = async (argv) => {
42
42
  }
43
43
  });
44
44
 
45
+ // Default to silent mode unless verbose is enabled
46
+ if (process.env.UI5_VERBOSE !== "true") {
47
+ options.silent = true;
48
+ }
49
+
45
50
  if (args.length < 2) {
46
51
  console.error('Not enough arguments: copy-and-watch [options] <sources> <target>');
47
52
  process.exit(1);
@@ -22,7 +22,9 @@ const generate = async (argv) => {
22
22
  });
23
23
 
24
24
  return Promise.all(promises).then(() => {
25
- console.log("Files copied.");
25
+ if (process.env.UI5_VERBOSE === "true") {
26
+ console.log("Files copied.");
27
+ }
26
28
  });
27
29
  };
28
30
 
@@ -1,40 +1,42 @@
1
1
  const fs = require("fs").promises;
2
2
  const path = require("path");
3
3
 
4
- const iconTemplate = (name, pathData, ltr, collection, packageName) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
4
+ const iconTemplate = (name, pathData, ltr, viewBox, collection, packageName) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
5
5
 
6
6
  const name = "${name}";
7
7
  const pathData = "${pathData}";
8
8
  const ltr = ${ltr};
9
9
  const accData = null;
10
+ const viewBox = "${viewBox}";
10
11
  const collection = "${collection}";
11
12
  const packageName = "${packageName}";
12
13
 
13
- registerIcon(name, { pathData, ltr, collection, packageName });
14
+ registerIcon(name, { pathData, ltr, viewBox, collection, packageName });
14
15
 
15
16
  export default "${collection}/${name}";
16
- export { pathData, ltr, accData };`;
17
+ export { pathData, ltr, viewBox, accData };`;
17
18
 
18
19
 
19
- const iconAccTemplate = (name, pathData, ltr, accData, collection, packageName, versioned) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
20
+ const iconAccTemplate = (name, pathData, ltr, viewBox, accData, collection, packageName, versioned) => `import { registerIcon } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
20
21
  import { ${accData.key} } from "${versioned ? "../" : "./"}generated/i18n/i18n-defaults.js";
21
22
 
22
23
  const name = "${name}";
23
24
  const pathData = "${pathData}";
24
25
  const ltr = ${ltr};
25
26
  const accData = ${accData.key};
27
+ const viewBox = "${viewBox}";
26
28
  const collection = "${collection}";
27
29
  const packageName = "${packageName}";
28
30
 
29
- registerIcon(name, { pathData, ltr, accData, collection, packageName });
31
+ registerIcon(name, { pathData, ltr, viewBox, accData, collection, packageName });
30
32
 
31
33
  export default "${collection}/${name}";
32
- export { pathData, ltr, accData };`;
34
+ export { pathData, ltr, viewBox, accData };`;
33
35
 
34
36
 
35
37
 
36
38
  const collectionTemplate = (name, versions, fullName) => `import { isLegacyThemeFamilyAsync } from "@ui5/webcomponents-base/dist/config/Theme.js";
37
- import { pathData as pathData${versions[0]}, ltr, accData } from "./${versions[0]}/${name}.js";
39
+ import { pathData as pathData${versions[0]}, ltr, viewBox, accData } from "./${versions[0]}/${name}.js";
38
40
  import { pathData as pathData${versions[1]} } from "./${versions[1]}/${name}.js";
39
41
 
40
42
  const getPathData = async() => {
@@ -42,27 +44,29 @@ const getPathData = async() => {
42
44
  };
43
45
 
44
46
  export default "${fullName}";
45
- export { getPathData, ltr, accData };`;
47
+ export { getPathData, ltr, viewBox, accData };`;
46
48
 
47
49
 
48
50
  const typeDefinitionTemplate = (name, accData, collection) => `declare const pathData: string;
49
51
  declare const ltr: boolean;
52
+ declare const viewBox: string;
50
53
  declare const accData: ${accData ? '{ key: string; defaultText: string; }' : null}
51
54
  declare const _default: "${collection}/${name}";
52
55
 
53
56
  export default _default;
54
- export { pathData, ltr, accData };`
57
+ export { pathData, ltr, viewBox, accData };`
55
58
 
56
59
  const collectionTypeDefinitionTemplate = (name, accData) => `declare const getPathData: () => Promise<string>;
57
60
  declare const ltr: boolean;
61
+ declare const viewBox: string;
58
62
  declare const accData: ${accData ? '{ key: string; defaultText: string; }' : null}
59
63
  declare const _default: "${name}";
60
64
 
61
65
  export default _default;
62
- export { getPathData, ltr, accData };`
66
+ export { getPathData, ltr, viewBox, accData };`
63
67
 
64
68
 
65
- const svgTemplate = (pathData) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
69
+ const svgTemplate = (pathData, viewBox) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}">
66
70
  <path d="${pathData}"/>
67
71
  </svg>`;
68
72
 
@@ -80,15 +84,16 @@ const createIcons = async (argv) => {
80
84
  const iconData = json.data[name];
81
85
  const pathData = iconData.path;
82
86
  const ltr = !!iconData.ltr;
87
+ const viewBox = iconData.viewBox || "0 0 512 512";
83
88
  const acc = iconData.acc;
84
89
  const packageName = json.packageName;
85
90
  const collection = json.collection;
86
91
  const versioned = json.version;
87
92
 
88
- const content = acc ? iconAccTemplate(name, pathData, ltr, acc, collection, packageName, versioned) : iconTemplate(name, pathData, ltr, collection, packageName);
93
+ const content = acc ? iconAccTemplate(name, pathData, ltr, viewBox, acc, collection, packageName, versioned) : iconTemplate(name, pathData, ltr, viewBox, collection, packageName);
89
94
 
90
95
  promises.push(fs.writeFile(path.join(destDir, `${name}.js`), content));
91
- promises.push(fs.writeFile(path.join(destDir, `${name}.svg`), svgTemplate(pathData)));
96
+ promises.push(fs.writeFile(path.join(destDir, `${name}.svg`), svgTemplate(pathData, viewBox)));
92
97
  promises.push(fs.writeFile(path.join(destDir, `${name}.d.ts`), typeDefinitionTemplate(name, acc, collection)));
93
98
 
94
99
  // For versioned icons collections, the script creates top level (unversioned) module that internally imports the versioned ones.
@@ -80,15 +80,38 @@ const generate = async (argv) => {
80
80
  const fileNames = new Set();
81
81
 
82
82
  let dotIllustrationNames = [];
83
+ let v5IllustrationNames = new Set();
83
84
 
84
85
  try {
85
86
  await fs.access(srcPath);
86
87
  } catch (error) {
87
- console.log(`The path ${srcPath} does not exist.`);
88
+ if (process.env.UI5_VERBOSE === "true") {
89
+ console.log(`The path ${srcPath} does not exist.`);
90
+ }
88
91
  return Promise.resolve(null);
89
92
  }
90
93
 
91
- console.log(`Generating illustrations from ${srcPath} to ${destPath}`)
94
+ // For V4 TNT illustrations, check which ones have V5 versions available
95
+ // so we only register V5 loaders for illustrations that actually exist
96
+ if (collection === "V4" && illustrationSet === "tnt") {
97
+ const v5Path = srcPath.replace("/illustrations/", "/illustrations-v5/");
98
+ try {
99
+ const v5Files = await fs.readdir(path.normalize(v5Path));
100
+ const v5Pattern = new RegExp(`${illustrationsPrefix}-.+-(.+).svg`);
101
+ v5Files.forEach(file => {
102
+ const match = file.match(v5Pattern);
103
+ if (match) {
104
+ v5IllustrationNames.add(match[1]);
105
+ }
106
+ });
107
+ } catch (error) {
108
+ // V5 path doesn't exist, no V5 illustrations available
109
+ }
110
+ }
111
+
112
+ if (process.env.UI5_VERBOSE === "true") {
113
+ console.log(`Generating illustrations from ${srcPath} to ${destPath}`);
114
+ }
92
115
 
93
116
  const svgImportTemplate = svgContent => {
94
117
  return `export default \`${svgContent}\`;`
@@ -121,6 +144,23 @@ const generate = async (argv) => {
121
144
  // If no Dot is present, Spot will be imported as Dot
122
145
  const hasDot = dotIllustrationNames.indexOf(illustrationName) !== -1 ? 'Dot' : 'Spot';
123
146
 
147
+ // For V4 TNT illustrations that have V5 versions, register loaders for V5 and V5/HC
148
+ // so that when the illustration is imported directly (e.g., "@ui5/webcomponents-fiori/dist/illustrations/tnt/NoApplications.js")
149
+ // and Horizon themes are used, the correct V5 illustration is loaded dynamically.
150
+ // Note: Only TNT set has V5 illustrations, and not all TNT illustrations have V5 versions.
151
+ // The dynamic imports ensure that V5 SVGs are loaded lazily only when actually needed.
152
+ const hasV5Version = v5IllustrationNames.has(illustrationName);
153
+ const v5LoaderRegistration = collection === "V4" && illustrationSet === "tnt" && hasV5Version ? `
154
+ import { registerIllustrationLoader } from "@ui5/webcomponents-base/dist/asset-registries/Illustrations.js";
155
+
156
+ registerIllustrationLoader("${illustrationSet}/V5/${illustrationName}", async function loadIllustrationV5() {
157
+ return (await import("../../illustrations-v5/${illustrationSet}/${illustrationName}.js")).default;
158
+ });
159
+ registerIllustrationLoader("${illustrationSet}/V5/HC/${illustrationName}", async function loadIllustrationV5HC() {
160
+ return (await import("../../illustrations-v5/${illustrationSet}/hc/${illustrationName}.js")).default;
161
+ });
162
+ ` : '';
163
+
124
164
  return `import { unsafeRegisterIllustration } from "@ui5/webcomponents-base/dist/asset-registries/Illustrations.js";
125
165
  import dialogSvg from "./${illustrationsPrefix}-Dialog-${illustrationName}.js";
126
166
  import sceneSvg from "./${illustrationsPrefix}-Scene-${illustrationName}.js";
@@ -128,7 +168,7 @@ import spotSvg from "./${illustrationsPrefix}-Spot-${illustrationName}.js";
128
168
  import dotSvg from "./${illustrationsPrefix}-${hasDot}-${illustrationName}.js";${defaultText ? `import {
129
169
  IM_TITLE_${illustrationNameUpperCase},
130
170
  IM_SUBTITLE_${illustrationNameUpperCase},
131
- } from "../generated/i18n/i18n-defaults.js";` : ``}
171
+ } from "../generated/i18n/i18n-defaults.js";` : ``}${v5LoaderRegistration}
132
172
 
133
173
  const name = "${illustrationName}";
134
174
  const set = "${illustrationSet}";
@@ -193,7 +233,9 @@ export { dialogSvg, sceneSvg, spotSvg, dotSvg };`
193
233
  return Promise.all(nestedPromises);
194
234
  })
195
235
  .then(() => {
196
- console.log("Illustrations generated.");
236
+ if (process.env.UI5_VERBOSE === "true") {
237
+ console.log("Illustrations generated.");
238
+ }
197
239
  });
198
240
  };
199
241
 
@@ -8,11 +8,15 @@ import scopeVariables from "./scope-variables.mjs";
8
8
  import { writeFileIfChanged, getFileContent } from "./shared.mjs";
9
9
  import { pathToFileURL } from "url";
10
10
 
11
+
11
12
  const generate = async (argv) => {
13
+ const CSS_VARIABLES_TARGET = process.env.CSS_VARIABLES_TARGET === "host";
12
14
  const tsMode = process.env.UI5_TS === "true";
13
15
  const extension = tsMode ? ".css.ts" : ".css.js";
14
16
 
15
- const packageJSON = JSON.parse(fs.readFileSync("./package.json"))
17
+ const packageJSON = JSON.parse(fs.readFileSync("./package.json"));
18
+ const basePackageJSON = (await import("@ui5/webcomponents-base/package.json", { with: { type: "json" } })).default;
19
+
16
20
  const inputFilesGlob = "src/themes/*.css";
17
21
  const restArgs = argv.slice(2);
18
22
 
@@ -23,8 +27,15 @@ const generate = async (argv) => {
23
27
 
24
28
  build.onEnd(result => {
25
29
  result.outputFiles.forEach(async f => {
26
- // scoping
27
- let newText = scopeVariables(f.text, packageJSON);
30
+ let newText
31
+
32
+ if (CSS_VARIABLES_TARGET) {
33
+ newText = f.text;
34
+ } else {
35
+ // scoping
36
+ newText = scopeVariables(f.text, basePackageJSON);
37
+ }
38
+
28
39
  newText = newText.replaceAll(/\\/g, "\\\\"); // Escape backslashes as they might appear in css rules
29
40
  await mkdir(path.dirname(f.path), { recursive: true });
30
41
  writeFile(f.path, newText);
@@ -5,24 +5,59 @@ import * as path from "path";
5
5
  import { writeFile, mkdir } from "fs/promises";
6
6
  import postcss from "postcss";
7
7
  import combineDuplicatedSelectors from "../postcss-combine-duplicated-selectors/index.js"
8
+ import postcssPlugin from "./postcss-plugin.mjs";
8
9
  import { writeFileIfChanged, getFileContent } from "./shared.mjs";
9
10
  import scopeVariables from "./scope-variables.mjs";
11
+ import { mergeLightDark } from "./merge-light-dark.mjs";
10
12
  import { pathToFileURL } from "url";
11
13
 
14
+ /**
15
+ * Auto theme pairs: each entry defines a light theme, its dark counterpart,
16
+ * and the auto theme name that will be generated by merging them.
17
+ */
18
+ const AUTO_THEME_PAIRS = [
19
+ { light: "sap_horizon", dark: "sap_horizon_dark", auto: "sap_horizon_auto" },
20
+ { light: "sap_horizon_hcw", dark: "sap_horizon_hcb", auto: "sap_horizon_hc_auto" },
21
+ ];
22
+
12
23
  const generate = async (argv) => {
24
+ const CSS_VARIABLES_TARGET = process.env.CSS_VARIABLES_TARGET === "host";
13
25
  const tsMode = process.env.UI5_TS === "true";
14
26
  const extension = tsMode ? ".css.ts" : ".css.js";
15
27
 
16
- const packageJSON = JSON.parse(fs.readFileSync("./package.json"))
28
+ const packageJSON = JSON.parse(fs.readFileSync("./package.json"));
29
+ const basePackageJSON = (await import("@ui5/webcomponents-base/package.json", { with: { type: "json" } })).default;
17
30
 
18
- const inputFiles = await globby([
31
+ const allInputFiles = await globby([
19
32
  "src/**/parameters-bundle.css",
20
33
  ]);
34
+
35
+ // Filter out auto theme placeholders - they will be generated from merging light + dark
36
+ const autoThemeNames = AUTO_THEME_PAIRS.map(p => p.auto);
37
+ const inputFiles = allInputFiles.filter(f => !autoThemeNames.some(name => f.includes(`/${name}/`)));
38
+
21
39
  const restArgs = argv.slice(2);
22
40
 
41
+ const saveFiles = async (distPath, css, suffix = "") => {
42
+ await mkdir(path.dirname(distPath), { recursive: true });
43
+ writeFile(distPath.replace(".css", suffix + ".css"), css);
44
+
45
+ // JSON
46
+ const jsonPath = distPath.replace(/dist[\/\\]css/, "dist/generated/assets").replace(".css", suffix + ".css.json");
47
+ await mkdir(path.dirname(jsonPath), { recursive: true });
48
+ writeFileIfChanged(jsonPath, JSON.stringify(css));
49
+
50
+ // JS/TS
51
+ const jsPath = distPath.replace(/dist[\/\\]css/, "src/generated/").replace(".css", suffix + extension);
52
+ const jsContent = getFileContent(packageJSON.name, "\`" + css + "\`");
53
+ writeFileIfChanged(jsPath, jsContent);
54
+ }
55
+
56
+ const isThemingPackage = (filePath) => filePath.includes("packages/theming") || filePath.includes("packages\\theming");
57
+
23
58
  const processThemingPackageFile = async (f) => {
24
59
  const selector = ':root';
25
- const result = await postcss().process(f.text);
60
+ const result = await postcss().process(f.text, { from: undefined });
26
61
 
27
62
  const newRule = postcss.rule({ selector });
28
63
 
@@ -34,37 +69,75 @@ const generate = async (argv) => {
34
69
  });
35
70
  });
36
71
 
37
- return newRule.toString();
72
+ return { css: newRule.toString() };
38
73
  };
39
74
 
40
75
  const processComponentPackageFile = async (f) => {
41
- const result = await postcss(combineDuplicatedSelectors).process(f.text);
76
+ if (CSS_VARIABLES_TARGET) {
77
+ const result = await postcss([
78
+ combineDuplicatedSelectors,
79
+ postcssPlugin
80
+ ]).process(f.text, { from: undefined });
81
+
82
+ return { css: result.css };
83
+ }
42
84
 
43
- return scopeVariables(result.css, packageJSON, f.path);
85
+
86
+ const combined = await postcss([
87
+ combineDuplicatedSelectors,
88
+ ]).process(f.text, { from: undefined });
89
+
90
+ return { css: scopeVariables(combined.css, basePackageJSON, f.path) };
44
91
  }
45
92
 
93
+ /**
94
+ * Generate auto theme CSS by merging light and dark theme outputs.
95
+ * For the theming package (:root), adds color-scheme and toggle variables.
96
+ * For component packages (:host), just merges the variables.
97
+ */
98
+ const generateAutoThemes = async (processedCSS) => {
99
+ for (const { light, dark, auto } of AUTO_THEME_PAIRS) {
100
+ for (const [lightPath, lightCSS] of processedCSS) {
101
+ // Match paths containing the light theme folder
102
+ const lightPattern = new RegExp(`[/\\\\]${light}[/\\\\]`);
103
+ if (!lightPattern.test(lightPath)) continue;
104
+
105
+ const darkPath = lightPath.replace(lightPattern, (match) => match.replace(light, dark));
106
+ const darkCSS = processedCSS.get(darkPath);
107
+ if (!darkCSS) continue;
108
+
109
+ const selector = isThemingPackage(lightPath) ? ":root" : ":host";
110
+ let autoCSS = mergeLightDark(lightCSS, darkCSS, selector);
111
+
112
+ // For the theming package, add color-scheme and toggle setup
113
+ if (selector === ":root") {
114
+ autoCSS = addAutoThemeSetup(autoCSS);
115
+ }
116
+
117
+ const autoPath = lightPath.replace(lightPattern, (match) => match.replace(light, auto));
118
+ await saveFiles(autoPath, autoCSS);
119
+ }
120
+ }
121
+ };
122
+
46
123
  let scopingPlugin = {
47
124
  name: 'scoping',
48
125
  setup(build) {
49
126
  build.initialOptions.write = false;
50
127
 
51
- build.onEnd(result => {
52
- result.outputFiles.forEach(async f => {
53
- let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f);
128
+ build.onEnd(async (result) => {
129
+ const processedCSS = new Map();
54
130
 
55
- await mkdir(path.dirname(f.path), { recursive: true });
56
- writeFile(f.path, newText);
131
+ // Process all real theme files
132
+ await Promise.all(result.outputFiles.map(async f => {
133
+ let { css } = isThemingPackage(f.path) ? await processThemingPackageFile(f) : await processComponentPackageFile(f);
57
134
 
58
- // JSON
59
- const jsonPath = f.path.replace(/dist[\/\\]css/, "dist/generated/assets").replace(".css", ".css.json");
60
- await mkdir(path.dirname(jsonPath), { recursive: true });
61
- writeFileIfChanged(jsonPath, JSON.stringify(newText));
135
+ await saveFiles(f.path, css);
136
+ processedCSS.set(f.path, css);
137
+ }));
62
138
 
63
- // JS/TS
64
- const jsPath = f.path.replace(/dist[\/\\]css/, "src/generated/").replace(".css", extension);
65
- const jsContent = getFileContent(packageJSON.name, "\`" + newText + "\`");
66
- writeFileIfChanged(jsPath, jsContent);
67
- });
139
+ // Generate auto themes by merging light + dark
140
+ await generateAutoThemes(processedCSS);
68
141
  })
69
142
  },
70
143
  }
@@ -75,6 +148,7 @@ const generate = async (argv) => {
75
148
  minify: true,
76
149
  outdir: 'dist/css',
77
150
  outbase: 'src',
151
+ logLevel: process.env.UI5_VERBOSE === "true" ? "warning" : "error",
78
152
  plugins: [
79
153
  scopingPlugin,
80
154
  ],
@@ -90,13 +164,63 @@ const generate = async (argv) => {
90
164
  }
91
165
  }
92
166
 
167
+ /**
168
+ * Adds auto theme setup CSS: color-scheme declaration and
169
+ * light/dark toggle variables with @media switching.
170
+ *
171
+ * In light mode (default):
172
+ * --_ui5-light-scheme is guaranteed-invalid → var() fallbacks resolve to light values
173
+ * --_ui5-dark-scheme is valid empty → var() fallbacks are NOT used
174
+ *
175
+ * In dark mode (@media prefers-color-scheme: dark):
176
+ * --_ui5-light-scheme is valid empty → var() fallbacks are NOT used
177
+ * --_ui5-dark-scheme is guaranteed-invalid → var() fallbacks resolve to dark values
178
+ *
179
+ * @param {string} mergedCSS - The merged :root CSS from mergeLightDark()
180
+ * @returns {string} CSS with toggle setup and @media rule appended
181
+ */
182
+ const addAutoThemeSetup = (mergedCSS) => {
183
+ const root = postcss.parse(mergedCSS);
184
+
185
+ // Add color-scheme and toggle defaults to the :root rule
186
+ root.walkRules(":root", rule => {
187
+ rule.prepend(
188
+ postcss.decl({ prop: "--_ui5-dark-scheme", value: " " }),
189
+ );
190
+ rule.prepend(
191
+ postcss.decl({ prop: "--_ui5-light-scheme", value: "var(--_ui5-f2d95f8)" }),
192
+ );
193
+ rule.prepend(
194
+ postcss.decl({ prop: "color-scheme", value: "light dark" }),
195
+ );
196
+ });
197
+
198
+ // Add @media rule for dark mode - flips the toggles
199
+ const mediaRule = postcss.atRule({
200
+ name: "media",
201
+ params: "(prefers-color-scheme: dark)",
202
+ });
203
+ const darkRoot = postcss.rule({ selector: ":root" });
204
+ darkRoot.append(
205
+ postcss.decl({ prop: "--_ui5-light-scheme", value: " " }),
206
+ postcss.decl({ prop: "--_ui5-dark-scheme", value: "var(--_ui5-f2d95f8)" }),
207
+ postcss.decl({ prop: "color-scheme", value: "dark" }),
208
+ );
209
+ mediaRule.append(darkRoot);
210
+ root.append(mediaRule);
211
+
212
+ return root.toString();
213
+ };
214
+
93
215
  const filePath = process.argv[1];
94
- const fileUrl = pathToFileURL(filePath).href;
95
216
 
96
- if (import.meta.url === fileUrl) {
97
- generate(process.argv)
217
+ if (filePath) {
218
+ const fileUrl = pathToFileURL(filePath).href;
219
+ if (import.meta.url === fileUrl) {
220
+ generate(process.argv)
221
+ }
98
222
  }
99
223
 
100
224
  export default {
101
225
  _ui5mainFn: generate
102
- }
226
+ }
@@ -0,0 +1,131 @@
1
+ import postcss from "postcss";
2
+
3
+ /**
4
+ * Merges two CSS theme files (light and dark) into a single file using
5
+ * CSS light-dark() for color values and space toggles for non-color values.
6
+ *
7
+ * The space toggle pattern uses --_ui5-dark-scheme / --_ui5-light-scheme,
8
+ * following the same approach as the compact/cozy density toggles.
9
+ *
10
+ * @param {string} lightCSS - The light theme CSS string
11
+ * @param {string} darkCSS - The dark theme CSS string
12
+ * @param {string} selector - The selector to match (":root" for theming package, ":host" for component packages)
13
+ * @returns {string} Merged CSS string with light-dark() and space toggle values
14
+ */
15
+ const mergeLightDark = (lightCSS, darkCSS, selector = ":root") => {
16
+ const lightRoot = postcss.parse(lightCSS);
17
+ const darkRoot = postcss.parse(darkCSS);
18
+
19
+ // Extract all custom property declarations from each theme
20
+ const lightVars = extractVars(lightRoot, selector);
21
+ const darkVars = extractVars(darkRoot, selector);
22
+
23
+ // Build merged declarations
24
+ const allProps = new Set([...lightVars.keys(), ...darkVars.keys()]);
25
+ const mergedRule = postcss.rule({ selector });
26
+
27
+ for (const prop of allProps) {
28
+ const lightVal = lightVars.get(prop);
29
+ const darkVal = darkVars.get(prop);
30
+
31
+ if (prop.startsWith("--sapThemeMetaData")) {
32
+ // Keep light version only for metadata
33
+ if (lightVal !== undefined) {
34
+ mergedRule.append(postcss.decl({ prop, value: lightVal }));
35
+ }
36
+ continue;
37
+ }
38
+
39
+ if (lightVal !== undefined && darkVal !== undefined) {
40
+ if (lightVal === darkVal) {
41
+ // Identical values - output once
42
+ mergedRule.append(postcss.decl({ prop, value: lightVal }));
43
+ } else if (isColorValue(lightVal) && isColorValue(darkVal)) {
44
+ // Different color values - use light-dark()
45
+ mergedRule.append(postcss.decl({ prop, value: `light-dark(${lightVal}, ${darkVal})` }));
46
+ } else {
47
+ // Different non-color values - use space toggle
48
+ mergedRule.append(postcss.decl({
49
+ prop,
50
+ value: `var(--_ui5-light-scheme, ${lightVal}) var(--_ui5-dark-scheme, ${darkVal})`,
51
+ }));
52
+ }
53
+ } else if (lightVal !== undefined) {
54
+ // Only in light theme - use space toggle with guaranteed-invalid for dark
55
+ mergedRule.append(postcss.decl({
56
+ prop,
57
+ value: `var(--_ui5-light-scheme, ${lightVal}) var(--_ui5-dark-scheme, var(--_ui5-f2d95f8))`,
58
+ }));
59
+ } else {
60
+ // Only in dark theme - use space toggle with guaranteed-invalid for light
61
+ mergedRule.append(postcss.decl({
62
+ prop,
63
+ value: `var(--_ui5-dark-scheme, ${darkVal}) var(--_ui5-light-scheme, var(--_ui5-f2d95f8))`,
64
+ }));
65
+ }
66
+ }
67
+
68
+ return mergedRule.toString();
69
+ };
70
+
71
+ /**
72
+ * Extract custom property declarations from a parsed CSS root for a given selector.
73
+ * @param {postcss.Root} root
74
+ * @param {string} selector
75
+ * @returns {Map<string, string>}
76
+ */
77
+ const extractVars = (root, selector) => {
78
+ const vars = new Map();
79
+ root.walkRules(rule => {
80
+ if (rule.selector === selector) {
81
+ rule.walkDecls(decl => {
82
+ if (decl.prop.startsWith("--")) {
83
+ vars.set(decl.prop, decl.value);
84
+ }
85
+ });
86
+ }
87
+ });
88
+ return vars;
89
+ };
90
+
91
+ /**
92
+ * Heuristic to determine if a CSS value is a color value.
93
+ * light-dark() only works with <color> values.
94
+ */
95
+ const isColorValue = (value) => {
96
+ const v = value.trim().toLowerCase();
97
+
98
+ // hex colors
99
+ if (/^#[0-9a-f]{3,8}$/.test(v)) return true;
100
+
101
+ // rgb/rgba/hsl/hsla functions
102
+ if (/^(rgba?|hsla?|oklch|oklab|lab|lch|color)\s*\(/.test(v)) return true;
103
+
104
+ // transparent and common CSS color keywords
105
+ if (v === "transparent" || v === "currentcolor") return true;
106
+
107
+ // Named CSS colors - check for common ones used in SAP themes
108
+ const namedColors = new Set([
109
+ "black", "white", "red", "green", "blue", "yellow", "orange", "purple",
110
+ "pink", "gray", "grey", "brown", "cyan", "magenta", "lime", "navy",
111
+ "teal", "aqua", "silver", "maroon", "olive", "fuchsia",
112
+ ]);
113
+ if (namedColors.has(v)) return true;
114
+
115
+ // var() references to SAP color variables
116
+ // Match patterns like var(--sapXxxColor), var(--sapXxx_Background), etc.
117
+ if (/^var\(--sap\w*(Color|Background|BorderColor|TextColor|IconColor|ForegroundColor|HoverColor|ActiveColor|SelectedColor|Shadow)\w*\)$/.test(value.trim())) return true;
118
+
119
+ // Compound values containing color-related var() references
120
+ // e.g., "0.0625rem solid var(--sapContent_FocusColor)"
121
+ if (/var\(--sap\w*(Color|BorderColor|TextColor|IconColor|ForegroundColor|SelectedColor)\w*\)/.test(value)) {
122
+ // If the value also contains dimensional tokens, it's likely a shorthand (border, box-shadow)
123
+ // These are NOT pure color values - light-dark() won't work
124
+ if (/\d+(\.\d+)?(rem|px|em|%)/.test(value)) return false;
125
+ return true;
126
+ }
127
+
128
+ return false;
129
+ };
130
+
131
+ export { mergeLightDark, isColorValue };