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/build/rollup/rollup.config.ts b/packages/x-archetype-utils/src/build/rollup/rollup.config.ts
index db4c0f2560..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,11 +1,5 @@
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} });`],
+ 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
new file mode 100644
index 0000000000..8adad56e91
--- /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 ??= []).push(...comp.styles));
+
+ `
+ },
+ }
+}
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/build/rollup.config.ts b/packages/x-components/build/rollup.config.ts
index e970999269..26032845f1 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('./tools/inject-css.js')
- return `import injectCss from '${pathInjector}';injectCss(${varname});`
- },
- ],
+ mode: ['inject', varname => `(window.xCSSInjector ??= []).push(${varname});`],
}),
vueDocs,
generateEntryFiles({ buildPath, jsOutputDir, typesOutputDir }),
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/index.html b/packages/x-components/index.html
index 6601d1fdf8..8dcfbe5b55 100644
--- a/packages/x-components/index.html
+++ b/packages/x-components/index.html
@@ -15,7 +15,6 @@
/>
-
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/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/main.ts b/packages/x-components/src/main.ts
index 60238cec04..a80f89dabe 100644
--- a/packages/x-components/src/main.ts
+++ b/packages/x-components/src/main.ts
@@ -1,8 +1,13 @@
+/* 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'
+
import type { App } from 'vue'
-// eslint-disable-next-line import/no-named-default
-import { default as AppComponent } from './App.vue'
+import 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 +29,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 +54,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 */
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/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
+ }
+}
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..1acd5ee075
--- /dev/null
+++ b/packages/x-components/src/utils/css-injector/css-injector.ts
@@ -0,0 +1,52 @@
+import type { 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.
+ * @remark push is used to be compatible as array
+ *
+ * @param css - The styles to be added.
+ */
+ push(css: string): void {
+ if (!css) {
+ return
+ }
+ const sheet = new CSSStyleSheet()
+ sheet.replaceSync(css)
+ this.stylesToAdopt.push(sheet)
+ this.hosts.forEach(host => host.adoptedStyleSheets.push(sheet))
+ },
+ /**
+ * 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') {
+ 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
new file mode 100644
index 0000000000..9f56ad9f4d
--- /dev/null
+++ b/packages/x-components/src/utils/css-injector/css-injector.types.ts
@@ -0,0 +1,28 @@
+/**
+ * The style payload interface.
+ * @public
+ */
+/**
+ * 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. */
+ push: (css: string) => 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'
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/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
+