diff --git a/packages/twenty-apps/fixtures/minimal-app/css.d.ts b/packages/twenty-apps/fixtures/minimal-app/css.d.ts new file mode 100644 index 0000000000..35306c6fc9 --- /dev/null +++ b/packages/twenty-apps/fixtures/minimal-app/css.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/packages/twenty-apps/fixtures/minimal-app/my.front-component.tsx b/packages/twenty-apps/fixtures/minimal-app/my.front-component.tsx index 532e48ff7d..b6bb9ac048 100644 --- a/packages/twenty-apps/fixtures/minimal-app/my.front-component.tsx +++ b/packages/twenty-apps/fixtures/minimal-app/my.front-component.tsx @@ -1,5 +1,7 @@ import { defineFrontComponent } from 'twenty-sdk/define'; +import 'twenty-ui/style.css'; + export const MyComponent = () => { return (
diff --git a/packages/twenty-apps/fixtures/minimal-app/package.json b/packages/twenty-apps/fixtures/minimal-app/package.json index 8e04d0a7be..49d6c43036 100644 --- a/packages/twenty-apps/fixtures/minimal-app/package.json +++ b/packages/twenty-apps/fixtures/minimal-app/package.json @@ -15,7 +15,8 @@ }, "dependencies": { "twenty-sdk": "latest", - "twenty-client-sdk": "latest" + "twenty-client-sdk": "latest", + "twenty-ui": "latest" }, "devDependencies": { "typescript": "^5.9.3", diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/__tests__/css-injection-plugin.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/__tests__/css-injection-plugin.spec.ts new file mode 100644 index 0000000000..df5fabecd2 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/__tests__/css-injection-plugin.spec.ts @@ -0,0 +1,33 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'path'; + +import * as esbuild from 'esbuild'; + +import { MINIMAL_APP_PATH } from '@/cli/__tests__/apps/fixture-paths'; +import { getBaseFrontComponentBuildOptions } from '@/cli/utilities/build/common/front-component-build/utils/get-base-front-component-build-options'; + +describe('front-component build CSS injection', () => { + it('inlines an imported twenty-ui/style.css as a runtime document.head style injection', async () => { + const outputDir = await mkdtemp(join(tmpdir(), 'css-injection-')); + + try { + await esbuild.build({ + ...getBaseFrontComponentBuildOptions(), + entryPoints: [join(MINIMAL_APP_PATH, 'my.front-component.tsx')], + outdir: outputDir, + }); + + const output = await readFile( + join(outputDir, 'my.front-component.mjs'), + 'utf-8', + ); + + expect(output).toContain('document.createElement("style")'); + expect(output).toContain('document.head.appendChild'); + expect(output).toContain('box-sizing'); + } finally { + await rm(outputDir, { recursive: true, force: true }); + } + }, 30000); +}); diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/css-injection-plugin.ts b/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/css-injection-plugin.ts new file mode 100644 index 0000000000..79c3d53ff9 --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/css-injection-plugin.ts @@ -0,0 +1,24 @@ +import { readFile } from 'node:fs/promises'; + +import type * as esbuild from 'esbuild'; + +const buildStyleInjectionModule = (cssText: string): string => + `if (typeof document !== 'undefined') { + const styleElement = document.createElement('style'); + document.head.appendChild(styleElement); + styleElement.textContent = ${JSON.stringify(cssText)}; +}`; + +export const cssInjectionPlugin: esbuild.Plugin = { + name: 'css-injection', + setup: (build) => { + build.onLoad({ filter: /\.css$/ }, async ({ path }) => { + const cssText = await readFile(path, 'utf8'); + + return { + contents: buildStyleInjectionModule(cssText), + loader: 'js', + }; + }); + }, +}; diff --git a/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins.ts b/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins.ts index e00de62080..e93eaad622 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins.ts @@ -1,5 +1,6 @@ import type * as esbuild from 'esbuild'; +import { cssInjectionPlugin } from '../css-injection-plugin'; import { createJsxRuntimeRemoteWrapperPlugin } from '../jsx-runtime-remote-wrapper-plugin'; import { jsxTransformToRemoteDomWorkerFormatPlugin } from '../jsx-transform-to-remote-dom-worker-format-plugin'; import { createPreactAliasPlugin } from '../preact-alias-plugin'; @@ -17,5 +18,6 @@ export const getFrontComponentBuildPlugins = ( ), ...(options?.usePreact ? [createPreactAliasPlugin()] : []), jsxTransformToRemoteDomWorkerFormatPlugin, + cssInjectionPlugin, stripCommentsPlugin, ]; diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-extract-front-component-twenty-ui-css.spec.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-extract-front-component-twenty-ui-css.spec.ts new file mode 100644 index 0000000000..a9c6039fdb --- /dev/null +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/__tests__/manifest-extract-front-component-twenty-ui-css.spec.ts @@ -0,0 +1,22 @@ +import { join } from 'path'; + +import { MINIMAL_APP_PATH } from '@/cli/__tests__/apps/fixture-paths'; +import { extractManifestFromFile } from '@/cli/utilities/build/manifest/manifest-extract-config-from-file'; +import { type FrontComponentConfig } from '@/sdk/define/front-component/front-component-config'; + +describe('extractManifestFromFile - front component importing twenty-ui', () => { + it('extracts the config from a front component that side-effect imports twenty-ui/style.css', async () => { + const filePath = join(MINIMAL_APP_PATH, 'my.front-component.tsx'); + + const result = await extractManifestFromFile({ + appPath: MINIMAL_APP_PATH, + filePath, + }); + + expect(result.errors).toEqual([]); + expect(result.config.universalIdentifier).toBe( + 'e1e2e3e4-e5e6-4000-8000-000000000020', + ); + expect(result.config.name).toBe('my-component'); + }, 30000); +}); diff --git a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts index bf547162fc..04e935f0c9 100644 --- a/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts +++ b/packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts @@ -83,6 +83,7 @@ const loadModule = async ({ target: 'node18', jsx: 'automatic', tsconfig: hasTsconfig ? tsconfigPath : undefined, + loader: { '.css': 'empty' }, alias: { ...(reactPath && { react: reactPath }), ...(reactDomPath && { 'react-dom': reactDomPath }),