diff --git a/.eslintrc.js b/.eslintrc.js index 8989198..786ada9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,7 @@ const config = { '.eslintrc.cjs', 'src/browser/**/*.ts', 'tests/**/*.ts', + '*.ts', ], }, ], diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..1e058fd --- /dev/null +++ b/build.ts @@ -0,0 +1,28 @@ +import { type Plugin, build } from 'esbuild' + +const name = 'ui5' + +const ui5Plugin = (): Plugin => ({ + name, + setup: (pluginBuild) => { + pluginBuild.onResolve({ filter: /^sap\//u }, ({ path }) => ({ + path, + namespace: name, + })) + pluginBuild.onLoad({ filter: /.*/u, namespace: name }, ({ path }) => ({ + contents: `export default sap.ui.require(${JSON.stringify(path)});`, + loader: 'js', + })) + }, +}) + +;(async () => { + await build({ + plugins: [ui5Plugin()], + entryPoints: ['src/browser/css.ts', 'src/browser/xpath.ts'], + bundle: true, + define: { global: 'window' }, + format: 'cjs', + outdir: 'dist/browser', + }) +})() diff --git a/package.json b/package.json index e3aece6..cc9e732 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint:fix": "npm run lint:check -- --fix", "format:check": "prettier --check .", "format:fix": "prettier --write .", - "build:browser": "tsc -p src/browser/tsconfig.json && esbuild src/browser/css.ts --bundle --define:global=window --format=cjs --outdir=dist/browser && esbuild src/browser/xpath.ts --bundle --define:global=window --format=cjs --outdir=dist/browser", + "build:browser": "tsc -p src/browser/tsconfig.json && node build.ts", "build:node": "tsc -p src/node/tsconfig.json", "typecheck:config-files": "tsc", "build": "rimraf ./dist && npm run build:browser && npm run build:node", diff --git a/src/browser/common.ts b/src/browser/common.ts index e99240c..9e7b00a 100644 --- a/src/browser/common.ts +++ b/src/browser/common.ts @@ -1,6 +1,4 @@ -import UI5Metadata from 'sap/ui/base/Metadata' -import type UI5Core from 'sap/ui/core/Core' -import UI5Element from 'sap/ui/core/Element' +import type Core from 'sap/ui/core/Core' /* eslint-disable @typescript-eslint/no-namespace -- see comment below */ declare global { @@ -10,20 +8,11 @@ declare global { // more supported esm package and declare the global namespaces ourselves. see // https://github.com/SAP/ui5-typescript/issues/289#issuecomment-1562667387 - // ideally these would be defined like `const Element = Ui5Element` instead of - // these fake subclasses. see https://github.com/microsoft/TypeScript/issues/36348 - namespace sap.ui.core { - class Element extends UI5Element {} - const Core: { - // no idea how this works, but it seems to be a class when using the global - // declaration but an instance when importing it as a module. - // https://github.com/SAP/ui5-typescript/issues/443#issuecomment-2074074078 - new (): UI5Core - (): UI5Core - } - } - namespace sap.ui.base { - class Metadata extends UI5Metadata {} + // to make the esm imports work at runtime we use a custom esbuild plugin (see build.ts) + // but we still need this globally define namespace gfor out `isui5` function so we can + // test if the current page actually has ui5 in it + namespace sap.ui { + const core: Core | undefined } } /* eslint-enable @typescript-eslint/no-namespace */ diff --git a/src/browser/css.ts b/src/browser/css.ts index 70c0051..c4e5aeb 100644 --- a/src/browser/css.ts +++ b/src/browser/css.ts @@ -1,10 +1,13 @@ import type { SelectorEngine } from '../common/types' import { Ui5SelectorEngineError, isUi5 } from './common' import { type AstSelector, type AstString, createParser } from 'css-selector-parser' +import type Metadata from 'sap/ui/base/Metadata' +import type Ui5Element from 'sap/ui/core/Element' +import ElementRegistry from 'sap/ui/core/ElementRegistry' import { throwIfUndefined } from 'throw-expression' -const getAllParents = (element: sap.ui.core.Element): string[] => { - const getParents = (class_: sap.ui.base.Metadata): sap.ui.base.Metadata[] => { +const getAllParents = (element: Ui5Element): string[] => { + const getParents = (class_: Metadata): Metadata[] => { const parent = class_.getParent() if (parent !== undefined) { return [class_, ...getParents(parent)] @@ -60,47 +63,45 @@ function* querySelector(root: Element | Document, selector: AstSelector) { delete rule.classNames } - const controls = - // eslint-disable-next-line detachhead/suggestions-as-errors, @typescript-eslint/no-unnecessary-condition -- using the deprecated registry since we still want to support older ui5 versions, seems any part of this can be undefined if the page is in the middle of loading - sap.ui?.core?.Element?.registry.filter((element) => { - if ( - (rule.tag?.type === 'TagName' && - rule.tag.name !== element.getMetadata().getName() && - (rule.pseudoElement !== 'subclass' || - !getAllParents(element).includes(rule.tag.name))) || - (rule.ids && rule.ids[0] !== element.getId()) - ) { + const controls = ElementRegistry.filter((element) => { + if ( + (rule.tag?.type === 'TagName' && + rule.tag.name !== element.getMetadata().getName() && + (rule.pseudoElement !== 'subclass' || + !getAllParents(element).includes(rule.tag.name))) || + (rule.ids && rule.ids[0] !== element.getId()) + ) { + return false + } + + return (rule.attributes ?? []).every((attr) => { + let actualValue: string + try { + actualValue = String(element.getProperty(attr.name)) + } catch { + // property doesn't exist return false } - - return (rule.attributes ?? []).every((attr) => { - let actualValue: string - try { - actualValue = String(element.getProperty(attr.name)) - } catch { - // property doesn't exist - return false - } - if (!('value' in attr)) { - // eg. sap.m.Button[attr] - return true - } - const expectedValue = (attr.value as AstString).value - return { - '=': actualValue === expectedValue, - '^=': actualValue.startsWith(expectedValue), - '$=': actualValue.endsWith(expectedValue), - '*=': actualValue.includes(expectedValue), - '~=': actualValue.trim() === expectedValue, - '|=': actualValue.split('-')[0] === expectedValue, - }[ - throwIfUndefined( - attr.operator, - 'attribute operator was undefined when value was set (this should NEVER happen)', - ) - ] - }) - }) ?? [] + if (!('value' in attr)) { + // eg. sap.m.Button[attr] + return true + } + const expectedValue = (attr.value as AstString).value + return { + '=': actualValue === expectedValue, + '^=': actualValue.startsWith(expectedValue), + '$=': actualValue.endsWith(expectedValue), + '*=': actualValue.includes(expectedValue), + '~=': actualValue.trim() === expectedValue, + '|=': actualValue.split('-')[0] === expectedValue, + }[ + throwIfUndefined( + attr.operator, + 'attribute operator was undefined when value was set (this should NEVER happen)', + ) + ] + }) + }) for (const control of controls) { const element = control.getDomRef() if ( diff --git a/src/browser/xpath.ts b/src/browser/xpath.ts index 8ecaadf..9a90fa6 100644 --- a/src/browser/xpath.ts +++ b/src/browser/xpath.ts @@ -6,6 +6,7 @@ import { evaluateXPathToNodes, registerCustomXPathFunction, } from 'fontoxpath' +import UI5Element from 'sap/ui/core/Element' import { throwIfUndefined } from 'throw-expression' import { create } from 'xmlbuilder2' @@ -84,8 +85,8 @@ registerCustomXPathFunction( 'item()*', (_, element: Element, name: string) => { const id = element.getAttribute('id') - // eslint-disable-next-line detachhead/suggestions-as-errors -- using deprecated byId method to support older ui5 versions - const ui5Element = sap.ui.getCore().byId(id) + + const ui5Element = UI5Element.getElementById(id) let result try { result = ui5Element?.getProperty(name) as unknown