@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.
- package/CHANGELOG.md +327 -1
- package/assets-meta.js +6 -4
- package/bin/dev.js +9 -4
- package/bin/ui5nps.js +44 -8
- package/components-package/eslint.js +1 -1
- package/components-package/nps.js +23 -18
- package/icons-collection/nps.js +1 -1
- package/lib/amd-to-es6/index.js +3 -1
- package/lib/cem/cem.js +4 -0
- package/lib/cem/custom-elements-manifest.config.mjs +88 -4
- package/lib/cem/merge.mjs +220 -0
- package/lib/cem/schema-internal.json +41 -1
- package/lib/cem/schema.json +41 -1
- package/lib/cem/types-internal.d.ts +32 -2
- package/lib/cem/types.d.ts +32 -2
- package/lib/cem/utils.mjs +13 -3
- package/lib/cem/validate.js +7 -2
- package/lib/chokidar/chokidar.js +28 -0
- package/lib/copy-and-watch/index.js +5 -0
- package/lib/copy-list/index.js +3 -1
- package/lib/create-icons/index.js +18 -13
- package/lib/create-illustrations/index.js +46 -4
- package/lib/css-processors/css-processor-components.mjs +14 -3
- package/lib/css-processors/css-processor-themes.mjs +148 -24
- package/lib/css-processors/merge-light-dark.mjs +131 -0
- package/lib/css-processors/postcss-plugin.mjs +153 -0
- package/lib/css-processors/scope-variables.mjs +26 -1
- package/lib/css-processors/shared.mjs +7 -6
- package/lib/eslint/eslint.js +44 -0
- package/lib/generate-js-imports/illustrations.js +3 -1
- package/lib/generate-json-imports/i18n.js +3 -1
- package/lib/generate-json-imports/themes.js +6 -2
- package/lib/i18n/defaults.js +3 -1
- package/lib/i18n/toJSON.js +28 -4
- package/lib/test-runner/test-runner.js +56 -48
- package/lib/vite-bundler/vite-bundler.mjs +35 -0
- package/package.json +2 -4
package/lib/cem/validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/lib/copy-list/index.js
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
writeFileIfChanged(jsonPath, JSON.stringify(newText));
|
|
135
|
+
await saveFiles(f.path, css);
|
|
136
|
+
processedCSS.set(f.path, css);
|
|
137
|
+
}));
|
|
62
138
|
|
|
63
|
-
|
|
64
|
-
|
|
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 (
|
|
97
|
-
|
|
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 };
|