Skip to content

Commit

Permalink
fix(build): fix usage of external CSS Parsers (Tailwind, PandaCSS...) (
Browse files Browse the repository at this point in the history
…#729)

* fix(build): fix usage of external CSS Parsers (Tailwind, PandaCSS...)

* fix: transpile files from src

* test: fix tests with src

* test: fix tests

* fix: change set to array to keep the order
  • Loading branch information
aralroca authored Jan 26, 2025
1 parent 4d01792 commit b525d41
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 29 deletions.
2 changes: 2 additions & 0 deletions packages/brisa/src/utils/compile-files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 { clientBuild } from '../client-build';
import { getCSSLoader } from '@/utils/handle-css-files';

const BRISA_DEPS = ['brisa/server'];

Expand Down Expand Up @@ -88,6 +89,7 @@ export default async function compileFiles() {
splitting: false,
external,
define,
loader: getCSSLoader(),
plugins: extendPlugins(
[
{
Expand Down
96 changes: 76 additions & 20 deletions packages/brisa/src/utils/handle-css-files/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
import path from 'node:path';
import fs from 'node:fs';
import type { BrisaConstants } from '@/types';
import handleCSSFiles from '.';
import handleCSSFiles, { getCSSLoader } from '.';
import brisaTailwindCSS from 'brisa-tailwindcss';

const BUILD_DIR = path.join(import.meta.dirname, 'out');
const SRC_DIR = BUILD_DIR;
const LOG_PREFIX = {
INFO: '[INFO]',
TICK: '✔',
Expand All @@ -27,6 +28,7 @@ describe('utils/handle-css-files', () => {
if (!fs.existsSync(BUILD_DIR)) fs.mkdirSync(BUILD_DIR);
globalThis.mockConstants = {
BUILD_DIR,
SRC_DIR,
LOG_PREFIX,
} as unknown as BrisaConstants;
mockHash = spyOn(Bun, 'hash');
Expand All @@ -49,7 +51,9 @@ describe('utils/handle-css-files', () => {
await handleCSSFiles();

const newFilename = 'style-123456.css';
expect(fs.existsSync(path.join(BUILD_DIR, 'public', newFilename))).toBeTrue();
expect(
fs.existsSync(path.join(BUILD_DIR, 'public', newFilename)),
).toBeTrue();
});

it('should create a css-files.js file with the new CSS file names (renamed)', async () => {
Expand All @@ -66,7 +70,10 @@ describe('utils/handle-css-files', () => {

it('should move (rename) multi CSS files style-<hash>.css inside /public', async () => {
fs.writeFileSync(path.join(BUILD_DIR, 'test.css'), 'body { color: red; }');
fs.writeFileSync(path.join(BUILD_DIR, 'test2.css'), 'body { color: blue; }');
fs.writeFileSync(
path.join(BUILD_DIR, 'test2.css'),
'body { color: blue; }',
);

mockHash.mockReturnValueOnce(111111).mockReturnValueOnce(222222);

Expand All @@ -82,11 +89,12 @@ describe('utils/handle-css-files', () => {

it('should create a css-files.js file with multiple renamed css file names', async () => {
fs.writeFileSync(path.join(BUILD_DIR, 'test.css'), 'body { color: red; }');
fs.writeFileSync(path.join(BUILD_DIR, 'test2.css'), 'body { color: blue; }');
fs.writeFileSync(
path.join(BUILD_DIR, 'test2.css'),
'body { color: blue; }',
);

mockHash
.mockReturnValueOnce(111111)
.mockReturnValueOnce(222222);
mockHash.mockReturnValueOnce(111111).mockReturnValueOnce(222222);

await handleCSSFiles();

Expand All @@ -101,9 +109,9 @@ describe('utils/handle-css-files', () => {

it('should create a base.css content file using TailwindCSS integration when no file has @tailwind', async () => {
const CONFIG = { integrations: [brisaTailwindCSS()] };
globalThis.mockConstants = { BUILD_DIR, CONFIG, LOG_PREFIX };
globalThis.mockConstants = { BUILD_DIR, SRC_DIR, CONFIG, LOG_PREFIX };

mockHash.mockReturnValueOnce(999999);
mockHash.mockReturnValueOnce(999999);

await handleCSSFiles();

Expand All @@ -117,21 +125,24 @@ describe('utils/handle-css-files', () => {
fs.existsSync(path.join(BUILD_DIR, 'public', expectedFilename)),
).toBeTrue();
expect(
fs.readFileSync(path.join(BUILD_DIR, 'public', expectedFilename), 'utf-8'),
fs.readFileSync(
path.join(BUILD_DIR, 'public', expectedFilename),
'utf-8',
),
).toContain('MIT License | https://tailwindcss.com');
});

it('should create the base-<hash>.css on front of the others when no file has @tailwind', async () => {
const CONFIG = { integrations: [brisaTailwindCSS()] };
globalThis.mockConstants = { BUILD_DIR, CONFIG, LOG_PREFIX };
globalThis.mockConstants = { BUILD_DIR, SRC_DIR, CONFIG, LOG_PREFIX };

fs.writeFileSync(path.join(BUILD_DIR, 'test.css'), 'body { color: red; }');

mockHash.mockReturnValueOnce(111111).mockReturnValueOnce(222222);
mockHash.mockReturnValue(111111);

await handleCSSFiles();

const baseCSSFilename = 'base-222222.css';
const baseCSSFilename = 'base-111111.css';
const styleCSSFilename = 'style-111111.css';

const cssFiles = (
Expand All @@ -140,19 +151,21 @@ describe('utils/handle-css-files', () => {

expect(cssFiles).toEqual([baseCSSFilename, styleCSSFilename].toSorted());

expect(fs.existsSync(path.join(BUILD_DIR, 'public', baseCSSFilename))).toBeTrue();
expect(
fs.existsSync(path.join(BUILD_DIR, 'public', baseCSSFilename)),
).toBeTrue();
expect(
fs.readFileSync(path.join(BUILD_DIR, 'public', baseCSSFilename), 'utf-8'),
).toContain('MIT License | https://tailwindcss.com');
});

it('should NOT create a base.css file when some file has @tailwind', async () => {
const CONFIG = { integrations: [brisaTailwindCSS()] };
globalThis.mockConstants = { BUILD_DIR, CONFIG, LOG_PREFIX };
globalThis.mockConstants = { BUILD_DIR, SRC_DIR, CONFIG, LOG_PREFIX };

fs.writeFileSync(path.join(BUILD_DIR, 'test.css'), '@tailwind base;');

mockHash.mockReturnValueOnce(111111);
mockHash.mockReturnValue(111111);

await handleCSSFiles();

Expand All @@ -174,9 +187,9 @@ describe('utils/handle-css-files', () => {
fs.writeFileSync(path.join(BUILD_DIR, 'test.css'), code);

const CONFIG = { integrations: [brisaTailwindCSS()] };
globalThis.mockConstants = { BUILD_DIR, CONFIG, LOG_PREFIX };
globalThis.mockConstants = { BUILD_DIR, SRC_DIR, CONFIG, LOG_PREFIX };

mockHash.mockReturnValueOnce(111111);
mockHash.mockReturnValue(111111);

await handleCSSFiles();

Expand All @@ -191,6 +204,7 @@ describe('utils/handle-css-files', () => {
const CONFIG = { integrations: [brisaTailwindCSS()] };
globalThis.mockConstants = {
BUILD_DIR,
SRC_DIR,
CONFIG,
LOG_PREFIX,
IS_BUILD_PROCESS: true,
Expand All @@ -216,6 +230,7 @@ describe('utils/handle-css-files', () => {
const CONFIG = { integrations: [brisaTailwindCSS()] };
globalThis.mockConstants = {
BUILD_DIR,
SRC_DIR,
CONFIG,
LOG_PREFIX,
IS_BUILD_PROCESS: false,
Expand All @@ -232,25 +247,31 @@ describe('utils/handle-css-files', () => {
const CONFIG = { assetCompression: true };
globalThis.mockConstants = {
BUILD_DIR,
SRC_DIR,
CONFIG,
LOG_PREFIX,
IS_PRODUCTION: true,
};

fs.writeFileSync(path.join(BUILD_DIR, 'test.css'), 'body { color: red; }');
mockHash.mockReturnValueOnce(111111)
mockHash.mockReturnValueOnce(111111);

await handleCSSFiles();

expect(fs.readdirSync(path.join(BUILD_DIR, 'public')).toSorted()).toEqual(
['style-111111.css', 'style-111111.css.br', 'style-111111.css.gz'].toSorted(),
[
'style-111111.css',
'style-111111.css.br',
'style-111111.css.gz',
].toSorted(),
);
});

it('should NOT compress to gzip/brotli in DEV mode (IS_PRODUCTION=false) even if assetCompression is true', async () => {
const CONFIG = { assetCompression: true };
globalThis.mockConstants = {
BUILD_DIR,
SRC_DIR,
CONFIG,
LOG_PREFIX,
IS_PRODUCTION: false,
Expand All @@ -266,3 +287,38 @@ describe('utils/handle-css-files', () => {
]);
});
});

describe('getCSSLoader', () => {
it('should return a plain text loader for CSS files when useExternalTranspiler is true', () => {
globalThis.mockConstants = {
CONFIG: { integrations: [{ transpileCSS: true }] },
} as any;

const loader = getCSSLoader();

expect(loader).toEqual({
'.css': 'text',
'.scss': 'text',
'.sass': 'text',
'.less': 'text',
});
});

it('should return a plain text loader for CSS files when useExternalTranspiler is false', () => {
globalThis.mockConstants = {
CONFIG: { integrations: [{ transpileCSS: false }] },
} as any;

const loader = getCSSLoader();

expect(loader).toBeUndefined();
});

it('should return a plain text loader for CSS files when useExternalTranspiler is undefined', () => {
globalThis.mockConstants = { CONFIG: {} };

const loader = getCSSLoader();

expect(loader).toBeUndefined();
});
});
59 changes: 50 additions & 9 deletions packages/brisa/src/utils/handle-css-files/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path';
import type { Loader } from 'bun';
import fs from 'node:fs';
import { getConstants } from '@/constants';
import { logError } from '../log/log-build';
Expand All @@ -9,19 +10,38 @@ const cssGlob = new Glob('**/*.css');

export default async function handleCSSFiles() {
try {
const { BUILD_DIR, CONFIG, LOG_PREFIX, IS_BUILD_PROCESS, IS_PRODUCTION } =
getConstants();
const {
BUILD_DIR,
CONFIG,
LOG_PREFIX,
IS_BUILD_PROCESS,
IS_PRODUCTION,
SRC_DIR,
} = getConstants();
const publicFolder = path.join(BUILD_DIR, 'public');

if (!fs.existsSync(publicFolder)) fs.mkdirSync(publicFolder);

const cssFilePaths: string[] = await moveCSSInsidePublic(BUILD_DIR);
const cssFilePaths: string[] = await handleCSSInsidePublic(
BUILD_DIR,
publicFolder,
);
const integrations = (CONFIG?.integrations ?? []).filter(
(integration) => integration.transpileCSS,
);

// Using CSS integrations
if (integrations.length > 0) {
// Use the "src" CSS files to transpile it with the integration parser
// instead of the "build" because they are not transpiled by Bun CSS Parser
const cssFilePathsFromSrc = await handleCSSInsidePublic(
SRC_DIR,
publicFolder,
);
for (const file of cssFilePathsFromSrc) {
if (!cssFilePaths.includes(file)) cssFilePaths.push(file);
}

for (const integration of integrations) {
const startTime = Date.now();

Expand Down Expand Up @@ -73,7 +93,10 @@ export default async function handleCSSFiles() {

for (const file of cssFilePaths) {
const buffer = fs.readFileSync(path.join(publicFolder, file));
Bun.write(path.join(publicFolder, file + '.gz'), gzipSync(buffer as any) as any);
Bun.write(
path.join(publicFolder, file + '.gz'),
gzipSync(buffer as any) as any,
);
Bun.write(
path.join(publicFolder, file + '.br'),
brotliCompressSync(buffer as any) as any,
Expand Down Expand Up @@ -101,17 +124,35 @@ export default async function handleCSSFiles() {
}
}

async function moveCSSInsidePublic(buildDir: string) {
async function handleCSSInsidePublic(dir: string, outDir: string) {
const files = [];

for await (const filename of cssGlob.scan(buildDir)) {
const filePath = path.join(buildDir, filename);
for await (const filename of cssGlob.scan(dir)) {
const filePath = path.join(dir, filename);
const hash = Bun.hash(await Bun.file(filePath).arrayBuffer());
const newFilename = `style-${hash}.css`
const newFilename = `style-${hash}.css`;

fs.renameSync(filePath, path.join(buildDir, 'public', newFilename));
fs.copyFileSync(filePath, path.join(outDir, newFilename));
files.push(newFilename);
}

return files;
}

export function getCSSLoader(): { [x: string]: Loader } | undefined {
const { CONFIG } = getConstants();
const useExternalTranspiler = (CONFIG?.integrations ?? []).some(
(integration) => integration.transpileCSS,
);

// Adding plain text loader for CSS files avoid the Bun CSS Parser for these files
// already handled by the external transpiler (Tailwind, PandaCSS, etc)
if (useExternalTranspiler) {
return {
'.css': 'text',
'.scss': 'text',
'.sass': 'text',
'.less': 'text',
};
}
}
2 changes: 2 additions & 0 deletions packages/brisa/src/utils/transpile-actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import getActionsInfo from './get-actions-info';
import { getPurgedBody } from './get-purged-body';
import { log, logBuildError } from '@/utils/log/log-build';
import { jsx, jsxDEV } from '../ast/constants';
import { getCSSLoader } from '@/utils/handle-css-files';

type CompileActionsParams = {
actionsEntrypoints: string[];
Expand Down Expand Up @@ -620,6 +621,7 @@ export async function buildActions({
minify: IS_PRODUCTION,
splitting: true,
define,
loader: getCSSLoader(),
});

if (!res.success) {
Expand Down

0 comments on commit b525d41

Please sign in to comment.