Skip to content
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"type": "module",
"scripts": {
"generate": "yarn workspaces foreach --all --parallel run generate",
"generateAPI": "yarn workspaces foreach --all --parallel run generateAPI",
"generateAPI": "yarn generateCEM && yarn mergeCEM && yarn validateCEM",
"generateCEM": "yarn workspaces foreach --all --parallel run generateCEM",
"mergeCEM": "yarn workspaces foreach --all --parallel run mergeCEM",
"validateCEM": "yarn workspaces foreach --all --parallel run validateCEM",
"generateProd": "yarn workspaces foreach --all --parallel run generateProd",
"ts": "tsc -b",
"bundle": "yarn workspaces foreach --all --parallel run bundle",
Expand Down
4 changes: 3 additions & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"build": "wc-dev build",
"watch": "wc-dev watch",
"generate": "wc-dev generate",
"generateAPI": "wc-dev generateAPI",
"generateCEM": "wc-dev generateAPI.generateCEM",
"mergeCEM": "wc-dev generateAPI.mergeCEM",
"validateCEM": "wc-dev generateAPI.validateCEM",
"bundle": "wc-dev build.bundle",
"test": "yarn test:cypress",
"test:cypress": "wc-dev test-cy-ci",
Expand Down
6 changes: 3 additions & 3 deletions packages/base/package-scripts.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ const scripts = {
"ui5": `ui5nps-script "${LIB}copy-and-watch/index.js" "dist/sap/**/*" dist/prod/sap/`,
"preact": `ui5nps-script "${LIB}copy-and-watch/index.js" "dist/thirdparty/preact/**/*.js" dist/prod/thirdparty/preact/`,
"assets": `ui5nps-script "${LIB}copy-and-watch/index.js" "dist/generated/assets/**/*.json" dist/prod/generated/assets/`,
}
},
}
},
generateAPI: {
default: "ui5nps generateAPI.generateCEM generateAPI.validateCEM",
generateCEM: `ui5nps-script "${LIB}/cem/cem.js" analyze --config "${LIB}cem/custom-elements-manifest.config.mjs"`,
validateCEM: `ui5nps-script "${LIB}/cem/validate.js"`,
mergeCEM: `ui5nps-script "${LIB}cem/merge.mjs"`,
},
watch: {
default: 'ui5nps-p watch.src watch.styles', // concurently
Expand Down
4 changes: 3 additions & 1 deletion packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"start": "wc-dev start",
"build": "wc-dev build",
"generate": "wc-dev generate",
"generateAPI": "wc-dev generateAPI",
"generateCEM": "wc-dev generateAPI.generateCEM",
"mergeCEM": "wc-dev generateAPI.mergeCEM",
"validateCEM": "wc-dev generateAPI.validateCEM",
"generateProd": "wc-dev generateProd",
"bundle": "wc-dev build.bundle",
"test": "wc-dev test",
Expand Down
4 changes: 3 additions & 1 deletion packages/compat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"build": "wc-dev build",
"watch": "wc-dev watch",
"generate": "wc-dev generate",
"generateAPI": "wc-dev generateAPI",
"generateCEM": "wc-dev generateAPI.generateCEM",
"mergeCEM": "wc-dev generateAPI.mergeCEM",
"validateCEM": "wc-dev generateAPI.validateCEM",
"bundle": "wc-dev build.bundle",
"test": "yarn test:cypress",
"test:cypress": "wc-dev test-cy-ci",
Expand Down
4 changes: 3 additions & 1 deletion packages/fiori/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
"watch": "wc-dev watch",
"build": "wc-dev build",
"generate": "wc-dev generate",
"generateAPI": "wc-dev generateAPI",
"generateCEM": "wc-dev generateAPI.generateCEM",
"mergeCEM": "wc-dev generateAPI.mergeCEM",
"validateCEM": "wc-dev generateAPI.validateCEM",
"bundle": "wc-dev build.bundle",
"test": "yarn test:cypress",
"test:ssr": "node -e \"import('./test/ssr/component-imports.js')\"",
Expand Down
4 changes: 3 additions & 1 deletion packages/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"start": "wc-dev start",
"watch": "wc-dev watch",
"generate": "wc-dev generate",
"generateAPI": "wc-dev generateAPI",
"generateCEM": "wc-dev generateAPI.generateCEM",
"mergeCEM": "wc-dev generateAPI.mergeCEM",
"validateCEM": "wc-dev generateAPI.validateCEM",
"build": "wc-dev build",
"bundle": "wc-dev build.bundle",
"test": "yarn test:cypress",
Expand Down
4 changes: 2 additions & 2 deletions packages/tools/components-package/nps.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const getScripts = (options) => {
styleRelated: "ui5nps build.styles build.jsonImports build.jsImports",
},
prepare: {
default: `ui5nps clean prepare.all copy copyProps prepare.typescript generateAPI`,
default: `ui5nps clean prepare.all copy copyProps prepare.typescript`,
all: `ui5nps-p build.templates build.i18n prepare.styleRelated build.illustrations`, // concurently
styleRelated: "ui5nps build.styles build.jsonImports build.jsImports",
typescript: tsCommandOld,
Expand Down Expand Up @@ -164,9 +164,9 @@ const getScripts = (options) => {
bundle: `ui5nps-script ${LIB}dev-server/dev-server.mjs ${viteConfig}`,
},
generateAPI: {
default: tsOption ? "ui5nps generateAPI.generateCEM generateAPI.validateCEM" : "",
generateCEM: `ui5nps-script "${LIB}cem/cem.js" analyze --config "${LIB}cem/custom-elements-manifest.config.mjs"`,
validateCEM: `ui5nps-script "${LIB}cem/validate.js"`,
mergeCEM: `ui5nps-script "${LIB}cem/merge.mjs"`,
},
};

Expand Down
220 changes: 220 additions & 0 deletions packages/tools/lib/cem/merge.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { pathToFileURL } from "url";
import path from "path";
import { createRequire } from 'module';
import { readFile, writeFile } from "fs/promises";

const require = createRequire(import.meta.url);

const UI5_BASE_CLASS = "UI5Element";

const main = async (argv) => {
let customElementsPath = null;
const CACHED_CEMS = new Map();
const DECLARATION_PACKAGE = new WeakMap();
const DECLARATION_MODULE = new WeakMap();

function removeInheritedFrom(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (Array.isArray(obj)) {
return obj.map(item => removeInheritedFrom(item));
}

const result = {};
for (const [key, value] of Object.entries(obj)) {
if (key === 'inheritedFrom') {
continue;
}
result[key] = removeInheritedFrom(value);
}
return result;
}

async function readPackageJson(filePath) {
try {
return JSON.parse(await readFile(filePath, "utf-8"));
} catch (error) {
throw new Error(`Failed to read package.json at ${filePath}: ${error.message}`);
}
}

async function loadPackageJson(depName) {
try {
// First try the standard require method (works when exports includes package.json)
const pkg = require(`${depName}/package.json`);
const pkgPath = require.resolve(`${depName}/package.json`);
return { path: path.dirname(pkgPath), pkg };
} catch (e) {
// If that fails, resolve the package path and read package.json directly
try {
const packagePath = require.resolve(depName);
let currentDir = path.dirname(packagePath);

// Navigate up to find package.json (the resolved path might be deep in dist/ or similar)
while (currentDir !== path.parse(currentDir).root) {
try {
const pkgPath = path.join(currentDir, 'package.json');
const content = await readFile(pkgPath, 'utf-8');
const pkg = JSON.parse(content);

// Verify this is the correct package.json by checking the name
if (pkg.name === depName) {
return { path: currentDir, pkg };
}
} catch {
// Continue searching up the directory tree
}
currentDir = path.dirname(currentDir);
}
} catch (resolveError) {
// console.warn(`Could not resolve ${depName}:`, resolveError.message);
}
return null;
}
}

async function collectThirdPartyCem() {
const packageJSONPath = path.resolve(process.cwd(), "package.json");
const packageJSON = await readPackageJson(packageJSONPath);

const dependencyKeys = Object.keys(packageJSON).filter(key => key.toLowerCase().includes("dependencies"));
const dependencies = dependencyKeys.flatMap(key => Object.keys(packageJSON[key]));

const thirdPartCEM = (await Promise.all(dependencies.map(async dep => {
const result = await loadPackageJson(dep);
if (!result?.pkg?.customElements) return null;

return {
path: result.path,
name: dep,
cem: result.pkg.customElements
};
}))).filter(Boolean);

await Promise.all(thirdPartCEM.map(async dep => {
const cemPath = path.resolve(dep.path, dep.cem);
try {
const cemContent = JSON.parse(await readFile(cemPath, "utf-8"));
CACHED_CEMS.set(dep.name, cemContent);
} catch (error) {
console.warn(`Failed to read CEM for ${dep.name} from ${cemPath}: ${error.message}`);
}
}));
}

async function readCurrentCEM() {
const packageJSONPath = path.resolve(process.cwd(), "package.json");
const packageJSON = await readPackageJson(packageJSONPath);

if (!packageJSON?.customElements) {
return null;
}

customElementsPath = packageJSON.customElements;
const cemPath = path.resolve(process.cwd(), customElementsPath);

try {
const cemContent = JSON.parse(await readFile(cemPath, "utf-8"));
CACHED_CEMS.set(packageJSON.name, cemContent);
return cemContent;
} catch (error) {
throw new Error(`Failed to read CEM from ${cemPath}: ${error.message}`);
}
}

async function resolveReference(ref) {
const pkg = CACHED_CEMS.get(ref.package);

if (!pkg) {
return null;
}

const mod = (pkg.modules || []).find(m => m.path === ref.module);

if (!mod) {
return null;
}

const declaration = (mod.declarations || []).find(d => d.name === ref.name);

if (!declaration) {
return null;
}

DECLARATION_PACKAGE.set(declaration, ref.package);
DECLARATION_MODULE.set(declaration, ref.module);

return resolveDeclaration(declaration);
}

async function resolveDeclaration(declaration) {
if (!declaration.superclass || declaration.superclass.name === UI5_BASE_CLASS) {
return [declaration];
}

const superclassDeclarations = await resolveReference(declaration.superclass);
return [declaration, superclassDeclarations].flat().filter(Boolean);
}

const merge = async () => {
const currentCEM = await readCurrentCEM();
if (!currentCEM) {
throw new Error("No custom elements manifest found in current project");
}

await collectThirdPartyCem();

const modules = currentCEM.modules || [];

for (const mod of modules) {
const declarations = (mod.declarations || []).filter(d => d.kind === "class");

for (const declaration of declarations) {
const declarationHierarchy = await resolveDeclaration(declaration);
const allKeys = declarationHierarchy.flatMap(dec => Object.keys(dec));
const uniqueKeys = [...new Set(allKeys)];
const arrayKeys = uniqueKeys
.filter(key => !key.startsWith("_ui5"))
.filter(key => declarationHierarchy.some(dec => Array.isArray(dec[key])));

for (const key of arrayKeys) {
const allItems = declarationHierarchy.flatMap(dec => dec[key] || []);

// Remove duplicates based on name property
const seen = new Set();
declaration[key] = allItems.filter(item => {
if (!item.name) return true;
if (seen.has(item.name)) return false;
seen.add(item.name);
return true;
});
}
}
}

const cleanedCEM = removeInheritedFrom(currentCEM);
const outputPath = path.resolve(process.cwd(), customElementsPath);

try {
await writeFile(outputPath, JSON.stringify(cleanedCEM, null, 2), "utf-8");
console.log(`Successfully merged CEM to ${outputPath}`);
} catch (error) {
throw new Error(`Failed to write merged CEM to ${outputPath}: ${error.message}`);
}
};

await merge();
}

const filePath = process.argv[1];
const fileUrl = pathToFileURL(filePath).href;

if (import.meta.url === fileUrl) {
main(process.argv)
}

export default {
_ui5mainFn: main
}
Loading