diff --git a/packages/twenty-apps/examples/postcard/e2e/card-front-component.spec.ts b/packages/twenty-apps/examples/postcard/e2e/card-front-component.spec.ts index 6e3142a2ce..fae5d31d85 100644 --- a/packages/twenty-apps/examples/postcard/e2e/card-front-component.spec.ts +++ b/packages/twenty-apps/examples/postcard/e2e/card-front-component.spec.ts @@ -125,5 +125,15 @@ test.describe('Postcard card front component', () => { for (const fallback of FALLBACK_TEXTS) { await expect(page.getByText(fallback, { exact: false })).toHaveCount(0); } + + const sdkPanel = page.getByTestId(CARD_TEST_IDS.sdkPanel); + await expect(sdkPanel).toBeVisible(); + + await expect(page.getByTestId(CARD_TEST_IDS.sdkCore)).toHaveText('core: ok'); + await expect(page.getByTestId(CARD_TEST_IDS.sdkMetadata)).toHaveText( + 'metadata: ok', + ); + + await expect(page.getByTestId(CARD_TEST_IDS.sdkRest)).toHaveText('rest: ok'); }); }); diff --git a/packages/twenty-apps/examples/postcard/src/components/card-test-ids.ts b/packages/twenty-apps/examples/postcard/src/components/card-test-ids.ts index acb5361b43..8c8fac0dbd 100644 --- a/packages/twenty-apps/examples/postcard/src/components/card-test-ids.ts +++ b/packages/twenty-apps/examples/postcard/src/components/card-test-ids.ts @@ -6,4 +6,8 @@ export const CARD_TEST_IDS = { name: 'postcard-card-name', status: 'postcard-card-status', content: 'postcard-card-content', + sdkPanel: 'postcard-sdk-panel', + sdkCore: 'postcard-sdk-core', + sdkMetadata: 'postcard-sdk-metadata', + sdkRest: 'postcard-sdk-rest', } as const; diff --git a/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx b/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx index b5280470a2..c09d7545aa 100644 --- a/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx +++ b/packages/twenty-apps/examples/postcard/src/components/card.front-component.tsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { CoreApiClient } from 'twenty-client-sdk/core'; +import { MetadataApiClient } from 'twenty-client-sdk/metadata'; +import { RestApiClient } from 'twenty-client-sdk/rest'; import { defineFrontComponent } from 'twenty-sdk/define'; import { useRecordId } from 'twenty-sdk/front-component'; @@ -81,6 +83,96 @@ const CardDisplay = ({ ); }; +type SdkProbeState = 'pending' | 'ok' | 'error'; + +const SDK_PROBE_LABEL: Record = { + pending: '…', + ok: 'ok', + error: 'error', +}; + +const SdkProbeRow = ({ + testId, + label, + state, +}: { + testId: string; + label: string; + state: SdkProbeState; +}) => ( + + {label}: {SDK_PROBE_LABEL[state]} + +); + +const SdkHealthPanel = () => { + const [coreState, setCoreState] = useState('pending'); + const [metadataState, setMetadataState] = useState('pending'); + const [restState, setRestState] = useState('pending'); + + useEffect(() => { + let cancelled = false; + + const probe = async ( + run: () => Promise, + setState: (state: SdkProbeState) => void, + ) => { + try { + await run(); + if (!cancelled) { + setState('ok'); + } + } catch { + if (!cancelled) { + setState('error'); + } + } + }; + + probe(() => new CoreApiClient().query({ __typename: true }), setCoreState); + probe( + () => new MetadataApiClient().query({ __typename: true }), + setMetadataState, + ); + probe(() => new RestApiClient().get('/rest/postCards'), setRestState); + + return () => { + cancelled = true; + }; + }, []); + + return ( +
+ + + +
+ ); +}; + const PostCardPreview = () => { const recordId = useRecordId(); const [postCard, setPostCard] = useState(null); @@ -184,11 +276,14 @@ const PostCardPreview = () => { } return ( - + <> + + + ); }; diff --git a/packages/twenty-client-sdk/package.json b/packages/twenty-client-sdk/package.json index b226929936..f5572e0887 100644 --- a/packages/twenty-client-sdk/package.json +++ b/packages/twenty-client-sdk/package.json @@ -4,7 +4,7 @@ "sideEffects": false, "license": "AGPL-3.0", "scripts": { - "build": "npx rimraf dist && npx vite build && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist" + "build": "npx rimraf dist && npx vite build && npx vite build -c vite.metadata.config.ts && tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist" }, "exports": { "./core": { diff --git a/packages/twenty-client-sdk/project.json b/packages/twenty-client-sdk/project.json index 93498eae20..d97d8fe966 100644 --- a/packages/twenty-client-sdk/project.json +++ b/packages/twenty-client-sdk/project.json @@ -14,7 +14,7 @@ "options": { "cwd": "{projectRoot}", "commands": [ - "npx rimraf dist && npx vite build", + "npx rimraf dist && npx vite build && npx vite build -c vite.metadata.config.ts", "tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist" ], "parallel": false diff --git a/packages/twenty-client-sdk/vite.config.ts b/packages/twenty-client-sdk/vite.config.ts index b1680c0db4..fb6aaf5c57 100644 --- a/packages/twenty-client-sdk/vite.config.ts +++ b/packages/twenty-client-sdk/vite.config.ts @@ -1,50 +1,14 @@ import path from 'path'; import { defineConfig } from 'vite'; -import packageJson from './package.json'; + +import { entryFileNames, isExternal } from './vite.shared'; const entries = [ 'src/core/index.ts', - 'src/metadata/index.ts', 'src/rest/index.ts', 'src/generate/index.ts', ]; -const externalDeps = [ - ...Object.keys(packageJson.dependencies), - ...Object.keys(packageJson.devDependencies).filter( - (dep) => dep !== 'twenty-shared', - ), - 'node:fs/promises', - 'node:fs', - 'node:path', - 'node:os', - 'node:url', -]; - -const isExternal = (id: string) => - externalDeps.some((dep) => id === dep || id.startsWith(`${dep}/`)); - -const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => { - if (!chunk.isEntry) { - throw new Error( - `Should never occur, encountered a non entry chunk ${chunk.facadeModuleId}`, - ); - } - - const splitFaceModuleId = chunk.facadeModuleId?.split('/'); - if (splitFaceModuleId === undefined) { - throw new Error( - `Should never occur, splitFaceModuleId is undefined ${chunk.facadeModuleId}`, - ); - } - - const moduleDirectory = splitFaceModuleId[splitFaceModuleId?.length - 2]; - if (moduleDirectory === 'src') { - return `${chunk.name}.${extension}`; - } - return `${moduleDirectory}.${extension}`; -}; - export default defineConfig(() => { return { root: __dirname, diff --git a/packages/twenty-client-sdk/vite.metadata.config.ts b/packages/twenty-client-sdk/vite.metadata.config.ts new file mode 100644 index 0000000000..75fda15a5a --- /dev/null +++ b/packages/twenty-client-sdk/vite.metadata.config.ts @@ -0,0 +1,43 @@ +import path from 'path'; +import { defineConfig } from 'vite'; + +import { entryFileNames, isExternal } from './vite.shared'; + +// Built as its own single-entry library so the metadata entrypoint is a single +// self-contained file, instead of sharing a chunk-*.mjs with the other entries +// in the main multi-entry build (vite.config.ts). emptyOutDir: false so it +// writes alongside that build's output rather than wiping it. +export default defineConfig(() => { + return { + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/twenty-client-sdk-metadata', + resolve: { + tsconfigPaths: true, + alias: { + '@/': path.resolve(__dirname, 'src') + '/', + }, + }, + build: { + emptyOutDir: false, + outDir: 'dist', + lib: { entry: 'src/metadata/index.ts', name: 'twenty-client-sdk' }, + rollupOptions: { + external: isExternal, + output: [ + { + format: 'es', + entryFileNames: (chunk) => entryFileNames(chunk, 'mjs'), + }, + { + format: 'cjs', + interop: 'auto', + esModule: true, + exports: 'named', + entryFileNames: (chunk) => entryFileNames(chunk, 'cjs'), + }, + ], + }, + }, + logLevel: 'warn', + }; +}); diff --git a/packages/twenty-client-sdk/vite.shared.ts b/packages/twenty-client-sdk/vite.shared.ts new file mode 100644 index 0000000000..3c6c6b24e6 --- /dev/null +++ b/packages/twenty-client-sdk/vite.shared.ts @@ -0,0 +1,37 @@ +import packageJson from './package.json'; + +const externalDeps = [ + ...Object.keys(packageJson.dependencies), + ...Object.keys(packageJson.devDependencies).filter( + (dep) => dep !== 'twenty-shared', + ), + 'node:fs/promises', + 'node:fs', + 'node:path', + 'node:os', + 'node:url', +]; + +export const isExternal = (id: string) => + externalDeps.some((dep) => id === dep || id.startsWith(`${dep}/`)); + +export const entryFileNames = (chunk: any, extension: 'cjs' | 'mjs') => { + if (!chunk.isEntry) { + throw new Error( + `Should never occur, encountered a non entry chunk ${chunk.facadeModuleId}`, + ); + } + + const splitFaceModuleId = chunk.facadeModuleId?.split('/'); + if (splitFaceModuleId === undefined) { + throw new Error( + `Should never occur, splitFaceModuleId is undefined ${chunk.facadeModuleId}`, + ); + } + + const moduleDirectory = splitFaceModuleId[splitFaceModuleId?.length - 2]; + if (moduleDirectory === 'src') { + return `${chunk.name}.${extension}`; + } + return `${moduleDirectory}.${extension}`; +};