diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4def6043..0a290412 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -2,11 +2,35 @@ name: Test on: [push, pull_request, workflow_dispatch] jobs: test: - runs-on: ubuntu-latest + strategy: + matrix: + env: + - { os: 'ubuntu-22.04', tool: 'bun' } + - { os: 'macos-12', tool: 'bun' } + # Windows support for bun install is not implemented yet + # - { os: 'windows-2022', tool: 'bun' } + # - { os: 'ubuntu-22.04', tool: 'node+jest' } + # - { os: 'macos-12', tool: 'node+jest' } + - { os: 'windows-2022', tool: 'node+jest' } + runs-on: ${{ matrix.env.os }} steps: - uses: actions/checkout@v3 + + # Testing `bun` - uses: oven-sh/setup-bun@v1 + if: ${{ matrix.env.tool == 'bun' }} - run: | + bun -v bun install bun install --no-save esbuild@^0.19.11 bun test --coverage + if: ${{ matrix.env.tool == 'bun' }} + + # Testing `node+jest` + - run: | + node -v && npm -v + npm install + npm install --no-save jest esbuild@^0.19.11 + # https://jestjs.io/docs/ecmascript-modules + node --experimental-vm-modules node_modules/jest/bin/jest --coverage + if: ${{ matrix.env.tool == 'node+jest' }} diff --git a/bun.lockb b/bun.lockb index 853b4383..95ba727a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/nuejs/test/render.test.js b/packages/nuejs/test/render.test.js index 2dfdb145..8c9dd7f1 100644 --- a/packages/nuejs/test/render.test.js +++ b/packages/nuejs/test/render.test.js @@ -197,7 +197,7 @@ const IF_SIBLING = ` test('If sibling', () => { const els = [{ label: 'First'}] const html = render(IF_SIBLING, { els }) - expect(html).toInclude('First') + expect(html).toContain('First') }) @@ -207,7 +207,7 @@ test(':for error', () => { render('
\n

Hey

\n
') } catch (e) { - expect(e.lineText).toInclude(':for="foo bar"') + expect(e.lineText).toContain(':for="foo bar"') expect(e.column).toBeGreaterThan(1) expect(e.line).toBe(3) } @@ -218,9 +218,18 @@ test('{ expr } error', () => { render('
\nHey { foo[0] } { title }
') } catch (e) { - expect(e.subexpr).toBe('foo[0]') - expect(e.line).toBe(2) - expect(e.column).toBe(9) + // Getting different results from different environments + // bun: TypeError: undefined is not an object (evaluating '_.foo[0]') + if (process.isBun) { + expect(e.subexpr).toBe('foo[0]') + expect(e.line).toBe(2) + expect(e.column).toBe(9) + } else { + // node: TypeError: Cannot read properties of undefined (reading '0') + expect(e.subexpr).toBe('0') + expect(e.line).toBe(2) + expect(e.column).toBe(13) + } } }) diff --git a/packages/nuekit/package.json b/packages/nuekit/package.json index 27eb9b2c..d1823ecc 100644 --- a/packages/nuekit/package.json +++ b/packages/nuekit/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "diff-dom": "^5.0.6", + "es-main": "^1.3.0", "js-yaml": "^4.1.0", "marked": "^9.1.6", "nuejs-core": "^0.3.0" diff --git a/packages/nuekit/src/browser/app-router.js b/packages/nuekit/src/browser/app-router.js index 8407f19e..57728dfc 100644 --- a/packages/nuekit/src/browser/app-router.js +++ b/packages/nuekit/src/browser/app-router.js @@ -1,6 +1,8 @@ import { onclick, loadPage, setSelected } from './page-router.js' +const is_browser = typeof window == 'object' + const fns = [] async function fire(path) { @@ -12,7 +14,7 @@ async function fire(path) { } // clear existing routes -addEventListener('before:route', () => { +is_browser && addEventListener('before:route', () => { fns.splice(0, fns.length) }) diff --git a/packages/nuekit/src/browser/page-router.js b/packages/nuekit/src/browser/page-router.js index e2bed154..803996ca 100644 --- a/packages/nuekit/src/browser/page-router.js +++ b/packages/nuekit/src/browser/page-router.js @@ -38,7 +38,7 @@ export async function loadPage(path) { // back button -addEventListener('popstate', e => { +is_browser && addEventListener('popstate', e => { const { path, is_spa } = e.state || {} if (path) loadPage(path) }) diff --git a/packages/nuekit/src/builder.js b/packages/nuekit/src/builder.js index 2d9a6934..62f9c32a 100644 --- a/packages/nuekit/src/builder.js +++ b/packages/nuekit/src/builder.js @@ -1,5 +1,5 @@ -import { join, extname } from 'node:path' +import { join, normalize } from 'node:path' export async function getBuilder(is_esbuild) { try { diff --git a/packages/nuekit/src/cli.js b/packages/nuekit/src/cli.js index ca1e704b..a0364d97 100755 --- a/packages/nuekit/src/cli.js +++ b/packages/nuekit/src/cli.js @@ -1,6 +1,7 @@ #!/usr/bin/env bun import { log, colors } from './util.js' +import esMain from 'es-main' // [-npe] --> [-n, -p, -e] export function expandArgs(args) { @@ -103,6 +104,9 @@ async function runCommand(args) { else if (cmd == 'stats') await nue.stats() } +// Only run main when called as real CLI +if (esMain(import.meta)) { + const args = getArgs(process.argv) // help @@ -126,11 +130,4 @@ if (args.help) { } } - - - - - - - - +} diff --git a/packages/nuekit/src/site.js b/packages/nuekit/src/site.js index 9c76c3d2..44d54781 100644 --- a/packages/nuekit/src/site.js +++ b/packages/nuekit/src/site.js @@ -1,5 +1,5 @@ -import { log, parseMarkdown, getParts, getAppDir, getDirs, colors } from './util.js' +import { log, parseMarkdown, getParts, getAppDir, getDirs, colors, getPosixPath } from './util.js' import { join, extname, basename, sep, parse as parsePath } from 'node:path' import { parse as parseNue } from 'nuejs-core/index.js' import { promises as fs } from 'node:fs' @@ -124,7 +124,8 @@ export async function createSite(args) { }).forEach(path => { const ext = extname(path) - arr.push('/' + join(dir, to_ext ? path.replace(ext, '.' + to_ext) : path)) + const subpath = to_ext ? path.replace(ext, '.' + to_ext) : path + arr.push('/' + getPosixPath(join(dir, subpath))) }) } catch (e) { diff --git a/packages/nuekit/src/util.js b/packages/nuekit/src/util.js index 6ec44c93..02b65e5c 100644 --- a/packages/nuekit/src/util.js +++ b/packages/nuekit/src/util.js @@ -1,6 +1,6 @@ /* misc stuff. think shame.css */ -import { sep, parse } from 'node:path' +import { sep, parse, normalize } from 'node:path' import { marked } from 'marked' import yaml from 'js-yaml' @@ -46,6 +46,7 @@ export const colors = getColorFunctions() /* path parts */ export function getParts(path) { + path = normalize(path) const { dir, name, base } = parse(path) const appdir = getAppDir(path) const url = getUrl(dir, name) @@ -54,20 +55,26 @@ export function getParts(path) { export function getAppDir(path) { + path = normalize(path) const [ appdir ] = path.split(sep) return appdir == path ? '.' : appdir } export function getDirs(dir) { + dir = normalize(dir) if (!dir) return [] const els = dir.split(sep) return els.map((el, i) => els.slice(0, i + 1).join(sep)) } export function getUrl(dir, name) { - let url = dir.replace('\\', '/') + '/' + let url = getPosixPath(dir) + '/' if (url[0] != '/') url = '/' + url // if (name != 'index') url += name + '.html' return url } + +export function getPosixPath(path) { + return path.replaceAll('\\', '/') +} diff --git a/packages/nuekit/test/kit-init.test.js b/packages/nuekit/test/kit-init.test.js new file mode 100644 index 00000000..2ec8dba7 --- /dev/null +++ b/packages/nuekit/test/kit-init.test.js @@ -0,0 +1,16 @@ +import { promises as fs } from 'node:fs' +import { join } from 'node:path' +import { init } from '../src/init.js' + +// temporary directory +const root = '_test' + +// setup and teardown +beforeAll(async () => await fs.mkdir(root, { recursive: true })) +afterAll(async () => await fs.rm(root, { recursive: true, force: true })) + +test('init dist/@nue dir', async () => { + await init({ dist: root, is_dev: true, esbuild: false }) + const names = await fs.readdir(join(root, '@nue')) + expect(names.length).toBeGreaterThan(7) +}) diff --git a/packages/nuekit/test/kit.test.js b/packages/nuekit/test/kit.test.js index 1499059f..60c326d3 100644 --- a/packages/nuekit/test/kit.test.js +++ b/packages/nuekit/test/kit.test.js @@ -5,7 +5,10 @@ import { createSite } from '../src/site.js' import { createKit } from '../src/nuekit.js' import { promises as fs } from 'node:fs' import { join, parse } from 'node:path' -import { init } from '../src/init.js' + +import { toMatchPath } from './match-path.js' + +expect.extend({ toMatchPath }) // temporary directory const root = '_test' @@ -120,7 +123,7 @@ test('content collection', async () => { expect(coll.length).toBe(2) expect(coll[0].url).toBe('/blog/first.html') expect(coll[1].title).toBe('Second') - expect(coll[1].dir).toBe('blog/nested') + expect(coll[1].dir).toMatchPath('blog/nested') expect(coll[1].slug).toBe('hey.html') }) @@ -159,20 +162,12 @@ test('getRequestPaths', async () => { // SPA root const path = 'admin/index.html' await write(path) - expect(await site.getRequestPaths('/admin/')).toMatchObject({ path }) - expect(await site.getRequestPaths('/admin/customers')).toMatchObject({ path }) + expect((await site.getRequestPaths('/admin/')).path).toMatchPath(path) + expect((await site.getRequestPaths('/admin/customers')).path).toMatchPath(path) expect(await site.getRequestPaths('/admin/readme.html')).toMatchObject({ path: '404.html' }) }) - -test('init dist/@nue dir', async () => { - await init({ dist: root, is_dev: true, esbuild: false }) - const names = await fs.readdir(join(root, '@nue')) - expect(names.length).toBeGreaterThan(7) -}) - - test('inline CSS', async () => { const kit = await getKit() await write('inline/style.css', 'body { margin: 0 }') @@ -183,7 +178,7 @@ test('inline CSS', async () => { expect(data.inline_css[0]).toEqual({ path: "/inline/style.css", content: "body { margin: 0 }"}) const html = await kit.renderPage('inline/index.md', data) - expect(html).toInclude('') + expect(html).toContain('') }) @@ -207,8 +202,8 @@ test('index.html', async() => { await kit.gen('index.html') const html = await readDist(kit.dist, 'index.html') - expect(html).toInclude('hotreload.js') - expect(html).toInclude('island="test"') + expect(html).toContain('hotreload.js') + expect(html).toContain('island="test"') }) test('index.md', async() => { @@ -217,9 +212,9 @@ test('index.md', async() => { await kit.gen('index.md') const html = await readDist(kit.dist, 'index.html') - expect(html).toInclude('hotreload.js') - expect(html).toInclude('Hey') - expect(html).toInclude('

