diff --git a/packages/nuedom/src/compiler/compiler.js b/packages/nuedom/src/compiler/compiler.js index 7e22fe86..f571b301 100644 --- a/packages/nuedom/src/compiler/compiler.js +++ b/packages/nuedom/src/compiler/compiler.js @@ -27,7 +27,7 @@ const RE_FN = /(script|h_fn|fn):\s*(['"`])([^\2]*?)\2/g export function compileJS(js) { return js.replace(RE_FN, function(_, key, __, expr) { - return key == 'script' ? `${key}: function() { ${expr.trim().replaceAll('\\n', '\n')} \n\t\t}` + return key == 'script' ? `${key}: function() { ${expr.trim().replace(/\\r/g, '').replace(/\\n/g, '\n')} \n\t\t}` : `${key}: ${compileFn(expr, key[0] == 'h')}` }) } @@ -48,5 +48,3 @@ export function compileFn(str, is_event) { } return '_=>' + (is_simple ? str : `(${str})`) } - - diff --git a/packages/nuekit/src/cmd/build.js b/packages/nuekit/src/cmd/build.js index d9f2c772..a960cb6d 100644 --- a/packages/nuekit/src/cmd/build.js +++ b/packages/nuekit/src/cmd/build.js @@ -1,6 +1,6 @@ import { mkdir, rmdir, writeFile, unlink } from 'node:fs/promises' -import { join, sep } from 'node:path' +import path, { join } from 'node:path' import { tmpdir } from 'os' import { generateSitemap, generateFeed } from '../render/feed' @@ -55,7 +55,12 @@ export async function buildAll(subset, args) { await createSystemFiles(dist, init) // build files - subset = subset.filter(el => !el.is_yaml && !el.dir.startsWith(`@shared${sep}data`)) + const SHARED_DATA_DIR = `@shared/data`; + + subset = subset.filter(el => { + const dir = el.dir ? path.posix.normalize(el.dir) : undefined + return !el.is_yaml && !dir?.startsWith(SHARED_DATA_DIR); + }); await Promise.all(subset.map(async asset => { try { diff --git a/packages/nuekit/src/cmd/serve.js b/packages/nuekit/src/cmd/serve.js index dc91eec2..12f04003 100644 --- a/packages/nuekit/src/cmd/serve.js +++ b/packages/nuekit/src/cmd/serve.js @@ -1,5 +1,5 @@ -import { extname, join } from 'node:path' +import path, { extname, join } from 'node:path' import { generateSitemap, generateFeed } from '../render/feed' import { createServer, broadcast } from '../tools/server' @@ -59,6 +59,20 @@ export async function serve(site, { silent }) { // server requests export async function onServe(url, assets, opts={}) { const { params={}, conf={} } = opts + + // Redirect to trailing slash if it's a directory + if (!url.endsWith('/') && !extname(url)) { + const basePath = url.slice(1); + const htmlPath = basePath ? `${basePath}/index.html` : 'index.html'; + const mdPath = basePath ? `${basePath}/index.md` : 'index.md'; + + const indexAsset = assets.find(asset => asset.path === htmlPath || asset.path === mdPath); + + if (indexAsset) { + return { redirect: url + '/' }; + } + } + const asset = findAssetByURL(url, assets) const ext = extname(url) @@ -104,9 +118,10 @@ export async function onServe(url, assets, opts={}) { const sysfiles = getSystemFiles() -export function findAssetByURL(url, assets=[]) { +export function findAssetByURL(url, assets = []) { + const targetPath = url.endsWith('.html.js') ? url.slice(1, -3) : url return [...sysfiles, ...assets].find(asset => { - return url.endsWith('.html.js') ? asset.path == url.slice(1, -3) - : asset.url == url + const assetPath = asset.path ? path.posix.normalize(asset.path) : undefined + return assetPath === targetPath || asset.url === url }) } \ No newline at end of file diff --git a/packages/nuekit/src/deps.js b/packages/nuekit/src/deps.js index be28059d..fac717e9 100644 --- a/packages/nuekit/src/deps.js +++ b/packages/nuekit/src/deps.js @@ -1,8 +1,8 @@ -import { join, normalize, dirname, extname, basename, sep } from 'node:path' +import { posix, join, dirname, extname, basename } from 'node:path' // app, lib, server are @shared, but not auto-included -const AUTO_INCLUDED = ['data', 'design', 'ui'].map(dir => join('@shared', dir)) +const AUTO_INCLUDED = ['data', 'design', 'ui'].map(dir => posix.join('@shared', dir)) const ASSET_TYPES = ['.html', '.js', '.ts', '.yaml', '.css'] @@ -40,12 +40,12 @@ function isDep(page_path, asset_path, all_paths) { if (dir == '.') return true // shared dir -> auto-included - if (AUTO_INCLUDED.some(dir => asset_path.startsWith(dir + sep))) return true + if (AUTO_INCLUDED.some(dir => asset_path.startsWith(dir + '/'))) return true // SPA: entire app tree if (basename(page_path) == 'index.html') { const dir = dirname(page_path) - return dir == '.' ? !all_paths.some(el => extname(el) == '.md') : asset_path.startsWith(dir + sep) + return dir == '.' ? !all_paths.some(el => extname(el) == '.md') : asset_path.startsWith(dir + '/') } // index.md -> home dir @@ -58,7 +58,7 @@ function isDep(page_path, asset_path, all_paths) { // check if asset is in ui of any parent directory return page_dirs.some(pageDir => { - const ui_dir = pageDir ? join(pageDir, 'ui') : 'ui' + const ui_dir = pageDir ? posix.join(pageDir, 'ui') : 'ui' return asset_dir == ui_dir || asset_dir == pageDir }) } @@ -66,6 +66,6 @@ function isDep(page_path, asset_path, all_paths) { // parseDirs('a/b/c') --> ['a', 'a/b', 'a/b/c'] export function parseDirs(dir) { - const els = normalize(dir).split(sep) + const els = dir.split('/') return els.map((el, i) => els.slice(0, i + 1).join('/')) } \ No newline at end of file diff --git a/packages/nuekit/src/file.js b/packages/nuekit/src/file.js index b6270db8..b87b559c 100644 --- a/packages/nuekit/src/file.js +++ b/packages/nuekit/src/file.js @@ -1,10 +1,10 @@ -import { parse, sep, join } from 'node:path' +import { posix, parse } from 'node:path' import { lstat } from 'node:fs/promises' export async function createFile(root, path) { try { - const rootpath = join(root, path) + const rootpath = posix.join(root, path) const stat = await lstat(rootpath) const info = getFileInfo(path) const file = Bun.file(rootpath) @@ -24,19 +24,19 @@ export async function createFile(root, path) { } async function copy(dist) { - const to = join(dist, path) + const to = posix.join(dist, path) await Bun.write(to, file) return to } async function write(dist, content, ext) { const toname = ext ? info.base.replace(info.ext, ext) : info.base - const to = join(dist, info.dir, toname) + const to = posix.join(dist, info.dir, toname) await Bun.write(to, content) return to } - return { ...info, rootpath, mtime, text, copy, write, flush } + return { ...info, rootpath: posix.normalize(rootpath), mtime, text, copy, write, flush } } catch (error) { console.warn(`Warning: Error reading ${path}: ${error.message}`) @@ -45,7 +45,7 @@ export async function createFile(root, path) { } export function getFileInfo(path) { - const info = parse(path) + const info = parse(posix.normalize(path)) delete info.root const { ext, dir } = info @@ -53,9 +53,9 @@ export function getFileInfo(path) { const url = getURL(info) const slug = getSlug(info) - if (dir.includes(sep)) info.basedir = dir.split(sep)[0] + if (dir.includes('/')) info.basedir = dir.split('/')[0] - return { ...info, path, type, url, slug, [`is_${type}`]: true } + return { ...info, path: posix.normalize(path), type, url, slug, [`is_${type}`]: true } } export function getURL(file) { @@ -67,7 +67,7 @@ export function getURL(file) { } if (ext == '.ts') ext = '.js' - const els = dir.split(sep) + const els = dir ? dir.split('/') : [] els.push(name + ext) return `/${ els.join('/') }`.replace('//', '/') diff --git a/packages/nuekit/src/site.js b/packages/nuekit/src/site.js index ae990321..08ee1ca5 100644 --- a/packages/nuekit/src/site.js +++ b/packages/nuekit/src/site.js @@ -1,5 +1,5 @@ -import { parse, sep } from 'node:path' +import { posix, parse } from 'node:path' import { fswalk } from './tools/fswalk' import { createAsset } from './asset' import { createFile } from './file' @@ -71,7 +71,12 @@ export function sortAssets(items) { export async function mergeSharedData(assets, data={}) { - const shared = assets.filter(a => a.dir?.startsWith(`@shared${sep}data`)) + const SHARED_DATA_DIR = '@shared/data'; + + const shared = assets.filter(a => { + const dir = a.dir ? posix.normalize(a.dir) : null + return dir?.startsWith(SHARED_DATA_DIR); + }); const statics = shared.filter(f => f.is_json || f.is_yaml) const dataset = await Promise.all(statics.map(f => f.parse())) @@ -88,5 +93,3 @@ export async function mergeSharedData(assets, data={}) { return data } - - diff --git a/packages/nuekit/src/tools/fswalk.js b/packages/nuekit/src/tools/fswalk.js index c0b3229e..16d30def 100644 --- a/packages/nuekit/src/tools/fswalk.js +++ b/packages/nuekit/src/tools/fswalk.js @@ -1,6 +1,6 @@ import { readdir, stat } from 'node:fs/promises' -import { parse, join, relative } from 'node:path' +import { parse, join, relative, sep } from 'node:path' export function matches(path, patterns) { return patterns.some(pattern => path.includes(pattern)) @@ -24,7 +24,7 @@ async function walkDirectory(dir, root, opts) { for (const entry of entries) { const fullPath = join(dir, entry.name) - const relativePath = relative(root, fullPath) + let relativePath = relative(root, fullPath).split(sep).join('/') if (isSkipped(relativePath) || matches(relativePath, ignore)) continue diff --git a/packages/nuekit/src/tools/fswatch.js b/packages/nuekit/src/tools/fswatch.js index 66c8691e..5712a65f 100644 --- a/packages/nuekit/src/tools/fswatch.js +++ b/packages/nuekit/src/tools/fswatch.js @@ -1,6 +1,6 @@ import { promises as fs, watch } from 'node:fs' -import { join, extname } from 'node:path' +import { join, extname, posix } from 'node:path' import { fswalk, matches } from './fswalk' // Main fswatch function @@ -9,10 +9,11 @@ export function fswatch(root, opts = {}) { // const shouldProcess = createDeduplicator() // Start watching - const watcher = watch(root, { recursive: true }, async function(event, path) { + const watcher = watch(root, { recursive: true }, async function (event, raw_path) { const { onupdate, onremove } = watcher - if (!path) return - + if (!raw_path) return + const path = posix.normalize(raw_path) + // Skip editor backup files if (isEditorBackup(path)) return @@ -31,6 +32,7 @@ export function fswatch(root, opts = {}) { const paths = await fswalk(fullPath, ignore) for (const subPath of paths) { + // Ensure the full path is also POSIX normalized await onupdate(join(path, subPath)) } } @@ -38,11 +40,9 @@ export function fswatch(root, opts = {}) { if (onupdate && extname(path)) { await onupdate(path) } - } catch (error) { if (error.errno == -2 && onremove) { await onremove(path) - } else if (error.errno != -2) { console.error('fswatch error:', error) } @@ -66,4 +66,4 @@ export function createDeduplicator() { lastTime = now return true } -} \ No newline at end of file +} diff --git a/packages/nuekit/src/tools/server.js b/packages/nuekit/src/tools/server.js index 0b22ba27..5fb6610f 100644 --- a/packages/nuekit/src/tools/server.js +++ b/packages/nuekit/src/tools/server.js @@ -22,6 +22,11 @@ export function createServer({ port=4000, handler }, callback) { // res = { content, type, status } || HTML if (res) { + // Handle redirects from onServe + if (res.redirect) { + return Response.redirect(res.redirect, 301) + } + return new Response(res.content || res, { headers: { 'Content-Type': res.type || 'text/html; charset=utf-8' }, status: res.status || 200 @@ -61,4 +66,3 @@ export function broadcast(data) { try { ws.send(JSON.stringify(data)) } catch(e) {} }) } - diff --git a/packages/nuekit/test/cmd/create.test.js b/packages/nuekit/test/cmd/create.test.js index 3cef5dba..409307ad 100644 --- a/packages/nuekit/test/cmd/create.test.js +++ b/packages/nuekit/test/cmd/create.test.js @@ -1,4 +1,3 @@ - import { create, unzip, getLocalZip, fetchZip } from '../../src/cmd/create' import { rm, readdir } from 'node:fs/promises' diff --git a/packages/nuekit/test/fswatch.test.js b/packages/nuekit/test/fswatch.test.js index b950b4ab..140faf6d 100644 --- a/packages/nuekit/test/fswatch.test.js +++ b/packages/nuekit/test/fswatch.test.js @@ -1,5 +1,6 @@ import { test, expect } from 'bun:test' import { promises as fs } from 'node:fs' +import { tmpdir } from 'node:os' import { join } from 'node:path' import { @@ -35,7 +36,7 @@ test.skip('deduplicator blocks rapid events', async () => { }) test('watches single file changes', async () => { - const tmpDir = await fs.mkdtemp('/tmp/fswatch-test-') + const tmpDir = await fs.mkdtemp(join(tmpdir(), 'fswatch-test-')) const testFile = join(tmpDir, 'test.txt') const changes = [] @@ -55,7 +56,7 @@ test('watches single file changes', async () => { }) test('watches directory creation and processes files', async () => { - const tmpDir = await fs.mkdtemp('/tmp/fswatch-test-') + const tmpDir = await fs.mkdtemp(join(tmpdir(), 'fswatch-test-')) const newDir = join(tmpDir, 'newdir') const changes = [] @@ -78,7 +79,7 @@ test('watches directory creation and processes files', async () => { }) test('ignores files matching patterns', async () => { - const tmpDir = await fs.mkdtemp('/tmp/fswatch-test-') + const tmpDir = await fs.mkdtemp(join(tmpdir(), 'fswatch-test-')) const changes = [] const watcher = fswatch(tmpDir, { ignore: ['*.log', '.hidden*'] }) @@ -100,7 +101,7 @@ test('ignores files matching patterns', async () => { }) test('handles file removal', async () => { - const tmpDir = await fs.mkdtemp('/tmp/fswatch-test-') + const tmpDir = await fs.mkdtemp(join(tmpdir(), 'fswatch-test-')) const testFile = join(tmpDir, 'test.txt') const removed = [] @@ -122,7 +123,7 @@ test('handles file removal', async () => { }) test('ignores files without extensions', async () => { - const tmpDir = await fs.mkdtemp('/tmp/fswatch-test-') + const tmpDir = await fs.mkdtemp(join(tmpdir(), 'fswatch-test-')) const changes = [] const watcher = fswatch(tmpDir)