From fa7338ba80bdc7046382fa5c9de4219c668acc13 Mon Sep 17 00:00:00 2001 From: Fritz Lin Date: Mon, 15 Jan 2024 22:59:28 +0800 Subject: [PATCH] Fix Windows CI via node+jest (as alternative to bun) --- .github/workflows/test.yaml | 26 +++++++++++++++++++++- package.json | 3 +++ packages/nuejs/test/render.test.js | 15 ++++++++++--- packages/nuekit/package.json | 1 + packages/nuekit/src/browser/app-router.js | 4 +++- packages/nuekit/src/browser/page-router.js | 13 +++++------ packages/nuekit/src/cli.js | 13 +++++------ packages/nuekit/src/site.js | 5 +++-- packages/nuekit/src/util.js | 11 +++++++-- packages/nuekit/test/kit-init.test.js | 19 ++++++++++++++++ packages/nuekit/test/match-path.js | 26 ++++++++++++++++++++++ packages/nuekit/test/misc.test.js | 8 +++++-- packages/nuekit/test/nuekit.test.js | 24 +++++++++----------- pnpm-lock.yaml | 7 ++++++ 14 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 packages/nuekit/test/kit-init.test.js create mode 100644 packages/nuekit/test/match-path.js diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index adaca267..558e4af9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,11 +10,35 @@ on: 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 jest-extended esbuild@^0.19.11 + # https://jestjs.io/docs/ecmascript-modules + node --experimental-vm-modules node_modules/jest/bin/jest --runInBand --coverage + if: ${{ matrix.env.tool == 'node+jest' }} diff --git a/package.json b/package.json index d989b25b..5d5abdf2 100644 --- a/package.json +++ b/package.json @@ -9,5 +9,8 @@ "engines": { "bun": ">=1", "node": ">=18" + }, + "jest": { + "setupFilesAfterEnv": ["jest-extended/all"] } } diff --git a/packages/nuejs/test/render.test.js b/packages/nuejs/test/render.test.js index 2dfdb145..e0d0fe74 100644 --- a/packages/nuejs/test/render.test.js +++ b/packages/nuejs/test/render.test.js @@ -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 73584e04..650615c1 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", "import-meta-resolve": "^4.0.0", "js-yaml": "^4.1.0", "nuejs-core": "^0.3.0", diff --git a/packages/nuekit/src/browser/app-router.js b/packages/nuekit/src/browser/app-router.js index 50d82a8f..cfeeb645 100644 --- a/packages/nuekit/src/browser/app-router.js +++ b/packages/nuekit/src/browser/app-router.js @@ -2,6 +2,8 @@ // Router for single-page applications import { onclick, loadPage, setSelected } from './page-router.js' +const is_browser = typeof window == 'object' + const fns = [] async function fire(path) { @@ -13,7 +15,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 452fab69..7ea24835 100644 --- a/packages/nuekit/src/browser/page-router.js +++ b/packages/nuekit/src/browser/page-router.js @@ -48,13 +48,6 @@ export async function loadPage(path) { } -// back button -addEventListener('popstate', e => { - const { path, is_spa } = e.state || {} - if (path) loadPage(path) -}) - - // setup linking export function onclick(root, fn) { @@ -103,6 +96,12 @@ if (is_browser) { // initial selected setSelected(location.pathname) + + // back button + addEventListener('popstate', e => { + const { path } = e.state || {} + if (path) loadPage(path) + }) } diff --git a/packages/nuekit/src/cli.js b/packages/nuekit/src/cli.js index 367801c9..3d8ca7ac 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) { @@ -104,6 +105,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 @@ -127,11 +131,4 @@ if (args.help) { } } - - - - - - - - +} diff --git a/packages/nuekit/src/site.js b/packages/nuekit/src/site.js index 4391aa5a..9449583b 100644 --- a/packages/nuekit/src/site.js +++ b/packages/nuekit/src/site.js @@ -1,6 +1,6 @@ import { join, extname, basename, sep, parse as parsePath } from 'node:path' -import { log, getParts, getAppDir, getDirs, colors } from './util.js' +import { log, getParts, getAppDir, getDirs, colors, getPosixPath } from './util.js' import { parse as parseNue } from 'nuejs-core/index.js' import { nuemark } from 'nuemark/index.js' import { promises as fs } from 'node:fs' @@ -125,7 +125,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 acdd39b4..e2d13205 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' export function log(msg, extra='') { @@ -29,6 +29,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) @@ -37,6 +38,7 @@ export function getParts(path) { export function getAppDir(path) { + path = normalize(path) const [ appdir ] = path.split(sep) return appdir == path ? '' : appdir } @@ -44,14 +46,19 @@ export function getAppDir(path) { // getDirs('a/b/c') --> ['a', 'a/b', 'a/b/c'] export function getDirs(dir) { if (!dir) return [] + dir = normalize(dir) 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..72f31f5a --- /dev/null +++ b/packages/nuekit/test/kit-init.test.js @@ -0,0 +1,19 @@ +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.rm(root, { recursive: true, force: true }) + 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/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 7753ccf7..5a1bb196 100644 --- a/packages/nuekit/test/misc.test.js +++ b/packages/nuekit/test/misc.test.js @@ -6,6 +6,10 @@ 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 }) + const lcss = await findModule('lightningcss') const stylus = await findModule('stylus') @@ -85,8 +89,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/packages/nuekit/test/nuekit.test.js b/packages/nuekit/test/nuekit.test.js index 04c9b11f..2d18cc43 100644 --- a/packages/nuekit/test/nuekit.test.js +++ b/packages/nuekit/test/nuekit.test.js @@ -5,13 +5,19 @@ 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' // setup and teardown -beforeAll(async () => await fs.mkdir(root, { recursive: true })) +beforeAll(async () => { + await fs.rm(root, { recursive: true, force: true }) + await fs.mkdir(root, { recursive: true }) +}) afterAll(async () => await fs.rm(root, { recursive: true, force: true })) // helper function for creating files to the root directory @@ -124,7 +130,7 @@ test('content collection', async () => { expect(coll[0].url).toBe('/blog/first.html') expect(coll[0].title).toBe('First') 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') }) @@ -174,20 +180,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 }') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63c44393..34b1189c 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 import-meta-resolve: specifier: ^4.0.0 version: 4.0.0 @@ -83,6 +86,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: