From 14082caeb78fcd3677b017bacea4b2c1d6e1f780 Mon Sep 17 00:00:00 2001 From: davidglezz Date: Wed, 14 May 2025 12:00:35 +0200 Subject: [PATCH 1/9] feat(css-injector): simplify implementation Signed-off-by: davidglezz --- .../src/__tests__/css-injector.spec.ts | 76 ------------- .../src/css-injector/css-injector.ts | 102 ------------------ .../src/css-injector/css-injector.types.ts | 22 ---- packages/x-archetype-utils/src/index.ts | 2 - .../src/utils/__tests__/css-injector.spec.ts | 43 ++++++++ .../src/utils/css-injector/css-injector.ts | 47 ++++++++ .../utils/css-injector/css-injector.types.ts | 33 ++++++ packages/x-components/src/utils/index.ts | 2 + 8 files changed, 125 insertions(+), 202 deletions(-) delete mode 100644 packages/x-archetype-utils/src/__tests__/css-injector.spec.ts delete mode 100644 packages/x-archetype-utils/src/css-injector/css-injector.ts delete mode 100644 packages/x-archetype-utils/src/css-injector/css-injector.types.ts create mode 100644 packages/x-components/src/utils/__tests__/css-injector.spec.ts create mode 100644 packages/x-components/src/utils/css-injector/css-injector.ts create mode 100644 packages/x-components/src/utils/css-injector/css-injector.types.ts diff --git a/packages/x-archetype-utils/src/__tests__/css-injector.spec.ts b/packages/x-archetype-utils/src/__tests__/css-injector.spec.ts deleted file mode 100644 index ae0e4af52e..0000000000 --- a/packages/x-archetype-utils/src/__tests__/css-injector.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { WindowWithInjector } from '../css-injector/css-injector.types' -import { CssInjector } from '../css-injector/css-injector' - -const getInstance = () => (window as WindowWithInjector).xCSSInjector as CssInjector - -// NOTE: this test is expected to run secuentialy -describe('test custom css injector', () => { - it('reuses the same instance between initializations', () => { - const injector1 = new CssInjector() - const injector2 = new CssInjector() - - expect(injector1 === injector2).toBe(true) - }) - - it('is appended to the window under xCSSInjector', () => { - expect((window as WindowWithInjector).xCSSInjector).toBeInstanceOf(CssInjector) - }) - - it('can set the host element that will receive the styles', () => { - const injector = getInstance() - - // @ts-expect-error Property host is protected. - expect(injector.hosts.size).toBe(0) - - injector.setHost(document.head) - - // @ts-expect-error Property host is protected. - expect(injector.hosts.has(document)).toBe(true) - }) - - it('can remove host', () => { - const injector = getInstance() - - // TODO: after remove the deprecated method: injector.addHost(document) - // @ts-expect-error Property host is protected. - expect(injector.hosts.size).toBe(1) - - injector.removeHost(document) - - // @ts-expect-error Property host is protected. - expect(injector.hosts.size).toBe(0) - }) - - it('can add host', () => { - const injector = getInstance() - const domElement = document.createElement('div') - const shadowRoot = domElement.attachShadow({ mode: 'open' }) - - // @ts-expect-error Property host is protected. - expect(injector.hosts.size).toBe(0) - - injector.addHost(document) - injector.addHost(shadowRoot) - - // @ts-expect-error Property host is protected. - expect(injector.hosts.size).toBe(2) - // @ts-expect-error Property host is protected. - expect(injector.hosts.has(document)).toBeTruthy() - // @ts-expect-error Property host is protected. - expect(injector.hosts.has(shadowRoot)).toBeTruthy() - }) - - // adoptedStyleSheets.replaceSync is not implemented in jsdom - it.skip('adds styles string to all the hosts', () => { - const injector = getInstance() - - const styles = { - source: "* { background: 'red' }", - } - - injector.addStyle(styles) - - // @ts-expect-error Property host is protected. - expect(document.adoptedStyleSheets).toEqual(injector.stylesToAdopt) - }) -}) diff --git a/packages/x-archetype-utils/src/css-injector/css-injector.ts b/packages/x-archetype-utils/src/css-injector/css-injector.ts deleted file mode 100644 index 01ffff20b9..0000000000 --- a/packages/x-archetype-utils/src/css-injector/css-injector.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { Style, WindowWithInjector, XCSSInjector } from './css-injector.types' - -/** Singleton instance of the injector that will be used across all the initializations. */ -let instance: CssInjector | null = null - -/** - * Custom CSS injector that allows to inject styles into a host element. - * - * @public - */ -export class CssInjector implements XCSSInjector { - protected hosts = new Set() - protected stylesToAdopt: CSSStyleSheet[] = [] - - /** - * Initializes the instance of the injector if it's not already initialized and sets it in the - * window object if it's required. - * - * @param setInWindow - Whether to set the injector instance in the window object. - */ - public constructor(setInWindow = true) { - if (!(instance instanceof CssInjector)) { - // eslint-disable-next-line ts/no-this-alias - instance = this - } - - if (setInWindow) { - this.setInWindow() - } - - return instance - } - - /** - * Adds the style to the host element. - * - * @param style - The styles to be added. - * @param style.source - Styles source. - */ - addStyle(style: Style): void { - const sheet = new CSSStyleSheet() - sheet.replaceSync(style.source) - this.stylesToAdopt.push(sheet) - this.hosts.forEach(host => (host.adoptedStyleSheets = this.stylesToAdopt)) - } - - /** - * Sets the host element. Alias of addHost method. - * - * @param host - The host element. - * @deprecated Use addHost instead. - */ - setHost(host: Element | ShadowRoot): void { - this.addHost(host instanceof ShadowRoot ? host : document) - } - - /** - * Adds the element to the hosts set. - * - * @param host - The host element. - */ - addHost(host: Document | ShadowRoot): void { - this.hosts.add(host) - host.adoptedStyleSheets = this.stylesToAdopt - } - - /** - * Removes the element from the hosts set. - * - * @param host - The host element to remove. - */ - removeHost(host: Document | ShadowRoot): void { - this.hosts.delete(host) - } - - /** - * Sets the injector instance in the window object. - */ - setInWindow(): void { - if (typeof window !== 'undefined' && instance) { - ;(window as WindowWithInjector).xCSSInjector = instance - } - } - - /** - * Checks if the injector instance is in the window object. - * - * @returns Whether the injector instance is in the window object. - */ - isInWindow(): boolean { - return typeof window === 'undefined' - ? false - : (window as WindowWithInjector).xCSSInjector === instance - } -} - -/** - * Instance of the injector. - * - * @public - */ -export const cssInjector = new CssInjector(typeof window !== 'undefined') diff --git a/packages/x-archetype-utils/src/css-injector/css-injector.types.ts b/packages/x-archetype-utils/src/css-injector/css-injector.types.ts deleted file mode 100644 index ebce28ac13..0000000000 --- a/packages/x-archetype-utils/src/css-injector/css-injector.types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** The style payload interface. */ -export interface Style { - /** Css source. */ - source: string -} - -export interface XCSSInjector { - /** Function that will add the styles to the host. */ - addStyle: (style: Style) => void - /** @deprecated Use addHost method. */ - setHost: (el: Element | ShadowRoot) => void - /** Adds the element to the hosts set. */ - addHost: (el: Document | ShadowRoot) => void - /** Removes the element from the hosts set. */ - removeHost: (el: Document | ShadowRoot) => void - /** Set injector instance in the window object. */ - setInWindow: () => void - /** Check if the instance is set in the window object. */ - isInWindow: () => boolean -} - -export type WindowWithInjector = Window & { xCSSInjector?: XCSSInjector } diff --git a/packages/x-archetype-utils/src/index.ts b/packages/x-archetype-utils/src/index.ts index 39f56d20e6..032fddf153 100644 --- a/packages/x-archetype-utils/src/index.ts +++ b/packages/x-archetype-utils/src/index.ts @@ -1,6 +1,4 @@ export * from './build/rollup/rollup.config' export * from './build/webpack/webpack.config' -export * from './css-injector/css-injector' -export * from './css-injector/css-injector.types' export * from './i18n/i18n.plugin' export * from './i18n/i18n.types' diff --git a/packages/x-components/src/utils/__tests__/css-injector.spec.ts b/packages/x-components/src/utils/__tests__/css-injector.spec.ts new file mode 100644 index 0000000000..9a90d16f80 --- /dev/null +++ b/packages/x-components/src/utils/__tests__/css-injector.spec.ts @@ -0,0 +1,43 @@ +import type { WindowWithInjector } from '../css-injector/css-injector.types' +import { cssInjector as injector } from '../css-injector/css-injector' + +// NOTE: this test is expected to run secuentialy +describe('test custom css injector', () => { + it('is appended to the window under xCSSInjector', () => { + expect((window as WindowWithInjector).xCSSInjector).toBe(injector) + }) + + it('can remove host', () => { + // TODO: after remove the deprecated method: injector.addHost(document) + expect(injector.hosts.size).toBe(1) + + injector.removeHost(document) + + expect(injector.hosts.size).toBe(0) + }) + + it('can add host', () => { + const domElement = document.createElement('div') + const shadowRoot = domElement.attachShadow({ mode: 'open' }) + + expect(injector.hosts.size).toBe(0) + + injector.addHost(document) + injector.addHost(shadowRoot) + + expect(injector.hosts.size).toBe(2) + expect(injector.hosts.has(document)).toBeTruthy() + expect(injector.hosts.has(shadowRoot)).toBeTruthy() + }) + + // adoptedStyleSheets.replaceSync is not implemented in jsdom + it.skip('adds styles string to all the hosts', () => { + const styles = { + source: "* { background: 'red' }", + } + + injector.addStyle(styles) + + expect(document.adoptedStyleSheets).toEqual(injector.stylesToAdopt) + }) +}) diff --git a/packages/x-components/src/utils/css-injector/css-injector.ts b/packages/x-components/src/utils/css-injector/css-injector.ts new file mode 100644 index 0000000000..06dbe23aff --- /dev/null +++ b/packages/x-components/src/utils/css-injector/css-injector.ts @@ -0,0 +1,47 @@ +import type { Style, WindowWithInjector, XCSSInjector } from './css-injector.types' + +/** + * Custom CSS injector that allows to inject styles into a host element. + * + * @public + */ +export const cssInjector: XCSSInjector = { + hosts: new Set(), + stylesToAdopt: [] as CSSStyleSheet[], + /** + * Adds the style to the host element. + * + * @param style - The styles to be added. + * @param style.source - Styles source. + */ + addStyle(style: Style): void { + const sheet = new CSSStyleSheet() + sheet.replaceSync(style.source) + this.stylesToAdopt.push(sheet) + this.hosts.forEach(host => (host.adoptedStyleSheets = this.stylesToAdopt)) + }, + /** + * Adds the element to the hosts set. + * + * @param host - The host element. + */ + addHost(host: Document | ShadowRoot): void { + this.hosts.add(host) + host.adoptedStyleSheets = [...host.adoptedStyleSheets, ...this.stylesToAdopt] + }, + /** + * Removes the element from the hosts set. + * + * @param host - The host element to remove. + */ + removeHost(host: Document | ShadowRoot): void { + host.adoptedStyleSheets = host.adoptedStyleSheets.filter( + sheet => !this.stylesToAdopt.includes(sheet), + ) + this.hosts.delete(host) + }, +} + +if (typeof window !== 'undefined') { + ;(window as WindowWithInjector).xCSSInjector = cssInjector +} diff --git a/packages/x-components/src/utils/css-injector/css-injector.types.ts b/packages/x-components/src/utils/css-injector/css-injector.types.ts new file mode 100644 index 0000000000..f236297337 --- /dev/null +++ b/packages/x-components/src/utils/css-injector/css-injector.types.ts @@ -0,0 +1,33 @@ +/** + * The style payload interface. + * @public + */ +export interface Style { + /** Css source. */ + source: string +} + +/** + * The XCSSInjector interface. + * Custom CSS injector that allows to inject styles into a host element. + * @public + */ +export interface XCSSInjector { + /** Set of hosts that will receive the styles. */ + hosts: Set + /** Styles that will be injected into the hosts. */ + stylesToAdopt: CSSStyleSheet[] + /** Function that will add the styles to the host. */ + addStyle: (style: Style) => void + /** Adds the element to the hosts set. */ + addHost: (el: Document | ShadowRoot) => void + /** Removes the element from the hosts set. */ + removeHost: (el: Document | ShadowRoot) => void +} + +/** + * The XCSSInjector is added to window. + * @public + * @deprecated - It should not be required. It will be removed in the future. + */ +export type WindowWithInjector = Window & { xCSSInjector?: XCSSInjector } diff --git a/packages/x-components/src/utils/index.ts b/packages/x-components/src/utils/index.ts index c34f1487f6..b642331455 100644 --- a/packages/x-components/src/utils/index.ts +++ b/packages/x-components/src/utils/index.ts @@ -1,6 +1,8 @@ export * from './array' export * from './cancellable-promise' export * from './clone' +export * from './css-injector/css-injector' +export * from './css-injector/css-injector.types' export * from './currency-formatter' export { debounce as debounceFunction } from './debounce' export * from './filters' From 70edfa704238261b1b57ac0737194bb086a30872 Mon Sep 17 00:00:00 2001 From: davidglezz Date: Thu, 26 Feb 2026 11:53:41 +0100 Subject: [PATCH 2/9] feat(util): add function to create X Components root DOM element Signed-off-by: davidglezz --- .../x-components/src/utils/create-x-root.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 packages/x-components/src/utils/create-x-root.ts diff --git a/packages/x-components/src/utils/create-x-root.ts b/packages/x-components/src/utils/create-x-root.ts new file mode 100644 index 0000000000..1efa36ccc6 --- /dev/null +++ b/packages/x-components/src/utils/create-x-root.ts @@ -0,0 +1,22 @@ +import type { SnippetConfig } from '../x-installer' + +/** + * Creates a DOM element to mount the X Components app. + * + * @param snippetConfig - The snippet configuration. + * @param snippetConfig.isolate - Whether to isolate the DOM element using Shadow DOM. + * @returns The DOM element. + */ +export function createXRoot({ isolate }: SnippetConfig): ShadowRoot | HTMLElement { + const container = document.createElement('div') + container.classList.add('x-root-container') + document.body.appendChild(container) + + // Isolated by default + if (isolate !== false) { + const shadowRoot = container.attachShadow({ mode: 'open' }) + return shadowRoot + } else { + return container + } +} From 260c1726cdcf8a881669d1c9acf41b1bf2de374f Mon Sep 17 00:00:00 2001 From: davidglezz Date: Wed, 14 May 2025 14:22:02 +0200 Subject: [PATCH 3/9] feat(css-injector): replace the default css-injector Signed-off-by: davidglezz --- .../src/build/rollup/rollup.config.ts | 6 ------ packages/x-components/build/rollup.config.ts | 4 ++-- packages/x-components/build/tools/inject-css.js | 15 --------------- .../src/x-installer/x-installer/x-installer.ts | 5 ++++- 4 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 packages/x-components/build/tools/inject-css.js diff --git a/packages/x-archetype-utils/src/build/rollup/rollup.config.ts b/packages/x-archetype-utils/src/build/rollup/rollup.config.ts index db4c0f2560..3f482e76ab 100644 --- a/packages/x-archetype-utils/src/build/rollup/rollup.config.ts +++ b/packages/x-archetype-utils/src/build/rollup/rollup.config.ts @@ -1,10 +1,4 @@ export const rollupCssInjectorConfig = { - replace: { - // Replace X CSS injector by our custom one. - 'export default injectCss': - 'export default (css) => window.xCSSInjector.addStyle({ source: css });', - delimiters: ['', ''], - }, styles: { mode: ['inject', (varname: string) => `window.xCSSInjector.addStyle({ source: ${varname} });`], }, diff --git a/packages/x-components/build/rollup.config.ts b/packages/x-components/build/rollup.config.ts index e970999269..262b8b8861 100644 --- a/packages/x-components/build/rollup.config.ts +++ b/packages/x-components/build/rollup.config.ts @@ -84,8 +84,8 @@ export const rollupConfig: RollupOptions = { mode: [ 'inject', varname => { - const pathInjector = path.resolve('./tools/inject-css.js') - return `import injectCss from '${pathInjector}';injectCss(${varname});` + const pathInjector = path.resolve('../src/utils/css-injector/css-injector.ts') + return `import { cssInjector } from '${pathInjector}';cssInjector.addStyle({ source: ${varname} });` }, ], }), diff --git a/packages/x-components/build/tools/inject-css.js b/packages/x-components/build/tools/inject-css.js deleted file mode 100644 index e59854a047..0000000000 --- a/packages/x-components/build/tools/inject-css.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Simple CSS injector to append styles to the head. - * This injector can be overwritten at build time. - * - * @params css - CSS code. - */ -function injectCss(css) { - if (document) { - const el = document.createElement('style') - el.textContent = css - document.head.appendChild(el) - } -} - -export default injectCss diff --git a/packages/x-components/src/x-installer/x-installer/x-installer.ts b/packages/x-components/src/x-installer/x-installer/x-installer.ts index 5ca32f8dfd..e447b72e5d 100644 --- a/packages/x-components/src/x-installer/x-installer/x-installer.ts +++ b/packages/x-components/src/x-installer/x-installer/x-installer.ts @@ -8,6 +8,7 @@ import { forEach, isFunction } from '@empathyco/x-utils' import { createApp, reactive } from 'vue' import { bus } from '../../plugins/x-bus' import { XPlugin } from '../../plugins/x-plugin' +import { cssInjector } from '../../utils' import { BaseXAPI } from '../api/base-api' declare global { @@ -165,6 +166,8 @@ export class XInstaller { init(): Promise async init(snippetConfig = this.retrieveSnippetConfig()): Promise { if (snippetConfig) { + const mountingTarget = this.getMountingTarget(this.options.domElement) + cssInjector.addHost(mountingTarget instanceof ShadowRoot ? mountingTarget : document) this.snippetConfig = reactive(this.normaliseSnippetConfig(snippetConfig)) this.createApp() const bus = this.createBus() @@ -172,7 +175,7 @@ export class XInstaller { const plugin = this.installPlugin(pluginOptions, bus) await this.installExtraPlugins(bus) this.api?.setBus(bus) - this.app.mount(this.getMountingTarget(this.options.domElement)) + this.app.mount(mountingTarget) return { api: this.api, From b978f006160f5413b3e2c89bfff5712f5dd30c39 Mon Sep 17 00:00:00 2001 From: davidglezz Date: Thu, 26 Feb 2026 10:56:45 +0100 Subject: [PATCH 4/9] feat(base-teleport): use new css injector Signed-off-by: davidglezz feat(home): update BaseTeleport demo Signed-off-by: davidglezz --- packages/x-components/src/components/base-teleport.vue | 5 +++-- packages/x-components/src/views/home/Home.vue | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/x-components/src/components/base-teleport.vue b/packages/x-components/src/components/base-teleport.vue index 1043a3f6fb..39c4e8d2a9 100644 --- a/packages/x-components/src/components/base-teleport.vue +++ b/packages/x-components/src/components/base-teleport.vue @@ -16,6 +16,7 @@ import { watch, watchEffect, } from 'vue' +import { cssInjector } from '../utils/css-injector/css-injector' export default defineComponent({ name: 'BaseTeleport', @@ -71,7 +72,7 @@ export default defineComponent({ onUnmounted(() => { if (isIsolated && teleportHost.value) { - ;(window as any).xCSSInjector.removeHost(teleportHost.value.shadowRoot) + cssInjector.removeHost(teleportHost.value.shadowRoot!) } }) @@ -158,7 +159,7 @@ export default defineComponent({ isIsolated = instance?.appContext.app._container instanceof ShadowRoot if (isIsolated) { teleportHost.value.attachShadow({ mode: 'open' }) - ;(window as any).xCSSInjector.addHost(teleportHost.value.shadowRoot) + cssInjector.addHost(teleportHost.value.shadowRoot!) } } diff --git a/packages/x-components/src/views/home/Home.vue b/packages/x-components/src/views/home/Home.vue index a997ef1b86..331a6e47b8 100644 --- a/packages/x-components/src/views/home/Home.vue +++ b/packages/x-components/src/views/home/Home.vue @@ -554,7 +554,12 @@ - This is the teleport content + + Teleporting inside App Start + + + Teleporting outside App Start + diff --git a/packages/x-components/src/main.ts b/packages/x-components/src/main.ts index 60238cec04..5155c28e75 100644 --- a/packages/x-components/src/main.ts +++ b/packages/x-components/src/main.ts @@ -1,8 +1,10 @@ import type { App } from 'vue' +import type { SnippetConfig } from './x-installer' // eslint-disable-next-line import/no-named-default import { default as AppComponent } from './App.vue' import { setupDevtools } from './plugins/devtools/devtools.plugin' import router from './router' +import { createXRoot } from './utils/create-x-root' import { baseInstallXOptions, baseSnippetConfig } from './views/base-config' import { XInstaller } from './x-installer/x-installer/x-installer' import { FilterEntityFactory } from './x-modules/facets/entities/filter-entity.factory' @@ -24,21 +26,19 @@ FilterEntityFactory.instance.registerModifierByFilterModelName( SingleSelectModifier as any, ) +const snippetConfig = retrieveSnippetConfig() + const installer = new XInstaller({ ...baseInstallXOptions, rootComponent: AppComponent, - domElement: '#app', + domElement: createXRoot(snippetConfig), onCreateApp: initDevtools, installExtraPlugins({ app }) { app.use(router) }, }) -if (window.initX) { - void installer.init() -} else { - void installer.init(baseSnippetConfig) -} +void installer.init(snippetConfig) /** * If an app is provided, initialise the devtools. @@ -51,5 +51,17 @@ function initDevtools(app: App): void { setupDevtools(app) } } +/** + * Tries to retrieve the snippet config from the window.initX function or object. + */ +function retrieveSnippetConfig(): SnippetConfig { + if (typeof window.initX === 'function') { + return window.initX() + } + if (typeof window.initX === 'object') { + return window.initX + } + return baseSnippetConfig +} /* eslint-enable ts/no-unsafe-argument */ From 1feb1b0a22cc4304765f2efbebe59ce58aba846a Mon Sep 17 00:00:00 2001 From: davidglezz Date: Thu, 26 Feb 2026 12:02:41 +0100 Subject: [PATCH 6/9] build(css-injector): implement CSS injector plugin for Vue SFCs styles handling --- .../src/build/rollup/rollup.config.ts | 2 +- .../src/build/vite/css-injector-plugin.ts | 17 +++++++++++++++++ packages/x-components/build/rollup.config.ts | 8 +------- packages/x-components/src/main.ts | 4 +++- .../src/utils/css-injector/css-injector.ts | 16 +++++++++------- .../utils/css-injector/css-injector.types.ts | 7 +------ packages/x-components/vite.config.ts | 17 ++++++++++++++++- 7 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts diff --git a/packages/x-archetype-utils/src/build/rollup/rollup.config.ts b/packages/x-archetype-utils/src/build/rollup/rollup.config.ts index 3f482e76ab..9235c53714 100644 --- a/packages/x-archetype-utils/src/build/rollup/rollup.config.ts +++ b/packages/x-archetype-utils/src/build/rollup/rollup.config.ts @@ -1,5 +1,5 @@ export const rollupCssInjectorConfig = { styles: { - mode: ['inject', (varname: string) => `window.xCSSInjector.addStyle({ source: ${varname} });`], + mode: ['inject', (varname: string) => `window.xCSSInjector.addStyle([${varname}]);`], }, } diff --git a/packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts b/packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts new file mode 100644 index 0000000000..33f7dea77d --- /dev/null +++ b/packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts @@ -0,0 +1,17 @@ +/** + * This plugin add a custom block to Vue SFC files that injects the css styles using the global xCSSInjector. + */ +export function viteCssInjectorPlugin() { + return { + name: 'css-injector-plugin', + //enforce: 'pre', + transform(code: string, id: string) { + if (!id.endsWith('.vue') || !code.includes('')) return + return `${code} + + export default component => Promise.resolve(component).then(comp => window.xCSSInjector.addStyle(comp.styles)) + + ` + }, + } +} diff --git a/packages/x-components/build/rollup.config.ts b/packages/x-components/build/rollup.config.ts index 262b8b8861..c79b23cb11 100644 --- a/packages/x-components/build/rollup.config.ts +++ b/packages/x-components/build/rollup.config.ts @@ -81,13 +81,7 @@ export const rollupConfig: RollupOptions = { }) as Plugin, styles({ minimize: true, - mode: [ - 'inject', - varname => { - const pathInjector = path.resolve('../src/utils/css-injector/css-injector.ts') - return `import { cssInjector } from '${pathInjector}';cssInjector.addStyle({ source: ${varname} });` - }, - ], + mode: ['inject', varname => `window.xCSSInjector.addStyle([${varname} ]);`], }), vueDocs, generateEntryFiles({ buildPath, jsOutputDir, typesOutputDir }), diff --git a/packages/x-components/src/main.ts b/packages/x-components/src/main.ts index 5155c28e75..71d0ee8477 100644 --- a/packages/x-components/src/main.ts +++ b/packages/x-components/src/main.ts @@ -1,4 +1,6 @@ -import type { App } from 'vue' +/* eslint-disable perfectionist/sort-imports */ +// It must be the first, it setups the global cssInjector used by the styles injection system +import './utils/css-injector/css-injector' import type { SnippetConfig } from './x-installer' // eslint-disable-next-line import/no-named-default import { default as AppComponent } from './App.vue' diff --git a/packages/x-components/src/utils/css-injector/css-injector.ts b/packages/x-components/src/utils/css-injector/css-injector.ts index 06dbe23aff..97c1da2eae 100644 --- a/packages/x-components/src/utils/css-injector/css-injector.ts +++ b/packages/x-components/src/utils/css-injector/css-injector.ts @@ -1,4 +1,4 @@ -import type { Style, WindowWithInjector, XCSSInjector } from './css-injector.types' +import type { WindowWithInjector, XCSSInjector } from './css-injector.types' /** * Custom CSS injector that allows to inject styles into a host element. @@ -11,14 +11,16 @@ export const cssInjector: XCSSInjector = { /** * Adds the style to the host element. * - * @param style - The styles to be added. - * @param style.source - Styles source. + * @param css - The styles to be added. */ - addStyle(style: Style): void { + addStyle(css: string[]): void { + if (!Array.isArray(css)) { + return + } const sheet = new CSSStyleSheet() - sheet.replaceSync(style.source) + sheet.replaceSync(css.join('\n')) this.stylesToAdopt.push(sheet) - this.hosts.forEach(host => (host.adoptedStyleSheets = this.stylesToAdopt)) + this.hosts.forEach(host => host.adoptedStyleSheets.push(sheet)) }, /** * Adds the element to the hosts set. @@ -43,5 +45,5 @@ export const cssInjector: XCSSInjector = { } if (typeof window !== 'undefined') { - ;(window as WindowWithInjector).xCSSInjector = cssInjector + ;(window as WindowWithInjector).xCSSInjector ??= cssInjector } diff --git a/packages/x-components/src/utils/css-injector/css-injector.types.ts b/packages/x-components/src/utils/css-injector/css-injector.types.ts index f236297337..4971e575e0 100644 --- a/packages/x-components/src/utils/css-injector/css-injector.types.ts +++ b/packages/x-components/src/utils/css-injector/css-injector.types.ts @@ -2,11 +2,6 @@ * The style payload interface. * @public */ -export interface Style { - /** Css source. */ - source: string -} - /** * The XCSSInjector interface. * Custom CSS injector that allows to inject styles into a host element. @@ -18,7 +13,7 @@ export interface XCSSInjector { /** Styles that will be injected into the hosts. */ stylesToAdopt: CSSStyleSheet[] /** Function that will add the styles to the host. */ - addStyle: (style: Style) => void + addStyle: (css: string[]) => void /** Adds the element to the hosts set. */ addHost: (el: Document | ShadowRoot) => void /** Removes the element from the hosts set. */ diff --git a/packages/x-components/vite.config.ts b/packages/x-components/vite.config.ts index 05d5bcefe0..c1f53177f6 100644 --- a/packages/x-components/vite.config.ts +++ b/packages/x-components/vite.config.ts @@ -2,6 +2,7 @@ import { resolve } from 'path' import vue from '@vitejs/plugin-vue' import { defineConfig } from 'vite' import Inspector from 'vite-plugin-vue-inspector' +import { viteCssInjectorPlugin } from '../x-archetype-utils/src/build/vite/css-injector-plugin' export const vueDocsPlugin = { name: 'vue-docs', @@ -11,7 +12,21 @@ export const vueDocsPlugin = { } export default defineConfig({ - plugins: [vue(), vueDocsPlugin, Inspector()], + plugins: [ + viteCssInjectorPlugin(), + vue({ + features: { + customElement: true, + }, + template: { + compilerOptions: { + comments: false, + }, + }, + }), + vueDocsPlugin, + Inspector(), + ], resolve: { alias: { vue: resolve(__dirname, 'node_modules/vue'), From 7883858cfe084d04b98968793c7a4d3d25e51f3a Mon Sep 17 00:00:00 2001 From: davidglezz Date: Sun, 1 Mar 2026 12:35:02 +0100 Subject: [PATCH 7/9] build: fix resolve error Signed-off-by: davidglezz --- packages/x-components/vite.config.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/x-components/vite.config.ts b/packages/x-components/vite.config.ts index c1f53177f6..4a57580a9f 100644 --- a/packages/x-components/vite.config.ts +++ b/packages/x-components/vite.config.ts @@ -27,11 +27,6 @@ export default defineConfig({ vueDocsPlugin, Inspector(), ], - resolve: { - alias: { - vue: resolve(__dirname, 'node_modules/vue'), - }, - }, server: { port: 8080, host: '0.0.0.0', From 089caa2fe7dc45d914950eba8e44390dad4e6806 Mon Sep 17 00:00:00 2001 From: davidglezz Date: Sun, 1 Mar 2026 17:37:02 +0100 Subject: [PATCH 8/9] refactor(css-injector): rename addStyle to push to be array compatible Signed-off-by: davidglezz --- .../x-archetype-utils/src/build/rollup/rollup.config.ts | 2 +- .../src/build/vite/css-injector-plugin.ts | 2 +- packages/x-components/build/rollup.config.ts | 2 +- .../x-components/src/utils/css-injector/css-injector.ts | 9 ++++++--- .../src/utils/css-injector/css-injector.types.ts | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/x-archetype-utils/src/build/rollup/rollup.config.ts b/packages/x-archetype-utils/src/build/rollup/rollup.config.ts index 9235c53714..dfc255e33d 100644 --- a/packages/x-archetype-utils/src/build/rollup/rollup.config.ts +++ b/packages/x-archetype-utils/src/build/rollup/rollup.config.ts @@ -1,5 +1,5 @@ export const rollupCssInjectorConfig = { styles: { - mode: ['inject', (varname: string) => `window.xCSSInjector.addStyle([${varname}]);`], + mode: ['inject', (varname: string) => `(window.xCSSInjector ??= []).push(${varname});`], }, } diff --git a/packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts b/packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts index 33f7dea77d..8adad56e91 100644 --- a/packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts +++ b/packages/x-archetype-utils/src/build/vite/css-injector-plugin.ts @@ -9,7 +9,7 @@ export function viteCssInjectorPlugin() { if (!id.endsWith('.vue') || !code.includes('')) return return `${code} - export default component => Promise.resolve(component).then(comp => window.xCSSInjector.addStyle(comp.styles)) + export default component => Promise.resolve(component).then(comp => (window.xCSSInjector ??= []).push(...comp.styles)); ` }, diff --git a/packages/x-components/build/rollup.config.ts b/packages/x-components/build/rollup.config.ts index c79b23cb11..26032845f1 100644 --- a/packages/x-components/build/rollup.config.ts +++ b/packages/x-components/build/rollup.config.ts @@ -81,7 +81,7 @@ export const rollupConfig: RollupOptions = { }) as Plugin, styles({ minimize: true, - mode: ['inject', varname => `window.xCSSInjector.addStyle([${varname} ]);`], + mode: ['inject', varname => `(window.xCSSInjector ??= []).push(${varname});`], }), vueDocs, generateEntryFiles({ buildPath, jsOutputDir, typesOutputDir }), diff --git a/packages/x-components/src/utils/css-injector/css-injector.ts b/packages/x-components/src/utils/css-injector/css-injector.ts index 97c1da2eae..1acd5ee075 100644 --- a/packages/x-components/src/utils/css-injector/css-injector.ts +++ b/packages/x-components/src/utils/css-injector/css-injector.ts @@ -10,15 +10,16 @@ export const cssInjector: XCSSInjector = { stylesToAdopt: [] as CSSStyleSheet[], /** * Adds the style to the host element. + * @remark push is used to be compatible as array * * @param css - The styles to be added. */ - addStyle(css: string[]): void { - if (!Array.isArray(css)) { + push(css: string): void { + if (!css) { return } const sheet = new CSSStyleSheet() - sheet.replaceSync(css.join('\n')) + sheet.replaceSync(css) this.stylesToAdopt.push(sheet) this.hosts.forEach(host => host.adoptedStyleSheets.push(sheet)) }, @@ -45,5 +46,7 @@ export const cssInjector: XCSSInjector = { } if (typeof window !== 'undefined') { + const toAdd = ((window as WindowWithInjector).xCSSInjector ?? []) as string[] + toAdd.forEach(css => cssInjector.push(css)) ;(window as WindowWithInjector).xCSSInjector ??= cssInjector } diff --git a/packages/x-components/src/utils/css-injector/css-injector.types.ts b/packages/x-components/src/utils/css-injector/css-injector.types.ts index 4971e575e0..9f56ad9f4d 100644 --- a/packages/x-components/src/utils/css-injector/css-injector.types.ts +++ b/packages/x-components/src/utils/css-injector/css-injector.types.ts @@ -13,7 +13,7 @@ export interface XCSSInjector { /** Styles that will be injected into the hosts. */ stylesToAdopt: CSSStyleSheet[] /** Function that will add the styles to the host. */ - addStyle: (css: string[]) => void + push: (css: string) => void /** Adds the element to the hosts set. */ addHost: (el: Document | ShadowRoot) => void /** Removes the element from the hosts set. */ From 84ea2bdda96d54bf83a23b04d747eab15fdee45c Mon Sep 17 00:00:00 2001 From: davidglezz Date: Sun, 1 Mar 2026 17:39:20 +0100 Subject: [PATCH 9/9] build: add vite-plugin-css-injected-by-js for CSS injection support Signed-off-by: davidglezz --- packages/x-components/package.json | 1 + packages/x-components/src/main.ts | 5 +++-- packages/x-components/src/views/base-config.ts | 1 + packages/x-components/vite.config.ts | 9 ++++++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/x-components/package.json b/packages/x-components/package.json index e8795bb5e1..63714fb21e 100644 --- a/packages/x-components/package.json +++ b/packages/x-components/package.json @@ -116,6 +116,7 @@ "ts-node": "10.9.2", "typescript": "5.9.3", "vite": "6.4.1", + "vite-plugin-css-injected-by-js": "4.0.1", "vite-plugin-vue-inspector": "5.3.2", "vue": "3.5.28", "vue-docgen-cli": "4.79.0", diff --git a/packages/x-components/src/main.ts b/packages/x-components/src/main.ts index 71d0ee8477..a80f89dabe 100644 --- a/packages/x-components/src/main.ts +++ b/packages/x-components/src/main.ts @@ -2,8 +2,9 @@ // It must be the first, it setups the global cssInjector used by the styles injection system import './utils/css-injector/css-injector' import type { SnippetConfig } from './x-installer' -// eslint-disable-next-line import/no-named-default -import { default as AppComponent } from './App.vue' + +import type { App } from 'vue' +import AppComponent from './App.vue' import { setupDevtools } from './plugins/devtools/devtools.plugin' import router from './router' import { createXRoot } from './utils/create-x-root' diff --git a/packages/x-components/src/views/base-config.ts b/packages/x-components/src/views/base-config.ts index cea82006bb..47e5e83353 100644 --- a/packages/x-components/src/views/base-config.ts +++ b/packages/x-components/src/views/base-config.ts @@ -7,6 +7,7 @@ export const baseSnippetConfig: SnippetConfig = { lang: 'en', env: 'staging', scope: 'x-components-development', + isolate: false } // eslint-disable-next-line ts/no-unsafe-assignment diff --git a/packages/x-components/vite.config.ts b/packages/x-components/vite.config.ts index 4a57580a9f..5297c0501c 100644 --- a/packages/x-components/vite.config.ts +++ b/packages/x-components/vite.config.ts @@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue' import { defineConfig } from 'vite' import Inspector from 'vite-plugin-vue-inspector' import { viteCssInjectorPlugin } from '../x-archetype-utils/src/build/vite/css-injector-plugin' +import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' export const vueDocsPlugin = { name: 'vue-docs', @@ -14,6 +15,12 @@ export const vueDocsPlugin = { export default defineConfig({ plugins: [ viteCssInjectorPlugin(), + cssInjectedByJsPlugin({ + dev: { + enableDev: true, + }, + injectCode: (cssCode: string) => `(window.xCSSInjector ??= []).push(${cssCode});` + }), vue({ features: { customElement: true, @@ -25,7 +32,7 @@ export default defineConfig({ }, }), vueDocsPlugin, - Inspector(), + //Inspector(), ], server: { port: 8080,