Hey

') + expect(html).toContain('hotreload.js') + expect(html).toContain('Hey') + expect(html).toContain('

Hey

') }) @@ -230,11 +225,11 @@ test('bundle', async() => { // bun bundle const opts = { path: `./${root}/b.ts`, outdir: root, bundle: true } await buildJS(opts) - expect(await read('b.js')).toInclude('var foo = 30') + expect(await read('b.js')).toContain('var foo = 30') // esbuild bundl3 await buildJS({ ...opts, esbuild: true }) - expect(await read('b.js')).toInclude('var foo = 30') + expect(await read('b.js')).toContain('var foo = 30') }) test('syntax errors', async() => { diff --git a/packages/nuekit/test/match-path.js b/packages/nuekit/test/match-path.js new file mode 100644 index 00000000..270b2088 --- /dev/null +++ b/packages/nuekit/test/match-path.js @@ -0,0 +1,26 @@ +import { getPosixPath } from '../src/util.js' + +// https://stackoverflow.com/questions/67325342/how-to-run-os-agnostic-jest-test-files-that-check-paths +// https://jestjs.io/docs/expect#expectextendmatchers +export function toMatchPath(actual, expected) { + const { printReceived, printExpected, matcherHint } = this.utils + + const pass = getPosixPath(actual) == expected + + return { + pass, + message: () => pass + ? matcherHint('.not.toMatchPath') + + '\n\n' + + 'Expected path not to match:\n' + + ` ${printExpected(expected)}\n` + + 'Received:\n' + + ` ${printReceived(actual)}` + : matcherHint('.toMatchPath') + + '\n\n' + + 'Expected path to match:\n' + + ` ${printExpected(expected)}\n` + + 'Received:\n' + + ` ${printReceived(actual)}` + } +} diff --git a/packages/nuekit/test/misc.test.js b/packages/nuekit/test/misc.test.js index d6c091fa..2f70a354 100644 --- a/packages/nuekit/test/misc.test.js +++ b/packages/nuekit/test/misc.test.js @@ -5,6 +5,9 @@ import { match } from '../src/browser/app-router.js' import { renderHead } from '../src/layout.js' import { getArgs } from '../src/cli.js' +import { toMatchPath } from './match-path.js' + +expect.extend({ toMatchPath }) test('CLI args', () => { @@ -16,9 +19,9 @@ test('CLI args', () => { test('head', () => { const head = renderHead({ charset: 'foo', title: 'Hey', preload_image: 'hey.png' }) - expect(head).toInclude('meta charset="foo"') - expect(head).toInclude('Hey') - expect(head).toInclude('') + expect(head).toContain('meta charset="foo"') + expect(head).toContain('Hey') + expect(head).toContain('') }) @@ -38,8 +41,8 @@ test('app router', async () => { test('path parts', () => { const parts = getParts('docs/glossary/semantic-css.md') expect(parts.url).toBe('/docs/glossary/semantic-css.html') - expect(parts.dir).toBe('docs/glossary') - expect(parts.appdir).toBe('docs') + expect(parts.dir).toMatchPath('docs/glossary') + expect(parts.appdir).toMatchPath('docs') expect(parts.slug).toBe('semantic-css.html') }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 820114a5..d89ad05c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: diff-dom: specifier: ^5.0.6 version: 5.1.2 + es-main: + specifier: ^1.3.0 + version: 1.3.0 js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -29,15 +32,6 @@ importers: specifier: ^0.3.0 version: link:../nuejs - packages/nuemark: - dependencies: - js-yaml: - specifier: ^4.1.0 - version: 4.1.0 - marked: - specifier: ^9.1.2 - version: 9.1.6 - packages: /argparse@2.0.1: @@ -80,6 +74,10 @@ packages: engines: {node: '>=0.12'} dev: false + /es-main@1.3.0: + resolution: {integrity: sha512-AzORKdz1Zt97TzbYQnIrI3ZiibWpRXUfpo/w0xOJ20GpNYd2bd3MU9m31zS/aJ1TJl6JfLTok83Y8HjNunYT0A==} + dev: false + /htmlparser2@9.0.0: resolution: {integrity: sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==} dependencies: