Skip to content

Commit

Permalink
Merge pull request #461 from nobkd/feaat/bun-lcss
Browse files Browse the repository at this point in the history
feat!: bun css support
  • Loading branch information
tipiirai authored Feb 12, 2025
2 parents f1cf330 + d672747 commit ba8ab11
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 34 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.2",
"node": ">=18"
},
"scripts": {
Expand Down
2 changes: 2 additions & 0 deletions packages/nuejs.org/docs/command-line-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Usage

- **-b or --esbuild**: Uses esbuild as the bundler for your assets. Note that you need to install esbuild manually for this to work.

- **-l or --lcss**: Uses lightningcss as the minifier for your CSS. Note that you need to install lightningcss manually for this to work.

- **-P or --port**: Sets the port number to serve the site on. This is particularly useful if the default port (`8080`) is already in use.

### File Matches
Expand Down
6 changes: 4 additions & 2 deletions packages/nuejs.org/docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ links:
soc: //en.wikipedia.org/wiki/Separation_of_concerns
```
### lightning_css
### minify_css
This setting controls the processing of CSS by [Lightning CSS](//lightningcss.dev/), a tool that optimizes CSS for better performance. By default, this feature is enabled (`true`), which means that CSS will be processed for improvements. Setting this to `false` disables the default processing, and the CSS is served directly as is, which may be useful for debugging or specific use cases where processing is not desired.
This setting controls the processing of CSS by Bun or [Lightning CSS](//lightningcss.dev/), which optimize CSS for better performance. By default, this feature is enabled (`true`), which means that CSS will be processed for improvements. Setting this to `false` disables the default processing, and the CSS is served directly as is, which may be useful for debugging or specific use cases where processing is not desired.

### native_css_nesting

Currently only available with Lightning CSS (`--lcss` option).

Determines whether to use native CSS nesting instead of converting them to un-nested style rules that are supported in all browsers. Native CSS nesting allows you to write more intuitive and organized CSS by nesting styles. Setting this to `true` generates a smaller CSS output, which can enhance performance, but it may not be supported by older browsers. Always check the current [Can I Use](//caniuse.com/css-nesting) statistics for details on browser compatibility.

### port
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.2",
"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
71 changes: 58 additions & 13 deletions packages/nuekit/src/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,41 @@ 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
if (!is_lcss) {
const v = Bun.version.split('.').map(i => parseInt(i))
if (!(v[0] >= 1 && v[1] >= 2)) throw new Error('Bun version too low')
}
return cssBuilder
} catch {
throw 'CSS bundler not found. Please use Bun >=1.2 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 +68,55 @@ 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,
plugins: [{
name: 'mark non-css files as external',
setup(build) {
build.onResolve({ filter: /.*/, namespace: 'file' }, args => {
// Mark non-css files external. Might need some more handling later on?
if (args.kind === 'internal') return { ...args, external: true }
})
},
}],
})).outputs[0].text()

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

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 || (err.fileName && (await fs.readFile(err.fileName, 'utf-8')).split(/\r\n|\r|\n/)[err.loc.line - 1]),
text: err?.message || err?.data?.type,
...(err?.position || err?.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
8 changes: 4 additions & 4 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 @@ -176,9 +176,9 @@ export async function createKit(args) {

async function processCSS({ path, base, dir }) {
const data = await site.getData()
const css = data.lightning_css === false ?
const css = data.minify_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 ba8ab11

Please sign in to comment.