diff --git a/bun.lockb b/bun.lockb index f60b36a5..b6097839 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/building-your-application/configuring/integrations.md b/docs/building-your-application/configuring/integrations.md index ce5f8bab..32be0154 100644 --- a/docs/building-your-application/configuring/integrations.md +++ b/docs/building-your-application/configuring/integrations.md @@ -35,5 +35,6 @@ export type Integration = { defaultCSS?: { content: string; applyDefaultWhenEvery: (content: string) => boolean; - }} -``` \ No newline at end of file + }}; + afterBuild?(brisaConstants: BrisaConstants): void | Promise; +``` diff --git a/package.json b/package.json index f92d8c25..94dca1c9 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "www:build": "bun run --cwd packages/www build", "www:dev": "bun run --cwd packages/www dev", "www:deploy": "bun run build && bun run www:build && vercel --prod", - "prepare": "husky" + "prepare": "husky && bun run packages/brisa-tailwindcss/scripts/generate-json-deps.ts" }, "packageManager": "bun@1.1.34", "engines": { diff --git a/packages/brisa-tailwindcss/index.d.ts b/packages/brisa-tailwindcss/index.d.ts index a70770f9..ea87e2b4 100644 --- a/packages/brisa-tailwindcss/index.d.ts +++ b/packages/brisa-tailwindcss/index.d.ts @@ -19,4 +19,5 @@ export default function tailwindCSS(): { content: string; applyDefaultWhenEvery: (content: string) => boolean; }; + afterBuild(brisaConstants: BrisaConstants): void | Promise; }; diff --git a/packages/brisa-tailwindcss/index.test.ts b/packages/brisa-tailwindcss/index.test.ts index 96c26e9d..df24be54 100644 --- a/packages/brisa-tailwindcss/index.test.ts +++ b/packages/brisa-tailwindcss/index.test.ts @@ -1,6 +1,11 @@ -import { describe, expect, it } from 'bun:test'; +import { describe, expect, it, spyOn } from 'bun:test'; +import fs from 'node:fs/promises'; +import packageJSON from './package.json'; +import libs from './libs.json'; import brisaTailwindcss from '.'; +const TAILWIND_VERSION = packageJSON.devDependencies.tailwindcss; + describe('brisa-tailwindcss', () => { it('should return the correct name', () => { const integration = brisaTailwindcss(); @@ -75,4 +80,36 @@ describe('brisa-tailwindcss', () => { expect(transpiledCSS).not.toContain('@layer base'); }); + + it('should call Bun.$ to install tailwindcss inside the build folder #637', async () => { + const integration = brisaTailwindcss(); + const mockLog = spyOn(console, 'log'); + const mockCp = spyOn(fs, 'cp').mockResolvedValue(); + const mockExists = spyOn(fs, 'exists').mockResolvedValue(true); + + await integration.afterBuild({ + BUILD_DIR: import.meta.dirname, + LOG_PREFIX: { INFO: 'INFO', WAIT: 'WAIT', TICK: 'TICK' }, + }); + + expect(mockLog.mock.calls[0][0]).toBe('INFO'); + expect(mockLog.mock.calls[1]).toEqual([ + 'WAIT', + ' Embedding TailwindCSS in the build folder...', + ]); + expect(mockCp).toHaveBeenCalledTimes(libs.length); + for (const lib of libs) { + expect(mockCp).toHaveBeenCalledWith( + expect.stringContaining(lib), + expect.stringContaining(lib), + { recursive: true }, + ); + } + expect(mockLog.mock.calls[2][0]).toBe('INFO'); + expect(mockLog.mock.calls[2][1]).toBe('TICK'); + expect(mockLog.mock.calls[2][2]).toContain('TailwindCSS embedded in'); + mockLog.mockRestore(); + mockCp.mockRestore(); + mockExists.mockRestore(); + }); }); diff --git a/packages/brisa-tailwindcss/index.ts b/packages/brisa-tailwindcss/index.ts index 150545e6..0bfbc7aa 100644 --- a/packages/brisa-tailwindcss/index.ts +++ b/packages/brisa-tailwindcss/index.ts @@ -1,5 +1,8 @@ import tailwindcss from '@tailwindcss/postcss'; import postcss from 'postcss'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import libs from './libs.json'; // Note: is not bundled here to avoid issues with lightningcss export default function brisaTailwindcss() { @@ -20,5 +23,40 @@ export default function brisaTailwindcss() { `, applyDefaultWhenEvery: (content: string) => !content.includes('tailwind'), }, + // Tailwind has a subdependency called lightwindcss, which cannot be included in the bundle + // https://github.com/parcel-bundler/lightningcss/issues/701 + // then as a solution is to put it as "external" so the build works correctly. However, for the standalone + // build to continue being standalone, it needs this dependency along with all its subdependencies to + // work correctly. As a solution, we install tailwind in the build folder and in this way the problem is solved. + // Issue: https://github.com/brisa-build/brisa/issues/637 + async afterBuild({ BUILD_DIR, LOG_PREFIX }) { + const start = Date.now(); + const destNodeModules = path.join(BUILD_DIR, 'node_modules'); + const nodeModules = Bun.resolveSync('brisa', BUILD_DIR).split('brisa')[0]; + + console.log(LOG_PREFIX.INFO, ''); + console.log( + LOG_PREFIX.WAIT, + ' Embedding TailwindCSS in the build folder...', + ); + + await Promise.all( + libs.map(async (lib) => { + const from = path.join(nodeModules, lib); + const to = path.join(destNodeModules, lib); + + if (await fs.exists(from)) { + return fs.cp(from, to, { recursive: true }); + } + }), + ); + + const milliseconds = Date.now() - start; + console.log( + LOG_PREFIX.INFO, + LOG_PREFIX.TICK, + `TailwindCSS embedded in ${milliseconds}ms`, + ); + }, }; } diff --git a/packages/brisa-tailwindcss/libs.json b/packages/brisa-tailwindcss/libs.json new file mode 100644 index 00000000..eabefccc --- /dev/null +++ b/packages/brisa-tailwindcss/libs.json @@ -0,0 +1,25 @@ +[ + "lightningcss-darwin-x64", + "lightningcss-linux-x64-gnu", + "lightningcss-win32-x64-msvc", + "lightningcss-win32-arm64-msvc", + "lightningcss-darwin-arm64", + "lightningcss-linux-arm64-gnu", + "lightningcss-linux-arm-gnueabihf", + "lightningcss-linux-arm64-musl", + "lightningcss-linux-x64-musl", + "lightningcss-freebsd-x64", + "@alloc", + "tapable", + "jiti", + "detect-libc", + "nanoid", + "@tailwindcss", + "postcss", + "enhanced-resolve", + "picocolors", + "tailwindcss", + "graceful-fs", + "lightningcss", + "source-map-js" +] diff --git a/packages/brisa-tailwindcss/package.json b/packages/brisa-tailwindcss/package.json index e0a9249c..6a55bf37 100644 --- a/packages/brisa-tailwindcss/package.json +++ b/packages/brisa-tailwindcss/package.json @@ -10,12 +10,15 @@ "name": "Brisa Team", "email": "contact@brisa.build.com" }, + "scripts": { + "libs-json": "bun run scripts/generate-json-deps.ts" + }, "repository": { "type": "git", "url": "git+https://github.com/brisa-build/brisa.git", "directory": "packages/brisa-tailwindcss" }, - "files": ["index.ts", "index.d.ts"], + "files": ["index.ts", "index.d.ts", "libs.json"], "devDependencies": { "@tailwindcss/postcss": "4.0.0-alpha.33", "postcss": "8.4.49", diff --git a/packages/brisa-tailwindcss/scripts/generate-json-deps.ts b/packages/brisa-tailwindcss/scripts/generate-json-deps.ts new file mode 100644 index 00000000..97bacceb --- /dev/null +++ b/packages/brisa-tailwindcss/scripts/generate-json-deps.ts @@ -0,0 +1,39 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import packageJSON from '../package.json'; + +const TAILWIND_VERSION = packageJSON.devDependencies.tailwindcss; +const tempDir = path.join(import.meta.dirname, 'temp'); + +fs.mkdirSync(tempDir); +fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ + dependencies: { + tailwindcss: TAILWIND_VERSION, + '@tailwindcss/postcss': TAILWIND_VERSION, + }, + }), +); + +await Bun.$`cd ${tempDir} && bun i`.quiet(); + +const nodeModules = path.join(tempDir, 'node_modules'); +const lightningcssPackage = JSON.parse( + fs.readFileSync( + path.join(nodeModules, 'lightningcss', 'package.json'), + 'utf-8', + ), +); +const libs = new Set(Object.keys(lightningcssPackage.optionalDependencies)); + +for (const lib of fs.readdirSync(nodeModules)) { + if (lib.startsWith('.')) continue; + libs.add(lib); +} + +fs.writeFileSync( + path.join(import.meta.dirname, '..', 'libs.json'), + JSON.stringify([...libs]), +); +fs.rmSync(tempDir, { recursive: true }); diff --git a/packages/brisa/src/types/index.d.ts b/packages/brisa/src/types/index.d.ts index bfef3025..e42160fb 100644 --- a/packages/brisa/src/types/index.d.ts +++ b/packages/brisa/src/types/index.d.ts @@ -1312,6 +1312,7 @@ export type Integration = { content: string; applyDefaultWhenEvery: (content: string) => boolean; }; + afterBuild?(brisaConstants: BrisaConstants): void | Promise; }; export interface I18nDictionary { diff --git a/packages/brisa/src/utils/compile-serve-internals-into-build/index.test.ts b/packages/brisa/src/utils/compile-serve-internals-into-build/index.test.ts index e29eaf49..ada3becc 100644 --- a/packages/brisa/src/utils/compile-serve-internals-into-build/index.test.ts +++ b/packages/brisa/src/utils/compile-serve-internals-into-build/index.test.ts @@ -1,4 +1,12 @@ -import { expect, it, describe, beforeEach, afterEach, spyOn } from 'bun:test'; +import { + expect, + it, + describe, + beforeEach, + afterEach, + spyOn, + mock, +} from 'bun:test'; import path from 'node:path'; import fs from 'node:fs'; import compileBrisaInternalsToDoBuildPortable from '.'; @@ -205,4 +213,19 @@ describe('utils/compileServeInternalsIntoBuild', () => { }, }); }); + + it('should call all afterBuild functions inside integrations', async () => { + const mockAfterBuild = mock(async () => {}); + const mockAfterBuild2 = mock(() => {}); + + mockConstants.CONFIG.integrations = [ + { afterBuild: mockAfterBuild }, + { afterBuild: mockAfterBuild2 }, + ]; + + await compileBrisaInternalsToDoBuildPortable(); + + expect(mockAfterBuild).toHaveBeenCalledWith(mockConstants); + expect(mockAfterBuild2).toHaveBeenCalledWith(mockConstants); + }); }); diff --git a/packages/brisa/src/utils/compile-serve-internals-into-build/index.ts b/packages/brisa/src/utils/compile-serve-internals-into-build/index.ts index afe17d93..56a0abb3 100644 --- a/packages/brisa/src/utils/compile-serve-internals-into-build/index.ts +++ b/packages/brisa/src/utils/compile-serve-internals-into-build/index.ts @@ -27,8 +27,9 @@ const NO_SERVER_EXPORTS = new Set([ * */ export default async function compileServeInternalsIntoBuild() { + const constants = getConstants(); const { BUILD_DIR, LOG_PREFIX, CONFIG, ROOT_DIR, IS_PRODUCTION, BRISA_DIR } = - getConstants(); + constants; if (!IS_PRODUCTION) return; @@ -88,6 +89,14 @@ export default async function compileServeInternalsIntoBuild() { createBrisaModule(runtimeExec); addBrisaModule(); + const afterBuildFunctions = (CONFIG.integrations ?? []).filter( + (i) => i.afterBuild, + ); + + if (afterBuildFunctions.length) { + await Promise.all(afterBuildFunctions.map((i) => i.afterBuild!(constants))); + } + if (isServer) { const relativeServerFilePath = path.join( path.relative(ROOT_DIR, BUILD_DIR), 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 a431a890..057457ef 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 @@ -22,7 +22,7 @@ const i18nCode = 2799; const brisaSize = 5720; // TODO: Reduce this size :/ const webComponents = 1107; const unsuspenseSize = 213; -const rpcSize = 2509; // TODO: Reduce this size +const rpcSize = 2500; // TODO: Reduce this size const lazyRPCSize = 4105; // TODO: Reduce this size // lazyRPC is loaded after user interaction (action, link), // so it's not included in the initial size diff --git a/packages/www/src/public/content.json b/packages/www/src/public/content.json index 2e903f45..03ea7d2a 100644 --- a/packages/www/src/public/content.json +++ b/packages/www/src/public/content.json @@ -3359,7 +3359,7 @@ { "id": "/building-your-application/configuring/integrations#types", "title": "Types", - "text": "export type Configuration = {\n // ...\n integrations?: Integration[];\n};\n\nexport type Integration = {\n name: string;\n transpileCSS?(pathname: string, content: string): Promise;\n defaultCSS?: {\n content: string;\n applyDefaultWhenEvery: (content: string) => boolean;\n }}", + "text": "export type Configuration = {\n // ...\n integrations?: Integration[];\n};\n\nexport type Integration = {\n name: string;\n transpileCSS?(pathname: string, content: string): Promise;\n defaultCSS?: {\n content: string;\n applyDefaultWhenEvery: (content: string) => boolean;\n }};\n afterBuild?(brisaConstants: BrisaConstants): void | Promise;", "titles": [ "Integrations" ]