Skip to content

migrade from swc to esbuild for bundling #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This project is a live JSX editor built with Vite, React, TypeScript, and Tailwi
1. Clone the repository:

```sh
git clone https://github.com/your-username/live-jsx-app.git
git clone https://github.com/SelfMadeSystem/live-jsx-app.git
cd live-jsx-app
```

Expand All @@ -30,19 +30,19 @@ bun i
bun dev
```

4. Open your browser and navigate to `http://localhost:5173` or type `o` in the terminal with the development server running.
4. Open your browser and navigate to `http://localhost:5173`. Alternatively, type `o` in the terminal to open the app in your default browser.

## How does it work?

The app uses the Monaco Editor to allow you to write JSX and CSS code. The code is then transpiled using [SWC](https://swc.rs/) and [PostCSS](https://postcss.org/) with [TailwindCSS](https://tailwindcss.com/) to generate the preview. I made a custom Monaco TailwindCSS integration because, at the time of writing, [monaco-tailwindcss](https://github.com/remcohaszing/monaco-tailwindcss) [does not support TailwindCSS v4](https://github.com/remcohaszing/monaco-tailwindcss/issues/96). Getting it to support both TailwindCSS v3 and v4 will be annoying and is outside the scope of this project.
The app uses the Monaco Editor to allow you to write JSX and CSS code. The code is then transpiled using [esbuild](https://esbuild.github.io/) and [PostCSS](https://postcss.org/) with [TailwindCSS](https://tailwindcss.com/) to generate the preview. This project used [SWC](https://swc.rs/) originally, but I switched to esbuild because it's better suited for bundling multiple files. I made a custom Monaco TailwindCSS integration because, at the time of writing, [monaco-tailwindcss](https://github.com/remcohaszing/monaco-tailwindcss) [does not support TailwindCSS v4](https://github.com/remcohaszing/monaco-tailwindcss/issues/96). Getting it to support both TailwindCSS v3 and v4 will be annoying and is outside the scope of this project.

### Additional Features

- **Multi-file Support**: The app supports multiple files by creating a dependency graph and recursively resolving imports. It replaces imports with object URLs to enable seamless file linking.
- **NPM Package Support**: You can import almost any package from NPM that supports the browser using [Skypack](https://www.skypack.dev/). This includes TypeScript support, as the app automatically downloads type definitions and integrates them into monaco's type checker.
- **TailwindCSS Support**: The app uses TailwindCSS for styling. You can use any TailwindCSS class in your JSX code, and the app will automatically generate the corresponding CSS. Since this projects supports TailwindCSS v4, you can use the new CSS specific features like `@theme` and `@layer`. You may also opt out of using TailwindCSS by just not using it. If you include any `@import` statements in your CSS, the app will disable automatic TailwindCSS generation unless you specifically `@import "tailwindcss"`.
- **TailwindCSS v4 Support**: The app uses TailwindCSS for styling. You can use any TailwindCSS class in your JSX code, and the app will automatically generate the corresponding CSS. Since this projects supports TailwindCSS v4, you can use the new CSS specific features like `@theme` and `@layer`. You may also opt out of using TailwindCSS by just not using it. If you include any `@import` statements in your CSS, the app will disable automatic TailwindCSS generation unless you specifically `@import "tailwindcss"`.
- **CSS class autocompletion**: The app provides autocompletion for classes when writing CSS. It finds all the classes used in the JSX code and prompts them as suggestions in the CSS editor.

## License

Distributed under the MIT License. See `LICENSE` for more information.
Distributed under the MIT License. See [`LICENSE`](./LICENSE) for more information.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
"dependencies": {
"@ctrl/tinycolor": "^4.1.0",
"@monaco-editor/loader": "^1.5.0",
"@swc/wasm-web": "^1.11.13",
"@tailwindcss/language-service": "^0.14.12",
"@tailwindcss/vite": "^4.0.17",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/prop-types": "^15.7.14",
"ansi-to-html": "^0.7.2",
"console-feed": "^3.8.0",
"emmet-monaco-es": "^5.5.0",
"esbuild-wasm": "^0.25.2",
"highlight.js": "^11.11.1",
"monaco-editor": "^0.52.2",
"monaco-languageserver-types": "^0.4.0",
Expand Down
47 changes: 25 additions & 22 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { DEFAULT_CSS, DEFAULT_TSX } from './consts';
import { createLogger } from './logger';
import { MonacoContext } from './monaco/MonacoContext';
import { MonacoEditors } from './monaco/MonacoEditors';
import initSwc from '@swc/wasm-web';
import swcWasm from '@swc/wasm-web/wasm_bg.wasm?url';
import { initialize } from 'esbuild-wasm';
import esbuildUrl from 'esbuild-wasm/esbuild.wasm?url';
import { Hook, Unhook } from 'console-feed';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';

const logger = createLogger('App');

let swcStarted = false;
let swcInitialized = false;
let esbuildStarted = false;
let esbuildInitialized = false;

export default function App() {
const {
Expand All @@ -37,15 +37,15 @@ export default function App() {
const abortControllerRef = useRef<AbortController | null>(null);
const resizeCbRef = useRef<() => void>(() => {});

const [initialized, setInitialized] = useState(swcInitialized);
const [initialized, setInitialized] = useState(esbuildInitialized);
const [twInitialized, setTwInitialized] = useState(false);
const rebuildRef = useRef<m.editor.ITextModel[]>([]);

const handleChange = useCallback(
async (model: m.editor.ITextModel) => {
if (!initialized) {
logger.debug('SWC not initialized yet');
rebuildRef.current.push(model); // rebuild when swc is initialized
logger.debug('esbuild not initialized yet');
rebuildRef.current.push(model); // rebuild when esbuild is initialized
return;
}
logger.info('Model changed', model.uri.path);
Expand All @@ -64,20 +64,20 @@ export default function App() {
} else {
const filename = model.uri.path.split('/').pop();
if (filename) {
if (!compilerResultRef.current.tsFiles[filename]) {
compilerResultRef.current.tsFiles[filename] = {
filename: filename,
const shortFilename = filename.split('.').shift()!;
if (!compilerResultRef.current.tsFiles[shortFilename]) {
compilerResultRef.current.tsFiles[shortFilename] = {
filename: shortFilename,
contents: '',
newContents: model.getValue(),
builtJs: '',
success: false,
module: null,
objectUrl: '',
transformedJs: '',
classList: [],
};
} else {
compilerResultRef.current.tsFiles[filename].newContents =
compilerResultRef.current.tsFiles[shortFilename].newContents =
model.getValue();
}
}
Expand Down Expand Up @@ -119,9 +119,9 @@ export default function App() {
);

useEffect(() => {
// call handleChange with the rebuilt model when swc is initialized
if (initialized) {
logger.debug('SWC initialized, rebuilding models');
// call handleChange with the rebuilt model when esbuild is initialized
if (initialized && rebuildRef.current.length > 0) {
logger.debug('esbuild initialized, rebuilding models');
rebuildRef.current.forEach(model => {
handleChange(model);
});
Expand All @@ -131,18 +131,21 @@ export default function App() {
}, [handleChange, initialized]);

useEffect(() => {
if (swcStarted) {
if (esbuildStarted) {
return;
}
resetSize();
swcStarted = true;
async function importAndRunSwcOnMount() {
await initSwc(swcWasm);
esbuildStarted = true;
async function importAndRunEsbuildOnMount() {
await initialize({
wasmURL: esbuildUrl,
worker: true,
});
setInitialized(true);
swcInitialized = true;
logger.debug('SWC initialized');
esbuildInitialized = true;
logger.debug('esbuild initialized');
}
importAndRunSwcOnMount();
importAndRunEsbuildOnMount();
window.addEventListener('resize', () => resetSize());
}, [handleChange]);

Expand Down
69 changes: 6 additions & 63 deletions src/compiler/compilerResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import type * as m from 'monaco-editor';
import { createLogger } from '../logger';
import { TailwindHandler } from '../tailwind/TailwindHandler';
import { compileCss } from './parseCss';
import { type TypeScriptFile, compileTsx, transformJs } from './parseTsx';
import { type TypeScriptFile, compileTsx } from './parseTsx';
import {
TransformCssPropertiesOptions,
transformCssProperties,
} from './propertyTransform';
import { Module } from '@swc/wasm-web';

const logger = createLogger('compilerResult');

Expand Down Expand Up @@ -36,8 +35,6 @@ export type CompilerResult = {
twClasses: { name: string; css: string; color?: string }[];
/** The built JavaScript code. */
builtJs: string;
/** The parsed module */
module: Module | null;
/** The built CSS code. */
builtCss: string;
/** The transformed JavaScript code after applying `@property` transformations. */
Expand All @@ -64,7 +61,6 @@ export const defaultCompilerResult: CompilerResult = {
classes: [],
twClasses: [],
builtJs: '',
module: null,
builtCss: '',
transformedJs: '',
transformedCss: '',
Expand Down Expand Up @@ -112,7 +108,11 @@ export async function compile(
if (tsx !== previousResult.tsx) {
logger.debug('Compiling tsx');
const compiledTsx = await compileTsx(tsx, {
files: result.tsFiles,
signal: options.signal,
importMap: options.importMap,
setImportMap: options.setImportMap,
monaco: options.monaco,
});
if ('errors' in compiledTsx && compiledTsx.errors) {
result.errors.push(...compiledTsx.errors);
Expand All @@ -125,9 +125,6 @@ export async function compile(
result.tsxSuccess = true;
result.tsx = tsx;
}
if ('parsedModule' in compiledTsx && compiledTsx.parsedModule) {
result.module = compiledTsx.parsedModule;
}
if ('classList' in compiledTsx && compiledTsx.classList) {
result.allClasses = Array.from(compiledTsx.classList);
} else {
Expand All @@ -143,37 +140,6 @@ export async function compile(
result.tsxSuccess = previousResult.tsxSuccess;
}

for (const file in result.tsFiles) {
const tsFile = result.tsFiles[file];
if (tsFile.newContents !== tsFile.contents) {
logger.debug('Compiling tsx file', tsFile.filename);
const compiledTsx = await compileTsx(tsFile.newContents, {
signal: options.signal,
});
if ('errors' in compiledTsx && compiledTsx.errors) {
result.errors.push(...compiledTsx.errors);
}
if ('warnings' in compiledTsx && compiledTsx.warnings) {
result.warnings.push(...compiledTsx.warnings);
}
if ('code' in compiledTsx && compiledTsx.code) {
tsFile.builtJs = compiledTsx.code;
tsFile.success = true;
tsFile.contents = tsFile.newContents;
isDifferent = true;
}
if ('parsedModule' in compiledTsx && compiledTsx.parsedModule) {
tsFile.module = compiledTsx.parsedModule;
}
if ('classList' in compiledTsx && compiledTsx.classList) {
tsFile.classList = Array.from(compiledTsx.classList);
result.allClasses.push(...tsFile.classList);
} else {
tsFile.classList = [];
}
}
}

if (tailwindHandler) {
if (
css !== previousResult.css ||
Expand Down Expand Up @@ -222,33 +188,10 @@ export async function compile(
}

if (result.builtJs && result.builtCss && isDifferent) {
if (result.module) {
logger.debug('Transforming js');
const transformedJs = await transformJs(result.module, {
files: Object.values(result.tsFiles),
importMap: options.importMap,
monaco: options.monaco,
setImportMap: options.setImportMap,
signal: options.signal,
});

if ('errors' in transformedJs && transformedJs.errors) {
result.errors.push(...transformedJs.errors);
}
if ('warnings' in transformedJs && transformedJs.warnings) {
result.warnings.push(...transformedJs.warnings);
}
if ('code' in transformedJs && transformedJs.code) {
result.transformedJs = transformedJs.code;
}
} else {
result.transformedJs = result.builtJs;
}

logger.debug('Transforming properties');
const transformed = transformCssProperties(
result.builtCss,
result.transformedJs,
result.builtJs,
result.tsFiles,
transform,
);
Expand Down
Loading