Skip to content
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

feat!: bun css support #461

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
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
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
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