Skip to content

feat: add Assets-import-attributes module for node/ssr env #11522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/2-advanced/04-using-assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ These assets are important for **accessibility** and **globalization**.

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

`import "@ui5/<PACKAGE-NAME>/dist/Assets.js`
`import "@ui5/<PACKAGE-NAME>/dist/Assets-fetch.js`
- `import "@ui5/<PACKAGE-NAME>/dist/Assets.js`
- `import "@ui5/<PACKAGE-NAME>/dist/Assets-fetch.js`
- `import "@ui5/<PACKAGE-NAME>/dist/Assets-import-attributes.js`

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

Expand Down Expand Up @@ -76,3 +77,14 @@ When you import the `dist/Assets-fetch.js` file of a given package, assets are o
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

The approach can be used with a bundler and for CDN usage.

### Assets-import-attributes.js

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.

This approach allows you to dynamically import assets while explicitly specifying the file type.

**For example:**
```ts
await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
```
13 changes: 13 additions & 0 deletions packages/icons/src/AllIcons-import-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* The `AllIcons-import-attributes` entry point is used to import all icons.
*
* It serves as an alternative to the `AllIcons` and `AllIcons-fetch` modules and supports the
* `with: { type: 'json' }` import attribute for loading JSON files.
*
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
*
* Example usage:
* await import("../generated/assets/v5/SAP-icons.json", { with: { type: 'json' } })
*/

import "./json-imports/Icons-import-attributes.js";
11 changes: 11 additions & 0 deletions packages/icons/src/Assets-import-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* The `Assets-import-attributes` entry point is used to import i18n assets.
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
* `with: { type: 'json' }` import attribute for loading JSON files.
*
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
* Example usage:
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
*/

import "./generated/json-imports/i18n-import-attributes.js";
23 changes: 23 additions & 0 deletions packages/icons/src/json-imports/Icons-import-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { registerIconLoader, CollectionData } from "@ui5/webcomponents-base/dist/asset-registries/Icons.js";

const loadIconsBundle = async (collection: string): Promise<CollectionData> => {
let iconData: CollectionData;

if (collection === "SAP-icons-v5") {
iconData = (await import(/* webpackChunkName: "ui5-webcomponents-sap-icons-v5" */ "../generated/assets/v5/SAP-icons.json", { with: { type: "json" }})).default;
} else {
iconData = (await import(/* webpackChunkName: "ui5-webcomponents-sap-icons-v4" */ "../generated/assets/v4/SAP-icons.json", { with: { type: "json" }})).default;
}

if (typeof iconData === "string" && (iconData as string).endsWith(".json")) {
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.");
}
return iconData;
}

const registerLoaders = () => {
registerIconLoader("SAP-icons-v4", loadIconsBundle);
registerIconLoader("SAP-icons-v5", loadIconsBundle);
};

registerLoaders();
3 changes: 2 additions & 1 deletion packages/localization/lib/generate-json-imports/cldr.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import assets from "@ui5/webcomponents-tools/assets-meta.js";

const allLocales = assets.locales.all;

const imports = allLocales.map(locale => `import ${locale} from "../assets/cldr/${locale}.json";`).join("\n");
const caseDynamicImports = allLocales.map(locale => `\t\tcase "${locale}": return (await import(/* webpackChunkName: "ui5-webcomponents-cldr-${locale}" */ "../assets/cldr/${locale}.json")).default;`).join("\n");
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");
const caseFetchMetaResolve = allLocales.map(locale => `\t\tcase "${locale}": return (await fetch(new URL("../assets/cldr/${locale}.json", import.meta.url))).json();`).join("\n");
const localesKeysStrArray = allLocales.map(_ => `"${_}"`).join(",");

Expand Down Expand Up @@ -38,6 +38,7 @@ const generate = async () => {
return Promise.all([
fs.writeFile("src/generated/json-imports/LocaleData.ts", contentDynamic(caseDynamicImports)),
fs.writeFile("src/generated/json-imports/LocaleData-fetch.ts", contentDynamic(caseFetchMetaResolve)),
fs.writeFile("src/generated/json-imports/LocaleData-import-attributes.ts", contentDynamic(caseDynamicImportJSONAttr)),
]);
}

Expand Down
12 changes: 12 additions & 0 deletions packages/localization/src/Assets-import-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* The `Assets-import-attributes` entry point is used to import CLDR assets.
*
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports
* the `with: { type: 'json' }` import attribute for loading JSON files.
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
*
* Example usage:
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
*/

import "./generated/json-imports/LocaleData-import-attributes.js";
20 changes: 20 additions & 0 deletions packages/main/src/Assets-import-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* The `Assets-import-attributes` entry point is used to import all CLDR, theming, and i18n assets.
*
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
* `with: { type: 'json' }` import attribute for loading JSON files.
*
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
*
* Example usage:
* await import("../assets/i18n/messagebundle_bg.json", { with: { type: 'json' } })
*/

