Skip to content

Commit

Permalink
feat: use bun css bundler
Browse files Browse the repository at this point in the history
  • Loading branch information
nobkd committed Jan 26, 2025
1 parent 5ecb5ac commit 1c9d3b4
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 31 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Install and test with Bun
run: |
bun -v
bun install --no-save esbuild
bun install --no-save esbuild lightningcss
bun test --coverage
if: ${{ matrix.tool == 'bun' }}

Expand All @@ -48,6 +48,6 @@ jobs:
- name: Install and test with Node
run: |
node -v && npm -v
npm install --no-save jest jest-extended esbuild
npm install --no-save jest jest-extended esbuild lightningcss
npm test -- --coverage
if: ${{ matrix.tool == 'node+jest' }}
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 @@ -8,7 +8,7 @@
"packages/*"
],
"engines": {
"bun": ">=1",
"bun": ">=1.1.34",
"node": ">=18"
},
"scripts": {
Expand Down
3 changes: 1 addition & 2 deletions packages/nuekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"nue": "./src/cli.js"
},
"engines": {
"bun": ">= 1",
"bun": ">= 1.1.34",
"node": ">= 18"
},
"scripts": {
Expand All @@ -25,7 +25,6 @@
"es-main": "^1.3.0",
"import-meta-resolve": "^4.1.0",
"js-yaml": "^4.1.0",
"lightningcss": "^1.27.0",
"nue-glow": "*",
"nuejs-core": "*",
"nuemark": "*"
Expand Down
60 changes: 47 additions & 13 deletions packages/nuekit/src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,37 @@ import { promises as fs } from 'node:fs'
import { join } from 'node:path'

import { resolve } from 'import-meta-resolve'
import { Features, bundleAsync } from 'lightningcss'

// don't reuse saved builder when in test mode
const isTest = process.env.NODE_ENV == 'test'

let jsBuilder
export async function getBuilder(is_esbuild) {
export async function getJsBuilder(is_esbuild) {
if (!isTest && jsBuilder) return jsBuilder

try {
return jsBuilder = is_esbuild ? await import(resolve('esbuild', `file://${process.cwd()}/`)) : Bun
} catch {
throw 'Bundler not found. Please use Bun or install esbuild'
throw 'JS bundler not found. Please use Bun or install esbuild'
}
}

let cssBuilder
export async function getCssBuilder(is_lcss) {
if (!isTest && cssBuilder) return cssBuilder

try {
cssBuilder = is_lcss ? await import(resolve('lightningcss', `file://${process.cwd()}/`)) : Bun
return cssBuilder
} catch {
throw 'CSS bundler not found. Please use Bun >=1.1.34 or install lightningcss'
}
}

export async function buildJS(args) {
const { outdir, toname, minify, bundle } = args
const is_esbuild = args.esbuild || !process.isBun
const builder = await getBuilder(is_esbuild)
const builder = await getJsBuilder(is_esbuild)

const opts = {
external: bundle ? ['../@nue/*', '/@nue/*'] : is_esbuild ? undefined : ['*'],
Expand Down Expand Up @@ -53,25 +64,48 @@ export async function buildJS(args) {

} catch ({ errors }) {
const [err] = errors
const error = { text: err.message || err.text, ...(err.location || err.position) }
const error = { text: err.message || err.text, ...(err.position || err.location) }
error.title = error.text.includes('resolve') ? 'Import error' : 'Syntax error'
delete error.file
throw error
}
}

export async function lightningCSS(filename, minify, opts = {}) {
let include = Features.Colors
if (!opts.native_css_nesting) include |= Features.Nesting
export async function buildCSS(filename, minify, opts = {}, lcss) {
const is_lcss = lcss || !process.isBun
const builder = await getCssBuilder(is_lcss)

let include
if (is_lcss) {
include = builder.Features.Colors
if (opts.native_css_nesting) include |= builder.Features.Nesting
}

try {
return (await bundleAsync({ filename, include, minify })).code?.toString()
} catch ({ fileName, loc, data }) {
if (is_lcss) return (await builder.bundleAsync({
filename,
include,
minify,
})).code.toString()

else return await (await builder.build({
entrypoints: [filename],
minify,
throw: true,
experimentalCss: true,
// mark basically everything but `.css` as external (TODO: find better solution to this one)
external: ['*.svg', '*.png', '*.jpg', '*.jpeg', '*.webp', '*.ico', '*.woff', '*.woff2', '*.ttf', '*.otf'],
})).outputs[0].text()

} catch (e) {
// bun aggregate error
const [err] = e.errors || [null]

throw {
title: 'CSS syntax error',
lineText: (await fs.readFile(fileName, 'utf-8')).split(/\r\n|\r|\n/)[loc.line - 1],
text: data.type,
...loc
lineText: err?.position?.lineText || (await fs.readFile(e.fileName, 'utf-8')).split(/\r\n|\r|\n/)[e.loc.line - 1],
text: err?.message || e.data.type,
...(err?.position || e.loc),
}
}
}
3 changes: 2 additions & 1 deletion packages/nuekit/src/cli-help.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Options
-p or --production Build production version / Show production stats
-e or --environment Read extra options to override defaults in site.yaml
-n or --dry-run Show what would be built. Does not create outputs
-b or --esbuild Use esbuild as bundler. Please install it manually
-b or --esbuild Use esbuild as JS bundler. Please install it manually
-l or --lcss Use lightningcss as CSS bundler. Please install it manually
-P or --port Port to serve the site on
File matches
Expand Down
1 change: 1 addition & 0 deletions packages/nuekit/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function getArgs(argv) {
else if (['-h', '--help'].includes(arg)) args.help = true
else if (['-v', '--verbose'].includes(arg)) args.verbose = true
else if (['-b', '--esbuild'].includes(arg)) args.esbuild = true
else if (['-l', '--lcss'].includes(arg)) args.lcss = true
else if (['-d', '--deploy'].includes(arg)) args.deploy = args.is_prod = true
else if (['-I', '--incremental'].includes(arg)) args.incremental = true

Expand Down
6 changes: 3 additions & 3 deletions packages/nuekit/src/nuekit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join, parse as parsePath } from 'node:path'
import { parse as parseNue, compile as compileNue } from 'nuejs-core'
import { nuedoc } from 'nuemark'

import { lightningCSS, buildJS } from './builder.js'
import { buildCSS, buildJS } from './builder.js'
import { createServer, send } from './nueserver.js'
import { printStats, categorize } from './stats.js'
import { initNueDir } from './init.js'
Expand All @@ -20,7 +20,7 @@ const DOCTYPE = '<!doctype html>\n\n'


export async function createKit(args) {
const { root, is_prod, esbuild, dryrun } = args
const { root, is_prod, esbuild, lcss, dryrun } = args

// site: various file based functions
const site = await createSite(args)
Expand Down Expand Up @@ -178,7 +178,7 @@ export async function createKit(args) {
const data = await site.getData()
const css = data.lightning_css === false ?
await read(path) :
await lightningCSS(join(root, path), is_prod, data)
await buildCSS(join(root, path), is_prod, data, lcss)
await write(css, dir, base)
return { css }
}
Expand Down
35 changes: 26 additions & 9 deletions packages/nuekit/test/misc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs'
import { join } from 'node:path'

import { match } from '../src/browser/app-router.js'
import { lightningCSS } from '../src/builder.js'
import { buildCSS } from '../src/builder.js'
import { getArgs } from '../src/cli.js'
import { create } from '../src/create.js'
import { parsePathParts } from '../src/util.js'
Expand All @@ -29,34 +29,51 @@ async function write(filename, code) {
}


test('Lightning CSS errors', async () => {
test('CSS errors', async () => {
const code = 'body margin: 0 }'
const filepath = await write('lcss.css', code)

// lcss
try {
await lightningCSS(filepath, true)
await buildCSS(filepath, true, undefined, true)
} catch (e) {
expect(e.lineText).toBe(code)
expect(e.line).toBe(1)
}

// bcss
try {
await buildCSS(filepath, true)
} catch (e) {
expect(e.lineText).toBe(code)
expect(e.line).toBe(1)
}
})

test('Lightning CSS @import bundling', async () => {
test('CSS @import bundling', async () => {
const code = 'body { margin: 0 }'
const filename = 'cssimport.css'
await write(filename, code)
const filepath = await write('lcss.css', `@import "${filename}"`)

const css = await lightningCSS(filepath, true)
expect(css).toBe(code.replace(/\s/g, ''))
const lcss = await buildCSS(filepath, true, undefined, true)
const bcss = await buildCSS(filepath, true)

const min = code.replace(/\s/g, '')
expect(lcss).toContain(min)
expect(bcss).toContain(min)
})

test('Lightning CSS', async () => {
test('CSS', async () => {
const code = 'body { margin: 0 }'
const filepath = await write('lcss.css', code)

const css = await lightningCSS(filepath, true)
expect(css).toBe(code.replace(/\s/g, ''))
const lcss = await buildCSS(filepath, true, undefined, true)
const bcss = await buildCSS(filepath, true)

const min = code.replace(/\s/g, '')
expect(lcss).toContain(min)
expect(bcss).toContain(min)
})

test('CLI args', () => {
Expand Down

0 comments on commit 1c9d3b4

Please sign in to comment.