@ui5/webcomponents-tools 0.0.0-f2abb18c2 → 0.0.0-f42e7c18c

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 (106) hide show
  1. package/CHANGELOG.md +1620 -4
  2. package/README.md +6 -9
  3. package/assets-meta.js +26 -6
  4. package/bin/dev.js +1 -5
  5. package/components-package/cypress/support/commands.js +39 -0
  6. package/components-package/cypress/support/component-index.html +17 -0
  7. package/components-package/cypress/support/component.d.ts +23 -0
  8. package/components-package/cypress/support/component.js +34 -0
  9. package/components-package/cypress.config.js +19 -0
  10. package/components-package/eslint.js +98 -2
  11. package/components-package/nps.js +116 -55
  12. package/components-package/postcss.components.js +1 -21
  13. package/components-package/postcss.themes.js +1 -23
  14. package/components-package/vite.config.js +13 -0
  15. package/components-package/wdio.js +110 -39
  16. package/icons-collection/nps.js +54 -11
  17. package/lib/amd-to-es6/index.js +102 -0
  18. package/lib/amd-to-es6/no-remaining-require.js +33 -0
  19. package/lib/cem/custom-elements-manifest.config.mjs +527 -0
  20. package/lib/cem/event.mjs +168 -0
  21. package/lib/cem/schema-internal.json +1413 -0
  22. package/lib/cem/schema.json +1098 -0
  23. package/lib/cem/types-internal.d.ts +808 -0
  24. package/lib/cem/types.d.ts +736 -0
  25. package/lib/cem/utils.mjs +408 -0
  26. package/lib/cem/validate.js +70 -0
  27. package/lib/copy-and-watch/index.js +30 -5
  28. package/lib/copy-list/index.js +28 -0
  29. package/lib/create-icons/index.js +126 -52
  30. package/lib/create-illustrations/index.js +182 -0
  31. package/lib/create-new-component/index.js +63 -97
  32. package/lib/create-new-component/tsFileContentTemplate.js +71 -0
  33. package/lib/css-processors/css-processor-components.mjs +77 -0
  34. package/lib/css-processors/css-processor-themes.mjs +79 -0
  35. package/lib/css-processors/scope-variables.mjs +49 -0
  36. package/lib/css-processors/shared.mjs +76 -0
  37. package/lib/dev-server/custom-hot-update-plugin.js +39 -0
  38. package/lib/dev-server/dev-server.mjs +66 -0
  39. package/lib/dev-server/virtual-index-html-plugin.js +52 -0
  40. package/lib/generate-js-imports/illustrations.js +86 -0
  41. package/lib/generate-json-imports/i18n.js +66 -37
  42. package/lib/generate-json-imports/themes.js +55 -34
  43. package/lib/hbs2lit/src/compiler.js +24 -4
  44. package/lib/hbs2lit/src/includesReplacer.js +5 -5
  45. package/lib/hbs2lit/src/litVisitor2.js +113 -25
  46. package/lib/hbs2lit/src/svgProcessor.js +12 -5
  47. package/lib/hbs2ui5/RenderTemplates/LitRenderer.js +40 -14
  48. package/lib/hbs2ui5/index.js +57 -24
  49. package/lib/i18n/defaults.js +66 -57
  50. package/lib/i18n/toJSON.js +13 -12
  51. package/lib/postcss-combine-duplicated-selectors/index.js +185 -0
  52. package/lib/remove-dev-mode/remove-dev-mode.mjs +37 -0
  53. package/lib/scoping/get-all-tags.js +11 -11
  54. package/lib/scoping/lint-src.js +9 -8
  55. package/lib/scoping/missing-dependencies.js +65 -0
  56. package/lib/scoping/report-tags-usage.js +28 -0
  57. package/lib/scoping/scope-test-pages.js +2 -1
  58. package/lib/test-runner/test-runner.js +71 -0
  59. package/package.json +59 -53
  60. package/tsconfig.json +16 -0
  61. package/types/index.d.ts +1 -0
  62. package/bin/init-ui5-package.js +0 -3
  63. package/components-package/rollup.js +0 -147
  64. package/components-package/serve.json +0 -3
  65. package/lib/documentation/index.js +0 -143
  66. package/lib/documentation/templates/api-component-since.js +0 -3
  67. package/lib/documentation/templates/api-css-variables-section.js +0 -24
  68. package/lib/documentation/templates/api-events-section.js +0 -35
  69. package/lib/documentation/templates/api-methods-section.js +0 -26
  70. package/lib/documentation/templates/api-properties-section.js +0 -40
  71. package/lib/documentation/templates/api-slots-section.js +0 -28
  72. package/lib/documentation/templates/template.js +0 -38
  73. package/lib/init-package/index.js +0 -123
  74. package/lib/init-package/resources/.eslintignore +0 -3
  75. package/lib/init-package/resources/bundle.es5.js +0 -25
  76. package/lib/init-package/resources/bundle.esm.js +0 -34
  77. package/lib/init-package/resources/config/.eslintrc.js +0 -1
  78. package/lib/init-package/resources/config/postcss.components/postcss.config.js +0 -1
  79. package/lib/init-package/resources/config/postcss.themes/postcss.config.js +0 -1
  80. package/lib/init-package/resources/config/rollup.config.js +0 -1
  81. package/lib/init-package/resources/config/wdio.conf.js +0 -1
  82. package/lib/init-package/resources/package-scripts.js +0 -11
  83. package/lib/init-package/resources/src/Assets.js +0 -5
  84. package/lib/init-package/resources/src/MyFirstComponent.hbs +0 -1
  85. package/lib/init-package/resources/src/MyFirstComponent.js +0 -56
  86. package/lib/init-package/resources/src/i18n/messagebundle.properties +0 -2
  87. package/lib/init-package/resources/src/i18n/messagebundle_de.properties +0 -1
  88. package/lib/init-package/resources/src/i18n/messagebundle_en.properties +0 -1
  89. package/lib/init-package/resources/src/i18n/messagebundle_es.properties +0 -1
  90. package/lib/init-package/resources/src/i18n/messagebundle_fr.properties +0 -1
  91. package/lib/init-package/resources/src/themes/MyFirstComponent.css +0 -11
  92. package/lib/init-package/resources/src/themes/sap_belize/parameters-bundle.css +0 -3
  93. package/lib/init-package/resources/src/themes/sap_belize_hcb/parameters-bundle.css +0 -3
  94. package/lib/init-package/resources/src/themes/sap_belize_hcw/parameters-bundle.css +0 -3
  95. package/lib/init-package/resources/src/themes/sap_fiori_3/parameters-bundle.css +0 -3
  96. package/lib/init-package/resources/src/themes/sap_fiori_3_dark/parameters-bundle.css +0 -3
  97. package/lib/init-package/resources/src/themes/sap_fiori_3_hcb/parameters-bundle.css +0 -3
  98. package/lib/init-package/resources/src/themes/sap_fiori_3_hcw/parameters-bundle.css +0 -3
  99. package/lib/init-package/resources/test/pages/index.html +0 -56
  100. package/lib/init-package/resources/test/specs/Demo.spec.js +0 -12
  101. package/lib/jsdoc/config.json +0 -29
  102. package/lib/jsdoc/plugin.js +0 -2407
  103. package/lib/jsdoc/template/publish.js +0 -4092
  104. package/lib/postcss-css-to-esm/index.js +0 -42
  105. package/lib/postcss-css-to-json/index.js +0 -27
  106. package/package-lock.json +0 -48