// common assets
import "@ui5/webcomponents-localization/dist/Assets-import-attributes.js"; // CLDR
import "@ui5/webcomponents-theming/dist/Assets-import-attributes.js"; // Theming
import "@ui5/webcomponents-icons/dist/Assets-import-attributes.js"; // Icons texts

// own main package assets
import "./generated/json-imports/Themes-import-attributes.js";
import "./generated/json-imports/i18n-import-attributes.js";
4 changes: 4 additions & 0 deletions packages/main/test/ssr/Button-ssr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import "./config.js";
import "../../dist/Assets-import-attributes.js";
import "@ui5/webcomponents-icons/dist/AllIcons-import-attributes.js";
import Button from "../../dist/Button.js";
2 changes: 2 additions & 0 deletions packages/main/test/ssr/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js";
setLanguage("de");
13 changes: 13 additions & 0 deletions packages/theming/src/Assets-import-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* The `Assets-import-attributes` entry point is used to import theming assets.
*
* It serves as an alternative to the `Assets` and `Assets-fetch` modules and supports the
* `with: { type: 'json' }` import attribute for loading JSON files.
*
* This import attribute is required in some environments, such as Node.js with server-side rendering (SSR).
*
* Example usage:
* await import("../assets/themes/sap_horizon/parameters-bundle.css.json", { with: { type: 'json' } })
*/

import "./generated/json-imports/Themes-import-attributes.js";
7 changes: 6 additions & 1 deletion packages/tools/lib/generate-json-imports/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const generate = async () => {
const inputFolder = path.normalize(process.argv[2]);
const outputFileDynamic = path.normalize(`${process.argv[3]}/i18n.${ext}`);
const outputFileFetchMetaResolve = path.normalize(`${process.argv[3]}/i18n-fetch.${ext}`);
const outputFileDynamicImportJSONImport = path.normalize(`${process.argv[3]}/i18n-import-attributes.${ext}`);

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

let contentDynamic;
let contentFetchMetaResolve;
let contentDynamicImportJSONAttr;

// No i18n - just import dependencies, if any
if (languages.length === 0) {
contentDynamic = "";
contentFetchMetaResolve = "";
contentDynamicImportJSONAttr = "";
// There is i18n - generate the full file
} else {
// Keys for the array
Expand All @@ -62,18 +65,20 @@ const generate = async () => {
// Actual imports for json assets
const dynamicImportsString = languages.map(key => ` case "${key}": return (await import(/* webpackChunkName: "${packageName.replace("@", "").replace("/", "-")}-messagebundle-${key}" */ "../assets/i18n/messagebundle_${key}.json")).default;`).join("\n");
const fetchMetaResolveString = languages.map(key => ` case "${key}": return (await fetch(new URL("../assets/i18n/messagebundle_${key}.json", import.meta.url))).json();`).join("\n");
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");

// Resulting file content

contentDynamic = getContent(dynamicImportsString, languagesKeysStringArray, packageName);
contentFetchMetaResolve = getContent(fetchMetaResolveString, languagesKeysStringArray, packageName);

contentDynamicImportJSONAttr = getContent(dynamicImportJSONAttrString, languagesKeysStringArray, packageName);
}

await fs.mkdir(path.dirname(outputFileDynamic), { recursive: true });
return Promise.all([
fs.writeFile(outputFileDynamic, contentDynamic),
fs.writeFile(outputFileFetchMetaResolve, contentFetchMetaResolve),
fs.writeFile(outputFileDynamicImportJSONImport, contentDynamicImportJSONAttr),
]);
}

Expand Down
3 changes: 3 additions & 0 deletions packages/tools/lib/generate-json-imports/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const ext = isTypeScript ? 'ts' : 'js';
const generate = async () => {
const inputFolder = path.normalize(process.argv[2]);
const outputFileDynamic = path.normalize(`${process.argv[3]}/Themes.${ext}`);
const outputFileDynamicImportJSONAttr = path.normalize(`${process.argv[3]}/Themes-import-attributes.${ext}`);
const outputFileFetchMetaResolve = path.normalize(`${process.argv[3]}/Themes-fetch.${ext}`);

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

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

// dynamic imports file content
Expand Down Expand Up @@ -54,6 +56,7 @@ ${availableThemesArray}
await fs.mkdir(path.dirname(outputFileDynamic), { recursive: true });
return Promise.all([
fs.writeFile(outputFileDynamic, contentDynamic(dynamicImportLines)),
fs.writeFile(outputFileDynamicImportJSONAttr, contentDynamic(dynamicImportJSONAttrLines)),
fs.writeFile(outputFileFetchMetaResolve, contentDynamic(fetchMetaResolveLines)),
]);
};
Expand Down
Loading