From 70549873ca14b37772d1998361e2e0df1315a868 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 19 Nov 2024 00:01:36 +0100 Subject: [PATCH 01/39] wip: create get-client-build-details --- .../utils/get-client-build-details/index.ts | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 packages/brisa/src/utils/get-client-build-details/index.ts diff --git a/packages/brisa/src/utils/get-client-build-details/index.ts b/packages/brisa/src/utils/get-client-build-details/index.ts new file mode 100644 index 000000000..d899a2ca2 --- /dev/null +++ b/packages/brisa/src/utils/get-client-build-details/index.ts @@ -0,0 +1,121 @@ +import { sep } from 'node:path'; +import { getConstants } from '@/constants'; +import type { BuildArtifact } from 'bun'; +import AST from '../ast'; +import analyzeServerAst from '../analyze-server-ast'; +import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { + type: 'macro', +}; +import { + injectRPCCode, + injectRPCCodeForStaticApp, + injectRPCLazyCode, +} from '@/utils/rpc' with { type: 'macro' }; + +type WCs = Record; +type WCsEntrypoints = Record; + +type Options = { + webComponentsPerEntrypoint: WCsEntrypoints; + layoutWebComponents: WCs; + allWebComponents: WCs; +}; + +const ASTUtil = AST('tsx'); +const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; +const RPCLazyCode = injectRPCLazyCode() as unknown as string; + +export default async function getClientBuildDetails( + pages: BuildArtifact[], + options: Options, +) { + let clientBuildDetails = ( + await Promise.all(pages.map((p) => getClientPageDetails(p, options))) + ).filter(Boolean); + + // TODO: Create temporal pages + // TODO: Adapt plugin to analyze per entrypoint + // TODO: Create build with all the temporal pages + // TODO: Write the new outputs to the disk and cleanup the temporal pages + // TODO: Overwrite clientBuildDetails with code, size + // TODO: Test and refactor all this + // TODO: Benchmarks old vs new + + return clientBuildDetails; +} + +async function getClientPageDetails( + page: BuildArtifact, + { + allWebComponents, + webComponentsPerEntrypoint, + layoutWebComponents, + }: Options, +) { + const { BUILD_DIR } = getConstants(); + const route = page.path.replace(BUILD_DIR, ''); + const pagePath = page.path; + const isPage = route.startsWith(sep + 'pages' + sep); + const clientPagePath = pagePath.replace('pages', 'pages-client'); + + if (!isPage) return; + + const webComponents = webComponentsPerEntrypoint[pagePath]; + const pageWebComponents = layoutWebComponents + ? { ...layoutWebComponents, ...webComponents } + : webComponents; + const ast = await getAstFromPath(pagePath); + let size = 0; + let { useSuspense, useContextProvider, useActions, useHyperlink } = + // TODO: Remove layoutHasContextProvider as param and do it in a diferent way + analyzeServerAst(ast, allWebComponents); + + // Web components inside web components + const nestedComponents = await Promise.all( + Object.values(pageWebComponents).map(async (path) => + analyzeServerAst(await getAstFromPath(path), allWebComponents), + ), + ); + + for (const item of nestedComponents) { + useContextProvider ||= item.useContextProvider; + useSuspense ||= item.useSuspense; + useHyperlink ||= item.useHyperlink; + Object.assign(pageWebComponents, item.webComponents); + } + + const unsuspense = useSuspense ? unsuspenseScriptCode : ''; + const rpc = useActions || useHyperlink ? getRPCCode() : ''; + const lazyRPC = useActions || useHyperlink ? RPCLazyCode : ''; + + size += unsuspense.length; + size += rpc.length; + + const res = { + unsuspense, + rpc, + useContextProvider, + lazyRPC, + size, + useI18n: false, + i18nKeys: new Set(), + code: '', + }; + + return Object.keys(pageWebComponents).length > 0 + ? { ...res, clientPagePath } + : res; +} + +async function getAstFromPath(path: string) { + return ASTUtil.parseCodeToAST( + path[0] === '{' ? '' : await Bun.file(path).text(), + ); +} + +function getRPCCode() { + const { IS_PRODUCTION, IS_STATIC_EXPORT } = getConstants(); + return (IS_STATIC_EXPORT && IS_PRODUCTION + ? injectRPCCodeForStaticApp() + : injectRPCCode()) as unknown as string; +} From 9387f60e90f21638520439375cd8fbc7c86e4dc0 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 21 Nov 2024 23:08:23 +0100 Subject: [PATCH 02/39] feat: first version of new WC compiler --- .../brisa/src/utils/compile-files/index.ts | 38 +-- .../utils/get-client-build-details/index.ts | 264 +++++++++++++++++- 2 files changed, 263 insertions(+), 39 deletions(-) diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 95f680fd1..2b02a9ff9 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -18,6 +18,7 @@ import generateStaticExport from '@/utils/generate-static-export'; import getWebComponentsPerEntryPoints from '@/utils/get-webcomponents-per-entrypoints'; import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; import generateDynamicTypes from '@/utils/generate-dynamic-types'; +import getClientBuildDetails from '../get-client-build-details'; const TS_REGEX = /\.tsx?$/; const BRISA_DEPS = ['brisa/server']; @@ -305,33 +306,18 @@ async function compileClientCodePage( }) : null; - for (const page of pages) { - const route = page.path.replace(BUILD_DIR, ''); - const pagePath = page.path; - const isPage = route.startsWith(sep + 'pages' + sep); - const clientPagePath = pagePath.replace('pages', 'pages-client'); - let pageWebComponents = webComponentsPerEntrypoint[pagePath]; - - if (!isPage) continue; - - // It is necessary to add the web components of the layout before - // having the code of the page because it will add the web components - // in the following fields: code, size. - if (layoutWebComponents) { - pageWebComponents = { ...layoutWebComponents, ...pageWebComponents }; - } - - const pageCode = await getClientCodeInPage({ - pagePath, - allWebComponents, - pageWebComponents, - integrationsPath, - layoutHasContextProvider: layoutCode?.useContextProvider, - }); - - if (!pageCode) return null; + const pagesData = await getClientBuildDetails(pages, { + webComponentsPerEntrypoint, + layoutWebComponents, + allWebComponents, + integrationsPath, + }); - let { size, rpc, lazyRPC, code, unsuspense, useI18n, i18nKeys } = pageCode; + for (const data of pagesData) { + let { size, rpc, lazyRPC, code, unsuspense, useI18n, i18nKeys, pagePath } = + data; + const clientPagePath = pagePath.replace('pages', 'pages-client'); + const route = pagePath.replace(BUILD_DIR, ''); // If there are no actions in the page but there are actions in // the layout, then it is as if the page also has actions. diff --git a/packages/brisa/src/utils/get-client-build-details/index.ts b/packages/brisa/src/utils/get-client-build-details/index.ts index d899a2ca2..abfe1e212 100644 --- a/packages/brisa/src/utils/get-client-build-details/index.ts +++ b/packages/brisa/src/utils/get-client-build-details/index.ts @@ -1,8 +1,12 @@ import { sep } from 'node:path'; +import { join } from 'node:path'; +import { writeFile, rm } from 'node:fs/promises'; import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; import AST from '../ast'; import analyzeServerAst from '../analyze-server-ast'; +import { getFilterDevRuntimeErrors } from '@/utils/brisa-error-dialog/utils'; +import snakeToCamelCase from '@/utils/snake-to-camelcase'; import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { type: 'macro', }; @@ -11,6 +15,17 @@ import { injectRPCCodeForStaticApp, injectRPCLazyCode, } from '@/utils/rpc' with { type: 'macro' }; +import { injectClientContextProviderCode } from '@/utils/context-provider/inject-client' with { + type: 'macro', +}; +import { injectBrisaDialogErrorCode } from '@/utils/brisa-error-dialog/inject-code' with { + type: 'macro', +}; +import getDefinedEnvVar from '../get-defined-env-var'; +import { shouldTransferTranslatedPagePaths } from '../transfer-translated-page-paths'; +import clientBuildPlugin from '../client-build-plugin'; +import { logBuildError, logError } from '../log/log-build'; +import createContextPlugin from '../create-context/create-context-plugin'; type WCs = Record; type WCsEntrypoints = Record; @@ -19,6 +34,7 @@ type Options = { webComponentsPerEntrypoint: WCsEntrypoints; layoutWebComponents: WCs; allWebComponents: WCs; + integrationsPath?: string | null; }; const ASTUtil = AST('tsx'); @@ -29,47 +45,156 @@ export default async function getClientBuildDetails( pages: BuildArtifact[], options: Options, ) { + const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); let clientBuildDetails = ( - await Promise.all(pages.map((p) => getClientPageDetails(p, options))) - ).filter(Boolean); + await Promise.all(pages.map((p) => prepareEntrypoint(p, options))) + ).filter((p) => p?.entrypoint) as EntryPointData[]; + + const entrypoints = clientBuildDetails.map((p) => p.entrypoint!); + const envVar = getDefinedEnvVar(); + const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); + const webComponentsPath = Object.values(options.allWebComponents); + + const { success, logs, outputs } = await Bun.build({ + entrypoints, + root: SRC_DIR, + format: 'iife', + target: 'browser', + minify: IS_PRODUCTION, + external: CONFIG.external, + define: { + __DEV__: (!IS_PRODUCTION).toString(), + __WEB_CONTEXT_PLUGINS__: 'false', // useWebContextPlugins.toString(), (TODO) + __BASE_PATH__: JSON.stringify(CONFIG.basePath ?? ''), + __ASSET_PREFIX__: JSON.stringify(CONFIG.assetPrefix ?? ''), + __TRAILING_SLASH__: Boolean(CONFIG.trailingSlash).toString(), + __USE_LOCALE__: Boolean(I18N_CONFIG?.defaultLocale).toString(), + __USE_PAGE_TRANSLATION__: shouldTransferTranslatedPagePaths( + I18N_CONFIG?.pages, + ).toString(), + // For security: + 'import.meta.dirname': '', + ...envVar, + }, + plugins: extendPlugins( + [ + { + name: 'client-build-plugin', + setup(build) { + build.onLoad( + { + filter: new RegExp( + `(.*/src/web-components/(?!_integrations).*\\.(tsx|jsx|js|ts)|${webComponentsPath + .join('|') + // These replaces are to fix the regex in Windows + .replace(/\\/g, '\\\\')})$`.replace(/\//g, '[\\\\/]'), + ), + }, + async ({ path, loader }) => { + let code = await Bun.file(path).text(); + + try { + const res = clientBuildPlugin(code, path, { + isI18nAdded: true, // useI18n, (TODO) + isTranslateCoreAdded: true, // i18nKeys.size > 0, (TODO) + }); + code = res.code; + // useI18n ||= res.useI18n; (TODO) + // i18nKeys = new Set([...i18nKeys, ...res.i18nKeys]); (TODO) + } catch (error: any) { + logError({ + messages: [ + `Error transforming web component ${path}`, + error?.message, + ], + stack: error?.stack, + }); + } + + return { + contents: code, + loader, + }; + }, + ); + }, + }, + createContextPlugin(), + ], + { + dev: !IS_PRODUCTION, + isServer: false, + /* entrypoint: pagePath (TODO: change docs about this) */ + } as any, // TODO: Fix types + ), + }); - // TODO: Create temporal pages // TODO: Adapt plugin to analyze per entrypoint + // TODO: Com resoldré els defines que son per entrypoint? + // ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ // TODO: Create build with all the temporal pages + // TODO: Save outputs to correct paths // TODO: Write the new outputs to the disk and cleanup the temporal pages // TODO: Overwrite clientBuildDetails with code, size // TODO: Test and refactor all this // TODO: Benchmarks old vs new + // Remove all temp files + await Promise.all(entrypoints.map((e) => rm(e))); + + if (!success) { + logBuildError('Failed to compile web components', logs); + return clientBuildDetails; + } + + for (let i = 0; i < outputs.length; i++) { + clientBuildDetails[i].code = await outputs[i].text(); + clientBuildDetails[i].size = outputs[i].size; + } + return clientBuildDetails; } -async function getClientPageDetails( +type EntryPointData = { + unsuspense: string; + rpc: string; + useContextProvider: boolean; + lazyRPC: string; + size: number; + useI18n: boolean; + i18nKeys: Set; + code: string; + entrypoint?: string; + useWebContextPlugins?: boolean; + pagePath: string; +}; + +async function prepareEntrypoint( page: BuildArtifact, { allWebComponents, webComponentsPerEntrypoint, layoutWebComponents, + integrationsPath, }: Options, -) { +): Promise { const { BUILD_DIR } = getConstants(); const route = page.path.replace(BUILD_DIR, ''); const pagePath = page.path; const isPage = route.startsWith(sep + 'pages' + sep); - const clientPagePath = pagePath.replace('pages', 'pages-client'); if (!isPage) return; - const webComponents = webComponentsPerEntrypoint[pagePath]; + const webComponents = webComponentsPerEntrypoint[pagePath] ?? {}; const pageWebComponents = layoutWebComponents ? { ...layoutWebComponents, ...webComponents } : webComponents; const ast = await getAstFromPath(pagePath); let size = 0; let { useSuspense, useContextProvider, useActions, useHyperlink } = - // TODO: Remove layoutHasContextProvider as param and do it in a diferent way - analyzeServerAst(ast, allWebComponents); - + // TODO: Remove layoutHasContextProvider as param and do it in a diferent way + analyzeServerAst(ast, allWebComponents); + // Web components inside web components const nestedComponents = await Promise.all( Object.values(pageWebComponents).map(async (path) => @@ -100,11 +225,20 @@ async function getClientPageDetails( useI18n: false, i18nKeys: new Set(), code: '', + pagePath, }; - return Object.keys(pageWebComponents).length > 0 - ? { ...res, clientPagePath } - : res; + // No client build needed, TODO: We need to return the data?! + if (!Object.keys(pageWebComponents).length) return res; + + const { entrypoint, useWebContextPlugins } = await writeEntrypoint({ + webComponentsList: pageWebComponents, + useContextProvider, + integrationsPath, + pagePath, + }); + + return { ...res, entrypoint, useWebContextPlugins }; } async function getAstFromPath(path: string) { @@ -119,3 +253,107 @@ function getRPCCode() { ? injectRPCCodeForStaticApp() : injectRPCCode()) as unknown as string; } + +type TransformOptions = { + webComponentsList: Record; + useContextProvider: boolean; + integrationsPath?: string | null; + pagePath: string; +}; + +async function writeEntrypoint({ + webComponentsList, + useContextProvider, + integrationsPath, + pagePath, +}: TransformOptions) { + const { IS_DEVELOPMENT } = getConstants(); + const webEntrypoint = getTempFileName(pagePath); + let useWebContextPlugins = false; + const entries = Object.entries(webComponentsList); + + // Note: JS imports in Windows have / instead of \, so we need to replace it + // Note: Using "require" for component dependencies not move the execution + // on top avoiding missing global variables as window._P + let imports = entries + .map(([name, path]) => + path[0] === '{' + ? `require("${normalizePath(path)}");` + : `import ${snakeToCamelCase(name)} from "${path.replaceAll(sep, '/')}";`, + ) + .join('\n'); + + // Add web context plugins import only if there is a web context plugin + if (integrationsPath) { + const module = await import(integrationsPath); + if (module.webContextPlugins?.length > 0) { + useWebContextPlugins = true; + imports += `import {webContextPlugins} from "${integrationsPath}";`; + } + } + + const defineElement = + 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; + + const customElementKeys = entries + .filter(([_, path]) => path[0] !== '{') + .map(([k]) => k); + + if (useContextProvider) { + customElementKeys.unshift('context-provider'); + } + + if (IS_DEVELOPMENT) { + customElementKeys.unshift('brisa-error-dialog'); + } + + const customElementsDefinitions = customElementKeys + .map((k) => `defineElement("${k}", ${snakeToCamelCase(k)});`) + .join('\n'); + + let code = ''; + + if (useContextProvider) { + const contextProviderCode = + injectClientContextProviderCode() as unknown as string; + code += contextProviderCode; + } + + // IS_DEVELOPMENT to avoid PROD and TEST environments + if (IS_DEVELOPMENT) { + const brisaDialogErrorCode = (await injectBrisaDialogErrorCode()).replace( + '__FILTER_DEV_RUNTIME_ERRORS__', + getFilterDevRuntimeErrors(), + ); + code += brisaDialogErrorCode; + } + + // Inject web context plugins to window to be used inside web components + if (useWebContextPlugins) { + code += 'window._P=webContextPlugins;\n'; + } + + code += `${imports}\n`; + code += `${defineElement}\n${customElementsDefinitions};`; + + await writeFile(webEntrypoint, code); + + return { entrypoint: webEntrypoint, useWebContextPlugins }; +} + +function getTempFileName(pagePath: string) { + const { PAGES_DIR, BUILD_DIR } = getConstants(); + const tempName = pagePath + .replace(PAGES_DIR, '') + .replaceAll(sep, '-') + .replace(/\.[a-z]+$/, ''); + + return join(BUILD_DIR, '_brisa', `temp-${tempName}.ts`); +} + +export function normalizePath(rawPathname: string, separator = sep) { + const pathname = + rawPathname[0] === '{' ? JSON.parse(rawPathname).client : rawPathname; + + return pathname.replaceAll(separator, '/'); +} From 98c1308a3319655c0bf5962a3de5d6e66f09b699 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Fri, 22 Nov 2024 20:54:24 +0100 Subject: [PATCH 03/39] fix: fix some tests --- .../utils/get-client-build-details/index.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/brisa/src/utils/get-client-build-details/index.ts b/packages/brisa/src/utils/get-client-build-details/index.ts index abfe1e212..bf7e2575c 100644 --- a/packages/brisa/src/utils/get-client-build-details/index.ts +++ b/packages/brisa/src/utils/get-client-build-details/index.ts @@ -1,5 +1,4 @@ -import { sep } from 'node:path'; -import { join } from 'node:path'; +import { sep, join } from 'node:path'; import { writeFile, rm } from 'node:fs/promises'; import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; @@ -48,13 +47,22 @@ export default async function getClientBuildDetails( const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); let clientBuildDetails = ( await Promise.all(pages.map((p) => prepareEntrypoint(p, options))) - ).filter((p) => p?.entrypoint) as EntryPointData[]; + ).filter(Boolean) as EntryPointData[]; - const entrypoints = clientBuildDetails.map((p) => p.entrypoint!); + const entrypointsData = clientBuildDetails.reduce((acc, curr, index) => { + if (curr.entrypoint) acc.push({ ...curr, index }); + return acc; + }, [] as EntryPointData[]); + + const entrypoints = entrypointsData.map((p) => p.entrypoint!); const envVar = getDefinedEnvVar(); const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); const webComponentsPath = Object.values(options.allWebComponents); + if (entrypoints.length === 0) { + return clientBuildDetails; + } + const { success, logs, outputs } = await Bun.build({ entrypoints, root: SRC_DIR, @@ -130,9 +138,10 @@ export default async function getClientBuildDetails( }); // TODO: Adapt plugin to analyze per entrypoint - // TODO: Com resoldré els defines que son per entrypoint? + // TODO: Solve "define" for entrypoint // ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ // TODO: Create build with all the temporal pages + // TODO: How to solve the layout web components? // TODO: Save outputs to correct paths // TODO: Write the new outputs to the disk and cleanup the temporal pages // TODO: Overwrite clientBuildDetails with code, size @@ -148,8 +157,9 @@ export default async function getClientBuildDetails( } for (let i = 0; i < outputs.length; i++) { - clientBuildDetails[i].code = await outputs[i].text(); - clientBuildDetails[i].size = outputs[i].size; + const index = entrypointsData[i].index!; + clientBuildDetails[index].code = await outputs[i].text(); + clientBuildDetails[index].size = outputs[i].size; } return clientBuildDetails; @@ -167,6 +177,7 @@ type EntryPointData = { entrypoint?: string; useWebContextPlugins?: boolean; pagePath: string; + index?: number; }; async function prepareEntrypoint( From fb9f877ab59b8265e94047df0ebef71417eca39e Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 17:45:13 +0100 Subject: [PATCH 04/39] feat: improve generate entrypoint code --- .../generate-entrypoint-code/index.test.ts | 348 ++++++++++++++++++ .../generate-entrypoint-code/index.ts | 100 +++++ .../get-temp-page-name/index.test.ts | 16 + .../client-build/get-temp-page-name/index.ts | 14 + .../index.ts | 103 +----- .../client-build/normalize-path/index.test.ts | 27 ++ .../client-build/normalize-path/index.ts | 8 + .../brisa/src/utils/compile-files/index.ts | 4 +- 8 files changed, 524 insertions(+), 96 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts create mode 100644 packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts create mode 100644 packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts create mode 100644 packages/brisa/src/utils/client-build/get-temp-page-name/index.ts rename packages/brisa/src/utils/{get-client-build-details => client-build}/index.ts (73%) create mode 100644 packages/brisa/src/utils/client-build/normalize-path/index.test.ts create mode 100644 packages/brisa/src/utils/client-build/normalize-path/index.ts diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts new file mode 100644 index 000000000..c4bd159c1 --- /dev/null +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts @@ -0,0 +1,348 @@ +import { describe, expect, it, afterEach, mock } from 'bun:test'; +import { normalizeHTML } from '@/helpers'; +import { generateEntryPointCode } from '.'; +import { getConstants } from '@/constants'; + +describe('client build -> generateEntryPointCode', () => { + afterEach(() => { + globalThis.mockConstants = undefined; + }); + it('should generate the entrypoint code with the imports and customElements definition', async () => { + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: false, + }); + + expect(normalizeHTML(res.code)).toBe( + normalizeHTML(` + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + expect(res.useWebContextPlugins).toBeFalse(); + }); + + it('should generate the entrypoint code with the imports, customElements definition and context provider', async () => { + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: true, + }); + + const code = normalizeHTML(res.code); + + expect(code).toContain( + normalizeHTML(` + brisaElement(ClientContextProvider, ["context", "value", "pid", "cid"]); + `), + ); + + expect(code).toContain( + normalizeHTML(` + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + `), + ); + + expect(code).toContain( + normalizeHTML(` + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("context-provider", contextProvider); + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + expect(res.useWebContextPlugins).toBeFalse(); + }); + + it('should generate the entrypoint code with the imports, customElements definition and brisa-error-dialog (in dev)', async () => { + globalThis.mockConstants = { + ...getConstants(), + IS_DEVELOPMENT: true, + }; + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: false, + }); + + const code = normalizeHTML(res.code); + + expect(code).toContain( + normalizeHTML(` + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + `), + ); + + expect(code).toContain( + normalizeHTML(` + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("brisa-error-dialog", brisaErrorDialog); + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + + expect(code).toContain( + normalizeHTML(` + var brisaErrorDialog = brisaElement(ErrorDialog); + `), + ); + expect(res.useWebContextPlugins).toBeFalse(); + }); + + it('should return context provider without any web component', async () => { + const res = await generateEntryPointCode({ + webComponentsList: {}, + useContextProvider: true, + }); + + expect(normalizeHTML(res.code)).toContain( + normalizeHTML(` + brisaElement(ClientContextProvider, ["context", "value", "pid", "cid"]); + + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("context-provider", contextProvider); + `), + ); + expect(res.useWebContextPlugins).toBeFalse(); + }); + + it('should return empty when is without context and without any web component', async () => { + mock.module('/path/to/integrations', () => ({ + webContextPlugins: ['plugin1', 'plugin2'], + })); + const res = await generateEntryPointCode({ + webComponentsList: {}, + useContextProvider: false, + integrationsPath: '/path/to/integrations', + }); + + expect(normalizeHTML(res.code)).toBeEmpty(); + expect(res.useWebContextPlugins).toBeFalse(); + }); + + it('should generate the entrypoint code with the imports, customElements definition and webContextPlugins', async () => { + mock.module('/path/to/integrations', () => ({ + webContextPlugins: ['plugin1', 'plugin2'], + })); + + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: false, + integrationsPath: '/path/to/integrations', + }); + + const code = normalizeHTML(res.code); + + expect(code).toContain( + normalizeHTML(` + window._P=webContextPlugins; + + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + import {webContextPlugins} from "/path/to/integrations"; + `), + ); + + expect(code).toContain( + normalizeHTML(` + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + expect(res.useWebContextPlugins).toBeTrue(); + }); + + it('should generate the entrypoint code with the imports, customElements definition, context provider and webContextPlugins', async () => { + mock.module('/path/to/integrations', () => ({ + webContextPlugins: ['plugin1', 'plugin2'], + })); + + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: true, + integrationsPath: '/path/to/integrations', + }); + + const code = normalizeHTML(res.code); + + expect(code).toContain( + normalizeHTML(` + window._P=webContextPlugins; + + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + import {webContextPlugins} from "/path/to/integrations"; + `), + ); + + expect(code).toContain( + normalizeHTML(` + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("context-provider", contextProvider); + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + expect(res.useWebContextPlugins).toBeTrue(); + }); + + it('should generate the entrypoint code with the imports, customElements definition, brisa-error-dialog (in dev) and webContextPlugins', async () => { + globalThis.mockConstants = { + ...getConstants(), + IS_DEVELOPMENT: true, + }; + mock.module('/path/to/integrations', () => ({ + webContextPlugins: ['plugin1', 'plugin2'], + })); + + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: false, + integrationsPath: '/path/to/integrations', + }); + + const code = normalizeHTML(res.code); + + expect(code).toContain( + normalizeHTML(` + window._P=webContextPlugins; + + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + import {webContextPlugins} from "/path/to/integrations"; + `), + ); + + expect(code).toContain( + normalizeHTML(` + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("brisa-error-dialog", brisaErrorDialog); + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + + expect(code).toContain( + normalizeHTML(` + var brisaErrorDialog = brisaElement(ErrorDialog); + `), + ); + expect(res.useWebContextPlugins).toBeTrue(); + }); + + it('should generate the entrypoint code with the imports, customElements definition, context provider, brisa-error-dialog (in dev) and webContextPlugins', async () => { + globalThis.mockConstants = { + ...getConstants(), + IS_DEVELOPMENT: true, + }; + mock.module('/path/to/integrations', () => ({ + webContextPlugins: ['plugin1', 'plugin2'], + })); + + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: true, + integrationsPath: '/path/to/integrations', + }); + + const code = normalizeHTML(res.code); + + expect(code).toContain( + normalizeHTML(` + window._P=webContextPlugins; + + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + import {webContextPlugins} from "/path/to/integrations"; + `), + ); + + expect(code).toContain( + normalizeHTML(` + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("brisa-error-dialog", brisaErrorDialog); + defineElement("context-provider", contextProvider); + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + + expect(code).toContain( + normalizeHTML(` + var brisaErrorDialog = brisaElement(ErrorDialog); + `), + ); + + expect(code).toContain( + normalizeHTML(` + brisaElement(ClientContextProvider, ["context", "value", "pid", "cid"]); + `), + ); + expect(res.useWebContextPlugins).toBeTrue(); + }); + + it('should generate the entrypoint code with the imports, customElements definition and webContextPlugins with no plugins', async () => { + mock.module('/path/to/integrations', () => ({ + webContextPlugins: [], + })); + + const res = await generateEntryPointCode({ + webComponentsList: { + 'my-component': '/path/to/my-component', + 'my-component2': '/path/to/my-component2', + }, + useContextProvider: false, + integrationsPath: '/path/to/integrations', + }); + + const code = normalizeHTML(res.code); + + expect(code).toBe( + normalizeHTML(` + import myComponent from "/path/to/my-component"; + import myComponent2 from "/path/to/my-component2"; + + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), + ); + expect(res.useWebContextPlugins).toBeFalse(); + }); +}); diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts new file mode 100644 index 000000000..69be6f10c --- /dev/null +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts @@ -0,0 +1,100 @@ +import { sep } from 'node:path'; +import { getFilterDevRuntimeErrors } from '@/utils/brisa-error-dialog/utils'; +import { injectClientContextProviderCode } from '@/utils/context-provider/inject-client' with { + type: 'macro', +}; +import { injectBrisaDialogErrorCode } from '@/utils/brisa-error-dialog/inject-code' with { + type: 'macro', +}; +import { getConstants } from '@/constants'; +import { normalizePath } from '../normalize-path'; +import snakeToCamelCase from '@/utils/snake-to-camelcase'; + +type EntrpypointOptions = { + webComponentsList: Record; + useContextProvider: boolean; + integrationsPath?: string | null; +}; + +export async function generateEntryPointCode({ + webComponentsList, + useContextProvider, + integrationsPath, +}: EntrpypointOptions) { + const { IS_DEVELOPMENT } = getConstants(); + let useWebContextPlugins = false; + const entries = Object.entries(webComponentsList); + + if (!useContextProvider && !entries.length) { + return { + code: '', + useWebContextPlugins: false, + }; + } + + // Note: JS imports in Windows have / instead of \, so we need to replace it + // Note: Using "require" for component dependencies not move the execution + // on top avoiding missing global variables as window._P (P = plugins) + let imports = entries + .map(([name, path]) => + path[0] === '{' + ? `require("${normalizePath(path)}");` + : `import ${snakeToCamelCase(name)} from "${path.replaceAll(sep, '/')}";`, + ) + .join('\n'); + + // Add web context plugins import only if there is a web context plugin + if (integrationsPath) { + const module = await import(integrationsPath); + if (module.webContextPlugins?.length > 0) { + useWebContextPlugins = true; + imports += `import {webContextPlugins} from "${integrationsPath}";`; + } + } + + const defineElement = + 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; + + const customElementKeys = entries + .filter(([_, path]) => path[0] !== '{') + .map(([k]) => k); + + if (useContextProvider) { + customElementKeys.unshift('context-provider'); + } + + if (IS_DEVELOPMENT) { + customElementKeys.unshift('brisa-error-dialog'); + } + + const customElementsDefinitions = customElementKeys + .map((k) => `defineElement("${k}", ${snakeToCamelCase(k)});`) + .join('\n'); + + let code = ''; + + if (useContextProvider) { + const contextProviderCode = + injectClientContextProviderCode() as unknown as string; + code += contextProviderCode; + } + + // IS_DEVELOPMENT to avoid PROD and TEST environments + if (IS_DEVELOPMENT) { + const brisaDialogErrorCode = (await injectBrisaDialogErrorCode()).replace( + '__FILTER_DEV_RUNTIME_ERRORS__', + getFilterDevRuntimeErrors(), + ); + code += brisaDialogErrorCode; + } + + // Inject web context plugins to window to be used inside web components + if (useWebContextPlugins) { + code += 'window._P=webContextPlugins;\n'; + } + + code += `${imports}\n`; + code += `${defineElement}\n${customElementsDefinitions}`; + + return { code, useWebContextPlugins }; +} diff --git a/packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts b/packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts new file mode 100644 index 000000000..a93782a1d --- /dev/null +++ b/packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'bun:test'; +import { getConstants } from '@/constants'; +import { join } from 'node:path'; +import { getTempPageName } from '.'; + +describe('build utils -> client build', () => { + describe('getTempPageName', () => { + it('should return the correct temp file name', () => { + const { BUILD_DIR } = getConstants(); + const pagePath = '/path/to/page.tsx'; + const expected = join(BUILD_DIR, '_brisa', 'temp-path-to-page.ts'); + const result = getTempPageName(pagePath); + expect(result).toBe(expected); + }); + }); +}); diff --git a/packages/brisa/src/utils/client-build/get-temp-page-name/index.ts b/packages/brisa/src/utils/client-build/get-temp-page-name/index.ts new file mode 100644 index 000000000..241b46b01 --- /dev/null +++ b/packages/brisa/src/utils/client-build/get-temp-page-name/index.ts @@ -0,0 +1,14 @@ +import { getConstants } from '@/constants'; +import { join, sep } from 'node:path'; + +const EXTENSION_REGEX = /\.[a-z]+$/; + +export function getTempPageName(pagePath: string) { + const { PAGES_DIR, BUILD_DIR } = getConstants(); + const tempName = pagePath + .replace(PAGES_DIR, '') + .replaceAll(sep, '-') + .replace(EXTENSION_REGEX, ''); + + return join(BUILD_DIR, '_brisa', `temp${tempName}.ts`); +} diff --git a/packages/brisa/src/utils/get-client-build-details/index.ts b/packages/brisa/src/utils/client-build/index.ts similarity index 73% rename from packages/brisa/src/utils/get-client-build-details/index.ts rename to packages/brisa/src/utils/client-build/index.ts index bf7e2575c..1383be2df 100644 --- a/packages/brisa/src/utils/get-client-build-details/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -1,11 +1,9 @@ -import { sep, join } from 'node:path'; +import { sep } from 'node:path'; import { writeFile, rm } from 'node:fs/promises'; import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; import AST from '../ast'; import analyzeServerAst from '../analyze-server-ast'; -import { getFilterDevRuntimeErrors } from '@/utils/brisa-error-dialog/utils'; -import snakeToCamelCase from '@/utils/snake-to-camelcase'; import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { type: 'macro', }; @@ -14,17 +12,13 @@ import { injectRPCCodeForStaticApp, injectRPCLazyCode, } from '@/utils/rpc' with { type: 'macro' }; -import { injectClientContextProviderCode } from '@/utils/context-provider/inject-client' with { - type: 'macro', -}; -import { injectBrisaDialogErrorCode } from '@/utils/brisa-error-dialog/inject-code' with { - type: 'macro', -}; import getDefinedEnvVar from '../get-defined-env-var'; import { shouldTransferTranslatedPagePaths } from '../transfer-translated-page-paths'; import clientBuildPlugin from '../client-build-plugin'; import { logBuildError, logError } from '../log/log-build'; import createContextPlugin from '../create-context/create-context-plugin'; +import { getTempPageName } from './get-temp-page-name'; +import { generateEntryPointCode } from './generate-entrypoint-code'; type WCs = Record; type WCsEntrypoints = Record; @@ -278,93 +272,14 @@ async function writeEntrypoint({ integrationsPath, pagePath, }: TransformOptions) { - const { IS_DEVELOPMENT } = getConstants(); - const webEntrypoint = getTempFileName(pagePath); - let useWebContextPlugins = false; - const entries = Object.entries(webComponentsList); - - // Note: JS imports in Windows have / instead of \, so we need to replace it - // Note: Using "require" for component dependencies not move the execution - // on top avoiding missing global variables as window._P - let imports = entries - .map(([name, path]) => - path[0] === '{' - ? `require("${normalizePath(path)}");` - : `import ${snakeToCamelCase(name)} from "${path.replaceAll(sep, '/')}";`, - ) - .join('\n'); - - // Add web context plugins import only if there is a web context plugin - if (integrationsPath) { - const module = await import(integrationsPath); - if (module.webContextPlugins?.length > 0) { - useWebContextPlugins = true; - imports += `import {webContextPlugins} from "${integrationsPath}";`; - } - } - - const defineElement = - 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; - - const customElementKeys = entries - .filter(([_, path]) => path[0] !== '{') - .map(([k]) => k); - - if (useContextProvider) { - customElementKeys.unshift('context-provider'); - } - - if (IS_DEVELOPMENT) { - customElementKeys.unshift('brisa-error-dialog'); - } - - const customElementsDefinitions = customElementKeys - .map((k) => `defineElement("${k}", ${snakeToCamelCase(k)});`) - .join('\n'); - - let code = ''; - - if (useContextProvider) { - const contextProviderCode = - injectClientContextProviderCode() as unknown as string; - code += contextProviderCode; - } - - // IS_DEVELOPMENT to avoid PROD and TEST environments - if (IS_DEVELOPMENT) { - const brisaDialogErrorCode = (await injectBrisaDialogErrorCode()).replace( - '__FILTER_DEV_RUNTIME_ERRORS__', - getFilterDevRuntimeErrors(), - ); - code += brisaDialogErrorCode; - } - - // Inject web context plugins to window to be used inside web components - if (useWebContextPlugins) { - code += 'window._P=webContextPlugins;\n'; - } - - code += `${imports}\n`; - code += `${defineElement}\n${customElementsDefinitions};`; + const webEntrypoint = getTempPageName(pagePath); + const { code, useWebContextPlugins } = await generateEntryPointCode({ + webComponentsList, + useContextProvider, + integrationsPath, + }); await writeFile(webEntrypoint, code); return { entrypoint: webEntrypoint, useWebContextPlugins }; } - -function getTempFileName(pagePath: string) { - const { PAGES_DIR, BUILD_DIR } = getConstants(); - const tempName = pagePath - .replace(PAGES_DIR, '') - .replaceAll(sep, '-') - .replace(/\.[a-z]+$/, ''); - - return join(BUILD_DIR, '_brisa', `temp-${tempName}.ts`); -} - -export function normalizePath(rawPathname: string, separator = sep) { - const pathname = - rawPathname[0] === '{' ? JSON.parse(rawPathname).client : rawPathname; - - return pathname.replaceAll(separator, '/'); -} diff --git a/packages/brisa/src/utils/client-build/normalize-path/index.test.ts b/packages/brisa/src/utils/client-build/normalize-path/index.test.ts new file mode 100644 index 000000000..b84680be8 --- /dev/null +++ b/packages/brisa/src/utils/client-build/normalize-path/index.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'bun:test'; +import { normalizePath } from '.'; + +describe('build utils -> client build', () => { + describe('normalizePath', () => { + it('should return the correct normalized path', () => { + const rawPathname = '/path/to/page.tsx'; + const expected = '/path/to/page.tsx'; + const result = normalizePath(rawPathname); + expect(result).toBe(expected); + }); + + it('should return the correct normalized path with custom separator', () => { + const rawPathname = '/path/to/page.tsx'; + const expected = '/path/to/page.tsx'; + const result = normalizePath(rawPathname, '/'); + expect(result).toBe(expected); + }); + + it('should return the correct normalized path with custom separator', () => { + const rawPathname = '\\path\\to\\page.tsx'; + const expected = '/path/to/page.tsx'; + const result = normalizePath(rawPathname, '\\'); + expect(result).toBe(expected); + }); + }); +}); diff --git a/packages/brisa/src/utils/client-build/normalize-path/index.ts b/packages/brisa/src/utils/client-build/normalize-path/index.ts new file mode 100644 index 000000000..38535d024 --- /dev/null +++ b/packages/brisa/src/utils/client-build/normalize-path/index.ts @@ -0,0 +1,8 @@ +import { sep } from 'node:path'; + +export function normalizePath(rawPathname: string, separator = sep) { + const pathname = + rawPathname[0] === '{' ? JSON.parse(rawPathname).client : rawPathname; + + return pathname.replaceAll(separator, '/'); +} diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 2b02a9ff9..15176c7ca 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -1,7 +1,7 @@ import { gzipSync, type BuildArtifact } from 'bun'; import { brotliCompressSync } from 'node:zlib'; import fs from 'node:fs'; -import { join, sep } from 'node:path'; +import { join } from 'node:path'; import { getConstants } from '@/constants'; import byteSizeToString from '@/utils/byte-size-to-string'; @@ -18,7 +18,7 @@ import generateStaticExport from '@/utils/generate-static-export'; import getWebComponentsPerEntryPoints from '@/utils/get-webcomponents-per-entrypoints'; import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; import generateDynamicTypes from '@/utils/generate-dynamic-types'; -import getClientBuildDetails from '../get-client-build-details'; +import getClientBuildDetails from '@/utils/client-build'; const TS_REGEX = /\.tsx?$/; const BRISA_DEPS = ['brisa/server']; From 0f6648f1c1fca5edefc35370b04cc645f6a32854 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 17:50:14 +0100 Subject: [PATCH 05/39] refactor: refactor generateEntryPointCode module --- .../generate-entrypoint-code/index.ts | 119 ++++++++++-------- 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts index 69be6f10c..60c27c543 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts @@ -10,7 +10,7 @@ import { getConstants } from '@/constants'; import { normalizePath } from '../normalize-path'; import snakeToCamelCase from '@/utils/snake-to-camelcase'; -type EntrpypointOptions = { +type EntrypointOptions = { webComponentsList: Record; useContextProvider: boolean; integrationsPath?: string | null; @@ -20,81 +20,92 @@ export async function generateEntryPointCode({ webComponentsList, useContextProvider, integrationsPath, -}: EntrpypointOptions) { +}: EntrypointOptions) { const { IS_DEVELOPMENT } = getConstants(); - let useWebContextPlugins = false; const entries = Object.entries(webComponentsList); - if (!useContextProvider && !entries.length) { - return { - code: '', - useWebContextPlugins: false, - }; + if (!useContextProvider && entries.length === 0) { + return { code: '', useWebContextPlugins: false }; } - // Note: JS imports in Windows have / instead of \, so we need to replace it - // Note: Using "require" for component dependencies not move the execution - // on top avoiding missing global variables as window._P (P = plugins) - let imports = entries - .map(([name, path]) => - path[0] === '{' - ? `require("${normalizePath(path)}");` - : `import ${snakeToCamelCase(name)} from "${path.replaceAll(sep, '/')}";`, - ) - .join('\n'); - - // Add web context plugins import only if there is a web context plugin - if (integrationsPath) { - const module = await import(integrationsPath); - if (module.webContextPlugins?.length > 0) { - useWebContextPlugins = true; - imports += `import {webContextPlugins} from "${integrationsPath}";`; - } - } - - const defineElement = - 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; + const { imports, useWebContextPlugins } = await getImports( + entries, + integrationsPath, + ); const customElementKeys = entries .filter(([_, path]) => path[0] !== '{') - .map(([k]) => k); + .map(([key]) => key); - if (useContextProvider) { - customElementKeys.unshift('context-provider'); - } - - if (IS_DEVELOPMENT) { - customElementKeys.unshift('brisa-error-dialog'); - } - - const customElementsDefinitions = customElementKeys - .map((k) => `defineElement("${k}", ${snakeToCamelCase(k)});`) - .join('\n'); + addOptionalComponents(customElementKeys, useContextProvider, IS_DEVELOPMENT); let code = ''; if (useContextProvider) { - const contextProviderCode = - injectClientContextProviderCode() as unknown as string; - code += contextProviderCode; + code += injectClientContextProviderCode(); } - // IS_DEVELOPMENT to avoid PROD and TEST environments if (IS_DEVELOPMENT) { - const brisaDialogErrorCode = (await injectBrisaDialogErrorCode()).replace( - '__FILTER_DEV_RUNTIME_ERRORS__', - getFilterDevRuntimeErrors(), - ); - code += brisaDialogErrorCode; + code += await getDevelopmentCode(); } - // Inject web context plugins to window to be used inside web components if (useWebContextPlugins) { code += 'window._P=webContextPlugins;\n'; } - code += `${imports}\n`; - code += `${defineElement}\n${customElementsDefinitions}`; + code += `${imports}\n${getDefineElementCode(customElementKeys)}`; return { code, useWebContextPlugins }; } + +async function getImports( + entries: [string, string][], + integrationsPath?: string | null, +) { + const imports = entries.map(([name, path]) => + path[0] === '{' + ? `require("${normalizePath(path)}");` + : `import ${snakeToCamelCase(name)} from "${path.replaceAll(sep, '/')}";`, + ); + + if (integrationsPath) { + const module = await import(integrationsPath); + if (module.webContextPlugins?.length > 0) { + imports.push(`import {webContextPlugins} from "${integrationsPath}";`); + return { imports: imports.join('\n'), useWebContextPlugins: true }; + } + } + + return { imports: imports.join('\n'), useWebContextPlugins: false }; +} + +function getDefineElementCode(keys: string[]): string { + const defineElementCode = + 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; + const definitions = keys + .map((key) => `defineElement("${key}", ${snakeToCamelCase(key)});`) + .join('\n'); + + return `${defineElementCode}\n${definitions}`; +} + +async function getDevelopmentCode(): Promise { + const brisaDialogErrorCode = (await injectBrisaDialogErrorCode()).replace( + '__FILTER_DEV_RUNTIME_ERRORS__', + getFilterDevRuntimeErrors(), + ); + return brisaDialogErrorCode; +} + +function addOptionalComponents( + keys: string[], + useContextProvider: boolean, + isDevelopment: boolean, +) { + if (useContextProvider) { + keys.unshift('context-provider'); + } + if (isDevelopment) { + keys.unshift('brisa-error-dialog'); + } +} From 69a6eb655eb314666cd00d4fdc6c4beb5c02699e Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 18:15:47 +0100 Subject: [PATCH 06/39] refactor: improve generate-entrypoint-code --- .../generate-entrypoint-code/index.test.ts | 66 ++++++++--------- .../generate-entrypoint-code/index.ts | 70 ++++++++++--------- 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts index c4bd159c1..bdaae34e1 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts @@ -154,24 +154,21 @@ describe('client build -> generateEntryPointCode', () => { const code = normalizeHTML(res.code); - expect(code).toContain( + expect(code).toBe( normalizeHTML(` - window._P=webContextPlugins; - import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; - `), - ); + + window._P=webContextPlugins; - expect(code).toContain( - normalizeHTML(` - const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); - defineElement("my-component", myComponent); - defineElement("my-component2", myComponent2); - `), + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), ); + expect(res.useWebContextPlugins).toBeTrue(); }); @@ -191,25 +188,28 @@ describe('client build -> generateEntryPointCode', () => { const code = normalizeHTML(res.code); - expect(code).toContain( + expect(code).toStartWith( normalizeHTML(` - window._P=webContextPlugins; - import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; `), ); - expect(code).toContain( + expect(code).toContain('function ClientContextProvider'); + + expect(code).toEndWith( normalizeHTML(` - const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + window._P=webContextPlugins; + + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); - defineElement("context-provider", contextProvider); - defineElement("my-component", myComponent); - defineElement("my-component2", myComponent2); - `), + defineElement("context-provider", contextProvider); + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), ); + expect(res.useWebContextPlugins).toBeTrue(); }); @@ -233,10 +233,8 @@ describe('client build -> generateEntryPointCode', () => { const code = normalizeHTML(res.code); - expect(code).toContain( + expect(code).toStartWith( normalizeHTML(` - window._P=webContextPlugins; - import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; @@ -245,6 +243,8 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toContain( normalizeHTML(` + window._P=webContextPlugins; + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); defineElement("brisa-error-dialog", brisaErrorDialog); @@ -281,10 +281,8 @@ describe('client build -> generateEntryPointCode', () => { const code = normalizeHTML(res.code); - expect(code).toContain( + expect(code).toStartWith( normalizeHTML(` - window._P=webContextPlugins; - import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; @@ -293,13 +291,15 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toContain( normalizeHTML(` - const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); - - defineElement("brisa-error-dialog", brisaErrorDialog); - defineElement("context-provider", contextProvider); - defineElement("my-component", myComponent); - defineElement("my-component2", myComponent2); - `), + window._P=webContextPlugins; + + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("brisa-error-dialog", brisaErrorDialog); + defineElement("context-provider", contextProvider); + defineElement("my-component", myComponent); + defineElement("my-component2", myComponent2); + `), ); expect(code).toContain( diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts index 60c27c543..ed23c28bc 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts @@ -1,14 +1,14 @@ import { sep } from 'node:path'; import { getFilterDevRuntimeErrors } from '@/utils/brisa-error-dialog/utils'; +import { getConstants } from '@/constants'; +import { normalizePath } from '../normalize-path'; +import snakeToCamelCase from '@/utils/snake-to-camelcase'; import { injectClientContextProviderCode } from '@/utils/context-provider/inject-client' with { type: 'macro', }; import { injectBrisaDialogErrorCode } from '@/utils/brisa-error-dialog/inject-code' with { type: 'macro', }; -import { getConstants } from '@/constants'; -import { normalizePath } from '../normalize-path'; -import snakeToCamelCase from '@/utils/snake-to-camelcase'; type EntrypointOptions = { webComponentsList: Record; @@ -33,27 +33,26 @@ export async function generateEntryPointCode({ integrationsPath, ); - const customElementKeys = entries - .filter(([_, path]) => path[0] !== '{') - .map(([key]) => key); + const pluginsGlobal = useWebContextPlugins + ? 'window._P=webContextPlugins;\n' + : ''; - addOptionalComponents(customElementKeys, useContextProvider, IS_DEVELOPMENT); + const wcSelectors = getWebComponentSelectors(entries, { + useContextProvider, + isDevelopment: IS_DEVELOPMENT, + }); - let code = ''; + let code = `${imports}\n`; if (useContextProvider) { code += injectClientContextProviderCode(); } if (IS_DEVELOPMENT) { - code += await getDevelopmentCode(); - } - - if (useWebContextPlugins) { - code += 'window._P=webContextPlugins;\n'; + code += await injectDevelopmentCode(); } - code += `${imports}\n${getDefineElementCode(customElementKeys)}`; + code += `${pluginsGlobal}\n${defineElements(wcSelectors)}`; return { code, useWebContextPlugins }; } @@ -79,33 +78,40 @@ async function getImports( return { imports: imports.join('\n'), useWebContextPlugins: false }; } -function getDefineElementCode(keys: string[]): string { +function getWebComponentSelectors( + entries: [string, string][], + { + useContextProvider, + isDevelopment, + }: { useContextProvider: boolean; isDevelopment: boolean }, +) { + const customElementKeys = entries + .filter(([_, path]) => path[0] !== '{') + .map(([key]) => key); + + if (useContextProvider) { + customElementKeys.unshift('context-provider'); + } + if (isDevelopment) { + customElementKeys.unshift('brisa-error-dialog'); + } + + return customElementKeys; +} + +function defineElements(selectors: string[]): string { const defineElementCode = 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; - const definitions = keys + const definitions = selectors .map((key) => `defineElement("${key}", ${snakeToCamelCase(key)});`) .join('\n'); return `${defineElementCode}\n${definitions}`; } -async function getDevelopmentCode(): Promise { - const brisaDialogErrorCode = (await injectBrisaDialogErrorCode()).replace( +async function injectDevelopmentCode(): Promise { + return (await injectBrisaDialogErrorCode()).replace( '__FILTER_DEV_RUNTIME_ERRORS__', getFilterDevRuntimeErrors(), ); - return brisaDialogErrorCode; -} - -function addOptionalComponents( - keys: string[], - useContextProvider: boolean, - isDevelopment: boolean, -) { - if (useContextProvider) { - keys.unshift('context-provider'); - } - if (isDevelopment) { - keys.unshift('brisa-error-dialog'); - } } From 7d90a1dcb6fee2e597d88328c129ce6b4da7b91e Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 18:19:48 +0100 Subject: [PATCH 07/39] reformat: add comments --- .../generate-entrypoint-code/index.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts index ed23c28bc..83b2c232f 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts @@ -16,6 +16,11 @@ type EntrypointOptions = { integrationsPath?: string | null; }; +/** + * Generates the complete entry point code for client-side rendering. + * This includes imports, Web Components registration, development-only + * debugging tools, and optional context provider support. + */ export async function generateEntryPointCode({ webComponentsList, useContextProvider, @@ -57,6 +62,10 @@ export async function generateEntryPointCode({ return { code, useWebContextPlugins }; } +/** + * Generates import statements for Web Components and optional integrations. + * Also determines if web context plugins are present in the integration module. + */ async function getImports( entries: [string, string][], integrationsPath?: string | null, @@ -69,6 +78,7 @@ async function getImports( if (integrationsPath) { const module = await import(integrationsPath); + if (module.webContextPlugins?.length > 0) { imports.push(`import {webContextPlugins} from "${integrationsPath}";`); return { imports: imports.join('\n'), useWebContextPlugins: true }; @@ -78,6 +88,10 @@ async function getImports( return { imports: imports.join('\n'), useWebContextPlugins: false }; } +/** + * Generates the list of Web Component selectors to define. + * Includes internal components like context provider and error dialog. + */ function getWebComponentSelectors( entries: [string, string][], { @@ -99,6 +113,11 @@ function getWebComponentSelectors( return customElementKeys; } +/** + * Generates the JavaScript code to define all Web Components. + * Uses the `defineElement` function to register each Web Component + * only if it is not already defined in the custom elements registry. + */ function defineElements(selectors: string[]): string { const defineElementCode = 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; @@ -109,6 +128,10 @@ function defineElements(selectors: string[]): string { return `${defineElementCode}\n${definitions}`; } +/** + * Injects development-only debugging tools like the brisa-error-dialog. + * This code is only included when running in development mode. + */ async function injectDevelopmentCode(): Promise { return (await injectBrisaDialogErrorCode()).replace( '__FILTER_DEV_RUNTIME_ERRORS__', From 789edf5af34d2c4bc913b76530930faac5c63b17 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 18:33:14 +0100 Subject: [PATCH 08/39] feat: improve getTempPageName --- .../get-temp-page-name/index.test.ts | 31 +++++++++++++++++++ .../client-build/get-temp-page-name/index.ts | 30 +++++++++++++++--- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts b/packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts index a93782a1d..1445f9e56 100644 --- a/packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts +++ b/packages/brisa/src/utils/client-build/get-temp-page-name/index.test.ts @@ -12,5 +12,36 @@ describe('build utils -> client build', () => { const result = getTempPageName(pagePath); expect(result).toBe(expected); }); + + it('should not conflict between similar page paths', () => { + const { BUILD_DIR } = getConstants(); + const pagePath1 = '/path/to/page-example.tsx'; + const pagePath2 = '/path/to/page/example.tsx'; + + const expected1 = join( + BUILD_DIR, + '_brisa', + 'temp-path-to-page_example.ts', + ); + const expected2 = join( + BUILD_DIR, + '_brisa', + 'temp-path-to-page-example.ts', + ); + + const result1 = getTempPageName(pagePath1); + const result2 = getTempPageName(pagePath2); + + expect(result1).toBe(expected1); + expect(result2).toBe(expected2); + }); + + it('should handle page paths with multiple extensions correctly', () => { + const { BUILD_DIR } = getConstants(); + const pagePath = '/path/to/page.min.tsx'; + const expected = join(BUILD_DIR, '_brisa', 'temp-path-to-page.min.ts'); + const result = getTempPageName(pagePath); + expect(result).toBe(expected); + }); }); }); diff --git a/packages/brisa/src/utils/client-build/get-temp-page-name/index.ts b/packages/brisa/src/utils/client-build/get-temp-page-name/index.ts index 241b46b01..d8aa17883 100644 --- a/packages/brisa/src/utils/client-build/get-temp-page-name/index.ts +++ b/packages/brisa/src/utils/client-build/get-temp-page-name/index.ts @@ -1,14 +1,34 @@ import { getConstants } from '@/constants'; import { join, sep } from 'node:path'; -const EXTENSION_REGEX = /\.[a-z]+$/; +const REGEX = new RegExp(`${sep}|-|\\.[a-z]+$`, 'g'); +/** + * Generates a temporary TypeScript file path for a given page. + * This is used during the build process to create intermediate files. + * + * This function is part of the client build process. During the server build, + * all Web Components used on a page are analyzed and associated with their + * respective entrypoints. + * + * For each entrypoint, the client build requires a temporary file that contains: + * + * 1. Import statements for all the Web Components needed by the client page. + * 2. Definitions for those Web Components, ensuring they are registered correctly. + * + * This function creates a unique temporary file path for each entrypoint, ensuring + * that the client build can correctly generate the necessary imports and definitions. + */ export function getTempPageName(pagePath: string) { const { PAGES_DIR, BUILD_DIR } = getConstants(); - const tempName = pagePath - .replace(PAGES_DIR, '') - .replaceAll(sep, '-') - .replace(EXTENSION_REGEX, ''); + + const tempName = pagePath.replace(PAGES_DIR, '').replace(REGEX, resolveRegex); return join(BUILD_DIR, '_brisa', `temp${tempName}.ts`); } + +function resolveRegex(match: string) { + if (match === sep) return '-'; + if (match === '-') return '_'; + return ''; +} From 561c549c9e963d2a20e7eb912c0464eb7b6b89e4 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 19:11:36 +0100 Subject: [PATCH 09/39] refactor: move logic to preEntrypointAnalysis --- .../brisa/src/utils/client-build/index.ts | 49 +++++-------- .../pre-entrypoint-analysis/index.ts | 68 +++++++++++++++++++ .../utils/get-client-code-in-page/index.ts | 34 ++++------ 3 files changed, 100 insertions(+), 51 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 1383be2df..2c42a14e1 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -2,8 +2,6 @@ import { sep } from 'node:path'; import { writeFile, rm } from 'node:fs/promises'; import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; -import AST from '../ast'; -import analyzeServerAst from '../analyze-server-ast'; import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { type: 'macro', }; @@ -19,6 +17,7 @@ import { logBuildError, logError } from '../log/log-build'; import createContextPlugin from '../create-context/create-context-plugin'; import { getTempPageName } from './get-temp-page-name'; import { generateEntryPointCode } from './generate-entrypoint-code'; +import { preEntrypointAnalysis } from './pre-entrypoint-analysis'; type WCs = Record; type WCsEntrypoints = Record; @@ -30,7 +29,6 @@ type Options = { integrationsPath?: string | null; }; -const ASTUtil = AST('tsx'); const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; const RPCLazyCode = injectRPCLazyCode() as unknown as string; @@ -190,30 +188,25 @@ async function prepareEntrypoint( if (!isPage) return; - const webComponents = webComponentsPerEntrypoint[pagePath] ?? {}; - const pageWebComponents = layoutWebComponents - ? { ...layoutWebComponents, ...webComponents } - : webComponents; - const ast = await getAstFromPath(pagePath); let size = 0; - let { useSuspense, useContextProvider, useActions, useHyperlink } = - // TODO: Remove layoutHasContextProvider as param and do it in a diferent way - analyzeServerAst(ast, allWebComponents); + const wcs = webComponentsPerEntrypoint[pagePath] ?? {}; + const pageWebComponents = layoutWebComponents + ? { ...layoutWebComponents, ...wcs } + : wcs; - // Web components inside web components - const nestedComponents = await Promise.all( - Object.values(pageWebComponents).map(async (path) => - analyzeServerAst(await getAstFromPath(path), allWebComponents), - ), + const { + useSuspense, + useContextProvider, + useActions, + useHyperlink, + webComponents, + } = await preEntrypointAnalysis( + pagePath, + allWebComponents, + pageWebComponents, + false, // TODO: Remove layoutHasContextProvider as param and do it in a diferent way ); - for (const item of nestedComponents) { - useContextProvider ||= item.useContextProvider; - useSuspense ||= item.useSuspense; - useHyperlink ||= item.useHyperlink; - Object.assign(pageWebComponents, item.webComponents); - } - const unsuspense = useSuspense ? unsuspenseScriptCode : ''; const rpc = useActions || useHyperlink ? getRPCCode() : ''; const lazyRPC = useActions || useHyperlink ? RPCLazyCode : ''; @@ -234,10 +227,10 @@ async function prepareEntrypoint( }; // No client build needed, TODO: We need to return the data?! - if (!Object.keys(pageWebComponents).length) return res; + if (!Object.keys(webComponents).length) return res; const { entrypoint, useWebContextPlugins } = await writeEntrypoint({ - webComponentsList: pageWebComponents, + webComponentsList: webComponents, useContextProvider, integrationsPath, pagePath, @@ -246,12 +239,6 @@ async function prepareEntrypoint( return { ...res, entrypoint, useWebContextPlugins }; } -async function getAstFromPath(path: string) { - return ASTUtil.parseCodeToAST( - path[0] === '{' ? '' : await Bun.file(path).text(), - ); -} - function getRPCCode() { const { IS_PRODUCTION, IS_STATIC_EXPORT } = getConstants(); return (IS_STATIC_EXPORT && IS_PRODUCTION diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts new file mode 100644 index 000000000..370fb7361 --- /dev/null +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts @@ -0,0 +1,68 @@ +import analyzeServerAst from '@/utils/analyze-server-ast'; +import AST from '@/utils/ast'; + +type WCs = Record; + +const ASTUtil = AST('tsx'); + +/** + * Performs a comprehensive analysis of a given file path and its associated web components. + * + * This function parses the provided file's Abstract Syntax Tree (AST) to extract metadata + * about the usage of key features such as suspense, context providers, actions, and hyperlinks. + * It also recursively analyzes nested web components to aggregate their dependencies and behavior. + * + * @param path - The file path to analyze. + * @param allWebComponents - A record of all available web components, used for analysis. + * @param webComponents - A record of web components specific to the given path. + * @param layoutHasContextProvider - Indicates if the layout has a context provider. + * @returns An object containing: + * - `useSuspense`: Indicates if suspense is used. + * - `useContextProvider`: Indicates if a context provider is used. + * - `useActions`: Indicates if actions are used. + * - `useHyperlink`: Indicates if hyperlinks are used. + * - `webComponents`: An aggregated list of web components and their dependencies. + */ +export async function preEntrypointAnalysis( + path: string, + allWebComponents: WCs, + webComponents: WCs = {}, + layoutHasContextProvider?: boolean, +) { + const ast = await getAstFromPath(path); + + // Perform the analysis + let { useSuspense, useContextProvider, useActions, useHyperlink } = + analyzeServerAst(ast, allWebComponents, layoutHasContextProvider); + + // Analyze nested web components + const nestedComponents = await Promise.all( + Object.values(webComponents).map(async (componentPath) => + analyzeServerAst(await getAstFromPath(componentPath), allWebComponents), + ), + ); + + // Aggregate results from nested components + const aggregatedWebComponents = { ...webComponents }; + + for (const item of nestedComponents) { + useContextProvider ||= item.useContextProvider; + useSuspense ||= item.useSuspense; + useHyperlink ||= item.useHyperlink; + Object.assign(aggregatedWebComponents, item.webComponents); + } + + return { + useSuspense, + useContextProvider, + useActions, + useHyperlink, + webComponents: aggregatedWebComponents, + }; +} + +async function getAstFromPath(path: string) { + return ASTUtil.parseCodeToAST( + path[0] === '{' ? '' : await Bun.file(path).text(), + ); +} diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index ac6917a55..ca882d2fa 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -21,10 +21,10 @@ import { getFilterDevRuntimeErrors } from '@/utils/brisa-error-dialog/utils'; import clientBuildPlugin from '@/utils/client-build-plugin'; import createContextPlugin from '@/utils/create-context/create-context-plugin'; import snakeToCamelCase from '@/utils/snake-to-camelcase'; -import analyzeServerAst from '@/utils/analyze-server-ast'; import { logBuildError, logError } from '@/utils/log/log-build'; import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; import getDefinedEnvVar from '../get-defined-env-var'; +import { preEntrypointAnalysis } from '../client-build/pre-entrypoint-analysis'; type TransformOptions = { webComponentsList: Record; @@ -68,25 +68,19 @@ export default async function getClientCodeInPage({ let size = 0; let code = ''; - const ast = await getAstFromPath(pagePath); - - let { useSuspense, useContextProvider, useActions, useHyperlink } = - analyzeServerAst(ast, allWebComponents, layoutHasContextProvider); - - // Web components inside web components - const nestedComponents = await Promise.all( - Object.values(pageWebComponents).map(async (path) => - analyzeServerAst(await getAstFromPath(path), allWebComponents), - ), + const { + useSuspense, + useContextProvider, + useActions, + useHyperlink, + webComponents, + } = await preEntrypointAnalysis( + pagePath, + allWebComponents, + pageWebComponents, + layoutHasContextProvider, ); - for (const item of nestedComponents) { - useContextProvider ||= item.useContextProvider; - useSuspense ||= item.useSuspense; - useHyperlink ||= item.useHyperlink; - Object.assign(pageWebComponents, item.webComponents); - } - const unsuspense = useSuspense ? unsuspenseScriptCode : ''; const rpc = useActions || useHyperlink ? getRPCCode() : ''; const lazyRPC = useActions || useHyperlink ? RPCLazyCode : ''; @@ -94,7 +88,7 @@ export default async function getClientCodeInPage({ size += unsuspense.length; size += rpc.length; - if (!Object.keys(pageWebComponents).length) { + if (!Object.keys(webComponents).length) { return { code, unsuspense, @@ -108,7 +102,7 @@ export default async function getClientCodeInPage({ } const transformedCode = await transformToWebComponents({ - webComponentsList: pageWebComponents, + webComponentsList: webComponents, useContextProvider, integrationsPath, pagePath, From e11172e3478abafd5d41c5455a06cb3c15284b60 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 19:17:56 +0100 Subject: [PATCH 10/39] perf: convert analysis to concurrent --- .../pre-entrypoint-analysis/index.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts index 370fb7361..8bd2df001 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts @@ -29,27 +29,31 @@ export async function preEntrypointAnalysis( webComponents: WCs = {}, layoutHasContextProvider?: boolean, ) { - const ast = await getAstFromPath(path); - - // Perform the analysis - let { useSuspense, useContextProvider, useActions, useHyperlink } = - analyzeServerAst(ast, allWebComponents, layoutHasContextProvider); + const mainAnalysisPromise = getAstFromPath(path).then((ast) => + analyzeServerAst(ast, allWebComponents, layoutHasContextProvider), + ); - // Analyze nested web components - const nestedComponents = await Promise.all( - Object.values(webComponents).map(async (componentPath) => + const nestedAnalysisPromises = Object.entries(webComponents).map( + async ([, componentPath]) => analyzeServerAst(await getAstFromPath(componentPath), allWebComponents), - ), ); - // Aggregate results from nested components - const aggregatedWebComponents = { ...webComponents }; + // Wait for all analyses to complete + const [mainAnalysis, nestedResults] = await Promise.all([ + mainAnalysisPromise, + Promise.all(nestedAnalysisPromises), + ]); - for (const item of nestedComponents) { - useContextProvider ||= item.useContextProvider; - useSuspense ||= item.useSuspense; - useHyperlink ||= item.useHyperlink; - Object.assign(aggregatedWebComponents, item.webComponents); + let { useSuspense, useContextProvider, useActions, useHyperlink } = + mainAnalysis; + + // Aggregate results + const aggregatedWebComponents = { ...webComponents }; + for (const analysis of nestedResults) { + useContextProvider ||= analysis.useContextProvider; + useSuspense ||= analysis.useSuspense; + useHyperlink ||= analysis.useHyperlink; + Object.assign(aggregatedWebComponents, analysis.webComponents); } return { From 58e23032582f190f582cbeaec4caa030ad441f50 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 20:46:08 +0100 Subject: [PATCH 11/39] test: cover pre-entrypoint-analysis tests --- .gitignore | 1 + .../pre-entrypoint-analysis/index.test.ts | 198 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts diff --git a/.gitignore b/.gitignore index 551e10e02..eb9799096 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ _brisa **/out/* **/out-*/* **/dist/* +**/.temp-test-files/* packages/brisa/index.js packages/brisa/out packages/brisa/jsx-runtime/ diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts new file mode 100644 index 000000000..a542ad11b --- /dev/null +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts @@ -0,0 +1,198 @@ +import { describe, it, expect, beforeAll, afterAll } from 'bun:test'; +import { preEntrypointAnalysis } from '.'; +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +const TEMP_DIR = path.join(import.meta.dirname, '.temp-test-files'); + +// Utility to create a unique file with Bun.hash +function createTempFileSync(content: string, extension = 'tsx') { + const fileName = `${Bun.hash(content)}.${extension}`; + const filePath = path.join(TEMP_DIR, fileName); + return { filePath, content }; +} + +// Write files synchronously to disk +async function writeTempFiles( + files: Array<{ filePath: string; content: string }>, +) { + await Promise.all( + files.map(({ filePath, content }) => writeFile(filePath, content, 'utf-8')), + ); +} + +describe('utils', () => { + describe('preEntrypointAnalysis', () => { + beforeAll(async () => { + await mkdir(TEMP_DIR, { recursive: true }); + }); + + afterAll(async () => { + await rm(TEMP_DIR, { recursive: true, force: true }); + }); + + it('should analyze the main file and detect no features', async () => { + const mainFile = createTempFileSync(` + export default function Component() { + return
hello
; + } + `); + await writeTempFiles([mainFile]); + + const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); + expect(result).toEqual({ + useSuspense: false, + useContextProvider: false, + useActions: false, + useHyperlink: false, + webComponents: {}, + }); + }); + + it('should detect suspense in the main file', async () => { + const mainFile = createTempFileSync(` + export default function Component() { + return
hello
; + } + + Component.suspense = () =>
loading...
; + `); + await writeTempFiles([mainFile]); + + const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); + expect(result).toEqual({ + useSuspense: true, + useContextProvider: false, + useActions: false, + useHyperlink: false, + webComponents: {}, + }); + }); + + it('should detect web components in the main file and nested components', async () => { + const mainFile = createTempFileSync(` + export default function Component() { + return hello; + } + `); + const nestedFile = createTempFileSync(` + export default function NestedComponent() { + return nested; + } + `); + + await writeTempFiles([mainFile, nestedFile]); + + const allWebComponents = { + 'my-component': mainFile.filePath, + 'nested-component': nestedFile.filePath, + }; + + const entrypointWebComponents = { + 'my-component': mainFile.filePath, + }; + + const result = await preEntrypointAnalysis( + mainFile.filePath, + allWebComponents, + entrypointWebComponents, + ); + + expect(result).toEqual({ + useSuspense: false, + useContextProvider: false, + useActions: false, + useHyperlink: false, + webComponents: { + 'my-component': mainFile.filePath, + 'nested-component': nestedFile.filePath, + }, + }); + }); + + it('should handle context provider and actions', async () => { + const mainFile = createTempFileSync(` + export default function Component() { + return hello; + } + `); + await writeTempFiles([mainFile]); + + const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); + expect(result).toEqual({ + useSuspense: false, + useContextProvider: true, + useActions: false, + useHyperlink: false, + webComponents: {}, + }); + }); + + it('should detect hyperlinks in the main file', async () => { + const mainFile = createTempFileSync(` + export default function Component() { + return Relative Link; + } + `); + await writeTempFiles([mainFile]); + + const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); + expect(result).toEqual({ + useSuspense: false, + useContextProvider: false, + useActions: false, + useHyperlink: true, + webComponents: {}, + }); + }); + + it('should handle multiple nested components and aggregate metadata', async () => { + const mainFile = createTempFileSync(` + export default function Component() { + return hello; + } + `); + const nestedFile1 = createTempFileSync(` + export default function NestedComponent1() { + return nested 1; + } + `); + const nestedFile2 = createTempFileSync(` + export default function NestedComponent2() { + return nested 2; + } + `); + + await writeTempFiles([mainFile, nestedFile1, nestedFile2]); + + const allWebComponents = { + 'nested-component-1': nestedFile1.filePath, + 'nested-component-2': nestedFile2.filePath, + 'nested-component-3': 'nested-component-3.js', + }; + + const entrypointWebComponents = { + 'nested-component-1': nestedFile1.filePath, + 'nested-component-2': nestedFile2.filePath, + }; + + const result = await preEntrypointAnalysis( + mainFile.filePath, + allWebComponents, + entrypointWebComponents, + ); + + expect(result).toEqual({ + useSuspense: false, + useContextProvider: false, + useActions: false, + useHyperlink: false, + webComponents: { + 'nested-component-1': nestedFile1.filePath, + 'nested-component-2': nestedFile2.filePath, + 'nested-component-3': 'nested-component-3.js', + }, + }); + }); + }); +}); From 8e3bef6528e7e85521b9f7035a0c1f562401de97 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 21:18:57 +0100 Subject: [PATCH 12/39] feat: move file system temp entrypoint logic to module --- .../fs-temp-entrypoint-manager/index.test.ts | 70 +++++++++++++++++++ .../fs-temp-entrypoint-manager/index.ts | 36 ++++++++++ .../brisa/src/utils/client-build/index.ts | 37 ++-------- .../pre-entrypoint-analysis/index.test.ts | 2 +- 4 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.test.ts create mode 100644 packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.ts diff --git a/packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.test.ts b/packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.test.ts new file mode 100644 index 000000000..9e4ca46ee --- /dev/null +++ b/packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it, beforeEach, afterEach } from 'bun:test'; +import path from 'node:path'; +import { rm, readFile, exists, mkdir } from 'node:fs/promises'; +import { normalizeHTML } from '@/helpers'; +import { + removeTempEntrypoint, + removeTempEntrypoints, + writeTempEntrypoint, +} from '.'; + +const TEMP_DIR = path.join(import.meta.dirname, '.temp-test-files'); + +describe('client build -> fs-temp-entrypoint-manager', () => { + beforeEach(async () => { + globalThis.mockConstants = { + PAGES_DIR: TEMP_DIR, + BUILD_DIR: TEMP_DIR, + }; + await mkdir(path.join(TEMP_DIR, '_brisa'), { recursive: true }); + }); + + afterEach(async () => { + delete globalThis.mockConstants; + await rm(TEMP_DIR, { recursive: true }); + }); + + it('should write and remove temp entrypoint', async () => { + const { entrypoint } = await writeTempEntrypoint({ + webComponentsList: { + 'test-component': 'test-component.ts', + }, + useContextProvider: false, + pagePath: '/test-page.ts', + }); + expect(await exists(entrypoint)).toBeTrue(); + expect(normalizeHTML(await readFile(entrypoint, 'utf-8'))).toBe( + normalizeHTML(` + import testComponent from "test-component.ts"; + + const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); + + defineElement("test-component", testComponent); + `), + ); + await removeTempEntrypoint(entrypoint); + expect(await exists(entrypoint)).toBeFalse(); + }); + + it('should write and remove temp entrypoints', async () => { + const { entrypoint: entrypoint1 } = await writeTempEntrypoint({ + webComponentsList: { + 'test-component-1': 'test-component-1.ts', + }, + useContextProvider: false, + pagePath: '/test-page-1.ts', + }); + const { entrypoint: entrypoint2 } = await writeTempEntrypoint({ + webComponentsList: { + 'test-component-2': 'test-component-2.ts', + }, + useContextProvider: false, + pagePath: '/test-page-2.ts', + }); + expect(await exists(entrypoint1)).toBeTrue(); + expect(await exists(entrypoint2)).toBeTrue(); + await removeTempEntrypoints([entrypoint1, entrypoint2]); + expect(await exists(entrypoint1)).toBeFalse(); + expect(await exists(entrypoint2)).toBeFalse(); + }); +}); diff --git a/packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.ts b/packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.ts new file mode 100644 index 000000000..460c5630c --- /dev/null +++ b/packages/brisa/src/utils/client-build/fs-temp-entrypoint-manager/index.ts @@ -0,0 +1,36 @@ +import { writeFile, rm } from 'node:fs/promises'; +import { generateEntryPointCode } from '../generate-entrypoint-code'; +import { getTempPageName } from '../get-temp-page-name'; + +type TransformOptions = { + webComponentsList: Record; + useContextProvider: boolean; + integrationsPath?: string | null; + pagePath: string; +}; + +export async function writeTempEntrypoint({ + webComponentsList, + useContextProvider, + integrationsPath, + pagePath, +}: TransformOptions) { + const webEntrypoint = getTempPageName(pagePath); + const { code, useWebContextPlugins } = await generateEntryPointCode({ + webComponentsList, + useContextProvider, + integrationsPath, + }); + + await writeFile(webEntrypoint, code); + + return { entrypoint: webEntrypoint, useWebContextPlugins }; +} + +export async function removeTempEntrypoint(entrypoint: string) { + return rm(entrypoint); +} + +export async function removeTempEntrypoints(entrypoints: string[]) { + return Promise.all(entrypoints.map(removeTempEntrypoint)); +} diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 2c42a14e1..6953dcfa9 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -1,5 +1,5 @@ import { sep } from 'node:path'; -import { writeFile, rm } from 'node:fs/promises'; +import { rm } from 'node:fs/promises'; import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { @@ -15,9 +15,11 @@ import { shouldTransferTranslatedPagePaths } from '../transfer-translated-page-p import clientBuildPlugin from '../client-build-plugin'; import { logBuildError, logError } from '../log/log-build'; import createContextPlugin from '../create-context/create-context-plugin'; -import { getTempPageName } from './get-temp-page-name'; -import { generateEntryPointCode } from './generate-entrypoint-code'; import { preEntrypointAnalysis } from './pre-entrypoint-analysis'; +import { + removeTempEntrypoints, + writeTempEntrypoint, +} from './fs-temp-entrypoint-manager'; type WCs = Record; type WCsEntrypoints = Record; @@ -141,7 +143,7 @@ export default async function getClientBuildDetails( // TODO: Benchmarks old vs new // Remove all temp files - await Promise.all(entrypoints.map((e) => rm(e))); + await removeTempEntrypoints(entrypoints); if (!success) { logBuildError('Failed to compile web components', logs); @@ -229,7 +231,7 @@ async function prepareEntrypoint( // No client build needed, TODO: We need to return the data?! if (!Object.keys(webComponents).length) return res; - const { entrypoint, useWebContextPlugins } = await writeEntrypoint({ + const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ webComponentsList: webComponents, useContextProvider, integrationsPath, @@ -245,28 +247,3 @@ function getRPCCode() { ? injectRPCCodeForStaticApp() : injectRPCCode()) as unknown as string; } - -type TransformOptions = { - webComponentsList: Record; - useContextProvider: boolean; - integrationsPath?: string | null; - pagePath: string; -}; - -async function writeEntrypoint({ - webComponentsList, - useContextProvider, - integrationsPath, - pagePath, -}: TransformOptions) { - const webEntrypoint = getTempPageName(pagePath); - const { code, useWebContextPlugins } = await generateEntryPointCode({ - webComponentsList, - useContextProvider, - integrationsPath, - }); - - await writeFile(webEntrypoint, code); - - return { entrypoint: webEntrypoint, useWebContextPlugins }; -} diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts index a542ad11b..9ff28741e 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts @@ -21,7 +21,7 @@ async function writeTempFiles( ); } -describe('utils', () => { +describe('client build', () => { describe('preEntrypointAnalysis', () => { beforeAll(async () => { await mkdir(TEMP_DIR, { recursive: true }); From 4bcaa49f4c66196614a934a06949446c638b21ae Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 21:59:57 +0100 Subject: [PATCH 13/39] feat: improve pre-entrypoint-analysis --- .../brisa/src/utils/client-build/index.ts | 56 +------- .../pre-entrypoint-analysis/index.test.ts | 130 ++++++++++++++---- .../pre-entrypoint-analysis/index.ts | 40 +++++- .../get-client-code-in-page/index.test.ts | 2 + .../utils/get-client-code-in-page/index.ts | 76 ++-------- 5 files changed, 159 insertions(+), 145 deletions(-) diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 6953dcfa9..65382d20a 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -1,15 +1,6 @@ import { sep } from 'node:path'; -import { rm } from 'node:fs/promises'; import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; -import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { - type: 'macro', -}; -import { - injectRPCCode, - injectRPCCodeForStaticApp, - injectRPCLazyCode, -} from '@/utils/rpc' with { type: 'macro' }; import getDefinedEnvVar from '../get-defined-env-var'; import { shouldTransferTranslatedPagePaths } from '../transfer-translated-page-paths'; import clientBuildPlugin from '../client-build-plugin'; @@ -31,9 +22,6 @@ type Options = { integrationsPath?: string | null; }; -const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; -const RPCLazyCode = injectRPCLazyCode() as unknown as string; - export default async function getClientBuildDetails( pages: BuildArtifact[], options: Options, @@ -190,60 +178,26 @@ async function prepareEntrypoint( if (!isPage) return; - let size = 0; const wcs = webComponentsPerEntrypoint[pagePath] ?? {}; const pageWebComponents = layoutWebComponents ? { ...layoutWebComponents, ...wcs } : wcs; - const { - useSuspense, - useContextProvider, - useActions, - useHyperlink, - webComponents, - } = await preEntrypointAnalysis( + const analysis = await preEntrypointAnalysis( pagePath, allWebComponents, pageWebComponents, false, // TODO: Remove layoutHasContextProvider as param and do it in a diferent way ); - const unsuspense = useSuspense ? unsuspenseScriptCode : ''; - const rpc = useActions || useHyperlink ? getRPCCode() : ''; - const lazyRPC = useActions || useHyperlink ? RPCLazyCode : ''; - - size += unsuspense.length; - size += rpc.length; - - const res = { - unsuspense, - rpc, - useContextProvider, - lazyRPC, - size, - useI18n: false, - i18nKeys: new Set(), - code: '', - pagePath, - }; - - // No client build needed, TODO: We need to return the data?! - if (!Object.keys(webComponents).length) return res; + if (!Object.keys(analysis.webComponents).length) return analysis; const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ - webComponentsList: webComponents, - useContextProvider, + webComponentsList: analysis.webComponents, + useContextProvider: analysis.useContextProvider, integrationsPath, pagePath, }); - return { ...res, entrypoint, useWebContextPlugins }; -} - -function getRPCCode() { - const { IS_PRODUCTION, IS_STATIC_EXPORT } = getConstants(); - return (IS_STATIC_EXPORT && IS_PRODUCTION - ? injectRPCCodeForStaticApp() - : injectRPCCode()) as unknown as string; + return { ...analysis, entrypoint, useWebContextPlugins }; } diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts index 9ff28741e..9d60bf59b 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts @@ -2,9 +2,23 @@ import { describe, it, expect, beforeAll, afterAll } from 'bun:test'; import { preEntrypointAnalysis } from '.'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; +import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { + type: 'macro', +}; +import { + injectRPCCode, + injectRPCCodeForStaticApp, + injectRPCLazyCode, +} from '@/utils/rpc' with { type: 'macro' }; +import { getConstants } from '@/constants'; const TEMP_DIR = path.join(import.meta.dirname, '.temp-test-files'); +const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; +const rpcCode = injectRPCCode() as unknown as string; +const lazyRPCCOde = injectRPCLazyCode() as unknown as string; +const rpcStatic = injectRPCCodeForStaticApp() as unknown as string; + // Utility to create a unique file with Bun.hash function createTempFileSync(content: string, extension = 'tsx') { const fileName = `${Bun.hash(content)}.${extension}`; @@ -29,6 +43,7 @@ describe('client build', () => { afterAll(async () => { await rm(TEMP_DIR, { recursive: true, force: true }); + globalThis.mockConstants = undefined; }); it('should analyze the main file and detect no features', async () => { @@ -41,11 +56,16 @@ describe('client build', () => { const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); expect(result).toEqual({ - useSuspense: false, - useContextProvider: false, - useActions: false, - useHyperlink: false, + unsuspense: '', + rpc: '', + lazyRPC: '', + size: 0, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: mainFile.filePath, webComponents: {}, + useContextProvider: false, }); }); @@ -61,23 +81,28 @@ describe('client build', () => { const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); expect(result).toEqual({ - useSuspense: true, - useContextProvider: false, - useActions: false, - useHyperlink: false, + unsuspense: unsuspenseScriptCode, + rpc: '', + lazyRPC: '', + size: unsuspenseScriptCode.length, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: mainFile.filePath, webComponents: {}, + useContextProvider: false, }); }); it('should detect web components in the main file and nested components', async () => { const mainFile = createTempFileSync(` export default function Component() { - return hello; + return hello; } `); const nestedFile = createTempFileSync(` export default function NestedComponent() { - return nested; + return ; } `); @@ -99,10 +124,15 @@ describe('client build', () => { ); expect(result).toEqual({ - useSuspense: false, + unsuspense: '', + rpc: '', + lazyRPC: '', + size: 0, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: mainFile.filePath, useContextProvider: false, - useActions: false, - useHyperlink: false, webComponents: { 'my-component': mainFile.filePath, 'nested-component': nestedFile.filePath, @@ -110,25 +140,63 @@ describe('client build', () => { }); }); - it('should handle context provider and actions', async () => { + it('should detect hyperlinks in the main file', async () => { const mainFile = createTempFileSync(` export default function Component() { - return hello; + return Relative Link; } `); await writeTempFiles([mainFile]); const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); expect(result).toEqual({ - useSuspense: false, - useContextProvider: true, - useActions: false, - useHyperlink: false, + unsuspense: '', + rpc: rpcCode, + lazyRPC: lazyRPCCOde, + size: rpcCode.length, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: mainFile.filePath, + useContextProvider: false, webComponents: {}, }); }); - it('should detect hyperlinks in the main file', async () => { + it('should detect hyperlinks in the main file and load static rpc for static app in prod', async () => { + globalThis.mockConstants = { + ...getConstants(), + IS_STATIC_EXPORT: true, + IS_PRODUCTION: true, + }; + const mainFile = createTempFileSync(` + export default function Component() { + return Relative Link; + } + `); + await writeTempFiles([mainFile]); + + const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); + expect(result).toEqual({ + unsuspense: '', + rpc: rpcStatic, + lazyRPC: lazyRPCCOde, + size: rpcStatic.length, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: mainFile.filePath, + useContextProvider: false, + webComponents: {}, + }); + }); + + it('should detect hyperlinks in the main file and load normal rpc for static app in dev', async () => { + globalThis.mockConstants = { + ...getConstants(), + IS_STATIC_EXPORT: true, + IS_PRODUCTION: false, + }; const mainFile = createTempFileSync(` export default function Component() { return Relative Link; @@ -138,10 +206,15 @@ describe('client build', () => { const result = await preEntrypointAnalysis(mainFile.filePath, {}, {}); expect(result).toEqual({ - useSuspense: false, + unsuspense: '', + rpc: rpcCode, + lazyRPC: lazyRPCCOde, + size: rpcCode.length, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: mainFile.filePath, useContextProvider: false, - useActions: false, - useHyperlink: true, webComponents: {}, }); }); @@ -183,10 +256,15 @@ describe('client build', () => { ); expect(result).toEqual({ - useSuspense: false, + unsuspense: '', + rpc: '', + lazyRPC: '', + size: 0, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: mainFile.filePath, useContextProvider: false, - useActions: false, - useHyperlink: false, webComponents: { 'nested-component-1': nestedFile1.filePath, 'nested-component-2': nestedFile2.filePath, diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts index 8bd2df001..234f67204 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts @@ -1,9 +1,20 @@ +import { getConstants } from '@/constants'; import analyzeServerAst from '@/utils/analyze-server-ast'; import AST from '@/utils/ast'; +import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { + type: 'macro', +}; +import { + injectRPCCode, + injectRPCCodeForStaticApp, + injectRPCLazyCode, +} from '@/utils/rpc' with { type: 'macro' }; type WCs = Record; const ASTUtil = AST('tsx'); +const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; +const RPCLazyCode = injectRPCLazyCode() as unknown as string; /** * Performs a comprehensive analysis of a given file path and its associated web components. @@ -56,12 +67,28 @@ export async function preEntrypointAnalysis( Object.assign(aggregatedWebComponents, analysis.webComponents); } + let size = 0; + const unsuspense = useSuspense ? unsuspenseScriptCode : ''; + const rpc = useActions || useHyperlink ? getRPCCode() : ''; + const lazyRPC = useActions || useHyperlink ? RPCLazyCode : ''; + + size += unsuspense.length; + size += rpc.length; + return { - useSuspense, + unsuspense, + rpc, useContextProvider, - useActions, - useHyperlink, + lazyRPC, + pagePath: path, webComponents: aggregatedWebComponents, + + // Fields that need an extra analysis during/after build: + // TODO: Maybe useI18n and i18nKeys can be included to this previous analysis? + code: '', + size, + useI18n: false, + i18nKeys: new Set(), }; } @@ -70,3 +97,10 @@ async function getAstFromPath(path: string) { path[0] === '{' ? '' : await Bun.file(path).text(), ); } + +function getRPCCode() { + const { IS_PRODUCTION, IS_STATIC_EXPORT } = getConstants(); + return (IS_STATIC_EXPORT && IS_PRODUCTION + ? injectRPCCodeForStaticApp() + : injectRPCCode()) as unknown as string; +} diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts index 057457ef9..4fe391fd7 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts @@ -56,10 +56,12 @@ describe('utils', () => { rpc: '', lazyRPC: '', unsuspense: '', + pagePath: path.join(pages, 'somepage.tsx'), size: 0, useI18n: false, useContextProvider: false, i18nKeys: new Set(), + webComponents: {}, }; expect(output).toEqual(expected); }); diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index ca882d2fa..a19f22887 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -2,15 +2,6 @@ import { rm, writeFile } from 'node:fs/promises'; import { join, sep } from 'node:path'; import { getConstants } from '@/constants'; -import AST from '@/utils/ast'; -import { - injectRPCCode, - injectRPCCodeForStaticApp, - injectRPCLazyCode, -} from '@/utils/rpc' with { type: 'macro' }; -import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { - type: 'macro', -}; import { injectClientContextProviderCode } from '@/utils/context-provider/inject-client' with { type: 'macro', }; @@ -41,23 +32,6 @@ type ClientCodeInPageProps = { layoutHasContextProvider?: boolean; }; -const ASTUtil = AST('tsx'); -const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; -const RPCLazyCode = injectRPCLazyCode() as unknown as string; - -function getRPCCode() { - const { IS_PRODUCTION, IS_STATIC_EXPORT } = getConstants(); - return (IS_STATIC_EXPORT && IS_PRODUCTION - ? injectRPCCodeForStaticApp() - : injectRPCCode()) as unknown as string; -} - -async function getAstFromPath(path: string) { - return ASTUtil.parseCodeToAST( - path[0] === '{' ? '' : await Bun.file(path).text(), - ); -} - export default async function getClientCodeInPage({ pagePath, allWebComponents = {}, @@ -65,61 +39,33 @@ export default async function getClientCodeInPage({ integrationsPath, layoutHasContextProvider, }: ClientCodeInPageProps) { - let size = 0; - let code = ''; - - const { - useSuspense, - useContextProvider, - useActions, - useHyperlink, - webComponents, - } = await preEntrypointAnalysis( + const analysis = await preEntrypointAnalysis( pagePath, allWebComponents, pageWebComponents, layoutHasContextProvider, ); - const unsuspense = useSuspense ? unsuspenseScriptCode : ''; - const rpc = useActions || useHyperlink ? getRPCCode() : ''; - const lazyRPC = useActions || useHyperlink ? RPCLazyCode : ''; - - size += unsuspense.length; - size += rpc.length; - - if (!Object.keys(webComponents).length) { - return { - code, - unsuspense, - rpc, - useContextProvider, - lazyRPC, - size, - useI18n: false, - i18nKeys: new Set(), - }; + if (!Object.keys(analysis.webComponents).length) { + return analysis; } const transformedCode = await transformToWebComponents({ - webComponentsList: webComponents, - useContextProvider, + webComponentsList: analysis.webComponents, + useContextProvider: analysis.useContextProvider, integrationsPath, pagePath, }); if (!transformedCode) return null; - code += transformedCode?.code; - size += transformedCode?.size ?? 0; - return { - code, - unsuspense, - rpc, - useContextProvider, - lazyRPC, - size, + code: analysis.code + transformedCode?.code, + unsuspense: analysis.unsuspense, + rpc: analysis.rpc, + useContextProvider: analysis.useContextProvider, + lazyRPC: analysis.lazyRPC, + size: analysis.size + (transformedCode?.size ?? 0), useI18n: transformedCode.useI18n, i18nKeys: transformedCode.i18nKeys, }; From eb1092c31e91871356d1e8c6163e87d3518722c1 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 22:03:43 +0100 Subject: [PATCH 14/39] fix: solve layout --- packages/brisa/src/utils/client-build/index.ts | 4 +++- packages/brisa/src/utils/compile-files/index.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 65382d20a..b54b79d89 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -20,6 +20,7 @@ type Options = { layoutWebComponents: WCs; allWebComponents: WCs; integrationsPath?: string | null; + layoutHasContextProvider?: boolean; }; export default async function getClientBuildDetails( @@ -169,6 +170,7 @@ async function prepareEntrypoint( webComponentsPerEntrypoint, layoutWebComponents, integrationsPath, + layoutHasContextProvider, }: Options, ): Promise { const { BUILD_DIR } = getConstants(); @@ -187,7 +189,7 @@ async function prepareEntrypoint( pagePath, allWebComponents, pageWebComponents, - false, // TODO: Remove layoutHasContextProvider as param and do it in a diferent way + layoutHasContextProvider, ); if (!Object.keys(analysis.webComponents).length) return analysis; diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 15176c7ca..89cc63c1e 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -311,6 +311,7 @@ async function compileClientCodePage( layoutWebComponents, allWebComponents, integrationsPath, + layoutHasContextProvider: layoutCode?.useContextProvider, }); for (const data of pagesData) { From cc44a7a189ac9eaa6137f79928fa8c71c84908a0 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 22:11:46 +0100 Subject: [PATCH 15/39] test: fix test --- .../pre-entrypoint-analysis/index.test.ts | 27 +++++++------------ .../pre-entrypoint-analysis/index.ts | 11 +++++--- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts index 9d60bf59b..177e0a090 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts @@ -1,24 +1,17 @@ import { describe, it, expect, beforeAll, afterAll } from 'bun:test'; -import { preEntrypointAnalysis } from '.'; +import { + preEntrypointAnalysis, + rpcCode, + RPCLazyCode, + rpcStatic, + unsuspenseScriptCode, +} from '.'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { injectUnsuspenseCode } from '@/utils/inject-unsuspense-code' with { - type: 'macro', -}; -import { - injectRPCCode, - injectRPCCodeForStaticApp, - injectRPCLazyCode, -} from '@/utils/rpc' with { type: 'macro' }; import { getConstants } from '@/constants'; const TEMP_DIR = path.join(import.meta.dirname, '.temp-test-files'); -const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; -const rpcCode = injectRPCCode() as unknown as string; -const lazyRPCCOde = injectRPCLazyCode() as unknown as string; -const rpcStatic = injectRPCCodeForStaticApp() as unknown as string; - // Utility to create a unique file with Bun.hash function createTempFileSync(content: string, extension = 'tsx') { const fileName = `${Bun.hash(content)}.${extension}`; @@ -152,7 +145,7 @@ describe('client build', () => { expect(result).toEqual({ unsuspense: '', rpc: rpcCode, - lazyRPC: lazyRPCCOde, + lazyRPC: RPCLazyCode, size: rpcCode.length, code: '', useI18n: false, @@ -180,7 +173,7 @@ describe('client build', () => { expect(result).toEqual({ unsuspense: '', rpc: rpcStatic, - lazyRPC: lazyRPCCOde, + lazyRPC: RPCLazyCode, size: rpcStatic.length, code: '', useI18n: false, @@ -208,7 +201,7 @@ describe('client build', () => { expect(result).toEqual({ unsuspense: '', rpc: rpcCode, - lazyRPC: lazyRPCCOde, + lazyRPC: RPCLazyCode, size: rpcCode.length, code: '', useI18n: false, diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts index 234f67204..89d4d8028 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts @@ -13,8 +13,11 @@ import { type WCs = Record; const ASTUtil = AST('tsx'); -const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; -const RPCLazyCode = injectRPCLazyCode() as unknown as string; + +export const unsuspenseScriptCode = injectUnsuspenseCode() as unknown as string; +export const rpcCode = injectRPCCode() as unknown as string; +export const RPCLazyCode = injectRPCLazyCode() as unknown as string; +export const rpcStatic = injectRPCCodeForStaticApp() as unknown as string; /** * Performs a comprehensive analysis of a given file path and its associated web components. @@ -101,6 +104,6 @@ async function getAstFromPath(path: string) { function getRPCCode() { const { IS_PRODUCTION, IS_STATIC_EXPORT } = getConstants(); return (IS_STATIC_EXPORT && IS_PRODUCTION - ? injectRPCCodeForStaticApp() - : injectRPCCode()) as unknown as string; + ? rpcStatic + : rpcCode) as unknown as string; } From 0a9b0c8a0516afdd899f38d750890ce14c843fee Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 22:25:51 +0100 Subject: [PATCH 16/39] test: fix tests --- .../pre-entrypoint-analysis/index.test.ts | 2 +- .../brisa/src/utils/compile-files/index.test.ts | 15 ++------------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts index 177e0a090..07da608ca 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.test.ts @@ -19,7 +19,7 @@ function createTempFileSync(content: string, extension = 'tsx') { return { filePath, content }; } -// Write files synchronously to disk +// Write files concurrently to disk async function writeTempFiles( files: Array<{ filePath: string; content: string }>, ) { diff --git a/packages/brisa/src/utils/compile-files/index.test.ts b/packages/brisa/src/utils/compile-files/index.test.ts index 4aad3e5bb..0ec030724 100644 --- a/packages/brisa/src/utils/compile-files/index.test.ts +++ b/packages/brisa/src/utils/compile-files/index.test.ts @@ -138,7 +138,7 @@ describe('utils', () => { expect(logs).toBeEmpty(); expect(success).toBe(true); - expect(mockExtendPlugins).toHaveBeenCalledTimes(4); + expect(mockExtendPlugins).toHaveBeenCalledTimes(2); expect(mockExtendPlugins.mock.calls[0][1]).toEqual({ dev: false, isServer: true, @@ -146,17 +146,6 @@ describe('utils', () => { expect(mockExtendPlugins.mock.calls[1][1]).toEqual({ dev: false, isServer: false, - entrypoint: path.join(BUILD_DIR, 'pages', 'page-with-web-component.js'), - }); - expect(mockExtendPlugins.mock.calls[2][1]).toEqual({ - dev: false, - isServer: false, - entrypoint: path.join(BUILD_DIR, 'pages', '_404.js'), - }); - expect(mockExtendPlugins.mock.calls[3][1]).toEqual({ - dev: false, - isServer: false, - entrypoint: path.join(BUILD_DIR, 'pages', '_500.js'), }); const files = fs @@ -647,7 +636,7 @@ describe('utils', () => { ${info} ${info}Route | JS server | JS client (gz) ${info}---------------------------------------------- - ${info}λ /pages/index | 444 B | ${greenLog('4 kB')} + ${info}λ /pages/index | 444 B | ${greenLog('3 kB')} ${info}Δ /layout | 855 B | ${info}Ω /i18n | 221 B | ${info} From eef91077507de13db6b4082779c7fe8fc63bb390 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 23:01:20 +0100 Subject: [PATCH 17/39] fix: improve current get-client-code-in-page --- .../generate-entrypoint-code/index.test.ts | 13 +- .../generate-entrypoint-code/index.ts | 12 +- .../get-client-code-in-page/index.test.ts | 4 +- .../utils/get-client-code-in-page/index.ts | 116 +++--------------- 4 files changed, 28 insertions(+), 117 deletions(-) diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts index bdaae34e1..2a6dc7143 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.test.ts @@ -156,12 +156,11 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toBe( normalizeHTML(` + window._P=webContextPlugins; import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; - window._P=webContextPlugins; - const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); defineElement("my-component", myComponent); @@ -190,6 +189,8 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toStartWith( normalizeHTML(` + window._P=webContextPlugins; + import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; @@ -200,8 +201,6 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toEndWith( normalizeHTML(` - window._P=webContextPlugins; - const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); defineElement("context-provider", contextProvider); @@ -235,6 +234,7 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toStartWith( normalizeHTML(` + window._P=webContextPlugins; import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; @@ -243,8 +243,6 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toContain( normalizeHTML(` - window._P=webContextPlugins; - const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); defineElement("brisa-error-dialog", brisaErrorDialog); @@ -283,6 +281,7 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toStartWith( normalizeHTML(` + window._P=webContextPlugins; import myComponent from "/path/to/my-component"; import myComponent2 from "/path/to/my-component2"; import {webContextPlugins} from "/path/to/integrations"; @@ -291,8 +290,6 @@ describe('client build -> generateEntryPointCode', () => { expect(code).toContain( normalizeHTML(` - window._P=webContextPlugins; - const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component); defineElement("brisa-error-dialog", brisaErrorDialog); diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts index 83b2c232f..5ff82bdd5 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts @@ -38,17 +38,17 @@ export async function generateEntryPointCode({ integrationsPath, ); - const pluginsGlobal = useWebContextPlugins - ? 'window._P=webContextPlugins;\n' - : ''; + // Note: window._P should be in the first line, in this way, the imports + // can use this variable + let code = useWebContextPlugins + ? `window._P=webContextPlugins;\n${imports}` + : `${imports}\n`; const wcSelectors = getWebComponentSelectors(entries, { useContextProvider, isDevelopment: IS_DEVELOPMENT, }); - let code = `${imports}\n`; - if (useContextProvider) { code += injectClientContextProviderCode(); } @@ -57,7 +57,7 @@ export async function generateEntryPointCode({ code += await injectDevelopmentCode(); } - code += `${pluginsGlobal}\n${defineElements(wcSelectors)}`; + code += defineElements(wcSelectors); return { code, useWebContextPlugins }; } diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts index 4fe391fd7..f555c63ab 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts @@ -20,7 +20,7 @@ const pageWebComponents = { const i18nCode = 2799; const brisaSize = 5720; // TODO: Reduce this size :/ -const webComponents = 1107; +const webComponents = 1118; const unsuspenseSize = 213; const rpcSize = 2500; // TODO: Reduce this size const lazyRPCSize = 4105; // TODO: Reduce this size @@ -163,7 +163,7 @@ describe('utils', () => { }); it('should load lazyRPC in /somepage because it has an hyperlink', async () => { - const webComponentSize = 366; + const webComponentSize = 377; const output = await getClientCodeInPage({ pagePath: path.join(pages, 'somepage.tsx'), allWebComponents, diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index a19f22887..c00eb03df 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -1,21 +1,14 @@ -import { rm, writeFile } from 'node:fs/promises'; -import { join, sep } from 'node:path'; - import { getConstants } from '@/constants'; -import { injectClientContextProviderCode } from '@/utils/context-provider/inject-client' with { - type: 'macro', -}; -import { injectBrisaDialogErrorCode } from '@/utils/brisa-error-dialog/inject-code' with { - type: 'macro', -}; -import { getFilterDevRuntimeErrors } from '@/utils/brisa-error-dialog/utils'; import clientBuildPlugin from '@/utils/client-build-plugin'; import createContextPlugin from '@/utils/create-context/create-context-plugin'; -import snakeToCamelCase from '@/utils/snake-to-camelcase'; import { logBuildError, logError } from '@/utils/log/log-build'; import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; import getDefinedEnvVar from '../get-defined-env-var'; import { preEntrypointAnalysis } from '../client-build/pre-entrypoint-analysis'; +import { + removeTempEntrypoint, + writeTempEntrypoint, +} from '../client-build/fs-temp-entrypoint-manager'; type TransformOptions = { webComponentsList: Record; @@ -77,98 +70,26 @@ export async function transformToWebComponents({ integrationsPath, pagePath, }: TransformOptions) { - const { - SRC_DIR, - BUILD_DIR, - CONFIG, - I18N_CONFIG, - IS_DEVELOPMENT, - IS_PRODUCTION, - VERSION, - } = getConstants(); + const { SRC_DIR, CONFIG, I18N_CONFIG, IS_PRODUCTION } = getConstants(); const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); - const internalDir = join(BUILD_DIR, '_brisa'); - const webEntrypoint = join(internalDir, `temp-${VERSION}.ts`); let useI18n = false; let i18nKeys = new Set(); const webComponentsPath = Object.values(webComponentsList); - let useWebContextPlugins = false; - const entries = Object.entries(webComponentsList); - - // Note: JS imports in Windows have / instead of \, so we need to replace it - // Note: Using "require" for component dependencies not move the execution - // on top avoiding missing global variables as window._P - let imports = entries - .map(([name, path]) => - path[0] === '{' - ? `require("${normalizePath(path)}");` - : `import ${snakeToCamelCase(name)} from "${path.replaceAll(sep, '/')}";`, - ) - .join('\n'); - - // Add web context plugins import only if there is a web context plugin - if (integrationsPath) { - const module = await import(integrationsPath); - if (module.webContextPlugins?.length > 0) { - useWebContextPlugins = true; - imports += `import {webContextPlugins} from "${integrationsPath}";`; - } - } - - const defineElement = - 'const defineElement = (name, component) => name && !customElements.get(name) && customElements.define(name, component);'; - - const customElementKeys = entries - .filter(([_, path]) => path[0] !== '{') - .map(([k]) => k); - - if (useContextProvider) { - customElementKeys.unshift('context-provider'); - } - - if (IS_DEVELOPMENT) { - customElementKeys.unshift('brisa-error-dialog'); - } - - const customElementsDefinitions = customElementKeys - .map((k) => `defineElement("${k}", ${snakeToCamelCase(k)});`) - .join('\n'); - - let code = ''; - if (useContextProvider) { - const contextProviderCode = - injectClientContextProviderCode() as unknown as string; - code += contextProviderCode; - } - - // IS_DEVELOPMENT to avoid PROD and TEST environments - if (IS_DEVELOPMENT) { - const brisaDialogErrorCode = (await injectBrisaDialogErrorCode()).replace( - '__FILTER_DEV_RUNTIME_ERRORS__', - getFilterDevRuntimeErrors(), - ); - code += brisaDialogErrorCode; - } - - // Inject web context plugins to window to be used inside web components - if (useWebContextPlugins) { - code += 'window._P=webContextPlugins;\n'; - } - - code += `${imports}\n`; - code += `${defineElement}\n${customElementsDefinitions};`; - - await writeFile(webEntrypoint, code); + const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ + webComponentsList, + useContextProvider, + integrationsPath, + pagePath, + }); const envVar = getDefinedEnvVar(); const { success, logs, outputs } = await Bun.build({ - entrypoints: [webEntrypoint], + entrypoints: [entrypoint], root: SRC_DIR, - // TODO: format: "iife" when Bun support it - // https://bun.sh/docs/bundler#format + format: 'iife', target: 'browser', minify: IS_PRODUCTION, external: CONFIG.external, @@ -235,7 +156,7 @@ export async function transformToWebComponents({ ), }); - await rm(webEntrypoint); + await removeTempEntrypoint(entrypoint); if (!success) { logBuildError('Failed to compile web components', logs); @@ -243,16 +164,9 @@ export async function transformToWebComponents({ } return { - code: '(() => {' + (await outputs[0].text()) + '})();', + code: await outputs[0].text(), size: outputs[0].size, useI18n, i18nKeys, }; } - -export function normalizePath(rawPathname: string, separator = sep) { - const pathname = - rawPathname[0] === '{' ? JSON.parse(rawPathname).client : rawPathname; - - return pathname.replaceAll(separator, '/'); -} From a5f7dab3a0e31fbf7643ab327a1b33d7711b13ae Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sun, 24 Nov 2024 23:12:19 +0100 Subject: [PATCH 18/39] docs: modify plugins docs + fix type --- .../configuring/plugins.md | 15 +++++---------- packages/brisa/src/cli/build-standalone/index.ts | 2 +- packages/brisa/src/types/index.d.ts | 14 ++++---------- packages/brisa/src/utils/client-build/index.ts | 3 +-- .../src/utils/get-client-code-in-page/index.ts | 2 +- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/docs/building-your-application/configuring/plugins.md b/docs/building-your-application/configuring/plugins.md index ea639cc4d..3e7e3827b 100644 --- a/docs/building-your-application/configuring/plugins.md +++ b/docs/building-your-application/configuring/plugins.md @@ -19,7 +19,7 @@ import type { Configuration } from "brisa"; import { MyPlugin } from "my-plugin"; export default { - extendPlugins(plugins, { dev, isServer, entrypoint }) { + extendPlugins(plugins, { dev, isServer }) { return [...plugins, MyPlugin]; }, } satisfies Configuration; @@ -32,15 +32,10 @@ export default { **Options:** -| Field | Type | Description | -| ---------- | --------------------- | ---------------------------------------------------- | -| dev | `boolean` | Indicates whether it's a development build. | -| isServer | `boolean` | Indicates whether it's a server build. | -| entrypoint | `string \| undefined` | Entry point for client builds, optional for servers. | - -> [!NOTE] -> -> On the server it is only executed once and the build is with all the entrypoints, while on the client a separate build is made for each page, that's why on the client there is the `entrypoint` field in the options. +| Field | Type | Description | +| -------- | --------- | ------------------------------------------- | +| dev | `boolean` | Indicates whether it's a development build. | +| isServer | `boolean` | Indicates whether it's a server build. | A plugin is defined as simple JavaScript object containing a name property and a setup function. Example of one: diff --git a/packages/brisa/src/cli/build-standalone/index.ts b/packages/brisa/src/cli/build-standalone/index.ts index 1bed3f15a..c9a5170df 100644 --- a/packages/brisa/src/cli/build-standalone/index.ts +++ b/packages/brisa/src/cli/build-standalone/index.ts @@ -184,7 +184,7 @@ async function compileStandaloneWebComponents(standaloneWC: string[]) { }, createContextPlugin(), ], - { dev: !IS_PRODUCTION, isServer: false, entrypoint: '' }, + { dev: !IS_PRODUCTION, isServer: false }, ), }); } diff --git a/packages/brisa/src/types/index.d.ts b/packages/brisa/src/types/index.d.ts index e42160fb4..175254311 100644 --- a/packages/brisa/src/types/index.d.ts +++ b/packages/brisa/src/types/index.d.ts @@ -866,16 +866,10 @@ export type JSXComponent< error?: JSXComponent; }; -export type ExtendPluginOptions = - | { - dev: boolean; - isServer: true; - } - | { - dev: boolean; - isServer: false; - entrypoint: string; - }; +export type ExtendPluginOptions = { + dev: boolean; + isServer: boolean; +}; export type ExtendPlugins = ( plugins: BunPlugin[], diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index b54b79d89..d1ad2cd8d 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -115,8 +115,7 @@ export default async function getClientBuildDetails( { dev: !IS_PRODUCTION, isServer: false, - /* entrypoint: pagePath (TODO: change docs about this) */ - } as any, // TODO: Fix types + }, ), }); diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index c00eb03df..0ca05d197 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -152,7 +152,7 @@ export async function transformToWebComponents({ }, createContextPlugin(), ], - { dev: !IS_PRODUCTION, isServer: false, entrypoint: pagePath }, + { dev: !IS_PRODUCTION, isServer: false }, ), }); From 4baf40920a6bb25e477c5a6c05f8b373f9b1a070 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Mon, 25 Nov 2024 00:08:30 +0100 Subject: [PATCH 19/39] feat: add get-client-build-details --- .../get-client-build-details/index.test.ts | 281 ++++++++++++++++++ .../get-client-build-details/index.ts | 58 ++++ .../brisa/src/utils/client-build/index.ts | 87 +----- .../brisa/src/utils/client-build/types.ts | 25 ++ .../brisa/src/utils/compile-files/index.ts | 4 +- 5 files changed, 371 insertions(+), 84 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/get-client-build-details/index.test.ts create mode 100644 packages/brisa/src/utils/client-build/get-client-build-details/index.ts create mode 100644 packages/brisa/src/utils/client-build/types.ts diff --git a/packages/brisa/src/utils/client-build/get-client-build-details/index.test.ts b/packages/brisa/src/utils/client-build/get-client-build-details/index.test.ts new file mode 100644 index 000000000..9ef9d80aa --- /dev/null +++ b/packages/brisa/src/utils/client-build/get-client-build-details/index.test.ts @@ -0,0 +1,281 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { mkdir, rm, writeFile, readdir, readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { getClientBuildDetails } from '.'; +import type { Options, WCs } from '../types'; +import { getConstants } from '@/constants'; + +const TEMP_DIR = path.join(import.meta.dirname, '.temp-test-files'); +const PAGES_DIR = path.join(TEMP_DIR, 'pages'); +const INTERNAL_DIR = path.join(TEMP_DIR, '_brisa'); + +function createTempFileSync(dir: string, content: string, extension = 'tsx') { + const fileName = `page-${Bun.hash(content)}.${extension}`; + const filePath = path.join(dir, fileName); + return { filePath, content }; +} + +async function writeTempFiles( + files: Array<{ filePath: string; content: string }>, +) { + await Promise.all( + files.map(({ filePath, content }) => writeFile(filePath, content, 'utf-8')), + ); +} + +describe('client build -> get-client-build-details', () => { + beforeEach(async () => { + await mkdir(PAGES_DIR, { recursive: true }); + await mkdir(INTERNAL_DIR, { recursive: true }); + globalThis.mockConstants = { + ...getConstants(), + PAGES_DIR, + BUILD_DIR: TEMP_DIR, + }; + }); + + afterEach(async () => { + await rm(TEMP_DIR, { recursive: true, force: true }); + globalThis.mockConstants = undefined; + }); + + it('should process a single page without web components', async () => { + const page = createTempFileSync( + PAGES_DIR, + ` + export default function Page() { + return
Hello World
; + } + `, + ); + + await writeTempFiles([page]); + + const pages = [{ path: page.filePath }] as any; + const options = { + allWebComponents: {}, + webComponentsPerEntrypoint: {}, + layoutWebComponents: {}, + layoutHasContextProvider: false, + }; + + const result = await getClientBuildDetails(pages, options); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + unsuspense: '', + rpc: '', + lazyRPC: '', + size: 0, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: page.filePath, + useContextProvider: false, + webComponents: {}, + }); + }); + + it('should process a page with web components and generate an entrypoint', async () => { + const page = createTempFileSync( + PAGES_DIR, + ` + export default function Page() { + return Hello World; + } + `, + ); + + const wcFile = createTempFileSync( + PAGES_DIR, + ` + export default function MyComponent() { + return
My Component
; + } + `, + ); + + await writeTempFiles([page, wcFile]); + + const webComponentsMap: WCs = { + 'my-component': wcFile.filePath, + }; + + const pages = [{ path: page.filePath }] as any; + const options: Options = { + allWebComponents: webComponentsMap, + webComponentsPerEntrypoint: { + [page.filePath]: webComponentsMap, + }, + layoutWebComponents: {}, + layoutHasContextProvider: false, + }; + + const result = await getClientBuildDetails(pages, options); + + expect(result).toHaveLength(1); + const entrypointResult = result[0]; + + expect(entrypointResult).toMatchObject({ + unsuspense: '', + rpc: '', + lazyRPC: '', + size: 0, + code: '', + useI18n: false, + i18nKeys: new Set(), + pagePath: page.filePath, + useContextProvider: false, + webComponents: { + 'my-component': wcFile.filePath, + }, + }); + + expect(entrypointResult.entrypoint).toBeDefined(); + expect(entrypointResult.useWebContextPlugins).toBe(false); + + // Validate the entrypoint file was written + const entrypointDir = path.dirname(entrypointResult.entrypoint!); + const files = await readdir(entrypointDir); + expect(files).toContain(path.basename(entrypointResult.entrypoint!)); + + const entrypointContent = await readFile( + entrypointResult.entrypoint!, + 'utf-8', + ); + expect(entrypointContent).toContain('my-component'); + }); + + it('should skip non-page files', async () => { + const nonPageFile = createTempFileSync( + TEMP_DIR, + ` + export default function NotAPage() { + return
This is not a page
; + } + `, + ); + + await writeTempFiles([nonPageFile]); + + const pages = [{ path: nonPageFile.filePath }] as any; + const options = { + allWebComponents: {}, + webComponentsPerEntrypoint: {}, + layoutWebComponents: {}, + layoutHasContextProvider: false, + }; + + const result = await getClientBuildDetails(pages, options); + + expect(result).toHaveLength(0); + }); + + it('should handle multiple pages and aggregate results', async () => { + const page1 = createTempFileSync( + PAGES_DIR, + ` + export default function Page1() { + return Hello Page 1; + } + `, + ); + + const page2 = createTempFileSync( + PAGES_DIR, + ` + export default function Page2() { + return Hello Page 2; + } + `, + ); + + const wcFile = createTempFileSync( + PAGES_DIR, + ` + export default function MyComponent() { + return
My Component
; + } + `, + ); + + await writeTempFiles([page1, page2, wcFile]); + + const webComponentsMap: WCs = { + 'my-component': wcFile.filePath, + }; + + const pagesOutputs = [ + { path: page1.filePath }, + { path: page2.filePath }, + ] as any; + + const options = { + allWebComponents: webComponentsMap, + webComponentsPerEntrypoint: { + [page2.filePath]: webComponentsMap, + [page1.filePath]: webComponentsMap, + }, + layoutWebComponents: {}, + layoutHasContextProvider: false, + }; + + const result = await getClientBuildDetails(pagesOutputs, options); + const pages = [page1, page2]; + + for (let i = 0; i < pages.length; i++) { + const entrypointResult = result[i]; + + expect(entrypointResult).toMatchObject({ + unsuspense: '', + rpc: '', + lazyRPC: '', + pagePath: pages[i].filePath, + size: 0, + code: '', + useI18n: false, + i18nKeys: new Set(), + useContextProvider: false, + webComponents: { + 'my-component': wcFile.filePath, + }, + }); + + expect(entrypointResult.entrypoint).toBeDefined(); + + const entrypointContent = await readFile( + entrypointResult.entrypoint!, + 'utf-8', + ); + expect(entrypointContent).toContain('my-component'); + } + }); + + it('should return rpc when there is an hyperlink', async () => { + const page = createTempFileSync( + PAGES_DIR, + ` + export default function Page() { + return Click me; + } + `, + ); + + await writeTempFiles([page]); + const pages = [{ path: page.filePath }] as any; + + const options = { + allWebComponents: {}, + webComponentsPerEntrypoint: {}, + layoutWebComponents: {}, + layoutHasContextProvider: false, + }; + + const result = await getClientBuildDetails(pages, options); + + // Valida que se generaron datos para RPC + expect(result).toHaveLength(1); + expect(result[0].rpc.length).toBeGreaterThan(0); + expect(result[0].size).toBeGreaterThan(0); + }); +}); diff --git a/packages/brisa/src/utils/client-build/get-client-build-details/index.ts b/packages/brisa/src/utils/client-build/get-client-build-details/index.ts new file mode 100644 index 000000000..36bd23699 --- /dev/null +++ b/packages/brisa/src/utils/client-build/get-client-build-details/index.ts @@ -0,0 +1,58 @@ +import type { BuildArtifact } from 'bun'; +import { sep } from 'node:path'; +import type { EntryPointData, Options } from '../types'; +import { getConstants } from '@/constants'; +import { preEntrypointAnalysis } from '../pre-entrypoint-analysis'; +import { writeTempEntrypoint } from '../fs-temp-entrypoint-manager'; + +export async function getClientBuildDetails( + pages: BuildArtifact[], + options: Options, +) { + return ( + await Promise.all( + pages.map((p) => getClientEntrypointBuildDetails(p, options)), + ) + ).filter(Boolean) as EntryPointData[]; +} + +export async function getClientEntrypointBuildDetails( + page: BuildArtifact, + { + allWebComponents, + webComponentsPerEntrypoint, + layoutWebComponents, + integrationsPath, + layoutHasContextProvider, + }: Options, +): Promise { + const { BUILD_DIR } = getConstants(); + const route = page.path.replace(BUILD_DIR, ''); + const pagePath = page.path; + const isPage = route.startsWith(sep + 'pages' + sep); + + if (!isPage) return; + + const wcs = webComponentsPerEntrypoint[pagePath] ?? {}; + const pageWebComponents = layoutWebComponents + ? { ...layoutWebComponents, ...wcs } + : wcs; + + const analysis = await preEntrypointAnalysis( + pagePath, + allWebComponents, + pageWebComponents, + layoutHasContextProvider, + ); + + if (!Object.keys(analysis.webComponents).length) return analysis; + + const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ + webComponentsList: analysis.webComponents, + useContextProvider: analysis.useContextProvider, + integrationsPath, + pagePath, + }); + + return { ...analysis, entrypoint, useWebContextPlugins }; +} diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index d1ad2cd8d..cf15b3b84 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -1,4 +1,3 @@ -import { sep } from 'node:path'; import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; import getDefinedEnvVar from '../get-defined-env-var'; @@ -6,31 +5,16 @@ import { shouldTransferTranslatedPagePaths } from '../transfer-translated-page-p import clientBuildPlugin from '../client-build-plugin'; import { logBuildError, logError } from '../log/log-build'; import createContextPlugin from '../create-context/create-context-plugin'; -import { preEntrypointAnalysis } from './pre-entrypoint-analysis'; -import { - removeTempEntrypoints, - writeTempEntrypoint, -} from './fs-temp-entrypoint-manager'; +import { removeTempEntrypoints } from './fs-temp-entrypoint-manager'; +import { getClientBuildDetails } from './get-client-build-details'; +import type { EntryPointData, Options } from './types'; -type WCs = Record; -type WCsEntrypoints = Record; - -type Options = { - webComponentsPerEntrypoint: WCsEntrypoints; - layoutWebComponents: WCs; - allWebComponents: WCs; - integrationsPath?: string | null; - layoutHasContextProvider?: boolean; -}; - -export default async function getClientBuildDetails( +export default async function buildMultiClientEntrypoints( pages: BuildArtifact[], options: Options, ) { const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); - let clientBuildDetails = ( - await Promise.all(pages.map((p) => prepareEntrypoint(p, options))) - ).filter(Boolean) as EntryPointData[]; + let clientBuildDetails = await getClientBuildDetails(pages, options); const entrypointsData = clientBuildDetails.reduce((acc, curr, index) => { if (curr.entrypoint) acc.push({ ...curr, index }); @@ -122,11 +106,6 @@ export default async function getClientBuildDetails( // TODO: Adapt plugin to analyze per entrypoint // TODO: Solve "define" for entrypoint // ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ - // TODO: Create build with all the temporal pages - // TODO: How to solve the layout web components? - // TODO: Save outputs to correct paths - // TODO: Write the new outputs to the disk and cleanup the temporal pages - // TODO: Overwrite clientBuildDetails with code, size // TODO: Test and refactor all this // TODO: Benchmarks old vs new @@ -146,59 +125,3 @@ export default async function getClientBuildDetails( return clientBuildDetails; } - -type EntryPointData = { - unsuspense: string; - rpc: string; - useContextProvider: boolean; - lazyRPC: string; - size: number; - useI18n: boolean; - i18nKeys: Set; - code: string; - entrypoint?: string; - useWebContextPlugins?: boolean; - pagePath: string; - index?: number; -}; - -async function prepareEntrypoint( - page: BuildArtifact, - { - allWebComponents, - webComponentsPerEntrypoint, - layoutWebComponents, - integrationsPath, - layoutHasContextProvider, - }: Options, -): Promise { - const { BUILD_DIR } = getConstants(); - const route = page.path.replace(BUILD_DIR, ''); - const pagePath = page.path; - const isPage = route.startsWith(sep + 'pages' + sep); - - if (!isPage) return; - - const wcs = webComponentsPerEntrypoint[pagePath] ?? {}; - const pageWebComponents = layoutWebComponents - ? { ...layoutWebComponents, ...wcs } - : wcs; - - const analysis = await preEntrypointAnalysis( - pagePath, - allWebComponents, - pageWebComponents, - layoutHasContextProvider, - ); - - if (!Object.keys(analysis.webComponents).length) return analysis; - - const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ - webComponentsList: analysis.webComponents, - useContextProvider: analysis.useContextProvider, - integrationsPath, - pagePath, - }); - - return { ...analysis, entrypoint, useWebContextPlugins }; -} diff --git a/packages/brisa/src/utils/client-build/types.ts b/packages/brisa/src/utils/client-build/types.ts new file mode 100644 index 000000000..ceeefdd6e --- /dev/null +++ b/packages/brisa/src/utils/client-build/types.ts @@ -0,0 +1,25 @@ +export type WCs = Record; +export type WCsEntrypoints = Record; + +export type Options = { + webComponentsPerEntrypoint: WCsEntrypoints; + layoutWebComponents: WCs; + allWebComponents: WCs; + integrationsPath?: string | null; + layoutHasContextProvider?: boolean; +}; + +export type EntryPointData = { + unsuspense: string; + rpc: string; + useContextProvider: boolean; + lazyRPC: string; + size: number; + useI18n: boolean; + i18nKeys: Set; + code: string; + entrypoint?: string; + useWebContextPlugins?: boolean; + pagePath: string; + index?: number; +}; diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 89cc63c1e..702e82e29 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -18,7 +18,7 @@ import generateStaticExport from '@/utils/generate-static-export'; import getWebComponentsPerEntryPoints from '@/utils/get-webcomponents-per-entrypoints'; import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; import generateDynamicTypes from '@/utils/generate-dynamic-types'; -import getClientBuildDetails from '@/utils/client-build'; +import buildMultiClientEntrypoints from '@/utils/client-build'; const TS_REGEX = /\.tsx?$/; const BRISA_DEPS = ['brisa/server']; @@ -306,7 +306,7 @@ async function compileClientCodePage( }) : null; - const pagesData = await getClientBuildDetails(pages, { + const pagesData = await buildMultiClientEntrypoints(pages, { webComponentsPerEntrypoint, layoutWebComponents, allWebComponents, From 93acd90e12ded3f6974ac007e418276ed47f0673 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Mon, 25 Nov 2024 00:23:32 +0100 Subject: [PATCH 20/39] feat: add runBuild helper --- .../brisa/src/utils/client-build/index.ts | 95 ++----------------- .../src/utils/client-build/run-build/index.ts | 92 ++++++++++++++++++ 2 files changed, 100 insertions(+), 87 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/run-build/index.ts diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index cf15b3b84..871ce7102 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -1,19 +1,17 @@ -import { getConstants } from '@/constants'; import type { BuildArtifact } from 'bun'; -import getDefinedEnvVar from '../get-defined-env-var'; -import { shouldTransferTranslatedPagePaths } from '../transfer-translated-page-paths'; -import clientBuildPlugin from '../client-build-plugin'; -import { logBuildError, logError } from '../log/log-build'; -import createContextPlugin from '../create-context/create-context-plugin'; +import { logBuildError } from '../log/log-build'; import { removeTempEntrypoints } from './fs-temp-entrypoint-manager'; import { getClientBuildDetails } from './get-client-build-details'; import type { EntryPointData, Options } from './types'; +import { runBuild } from './run-build'; +// TODO: Benchmarks old vs new +// TODO: Move to module (build-multi-entrypoints) + add tests +// TODO: Move getClientCodeInPage to module like build-single-entrypoint + add tests export default async function buildMultiClientEntrypoints( pages: BuildArtifact[], options: Options, ) { - const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); let clientBuildDetails = await getClientBuildDetails(pages, options); const entrypointsData = clientBuildDetails.reduce((acc, curr, index) => { @@ -22,92 +20,15 @@ export default async function buildMultiClientEntrypoints( }, [] as EntryPointData[]); const entrypoints = entrypointsData.map((p) => p.entrypoint!); - const envVar = getDefinedEnvVar(); - const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); - const webComponentsPath = Object.values(options.allWebComponents); if (entrypoints.length === 0) { return clientBuildDetails; } - const { success, logs, outputs } = await Bun.build({ + const { success, logs, outputs } = await runBuild( entrypoints, - root: SRC_DIR, - format: 'iife', - target: 'browser', - minify: IS_PRODUCTION, - external: CONFIG.external, - define: { - __DEV__: (!IS_PRODUCTION).toString(), - __WEB_CONTEXT_PLUGINS__: 'false', // useWebContextPlugins.toString(), (TODO) - __BASE_PATH__: JSON.stringify(CONFIG.basePath ?? ''), - __ASSET_PREFIX__: JSON.stringify(CONFIG.assetPrefix ?? ''), - __TRAILING_SLASH__: Boolean(CONFIG.trailingSlash).toString(), - __USE_LOCALE__: Boolean(I18N_CONFIG?.defaultLocale).toString(), - __USE_PAGE_TRANSLATION__: shouldTransferTranslatedPagePaths( - I18N_CONFIG?.pages, - ).toString(), - // For security: - 'import.meta.dirname': '', - ...envVar, - }, - plugins: extendPlugins( - [ - { - name: 'client-build-plugin', - setup(build) { - build.onLoad( - { - filter: new RegExp( - `(.*/src/web-components/(?!_integrations).*\\.(tsx|jsx|js|ts)|${webComponentsPath - .join('|') - // These replaces are to fix the regex in Windows - .replace(/\\/g, '\\\\')})$`.replace(/\//g, '[\\\\/]'), - ), - }, - async ({ path, loader }) => { - let code = await Bun.file(path).text(); - - try { - const res = clientBuildPlugin(code, path, { - isI18nAdded: true, // useI18n, (TODO) - isTranslateCoreAdded: true, // i18nKeys.size > 0, (TODO) - }); - code = res.code; - // useI18n ||= res.useI18n; (TODO) - // i18nKeys = new Set([...i18nKeys, ...res.i18nKeys]); (TODO) - } catch (error: any) { - logError({ - messages: [ - `Error transforming web component ${path}`, - error?.message, - ], - stack: error?.stack, - }); - } - - return { - contents: code, - loader, - }; - }, - ); - }, - }, - createContextPlugin(), - ], - { - dev: !IS_PRODUCTION, - isServer: false, - }, - ), - }); - - // TODO: Adapt plugin to analyze per entrypoint - // TODO: Solve "define" for entrypoint - // ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ - // TODO: Test and refactor all this - // TODO: Benchmarks old vs new + options.allWebComponents, + ); // Remove all temp files await removeTempEntrypoints(entrypoints); diff --git a/packages/brisa/src/utils/client-build/run-build/index.ts b/packages/brisa/src/utils/client-build/run-build/index.ts new file mode 100644 index 000000000..99621cb39 --- /dev/null +++ b/packages/brisa/src/utils/client-build/run-build/index.ts @@ -0,0 +1,92 @@ +import { getConstants } from '@/constants'; +import clientBuildPlugin from '@/utils/client-build-plugin'; +import getDefinedEnvVar from '@/utils/get-defined-env-var'; +import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; +import type { WCs } from '../types'; +import { logError } from '@/utils/log/log-build'; +import createContextPlugin from '@/utils/create-context/create-context-plugin'; + +// TODO: Adapt to both files, multi-build & single-build (for layout) +// TODO: Adapt plugin to analyze per entrypoint +// TODO: Solve "define" for entrypoint +// ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ +// TODO: Test and refactor all this +export function runBuild(entrypoints: string[], allWebComponents: WCs) { + const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); + const envVar = getDefinedEnvVar(); + const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); + const webComponentsPath = Object.values(allWebComponents); + + return Bun.build({ + entrypoints, + root: SRC_DIR, + format: 'iife', + target: 'browser', + minify: IS_PRODUCTION, + external: CONFIG.external, + define: { + __DEV__: (!IS_PRODUCTION).toString(), + __WEB_CONTEXT_PLUGINS__: 'false', // useWebContextPlugins.toString(), (TODO) + __BASE_PATH__: JSON.stringify(CONFIG.basePath ?? ''), + __ASSET_PREFIX__: JSON.stringify(CONFIG.assetPrefix ?? ''), + __TRAILING_SLASH__: Boolean(CONFIG.trailingSlash).toString(), + __USE_LOCALE__: Boolean(I18N_CONFIG?.defaultLocale).toString(), + __USE_PAGE_TRANSLATION__: shouldTransferTranslatedPagePaths( + I18N_CONFIG?.pages, + ).toString(), + // For security: + 'import.meta.dirname': '', + ...envVar, + }, + plugins: extendPlugins( + [ + { + name: 'client-build-plugin', + setup(build) { + build.onLoad( + { + filter: new RegExp( + `(.*/src/web-components/(?!_integrations).*\\.(tsx|jsx|js|ts)|${webComponentsPath + .join('|') + // These replaces are to fix the regex in Windows + .replace(/\\/g, '\\\\')})$`.replace(/\//g, '[\\\\/]'), + ), + }, + async ({ path, loader }) => { + let code = await Bun.file(path).text(); + + try { + const res = clientBuildPlugin(code, path, { + isI18nAdded: true, // useI18n, (TODO) + isTranslateCoreAdded: true, // i18nKeys.size > 0, (TODO) + }); + code = res.code; + // useI18n ||= res.useI18n; (TODO) + // i18nKeys = new Set([...i18nKeys, ...res.i18nKeys]); (TODO) + } catch (error: any) { + logError({ + messages: [ + `Error transforming web component ${path}`, + error?.message, + ], + stack: error?.stack, + }); + } + + return { + contents: code, + loader, + }; + }, + ); + }, + }, + createContextPlugin(), + ], + { + dev: !IS_PRODUCTION, + isServer: false, + }, + ), + }); +} From dd0d0cf790c6cc174f07c16cbea03a867dab420f Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Mon, 25 Nov 2024 00:26:46 +0100 Subject: [PATCH 21/39] refactor: change param name --- packages/brisa/src/utils/client-build/run-build/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/brisa/src/utils/client-build/run-build/index.ts b/packages/brisa/src/utils/client-build/run-build/index.ts index 99621cb39..04e6b6060 100644 --- a/packages/brisa/src/utils/client-build/run-build/index.ts +++ b/packages/brisa/src/utils/client-build/run-build/index.ts @@ -11,11 +11,11 @@ import createContextPlugin from '@/utils/create-context/create-context-plugin'; // TODO: Solve "define" for entrypoint // ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ // TODO: Test and refactor all this -export function runBuild(entrypoints: string[], allWebComponents: WCs) { +export function runBuild(entrypoints: string[], webComponents: WCs) { const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); const envVar = getDefinedEnvVar(); const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); - const webComponentsPath = Object.values(allWebComponents); + const webComponentsPath = Object.values(webComponents); return Bun.build({ entrypoints, From b4ca557a22cdb9618a27aff86a63ac2e29011770 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Mon, 25 Nov 2024 15:40:09 +0100 Subject: [PATCH 22/39] feat: integrate runBuild helper to get-client-code-in-page --- .../brisa/src/utils/client-build/index.ts | 3 +- .../src/utils/client-build/run-build/index.ts | 39 ++++++-- .../utils/get-client-code-in-page/index.ts | 95 ++----------------- 3 files changed, 44 insertions(+), 93 deletions(-) diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 871ce7102..c268d3c59 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -25,7 +25,8 @@ export default async function buildMultiClientEntrypoints( return clientBuildDetails; } - const { success, logs, outputs } = await runBuild( + // TODO: Use analysis for useI18n and i18nKeys + const { success, logs, outputs, analysis } = await runBuild( entrypoints, options.allWebComponents, ); diff --git a/packages/brisa/src/utils/client-build/run-build/index.ts b/packages/brisa/src/utils/client-build/run-build/index.ts index 04e6b6060..947d2cd3b 100644 --- a/packages/brisa/src/utils/client-build/run-build/index.ts +++ b/packages/brisa/src/utils/client-build/run-build/index.ts @@ -11,13 +11,17 @@ import createContextPlugin from '@/utils/create-context/create-context-plugin'; // TODO: Solve "define" for entrypoint // ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ // TODO: Test and refactor all this -export function runBuild(entrypoints: string[], webComponents: WCs) { +export async function runBuild(entrypoints: string[], webComponents: WCs) { const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); const envVar = getDefinedEnvVar(); const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); const webComponentsPath = Object.values(webComponents); - return Bun.build({ + const entrypointsSet = new Set(entrypoints); + const analysis: Record }> = + {}; + + const buildResult = await Bun.build({ entrypoints, root: SRC_DIR, format: 'iife', @@ -43,6 +47,15 @@ export function runBuild(entrypoints: string[], webComponents: WCs) { { name: 'client-build-plugin', setup(build) { + let currentEntrypoint = entrypoints[0]; + + build.onResolve({ filter: /.*/ }, (args) => { + if (args.importer && entrypointsSet.has(args.importer)) { + currentEntrypoint = args.importer; + } + return undefined; + }); + build.onLoad( { filter: new RegExp( @@ -56,13 +69,22 @@ export function runBuild(entrypoints: string[], webComponents: WCs) { let code = await Bun.file(path).text(); try { + if (!analysis[currentEntrypoint]) { + analysis[currentEntrypoint] = { + useI18n: false, + i18nKeys: new Set(), + }; + } + const currentAnalysis = analysis[currentEntrypoint]; const res = clientBuildPlugin(code, path, { - isI18nAdded: true, // useI18n, (TODO) - isTranslateCoreAdded: true, // i18nKeys.size > 0, (TODO) + isI18nAdded: currentAnalysis.useI18n, + isTranslateCoreAdded: currentAnalysis.i18nKeys.size > 0, }); code = res.code; - // useI18n ||= res.useI18n; (TODO) - // i18nKeys = new Set([...i18nKeys, ...res.i18nKeys]); (TODO) + currentAnalysis.useI18n ||= res.useI18n; + res.i18nKeys.forEach((key) => + currentAnalysis.i18nKeys.add(key), + ); } catch (error: any) { logError({ messages: [ @@ -89,4 +111,9 @@ export function runBuild(entrypoints: string[], webComponents: WCs) { }, ), }); + + return { + ...buildResult, + analysis, + }; } diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index 0ca05d197..6f3d39567 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -1,14 +1,10 @@ -import { getConstants } from '@/constants'; -import clientBuildPlugin from '@/utils/client-build-plugin'; -import createContextPlugin from '@/utils/create-context/create-context-plugin'; -import { logBuildError, logError } from '@/utils/log/log-build'; -import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; -import getDefinedEnvVar from '../get-defined-env-var'; +import { logBuildError } from '@/utils/log/log-build'; import { preEntrypointAnalysis } from '../client-build/pre-entrypoint-analysis'; import { removeTempEntrypoint, writeTempEntrypoint, } from '../client-build/fs-temp-entrypoint-manager'; +import { runBuild } from '../client-build/run-build'; type TransformOptions = { webComponentsList: Record; @@ -70,13 +66,7 @@ export async function transformToWebComponents({ integrationsPath, pagePath, }: TransformOptions) { - const { SRC_DIR, CONFIG, I18N_CONFIG, IS_PRODUCTION } = getConstants(); - - const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); - let useI18n = false; - let i18nKeys = new Set(); - const webComponentsPath = Object.values(webComponentsList); - + // TODO: Resolve useWebContextPlugins inside build for multi and single entrypoint const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ webComponentsList, useContextProvider, @@ -84,77 +74,10 @@ export async function transformToWebComponents({ pagePath, }); - const envVar = getDefinedEnvVar(); - - const { success, logs, outputs } = await Bun.build({ - entrypoints: [entrypoint], - root: SRC_DIR, - format: 'iife', - target: 'browser', - minify: IS_PRODUCTION, - external: CONFIG.external, - define: { - __DEV__: (!IS_PRODUCTION).toString(), - __WEB_CONTEXT_PLUGINS__: useWebContextPlugins.toString(), - __BASE_PATH__: JSON.stringify(CONFIG.basePath ?? ''), - __ASSET_PREFIX__: JSON.stringify(CONFIG.assetPrefix ?? ''), - __TRAILING_SLASH__: Boolean(CONFIG.trailingSlash).toString(), - __USE_LOCALE__: Boolean(I18N_CONFIG?.defaultLocale).toString(), - __USE_PAGE_TRANSLATION__: shouldTransferTranslatedPagePaths( - I18N_CONFIG?.pages, - ).toString(), - // For security: - 'import.meta.dirname': '', - ...envVar, - }, - plugins: extendPlugins( - [ - { - name: 'client-build-plugin', - setup(build) { - build.onLoad( - { - filter: new RegExp( - `(.*/src/web-components/(?!_integrations).*\\.(tsx|jsx|js|ts)|${webComponentsPath - .join('|') - // These replaces are to fix the regex in Windows - .replace(/\\/g, '\\\\')})$`.replace(/\//g, '[\\\\/]'), - ), - }, - async ({ path, loader }) => { - let code = await Bun.file(path).text(); - - try { - const res = clientBuildPlugin(code, path, { - isI18nAdded: useI18n, - isTranslateCoreAdded: i18nKeys.size > 0, - }); - code = res.code; - useI18n ||= res.useI18n; - i18nKeys = new Set([...i18nKeys, ...res.i18nKeys]); - } catch (error: any) { - logError({ - messages: [ - `Error transforming web component ${path}`, - error?.message, - ], - stack: error?.stack, - }); - } - - return { - contents: code, - loader, - }; - }, - ); - }, - }, - createContextPlugin(), - ], - { dev: !IS_PRODUCTION, isServer: false }, - ), - }); + const { success, logs, outputs, analysis } = await runBuild( + [entrypoint], + webComponentsList, + ); await removeTempEntrypoint(entrypoint); @@ -166,7 +89,7 @@ export async function transformToWebComponents({ return { code: await outputs[0].text(), size: outputs[0].size, - useI18n, - i18nKeys, + useI18n: analysis[entrypoint]?.useI18n, + i18nKeys: analysis[entrypoint]?.i18nKeys, }; } From aa55dcaf8b46458724edc566a295f1b8bf12e378 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Mon, 25 Nov 2024 17:47:30 +0100 Subject: [PATCH 23/39] test: cover runBuild --- .../client-build/run-build/index.test.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 packages/brisa/src/utils/client-build/run-build/index.test.ts diff --git a/packages/brisa/src/utils/client-build/run-build/index.test.ts b/packages/brisa/src/utils/client-build/run-build/index.test.ts new file mode 100644 index 000000000..f51b65da7 --- /dev/null +++ b/packages/brisa/src/utils/client-build/run-build/index.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import path from 'node:path'; +import { mkdir, rm } from 'node:fs/promises'; + +import { getConstants } from '@/constants'; +import { writeTempEntrypoint } from '../fs-temp-entrypoint-manager'; +import { runBuild } from '.'; + +const SRC = path.join(import.meta.dir, '..', '..', '..', '__fixtures__'); +const build = path.join(import.meta.dir, '.temp-test-files'); + +describe('client build -> runBuild', () => { + beforeEach(async () => { + await mkdir(path.join(build, '_brisa'), { recursive: true }); + globalThis.mockConstants = { + ...getConstants(), + SRC_DIR: SRC, + BUILD_DIR: build, + }; + }); + + afterEach(async () => { + await rm(build, { recursive: true, force: true }); + globalThis.mockConstants = undefined; + }); + + it('should run the build with a single entrypoint', async () => { + const { entrypoint } = await writeTempEntrypoint({ + webComponentsList: { + 'custom-counter': path.join( + SRC, + 'web-components', + 'custom-counter.tsx', + ), + }, + useContextProvider: false, + pagePath: 'foo', + }); + const { success, logs, outputs, analysis } = await runBuild( + [entrypoint], + {}, + ); + + expect(success).toBe(true); + expect(logs).toHaveLength(0); + expect(outputs).toHaveLength(1); + expect(outputs[0].size).toBeGreaterThan(0); + expect(outputs[0].text()).resolves.toContain( + ' defineElement("custom-counter"', + ); + expect(analysis).toEqual({ + [entrypoint]: { + // SRC has i18n.ts, so always true + useI18n: true, + i18nKeys: new Set(), + }, + }); + }); + + it('should run the build with multiple entrypoints', async () => { + const { entrypoint: entrypoint1 } = await writeTempEntrypoint({ + webComponentsList: { + 'custom-counter': path.join( + SRC, + 'web-components', + 'custom-counter.tsx', + ), + }, + useContextProvider: false, + pagePath: 'foo', + }); + const { entrypoint: entrypoint2 } = await writeTempEntrypoint({ + webComponentsList: { + 'web-component': path.join(SRC, 'web-components', 'web-component.tsx'), + }, + useContextProvider: false, + pagePath: 'bar', + }); + + const { success, logs, outputs, analysis } = await runBuild( + [entrypoint1, entrypoint2], + {}, + ); + + expect(success).toBe(true); + expect(logs).toHaveLength(0); + expect(outputs).toHaveLength(2); + expect(outputs[0].size).toBeGreaterThan(0); + expect(outputs[1].size).toBeGreaterThan(0); + expect(outputs[0].text()).resolves.toContain( + ' defineElement("custom-counter"', + ); + expect(outputs[1].text()).resolves.toContain( + ' defineElement("web-component"', + ); + expect(analysis).toEqual({ + [entrypoint1]: { + useI18n: true, + i18nKeys: new Set(), + }, + [entrypoint2]: { + useI18n: true, + i18nKeys: new Set(['hello']), + }, + }); + }); +}); From f2063a98ab37423d33e0be6ea0ce2b211cf00bcf Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Mon, 25 Nov 2024 23:20:55 +0100 Subject: [PATCH 24/39] feat: add dependecy graph to client build --- .../client-build/run-build/index.test.ts | 5 +- .../src/utils/client-build/run-build/index.ts | 119 ++++++++++-------- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/packages/brisa/src/utils/client-build/run-build/index.test.ts b/packages/brisa/src/utils/client-build/run-build/index.test.ts index f51b65da7..41761e35e 100644 --- a/packages/brisa/src/utils/client-build/run-build/index.test.ts +++ b/packages/brisa/src/utils/client-build/run-build/index.test.ts @@ -50,8 +50,7 @@ describe('client build -> runBuild', () => { ); expect(analysis).toEqual({ [entrypoint]: { - // SRC has i18n.ts, so always true - useI18n: true, + useI18n: false, i18nKeys: new Set(), }, }); @@ -95,7 +94,7 @@ describe('client build -> runBuild', () => { ); expect(analysis).toEqual({ [entrypoint1]: { - useI18n: true, + useI18n: false, i18nKeys: new Set(), }, [entrypoint2]: { diff --git a/packages/brisa/src/utils/client-build/run-build/index.ts b/packages/brisa/src/utils/client-build/run-build/index.ts index 947d2cd3b..296aed554 100644 --- a/packages/brisa/src/utils/client-build/run-build/index.ts +++ b/packages/brisa/src/utils/client-build/run-build/index.ts @@ -6,8 +6,12 @@ import type { WCs } from '../types'; import { logError } from '@/utils/log/log-build'; import createContextPlugin from '@/utils/create-context/create-context-plugin'; +type DepGraph = { + source: string; + target: string; +}; + // TODO: Adapt to both files, multi-build & single-build (for layout) -// TODO: Adapt plugin to analyze per entrypoint // TODO: Solve "define" for entrypoint // ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ // TODO: Test and refactor all this @@ -16,11 +20,20 @@ export async function runBuild(entrypoints: string[], webComponents: WCs) { const envVar = getDefinedEnvVar(); const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); const webComponentsPath = Object.values(webComponents); - - const entrypointsSet = new Set(entrypoints); + const depGraph: DepGraph[] = []; + const entrypointSet = new Set(entrypoints); const analysis: Record }> = {}; + function pathToEntrypoint(path: string) { + if (entrypointSet.has(path)) return path; + + const dependency = depGraph.find((dep) => dep.target === path); + if (!dependency) return undefined; + + return pathToEntrypoint(dependency.source); + } + const buildResult = await Bun.build({ entrypoints, root: SRC_DIR, @@ -47,60 +60,64 @@ export async function runBuild(entrypoints: string[], webComponents: WCs) { { name: 'client-build-plugin', setup(build) { - let currentEntrypoint = entrypoints[0]; + const filter = new RegExp( + `(.*/src/web-components/(?!_integrations).*\\.(tsx|jsx|js|ts)|${webComponentsPath + .join('|') + // These replaces are to fix the regex in Windows + .replace(/\\/g, '\\\\')})$`.replace(/\//g, '[\\\\/]'), + ); - build.onResolve({ filter: /.*/ }, (args) => { - if (args.importer && entrypointsSet.has(args.importer)) { - currentEntrypoint = args.importer; - } - return undefined; + // TODO: Simplify this code after this Bun feature: + // https://github.com/oven-sh/bun/issues/15003 + build.onResolve({ filter }, (args) => { + if (!args.importer) return; + + depGraph.push({ source: args.importer, target: args.path }); + + return null; }); - build.onLoad( - { - filter: new RegExp( - `(.*/src/web-components/(?!_integrations).*\\.(tsx|jsx|js|ts)|${webComponentsPath - .join('|') - // These replaces are to fix the regex in Windows - .replace(/\\/g, '\\\\')})$`.replace(/\//g, '[\\\\/]'), - ), - }, - async ({ path, loader }) => { - let code = await Bun.file(path).text(); + build.onLoad({ filter }, async ({ path, loader }) => { + let code = await Bun.file(path).text(); - try { - if (!analysis[currentEntrypoint]) { - analysis[currentEntrypoint] = { - useI18n: false, - i18nKeys: new Set(), - }; - } - const currentAnalysis = analysis[currentEntrypoint]; - const res = clientBuildPlugin(code, path, { - isI18nAdded: currentAnalysis.useI18n, - isTranslateCoreAdded: currentAnalysis.i18nKeys.size > 0, - }); - code = res.code; - currentAnalysis.useI18n ||= res.useI18n; - res.i18nKeys.forEach((key) => - currentAnalysis.i18nKeys.add(key), - ); - } catch (error: any) { - logError({ - messages: [ - `Error transforming web component ${path}`, - error?.message, - ], - stack: error?.stack, - }); + try { + const entrypoint = pathToEntrypoint(path); + const currentAnalysis = + entrypoint && analysis[entrypoint] + ? analysis[entrypoint] + : { + useI18n: false, + i18nKeys: new Set(), + }; + + if (entrypoint && !analysis[entrypoint]) { + analysis[entrypoint] = currentAnalysis; } - return { - contents: code, - loader, - }; - }, - ); + const res = clientBuildPlugin(code, path, { + isI18nAdded: currentAnalysis.useI18n, + isTranslateCoreAdded: currentAnalysis.i18nKeys.size > 0, + }); + code = res.code; + currentAnalysis.useI18n ||= res.useI18n; + res.i18nKeys.forEach((key) => + currentAnalysis.i18nKeys.add(key), + ); + } catch (error: any) { + logError({ + messages: [ + `Error transforming web component ${path}`, + error?.message, + ], + stack: error?.stack, + }); + } + + return { + contents: code, + loader, + }; + }); }, }, createContextPlugin(), From dbdb5c58096d5f7fcc177ce7b1460bb11ae221af Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 26 Nov 2024 00:17:32 +0100 Subject: [PATCH 25/39] feat: integrate analysis --- packages/brisa/src/utils/client-build/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index c268d3c59..bda380b83 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -25,7 +25,6 @@ export default async function buildMultiClientEntrypoints( return clientBuildDetails; } - // TODO: Use analysis for useI18n and i18nKeys const { success, logs, outputs, analysis } = await runBuild( entrypoints, options.allWebComponents, @@ -41,8 +40,11 @@ export default async function buildMultiClientEntrypoints( for (let i = 0; i < outputs.length; i++) { const index = entrypointsData[i].index!; + const pathname = entrypoints[i]; clientBuildDetails[index].code = await outputs[i].text(); clientBuildDetails[index].size = outputs[i].size; + clientBuildDetails[index].useI18n = analysis[pathname]?.useI18n; // TODO: fix this + clientBuildDetails[index].i18nKeys = analysis[pathname]?.i18nKeys; // TODO: fix this } return clientBuildDetails; From d64b17e6295f93c7c8852c8577d8cdc8f2ab0ec6 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 26 Nov 2024 00:37:10 +0100 Subject: [PATCH 26/39] fix: change to concurrent code --- .../brisa/src/utils/client-build/index.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index bda380b83..451bc877f 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -38,14 +38,20 @@ export default async function buildMultiClientEntrypoints( return clientBuildDetails; } - for (let i = 0; i < outputs.length; i++) { - const index = entrypointsData[i].index!; - const pathname = entrypoints[i]; - clientBuildDetails[index].code = await outputs[i].text(); - clientBuildDetails[index].size = outputs[i].size; - clientBuildDetails[index].useI18n = analysis[pathname]?.useI18n; // TODO: fix this - clientBuildDetails[index].i18nKeys = analysis[pathname]?.i18nKeys; // TODO: fix this - } + await Promise.all( + outputs.map(async (output, i) => { + const index = entrypointsData[i].index!; + const pathname = entrypoints[i]; + + clientBuildDetails[index] = { + ...clientBuildDetails[index], + code: await output.text(), + size: output.size, + useI18n: analysis[pathname]?.useI18n ?? false, // TODO: fix this + i18nKeys: analysis[pathname]?.i18nKeys ?? new Set(), // TODO: fix this + }; + }) + ); return clientBuildDetails; } From 87afa2641fc00fedccef1cb7da161bb65c741169 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 26 Nov 2024 22:25:27 +0100 Subject: [PATCH 27/39] feat(build): adapt i18n key analysis to work in the new build --- .../brisa/src/cli/build-standalone/index.ts | 3 +- .../utils/brisa-error-dialog/inject-code.ts | 2 +- .../utils/client-build-plugin/index.test.ts | 129 ++++++------ .../src/utils/client-build-plugin/index.ts | 40 +--- .../client-build-plugin/integration.test.tsx | 4 +- .../process-client-ast/index.test.ts | 195 ++++++++++-------- .../process-client-ast/index.ts | 127 +++++++++--- .../generate-entrypoint-code/index.ts | 1 + .../brisa/src/utils/client-build/index.ts | 14 +- .../client-build/process-i18n/index.test.ts | 142 +++++++++++++ .../utils/client-build/process-i18n/index.ts | 49 +++++ .../client-build/run-build/index.test.ts | 34 ++- .../src/utils/client-build/run-build/index.ts | 70 +------ packages/brisa/src/utils/compile-wc/index.ts | 2 +- .../utils/context-provider/inject-client.ts | 2 +- .../get-client-code-in-page/index.test.ts | 6 +- .../utils/get-client-code-in-page/index.ts | 9 +- 17 files changed, 506 insertions(+), 323 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/process-i18n/index.test.ts create mode 100644 packages/brisa/src/utils/client-build/process-i18n/index.ts diff --git a/packages/brisa/src/cli/build-standalone/index.ts b/packages/brisa/src/cli/build-standalone/index.ts index c9a5170df..5613c0e50 100644 --- a/packages/brisa/src/cli/build-standalone/index.ts +++ b/packages/brisa/src/cli/build-standalone/index.ts @@ -164,11 +164,10 @@ async function compileStandaloneWebComponents(standaloneWC: string[]) { let code = await Bun.file(path).text(); try { - const res = clientBuildPlugin(code, path, { + code = clientBuildPlugin(code, path, { forceTranspilation: true, customElementSelectorToDefine: webComponentsSelector[path], }); - code = res.code; } catch (error) { console.log(LOG_PREFIX.ERROR, `Error transforming ${path}`); console.log(LOG_PREFIX.ERROR, (error as Error).message); diff --git a/packages/brisa/src/utils/brisa-error-dialog/inject-code.ts b/packages/brisa/src/utils/brisa-error-dialog/inject-code.ts index 749ac3b71..e289a75fc 100644 --- a/packages/brisa/src/utils/brisa-error-dialog/inject-code.ts +++ b/packages/brisa/src/utils/brisa-error-dialog/inject-code.ts @@ -28,7 +28,7 @@ export async function injectBrisaDialogErrorCode() { // https://github.com/oven-sh/bun/issues/7611 await Bun.readableStreamToText(Bun.file(path).stream()), internalComponentId, - ).code, + ), loader, })); }, diff --git a/packages/brisa/src/utils/client-build-plugin/index.test.ts b/packages/brisa/src/utils/client-build-plugin/index.test.ts index afbc1c88a..0d5617858 100644 --- a/packages/brisa/src/utils/client-build-plugin/index.test.ts +++ b/packages/brisa/src/utils/client-build-plugin/index.test.ts @@ -23,7 +23,7 @@ describe('utils', () => { clientBuildPlugin( input, '/src/web-components/_native/my-component.tsx', - ).code, + ), ); const expected = toInline(input); expect(output).toBe(expected); @@ -41,7 +41,7 @@ describe('utils', () => { clientBuildPlugin( input, '/src/web-components/_partials/my-component.tsx', - ).code, + ), ); const expected = toInline(` export default function partial() { @@ -58,7 +58,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, '__BRISA_CLIENT__ContextProvider').code, + clientBuildPlugin(input, '__BRISA_CLIENT__ContextProvider'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -79,7 +79,7 @@ describe('utils', () => { const output = toInline( clientBuildPlugin(input, '/src/components/my-component.tsx', { forceTranspilation: true, - }).code, + }), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -100,7 +100,7 @@ describe('utils', () => { const output = toInline( clientBuildPlugin(input, '/src/web-components/my-component.tsx', { customElementSelectorToDefine: 'my-component', - }).code, + }), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -125,7 +125,7 @@ describe('utils', () => { const output = toInline( clientBuildPlugin(input, '/src/web-components/my-component.tsx', { customElementSelectorToDefine: 'my-component', - }).code, + }), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -146,7 +146,7 @@ describe('utils', () => { const output = toInline( clientBuildPlugin(input, '/src/web-components/my-component.tsx', { customElementSelectorToDefine: 'my-component', - }).code, + }), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -165,7 +165,7 @@ describe('utils', () => { const element =
foo
`; const output = toInline( - clientBuildPlugin(input, '/src/components/my-component.tsx').code, + clientBuildPlugin(input, '/src/components/my-component.tsx'), ); const expected = toInline( `const element = ['div', {}, 'foo'];export default null;`, @@ -178,7 +178,7 @@ describe('utils', () => { const element = () =>
foo
`; const output = toInline( - clientBuildPlugin(input, '/src/components/my-component.tsx').code, + clientBuildPlugin(input, '/src/components/my-component.tsx'), ); const expected = toInline( `const element = () => ['div', {}, 'foo'];export default null;`, @@ -193,7 +193,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -214,7 +214,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -235,7 +235,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -260,7 +260,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -285,7 +285,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -308,7 +308,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -332,7 +332,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -355,7 +355,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -378,7 +378,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -415,7 +415,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -459,7 +459,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -517,7 +517,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -542,7 +542,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -572,7 +572,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -601,7 +601,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -624,7 +624,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -648,7 +648,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -670,7 +670,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -690,7 +690,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -712,7 +712,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -736,7 +736,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -760,7 +760,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -784,7 +784,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -806,7 +806,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -826,7 +826,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -846,7 +846,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -866,7 +866,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -887,7 +887,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -910,7 +910,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -934,7 +934,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = @@ -957,7 +957,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = @@ -981,7 +981,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = @@ -1006,7 +1006,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = @@ -1031,7 +1031,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1060,7 +1060,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1089,7 +1089,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1118,7 +1118,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1153,7 +1153,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = @@ -1181,7 +1181,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = @@ -1205,7 +1205,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1231,7 +1231,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1257,7 +1257,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1283,7 +1283,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1314,8 +1314,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/conditional-render.tsx') - .code, + clientBuildPlugin(input, 'src/web-components/conditional-render.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -1341,7 +1340,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -1367,7 +1366,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -1392,7 +1391,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1416,7 +1415,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1439,7 +1438,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -1463,7 +1462,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1498,7 +1497,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1529,7 +1528,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` @@ -1559,7 +1558,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -1611,7 +1610,7 @@ describe('utils', () => { } `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` import {brisaElement, _on, _off} from "brisa/client"; @@ -1646,7 +1645,7 @@ describe('utils', () => { `; const output = toInline( - clientBuildPlugin(input, 'src/web-components/my-component.tsx').code, + clientBuildPlugin(input, 'src/web-components/my-component.tsx'), ); const expected = toInline(` diff --git a/packages/brisa/src/utils/client-build-plugin/index.ts b/packages/brisa/src/utils/client-build-plugin/index.ts index 397717b88..fe6fd726f 100644 --- a/packages/brisa/src/utils/client-build-plugin/index.ts +++ b/packages/brisa/src/utils/client-build-plugin/index.ts @@ -13,7 +13,6 @@ import replaceExportDefault from './replace-export-default'; import processClientAst from './process-client-ast'; import getReactiveReturnStatement from './get-reactive-return-statement'; import { WEB_COMPONENT_ALTERNATIVE_REGEX, NATIVE_FOLDER } from './constants'; -import addI18nBridge from './add-i18n-bridge'; import defineCustomElementToAST from './define-custom-element-to-ast'; type ClientBuildPluginConfig = { @@ -23,12 +22,6 @@ type ClientBuildPluginConfig = { customElementSelectorToDefine?: null | string; }; -type ClientBuildPluginResult = { - code: string; - useI18n: boolean; - i18nKeys: Set; -}; - const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); const BRISA_INTERNAL_PATH = '__BRISA_CLIENT__'; const DEFAULT_CONFIG: ClientBuildPluginConfig = { @@ -42,7 +35,7 @@ export default function clientBuildPlugin( code: string, path: string, config = DEFAULT_CONFIG, -): ClientBuildPluginResult { +): string { const isInternal = path.startsWith(BRISA_INTERNAL_PATH); if ( @@ -50,20 +43,11 @@ export default function clientBuildPlugin( !isInternal && !config.forceTranspilation ) { - return { code, useI18n: false, i18nKeys: new Set() }; + return code; } const rawAst = parseCodeToAST(code); - let { useI18n, i18nKeys, ast } = processClientAst(rawAst, path); - - if (useI18n) { - ast = addI18nBridge(ast, { - usei18nKeysLogic: i18nKeys.size > 0, - i18nAdded: Boolean(config.isI18nAdded), - isTranslateCoreAdded: Boolean(config.isTranslateCoreAdded), - }); - } - + const ast = processClientAst(rawAst, path); const astWithDirectExport = transformToDirectExport(ast); const out = transformToReactiveProps(astWithDirectExport); const reactiveAst = transformToReactiveArrays(out.ast, path); @@ -77,11 +61,7 @@ export default function clientBuildPlugin( !isInternal && !config.forceTranspilation) ) { - return { - code: generateCodeFromAST(reactiveAst), - useI18n, - i18nKeys, - }; + return generateCodeFromAST(reactiveAst); } for (const { observedAttributes = new Set() } of Object.values( @@ -135,13 +115,9 @@ export default function clientBuildPlugin( // Useful for internal web components as context-provider if (isInternal) { const internalComponentName = path.split(BRISA_INTERNAL_PATH).at(-1)!; - return { - code: generateCodeFromAST( - replaceExportDefault(reactiveAst, internalComponentName), - ), - useI18n, - i18nKeys, - }; + return generateCodeFromAST( + replaceExportDefault(reactiveAst, internalComponentName), + ); } // This is used for standalone component builds (library components) @@ -152,5 +128,5 @@ export default function clientBuildPlugin( }); } - return { code: generateCodeFromAST(reactiveAst), useI18n, i18nKeys }; + return generateCodeFromAST(reactiveAst); } diff --git a/packages/brisa/src/utils/client-build-plugin/integration.test.tsx b/packages/brisa/src/utils/client-build-plugin/integration.test.tsx index 0222ca59b..46ba5213f 100644 --- a/packages/brisa/src/utils/client-build-plugin/integration.test.tsx +++ b/packages/brisa/src/utils/client-build-plugin/integration.test.tsx @@ -19,9 +19,7 @@ declare global { function defineBrisaWebComponent(code: string, path: string) { const componentName = path.split('/').pop()?.split('.')[0] as string; - const webComponent = `(() => {${normalizeHTML( - clientBuildPlugin(code, path).code, - ) + const webComponent = `(() => {${normalizeHTML(clientBuildPlugin(code, path)) .replace('import {brisaElement, _on, _off} from "brisa/client";', '') .replace('export default', 'const _Test =')}return _Test;})()`; diff --git a/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.test.ts b/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.test.ts index 5fd2c230c..0cf0b7099 100644 --- a/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.test.ts +++ b/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.test.ts @@ -1,12 +1,58 @@ import { describe, it, expect, spyOn } from 'bun:test'; +import { join } from 'node:path'; import AST from '@/utils/ast'; -import processClientAst from '.'; +import processClientAST from '.'; import { normalizeHTML, toInline } from '@/helpers'; +const brisaClient = join('brisa', 'client', 'index.js'); const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); describe('utils', () => { describe('process-client-ast', () => { + it('should remove import from "react/jsx-runtime" (some TSX -> JS transpilers like @swc add it, but then jsx-runtme is not used...)', () => { + const ast = parseCodeToAST(` + import { jsx } from 'react/jsx-runtime'; + export default function Component() { + return jsx('div', null, 'Hello World'); + } + `); + + const res = processClientAST(ast); + + expect(toInline(generateCodeFromAST(res))).toBe( + normalizeHTML(` + export default function Component() { + return jsx('div', null, 'Hello World'); + } + `), + ); + }); + + it('should remove ".css" imports and log a warning', () => { + const mockLog = spyOn(console, 'log'); + const ast = parseCodeToAST(` + import './styles.css'; + export default function Component() { + return jsx('div', null, 'Hello World'); + } + `); + + const res = processClientAST(ast); + + const logs = mockLog.mock.calls.toString(); + mockLog.mockRestore(); + + expect(toInline(generateCodeFromAST(res))).toBe( + normalizeHTML(` + export default function Component() { + return jsx('div', null, 'Hello World'); + } + `), + ); + expect(logs).toContain( + 'Add this global import into the layout or the page.', + ); + }); it('should detect i18n when is declated and used to consume the locale', () => { const ast = parseCodeToAST(` export default function Component({i18n}) { @@ -15,10 +61,24 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); + + expect(res).toContain('useI18n = true'); + expect(res).not.toContain('i18nKeys'); + }); + + it('should not detect i18n when the path is brisa client', () => { + const ast = parseCodeToAST(` + export default function Component({i18n}) { + const { locale } = i18n; + return
{locale}
+ } + `); + + const res = generateCodeFromAST(processClientAST(ast, brisaClient)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toBeEmpty(); + expect(res).not.toContain('useI18n = true'); + expect(res).not.toContain('i18nKeys'); }); it('should detect i18n when is used to consume the locale from webContext identifier', () => { @@ -29,10 +89,10 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toBeEmpty(); + expect(res).toContain('useI18n = true'); + expect(res).not.toContain('i18nKeys'); }); it('should detect i18n when is used to consume the locale from webContext identifier + destructuring', () => { @@ -43,10 +103,10 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toBeEmpty(); + expect(res).toContain('useI18n = true'); + expect(res).not.toContain('i18nKeys'); }); it('should detect i18n when is used to consume t function', () => { @@ -56,10 +116,10 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello'])); + expect(res).toContain('useI18n = true'); + expect(res).toContain('i18nKeys = ["hello"]'); }); it('should detect i18n when is used to consume t function from arrow function', () => { @@ -71,10 +131,10 @@ describe('utils', () => { export default Component; `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello'])); + expect(res).toContain('useI18n = true'); + expect(res).toContain('i18nKeys = ["hello"]'); }); it('should return all the i18n keys used in the component', () => { @@ -84,10 +144,10 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello'])); + expect(res).toContain('useI18n = true'); + expect(res).toContain('i18nKeys = ["hello"]'); }); it('should return all the i18n keys used in the component using destructuring', () => { @@ -97,10 +157,10 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello'])); + expect(res).toContain('useI18n = true'); + expect(res).toContain('i18nKeys = ["hello"]'); }); it('should return all the i18n keys used in the component using webContext identifier', () => { @@ -110,10 +170,10 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello'])); + expect(res).toContain('useI18n = true'); + expect(res).toContain('i18nKeys = ["hello"]'); }); it('should not return as i18n keys when is not using i18n', () => { @@ -125,10 +185,10 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeFalse(); - expect(res.i18nKeys).toBeEmpty(); + expect(res).not.toContain('useI18n'); + expect(res).not.toContain('i18nKeys'); }); it('should log a warning and no return i18n keys when there is no literal as first argument', () => { @@ -141,13 +201,14 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); + + expect(res).toContain('useI18n = true'); + expect(res).not.toContain('i18nKeys'); const logs = mockLog.mock.calls.toString(); mockLog.mockRestore(); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toBeEmpty(); expect(logs).toContain('Ops! Warning:'); expect(logs).toContain('Addressing Dynamic i18n Key Export Limitations'); expect(logs).toContain( @@ -164,15 +225,16 @@ describe('utils', () => { Component.i18nKeys = ["hello-world"]; `); - const res = processClientAst(ast); + const res = generateCodeFromAST(processClientAST(ast)); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello', 'hello-world'])); - expect(toInline(generateCodeFromAST(res.ast))).toBe( + expect(toInline(res)).toBe( toInline(` export default function Component({}, {i18n}) { return i18n.t("hello"); } + + window.i18nKeys = ["hello", "hello-world"]; + window.useI18n = true; `), ); }); @@ -188,18 +250,20 @@ describe('utils', () => { Component.i18nKeys = ["hello-world"]; `); - const res = processClientAst(ast); + const res = processClientAST(ast); expect(mockLog).not.toHaveBeenCalled(); mockLog.mockRestore(); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello-world'])); - expect(toInline(generateCodeFromAST(res.ast))).toBe( + + expect(toInline(generateCodeFromAST(res))).toBe( toInline(` export default function Component({}, {i18n}) { const someVar = "hello-world"; return i18n.t(someVar); } + + window.i18nKeys = ["hello-world"]; + window.useI18n = true; `), ); }); @@ -216,11 +280,9 @@ describe('utils', () => { } `); - const res = processClientAst(ast); + const res = processClientAST(ast); - expect(res.useI18n).toBeTrue(); - expect(res.i18nKeys).toEqual(new Set(['hello-world'])); - expect(toInline(generateCodeFromAST(res.ast))).toBe( + expect(toInline(generateCodeFromAST(res))).toBe( toInline(` export default function Component({}, {i18n}) { const someVar = "hello-world"; @@ -228,53 +290,10 @@ describe('utils', () => { } if (true) {} + window.i18nKeys = ["hello-world"]; + window.useI18n = true; `), ); }); - - it('should remove import from "react/jsx-runtime" (some TSX -> JS transpilers like @swc add it, but then jsx-runtme is not used...)', () => { - const ast = parseCodeToAST(` - import { jsx } from 'react/jsx-runtime'; - export default function Component() { - return jsx('div', null, 'Hello World'); - } - `); - - const res = processClientAst(ast); - - expect(toInline(generateCodeFromAST(res.ast))).toBe( - normalizeHTML(` - export default function Component() { - return jsx('div', null, 'Hello World'); - } - `), - ); - }); - - it('should remove ".css" imports and log a warning', () => { - const mockLog = spyOn(console, 'log'); - const ast = parseCodeToAST(` - import './styles.css'; - export default function Component() { - return jsx('div', null, 'Hello World'); - } - `); - - const res = processClientAst(ast); - - const logs = mockLog.mock.calls.toString(); - mockLog.mockRestore(); - - expect(toInline(generateCodeFromAST(res.ast))).toBe( - normalizeHTML(` - export default function Component() { - return jsx('div', null, 'Hello World'); - } - `), - ); - expect(logs).toContain( - 'Add this global import into the layout or the page.', - ); - }); }); }); diff --git a/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.ts b/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.ts index 996b807ce..3bd209854 100644 --- a/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.ts +++ b/packages/brisa/src/utils/client-build-plugin/process-client-ast/index.ts @@ -1,19 +1,49 @@ import type { ESTree } from 'meriyah'; +import { join } from 'node:path'; import { logWarning } from '@/utils/log/log-build'; -import AST from '@/utils/ast'; import { toInline } from '@/helpers'; +import AST from '@/utils/ast'; const { generateCodeFromAST } = AST('tsx'); +const brisaClientPath = join('brisa', 'client', 'index.js'); export default function processClientAst(ast: ESTree.Program, path = '') { let i18nKeys = new Set(); let useI18n = false; const logs: any[] = []; + const isBrisaClient = path.endsWith(brisaClientPath); let isDynamicKeysSpecified = false; const newAst = JSON.parse(JSON.stringify(ast), (key, value) => { - useI18n ||= value?.type === 'Identifier' && value?.name === 'i18n'; + useI18n ||= + !isBrisaClient && value?.type === 'Identifier' && value?.name === 'i18n'; + + if ( + value?.type === 'CallExpression' && + ((value?.callee?.type === 'Identifier' && value?.callee?.name === 't') || + (value?.callee?.property?.type === 'Identifier' && + value?.callee?.property?.name === 't')) + ) { + if (value?.arguments?.[0]?.type === 'Literal') { + i18nKeys.add(value?.arguments?.[0]?.value); + } else { + logs.push(value); + } + } + // Add dynamic keys from: MyWebComponent.i18nKeys = ['footer', /projects.*title/]; + if ( + value?.type === 'ExpressionStatement' && + value.expression.left?.property?.name === 'i18nKeys' && + value.expression?.right?.type === 'ArrayExpression' + ) { + for (const element of value.expression.right.elements ?? []) { + i18nKeys.add(element.value); + isDynamicKeysSpecified = true; + } + // Remove the expression statement + return null; + } // Remove react/jsx-runtime import, some transpilers like @swc add it, // but we are not using jsx-runtime here, we are using jsx-buildtime if ( @@ -48,35 +78,8 @@ export default function processClientAst(ast: ESTree.Program, path = '') { return null; } - if ( - value?.type === 'CallExpression' && - ((value?.callee?.type === 'Identifier' && value?.callee?.name === 't') || - (value?.callee?.property?.type === 'Identifier' && - value?.callee?.property?.name === 't')) - ) { - if (value?.arguments?.[0]?.type === 'Literal') { - i18nKeys.add(value?.arguments?.[0]?.value); - } else { - logs.push(value); - } - } - - // Add dynamic keys from: MyWebComponent.i18nKeys = ['footer', /projects.*title/]; - if ( - value?.type === 'ExpressionStatement' && - value.expression.left?.property?.name === 'i18nKeys' && - value.expression?.right?.type === 'ArrayExpression' - ) { - for (const element of value.expression.right.elements ?? []) { - i18nKeys.add(element.value); - isDynamicKeysSpecified = true; - } - // Remove the expression statement - return null; - } - - // Remove arrays with empty values - if (Array.isArray(value)) return value.filter((v) => v); + // Clean null values inside arrays + if (Array.isArray(value)) return value.filter(Boolean); return value; }); @@ -107,7 +110,65 @@ export default function processClientAst(ast: ESTree.Program, path = '') { ); } - if (!useI18n) i18nKeys = new Set(); + // This is a workaround to in a post-analysis collect all i18nKeys and useI18n from the + // entrypoint to inject the i18n bridge and clean these variables. That is, they are not + // real gobal variables, it is a communication between dependency graph for the post-analysis. + // + // It is necessary to think that each file can be connected to different entrypoints, so at + // this point we note the keys and if it uses i18n (lang or other attributes without translations). + // + // Communication variables: window.i18nKeys & window.useI18n + if (useI18n && i18nKeys.size) { + newAst.body.push({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'window', + }, + property: { + type: 'Identifier', + name: 'i18nKeys', + }, + }, + right: { + type: 'ArrayExpression', + elements: Array.from(i18nKeys).map((key) => ({ + type: 'Literal', + value: key, + })), + }, + }, + }); + } + if (useI18n) { + newAst.body.push({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'window', + }, + property: { + type: 'Identifier', + name: 'useI18n', + }, + }, + right: { + type: 'Literal', + value: useI18n, + }, + }, + }); + } - return { useI18n, i18nKeys, ast: newAst }; + return newAst; } diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts index 5ff82bdd5..df28282f3 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts @@ -77,6 +77,7 @@ async function getImports( ); if (integrationsPath) { + // TODO: cache dynamic import can improve performance? We need to try it const module = await import(integrationsPath); if (module.webContextPlugins?.length > 0) { diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 451bc877f..621444833 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -4,10 +4,13 @@ import { removeTempEntrypoints } from './fs-temp-entrypoint-manager'; import { getClientBuildDetails } from './get-client-build-details'; import type { EntryPointData, Options } from './types'; import { runBuild } from './run-build'; +import { processI18n } from './process-i18n'; // TODO: Benchmarks old vs new // TODO: Move to module (build-multi-entrypoints) + add tests // TODO: Move getClientCodeInPage to module like build-single-entrypoint + add tests +// TODO: Move compileClientCodePage from compile-files to inside this client-build folder + tests +// TODO: move add-i18n-bridge to post-build export default async function buildMultiClientEntrypoints( pages: BuildArtifact[], options: Options, @@ -25,9 +28,10 @@ export default async function buildMultiClientEntrypoints( return clientBuildDetails; } - const { success, logs, outputs, analysis } = await runBuild( + const { success, logs, outputs } = await runBuild( entrypoints, options.allWebComponents, + entrypointsData[0].useContextProvider, ); // Remove all temp files @@ -42,15 +46,13 @@ export default async function buildMultiClientEntrypoints( outputs.map(async (output, i) => { const index = entrypointsData[i].index!; const pathname = entrypoints[i]; - + clientBuildDetails[index] = { ...clientBuildDetails[index], - code: await output.text(), size: output.size, - useI18n: analysis[pathname]?.useI18n ?? false, // TODO: fix this - i18nKeys: analysis[pathname]?.i18nKeys ?? new Set(), // TODO: fix this + ...processI18n(await output.text(), pathname), }; - }) + }), ); return clientBuildDetails; diff --git a/packages/brisa/src/utils/client-build/process-i18n/index.test.ts b/packages/brisa/src/utils/client-build/process-i18n/index.test.ts new file mode 100644 index 000000000..9dee910ef --- /dev/null +++ b/packages/brisa/src/utils/client-build/process-i18n/index.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect, spyOn } from 'bun:test'; +import AST from '@/utils/ast'; +import { processI18n } from '.'; +import { normalizeHTML, toInline } from '@/helpers'; + +const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); +const out = (c: string) => + normalizeHTML(generateCodeFromAST(parseCodeToAST(c))); + +describe('utils', () => { + describe('client-build -> process-i18n', () => { + it('should return useI18n + cleanup', () => { + const code = ` + export default function Component({i18n}) { + const { locale } = i18n; + return
{locale}
+ } + + window.useI18n = true; + `; + + const res = processI18n(code, ''); + + expect(normalizeHTML(res.code)).toBe( + out(` + export default function Component({i18n}) { + const { locale } = i18n; + return
{locale}
+ } + `), + ); + expect(res.useI18n).toBeTrue(); + expect(res.i18nKeys).toBeEmpty(); + }); + + it('should return useI18n + cleanup multi useI18n (entrypoint with diferent pre-analyzed files)', () => { + const code = ` + export default function Component({i18n}) { + const { locale } = i18n; + return
{locale}
+ } + + window.useI18n = true; + window.useI18n = true; + window.useI18n = true; + window.useI18n = true; + `; + + const res = processI18n(code, ''); + + expect(normalizeHTML(res.code)).toBe( + out(` + export default function Component({i18n}) { + const { locale } = i18n; + return
{locale}
+ } + `), + ); + expect(res.useI18n).toBeTrue(); + expect(res.i18nKeys).toBeEmpty(); + }); + + it('should return useI18n and i18nKeys + cleanup', () => { + const code = ` + export default function Component({i18n}) { + const { t } = i18n; + return
{t("hello")}
+ } + + window.useI18n = true; + window.i18nKeys = ["hello"] + `; + + const res = processI18n(code, ''); + + expect(normalizeHTML(res.code)).toBe( + out(` + export default function Component({i18n}) { + const { t } = i18n; + return
{t("hello")}
+ } + `), + ); + expect(res.useI18n).toBeTrue(); + expect(res.i18nKeys).toEqual(new Set(['hello'])); + }); + + it('should return useI18n and i18nKeys + cleanup multi', () => { + const code = ` + export default function Component({i18n}) { + const { t } = i18n; + return
{t("hello")}
+ } + + window.useI18n = true; + window.i18nKeys = ["hello"] + window.useI18n = true; + window.i18nKeys = ["hello"] + `; + + const res = processI18n(code, ''); + + expect(normalizeHTML(res.code)).toBe( + out(` + export default function Component({i18n}) { + const { t } = i18n; + return
{t("hello")}
+ } + `), + ); + expect(res.useI18n).toBeTrue(); + expect(res.i18nKeys).toEqual(new Set(['hello'])); + }); + + it('should return useI18n and i18nKeys + collect and cleanup multi', () => { + const code = ` + export default function Component({i18n}) { + const { t } = i18n; + return
{t("hello")}
+ } + + window.useI18n = true; + window.i18nKeys = ["foo", "bar"] + window.i18nKeys = ["bar"] + window.i18nKeys = ["baz"] + `; + + const res = processI18n(code, ''); + + expect(normalizeHTML(res.code)).toBe( + out(` + export default function Component({i18n}) { + const { t } = i18n; + return
{t("hello")}
+ } + `), + ); + expect(res.useI18n).toBeTrue(); + expect(res.i18nKeys).toEqual(new Set(['foo', 'bar', 'baz'])); + }); + }); +}); diff --git a/packages/brisa/src/utils/client-build/process-i18n/index.ts b/packages/brisa/src/utils/client-build/process-i18n/index.ts new file mode 100644 index 000000000..df5aa73c6 --- /dev/null +++ b/packages/brisa/src/utils/client-build/process-i18n/index.ts @@ -0,0 +1,49 @@ +import AST from '@/utils/ast'; + +const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); + +export function processI18n(code: string, path: string) { + const rawAst = parseCodeToAST(code); + const i18nKeys = new Set(); + let useI18n = false; + + const ast = JSON.parse(JSON.stringify(rawAst), (key, value) => { + if ( + isWindowProperty(value, 'i18nKeys') && + value.expression?.right?.type === 'ArrayExpression' + ) { + for (const element of value.expression.right.elements ?? []) { + i18nKeys.add(element.value); + } + return null; + } + + if (isWindowProperty(value, 'useI18n')) { + useI18n = true; + return null; + } + + // Clean null values inside arrays + if (Array.isArray(value)) return value.filter(Boolean); + + return value; + }); + + // TODO + // Add the i18n Bridge to AST + // if (useI18n) { + // ast = addI18nBridge(ast, { + // usei18nKeysLogic: i18nKeys.size > 0 + // }); + // } + + return { code: generateCodeFromAST(ast), useI18n, i18nKeys }; +} + +function isWindowProperty(value: any, property: string) { + return ( + value?.type === 'ExpressionStatement' && + value.expression?.left?.object?.name === 'window' && + value.expression?.left?.property?.name === property + ); +} diff --git a/packages/brisa/src/utils/client-build/run-build/index.test.ts b/packages/brisa/src/utils/client-build/run-build/index.test.ts index 41761e35e..e57ded0c7 100644 --- a/packages/brisa/src/utils/client-build/run-build/index.test.ts +++ b/packages/brisa/src/utils/client-build/run-build/index.test.ts @@ -36,10 +36,8 @@ describe('client build -> runBuild', () => { useContextProvider: false, pagePath: 'foo', }); - const { success, logs, outputs, analysis } = await runBuild( - [entrypoint], - {}, - ); + const { success, logs, outputs } = await runBuild([entrypoint], {}); + const entrypointBuild = await outputs[0].text(); expect(success).toBe(true); expect(logs).toHaveLength(0); @@ -48,12 +46,8 @@ describe('client build -> runBuild', () => { expect(outputs[0].text()).resolves.toContain( ' defineElement("custom-counter"', ); - expect(analysis).toEqual({ - [entrypoint]: { - useI18n: false, - i18nKeys: new Set(), - }, - }); + expect(entrypointBuild).not.toContain('useI18n'); + expect(entrypointBuild).not.toContain('i18nKeys'); }); it('should run the build with multiple entrypoints', async () => { @@ -76,11 +70,14 @@ describe('client build -> runBuild', () => { pagePath: 'bar', }); - const { success, logs, outputs, analysis } = await runBuild( + const { success, logs, outputs } = await runBuild( [entrypoint1, entrypoint2], {}, ); + const entrypoint1Build = await outputs[0].text(); + const entrypoint2Build = await outputs[1].text(); + expect(success).toBe(true); expect(logs).toHaveLength(0); expect(outputs).toHaveLength(2); @@ -92,15 +89,10 @@ describe('client build -> runBuild', () => { expect(outputs[1].text()).resolves.toContain( ' defineElement("web-component"', ); - expect(analysis).toEqual({ - [entrypoint1]: { - useI18n: false, - i18nKeys: new Set(), - }, - [entrypoint2]: { - useI18n: true, - i18nKeys: new Set(['hello']), - }, - }); + + expect(entrypoint1Build).not.toContain('useI18n'); + expect(entrypoint1Build).not.toContain('i18nKeys'); + expect(entrypoint2Build).toContain('useI18n'); + expect(entrypoint2Build).toContain('i18nKeys = ["hello"]'); }); }); diff --git a/packages/brisa/src/utils/client-build/run-build/index.ts b/packages/brisa/src/utils/client-build/run-build/index.ts index 296aed554..45d18d2a5 100644 --- a/packages/brisa/src/utils/client-build/run-build/index.ts +++ b/packages/brisa/src/utils/client-build/run-build/index.ts @@ -6,35 +6,17 @@ import type { WCs } from '../types'; import { logError } from '@/utils/log/log-build'; import createContextPlugin from '@/utils/create-context/create-context-plugin'; -type DepGraph = { - source: string; - target: string; -}; - -// TODO: Adapt to both files, multi-build & single-build (for layout) -// TODO: Solve "define" for entrypoint -// ... _WEB_CONTEXT_PLUGIN_, _USE_PAGE_TRANSLATION_ -// TODO: Test and refactor all this -export async function runBuild(entrypoints: string[], webComponents: WCs) { +export async function runBuild( + entrypoints: string[], + webComponents: WCs, + useWebContextPlugins = false, +) { const { IS_PRODUCTION, SRC_DIR, CONFIG, I18N_CONFIG } = getConstants(); const envVar = getDefinedEnvVar(); const extendPlugins = CONFIG.extendPlugins ?? ((plugins) => plugins); const webComponentsPath = Object.values(webComponents); - const depGraph: DepGraph[] = []; - const entrypointSet = new Set(entrypoints); - const analysis: Record }> = - {}; - - function pathToEntrypoint(path: string) { - if (entrypointSet.has(path)) return path; - - const dependency = depGraph.find((dep) => dep.target === path); - if (!dependency) return undefined; - return pathToEntrypoint(dependency.source); - } - - const buildResult = await Bun.build({ + return await Bun.build({ entrypoints, root: SRC_DIR, format: 'iife', @@ -43,7 +25,7 @@ export async function runBuild(entrypoints: string[], webComponents: WCs) { external: CONFIG.external, define: { __DEV__: (!IS_PRODUCTION).toString(), - __WEB_CONTEXT_PLUGINS__: 'false', // useWebContextPlugins.toString(), (TODO) + __WEB_CONTEXT_PLUGINS__: useWebContextPlugins.toString(), __BASE_PATH__: JSON.stringify(CONFIG.basePath ?? ''), __ASSET_PREFIX__: JSON.stringify(CONFIG.assetPrefix ?? ''), __TRAILING_SLASH__: Boolean(CONFIG.trailingSlash).toString(), @@ -67,42 +49,11 @@ export async function runBuild(entrypoints: string[], webComponents: WCs) { .replace(/\\/g, '\\\\')})$`.replace(/\//g, '[\\\\/]'), ); - // TODO: Simplify this code after this Bun feature: - // https://github.com/oven-sh/bun/issues/15003 - build.onResolve({ filter }, (args) => { - if (!args.importer) return; - - depGraph.push({ source: args.importer, target: args.path }); - - return null; - }); - build.onLoad({ filter }, async ({ path, loader }) => { let code = await Bun.file(path).text(); try { - const entrypoint = pathToEntrypoint(path); - const currentAnalysis = - entrypoint && analysis[entrypoint] - ? analysis[entrypoint] - : { - useI18n: false, - i18nKeys: new Set(), - }; - - if (entrypoint && !analysis[entrypoint]) { - analysis[entrypoint] = currentAnalysis; - } - - const res = clientBuildPlugin(code, path, { - isI18nAdded: currentAnalysis.useI18n, - isTranslateCoreAdded: currentAnalysis.i18nKeys.size > 0, - }); - code = res.code; - currentAnalysis.useI18n ||= res.useI18n; - res.i18nKeys.forEach((key) => - currentAnalysis.i18nKeys.add(key), - ); + code = clientBuildPlugin(code, path); } catch (error: any) { logError({ messages: [ @@ -128,9 +79,4 @@ export async function runBuild(entrypoints: string[], webComponents: WCs) { }, ), }); - - return { - ...buildResult, - analysis, - }; } diff --git a/packages/brisa/src/utils/compile-wc/index.ts b/packages/brisa/src/utils/compile-wc/index.ts index 5b1ceaa46..71e4220d5 100644 --- a/packages/brisa/src/utils/compile-wc/index.ts +++ b/packages/brisa/src/utils/compile-wc/index.ts @@ -8,5 +8,5 @@ import clientBuildPlugin from '../client-build-plugin'; * Docs: https:/brisa.build/api-reference/compiler-apis/compileWC */ export default function compileWC(code: string) { - return clientBuildPlugin(code, 'web-component.tsx').code; + return clientBuildPlugin(code, 'web-component.tsx'); } diff --git a/packages/brisa/src/utils/context-provider/inject-client.ts b/packages/brisa/src/utils/context-provider/inject-client.ts index 4f58cbc1d..1263c67d4 100644 --- a/packages/brisa/src/utils/context-provider/inject-client.ts +++ b/packages/brisa/src/utils/context-provider/inject-client.ts @@ -21,7 +21,7 @@ export async function injectClientContextProviderCode() { // https://github.com/oven-sh/bun/issues/7611 await Bun.readableStreamToText(Bun.file(path).stream()), internalComponentId, - ).code, + ), loader, })); }, diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts index f555c63ab..91a7d3233 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts @@ -302,7 +302,7 @@ describe('utils', () => { pageWebComponents, integrationsPath, }); - expect(output!.code).toContain('window._P='); + expect(output!.code).toContain('window._P ='); }); it('should add the integrations with emoji-picker as direct import', async () => { @@ -341,9 +341,9 @@ describe('utils', () => { }, integrationsPath, }); - const contentOfLib = '()=>`has ${window._P.length} web context plugin`'; + const contentOfLib = '() => `has ${window._P.length} web context plugin`'; - expect(output!.code).toContain('window._P='); + expect(output!.code).toContain('window._P ='); expect(output!.code).toContain(contentOfLib); GlobalRegistrator.register(); diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index 6f3d39567..f68465813 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -5,6 +5,7 @@ import { writeTempEntrypoint, } from '../client-build/fs-temp-entrypoint-manager'; import { runBuild } from '../client-build/run-build'; +import { processI18n } from '../client-build/process-i18n'; type TransformOptions = { webComponentsList: Record; @@ -66,7 +67,6 @@ export async function transformToWebComponents({ integrationsPath, pagePath, }: TransformOptions) { - // TODO: Resolve useWebContextPlugins inside build for multi and single entrypoint const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ webComponentsList, useContextProvider, @@ -74,9 +74,10 @@ export async function transformToWebComponents({ pagePath, }); - const { success, logs, outputs, analysis } = await runBuild( + const { success, logs, outputs } = await runBuild( [entrypoint], webComponentsList, + useWebContextPlugins, ); await removeTempEntrypoint(entrypoint); @@ -87,9 +88,7 @@ export async function transformToWebComponents({ } return { - code: await outputs[0].text(), size: outputs[0].size, - useI18n: analysis[entrypoint]?.useI18n, - i18nKeys: analysis[entrypoint]?.i18nKeys, + ...processI18n(await outputs[0].text(), pagePath), }; } From de27446940ead5cfbcd5075ef01ee8ede8b55902 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Wed, 27 Nov 2024 01:00:31 +0100 Subject: [PATCH 28/39] feat: adapt i18n bridge to new build --- .../add-i18n-bridge/index.test.ts | 483 ------------------ .../add-i18n-bridge/index.ts | 69 --- .../brisa/src/utils/client-build/index.ts | 3 +- .../client-build/process-i18n/index.test.ts | 49 +- .../utils/client-build/process-i18n/index.ts | 59 ++- .../process-i18n/inject-bridge.ts | 81 +++ .../utils/get-client-code-in-page/index.ts | 2 +- 7 files changed, 169 insertions(+), 577 deletions(-) delete mode 100644 packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.test.ts delete mode 100644 packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.ts create mode 100644 packages/brisa/src/utils/client-build/process-i18n/inject-bridge.ts diff --git a/packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.test.ts b/packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.test.ts deleted file mode 100644 index e5f4559cc..000000000 --- a/packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.test.ts +++ /dev/null @@ -1,483 +0,0 @@ -import AST from '@/utils/ast'; -import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test'; -import addI18nBridge from '.'; -import { normalizeHTML } from '@/helpers'; -import { GlobalRegistrator } from '@happy-dom/global-registrator'; -import type { I18nConfig } from '@/types'; - -const I18N_CONFIG = { - defaultLocale: 'en', - locales: ['en', 'pt'], - messages: { - en: { - hello: 'Hello {{name}}', - }, - pt: { - hello: 'Olá {{name}}', - }, - }, - pages: {}, -}; - -mock.module('@/constants', () => ({ - default: { I18N_CONFIG }, -})); - -const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); -const emptyAst = parseCodeToAST(''); - -describe('utils', () => { - beforeEach(() => { - mock.module('@/constants', () => ({ - default: { I18N_CONFIG }, - })); - }); - describe('client-build-plugin', () => { - describe('add-i18n-bridge', () => { - it('should add the code at the bottom', () => { - const code = ` - import foo from 'bar'; - import baz from 'qux'; - - const a = 1; - `; - const outputAst = addI18nBridge(parseCodeToAST(code), { - usei18nKeysLogic: false, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - const outputCode = normalizeHTML(generateCodeFromAST(outputAst)); - const expectedCode = normalizeHTML(` - import foo from 'bar'; - import baz from 'qux'; - - const a = 1; - - const i18nConfig = { - defaultLocale: "en", - locales: ["en", "pt"] - }; - - window.i18n = { - ...i18nConfig, - get locale() {return document.documentElement.lang;} - }; - `); - expect(outputCode).toBe(expectedCode); - }); - - it('should add only the i18n keys logic at the botton if "i18nAdded" is true', () => { - const code = ` - import foo from 'bar'; - import baz from 'qux'; - - const a = 1; - `; - const outputAst = addI18nBridge(parseCodeToAST(code), { - usei18nKeysLogic: true, - i18nAdded: true, - isTranslateCoreAdded: false, - }); - const outputCode = normalizeHTML(generateCodeFromAST(outputAst)); - const expectedCode = normalizeHTML(` - import {translateCore} from "brisa"; - import foo from 'bar'; - import baz from 'qux'; - - const a = 1; - - Object.assign(window.i18n, { - get t() {return translateCore(this.locale, {...{defaultLocale: "en",locales: ["en", "pt"]},messages: this.messages});}, - get messages() {return {[this.locale]: window.i18nMessages};}, - overrideMessages(callback) { - const p = callback(window.i18nMessages); - const a = m => Object.assign(window.i18nMessages, m); - return p.then?.(a) ?? a(p); - } - }); - `); - expect(outputCode).toBe(expectedCode); - }); - - it('should add the code at the bottom with i18n keys logic and the import on top', () => { - const code = ` - import foo from 'bar'; - import baz from 'qux'; - - const a = 1; - `; - const outputAst = addI18nBridge(parseCodeToAST(code), { - usei18nKeysLogic: true, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - const outputCode = normalizeHTML(generateCodeFromAST(outputAst)); - const expectedCode = normalizeHTML(` - import {translateCore} from "brisa"; - import foo from 'bar'; - import baz from 'qux'; - - const a = 1; - - const i18nConfig = { - defaultLocale: "en", - locales: ["en", "pt"] - }; - - window.i18n = { - ...i18nConfig, - get locale() {return document.documentElement.lang;}, - get t() {return translateCore(this.locale, {...i18nConfig,messages: this.messages});}, - get messages() {return {[this.locale]: window.i18nMessages};}, - overrideMessages(callback) { - const p = callback(window.i18nMessages); - const a = m => Object.assign(window.i18nMessages, m); - return p.then?.(a) ?? a(p); - } - }; - `); - expect(outputCode).toBe(expectedCode); - }); - - it('should add work with empty code', () => { - const outputAst = addI18nBridge(emptyAst, { - usei18nKeysLogic: false, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - const outputCode = normalizeHTML(generateCodeFromAST(outputAst)); - const expectedCode = normalizeHTML(` - const i18nConfig = { - defaultLocale: "en", - locales: ["en", "pt"] - }; - - window.i18n = { - ...i18nConfig, - get locale() {return document.documentElement.lang;} - }; - `); - expect(outputCode).toBe(expectedCode); - }); - - it('should work with empty code with i18n keys logic', () => { - const outputAst = addI18nBridge(emptyAst, { - usei18nKeysLogic: true, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - const outputCode = normalizeHTML(generateCodeFromAST(outputAst)); - const expectedCode = normalizeHTML(` - import {translateCore} from "brisa"; - - const i18nConfig = { - defaultLocale: "en", - locales: ["en", "pt"] - }; - - window.i18n = { - ...i18nConfig, - get locale() {return document.documentElement.lang;}, - get t() {return translateCore(this.locale, {...i18nConfig,messages: this.messages});}, - get messages() {return {[this.locale]: window.i18nMessages};}, - overrideMessages(callback) { - const p = callback(window.i18nMessages); - const a = m => Object.assign(window.i18nMessages, m); - return p.then?.(a) ?? a(p); - } - }; - `); - expect(outputCode).toBe(expectedCode); - }); - }); - describe('add-i18n-bridge functionality', () => { - beforeEach(() => GlobalRegistrator.register()); - afterEach(() => GlobalRegistrator.unregister()); - - it('should work the window.i18n without translate code', async () => { - const ast = addI18nBridge(emptyAst, { - usei18nKeysLogic: false, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - const output = generateCodeFromAST(ast); - const script = document.createElement('script'); - script.innerHTML = output; - document.documentElement.lang = 'pt'; - document.body.appendChild(script); - - expect(window.i18n.locale).toBe('pt'); - }); - - it('should work the window.i18n with translate code', async () => { - const ast = addI18nBridge(emptyAst, { - usei18nKeysLogic: true, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - let output = generateCodeFromAST(ast); - - output = output.replace( - normalizeHTML("import {translateCore} from 'brisa';"), - 'const translateCore = () => (k) => "Olá John";', - ); - - window.i18nMessages = I18N_CONFIG.messages['pt']; - - const script = document.createElement('script'); - script.innerHTML = output; - document.documentElement.lang = 'pt'; - document.body.appendChild(script); - - expect(window.i18n.locale).toBe('pt'); - expect(window.i18n.t('hello', { name: 'John' })).toBe('Olá John'); - }); - - it('should work the window.i18n with translate code in separate steps', async () => { - const ast1 = addI18nBridge(emptyAst, { - usei18nKeysLogic: false, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - const ast2 = addI18nBridge(emptyAst, { - usei18nKeysLogic: true, - i18nAdded: true, - isTranslateCoreAdded: false, - }); - - let output = generateCodeFromAST(ast1) + generateCodeFromAST(ast2); - - output = output.replace( - normalizeHTML("import {translateCore} from 'brisa';"), - 'const translateCore = () => (k) => "Olá John";', - ); - - window.i18nMessages = I18N_CONFIG.messages['pt']; - - const script = document.createElement('script'); - script.innerHTML = output; - document.documentElement.lang = 'pt'; - document.body.appendChild(script); - - expect(window.i18n.locale).toBe('pt'); - expect(window.i18n.t('hello', { name: 'John' })).toBe('Olá John'); - }); - - it('should override messages using i18n.overrideMessages util', () => { - const ast = addI18nBridge(emptyAst, { - usei18nKeysLogic: true, - i18nAdded: false, - isTranslateCoreAdded: true, - }); - let output = generateCodeFromAST(ast); - - output = output.replace( - normalizeHTML("import {translateCore} from 'brisa';"), - "const translateCore = () => (k) => window.i18nMessages[k].replace('{{name}}', 'John');", - ); - - window.i18nMessages = structuredClone(I18N_CONFIG.messages['pt']); - - const script = document.createElement('script'); - script.innerHTML = output; - document.documentElement.lang = 'pt'; - document.body.appendChild(script); - - expect(window.i18n.locale).toBe('pt'); - expect(window.i18n.t('hello', { name: 'John' })).toBe('Olá John'); - - window.i18n.overrideMessages((messages: Record) => ({ - ...messages, - hello: 'Olááá {{name}}', - })); - - expect(window.i18n.messages).toEqual({ pt: window.i18nMessages }); - expect(window.i18nMessages).toEqual({ hello: 'Olááá {{name}}' }); - expect(window.i18n.t('hello', { name: 'John' })).toBe('Olááá John'); - }); - - it('should override messages using ASYNC i18n.overrideMessages util', async () => { - const ast = addI18nBridge(emptyAst, { - usei18nKeysLogic: true, - i18nAdded: false, - isTranslateCoreAdded: true, - }); - let output = generateCodeFromAST(ast); - - output = output.replace( - normalizeHTML("import {translateCore} from 'brisa';"), - "const translateCore = () => (k) => window.i18nMessages[k].replace('{{name}}', 'John');", - ); - - window.i18nMessages = structuredClone(I18N_CONFIG.messages['pt']); - - const script = document.createElement('script'); - script.innerHTML = output; - document.documentElement.lang = 'pt'; - document.body.appendChild(script); - - expect(window.i18n.locale).toBe('pt'); - expect(window.i18n.t('hello', { name: 'John' })).toBe('Olá John'); - - await window.i18n.overrideMessages( - async (messages: Record) => ({ - ...messages, - hello: 'Olááá {{name}}', - }), - ); - - expect(window.i18n.messages).toEqual({ pt: window.i18nMessages }); - expect(window.i18nMessages).toEqual({ hello: 'Olááá {{name}}' }); - expect(window.i18n.t('hello', { name: 'John' })).toBe('Olááá John'); - }); - - it('should import the i18n pages when config.transferToClient is true', () => { - mock.module('@/constants', () => ({ - default: { - I18N_CONFIG: { - ...I18N_CONFIG, - pages: { - config: { - transferToClient: true, - }, - '/about-us': { - en: '/about-us/', - es: '/sobre-nosotros/', - }, - '/user/[username]': { - en: '/user/[username]', - es: '/usuario/[username]', - }, - '/somepage': { - en: '/somepage', - es: '/alguna-pagina', - }, - }, - } as I18nConfig, - }, - })); - - const ast = addI18nBridge(emptyAst, { - usei18nKeysLogic: false, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - - const output = generateCodeFromAST(ast); - const script = document.createElement('script'); - - script.innerHTML = output; - - document.body.appendChild(script); - - expect(window.i18n.pages).toEqual({ - '/about-us': { - en: '/about-us/', - es: '/sobre-nosotros/', - }, - '/user/[username]': { - en: '/user/[username]', - es: '/usuario/[username]', - }, - '/somepage': { - en: '/somepage', - es: '/alguna-pagina', - }, - }); - }); - - it('should import the i18n pages when config.transferToClient is an array', () => { - mock.module('@/constants', () => ({ - default: { - I18N_CONFIG: { - ...I18N_CONFIG, - pages: { - config: { - transferToClient: ['/about-us', '/user/[username]'], - }, - '/about-us': { - en: '/about-us/', - es: '/sobre-nosotros/', - }, - '/user/[username]': { - en: '/user/[username]', - es: '/usuario/[username]', - }, - '/somepage': { - en: '/somepage', - es: '/alguna-pagina', - }, - }, - } as I18nConfig, - }, - })); - - const ast = addI18nBridge(emptyAst, { - usei18nKeysLogic: false, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - - const output = generateCodeFromAST(ast); - const script = document.createElement('script'); - - script.innerHTML = output; - - document.body.appendChild(script); - - expect(window.i18n.pages).toEqual({ - '/about-us': { - en: '/about-us/', - es: '/sobre-nosotros/', - }, - '/user/[username]': { - en: '/user/[username]', - es: '/usuario/[username]', - }, - }); - }); - - it('should add interpolation.format function on the client-side', () => { - mock.module('@/constants', () => ({ - default: { - I18N_CONFIG: { - ...I18N_CONFIG, - interpolation: { - format: (value, formatName, lang) => { - if (formatName === 'uppercase') { - return (value as string).toUpperCase(); - } - if (formatName === 'lang') return lang; - return value; - }, - }, - } as I18nConfig, - }, - })); - - const ast = addI18nBridge(emptyAst, { - usei18nKeysLogic: true, - i18nAdded: false, - isTranslateCoreAdded: false, - }); - - const output = generateCodeFromAST(ast); - expect(normalizeHTML(output)).toContain( - normalizeHTML(`return translateCore(this.locale, { - ...i18nConfig, - messages: this.messages, - interpolation: { - ...i18nConfig.interpolation, - format: (value, formatName, lang) => { - if (formatName === "uppercase") return value.toUpperCase(); - if (formatName === "lang") return lang; - return value; - } - } - } - );`), - ); - }); - }); - }); -}); diff --git a/packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.ts b/packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.ts deleted file mode 100644 index 054401de5..000000000 --- a/packages/brisa/src/utils/client-build-plugin/add-i18n-bridge/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import constants from '@/constants'; -import AST from '@/utils/ast'; -import { TRANSLATE_CORE_IMPORT } from '@/utils/client-build-plugin/constants'; -import transferTranslatedPagePaths from '@/utils/transfer-translated-page-paths'; -import type { ESTree } from 'meriyah'; - -const { parseCodeToAST } = AST('tsx'); - -type I18nBridgeConfig = { - usei18nKeysLogic: boolean; - isTranslateCoreAdded: boolean; - i18nAdded: boolean; -}; - -const i18nKeysLogic = (configText = 'i18nConfig') => { - const formatters = - typeof constants.I18N_CONFIG?.interpolation?.format === 'function' - ? `interpolation: {...i18nConfig.interpolation, format:${constants.I18N_CONFIG.interpolation?.format.toString()}},` - : ''; - - return ` - get t() { - return translateCore(this.locale, { ...${configText}, messages: this.messages, ${formatters} }); - }, - get messages() { return {[this.locale]: window.i18nMessages } }, - overrideMessages(callback) { - const p = callback(window.i18nMessages); - const a = m => Object.assign(window.i18nMessages, m); - return p.then?.(a) ?? a(p); - } -`; -}; - -export default function addI18nBridge( - ast: ESTree.Program, - { usei18nKeysLogic, i18nAdded, isTranslateCoreAdded }: I18nBridgeConfig, -) { - if (i18nAdded && isTranslateCoreAdded) return ast; - - const i18nConfig = JSON.stringify({ - ...constants.I18N_CONFIG, - messages: undefined, - pages: transferTranslatedPagePaths(constants.I18N_CONFIG?.pages), - }); - let body = ast.body; - - const bridgeAst = parseCodeToAST(` - const i18nConfig = ${i18nConfig}; - - window.i18n = { - ...i18nConfig, - get locale(){ return document.documentElement.lang }, - ${usei18nKeysLogic ? i18nKeysLogic() : ''} - } - `); - - if (usei18nKeysLogic && i18nAdded && !isTranslateCoreAdded) { - const newAst = parseCodeToAST( - `Object.assign(window.i18n, {${i18nKeysLogic(i18nConfig)}})`, - ); - body = [TRANSLATE_CORE_IMPORT, ...ast.body, ...newAst.body]; - } else if (usei18nKeysLogic) { - body = [TRANSLATE_CORE_IMPORT, ...ast.body, ...bridgeAst.body]; - } else { - body = [...ast.body, ...bridgeAst.body]; - } - - return { ...ast, body }; -} diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 621444833..709dea894 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -45,12 +45,11 @@ export default async function buildMultiClientEntrypoints( await Promise.all( outputs.map(async (output, i) => { const index = entrypointsData[i].index!; - const pathname = entrypoints[i]; clientBuildDetails[index] = { ...clientBuildDetails[index], size: output.size, - ...processI18n(await output.text(), pathname), + ...processI18n(await output.text()), }; }), ); diff --git a/packages/brisa/src/utils/client-build/process-i18n/index.test.ts b/packages/brisa/src/utils/client-build/process-i18n/index.test.ts index 9dee910ef..77ccc7c74 100644 --- a/packages/brisa/src/utils/client-build/process-i18n/index.test.ts +++ b/packages/brisa/src/utils/client-build/process-i18n/index.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, spyOn } from 'bun:test'; +import { describe, it, expect } from 'bun:test'; import AST from '@/utils/ast'; import { processI18n } from '.'; -import { normalizeHTML, toInline } from '@/helpers'; +import { normalizeHTML } from '@/helpers'; const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); const out = (c: string) => @@ -19,9 +19,10 @@ describe('utils', () => { window.useI18n = true; `; - const res = processI18n(code, ''); + const res = processI18n(code); + const resCode = normalizeHTML(res.code); - expect(normalizeHTML(res.code)).toBe( + expect(resCode).toEndWith( out(` export default function Component({i18n}) { const { locale } = i18n; @@ -31,6 +32,10 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toBeEmpty(); + + // Bridge without keys + expect(resCode).toContain('window.i18n'); + expect(resCode).not.toContain('messages()'); }); it('should return useI18n + cleanup multi useI18n (entrypoint with diferent pre-analyzed files)', () => { @@ -46,9 +51,10 @@ describe('utils', () => { window.useI18n = true; `; - const res = processI18n(code, ''); + const res = processI18n(code); + const resCode = normalizeHTML(res.code); - expect(normalizeHTML(res.code)).toBe( + expect(resCode).toEndWith( out(` export default function Component({i18n}) { const { locale } = i18n; @@ -58,6 +64,10 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toBeEmpty(); + + // Bridge without keys + expect(resCode).toContain('window.i18n'); + expect(resCode).not.toContain('messages()'); }); it('should return useI18n and i18nKeys + cleanup', () => { @@ -71,9 +81,10 @@ describe('utils', () => { window.i18nKeys = ["hello"] `; - const res = processI18n(code, ''); + const res = processI18n(code); + const resCode = normalizeHTML(res.code); - expect(normalizeHTML(res.code)).toBe( + expect(resCode).toEndWith( out(` export default function Component({i18n}) { const { t } = i18n; @@ -83,6 +94,10 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toEqual(new Set(['hello'])); + + // Bridge with keys + expect(resCode).toContain('window.i18n'); + expect(resCode).toContain('messages()'); }); it('should return useI18n and i18nKeys + cleanup multi', () => { @@ -98,9 +113,10 @@ describe('utils', () => { window.i18nKeys = ["hello"] `; - const res = processI18n(code, ''); + const res = processI18n(code); + const resCode = normalizeHTML(res.code); - expect(normalizeHTML(res.code)).toBe( + expect(resCode).toEndWith( out(` export default function Component({i18n}) { const { t } = i18n; @@ -110,6 +126,10 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toEqual(new Set(['hello'])); + + // Bridge with keys + expect(resCode).toContain('window.i18n'); + expect(resCode).toContain('messages()'); }); it('should return useI18n and i18nKeys + collect and cleanup multi', () => { @@ -125,9 +145,10 @@ describe('utils', () => { window.i18nKeys = ["baz"] `; - const res = processI18n(code, ''); + const res = processI18n(code); + const resCode = normalizeHTML(res.code); - expect(normalizeHTML(res.code)).toBe( + expect(resCode).toEndWith( out(` export default function Component({i18n}) { const { t } = i18n; @@ -137,6 +158,10 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toEqual(new Set(['foo', 'bar', 'baz'])); + + // Bridge with keys + expect(resCode).toContain('window.i18n'); + expect(resCode).toContain('messages()'); }); }); }); diff --git a/packages/brisa/src/utils/client-build/process-i18n/index.ts b/packages/brisa/src/utils/client-build/process-i18n/index.ts index df5aa73c6..0128dd28e 100644 --- a/packages/brisa/src/utils/client-build/process-i18n/index.ts +++ b/packages/brisa/src/utils/client-build/process-i18n/index.ts @@ -1,8 +1,18 @@ import AST from '@/utils/ast'; +import { build } from './inject-bridge' with { type: 'macro' }; +import { getConstants } from '@/constants'; +import transferTranslatedPagePaths from '@/utils/transfer-translated-page-paths'; +import type { ESTree } from 'meriyah'; const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); +const bridgeWithKeys = await build({ usei18nKeysLogic: true }); +const bridgeWithoutKeys = await build({ usei18nKeysLogic: false }); +const bridgeWithKeysAndFormatter = await build({ + usei18nKeysLogic: true, + useFormatter: true, +}); -export function processI18n(code: string, path: string) { +export function processI18n(code: string) { const rawAst = parseCodeToAST(code); const i18nKeys = new Set(); let useI18n = false; @@ -29,15 +39,11 @@ export function processI18n(code: string, path: string) { return value; }); - // TODO - // Add the i18n Bridge to AST - // if (useI18n) { - // ast = addI18nBridge(ast, { - // usei18nKeysLogic: i18nKeys.size > 0 - // }); - // } - - return { code: generateCodeFromAST(ast), useI18n, i18nKeys }; + return { + code: useI18n ? generateCodeFromAST(astWithBridge(ast, i18nKeys)) : code, + useI18n, + i18nKeys, + }; } function isWindowProperty(value: any, property: string) { @@ -47,3 +53,36 @@ function isWindowProperty(value: any, property: string) { value.expression?.left?.property?.name === property ); } + +function astWithBridge(ast: ESTree.Program, i18nKeys: Set) { + const { I18N_CONFIG } = getConstants(); + const usei18nKeysLogic = i18nKeys.size > 0; + const i18nConfig = JSON.stringify({ + ...I18N_CONFIG, + messages: undefined, + pages: transferTranslatedPagePaths(I18N_CONFIG?.pages), + }); + const formatterString = + typeof I18N_CONFIG?.interpolation?.format === 'function' + ? I18N_CONFIG.interpolation?.format.toString() + : ''; + + const bridge = + usei18nKeysLogic && formatterString + ? bridgeWithKeysAndFormatter + : usei18nKeysLogic + ? bridgeWithKeys + : bridgeWithoutKeys; + + // Note: It's important to run on the top of the AST, this way then the + // brisaElement will be able to use window.i18n + ast.body.unshift( + ...parseCodeToAST( + bridge + .replaceAll('__CONFIG__', i18nConfig) + .replaceAll('__FORMATTER__', formatterString), + ).body, + ); + + return ast; +} diff --git a/packages/brisa/src/utils/client-build/process-i18n/inject-bridge.ts b/packages/brisa/src/utils/client-build/process-i18n/inject-bridge.ts new file mode 100644 index 000000000..547dbd991 --- /dev/null +++ b/packages/brisa/src/utils/client-build/process-i18n/inject-bridge.ts @@ -0,0 +1,81 @@ +import { logBuildError } from '@/utils/log/log-build'; +import { join, resolve } from 'node:path'; + +type I18nBridgeConfig = { + usei18nKeysLogic?: boolean; + useFormatter?: boolean; +}; + +const translateCoreFile = resolve( + import.meta.dirname, + join('..', '..', 'translate-core', 'index.ts'), +); + +export async function build( + { usei18nKeysLogic = false, useFormatter = false }: I18nBridgeConfig = { + usei18nKeysLogic: false, + useFormatter: false, + }, +) { + const { success, logs, outputs } = await Bun.build({ + entrypoints: [translateCoreFile], + target: 'browser', + root: import.meta.dirname, + minify: true, + format: 'iife', + plugins: [ + { + name: 'i18n-bridge', + setup(build) { + const filter = /.*/; + + build.onLoad({ filter }, async ({ path, loader }) => { + const contents = ` + ${ + usei18nKeysLogic + ? // TODO: use (path).text() when Bun fix this issue: + // https://github.com/oven-sh/bun/issues/7611 + await Bun.readableStreamToText(Bun.file(path).stream()) + : '' + } + + const i18nConfig = __CONFIG__; + + window.i18n = { + ...i18nConfig, + get locale(){ return document.documentElement.lang }, + ${usei18nKeysLogic ? i18nKeysLogic(useFormatter) : ''} + } + `; + + return { contents, loader }; + }); + }, + }, + ], + }); + + if (!success) { + logBuildError('Failed to integrate i18n core', logs); + } + + return (await outputs?.[0]?.text?.()) ?? ''; +} + +function i18nKeysLogic(useFormatter: boolean) { + const formatters = useFormatter + ? `interpolation: {...i18nConfig.interpolation, format:__FORMATTER__},` + : ''; + + return ` + get t() { + return translateCore(this.locale, { ...i18nConfig, messages: this.messages, ${formatters} }); + }, + get messages() { return {[this.locale]: window.i18nMessages } }, + overrideMessages(callback) { + const p = callback(window.i18nMessages); + const a = m => Object.assign(window.i18nMessages, m); + return p.then?.(a) ?? a(p); + } + `; +} diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index f68465813..dbe9fdef4 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -89,6 +89,6 @@ export async function transformToWebComponents({ return { size: outputs[0].size, - ...processI18n(await outputs[0].text(), pagePath), + ...processI18n(await outputs[0].text()), }; } From 2526d9d56aadf579df01124fabaa07934f9f7a51 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 13:09:15 +0100 Subject: [PATCH 29/39] fix: return i18n size in build-time --- packages/brisa/src/utils/ast/index.ts | 15 +++++++++++---- packages/brisa/src/utils/client-build/index.ts | 1 - .../client-build/process-i18n/index.test.ts | 9 +++++++-- .../src/utils/client-build/process-i18n/index.ts | 11 +++++++---- .../brisa/src/utils/compile-files/index.test.ts | 2 +- .../utils/get-client-code-in-page/index.test.ts | 16 ++++++++-------- .../src/utils/get-client-code-in-page/index.ts | 5 +---- 7 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/brisa/src/utils/ast/index.ts b/packages/brisa/src/utils/ast/index.ts index 193df1328..7c790fcdb 100644 --- a/packages/brisa/src/utils/ast/index.ts +++ b/packages/brisa/src/utils/ast/index.ts @@ -4,10 +4,14 @@ import { type ESTree, parseScript } from 'meriyah'; import { logError } from '../log/log-build'; export default function AST(loader: JavaScriptLoader = 'tsx') { - const transpiler = - typeof Bun !== 'undefined' - ? new Bun.Transpiler({ loader }) - : { transformSync: (code: string) => code }; + const isBun = typeof Bun !== 'undefined'; + const defaultTranspiler = { transformSync: (code: string) => code }; + + const minifier = isBun + ? new Bun.Transpiler({ loader, minifyWhitespace: true }) + : defaultTranspiler; + + const transpiler = isBun ? new Bun.Transpiler({ loader }) : defaultTranspiler; return { parseCodeToAST(code: string): ESTree.Program { @@ -28,5 +32,8 @@ export default function AST(loader: JavaScriptLoader = 'tsx') { generateCodeFromAST(ast: ESTree.Program) { return generate(ast, { indent: ' ' }); }, + minify(code: string) { + return minifier.transformSync(code); + }, }; } diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts index 709dea894..72d58596d 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/index.ts @@ -48,7 +48,6 @@ export default async function buildMultiClientEntrypoints( clientBuildDetails[index] = { ...clientBuildDetails[index], - size: output.size, ...processI18n(await output.text()), }; }), diff --git a/packages/brisa/src/utils/client-build/process-i18n/index.test.ts b/packages/brisa/src/utils/client-build/process-i18n/index.test.ts index 77ccc7c74..8fb95e52b 100644 --- a/packages/brisa/src/utils/client-build/process-i18n/index.test.ts +++ b/packages/brisa/src/utils/client-build/process-i18n/index.test.ts @@ -3,9 +3,9 @@ import AST from '@/utils/ast'; import { processI18n } from '.'; import { normalizeHTML } from '@/helpers'; -const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); +const { parseCodeToAST, generateCodeFromAST, minify } = AST('tsx'); const out = (c: string) => - normalizeHTML(generateCodeFromAST(parseCodeToAST(c))); + minify(normalizeHTML(generateCodeFromAST(parseCodeToAST(c)))); describe('utils', () => { describe('client-build -> process-i18n', () => { @@ -32,6 +32,7 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toBeEmpty(); + expect(res.size).toBe(res.code.length); // Bridge without keys expect(resCode).toContain('window.i18n'); @@ -64,6 +65,7 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toBeEmpty(); + expect(res.size).toBe(res.code.length); // Bridge without keys expect(resCode).toContain('window.i18n'); @@ -94,6 +96,7 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toEqual(new Set(['hello'])); + expect(res.size).toBe(res.code.length); // Bridge with keys expect(resCode).toContain('window.i18n'); @@ -126,6 +129,7 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toEqual(new Set(['hello'])); + expect(res.size).toBe(res.code.length); // Bridge with keys expect(resCode).toContain('window.i18n'); @@ -158,6 +162,7 @@ describe('utils', () => { ); expect(res.useI18n).toBeTrue(); expect(res.i18nKeys).toEqual(new Set(['foo', 'bar', 'baz'])); + expect(res.size).toBe(res.code.length); // Bridge with keys expect(resCode).toContain('window.i18n'); diff --git a/packages/brisa/src/utils/client-build/process-i18n/index.ts b/packages/brisa/src/utils/client-build/process-i18n/index.ts index 0128dd28e..ecebe2b57 100644 --- a/packages/brisa/src/utils/client-build/process-i18n/index.ts +++ b/packages/brisa/src/utils/client-build/process-i18n/index.ts @@ -4,7 +4,7 @@ import { getConstants } from '@/constants'; import transferTranslatedPagePaths from '@/utils/transfer-translated-page-paths'; import type { ESTree } from 'meriyah'; -const { parseCodeToAST, generateCodeFromAST } = AST('tsx'); +const { parseCodeToAST, generateCodeFromAST, minify } = AST('tsx'); const bridgeWithKeys = await build({ usei18nKeysLogic: true }); const bridgeWithoutKeys = await build({ usei18nKeysLogic: false }); const bridgeWithKeysAndFormatter = await build({ @@ -39,10 +39,13 @@ export function processI18n(code: string) { return value; }); + const newCode = useI18n ? astToI18nCode(ast, i18nKeys) : code; + return { - code: useI18n ? generateCodeFromAST(astWithBridge(ast, i18nKeys)) : code, + code: newCode, useI18n, i18nKeys, + size: newCode.length, }; } @@ -54,7 +57,7 @@ function isWindowProperty(value: any, property: string) { ); } -function astWithBridge(ast: ESTree.Program, i18nKeys: Set) { +function astToI18nCode(ast: ESTree.Program, i18nKeys: Set) { const { I18N_CONFIG } = getConstants(); const usei18nKeysLogic = i18nKeys.size > 0; const i18nConfig = JSON.stringify({ @@ -84,5 +87,5 @@ function astWithBridge(ast: ESTree.Program, i18nKeys: Set) { ).body, ); - return ast; + return minify(generateCodeFromAST(ast)); } diff --git a/packages/brisa/src/utils/compile-files/index.test.ts b/packages/brisa/src/utils/compile-files/index.test.ts index 0ec030724..c73c2f4af 100644 --- a/packages/brisa/src/utils/compile-files/index.test.ts +++ b/packages/brisa/src/utils/compile-files/index.test.ts @@ -636,7 +636,7 @@ describe('utils', () => { ${info} ${info}Route | JS server | JS client (gz) ${info}---------------------------------------------- - ${info}λ /pages/index | 444 B | ${greenLog('3 kB')} + ${info}λ /pages/index | 444 B | ${greenLog('5 kB')} ${info}Δ /layout | 855 B | ${info}Ω /i18n | 221 B | ${info} diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts index 91a7d3233..f044e01f5 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.test.ts @@ -18,7 +18,7 @@ const pageWebComponents = { 'native-some-example': allWebComponents['native-some-example'], }; -const i18nCode = 2799; +const i18nCode = 3653; const brisaSize = 5720; // TODO: Reduce this size :/ const webComponents = 1118; const unsuspenseSize = 213; @@ -248,12 +248,12 @@ describe('utils', () => { }); const allDefineElementCalls = output!.code.match( - /(defineElement\("([a-z]|-)+", ([a-z]|[A-Z])+\))/gm, + /(defineElement\("([a-z]|-)+",([a-z]|[A-Z])+\))/gm, ); expect(allDefineElementCalls).toEqual([ - `defineElement("brisa-error-dialog", brisaErrorDialog)`, - `defineElement("context-provider", contextProvider)`, - `defineElement("native-some-example", SomeExample)`, + `defineElement("brisa-error-dialog",brisaErrorDialog)`, + `defineElement("context-provider",contextProvider)`, + `defineElement("native-some-example",SomeExample)`, ]); }); @@ -302,7 +302,7 @@ describe('utils', () => { pageWebComponents, integrationsPath, }); - expect(output!.code).toContain('window._P ='); + expect(output!.code).toContain('window._P='); }); it('should add the integrations with emoji-picker as direct import', async () => { @@ -341,9 +341,9 @@ describe('utils', () => { }, integrationsPath, }); - const contentOfLib = '() => `has ${window._P.length} web context plugin`'; + const contentOfLib = '()=>`has ${window._P.length} web context plugin`'; - expect(output!.code).toContain('window._P ='); + expect(output!.code).toContain('window._P='); expect(output!.code).toContain(contentOfLib); GlobalRegistrator.register(); diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index dbe9fdef4..73124dd54 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -87,8 +87,5 @@ export async function transformToWebComponents({ return null; } - return { - size: outputs[0].size, - ...processI18n(await outputs[0].text()), - }; + return processI18n(await outputs[0].text()); } From c1618e77ebe7db343104f1e72b86ba0934751dc9 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 16:38:04 +0100 Subject: [PATCH 30/39] reformat: rename getClientCodeInPage to layoutBuild --- .../src/core/test/run-web-components/index.ts | 4 +- .../layout-build}/index.test.ts | 120 +++++++++--------- .../layout-build}/index.ts | 24 ++-- .../brisa/src/utils/compile-files/index.ts | 6 +- 4 files changed, 77 insertions(+), 77 deletions(-) rename packages/brisa/src/utils/{get-client-code-in-page => client-build/layout-build}/index.test.ts (78%) rename packages/brisa/src/utils/{get-client-code-in-page => client-build/layout-build}/index.ts (82%) diff --git a/packages/brisa/src/core/test/run-web-components/index.ts b/packages/brisa/src/core/test/run-web-components/index.ts index 47afdfe4c..3abbd2e9c 100644 --- a/packages/brisa/src/core/test/run-web-components/index.ts +++ b/packages/brisa/src/core/test/run-web-components/index.ts @@ -1,7 +1,7 @@ import { join } from 'node:path'; import fs from 'node:fs'; import { getConstants } from '@/constants'; -import { transformToWebComponents } from '@/utils/get-client-code-in-page'; +import { transformToWebComponents } from '@/utils/client-build/layout-build'; import getWebComponentsList from '@/utils/get-web-components-list'; import getImportableFilepath from '@/utils/get-importable-filepath'; @@ -29,7 +29,7 @@ export default async function runWebComponents() { } const res = await transformToWebComponents({ - pagePath: '__tests__', + layoutPath: '__tests__', webComponentsList: allWebComponents, integrationsPath, useContextProvider: true, diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts b/packages/brisa/src/utils/client-build/layout-build/index.test.ts similarity index 78% rename from packages/brisa/src/utils/get-client-code-in-page/index.test.ts rename to packages/brisa/src/utils/client-build/layout-build/index.test.ts index f044e01f5..083832d1b 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.test.ts +++ b/packages/brisa/src/utils/client-build/layout-build/index.test.ts @@ -3,11 +3,11 @@ import fs from 'node:fs'; import path from 'node:path'; import { GlobalRegistrator } from '@happy-dom/global-registrator'; -import getClientCodeInPage from '.'; +import layoutBuild from '.'; import { getConstants } from '@/constants'; import getWebComponentsList from '@/utils/get-web-components-list'; -const src = path.join(import.meta.dir, '..', '..', '__fixtures__'); +const src = path.join(import.meta.dir, '..', '..', '..', '__fixtures__'); const webComponentsDir = path.join(src, 'web-components'); const build = path.join(src, `out-${crypto.randomUUID()}}`); const brisaInternals = path.join(build, '_brisa'); @@ -28,7 +28,7 @@ const lazyRPCSize = 4105; // TODO: Reduce this size // so it's not included in the initial size const initialSize = unsuspenseSize + rpcSize; -describe('utils', () => { +describe('client-build', () => { beforeEach(async () => { fs.mkdirSync(build, { recursive: true }); fs.mkdirSync(brisaInternals, { recursive: true }); @@ -47,10 +47,10 @@ describe('utils', () => { globalThis.mockConstants = undefined; }); - describe('getClientCodeInPage', () => { + describe('layout-build', () => { it('should not return client code in page without web components, without suspense, without server actions', async () => { - const pagePath = path.join(pages, 'somepage.tsx'); - const output = await getClientCodeInPage({ pagePath, allWebComponents }); + const layoutPath = path.join(pages, 'somepage.tsx'); + const output = await layoutBuild({ layoutPath, allWebComponents }); const expected = { code: '', rpc: '', @@ -67,9 +67,9 @@ describe('utils', () => { }); it('should return client code size of brisa + 2 web-components in page with web components', async () => { - const pagePath = path.join(pages, 'page-with-web-component.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'page-with-web-component.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -81,8 +81,8 @@ describe('utils', () => { }); it('should return client code size as 0 when a page does not have web components', async () => { - const pagePath = path.join(pages, 'somepage.tsx'); - const output = await getClientCodeInPage({ pagePath, allWebComponents }); + const layoutPath = path.join(pages, 'somepage.tsx'); + const output = await layoutBuild({ layoutPath, allWebComponents }); expect(output!.size).toEqual(0); }); @@ -92,9 +92,9 @@ describe('utils', () => { IS_STATIC_EXPORT: true, IS_PRODUCTION: true, }; - const pagePath = path.join(pages, 'index.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'index.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -108,9 +108,9 @@ describe('utils', () => { IS_STATIC_EXPORT: true, IS_PRODUCTION: false, }; - const pagePath = path.join(pages, 'index.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'index.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -126,9 +126,9 @@ describe('utils', () => { IS_STATIC_EXPORT: false, IS_PRODUCTION: true, }; - const pagePath = path.join(pages, 'index.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'index.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -139,8 +139,8 @@ describe('utils', () => { }); it('should return client code in page with suspense and rpc', async () => { - const pagePath = path.join(pages, 'index.tsx'); - const output = await getClientCodeInPage({ pagePath, allWebComponents }); + const layoutPath = path.join(pages, 'index.tsx'); + const output = await layoutBuild({ layoutPath, allWebComponents }); expect(output?.unsuspense.length).toBe(unsuspenseSize); expect(output?.rpc.length).toBe(rpcSize); @@ -152,9 +152,9 @@ describe('utils', () => { }); it('should define 2 web components if there is 1 web component and another one inside', async () => { - const pagePath = path.join(pages, 'page-with-web-component.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'page-with-web-component.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -164,8 +164,8 @@ describe('utils', () => { it('should load lazyRPC in /somepage because it has an hyperlink', async () => { const webComponentSize = 377; - const output = await getClientCodeInPage({ - pagePath: path.join(pages, 'somepage.tsx'), + const output = await layoutBuild({ + layoutPath: path.join(pages, 'somepage.tsx'), allWebComponents, pageWebComponents: { 'with-link': allWebComponents['with-link'], @@ -181,9 +181,9 @@ describe('utils', () => { }); it('should add context-provider if the page has a context-provider without serverOnly attribute', async () => { - const pagePath = path.join(pages, 'somepage-with-context.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'somepage-with-context.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -196,9 +196,9 @@ describe('utils', () => { IS_DEVELOPMENT: true, IS_PRODUCTION: false, }; - const pagePath = path.join(pages, 'somepage.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'somepage.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -211,9 +211,9 @@ describe('utils', () => { IS_DEVELOPMENT: false, IS_PRODUCTION: true, }; - const pagePath = path.join(pages, 'somepage.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'somepage.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -221,10 +221,10 @@ describe('utils', () => { }); it('should add context-provider if the page has not a context-provider but layoutHasContextProvider is true', async () => { - const pagePath = path.join(pages, 'somepage.tsx'); + const layoutPath = path.join(pages, 'somepage.tsx'); const layoutHasContextProvider = true; - const output = await getClientCodeInPage({ - pagePath, + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, layoutHasContextProvider, @@ -238,10 +238,10 @@ describe('utils', () => { IS_DEVELOPMENT: true, IS_PRODUCTION: false, }; - const pagePath = path.join(pages, 'somepage.tsx'); + const layoutPath = path.join(pages, 'somepage.tsx'); const layoutHasContextProvider = true; - const output = await getClientCodeInPage({ - pagePath, + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, layoutHasContextProvider, @@ -258,9 +258,9 @@ describe('utils', () => { }); it('should not add context-provider if the page has a context-provider with serverOnly attribute', async () => { - const pagePath = path.join(pages, 'somepage.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const layoutPath = path.join(pages, 'somepage.tsx'); + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -268,10 +268,10 @@ describe('utils', () => { }); it('should allow environment variables in web components with BRISA_PUBLIC_ prefix', async () => { - const pagePath = path.join(pages, 'page-with-web-component.tsx'); + const layoutPath = path.join(pages, 'page-with-web-component.tsx'); Bun.env.BRISA_PUBLIC_TEST = 'value of test env variable'; - const output = await getClientCodeInPage({ - pagePath, + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, }); @@ -279,10 +279,10 @@ describe('utils', () => { }); it('should NOT add the integrations web context plugins when there are not plugins', async () => { - const pagePath = path.join(pages, 'page-with-web-component.tsx'); + const layoutPath = path.join(pages, 'page-with-web-component.tsx'); const integrationsPath = path.join(webComponentsDir, '_integrations.tsx'); - const output = await getClientCodeInPage({ - pagePath, + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, integrationsPath, @@ -291,13 +291,13 @@ describe('utils', () => { }); it('should add the integrations web context plugins when there are plugins', async () => { - const pagePath = path.join(pages, 'page-with-web-component.tsx'); + const layoutPath = path.join(pages, 'page-with-web-component.tsx'); const integrationsPath = path.join( webComponentsDir, '_integrations2.tsx', ); - const output = await getClientCodeInPage({ - pagePath, + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents, integrationsPath, @@ -306,13 +306,13 @@ describe('utils', () => { }); it('should add the integrations with emoji-picker as direct import', async () => { - const pagePath = path.join(pages, 'page-with-web-component.tsx'); + const layoutPath = path.join(pages, 'page-with-web-component.tsx'); const integrationsPath = path.join( webComponentsDir, '_integrations3.tsx', ); - const output = await getClientCodeInPage({ - pagePath, + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents: { ...pageWebComponents, @@ -325,14 +325,14 @@ describe('utils', () => { }); it('should integrate some-lib with web context plugins', async () => { - const pagePath = path.join(pages, 'page-with-web-component.tsx'); + const layoutPath = path.join(pages, 'page-with-web-component.tsx'); const integrationsPath = path.join( webComponentsDir, '_integrations4.tsx', ); - const output = await getClientCodeInPage({ - pagePath, + const output = await layoutBuild({ + layoutPath, allWebComponents, pageWebComponents: { ...pageWebComponents, diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/client-build/layout-build/index.ts similarity index 82% rename from packages/brisa/src/utils/get-client-code-in-page/index.ts rename to packages/brisa/src/utils/client-build/layout-build/index.ts index 73124dd54..3c1ce4235 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/client-build/layout-build/index.ts @@ -1,36 +1,36 @@ import { logBuildError } from '@/utils/log/log-build'; -import { preEntrypointAnalysis } from '../client-build/pre-entrypoint-analysis'; +import { preEntrypointAnalysis } from '../pre-entrypoint-analysis'; import { removeTempEntrypoint, writeTempEntrypoint, -} from '../client-build/fs-temp-entrypoint-manager'; -import { runBuild } from '../client-build/run-build'; -import { processI18n } from '../client-build/process-i18n'; +} from '../fs-temp-entrypoint-manager'; +import { runBuild } from '../run-build'; +import { processI18n } from '../process-i18n'; type TransformOptions = { webComponentsList: Record; useContextProvider: boolean; integrationsPath?: string | null; - pagePath: string; + layoutPath: string; }; type ClientCodeInPageProps = { - pagePath: string; + layoutPath: string; allWebComponents?: Record; pageWebComponents?: Record; integrationsPath?: string | null; layoutHasContextProvider?: boolean; }; -export default async function getClientCodeInPage({ - pagePath, +export default async function layoutBuild({ + layoutPath, allWebComponents = {}, pageWebComponents = {}, integrationsPath, layoutHasContextProvider, }: ClientCodeInPageProps) { const analysis = await preEntrypointAnalysis( - pagePath, + layoutPath, allWebComponents, pageWebComponents, layoutHasContextProvider, @@ -44,7 +44,7 @@ export default async function getClientCodeInPage({ webComponentsList: analysis.webComponents, useContextProvider: analysis.useContextProvider, integrationsPath, - pagePath, + layoutPath, }); if (!transformedCode) return null; @@ -65,13 +65,13 @@ export async function transformToWebComponents({ webComponentsList, useContextProvider, integrationsPath, - pagePath, + layoutPath, }: TransformOptions) { const { entrypoint, useWebContextPlugins } = await writeTempEntrypoint({ webComponentsList, useContextProvider, integrationsPath, - pagePath, + pagePath: layoutPath, }); const { success, logs, outputs } = await runBuild( diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 702e82e29..ad9114d49 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -5,7 +5,7 @@ import { join } from 'node:path'; import { getConstants } from '@/constants'; import byteSizeToString from '@/utils/byte-size-to-string'; -import getClientCodeInPage from '@/utils/get-client-code-in-page'; +import layoutBuild from '@/utils/client-build/layout-build'; import getEntrypoints, { getEntrypointsRouter } from '@/utils/get-entrypoints'; import getImportableFilepath from '@/utils/get-importable-filepath'; import getWebComponentsList from '@/utils/get-web-components-list'; @@ -298,8 +298,8 @@ async function compileClientCodePage( const clientSizesPerPage: Record = {}; const layoutWebComponents = webComponentsPerEntrypoint[layoutBuildPath]; const layoutCode = layoutBuildPath - ? await getClientCodeInPage({ - pagePath: layoutBuildPath, + ? await layoutBuild({ + layoutPath: layoutBuildPath, allWebComponents, pageWebComponents: layoutWebComponents, integrationsPath, From b5fa76695e949242f62236e5e384c7761887d41e Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 20:36:07 +0100 Subject: [PATCH 31/39] refactor: move clientPageBuild to module --- .../client-build/pages-build/index.test.ts | 155 ++++++++++++++++++ .../client-build/{ => pages-build}/index.ts | 18 +- .../brisa/src/utils/client-build/types.ts | 1 + .../brisa/src/utils/compile-files/index.ts | 4 +- 4 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/pages-build/index.test.ts rename packages/brisa/src/utils/client-build/{ => pages-build}/index.ts (70%) diff --git a/packages/brisa/src/utils/client-build/pages-build/index.test.ts b/packages/brisa/src/utils/client-build/pages-build/index.test.ts new file mode 100644 index 000000000..f2832fca3 --- /dev/null +++ b/packages/brisa/src/utils/client-build/pages-build/index.test.ts @@ -0,0 +1,155 @@ +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import fs from 'node:fs'; +import path from 'node:path'; +import clientPageBuild from '.'; +import { getConstants } from '@/constants'; +import getWebComponentsList from '@/utils/get-web-components-list'; +import type { BuildArtifact } from 'bun'; + +const src = path.join(import.meta.dir, '..', '..', '..', '__fixtures__'); +const build = path.join(src, `out-${crypto.randomUUID()}}`); +const brisaInternals = path.join(build, '_brisa'); +const allWebComponents = await getWebComponentsList(src); +const pageWebComponents = { + 'web-component': allWebComponents['web-component'], + 'native-some-example': allWebComponents['native-some-example'], +}; + +const toArtifact = (path: string) => + ({ path, text: () => Bun.file(path).text() }) as BuildArtifact; + +describe('client-build', () => { + beforeEach(async () => { + fs.mkdirSync(build, { recursive: true }); + fs.mkdirSync(brisaInternals, { recursive: true }); + const constants = getConstants() ?? {}; + globalThis.mockConstants = { + ...constants, + SRC_DIR: src, + IS_PRODUCTION: true, + IS_DEVELOPMENT: false, + BUILD_DIR: src, + }; + }); + + afterEach(() => { + fs.rmSync(build, { recursive: true }); + globalThis.mockConstants = undefined; + }); + + describe('clientPageBuild', () => { + it('should not compile client code in page without web components, without suspense, without server actions', async () => { + const pagePath = path.join(src, 'pages', 'somepage.tsx'); + const output = await clientPageBuild([toArtifact(pagePath)], { + allWebComponents, + webComponentsPerEntrypoint: {}, + layoutWebComponents: {}, + }); + + expect(output).toEqual([ + { + unsuspense: '', + rpc: '', + lazyRPC: '', + pagePath: pagePath, + code: '', + size: 0, + useContextProvider: false, + useI18n: false, + i18nKeys: new Set(), + webComponents: {}, + }, + ]); + }); + + it('should return client code size of brisa + 2 web-components in page with web components', async () => { + const pagePath = path.join(src, 'pages', 'page-with-web-component.tsx'); + const output = await clientPageBuild([toArtifact(pagePath)], { + allWebComponents, + webComponentsPerEntrypoint: { + [pagePath]: pageWebComponents, + }, + layoutWebComponents: {}, + }); + + expect(output.length).toEqual(1); + expect(output[0].size).toBeGreaterThan(0); + expect(output[0].unsuspense).toBeEmpty(); + expect(output[0].rpc).toBeEmpty(); + expect(output[0].lazyRPC).toBeEmpty(); + expect(output[0].useContextProvider).toBe(false); + expect(output[0].useI18n).toBe(true); + expect(output[0].i18nKeys).toEqual(new Set(['hello'])); + expect(output[0].webComponents).toEqual({ + 'web-component': allWebComponents['web-component'], + 'native-some-example': allWebComponents['native-some-example'], + }); + }); + + it('should return client code size of brisa + 2 wc + rpc + suspense', async () => { + const pagePath = path.join(src, 'pages', 'index.tsx'); + const output = await clientPageBuild([toArtifact(pagePath)], { + allWebComponents, + webComponentsPerEntrypoint: { + [pagePath]: pageWebComponents, + }, + layoutWebComponents: {}, + }); + + expect(output.length).toEqual(1); + expect(output[0].size).toBeGreaterThan(0); + expect(output[0].unsuspense).not.toBeEmpty(); + expect(output[0].rpc).not.toBeEmpty(); + expect(output[0].lazyRPC).not.toBeEmpty(); + expect(output[0].useContextProvider).toBe(false); + expect(output[0].useI18n).toBe(true); + expect(output[0].i18nKeys).toEqual(new Set(['hello'])); + expect(output[0].webComponents).toEqual({ + 'web-component': allWebComponents['web-component'], + 'native-some-example': allWebComponents['native-some-example'], + }); + }); + + it('should build multi pages', async () => { + const pagePath = path.join(src, 'pages', 'index.tsx'); + const pagePath2 = path.join(src, 'pages', 'page-with-web-component.tsx'); + + const output = await clientPageBuild( + [toArtifact(pagePath), toArtifact(pagePath2)], + { + allWebComponents, + webComponentsPerEntrypoint: { + [pagePath]: {}, + [pagePath2]: pageWebComponents, + }, + layoutWebComponents: {}, + }, + ); + + expect(output.length).toEqual(2); + + // First page + expect(output[0].size).toBeGreaterThan(0); + expect(output[0].unsuspense).not.toBeEmpty(); + expect(output[0].rpc).not.toBeEmpty(); + expect(output[0].lazyRPC).not.toBeEmpty(); + expect(output[0].useContextProvider).toBe(false); + expect(output[0].useI18n).toBe(false); + expect(output[0].i18nKeys).toBeEmpty(); + expect(output[0].webComponents).toBeEmpty(); + + // Second page + expect(output[1].size).toBeGreaterThan(0); + expect(output[1].unsuspense).toBeEmpty(); + expect(output[1].rpc).toBeEmpty(); + expect(output[1].lazyRPC).toBeEmpty(); + expect(output[1].useContextProvider).toBe(false); + expect(output[1].useI18n).toBe(true); + expect(output[1].i18nKeys).toEqual(new Set(['hello'])); + expect(output[1].webComponents).toEqual({ + 'web-component': allWebComponents['web-component'], + 'native-some-example': allWebComponents['native-some-example'], + }); + }); + }); +}); diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/pages-build/index.ts similarity index 70% rename from packages/brisa/src/utils/client-build/index.ts rename to packages/brisa/src/utils/client-build/pages-build/index.ts index 72d58596d..393a8f157 100644 --- a/packages/brisa/src/utils/client-build/index.ts +++ b/packages/brisa/src/utils/client-build/pages-build/index.ts @@ -1,20 +1,18 @@ import type { BuildArtifact } from 'bun'; -import { logBuildError } from '../log/log-build'; -import { removeTempEntrypoints } from './fs-temp-entrypoint-manager'; -import { getClientBuildDetails } from './get-client-build-details'; -import type { EntryPointData, Options } from './types'; -import { runBuild } from './run-build'; -import { processI18n } from './process-i18n'; +import { logBuildError } from '../../log/log-build'; +import { removeTempEntrypoints } from '../fs-temp-entrypoint-manager'; +import { getClientBuildDetails } from '../get-client-build-details'; +import type { EntryPointData, Options } from '../types'; +import { runBuild } from '../run-build'; +import { processI18n } from '../process-i18n'; // TODO: Benchmarks old vs new -// TODO: Move to module (build-multi-entrypoints) + add tests -// TODO: Move getClientCodeInPage to module like build-single-entrypoint + add tests // TODO: Move compileClientCodePage from compile-files to inside this client-build folder + tests // TODO: move add-i18n-bridge to post-build -export default async function buildMultiClientEntrypoints( +export default async function clientPageBuild( pages: BuildArtifact[], options: Options, -) { +): Promise { let clientBuildDetails = await getClientBuildDetails(pages, options); const entrypointsData = clientBuildDetails.reduce((acc, curr, index) => { diff --git a/packages/brisa/src/utils/client-build/types.ts b/packages/brisa/src/utils/client-build/types.ts index ceeefdd6e..04ed88903 100644 --- a/packages/brisa/src/utils/client-build/types.ts +++ b/packages/brisa/src/utils/client-build/types.ts @@ -22,4 +22,5 @@ export type EntryPointData = { useWebContextPlugins?: boolean; pagePath: string; index?: number; + webComponents?: WCs; }; diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index ad9114d49..6cf9f89a7 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -18,7 +18,7 @@ import generateStaticExport from '@/utils/generate-static-export'; import getWebComponentsPerEntryPoints from '@/utils/get-webcomponents-per-entrypoints'; import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; import generateDynamicTypes from '@/utils/generate-dynamic-types'; -import buildMultiClientEntrypoints from '@/utils/client-build'; +import clientPageBuild from '@/utils/client-build/pages-build'; const TS_REGEX = /\.tsx?$/; const BRISA_DEPS = ['brisa/server']; @@ -306,7 +306,7 @@ async function compileClientCodePage( }) : null; - const pagesData = await buildMultiClientEntrypoints(pages, { + const pagesData = await clientPageBuild(pages, { webComponentsPerEntrypoint, layoutWebComponents, allWebComponents, From 288f727d0d9abe4eb9f97b9f73daf6e28aaa26a2 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 20:41:46 +0100 Subject: [PATCH 32/39] refactor: move clientBuild --- .../brisa/src/utils/client-build/index.ts | 256 ++++++++++++++++++ .../brisa/src/utils/compile-files/index.ts | 254 +---------------- 2 files changed, 258 insertions(+), 252 deletions(-) create mode 100644 packages/brisa/src/utils/client-build/index.ts diff --git a/packages/brisa/src/utils/client-build/index.ts b/packages/brisa/src/utils/client-build/index.ts new file mode 100644 index 000000000..e085fcf38 --- /dev/null +++ b/packages/brisa/src/utils/client-build/index.ts @@ -0,0 +1,256 @@ +import { gzipSync, type BuildArtifact } from 'bun'; +import { brotliCompressSync } from 'node:zlib'; +import fs from 'node:fs'; +import { join } from 'node:path'; + +import { getConstants } from '@/constants'; +import layoutBuild from '@/utils/client-build/layout-build'; +import { getEntrypointsRouter } from '@/utils/get-entrypoints'; +import getI18nClientMessages from '@/utils/get-i18n-client-messages'; +import generateDynamicTypes from '@/utils/generate-dynamic-types'; +import clientPageBuild from '@/utils/client-build/pages-build'; + +const TS_REGEX = /\.tsx?$/; + +export async function clientBuild( + pages: BuildArtifact[], + { + allWebComponents, + webComponentsPerEntrypoint, + integrationsPath, + layoutPath, + pagesRoutes, + }: { + allWebComponents: Record; + webComponentsPerEntrypoint: Record>; + integrationsPath?: string | null; + layoutPath?: string | null; + pagesRoutes: ReturnType; + }, +) { + const { BUILD_DIR, I18N_CONFIG, IS_PRODUCTION } = getConstants(); + const pagesClientPath = join(BUILD_DIR, 'pages-client'); + const internalPath = join(BUILD_DIR, '_brisa'); + const layoutBuildPath = layoutPath ? getBuildPath(layoutPath) : ''; + const writes = []; + + // During hotreloading it is important to clean pages-client because + // new client files are generated with hash, this hash can change + // and many files would be accumulated during development. + // + // On the other hand, in production it will always be empty because + // the whole build is cleaned at startup. + if (fs.existsSync(pagesClientPath)) { + fs.rmSync(pagesClientPath, { recursive: true }); + } + // Create pages-client + fs.mkdirSync(pagesClientPath); + + if (!fs.existsSync(internalPath)) fs.mkdirSync(internalPath); + + const clientSizesPerPage: Record = {}; + const layoutWebComponents = webComponentsPerEntrypoint[layoutBuildPath]; + const layoutCode = layoutBuildPath + ? await layoutBuild({ + layoutPath: layoutBuildPath, + allWebComponents, + pageWebComponents: layoutWebComponents, + integrationsPath, + }) + : null; + + const pagesData = await clientPageBuild(pages, { + webComponentsPerEntrypoint, + layoutWebComponents, + allWebComponents, + integrationsPath, + layoutHasContextProvider: layoutCode?.useContextProvider, + }); + + for (const data of pagesData) { + let { size, rpc, lazyRPC, code, unsuspense, useI18n, i18nKeys, pagePath } = + data; + const clientPagePath = pagePath.replace('pages', 'pages-client'); + const route = pagePath.replace(BUILD_DIR, ''); + + // If there are no actions in the page but there are actions in + // the layout, then it is as if the page also has actions. + if (!rpc && layoutCode?.rpc) { + size += layoutCode.rpc.length; + rpc = layoutCode.rpc; + } + + // It is not necessary to increase the size here because this + // code even if it is necessary to generate it if it does not + // exist yet, it is not part of the initial size of the page + // because it is loaded in a lazy way. + if (!lazyRPC && layoutCode?.lazyRPC) { + lazyRPC = layoutCode.lazyRPC; + } + + // If there is no unsuspense in the page but there is unsuspense + // in the layout, then it is as if the page also has unsuspense. + if (!unsuspense && layoutCode?.unsuspense) { + size += layoutCode.unsuspense.length; + unsuspense = layoutCode.unsuspense; + } + + // fix i18n when it is not defined in the page but it is defined + // in the layout + if (!useI18n && layoutCode?.useI18n) { + useI18n = layoutCode.useI18n; + } + if (layoutCode?.i18nKeys.size) { + i18nKeys = new Set([...i18nKeys, ...layoutCode.i18nKeys]); + } + + clientSizesPerPage[route] = size; + + if (!size) continue; + + const hash = Bun.hash(code); + const clientPage = clientPagePath.replace('.js', `-${hash}.js`); + clientSizesPerPage[route] = 0; + + // create _unsuspense.js and _unsuspense.txt (list of pages with unsuspense) + clientSizesPerPage[route] += addExtraChunk(unsuspense, '_unsuspense', { + pagesClientPath, + pagePath, + writes, + }); + + // create _rpc-[versionhash].js and _rpc.txt (list of pages with actions) + clientSizesPerPage[route] += addExtraChunk(rpc, '_rpc', { + pagesClientPath, + pagePath, + writes, + }); + + // create _rpc-lazy-[versionhash].js + clientSizesPerPage[route] += addExtraChunk(lazyRPC, '_rpc-lazy', { + pagesClientPath, + pagePath, + skipList: true, + writes, + }); + + if (!code) continue; + + if (useI18n && i18nKeys.size && I18N_CONFIG?.messages) { + for (const locale of I18N_CONFIG?.locales ?? []) { + const i18nPagePath = clientPage.replace('.js', `-${locale}.js`); + const messages = getI18nClientMessages(locale, i18nKeys); + const i18nCode = `window.i18nMessages={...window.i18nMessages,...(${JSON.stringify(messages)})};`; + + writes.push(Bun.write(i18nPagePath, i18nCode)); + + // Compression in production + if (IS_PRODUCTION) { + writes.push( + Bun.write( + `${i18nPagePath}.gz`, + gzipSync(new TextEncoder().encode(i18nCode)), + ), + ); + writes.push( + Bun.write(`${i18nPagePath}.br`, brotliCompressSync(i18nCode)), + ); + } + } + } + + // create page file + writes.push( + Bun.write(clientPagePath.replace('.js', '.txt'), hash.toString()), + ); + writes.push(Bun.write(clientPage, code)); + + // Compression in production + if (IS_PRODUCTION) { + const gzipClientPage = gzipSync(new TextEncoder().encode(code)); + + writes.push(Bun.write(`${clientPage}.gz`, gzipClientPage)); + writes.push(Bun.write(`${clientPage}.br`, brotliCompressSync(code))); + clientSizesPerPage[route] += gzipClientPage.length; + } + } + + writes.push( + Bun.write( + join(internalPath, 'types.ts'), + generateDynamicTypes({ allWebComponents, pagesRoutes }), + ), + ); + + // Although on Mac it can work without await, on Windows it does not and it is mandatory + await Promise.all(writes); + + return clientSizesPerPage; +} + +function addExtraChunk( + code: string, + filename: string, + { + pagesClientPath, + pagePath, + skipList = false, + writes, + }: { + pagesClientPath: string; + pagePath: string; + skipList?: boolean; + writes: Promise[]; + }, +) { + const { BUILD_DIR, VERSION, IS_PRODUCTION } = getConstants(); + const jsFilename = `${filename}-${VERSION}.js`; + + if (!code) return 0; + + if (!skipList && fs.existsSync(join(pagesClientPath, jsFilename))) { + const listPath = join(pagesClientPath, `${filename}.txt`); + + writes.push( + Bun.write( + listPath, + `${fs.readFileSync(listPath).toString()}\n${pagePath.replace(BUILD_DIR, '')}`, + ), + ); + + return 0; + } + + writes.push(Bun.write(join(pagesClientPath, jsFilename), code)); + + if (!skipList) { + writes.push( + Bun.write( + join(pagesClientPath, `${filename}.txt`), + pagePath.replace(BUILD_DIR, ''), + ), + ); + } + + if (IS_PRODUCTION) { + const gzipUnsuspense = gzipSync(new TextEncoder().encode(code)); + + writes.push( + Bun.write(join(pagesClientPath, `${jsFilename}.gz`), gzipUnsuspense), + ); + writes.push( + Bun.write( + join(pagesClientPath, `${jsFilename}.br`), + brotliCompressSync(code), + ), + ); + return gzipUnsuspense.length; + } + + return code.length; +} + +function getBuildPath(path: string) { + const { SRC_DIR, BUILD_DIR } = getConstants(); + return path.replace(SRC_DIR, BUILD_DIR).replace(TS_REGEX, '.js'); +} diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 6cf9f89a7..99730304e 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -1,26 +1,19 @@ -import { gzipSync, type BuildArtifact } from 'bun'; -import { brotliCompressSync } from 'node:zlib'; -import fs from 'node:fs'; import { join } from 'node:path'; import { getConstants } from '@/constants'; import byteSizeToString from '@/utils/byte-size-to-string'; -import layoutBuild from '@/utils/client-build/layout-build'; import getEntrypoints, { getEntrypointsRouter } from '@/utils/get-entrypoints'; import getImportableFilepath from '@/utils/get-importable-filepath'; import getWebComponentsList from '@/utils/get-web-components-list'; import { logTable } from '@/utils/log/log-build'; import serverComponentPlugin from '@/utils/server-component-plugin'; import createContextPlugin from '@/utils/create-context/create-context-plugin'; -import getI18nClientMessages from '@/utils/get-i18n-client-messages'; import { transpileActions, buildActions } from '@/utils/transpile-actions'; import generateStaticExport from '@/utils/generate-static-export'; import getWebComponentsPerEntryPoints from '@/utils/get-webcomponents-per-entrypoints'; import { shouldTransferTranslatedPagePaths } from '@/utils/transfer-translated-page-paths'; -import generateDynamicTypes from '@/utils/generate-dynamic-types'; -import clientPageBuild from '@/utils/client-build/pages-build'; +import { clientBuild } from '../client-build'; -const TS_REGEX = /\.tsx?$/; const BRISA_DEPS = ['brisa/server']; export default async function compileFiles() { @@ -153,7 +146,7 @@ export default async function compileFiles() { if (!actionResult.success) logs.push(...actionResult.logs); } - const pagesSize = await compileClientCodePage(outputs, { + const pagesSize = await clientBuild(outputs, { allWebComponents, webComponentsPerEntrypoint: getWebComponentsPerEntryPoints( webComponentsPerFile, @@ -258,246 +251,3 @@ export default async function compileFiles() { return { success, logs, pagesSize: pagesSize }; } - -async function compileClientCodePage( - pages: BuildArtifact[], - { - allWebComponents, - webComponentsPerEntrypoint, - integrationsPath, - layoutPath, - pagesRoutes, - }: { - allWebComponents: Record; - webComponentsPerEntrypoint: Record>; - integrationsPath?: string | null; - layoutPath?: string | null; - pagesRoutes: ReturnType; - }, -) { - const { BUILD_DIR, I18N_CONFIG, IS_PRODUCTION } = getConstants(); - const pagesClientPath = join(BUILD_DIR, 'pages-client'); - const internalPath = join(BUILD_DIR, '_brisa'); - const layoutBuildPath = layoutPath ? getBuildPath(layoutPath) : ''; - const writes = []; - - // During hotreloading it is important to clean pages-client because - // new client files are generated with hash, this hash can change - // and many files would be accumulated during development. - // - // On the other hand, in production it will always be empty because - // the whole build is cleaned at startup. - if (fs.existsSync(pagesClientPath)) { - fs.rmSync(pagesClientPath, { recursive: true }); - } - // Create pages-client - fs.mkdirSync(pagesClientPath); - - if (!fs.existsSync(internalPath)) fs.mkdirSync(internalPath); - - const clientSizesPerPage: Record = {}; - const layoutWebComponents = webComponentsPerEntrypoint[layoutBuildPath]; - const layoutCode = layoutBuildPath - ? await layoutBuild({ - layoutPath: layoutBuildPath, - allWebComponents, - pageWebComponents: layoutWebComponents, - integrationsPath, - }) - : null; - - const pagesData = await clientPageBuild(pages, { - webComponentsPerEntrypoint, - layoutWebComponents, - allWebComponents, - integrationsPath, - layoutHasContextProvider: layoutCode?.useContextProvider, - }); - - for (const data of pagesData) { - let { size, rpc, lazyRPC, code, unsuspense, useI18n, i18nKeys, pagePath } = - data; - const clientPagePath = pagePath.replace('pages', 'pages-client'); - const route = pagePath.replace(BUILD_DIR, ''); - - // If there are no actions in the page but there are actions in - // the layout, then it is as if the page also has actions. - if (!rpc && layoutCode?.rpc) { - size += layoutCode.rpc.length; - rpc = layoutCode.rpc; - } - - // It is not necessary to increase the size here because this - // code even if it is necessary to generate it if it does not - // exist yet, it is not part of the initial size of the page - // because it is loaded in a lazy way. - if (!lazyRPC && layoutCode?.lazyRPC) { - lazyRPC = layoutCode.lazyRPC; - } - - // If there is no unsuspense in the page but there is unsuspense - // in the layout, then it is as if the page also has unsuspense. - if (!unsuspense && layoutCode?.unsuspense) { - size += layoutCode.unsuspense.length; - unsuspense = layoutCode.unsuspense; - } - - // fix i18n when it is not defined in the page but it is defined - // in the layout - if (!useI18n && layoutCode?.useI18n) { - useI18n = layoutCode.useI18n; - } - if (layoutCode?.i18nKeys.size) { - i18nKeys = new Set([...i18nKeys, ...layoutCode.i18nKeys]); - } - - clientSizesPerPage[route] = size; - - if (!size) continue; - - const hash = Bun.hash(code); - const clientPage = clientPagePath.replace('.js', `-${hash}.js`); - clientSizesPerPage[route] = 0; - - // create _unsuspense.js and _unsuspense.txt (list of pages with unsuspense) - clientSizesPerPage[route] += addExtraChunk(unsuspense, '_unsuspense', { - pagesClientPath, - pagePath, - writes, - }); - - // create _rpc-[versionhash].js and _rpc.txt (list of pages with actions) - clientSizesPerPage[route] += addExtraChunk(rpc, '_rpc', { - pagesClientPath, - pagePath, - writes, - }); - - // create _rpc-lazy-[versionhash].js - clientSizesPerPage[route] += addExtraChunk(lazyRPC, '_rpc-lazy', { - pagesClientPath, - pagePath, - skipList: true, - writes, - }); - - if (!code) continue; - - if (useI18n && i18nKeys.size && I18N_CONFIG?.messages) { - for (const locale of I18N_CONFIG?.locales ?? []) { - const i18nPagePath = clientPage.replace('.js', `-${locale}.js`); - const messages = getI18nClientMessages(locale, i18nKeys); - const i18nCode = `window.i18nMessages={...window.i18nMessages,...(${JSON.stringify(messages)})};`; - - writes.push(Bun.write(i18nPagePath, i18nCode)); - - // Compression in production - if (IS_PRODUCTION) { - writes.push( - Bun.write( - `${i18nPagePath}.gz`, - gzipSync(new TextEncoder().encode(i18nCode)), - ), - ); - writes.push( - Bun.write(`${i18nPagePath}.br`, brotliCompressSync(i18nCode)), - ); - } - } - } - - // create page file - writes.push( - Bun.write(clientPagePath.replace('.js', '.txt'), hash.toString()), - ); - writes.push(Bun.write(clientPage, code)); - - // Compression in production - if (IS_PRODUCTION) { - const gzipClientPage = gzipSync(new TextEncoder().encode(code)); - - writes.push(Bun.write(`${clientPage}.gz`, gzipClientPage)); - writes.push(Bun.write(`${clientPage}.br`, brotliCompressSync(code))); - clientSizesPerPage[route] += gzipClientPage.length; - } - } - - writes.push( - Bun.write( - join(internalPath, 'types.ts'), - generateDynamicTypes({ allWebComponents, pagesRoutes }), - ), - ); - - // Although on Mac it can work without await, on Windows it does not and it is mandatory - await Promise.all(writes); - - return clientSizesPerPage; -} - -function addExtraChunk( - code: string, - filename: string, - { - pagesClientPath, - pagePath, - skipList = false, - writes, - }: { - pagesClientPath: string; - pagePath: string; - skipList?: boolean; - writes: Promise[]; - }, -) { - const { BUILD_DIR, VERSION, IS_PRODUCTION } = getConstants(); - const jsFilename = `${filename}-${VERSION}.js`; - - if (!code) return 0; - - if (!skipList && fs.existsSync(join(pagesClientPath, jsFilename))) { - const listPath = join(pagesClientPath, `${filename}.txt`); - - writes.push( - Bun.write( - listPath, - `${fs.readFileSync(listPath).toString()}\n${pagePath.replace(BUILD_DIR, '')}`, - ), - ); - - return 0; - } - - writes.push(Bun.write(join(pagesClientPath, jsFilename), code)); - - if (!skipList) { - writes.push( - Bun.write( - join(pagesClientPath, `${filename}.txt`), - pagePath.replace(BUILD_DIR, ''), - ), - ); - } - - if (IS_PRODUCTION) { - const gzipUnsuspense = gzipSync(new TextEncoder().encode(code)); - - writes.push( - Bun.write(join(pagesClientPath, `${jsFilename}.gz`), gzipUnsuspense), - ); - writes.push( - Bun.write( - join(pagesClientPath, `${jsFilename}.br`), - brotliCompressSync(code), - ), - ); - return gzipUnsuspense.length; - } - - return code.length; -} - -function getBuildPath(path: string) { - const { SRC_DIR, BUILD_DIR } = getConstants(); - return path.replace(SRC_DIR, BUILD_DIR).replace(TS_REGEX, '.js'); -} From cb22c454c111c3dc086b1002ee6e50c03870c4dd Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 20:50:53 +0100 Subject: [PATCH 33/39] docs: remove comments --- packages/brisa/src/utils/client-build/pages-build/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/brisa/src/utils/client-build/pages-build/index.ts b/packages/brisa/src/utils/client-build/pages-build/index.ts index 393a8f157..30faf2c26 100644 --- a/packages/brisa/src/utils/client-build/pages-build/index.ts +++ b/packages/brisa/src/utils/client-build/pages-build/index.ts @@ -6,9 +6,6 @@ import type { EntryPointData, Options } from '../types'; import { runBuild } from '../run-build'; import { processI18n } from '../process-i18n'; -// TODO: Benchmarks old vs new -// TODO: Move compileClientCodePage from compile-files to inside this client-build folder + tests -// TODO: move add-i18n-bridge to post-build export default async function clientPageBuild( pages: BuildArtifact[], options: Options, From 4659ca94365b733b58a673d67a1dc1d9fb17fa48 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 21:30:39 +0100 Subject: [PATCH 34/39] perf: optimize actions --- packages/brisa/src/utils/compile-files/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 99730304e..735a22bdc 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -70,6 +70,7 @@ export default async function compileFiles() { if (websocketPath) entrypoints.push(websocketPath); if (integrationsPath) entrypoints.push(integrationsPath); + const actionWrites: Promise[] = []; const { success, logs, outputs } = await Bun.build({ entrypoints, outdir: BUILD_DIR, @@ -111,9 +112,11 @@ export default async function compileFiles() { actionsEntrypoints.push(actionEntrypoint); actionIdCount += 1; - await Bun.write( - actionEntrypoint, - transpileActions(result.code), + actionWrites.push( + Bun.write( + actionEntrypoint, + transpileActions(result.code), + ), ); } @@ -142,7 +145,9 @@ export default async function compileFiles() { if (!success) return { success, logs, pagesSize: {} }; if (actionsEntrypoints.length) { - const actionResult = await buildActions({ actionsEntrypoints, define }); + const actionResult = await Promise.all(actionWrites).then(() => + buildActions({ actionsEntrypoints, define }), + ); if (!actionResult.success) logs.push(...actionResult.logs); } From 68cd1824f9b1699643f4151630cf6ad21a0112a7 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 22:00:48 +0100 Subject: [PATCH 35/39] docs: clean comments --- .../src/utils/client-build/generate-entrypoint-code/index.ts | 1 - .../src/utils/client-build/pre-entrypoint-analysis/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts index df28282f3..5ff82bdd5 100644 --- a/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts +++ b/packages/brisa/src/utils/client-build/generate-entrypoint-code/index.ts @@ -77,7 +77,6 @@ async function getImports( ); if (integrationsPath) { - // TODO: cache dynamic import can improve performance? We need to try it const module = await import(integrationsPath); if (module.webContextPlugins?.length > 0) { diff --git a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts index 89d4d8028..9320ed64a 100644 --- a/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts +++ b/packages/brisa/src/utils/client-build/pre-entrypoint-analysis/index.ts @@ -87,7 +87,6 @@ export async function preEntrypointAnalysis( webComponents: aggregatedWebComponents, // Fields that need an extra analysis during/after build: - // TODO: Maybe useI18n and i18nKeys can be included to this previous analysis? code: '', size, useI18n: false, From fd5fe0983504f2afbf22b6274d3b70d868d566e8 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 22:42:09 +0100 Subject: [PATCH 36/39] feat(dx): improve build log feedback --- packages/brisa/src/cli/build.ts | 2 +- .../src/utils/client-build/layout-build/index.ts | 6 +++++- .../brisa/src/utils/client-build/pages-build/index.ts | 8 +++++++- packages/brisa/src/utils/compile-files/index.ts | 4 +++- packages/brisa/src/utils/log/log-build.ts | 11 ++++++++--- packages/brisa/src/utils/transpile-actions/index.ts | 7 +++++-- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/brisa/src/cli/build.ts b/packages/brisa/src/cli/build.ts index 2aab495e7..cef7a8158 100644 --- a/packages/brisa/src/cli/build.ts +++ b/packages/brisa/src/cli/build.ts @@ -5,6 +5,7 @@ import { getConstants } from '@/constants'; import byteSizeToString from '@/utils/byte-size-to-string'; import { logTable, generateStaticExport } from './build-utils'; import compileBrisaInternalsToDoBuildPortable from '@/utils/compile-serve-internals-into-build'; +import { log } from '@/utils/log/log-build'; const outputText = { bun: 'Bun.js Web Service App', @@ -16,7 +17,6 @@ const outputText = { }; export default async function build() { - const log = process.env.QUIET_MODE === 'true' ? () => {} : console.log; const constants = getConstants(); const { IS_PRODUCTION, diff --git a/packages/brisa/src/utils/client-build/layout-build/index.ts b/packages/brisa/src/utils/client-build/layout-build/index.ts index 3c1ce4235..5c1c05f31 100644 --- a/packages/brisa/src/utils/client-build/layout-build/index.ts +++ b/packages/brisa/src/utils/client-build/layout-build/index.ts @@ -1,4 +1,4 @@ -import { logBuildError } from '@/utils/log/log-build'; +import { log, logBuildError } from '@/utils/log/log-build'; import { preEntrypointAnalysis } from '../pre-entrypoint-analysis'; import { removeTempEntrypoint, @@ -6,6 +6,7 @@ import { } from '../fs-temp-entrypoint-manager'; import { runBuild } from '../run-build'; import { processI18n } from '../process-i18n'; +import { getConstants } from '@/constants'; type TransformOptions = { webComponentsList: Record; @@ -29,6 +30,7 @@ export default async function layoutBuild({ integrationsPath, layoutHasContextProvider, }: ClientCodeInPageProps) { + const { LOG_PREFIX } = getConstants(); const analysis = await preEntrypointAnalysis( layoutPath, allWebComponents, @@ -40,6 +42,8 @@ export default async function layoutBuild({ return analysis; } + log(LOG_PREFIX.WAIT, `compiling layout...`); + const transformedCode = await transformToWebComponents({ webComponentsList: analysis.webComponents, useContextProvider: analysis.useContextProvider, diff --git a/packages/brisa/src/utils/client-build/pages-build/index.ts b/packages/brisa/src/utils/client-build/pages-build/index.ts index 30faf2c26..670d4845b 100644 --- a/packages/brisa/src/utils/client-build/pages-build/index.ts +++ b/packages/brisa/src/utils/client-build/pages-build/index.ts @@ -1,15 +1,17 @@ import type { BuildArtifact } from 'bun'; -import { logBuildError } from '../../log/log-build'; +import { log, logBuildError } from '../../log/log-build'; import { removeTempEntrypoints } from '../fs-temp-entrypoint-manager'; import { getClientBuildDetails } from '../get-client-build-details'; import type { EntryPointData, Options } from '../types'; import { runBuild } from '../run-build'; import { processI18n } from '../process-i18n'; +import { getConstants } from '@/constants'; export default async function clientPageBuild( pages: BuildArtifact[], options: Options, ): Promise { + const { LOG_PREFIX } = getConstants(); let clientBuildDetails = await getClientBuildDetails(pages, options); const entrypointsData = clientBuildDetails.reduce((acc, curr, index) => { @@ -23,6 +25,10 @@ export default async function clientPageBuild( return clientBuildDetails; } + log( + LOG_PREFIX.WAIT, + `compiling ${entrypointsData.length} client entrypoints...`, + ); const { success, logs, outputs } = await runBuild( entrypoints, options.allWebComponents, diff --git a/packages/brisa/src/utils/compile-files/index.ts b/packages/brisa/src/utils/compile-files/index.ts index 735a22bdc..fb3da1d73 100644 --- a/packages/brisa/src/utils/compile-files/index.ts +++ b/packages/brisa/src/utils/compile-files/index.ts @@ -5,7 +5,7 @@ import byteSizeToString from '@/utils/byte-size-to-string'; import getEntrypoints, { getEntrypointsRouter } from '@/utils/get-entrypoints'; import getImportableFilepath from '@/utils/get-importable-filepath'; import getWebComponentsList from '@/utils/get-web-components-list'; -import { logTable } from '@/utils/log/log-build'; +import { log, logTable } from '@/utils/log/log-build'; import serverComponentPlugin from '@/utils/server-component-plugin'; import createContextPlugin from '@/utils/create-context/create-context-plugin'; import { transpileActions, buildActions } from '@/utils/transpile-actions'; @@ -70,6 +70,8 @@ export default async function compileFiles() { if (websocketPath) entrypoints.push(websocketPath); if (integrationsPath) entrypoints.push(integrationsPath); + log(LOG_PREFIX.WAIT, `compiling ${entrypoints.length} server entrypoints...`); + const actionWrites: Promise[] = []; const { success, logs, outputs } = await Bun.build({ entrypoints, diff --git a/packages/brisa/src/utils/log/log-build.ts b/packages/brisa/src/utils/log/log-build.ts index 323153df8..8376e5de2 100644 --- a/packages/brisa/src/utils/log/log-build.ts +++ b/packages/brisa/src/utils/log/log-build.ts @@ -44,7 +44,12 @@ export function logTable(data: { [key: string]: string }[]) { lines.forEach((line) => console.log(LOG_PREFIX.INFO, line)); } -function log(type: 'Error' | 'Warning') { +export function log(...messages: string[]) { + if (process.env.QUIET_MODE === 'true') return; + console.log(...messages); +} + +function logProblem(type: 'Error' | 'Warning') { const { LOG_PREFIX } = getConstants(); const LOG = LOG_PREFIX[ @@ -100,11 +105,11 @@ export function logError({ footer = `${docTitle ?? 'Documentation'}: ${docLink}`; } - return log('Error')(messages, footer, stack); + return logProblem('Error')(messages, footer, stack); } export function logWarning(messages: string[], footer?: string) { - return log('Warning')(messages, footer); + return logProblem('Warning')(messages, footer); } export function logBuildError( diff --git a/packages/brisa/src/utils/transpile-actions/index.ts b/packages/brisa/src/utils/transpile-actions/index.ts index 1670060b0..3b10a8144 100644 --- a/packages/brisa/src/utils/transpile-actions/index.ts +++ b/packages/brisa/src/utils/transpile-actions/index.ts @@ -6,7 +6,7 @@ import { getConstants } from '@/constants'; import type { ActionInfo } from './get-actions-info'; import getActionsInfo from './get-actions-info'; import { getPurgedBody } from './get-purged-body'; -import { logBuildError } from '@/utils/log/log-build'; +import { log, logBuildError } from '@/utils/log/log-build'; import { jsx, jsxDEV } from '../ast/constants'; type CompileActionsParams = { @@ -594,7 +594,7 @@ export async function buildActions({ actionsEntrypoints, define, }: CompileActionsParams) { - const { BUILD_DIR, IS_PRODUCTION, CONFIG } = getConstants(); + const { BUILD_DIR, IS_PRODUCTION, CONFIG, LOG_PREFIX } = getConstants(); const isNode = CONFIG.output === 'node' && IS_PRODUCTION; const rawActionsDir = join(BUILD_DIR, 'actions_raw'); const barrelFile = join(rawActionsDir, 'index.ts'); @@ -605,6 +605,9 @@ export async function buildActions({ ); const external = CONFIG.external ? [...CONFIG.external, 'brisa'] : ['brisa']; + + log(LOG_PREFIX.WAIT, `compiling server actions...`); + const res = await Bun.build({ entrypoints: [barrelFile], outdir: join(BUILD_DIR, 'actions'), From a432fc980565942065ea710daad9cc95dcb89f51 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 22:47:57 +0100 Subject: [PATCH 37/39] feat(dx): improve build log feedback --- packages/brisa/src/utils/client-build/pages-build/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/brisa/src/utils/client-build/pages-build/index.ts b/packages/brisa/src/utils/client-build/pages-build/index.ts index 670d4845b..f89230ed8 100644 --- a/packages/brisa/src/utils/client-build/pages-build/index.ts +++ b/packages/brisa/src/utils/client-build/pages-build/index.ts @@ -12,6 +12,9 @@ export default async function clientPageBuild( options: Options, ): Promise { const { LOG_PREFIX } = getConstants(); + + log(LOG_PREFIX.WAIT, 'analyzing and preparing client build...'); + let clientBuildDetails = await getClientBuildDetails(pages, options); const entrypointsData = clientBuildDetails.reduce((acc, curr, index) => { From 8b6caae52d7a7c4b5d3dafdf864dff8a61bfd660 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 28 Nov 2024 22:54:38 +0100 Subject: [PATCH 38/39] feat(dx): fix log --- packages/brisa/src/utils/handle-css-files/index.test.ts | 4 ++-- packages/brisa/src/utils/handle-css-files/index.ts | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/brisa/src/utils/handle-css-files/index.test.ts b/packages/brisa/src/utils/handle-css-files/index.test.ts index 7cc4a94d3..2dd9c556c 100644 --- a/packages/brisa/src/utils/handle-css-files/index.test.ts +++ b/packages/brisa/src/utils/handle-css-files/index.test.ts @@ -166,8 +166,8 @@ describe('utils/handle-css-files', () => { await handleCSSFiles(); expect(mockLog).toHaveBeenCalledTimes(2); expect(mockLog).toHaveBeenCalledWith( - LOG_PREFIX.INFO, - `Transpiling CSS with brisa-tailwindcss`, + LOG_PREFIX.WAIT, + `transpiling CSS with brisa-tailwindcss...`, ); expect(mockLog).toHaveBeenCalledWith( LOG_PREFIX.INFO, diff --git a/packages/brisa/src/utils/handle-css-files/index.ts b/packages/brisa/src/utils/handle-css-files/index.ts index 56fbac764..71d750c1c 100644 --- a/packages/brisa/src/utils/handle-css-files/index.ts +++ b/packages/brisa/src/utils/handle-css-files/index.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import fs from 'node:fs'; import { getConstants } from '@/constants'; -import { logError } from '../log/log-build'; +import { log, logError } from '../log/log-build'; import { gzipSync } from 'bun'; import { brotliCompressSync } from 'node:zlib'; @@ -26,10 +26,7 @@ export default async function handleCSSFiles() { const startTime = Date.now(); if (IS_BUILD_PROCESS) { - console.log( - LOG_PREFIX.INFO, - `Transpiling CSS with ${integration.name}`, - ); + log(LOG_PREFIX.WAIT, `transpiling CSS with ${integration.name}...`); } let useDefault = true; From 496a19a8f6db97f79ebd62fe6420fc3f9ec3f8cf Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Sat, 30 Nov 2024 21:12:02 +0100 Subject: [PATCH 39/39] fix: fix integrations with plugins --- .../client-build/layout-build/index.test.ts | 8 +++- .../client-build/pages-build/index.test.ts | 37 +++++++++++++++++++ .../utils/client-build/pages-build/index.ts | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/brisa/src/utils/client-build/layout-build/index.test.ts b/packages/brisa/src/utils/client-build/layout-build/index.test.ts index 083832d1b..bc78649eb 100644 --- a/packages/brisa/src/utils/client-build/layout-build/index.test.ts +++ b/packages/brisa/src/utils/client-build/layout-build/index.test.ts @@ -19,7 +19,7 @@ const pageWebComponents = { }; const i18nCode = 3653; -const brisaSize = 5720; // TODO: Reduce this size :/ +const brisaSize = 5638; // TODO: Reduce this size :/ const webComponents = 1118; const unsuspenseSize = 213; const rpcSize = 2500; // TODO: Reduce this size @@ -287,7 +287,10 @@ describe('client-build', () => { pageWebComponents, integrationsPath, }); + // Declaration expect(output!.code).not.toContain('window._P='); + // Brisa element usage + expect(output!.code).not.toContain('._P)'); }); it('should add the integrations web context plugins when there are plugins', async () => { @@ -302,7 +305,10 @@ describe('client-build', () => { pageWebComponents, integrationsPath, }); + // Declaration expect(output!.code).toContain('window._P='); + // Brisa element usage + expect(output!.code).toContain('._P)'); }); it('should add the integrations with emoji-picker as direct import', async () => { diff --git a/packages/brisa/src/utils/client-build/pages-build/index.test.ts b/packages/brisa/src/utils/client-build/pages-build/index.test.ts index f2832fca3..de77e325a 100644 --- a/packages/brisa/src/utils/client-build/pages-build/index.test.ts +++ b/packages/brisa/src/utils/client-build/pages-build/index.test.ts @@ -8,6 +8,7 @@ import type { BuildArtifact } from 'bun'; const src = path.join(import.meta.dir, '..', '..', '..', '__fixtures__'); const build = path.join(src, `out-${crypto.randomUUID()}}`); +const webComponentsDir = path.join(src, 'web-components'); const brisaInternals = path.join(build, '_brisa'); const allWebComponents = await getWebComponentsList(src); const pageWebComponents = { @@ -152,4 +153,40 @@ describe('client-build', () => { }); }); }); + + it('should NOT add the integrations web context plugins when there are not plugins', async () => { + const pagePath = path.join(src, 'pages', 'page-with-web-component.tsx'); + const integrationsPath = path.join(webComponentsDir, '_integrations.tsx'); + const output = await clientPageBuild([toArtifact(pagePath)], { + allWebComponents, + webComponentsPerEntrypoint: { + [pagePath]: allWebComponents, + }, + integrationsPath, + layoutWebComponents: {}, + }); + + // Declaration + expect(output[0].code).not.toContain('window._P='); + // Brisa element usage + expect(output[0].code).not.toContain('._P)'); + }); + + it('should add the integrations web context plugins when there are plugins', async () => { + const pagePath = path.join(src, 'pages', 'page-with-web-component.tsx'); + const integrationsPath = path.join(webComponentsDir, '_integrations2.tsx'); + const output = await clientPageBuild([toArtifact(pagePath)], { + allWebComponents, + webComponentsPerEntrypoint: { + [pagePath]: allWebComponents, + }, + integrationsPath, + layoutWebComponents: {}, + }); + + // Declaration + expect(output[0].code).toContain('window._P='); + // Brisa element usage + expect(output[0].code).toContain('._P)'); + }); }); diff --git a/packages/brisa/src/utils/client-build/pages-build/index.ts b/packages/brisa/src/utils/client-build/pages-build/index.ts index f89230ed8..22645d6c8 100644 --- a/packages/brisa/src/utils/client-build/pages-build/index.ts +++ b/packages/brisa/src/utils/client-build/pages-build/index.ts @@ -35,7 +35,7 @@ export default async function clientPageBuild( const { success, logs, outputs } = await runBuild( entrypoints, options.allWebComponents, - entrypointsData[0].useContextProvider, + entrypointsData[0].useWebContextPlugins, ); // Remove all temp files