Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4a874cc

Browse files
authoredMay 27, 2025··
feat: add Assets-node module for node/ssr env (#11522)
**Change** The change provides new entry point for assets (cldr, i18n and theming), called `Assets-node.js`. This module is an alternative to `Assets.js`- the assets are only registered and loaded on the fly with dymamic imports when needed **with added support for the** `with: { type: 'json' }` import attribute, which is required in certain environments, such as Node.js with server-side rendering (SSR) - to properly load JSON files. **Example**: ```ts await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } }); ``` **Recommended usage**: in environments that require SSR compatibility (e.g., Node.js). **Known issues** - Webpack < 5.84: older versions of Webpack do not support the `with: { type: 'json' }` syntax and will fail to build. - Vite (dev mode): When using Vite, the development server may raise errors (e.g., expecting application/json) unless additional configuration is added to handle JSON imports with attributes.
1 parent 4574d87 commit 4a874cc

File tree

16 files changed

+171
-4
lines changed

16 files changed

+171
-4
lines changed
 

‎docs/2-advanced/04-using-assets.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ These assets are important for **accessibility** and **globalization**.
1616

1717
Import the `dist/Assets.js` file of the respective NPM package:
1818

19-
`import "@ui5/<PACKAGE-NAME>/dist/Assets.js`
20-
`import "@ui5/<PACKAGE-NAME>/dist/Assets-fetch.js`
19+
- `import "@ui5/<PACKAGE-NAME>/dist/Assets.js`
20+
- `import "@ui5/<PACKAGE-NAME>/dist/Assets-fetch.js`
21+
- `import "@ui5/<PACKAGE-NAME>/dist/Assets-node.js`
2122

2223
** Note: read "Techcnocal aspects" below on how to choose which one to use**
2324

@@ -76,3 +77,14 @@ When you import the `dist/Assets-fetch.js` file of a given package, assets are o
7677
The issue is how to get the correct URL for the fetch to work and this is solved by using `import.meta.url` to resolve a relative path from a module to a JSON file
7778

7879
The approach can be used with a bundler and for CDN usage.
80+
81+
### Assets-node.js
82+
83+
This module is an alternative to `Assets.js`, with added support for the `with: { type: 'json' }` import attribute, which is required in certain environments—such as Node.js with server-side rendering (SSR) — to properly load JSON files.
84+
85+
This approach allows you to dynamically import assets while explicitly specifying the file type.
86+
87+
**For example:**
88+
```ts
89+
await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
90+
```

‎packages/ai/src/Assets-fetch.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// main package assets (transitively base, theming and icons)
2+
import "@ui5/webcomponents/dist/Assets-fetch.js";
3+
4+
// own fiori package assets
5+
import "./generated/json-imports/Themes-fetch.js";
6+
import "./generated/json-imports/i18n-fetch.js";

‎packages/ai/src/Assets-node.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* The `Assets-node` entry point is used to import all CLDR, theming, and i18n assets.
3+
*
4+
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
5+
* `with: { type: 'json' }` import attribute for loading JSON files.
6+
*
7+
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
8+
*
9+
* Example usage:
10+
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
11+
*/
12+
13+
// main package assets (transitively base, theming and icons)
14+
import "@ui5/webcomponents/dist/Assets-node.js";
15+
16+
// own fiori package assets
17+
import "./generated/json-imports/Themes-node.js";
18+
import "./generated/json-imports/i18n-node.js";

‎packages/fiori/src/Assets-fetch.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// main package assets (transitively base, theming and icons)
2+
import "@ui5/webcomponents/dist/Assets-fetch.js";
3+
4+
// own fiori package assets
5+
import "./generated/json-imports/Themes-fetch.js";
6+
import "./generated/json-imports/i18n-fetch.js";

‎packages/fiori/src/Assets-node.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* The `Assets-node` entry point is used to import all CLDR, theming, and i18n assets.
3+
*
4+
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
5+
* `with: { type: 'json' }` import attribute for loading JSON files.
6+
*
7+
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
8+
*
9+
* Example usage:
10+
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
11+
*/
12+
13+
// main package assets (transitively base, theming and icons)
14+
import "@ui5/webcomponents/dist/Assets-node.js";
15+
16+
// own fiori package assets
17+
import "./generated/json-imports/Themes-node.js";
18+
import "./generated/json-imports/i18n-node.js";

‎packages/icons/src/AllIcons-node.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* The `AllIcons-node` entry point is used to import all icons.
3+
*
4+
* It serves as an alternative to the `AllIcons` and `AllIcons-fetch` modules and supports the
5+
* `with: { type: 'json' }` import attribute for loading JSON files.
6+
*
7+
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
8+
*
9+
* Example usage:
10+
* await import("../generated/assets/v5/SAP-icons.json", { with: { type: 'json' } })
11+
*/
12+
13+
import "./json-imports/Icons-node.js";

‎packages/icons/src/Assets-node.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* The `Assets-node` entry point is used to import i18n assets.
3+
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
4+
* `with: { type: 'json' }` import attribute for loading JSON files.
5+
*
6+
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
7+
* Example usage:
8+
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
9+
*/
10+
11+
import "./generated/json-imports/i18n-node.js";
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { registerIconLoader, CollectionData } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";
2+
3+
const loadIconsBundle = async (collection: string): Promise<CollectionData> => {
4+
let iconData: CollectionData;
5+
6+
if (collection === "SAP-icons-v5") {
7+
iconData = (await import(/* webpackChunkName: "ui5-webcomponents-sap-icons-v5" */ "../generated/assets/v5/SAP-icons.json", { with: { type: "json" }})).default;
8+
} else {
9+
iconData = (await import(/* webpackChunkName: "ui5-webcomponents-sap-icons-v4" */ "../generated/assets/v4/SAP-icons.json", { with: { type: "json" }})).default;
10+
}
11+
12+
if (typeof iconData === "string" && (iconData as string).endsWith(".json")) {
13+
throw new Error("[icons] Invalid bundling detected - dynamic JSON imports bundled as URLs. Switch to inlining JSON files from the build. Check the \"Assets\" documentation for more information.");
14+
}
15+
return iconData;
16+
}
17+
18+
const registerLoaders = () => {
19+
registerIconLoader("SAP-icons-v4", loadIconsBundle);
20+
registerIconLoader("SAP-icons-v5", loadIconsBundle);
21+
};
22+
23+
registerLoaders();

‎packages/localization/lib/generate-json-imports/cldr.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import assets from "@ui5/webcomponents-tools/assets-meta.js";
33

44
const allLocales = assets.locales.all;
55

6-
const imports = allLocales.map(locale => `import ${locale} from "../assets/cldr/${locale}.json";`).join("\n");
76
const caseDynamicImports = allLocales.map(locale => `\t\tcase "${locale}": return (await import(/* webpackChunkName: "ui5-webcomponents-cldr-${locale}" */ "../assets/cldr/${locale}.json")).default;`).join("\n");
7+
const caseDynamicImportJSONAttr = allLocales.map(locale => `\t\tcase "${locale}": return (await import(/* webpackChunkName: "ui5-webcomponents-cldr-${locale}" */ "../assets/cldr/${locale}.json", {with: { type: 'json'}})).default;`).join("\n");
88
const caseFetchMetaResolve = allLocales.map(locale => `\t\tcase "${locale}": return (await fetch(new URL("../assets/cldr/${locale}.json", import.meta.url))).json();`).join("\n");
99
const localesKeysStrArray = allLocales.map(_ => `"${_}"`).join(",");
1010

@@ -38,6 +38,7 @@ const generate = async () => {
3838
return Promise.all([
3939
fs.writeFile("src/generated/json-imports/LocaleData.ts", contentDynamic(caseDynamicImports)),
4040
fs.writeFile("src/generated/json-imports/LocaleData-fetch.ts", contentDynamic(caseFetchMetaResolve)),
41+
fs.writeFile("src/generated/json-imports/LocaleData-node.ts", contentDynamic(caseDynamicImportJSONAttr)),
4142
]);
4243
}
4344

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* The `Assets-node` entry point is used to import CLDR assets.
3+
*
4+
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports
5+
* the `with: { type: 'json' }` import attribute for loading JSON files.
6+
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
7+
*
8+
* Example usage:
9+
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
10+
*/
11+
12+
import "./generated/json-imports/LocaleData-node.js";

‎packages/main/src/Assets-node.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* The `Assets-node` entry point is used to import all CLDR, theming, and i18n assets.
3+
*
4+
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
5+
* `with: { type: 'json' }` import attribute for loading JSON files.
6+
*
7+
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
8+
*
9+
* Example usage:
10+
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
11+
*/
12+
13+
// common assets
14+
import "@ui5/webcomponents-localization/dist/Assets-node.js"; // CLDR
15+
import "@ui5/webcomponents-theming/dist/Assets-node.js"; // Theming
16+
import "@ui5/webcomponents-icons/dist/Assets-node.js"; // Icons texts
17+
18+
// own main package assets
19+
import "./generated/json-imports/Themes-node.js";
20+
import "./generated/json-imports/i18n-node.js";

‎packages/main/test/ssr/Button-ssr.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import "./config.js";
2+
import "../../dist/Assets-node.js";
3+
import "@ui5/webcomponents-icons/dist/AllIcons-node.js";
4+
import Button from "../../dist/Button.js";

‎packages/main/test/ssr/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
2+
setLanguage("de");

‎packages/theming/src/Assets-node.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* The `Assets-node` entry point is used to import theming assets.
3+
*
4+
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
5+
* `with: { type: 'json' }` import attribute for loading JSON files.
6+
*
7+
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
8+
*
9+
* Example usage:
10+
* await import("../assets/themes/sap_horizon/parameters-bundle.css.json", { with: { type: 'json' } })
11+
*/
12+
13+
import "./generated/json-imports/Themes-node.js";

‎packages/tools/lib/generate-json-imports/i18n.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const generate = async () => {
3939
const inputFolder = path.normalize(process.argv[2]);
4040
const outputFileDynamic = path.normalize(`${process.argv[3]}/i18n.${ext}`);
4141
const outputFileFetchMetaResolve = path.normalize(`${process.argv[3]}/i18n-fetch.${ext}`);
42+
const outputFileDynamicImportJSONImport = path.normalize(`${process.argv[3]}/i18n-node.${ext}`);
4243

4344
// All languages present in the file system
4445
const files = await fs.readdir(inputFolder);
@@ -49,11 +50,13 @@ const generate = async () => {
4950

5051
let contentDynamic;
5152
let contentFetchMetaResolve;
53+
let contentDynamicImportJSONAttr;
5254

5355
// No i18n - just import dependencies, if any
5456
if (languages.length === 0) {
5557
contentDynamic = "";
5658
contentFetchMetaResolve = "";
59+
contentDynamicImportJSONAttr = "";
5760
// There is i18n - generate the full file
5861
} else {
5962
// Keys for the array
@@ -62,18 +65,20 @@ const generate = async () => {
6265
// Actual imports for json assets
6366
const dynamicImportsString = languages.map(key => ` case "${key}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-messagebundle-${key}" */ "../assets/i18n/messagebundle_${key}.json")).default;`).join("\n");
6467
const fetchMetaResolveString = languages.map(key => ` case "${key}": return (await fetch(new URL("../assets/i18n/messagebundle_${key}.json", import.meta.url))).json();`).join("\n");
68+
const dynamicImportJSONAttrString = languages.map(key => ` case "${key}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-messagebundle-${key}" */ "../assets/i18n/messagebundle_${key}.json", {with: { type: 'json'}})).default;`).join("\n");
6569

6670
// Resulting file content
6771

6872
contentDynamic = getContent(dynamicImportsString, languagesKeysStringArray, packageName);
6973
contentFetchMetaResolve = getContent(fetchMetaResolveString, languagesKeysStringArray, packageName);
70-
74+
contentDynamicImportJSONAttr = getContent(dynamicImportJSONAttrString, languagesKeysStringArray, packageName);
7175
}
7276

7377
await fs.mkdir(path.dirname(outputFileDynamic), { recursive: true });
7478
return Promise.all([
7579
fs.writeFile(outputFileDynamic, contentDynamic),
7680
fs.writeFile(outputFileFetchMetaResolve, contentFetchMetaResolve),
81+
fs.writeFile(outputFileDynamicImportJSONImport, contentDynamicImportJSONAttr),
7782
]);
7883
}
7984

‎packages/tools/lib/generate-json-imports/themes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const ext = isTypeScript ? 'ts' : 'js';
88
const generate = async () => {
99
const inputFolder = path.normalize(process.argv[2]);
1010
const outputFileDynamic = path.normalize(`${process.argv[3]}/Themes.${ext}`);
11+
const outputFileDynamicImportJSONAttr = path.normalize(`${process.argv[3]}/Themes-node.${ext}`);
1112
const outputFileFetchMetaResolve = path.normalize(`${process.argv[3]}/Themes-fetch.${ext}`);
1213

1314
// All supported optional themes
@@ -24,6 +25,7 @@ const generate = async () => {
2425

2526
const availableThemesArray = `[${themesOnFileSystem.map(theme => `"${theme}"`).join(", ")}]`;
2627
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");
2729
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");
2830

2931
// dynamic imports file content
@@ -54,6 +56,7 @@ ${availableThemesArray}
5456
await fs.mkdir(path.dirname(outputFileDynamic), { recursive: true });
5557
return Promise.all([
5658
fs.writeFile(outputFileDynamic, contentDynamic(dynamicImportLines)),
59+
fs.writeFile(outputFileDynamicImportJSONAttr, contentDynamic(dynamicImportJSONAttrLines)),
5760
fs.writeFile(outputFileFetchMetaResolve, contentDynamic(fetchMetaResolveLines)),
5861
]);
5962
};

0 commit comments

Comments
 (0)
Please sign in to comment.