@ui5/webcomponents-tools 2.17.0-rc.3 → 2.17.0-rc.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
@@ -3,6 +3,33 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [2.17.0-rc.5](https://github.com/UI5/webcomponents/compare/v2.17.0-rc.4...v2.17.0-rc.5) (2025-12-04)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **tools:** dependencies usage ([#12716](https://github.com/UI5/webcomponents/issues/12716)) ([89bb0dd](https://github.com/UI5/webcomponents/commit/89bb0dd62322598bd1ea7ce984eaf0618546a6f2))
12
+
13
+
14
+ ### Features
15
+
16
+ * **framework:** introduce loadBaseThemingCSSVariables configuration ([#12699](https://github.com/UI5/webcomponents/issues/12699)) ([f01b2eb](https://github.com/UI5/webcomponents/commit/f01b2eb6256f2032bd802d0a60c4625b0d1af5fe))
17
+
18
+
19
+
20
+
21
+
22
+ # [2.17.0-rc.4](https://github.com/UI5/webcomponents/compare/v2.17.0-rc.3...v2.17.0-rc.4) (2025-11-27)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **build:** fix issue with legacy dev setup ([#12706](https://github.com/UI5/webcomponents/issues/12706)) ([89fa5ca](https://github.com/UI5/webcomponents/commit/89fa5ca9f83551363e8c1d9980269cd58fa09d85))
28
+
29
+
30
+
31
+
32
+
6
33
  # [2.17.0-rc.3](https://github.com/UI5/webcomponents/compare/v2.17.0-rc.2...v2.17.0-rc.3) (2025-11-20)
7
34
 
8
35
 
package/bin/ui5nps.js CHANGED
@@ -165,7 +165,12 @@ class Parser {
165
165
  return new Promise(async (resolve, reject) => {
166
166
  if (command.trim().startsWith("ui5nps-script")) {
167
167
  const argv = parseArgsStringToArgv(command);
168
- const importedContent = require(argv[1]);
168
+ if (!path.isAbsolute(argv[1])) {
169
+ throw new Error(`Script path must be absolute: ${argv[1]}`);
170
+ }
171
+
172
+ const importPath = argv[1];
173
+ const importedContent = require(importPath);
169
174
  let _ui5mainFn;
170
175
 
171
176
  if (importedContent.__esModule) {
@@ -174,6 +179,10 @@ class Parser {
174
179
  _ui5mainFn = importedContent._ui5mainFn;
175
180
  }
176
181
 
182
+ if (!_ui5mainFn) {
183
+ return reject(new Error(`No valid _ui5mainFn function exported from ${importPath} tried to be executed with ui5nps-script. Either provide a valid _ui5mainFn function or use another way to execute the script (via node).`));
184
+ }
185
+
177
186
  console.log(` | Executing command ${commandName} as module.`);
178
187
  const result = _ui5mainFn(argv);
179
188
 
@@ -66,15 +66,6 @@ const getScripts = (options) => {
66
66
  viteConfig = `-c "${require.resolve("@ui5/webcomponents-tools/components-package/vite.config.js")}"`;
67
67
  }
68
68
 
69
- let eslintConfig;
70
- if (fs.existsSync(".eslintrc.js") || fs.existsSync(".eslintrc.cjs")) {
71
- // preferred way of custom configuration in root project folder
72
- eslintConfig = "";
73
- } else {
74
- // no custom configuration - use default from tools project
75
- eslintConfig = `--config "${require.resolve("@ui5/webcomponents-tools/components-package/eslint.js")}"`;
76
- }
77
-
78
69
  const scripts = {
79
70
  __ui5envs: {
80
71
  UI5_CEM_MODE: options.dev,
@@ -86,8 +77,8 @@ const getScripts = (options) => {
86
77
  "generated": `ui5nps-script "${LIB}/rimraf/rimraf.js src/generated`,
87
78
  "dist": `ui5nps-script "${LIB}/rimraf/rimraf.js dist`,
88
79
  },
89
- lint: `eslint . ${eslintConfig}`,
90
- lintfix: `eslint . ${eslintConfig} --fix`,
80
+ lint: `ui5nps-script "${LIB}eslint/eslint.js"`,
81
+ lintfix: `ui5nps-script "${LIB}eslint/eslint.js" --fix`,
91
82
  generate: {
92
83
  default: `ui5nps prepare.all`,
93
84
  all: `ui5nps-p build.templates build.i18n prepare.styleRelated copyProps build.illustrations`, // concurently
@@ -101,7 +92,7 @@ const getScripts = (options) => {
101
92
  },
102
93
  build: {
103
94
  default: "ui5nps prepare lint build.bundle", // build.bundle2
104
- templates: options.legacy ? `mkdirp src/generated/templates && node "${LIB}hbs2ui5/index.js" -d src/ -o src/generated/templates` : "",
95
+ templates: options.legacy ? `mkdir -p src/generated/templates && node "${LIB}hbs2ui5/index.js" -d src/ -o src/generated/templates` : "",
105
96
  styles: {
106
97
  default: `ui5nps-p build.styles.themes build.styles.components`, // concurently
107
98
  themes: `ui5nps-script "${LIB}css-processors/css-processor-themes.mjs"`,
@@ -123,12 +114,13 @@ const getScripts = (options) => {
123
114
  default: "ui5nps build.jsImports.illustrationsLoaders",
124
115
  illustrationsLoaders: createIllustrationsLoadersScript,
125
116
  },
126
- bundle: `vite build ${viteConfig} --mode testing --base ${websiteBaseUrl}`,
117
+ bundle: `ui5nps-script "${LIB}vite-bundler/vite-bundler.mjs" ${viteConfig} --mode testing --base ${websiteBaseUrl}`,
127
118
  bundle2: ``,
128
119
  illustrations: createIllustrationsJSImportsScript,
129
120
  },
130
121
  copyProps: `ui5nps-script "${LIB}copy-and-watch/index.js" --silent "src/i18n/*.properties" dist/`,
131
122
  copyPropsWithWatch: `ui5nps-script "${LIB}copy-and-watch/index.js" --silent "src/i18n/*.properties" dist/ --watch --safe --skip-initial-copy`,
123
+ copySrcWithWatch: `ui5nps-script "${LIB}copy-and-watch/index.js" --silent "src/**/*.{js,json}" dist/ --watch --safe --skip-initial-copy`,
132
124
  copy: {
133
125
  default: options.legacy ? "ui5nps copy.src copy.props" : "",
134
126
  src: options.legacy ? `ui5nps-script "${LIB}copy-and-watch/index.js" --silent "src/**/*.{js,json}" dist/` : "",
@@ -137,7 +129,7 @@ const getScripts = (options) => {
137
129
  watch: {
138
130
  default: `ui5nps-p watch.templates watch.typescript watch.src watch.styles watch.i18n watch.props`, // concurently
139
131
  devServer: 'ui5nps-p watch.default watch.bundle', // concurently
140
- src: options.legacy ? 'ui5nps "copy.src --watch --safe --skip-initial-copy"' : "",
132
+ src: options.legacy ? 'ui5nps copySrcWithWatch' : "",
141
133
  typescript: tsWatchCommandStandalone,
142
134
  props: 'ui5nps copyPropsWithWatch',
143
135
  bundle: `ui5nps-script ${LIB}dev-server/dev-server.mjs ${viteConfig}`,
@@ -146,8 +138,8 @@ const getScripts = (options) => {
146
138
  themes: 'ui5nps build.styles.themesWithWatch',
147
139
  components: `ui5nps build.styles.componentsWithWatch`,
148
140
  },
149
- templates: options.legacy ? 'chokidar "src/**/*.hbs" -i "src/generated" -c "ui5nps build.templates"' : "",
150
- i18n: 'chokidar "src/i18n/messagebundle.properties" -c "ui5nps build.i18n.defaultsjs"'
141
+ templates: options.legacy ? `ui5nps-script "${LIB}chokidar/chokidar.js" "src/**/*.hbs" "ui5nps build.templates"` : "",
142
+ i18n: `ui5nps-script "${LIB}chokidar/chokidar.js" "src/i18n/messagebundle.properties" "ui5nps build.i18n.defaultsjs"`
151
143
  },
152
144
  start: "ui5nps prepare watch.devServer",
153
145
  test: `ui5nps-script "${LIB}/test-runner/test-runner.js"`,
@@ -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;
@@ -9,24 +9,28 @@ import { writeFileIfChanged, getFileContent } from "./shared.mjs";
9
9
  import { scopeUi5Variables, scopeThemingVariables } from "./scope-variables.mjs";
10
10
  import { pathToFileURL } from "url";
11
11
 
12
- async function processThemingPackageFile(f) {
12
+ async function processThemingPackageFile(f, scope = true) {
13
13
  const selector = ':root';
14
14
  const newRule = postcss.rule({ selector });
15
15
  const result = await postcss().process(f.text);
16
16
 
17
17
  result.root.walkRules(selector, rule => {
18
18
  for (const decl of rule.nodes) {
19
- if (decl.type !== 'decl' ) {
19
+ if (decl.type !== 'decl') {
20
20
  continue;
21
21
  } else if (decl.prop.startsWith('--sapFontUrl')) {
22
22
  continue;
23
23
  } else if (!decl.prop.startsWith('--sap')) {
24
24
  newRule.append(decl.clone());
25
25
  } else {
26
- const originalProp = decl.prop;
27
- const originalValue = decl.value;
28
-
29
- newRule.append(decl.clone({ prop: originalProp.replace("--sap", "--ui5-sap"), value: `var(${originalProp}, ${originalValue})` }));
26
+ if (scope) {
27
+ const originalProp = decl.prop;
28
+ const originalValue = decl.value;
29
+
30
+ newRule.append(decl.clone({ prop: originalProp.replace("--sap", "--ui5-sap"), value: `var(${originalProp}, ${originalValue})` }));
31
+ } else {
32
+ newRule.append(decl.clone());
33
+ }
30
34
  }
31
35
  }
32
36
  });
@@ -43,6 +47,24 @@ async function processComponentPackageFile(f, packageJSON) {
43
47
 
44
48
  return result;
45
49
  }
50
+ async function writeProcessedContent(basePath, content, packageJSON, extension) {
51
+ const cssPath = basePath;
52
+ const jsonPath = basePath.replace(/dist[\/\\]css/, "dist/generated/assets").replace(".css", ".css.json");
53
+ const jsPath = basePath.replace(/dist[\/\\]css/, "src/generated/").replace(".css", extension);
54
+
55
+ // Write CSS file
56
+ await mkdir(path.dirname(cssPath), { recursive: true });
57
+ await writeFile(cssPath, content);
58
+
59
+ // Write JSON file
60
+ await mkdir(path.dirname(jsonPath), { recursive: true });
61
+ await writeFileIfChanged(jsonPath, JSON.stringify(content));
62
+
63
+ // Write JS/TS file
64
+ const jsContent = getFileContent(packageJSON.name, `\`${content}\``);
65
+ await mkdir(path.dirname(jsPath), { recursive: true });
66
+ await writeFileIfChanged(jsPath, jsContent);
67
+ }
46
68
 
47
69
  async function generate(argv) {
48
70
  const tsMode = process.env.UI5_TS === "true";
@@ -55,27 +77,27 @@ async function generate(argv) {
55
77
  ]);
56
78
  const restArgs = argv.slice(2);
57
79
 
58
- let scopingPlugin = {
80
+ const scopingPlugin = {
59
81
  name: 'scoping',
60
82
  setup(build) {
61
83
  build.initialOptions.write = false;
62
84
 
63
85
  build.onEnd(result => {
64
86
  result.outputFiles.forEach(async f => {
65
- let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f, packageJSON);
66
-
67
- await mkdir(path.dirname(f.path), { recursive: true });
68
- writeFile(f.path, newText);
69
-
70
- // JSON
71
- const jsonPath = f.path.replace(/dist[\/\\]css/, "dist/generated/assets").replace(".css", ".css.json");
72
- await mkdir(path.dirname(jsonPath), { recursive: true });
73
- writeFileIfChanged(jsonPath, JSON.stringify(newText));
74
-
75
- // JS/TS
76
- const jsPath = f.path.replace(/dist[\/\\]css/, "src/generated/").replace(".css", extension);
77
- const jsContent = getFileContent(packageJSON.name, "\`" + newText + "\`");
78
- writeFileIfChanged(jsPath, jsContent);
87
+ if (f.path.includes("packages/theming")) {
88
+ const scopedText = await processThemingPackageFile(f);
89
+ const originalText = await processThemingPackageFile(f, false);
90
+
91
+ // Write scoped version
92
+ await writeProcessedContent(f.path, scopedText, packageJSON, extension);
93
+
94
+ // Write raw version
95
+ const originalPath = f.path.replace(/parameters-bundle.css$/, "parameters-bundle-raw.css");
96
+ await writeProcessedContent(originalPath, originalText, packageJSON, extension);
97
+ } else {
98
+ const processedText = await processComponentPackageFile(f, packageJSON);
99
+ await writeProcessedContent(f.path, processedText, packageJSON, extension);
100
+ }
79
101
  });
80
102
  })
81
103
  },
@@ -0,0 +1,44 @@
1
+ const fs = require("fs");
2
+ const { ESLint: ESLint7 } = require("eslint"); // isolated v7
3
+ const path = require("path");
4
+
5
+ const main = async argv => {
6
+ let eslintConfig;
7
+ if (fs.existsSync(".eslintrc.js") || fs.existsSync(".eslintrc.cjs")) {
8
+ // preferred way of custom configuration in root project folder
9
+ eslintConfig = null;
10
+ } else {
11
+ // no custom configuration - use default from tools project
12
+ eslintConfig = require.resolve("@ui5/webcomponents-tools/components-package/eslint.js")
13
+ };
14
+
15
+ const packageDir = path.dirname(require.resolve("@ui5/webcomponents-tools/package.json"));
16
+ const eslint = new ESLint7({
17
+ overrideConfigFile: eslintConfig,
18
+ fix: argv.includes("--fix"),
19
+ resolvePluginsRelativeTo: packageDir,
20
+ });
21
+ console.log("Running ESLint v7...");
22
+
23
+ // Lint files
24
+ const results = await eslint.lintFiles(["."]);
25
+
26
+ // Format results
27
+ const formatter = await eslint.loadFormatter("stylish");
28
+ const resultText = formatter.format(results);
29
+
30
+ // Output results
31
+ console.log(resultText);
32
+
33
+ // Exit with error code if there are errors
34
+ const hasErrors = results.some(result => result.errorCount > 0);
35
+ if (hasErrors) {
36
+ process.exit(1);
37
+ }
38
+ }
39
+
40
+ if (require.main === module) {
41
+ main(process.argv)
42
+ }
43
+
44
+ exports._ui5mainFn = main;
@@ -7,9 +7,10 @@ const ext = isTypeScript ? 'ts' : 'js';
7
7
 
8
8
  const generate = async (argv) => {
9
9
  const inputFolder = path.normalize(argv[2]);
10
- const outputFileDynamic = path.normalize(`${argv[3]}/Themes.${ext}`);
11
- const outputFileDynamicImportJSONAttr = path.normalize(`${argv[3]}/Themes-node.${ext}`);
12
- const outputFileFetchMetaResolve = path.normalize(`${argv[3]}/Themes-fetch.${ext}`);
10
+ const outputSuffix = argv[4] || "";
11
+ const outputFileDynamic = path.normalize(`${argv[3]}/Themes${outputSuffix}.${ext}`);
12
+ const outputFileDynamicImportJSONAttr = path.normalize(`${argv[3]}/Themes${outputSuffix}-node.${ext}`);
13
+ const outputFileFetchMetaResolve = path.normalize(`${argv[3]}/Themes${outputSuffix}-fetch.${ext}`);
13
14
 
14
15
  // All supported optional themes
15
16
  const allThemes = assets.themes.all;
@@ -24,9 +25,9 @@ const generate = async (argv) => {
24
25
  const packageName = JSON.parse(await fs.readFile("package.json")).name;
25
26
 
26
27
  const availableThemesArray = `[${themesOnFileSystem.map(theme => `"${theme}"`).join(", ")}]`;
27
- const dynamicImportLines = themesOnFileSystem.map(theme => `\t\tcase "${theme}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-${theme.replace("_", "-")}-parameters-bundle" */"../assets/themes/${theme}/parameters-bundle.css.json")).default;`).join("\n");
28
- const dynamicImportJSONAttrLines = themesOnFileSystem.map(theme => `\t\tcase "${theme}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-${theme.replace("_", "-")}-parameters-bundle" */"../assets/themes/${theme}/parameters-bundle.css.json", {with: { type: 'json'}})).default;`).join("\n");
29
- const fetchMetaResolveLines = themesOnFileSystem.map(theme => `\t\tcase "${theme}": return (await fetch(new URL("../assets/themes/${theme}/parameters-bundle.css.json", import.meta.url))).json();`).join("\n");
28
+ const dynamicImportLines = themesOnFileSystem.map(theme => `\t\tcase "${theme}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-${theme.replace("_", "-")}-parameters-bundle" */"../assets/themes/${theme}/parameters-bundle${outputSuffix}.css.json")).default;`).join("\n");
29
+ const dynamicImportJSONAttrLines = themesOnFileSystem.map(theme => `\t\tcase "${theme}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-${theme.replace("_", "-")}-parameters-bundle" */"../assets/themes/${theme}/parameters-bundle${outputSuffix}.css.json", {with: { type: 'json'}})).default;`).join("\n");
30
+ const fetchMetaResolveLines = themesOnFileSystem.map(theme => `\t\tcase "${theme}": return (await fetch(new URL("../assets/themes/${theme}/parameters-bundle${outputSuffix}.css.json", import.meta.url))).json();`).join("\n");
30
31
 
31
32
  // dynamic imports file content
32
33
  const contentDynamic = function (lines) {
@@ -49,7 +50,7 @@ const loadAndCheck = async (themeName) => {
49
50
  };
50
51
 
51
52
  ${availableThemesArray}
52
- .forEach(themeName => registerThemePropertiesLoader(${packageName.split("").map(c => `"${c}"`).join(" + ")}, themeName, loadAndCheck));
53
+ .forEach(themeName => registerThemePropertiesLoader(${`${packageName + outputSuffix}`.split("").map(c => `"${c}"`).join(" + ")}, themeName, loadAndCheck));
53
54
  `;
54
55
  }
55
56
 
@@ -19,8 +19,7 @@ const convertToJSON = async (file, distPath) => {
19
19
  const filename = path.basename(file, path.extname(file));
20
20
  const language = filename.match(/^messagebundle_(.*?)$/)[1];
21
21
  if (!allLanguages.includes(language)) {
22
- console.log("Not supported language: ", language);
23
- return;
22
+ console.warn("Not supported language or script: ", language);
24
23
  }
25
24
  const outputFile = path.normalize(`${distPath}/${filename}.json`);
26
25
 
@@ -3,7 +3,7 @@ const { readFileSync } = require("fs");
3
3
  const path = require("path");
4
4
  const fs = require("fs");
5
5
 
6
- function testFn() {
6
+ function testFn(outArgv) {
7
7
  // search for dev-server port
8
8
  // start in current folder
9
9
  // traversing upwards in case of mono repo tests and dev-server running in root folder of repository
@@ -48,15 +48,15 @@ function testFn() {
48
48
 
49
49
  // add single spec parameter if passed
50
50
  let spec = "";
51
- if (process.argv.length === 3) {
52
- const specFile = process.argv[2];
51
+ if (outArgv.length === 3) {
52
+ const specFile = outArgv[2];
53
53
  spec = `--spec ${specFile}`;
54
54
  }
55
55
 
56
56
  // more parameters - pass them to wdio
57
57
  let restParams = "";
58
- if (process.argv.length > 3) {
59
- restParams = process.argv.slice(2).join(" ");
58
+ if (outArgv.length > 3) {
59
+ restParams = outArgv.slice(2).join(" ");
60
60
  }
61
61
 
62
62
  let wdioConfig = "";
@@ -67,7 +67,7 @@ function testFn() {
67
67
  }
68
68
 
69
69
  // run wdio with calculated parameters
70
- const cmd = `yarn cross-env WDIO_LOG_LEVEL=error wdio ${wdioConfig} ${spec} ${baseUrl} ${restParams}`;
70
+ const cmd = `npx cross-env WDIO_LOG_LEVEL=error wdio ${wdioConfig} ${spec} ${baseUrl} ${restParams}`;
71
71
  console.log(`executing: ${cmd}`);
72
72
  child_process.execSync(cmd, {stdio: 'inherit'});
73
73
  }
@@ -0,0 +1,35 @@
1
+ import { build } from 'vite';
2
+ import yargs from 'yargs';
3
+ import { hideBin } from 'yargs/helpers';
4
+ import { pathToFileURL } from "url";
5
+
6
+ async function start(outArgv) {
7
+ const argv = yargs(hideBin(outArgv))
8
+ .alias("c", "config")
9
+ .alias("mode", "mode")
10
+ .alias("base", "base")
11
+ .argv;
12
+
13
+ try {
14
+ await build({
15
+ configFile: argv.config || undefined,
16
+ mode: argv.mode || undefined,
17
+ base: argv.base || undefined,
18
+ logLevel: 'info',
19
+ });
20
+ } catch (e) {
21
+ console.error(e)
22
+ process.exit(1);
23
+ }
24
+ };
25
+
26
+ const filePath = process.argv[1];
27
+ const fileUrl = pathToFileURL(filePath).href;
28
+
29
+ if (import.meta.url === fileUrl) {
30
+ start(process.argv)
31
+ }
32
+
33
+ export default {
34
+ _ui5mainFn: start
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ui5/webcomponents-tools",
3
- "version": "2.17.0-rc.3",
3
+ "version": "2.17.0-rc.5",
4
4
  "description": "UI5 Web Components: webcomponents.tools",
5
5
  "author": "SAP SE (https://www.sap.com)",
6
6
  "license": "Apache-2.0",
@@ -34,7 +34,6 @@
34
34
  "chai": "^4.3.4",
35
35
  "child_process": "^1.0.2",
36
36
  "chokidar": "^3.6.0",
37
- "chokidar-cli": "^3.0.0",
38
37
  "command-line-args": "^5.1.1",
39
38
  "comment-parser": "^1.4.0",
40
39
  "cross-env": "^7.0.3",
@@ -54,7 +53,6 @@
54
53
  "ignore": "^7.0.5",
55
54
  "is-port-reachable": "^3.1.0",
56
55
  "json-beautify": "^1.1.1",
57
- "mkdirp": "^1.0.4",
58
56
  "postcss": "^8.4.5",
59
57
  "postcss-cli": "^9.1.0",
60
58
  "postcss-selector-parser": "^6.0.10",
@@ -84,5 +82,5 @@
84
82
  "esbuild": "^0.25.0",
85
83
  "yargs": "^17.5.1"
86
84
  },
87
- "gitHead": "45ac45d27dd4c95cc840c51c95fef9d38e7586f0"
85
+ "gitHead": "affb457b35f4a92b8d6632de0042c8a9da7c244e"
88
86
  }