@@ -1,42 +1,63 @@
1
- const fs = require("fs");
1
+ const fs = require("fs").promises;
2
2
  const path = require('path');
3
- const mkdirp = require("mkdirp");
4
3
  const assets = require("../../assets-meta.js");
5
4
 
6
- const inputFolder = path.normalize(process.argv[2]);
7
- const outputFile = path.normalize(`${process.argv[3]}/Themes.js`);
5
+ const isTypeScript = process.env.UI5_TS;
6
+ const ext = isTypeScript ? 'ts' : 'js';
7
+
8
+ const generate = async () => {
9
+ const inputFolder = path.normalize(process.argv[2]);
10
+ const outputFileDynamic = path.normalize(`${process.argv[3]}/Themes.${ext}`);
11
+ const outputFileFetchMetaResolve = path.normalize(`${process.argv[3]}/Themes-fetch.${ext}`);
8
12
 
9
13
  // All supported optional themes
10
- const optionalThemes = assets.themes.all.filter(theme => theme !== assets.themes.default);
14
+ const allThemes = assets.themes.all;
11
15
 
12
16
  // All themes present in the file system
13
- const dirs = fs.readdirSync(inputFolder);
14
- const themesOnFileSystem = dirs.map(dir => {
15
- const matches = dir.match(/sap_.*$/);
16
- return matches ? dir : undefined;
17
- }).filter(key => !!key && optionalThemes.includes(key));
18
-
19
- const packageName = JSON.parse(fs.readFileSync("package.json")).name;
20
-
21
- const importLines = themesOnFileSystem.map(theme => `import ${theme} from "../assets/themes/${theme}/parameters-bundle.css.json";`).join("\n");
22
- const isInlinedCondition = themesOnFileSystem.map(theme => `isInlined(${theme})`).join(" || ");
23
- const registerLines = themesOnFileSystem.map(theme => `registerThemeProperties("${packageName}", "${theme}", ${theme});`).join("\n");
24
-
25
- // Resulting file content
26
- const content = `import { registerThemeProperties } from "@ui5/webcomponents-base/dist/asset-registries/Themes.js";
27
-
28
- ${importLines}
29
-
30
- const isInlined = obj => typeof (obj) === "object";
31
-
32
- if (${isInlinedCondition}) {
33
- console.warn(\`Inefficient bundling detected: consider bundling theme properties imports as URLs instead of inlining them.
34
- See rollup-plugin-url or webpack file-loader for more information.
35
- Suggested pattern: "assets\\\\\\/.*\\\\\\.json"\`);
36
- }
37
-
38
- ${registerLines}
17
+ const dirs = await fs.readdir(inputFolder);
18
+ const themesOnFileSystem = dirs.map(dir => {
19
+ const matches = dir.match(/sap_.*$/);
20
+ return matches ? dir : undefined;
21
+ }).filter(key => !!key && allThemes.includes(key));
22
+
23
+ const packageName = JSON.parse(await fs.readFile("package.json")).name;
24
+
25
+ const availableThemesArray = `[${themesOnFileSystem.map(theme => `"${theme}"`).join(", ")}]`;
26
+ 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");
27
+ 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
+
29
+ // dynamic imports file content
30
+ const contentDynamic = function (lines) {
31
+ return `// @ts-nocheck
32
+ import { registerThemePropertiesLoader } from "@ui5/webcomponents-base/dist/asset-registries/Themes.js";
33
+
34
+ const loadThemeProperties = async (themeName) => {
35
+ switch (themeName) {
36
+ ${lines}
37
+ default: throw "unknown theme"
38
+ }
39
+ };
40
+
41
+ const loadAndCheck = async (themeName) => {
42
+ const data = await loadThemeProperties(themeName);
43
+ if (typeof data === "string" && data.endsWith(".json")) {
44
+ throw new Error(\`[themes] Invalid bundling detected - dynamic JSON imports bundled as URLs. Switch to inlining JSON files from the build. Check the \"Assets\" documentation for more information.\`);
45
+ }
46
+ return data;
47
+ };
48
+
49
+ ${availableThemesArray}
50
+ .forEach(themeName => registerThemePropertiesLoader("${packageName}", themeName, loadAndCheck));
39
51
  `;
40
-
41
- mkdirp.sync(path.dirname(outputFile));
42
- fs.writeFileSync(outputFile, content);
52
+ }
53
+
54
+ await fs.mkdir(path.dirname(outputFileDynamic), { recursive: true });
55
+ return Promise.all([
56
+ fs.writeFile(outputFileDynamic, contentDynamic(dynamicImportLines)),
57
+ fs.writeFile(outputFileFetchMetaResolve, contentDynamic(fetchMetaResolveLines)),
58
+ ]);
59
+ };
60
+
61
+ generate().then(() => {
62
+ console.log("Generated themes JSON imports.");
63
+ });
@@ -12,15 +12,25 @@ const removeWhiteSpaces = (source) => {
12
12
  .replace(/}}\s+{{/g, "}}{{"); // Remove whitespace between }} and {{
13
13
  };
14
14
 
15
- const hbs2lit = (file) => {
16
- let sPreprocessed = includesReplacer.replace(file);
15
+ const hbs2lit = async (file, componentName) => {
16
+ let sPreprocessed = await includesReplacer.replace(file);
17
17
 
18
18
  sPreprocessed = removeWhiteSpaces(sPreprocessed);
19
19
 
20
+ const blockSignature = process.env.UI5_TS ? `this: ${componentName}` : ""
21
+
22
+ // icons hack
23
+ if (sPreprocessed.startsWith("<g ") || sPreprocessed.startsWith("<g>")) {
24
+ return `
25
+ function block0 (${blockSignature}) {
26
+ return svg\`${sPreprocessed}\`
27
+ }`;
28
+ }
29
+
20
30
  const ast = Handlebars.parse(sPreprocessed);
21
31
 
22
32
  const pv = new PartialsVisitor();
23
- const lv = new HTMLLitVisitor();
33
+ const lv = new HTMLLitVisitor(componentName);
24
34
 
25
35
  let result = "";
26
36
 
@@ -30,7 +40,17 @@ const hbs2lit = (file) => {
30
40
  lv.accept(ast);
31
41
 
32
42
  for (let key in lv.blocks) {
33
- result += lv.blocks[key] + "\n";
43
+ let block = lv.blocks[key];
44
+
45
+ if (block.match(/scopeTag/)) {
46
+ // const matches = block.match(/^(.*?)( => )(.*?);$/);
47
+ const matches = block.match(/^(function .*? \{ return )(.*?);\}$/);
48
+ const scopedCode = matches[2];
49
+ const normalCode = scopedCode.replace(/\${scopeTag\("/g, "").replace(/", tags, suffix\)}/g, "");
50
+ block = `${matches[1]}suffix ? ${scopedCode} : ${normalCode};}`;
51
+ }
52
+
53
+ result += block + "\n";
34
54
  }
35
55
 
36
56
  result = svgProcessor.process(result);
@@ -1,9 +1,9 @@
1
1
  const path = require("path");
2
- const fs = require("fs");
2
+ const fs = require("fs").promises;
3
3
 
4
- function replaceIncludes(file) {
4
+ const replaceIncludes = async (file) => {
5
5
  const filePath = path.dirname(file);
6
- let fileContent = fs.readFileSync(file, "utf-8");
6
+ let fileContent = await fs.readFile(file, "utf-8");
7
7
 
8
8
  const inclRegex = /{{>\s*include\s*["'](.+?)["']}}/g;
9
9
  let match;
@@ -20,11 +20,11 @@ function replaceIncludes(file) {
20
20
  targetFile = require.resolve(targetFile);
21
21
  }
22
22
 
23
- fileContent = fileContent.replace(match[0], replaceIncludes(targetFile));
23
+ fileContent = fileContent.replace(match[0], await replaceIncludes(targetFile));
24
24
  }
25
25
 
26
26
  return fileContent;
27
- }
27
+ };
28
28
 
29
29
  module.exports = {
30
30
  replace: replaceIncludes
@@ -8,17 +8,38 @@ let skipIfDefined = false;
8
8
  // when true => an HTML node value, when false => an attribute value
9
9
  let isNodeValue = false;
10
10
 
11
+ // when true => the current attribute is "style"
12
+ let isStyleAttribute = false;
13
+
11
14
  // matches event handlers @click= and boolean attrs ?disabled=
12
15
  const dynamicAttributeRgx = /\s(\?|@)([a-zA-Z|-]+)="?\s*$/;
13
16
 
14
- function HTMLLitVisitor(debug) {
17
+ if (!String.prototype.replaceAll) {
18
+ String.prototype.replaceAll = function(str, newStr){
19
+
20
+ // If a regex pattern
21
+ if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
22
+ return this.replace(str, newStr);
23
+ }
24
+
25
+ // If a string
26
+ return this.replace(new RegExp(str, 'g'), newStr);
27
+
28
+ };
29
+ }
30
+
31
+ function HTMLLitVisitor(componentName, debug) {
15
32
  this.blockCounter = 0;
16
33
  this.keys = [];
17
34
  this.blocks = {};
18
35
  this.result = "";
19
36
  this.mainBlock = "";
20
- this.blockPath = "context";
21
- this.blockParameters = ["context"];
37
+ this.blockLevel = 0;
38
+ this.componentName = componentName
39
+ const blockParametersDefinitionTS = [`this: ${componentName}`, "context: UI5Element", "tags: string[]", "suffix: string | undefined"];
40
+ const blockParametersDefinitionJS = ["context", "tags", "suffix"];
41
+ this.blockParametersDefinition = process.env.UI5_TS ? blockParametersDefinitionTS : blockParametersDefinitionJS;
42
+ this.blockParametersUsage = ["this", "context", "tags", "suffix"];
22
43
  this.paths = []; //contains all normalized relative paths
23
44
  this.debug = debug;
24
45
  if (this.debug) {
@@ -34,18 +55,18 @@ HTMLLitVisitor.prototype.Program = function(program) {
34
55
  this.keys.push(key);
35
56
  this.debug && this.blockByNumber.push(key);
36
57
 
37
- this.blocks[this.currentKey()] = "const " + this.currentKey() + " = (" + this.blockParameters.join(", ") + ") => { return ";
58
+ // this.blocks[this.currentKey()] = "function " + this.currentKey() + ` (this: any, ` + this.blockParametersDefinition.join(", ") + ") { ";
59
+ this.blocks[this.currentKey()] = `function ${this.currentKey()} (${this.blockParametersDefinition.join(", ")}) { `;
38
60
 
39
61
  if (this.keys.length > 1) { //it's a nested block
40
- this.blocks[this.prevKey()] += this.currentKey() + "(" + this.blockParameters.join(", ") + ")";
62
+ this.blocks[this.prevKey()] += this.currentKey() + ".call(" + this.blockParametersUsage.join(", ") + ")";
41
63
  } else {
42
64
  this.mainBlock = this.currentKey();
43
- this.paths.push(this.blockPath);
44
65
  }
45
66
 
46
- this.blocks[this.currentKey()] += "html`";
67
+ this.blocks[this.currentKey()] += "return html`";
47
68
  Visitor.prototype.Program.call(this, program);
48
- this.blocks[this.currentKey()] += "`; };";
69
+ this.blocks[this.currentKey()] += "`;}";
49
70
 
50
71
  this.keys.pop(key);
51
72
  };
@@ -54,7 +75,7 @@ HTMLLitVisitor.prototype.ContentStatement = function(content) {
54
75
  Visitor.prototype.ContentStatement.call(this, content);
55
76
  // let content = content.orgiinal; // attribute="__ attribute = "__ attribute ="__
56
77
 
57
- const contentStatement = content.original;
78
+ let contentStatement = content.original;
58
79
  skipIfDefined = !!dynamicAttributeRgx.exec(contentStatement);
59
80
 
60
81
  const closingIndex = contentStatement.lastIndexOf(">");
@@ -63,6 +84,15 @@ HTMLLitVisitor.prototype.ContentStatement = function(content) {
63
84
  isNodeValue = closingIndex > openingIndex;
64
85
  }
65
86
 
87
+ isStyleAttribute = !isNodeValue && contentStatement.match(/style *= *["']? *$/);
88
+
89
+ if (!isStyleAttribute && contentStatement.match(/style=/)) {
90
+ console.log("WARNING: style hard-coded", contentStatement);
91
+ }
92
+
93
+ // Scope custom element tags
94
+ contentStatement = contentStatement.replaceAll(/(<\/?\s*)([a-zA-Z0-9_]+-[a-zA-Z0-9_-]+)/g, "$1\${scopeTag(\"$2\", tags, suffix)}");
95
+
66
96
  this.blocks[this.currentKey()] += contentStatement;
67
97
  };
68
98
 
@@ -73,16 +103,19 @@ HTMLLitVisitor.prototype.MustacheStatement = function(mustache) {
73
103
  this.blocks[this.currentKey()] += "${index}";
74
104
  } else {
75
105
  const path = normalizePath.call(this, mustache.path.original);
76
- const hasCalculatingClasses = path.includes("context.classes");
77
- const hasStylesCalculation = path.includes("context.styles");
106
+ const hasCalculatingClasses = path.includes("this.classes");
78
107
 
79
108
  let parsedCode = "";
80
109
 
81
110
  if (isNodeValue && !mustache.escaped) {
82
111
  parsedCode = `\${unsafeHTML(${path})}`;
83
112
  } else if (hasCalculatingClasses) {
84
- parsedCode = `\${classMap(${path})}`;
85
- } else if (hasStylesCalculation) {
113
+ if (process.env.UI5_TS) {
114
+ parsedCode = `\${classMap(${path} as ClassMapValue)}`;
115
+ } else {
116
+ parsedCode = `\${classMap(${path})}`;
117
+ }
118
+ } else if (isStyleAttribute) {
86
119
  parsedCode = `\${styleMap(${path})}`;
87
120
  } else if (skipIfDefined){
88
121
  parsedCode = `\${${path}}`;
@@ -146,22 +179,37 @@ function visitEachBlock(block) {
146
179
  var bParamAdded = false;
147
180
  visitSubExpression.call(this, block);
148
181
 
149
- this.blocks[this.currentKey()] += "${ repeat(" + normalizePath.call(this, block.params[0].original) + ", (item, index) => item._id || index, (item, index) => ";
182
+ const reapeatDirectiveParamsTS = "(item, index) => (item as typeof item & {_id?: any})._id || index, (item, index: number)";
183
+ const reapeatDirectiveParamsJS = "(item, index) => item._id || index, (item, index)";
184
+ const repleatDirectiveParams = process.env.UI5_TS ? reapeatDirectiveParamsTS : reapeatDirectiveParamsJS;
185
+ this.blocks[this.currentKey()] += "${ repeat(" + normalizePath.call(this, block.params[0].original) + ", " + repleatDirectiveParams + " => ";
150
186
  this.paths.push(normalizePath.call(this, block.params[0].original));
151
- this.blockPath = "item";
187
+ this.blockLevel++;
152
188
 
153
- if (this.blockParameters.indexOf("item") === -1) {
189
+ // block params is [this, context, tags, suffix] for top level blocks
190
+ // blcok params is [this, context, tags, suffix, item, index] for nested blocks
191
+ if (!this.blockParametersUsage.includes("index")) {
192
+ // last item is not index, but an each block is processed, add the paramters for further nested blocks
154
193
  bParamAdded = true;
155
- this.blockParameters.unshift("index");
156
- this.blockParameters.unshift("item");
194
+ if (process.env.UI5_TS) {
195
+ this.blockParametersDefinition.push("item: any");
196
+ this.blockParametersDefinition.push("index: number");
197
+ } else {
198
+ this.blockParametersDefinition.push("item");
199
+ this.blockParametersDefinition.push("index");
200
+ }
201
+ this.blockParametersUsage.push("item");
202
+ this.blockParametersUsage.push("index");
157
203
  }
158
204
  this.acceptKey(block, "program");
159
205
  if (bParamAdded) {
160
- this.blockParameters.shift("item");
161
- this.blockParameters.shift("index");
206
+ // if parameters were added at this step, remove the last two
207
+ this.blockParametersDefinition.pop();
208
+ this.blockParametersDefinition.pop();
209
+ this.blockParametersUsage.pop();
210
+ this.blockParametersUsage.pop();
162
211
  }
163
- this.blockPath = "context";
164
-
212
+ this.blockLevel--;
165
213
  this.blocks[this.currentKey()] += ") }";
166
214
  }
167
215
 
@@ -170,12 +218,52 @@ function normalizePath(sPath) {
170
218
 
171
219
  //read carefully - https://github.com/wycats/handlebars.js/issues/1028
172
220
  //kpdecker commented on May 20, 2015
173
- if (result.indexOf("../") === 0) {
174
- let absolutePath = replaceAll(this.paths[this.paths.length - 1], ".", "/") + "/" + result;
221
+
222
+ if (result.indexOf("@root") === 0) {
223
+ // Trying to access root context via the HBS "@root" variable.
224
+ // Example: {{@root.property}} compiles to "context.property" - called from anywhere within the template.
225
+ result = result.replace("@root", "this");
226
+
227
+ } else if (result.indexOf("../") === 0) {
228
+ let absolutePath;
229
+ const levelsUp = (result.match(/..\//g) || []).length;
230
+
231
+ if (this.blockLevel <= levelsUp) {
232
+ // Trying to access root context from nested loops.
233
+ // Example: {{../../property}} compiles to "context.property" - when currently in a nested level loop.
234
+ // Example: {{../../../property}} compile to "context.property" - when requested levels are not present. fallback to root context.
235
+ absolutePath = `this.${replaceAll(result,"../", "")}`;
236
+ } else {
237
+ // Trying to access upper context (one-level-up) and based on the current lelev, that could be "context" or "item".
238
+ // Example: {{../property}} compiles to "context.property" - when called in a top level loop.
239
+ // Example: {{../property}} compiles to "item.property" - when called in a nested level loop.
240
+ // TODO: the second example, although correctly generated to "item.property", "item" will point to the current object within the nested loop,
241
+ // not the upper level loop as intended. So accessing the upper loop from nested loop is currently not working.
242
+ absolutePath = replaceAll(this.paths[this.paths.length - 1 - levelsUp], ".", "/") + "/" + result;
243
+ }
244
+
175
245
  result = replaceAll(path.normalize(absolutePath), path.sep, ".");
246
+
176
247
  } else {
177
- result = result ? replaceAll(this.blockPath + "/" + result, "/", ".") : this.blockPath;
248
+ // When neither "@root", nor "../" are used, use the following contexts:
249
+ // - use "context" - for the top level of execution, e.g "this.blockLevel = 0".
250
+ // - use "item" - for any nested level, e.g "this.blockLevel > 0".
251
+ // Example:
252
+ //
253
+ // {{text}} -> compiles to "context.text"
254
+ // {{#each items}}
255
+ // Item text: {{text}}</div> -> compiles to "item.text"
256
+ // {{#each words}}
257
+ // Word text: {{text}}</div> -> compiles to "item.text"
258
+ // {{/each}}
259
+ // Item text: {{text}}</div> -> compiles to "item.text"
260
+ // {{/each}}
261
+ // {{text}} -> compiles to "context.text"
262
+
263
+ const blockPath = this.blockLevel > 0 ? "item" : "this";
264
+ result = result ? replaceAll(blockPath + "/" + result, "/", ".") : blockPath;
178
265
  }
266
+
179
267
  return result;
180
268
  }
181
269
 
@@ -2,7 +2,7 @@
2
2
  const svgrx = new RegExp(/<svg[\s\S]*?>([\s\S]*?)<\/svg>/, 'g');
3
3
  const blockrx = /block[0-9]+/g;
4
4
 
5
- function process(input) {
5
+ function processSVG(input) {
6
6
  let matches;
7
7
  let template = input;
8
8
  let blockCounter = 0;
@@ -45,9 +45,16 @@ function getSVGMatches(template) {
45
45
  }
46
46
 
47
47
  function getSVGBlock(input, blockCounter) {
48
+ const definitionTS = `\nfunction blockSVG${blockCounter} (this: any, context: UI5Element, tags: string[], suffix: string | undefined) {
49
+ return svg\`${input}\`;
50
+ };`;
51
+ const definitionJS = `\nfunction blockSVG${blockCounter} (context, tags, suffix) {
52
+ return svg\`${input}\`;
53
+ };`;
54
+
48
55
  return {
49
- usage: `\${blockSVG${blockCounter}(context)}`,
50
- definition: `\nconst blockSVG${blockCounter} = (context) => {return svg\`${input}\`};`,
56
+ usage: `\${blockSVG${blockCounter}.call(this, context, tags, suffix)}`,
57
+ definition: process.env.UI5_TS ? definitionTS : definitionJS,
51
58
  };
52
59
  }
53
60
 
@@ -55,7 +62,7 @@ function replaceInternalBlocks(template, svgContent) {
55
62
  const internalBlocks = svgContent.match(blockrx) || [];
56
63
 
57
64
  internalBlocks.forEach(blockName => {
58
- const rx = new RegExp(`const ${blockName}.*(html\`).*};`);
65
+ const rx = new RegExp(`function ${blockName}.*(html\`).*;`);
59
66
  template = template.replace(rx, (match, p1) => {
60
67
  return match.replace(p1, "svg\`");
61
68
  });
@@ -65,5 +72,5 @@ function replaceInternalBlocks(template, svgContent) {
65
72
  }
66
73
 
67
74
  module.exports = {
68
- process: process
75
+ process: processSVG,
69
76
  };
@@ -1,19 +1,45 @@
1
- const buildRenderer = (controlName, litTemplate) => {
2
- return `
3
- /* eslint no-unused-vars: 0 */
4
- import ifDefined from '@ui5/webcomponents-base/dist/renderer/ifDefined.js';
5
- import { html, svg, repeat, classMap, styleMap, unsafeHTML, setTags, setSuffix } from '@ui5/webcomponents-base/dist/renderer/LitRenderer.js';
6
- ${litTemplate}
1
+ const tsImports = (controlName, hasTypes) => {
2
+ if (!process.env.UI5_TS) {
3
+ return "";
4
+ }
7
5
 
8
- const main = (context, tags, suffix) => {
9
- setTags(tags);
10
- setSuffix(suffix);
11
- return block0(context);
12
- };
13
-
14
- export default main;`
6
+ const importPrefix = process.env.UI5_BASE ? "../../../../../src/" : "@ui5/webcomponents-base/dist/"
7
+
8
+ return `import type UI5Element from "${importPrefix}UI5Element.js";
9
+ ${importForControl(controlName, hasTypes)}
10
+ import type { ClassMapValue } from "${importPrefix}types.js";
11
+ `;
12
+ }
13
+ const importForControl = (controlName, hasTypes) => {
14
+
15
+ if (!hasTypes) {
16
+ return `type ${controlName} = any;`;
17
+ }
18
+
19
+ if (process.env.UI5_BASE) {
20
+ // base package has a component in `test/elements` instead of `src`
21
+ return `import type ${controlName} from "../../../${controlName}.js";`
22
+ }
23
+ return `import type ${controlName} from "../../${controlName}.js";`
24
+ }
25
+
26
+ const buildRenderer = (controlName, litTemplate, hasTypes) => {
27
+ const importPrefix = process.env.UI5_BASE ? "../../../../../src/" : "@ui5/webcomponents-base/dist/";
28
+
29
+ const mainTemplateFunction = process.env.UI5_TS ?
30
+ `function template(this: ${controlName}) { return block0.call(this, this, (this.constructor as typeof UI5Element).tagsToScope, getCustomElementsScopingSuffix()); }` :
31
+ `function template() { return block0.call(this, this, this.constructor.tagsToScope, getCustomElementsScopingSuffix()); }`;
32
+
33
+ // typescript cannot process package imports for the same package and the paths are changed to relative for base package templates
34
+ return `/* eslint no-unused-vars: 0 */
35
+ import { html, svg, repeat, classMap, styleMap, ifDefined, unsafeHTML, scopeTag } from "${importPrefix}renderer/LitRenderer.js";
36
+ import { getCustomElementsScopingSuffix } from "${importPrefix}CustomElementsScopeUtils.js";
37
+ ${tsImports(controlName, hasTypes)}
38
+ ${litTemplate}
39
+ ${mainTemplateFunction}
40
+ export default template;`;
15
41
  };
16
42
 
17
43
  module.exports = {
18
44
  generateTemplate: buildRenderer
19
- };
45
+ };
@@ -1,10 +1,12 @@
1
- const fs = require('fs');
1
+ const fs = require('fs').promises;
2
+ const existsSync = require('fs').existsSync;
2
3
  const getopts = require('getopts');
3
4
  const hbs2lit = require('../hbs2lit');
4
5
  const path = require('path');
5
6
  const litRenderer = require('./RenderTemplates/LitRenderer');
6
7
  const recursiveReadDir = require("recursive-readdir");
7
- const mkdirp = require('mkdirp');
8
+
9
+ let missingTypesReported = false;
8
10
 
9
11
  const args = getopts(process.argv.slice(2), {
10
12
  alias: {
@@ -22,15 +24,29 @@ const onError = (place) => {
22
24
  console.log(`A problem occoured when reading ${place}. Please recheck passed parameters.`);
23
25
  };
24
26
 
25
- const isHandlebars = (fileName) => fileName.indexOf('.hbs') !== -1;
27
+ const isHandlebars = (fileName) => fileName.endsWith('.hbs');
26
28
 
27
- const processFile = (file, outputDir) => {
28
- const litCode = hbs2lit(file);
29
- const absoluteOutputDir = composeAbsoluteOutputDir(file, outputDir);
29
+ const hasTypes = (file, componentName) => {
30
+ const tsFile = path.join(path.dirname(file), componentName + ".ts")
31
+ const dtsFile = path.join(path.dirname(file), componentName + ".d.ts")
32
+ return existsSync(tsFile) || existsSync(dtsFile);
33
+ }
34
+
35
+ const processFile = async (file, outputDir) => {
30
36
  const componentNameMatcher = /(\w+)(\.hbs)/gim;
31
37
  const componentName = componentNameMatcher.exec(file)[1];
38
+ const componentHasTypes = hasTypes(file, componentName);
39
+ if (!componentHasTypes) {
40
+ if (!missingTypesReported) {
41
+ console.warn("[Warn] The following templates do not have a corresponging .ts or .d.ts file and won't be type checked:")
42
+ missingTypesReported = true;
43
+ }
44
+ console.log(" -> " + componentName + ".hbs");
45
+ }
46
+ const litCode = await hbs2lit(file, componentName);
47
+ const absoluteOutputDir = composeAbsoluteOutputDir(file, outputDir);
32
48
 
33
- writeRenderers(absoluteOutputDir, componentName, litRenderer.generateTemplate(componentName, litCode));
49
+ return writeRenderers(absoluteOutputDir, componentName, litRenderer.generateTemplate(componentName, litCode, componentHasTypes));
34
50
  };
35
51
 
36
52
  const composeAbsoluteOutputDir = (file, outputDir) => {
@@ -40,39 +56,54 @@ const composeAbsoluteOutputDir = (file, outputDir) => {
40
56
  const fileDir = file.split(path.sep).slice(1, -1).join(path.sep);
41
57
 
42
58
  // (2) Compose full output dir - "dist/generated/templates/lvl1/lvl2"
43
- return `${outputDir}${path.sep}${fileDir}`;
59
+ return `${outputDir}${path.sep}${fileDir}`;
44
60
  };
45
61
 
46
62
  const wrapDirectory = (directory, outputDir) => {
47
63
  directory = path.normalize(directory);
48
64
  outputDir = path.normalize(outputDir);
49
65
 
50
- recursiveReadDir(directory, (err, files) => {
51
-
52
- if (err) {
53
- onError('directory');
54
- }
66
+ return new Promise((resolve, reject) => {
67
+ recursiveReadDir(directory, (err, files) => {
55
68
 
56
- files.forEach(fileName => {
57
- if (isHandlebars(fileName)) {
58
- processFile(fileName, outputDir);
69
+ if (err) {
70
+ onError('directory');
71
+ reject();
59
72
  }
73
+
74
+ const promises = files.map(fileName => {
75
+ if (isHandlebars(fileName)) {
76
+ return processFile(fileName, outputDir);
77
+ }
78
+ }).filter(x => !!x);
79
+
80
+ resolve(Promise.all(promises));
60
81
  });
61
- })
82
+ });
62
83
  };
63
84
 
64
- const writeRenderers = (outputDir, controlName, fileContent) => {
85
+ const writeRenderers = async (outputDir, controlName, fileContent) => {
65
86
  try {
66
- if (!fs.existsSync(outputDir)) {
67
- mkdirp.sync(outputDir);
68
- }
69
87
 
70
- const compiledFilePath = `${outputDir}${path.sep}${controlName}Template.lit.js`;
88
+ await fs.mkdir(outputDir, { recursive: true });
89
+
90
+ const compiledFilePath = `${outputDir}${path.sep}${controlName}Template.lit.${process.env.UI5_TS ? "ts" : "js"}`;
71
91
 
72
92
  // strip DOS line endings because the break the source maps
73
93
  let fileContentUnix = fileContent.replace(/\r\n/g, "\n");
74
94
  fileContentUnix = fileContentUnix.replace(/\r/g, "\n");
75
- fs.writeFileSync(compiledFilePath, fileContentUnix);
95
+
96
+ // Only write to the file system actual changes - each updated file, no matter if the same or not, triggers an expensive operation for rollup
97
+ // Note: .hbs files that include a changed .hbs file will also be recompiled as their content will be updated too
98
+
99
+ let existingFileContent = "";
100
+ try {
101
+ existingFileContent = (await fs.readFile(compiledFilePath)).toString();
102
+ } catch (e) {}
103
+
104
+ if (existingFileContent !== fileContentUnix) {
105
+ return fs.writeFile(compiledFilePath, fileContentUnix);
106
+ }
76
107
 
77
108
  } catch (e) {
78
109
  console.log(e);
@@ -82,5 +113,7 @@ const writeRenderers = (outputDir, controlName, fileContent) => {
82
113
  if (!args['d'] || !args['o']) {
83
114
  console.log('Please provide an input and output directory (-d and -o)');
84
115
  } else {
85
- wrapDirectory(args['d'], args['o']);
116
+ wrapDirectory(args['d'], args['o']).then(() => {
117
+ console.log("Templates generated");
118
+ });
86
119
  }