htmx-router
Advanced tools
Comparing version 0.2.0 to 1.0.0-alpha.0
#!/usr/bin/env node | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const dynamic_1 = require("./dynamic"); | ||
const static_1 = require("./static"); | ||
const isDynamic = process.argv.includes('--dynamic'); | ||
const cwd = process.argv[2] || "./"; | ||
console.log(`Building ${isDynamic ? "dynamic" : "static"} routes`); | ||
if (isDynamic) { | ||
(0, dynamic_1.BuildDynamic)(cwd); | ||
import { writeFile } from "fs/promises"; | ||
import { relative } from "path"; | ||
import { GenerateClient } from "../client/index.js"; | ||
import { ReadConfig } from "../cli/config.js"; | ||
const config = await ReadConfig(); | ||
console.log("Building router"); | ||
const routes = relative(config.router.output, config.router.folder).replaceAll("\\", "/").slice(1); | ||
await writeFile(config.router.output, `/*------------------------------------------ | ||
* Generated by htmx-router * | ||
* Warn: Any changes will be overwritten * | ||
-------------------------------------------*/ | ||
import { GenericContext, RouteTree } from "htmx-router/bin/router"; | ||
import { RegisterDynamic } from "htmx-router/bin/util/dynamic"; | ||
import { GetMountUrl } from 'htmx-router/bin/client/mount'; | ||
import { GetSheetUrl } from 'htmx-router/bin/util/css'; | ||
import { RouteModule } from "htmx-router"; | ||
const modules = import.meta.glob('${routes}/**/*.{ts,tsx}', { eager: true }); | ||
export const tree = new RouteTree(); | ||
for (const path in modules) { | ||
const tail = path.lastIndexOf("."); | ||
const url = path.slice(${routes.length + 1}, tail); | ||
tree.ingest(url, modules[path] as RouteModule<any>); | ||
} | ||
else { | ||
(0, static_1.BuildStatic)(cwd); | ||
export function Dynamic<T extends Record<string, string>>(props: { | ||
params: T, | ||
loader: (params: T, ctx: GenericContext) => Promise<JSX.Element> | ||
children?: JSX.Element | ||
}): JSX.Element { | ||
const path = RegisterDynamic(props.loader); | ||
const query = new URLSearchParams(); | ||
for (const key in props.params) query.set(key, props.params[key]); | ||
const url = path + query.toString(); | ||
return <div | ||
hx-get={url} | ||
hx-trigger="load" | ||
hx-swap="outerHTML transition:true" | ||
style={{ display: "contents" }} | ||
>{props.children ? props.children : ""}</div> | ||
} | ||
export function RouteHeaders() { | ||
return <> | ||
<link href={GetSheetUrl()} rel="stylesheet"></link> | ||
<script src={GetMountUrl()}></script> | ||
</> | ||
}`); | ||
if (config.client) { | ||
console.log("Building client islands"); | ||
await GenerateClient(config.client, true); | ||
} |
@@ -1,2 +0,1 @@ | ||
import type * as CSS from 'csstype'; | ||
export declare function StyleCSS(props: CSS.Properties<string | number>): string; | ||
export declare function CutString(str: string, pivot: string, offset?: number): [string, string]; |
@@ -1,16 +0,27 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.StyleCSS = void 0; | ||
function StyleCSS(props) { | ||
let out = ""; | ||
for (const key in props) { | ||
const value = props[key]; | ||
if (typeof (value) !== "string" && typeof (value) !== "number") | ||
continue; | ||
const safeKey = key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); | ||
const safeVal = value.toString().replace(/"/g, "\\\""); | ||
out += `${safeKey}: ${safeVal};`; | ||
export function CutString(str, pivot, offset = 1) { | ||
if (offset > 0) { | ||
let cursor = 0; | ||
while (offset !== 0) { | ||
const i = str.indexOf(pivot, cursor); | ||
if (i === -1) | ||
return [str, ""]; | ||
cursor = i + 1; | ||
offset--; | ||
} | ||
cursor--; | ||
return [str.slice(0, cursor), str.slice(cursor + pivot.length)]; | ||
} | ||
return out; | ||
if (offset < 0) { | ||
let cursor = str.length; | ||
while (offset !== 0) { | ||
const i = str.lastIndexOf(pivot, cursor); | ||
if (i === -1) | ||
return [str, ""]; | ||
cursor = i - 1; | ||
offset++; | ||
} | ||
cursor++; | ||
return [str.slice(0, cursor), str.slice(cursor + pivot.length)]; | ||
} | ||
return [str, ""]; | ||
} | ||
exports.StyleCSS = StyleCSS; |
@@ -1,6 +0,8 @@ | ||
import { ErrorResponse, Redirect, Outlet, Override } from "./shared"; | ||
import { RouteTree, IsAllowedExt } from "./router"; | ||
import { RenderArgs } from "./render-args"; | ||
import { Link } from "./components"; | ||
import { StyleCSS } from "./helper"; | ||
export { IsAllowedExt, RouteTree, ErrorResponse, Redirect, Override, RenderArgs, Outlet, StyleCSS, Link }; | ||
import { RouteModule, CatchFunction, RenderFunction } from './types.js'; | ||
import { RouteContext, GenericContext } from "./router.js"; | ||
import { createRequestHandler } from './request/index.js'; | ||
import { Cookies, CookieOptions } from "./util/cookies.js"; | ||
import { EventSourceConnection } from "./util/event-source.js"; | ||
import { StyleClass } from './util/css.js'; | ||
import { Endpoint } from './util/endpoint.js'; | ||
export { CatchFunction, CookieOptions, Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RenderFunction, RouteContext, RouteModule, StyleClass, }; |
@@ -1,16 +0,7 @@ | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Link = exports.StyleCSS = exports.RenderArgs = exports.Override = exports.Redirect = exports.ErrorResponse = exports.RouteTree = exports.IsAllowedExt = void 0; | ||
const shared_1 = require("./shared"); | ||
Object.defineProperty(exports, "ErrorResponse", { enumerable: true, get: function () { return shared_1.ErrorResponse; } }); | ||
Object.defineProperty(exports, "Redirect", { enumerable: true, get: function () { return shared_1.Redirect; } }); | ||
Object.defineProperty(exports, "Override", { enumerable: true, get: function () { return shared_1.Override; } }); | ||
const router_1 = require("./router"); | ||
Object.defineProperty(exports, "RouteTree", { enumerable: true, get: function () { return router_1.RouteTree; } }); | ||
Object.defineProperty(exports, "IsAllowedExt", { enumerable: true, get: function () { return router_1.IsAllowedExt; } }); | ||
const render_args_1 = require("./render-args"); | ||
Object.defineProperty(exports, "RenderArgs", { enumerable: true, get: function () { return render_args_1.RenderArgs; } }); | ||
const components_1 = require("./components"); | ||
Object.defineProperty(exports, "Link", { enumerable: true, get: function () { return components_1.Link; } }); | ||
const helper_1 = require("./helper"); | ||
Object.defineProperty(exports, "StyleCSS", { enumerable: true, get: function () { return helper_1.StyleCSS; } }); | ||
import { RouteContext, GenericContext } from "./router.js"; | ||
import { createRequestHandler } from './request/index.js'; | ||
import { Cookies } from "./util/cookies.js"; | ||
import { EventSourceConnection } from "./util/event-source.js"; | ||
import { StyleClass } from './util/css.js'; | ||
import { Endpoint } from './util/endpoint.js'; | ||
export { Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RouteContext, StyleClass, }; |
@@ -1,23 +0,48 @@ | ||
/// <reference types="node" /> | ||
import type http from "node:http"; | ||
import { Override, Redirect, RouteModule } from "./shared"; | ||
import { MaskType, RenderArgs } from "./render-args"; | ||
export declare function IsAllowedExt(ext: string): boolean; | ||
import { Parameterized, ParameterShaper } from './util/parameters.js'; | ||
import { RouteModule } from "./types.js"; | ||
import { Cookies } from './util/cookies.js'; | ||
export declare class GenericContext { | ||
request: Request; | ||
headers: Headers; | ||
cookie: Cookies; | ||
params: { | ||
[key: string]: string; | ||
}; | ||
url: URL; | ||
render: (res: JSX.Element) => Response; | ||
constructor(request: GenericContext["request"], url: GenericContext["url"], renderer: GenericContext["render"]); | ||
shape<T extends ParameterShaper>(shape: T): RouteContext<T>; | ||
} | ||
export declare class RouteContext<T extends ParameterShaper> { | ||
request: Request; | ||
headers: Headers; | ||
cookie: Cookies; | ||
params: Parameterized<T>; | ||
url: URL; | ||
render: (res: JSX.Element) => Response; | ||
constructor(base: GenericContext, shape: T); | ||
} | ||
export declare class RouteLeaf { | ||
module: RouteModule; | ||
mask: boolean[]; | ||
constructor(module: RouteModule, mask: boolean[]); | ||
render(args: RenderArgs, mask: MaskType, routeName: string): Promise<string>; | ||
module: RouteModule<any>; | ||
constructor(module: RouteModule<any>); | ||
resolve(ctx: GenericContext): Promise<Response | null>; | ||
error(ctx: GenericContext, e: unknown): Promise<Response | null>; | ||
private renderWrapper; | ||
} | ||
export declare class RouteTree { | ||
root: boolean; | ||
nested: Map<string, RouteTree>; | ||
index: RouteLeaf | null; | ||
slug: RouteLeaf | null; | ||
wild: RouteTree | null; | ||
wildCard: string; | ||
default: RouteLeaf | null; | ||
route: RouteLeaf | null; | ||
constructor(); | ||
assignRoot(module: RouteModule): void; | ||
ingest(path: string | string[], module: RouteModule, override: boolean[]): void; | ||
calculateDepth(from: string[], to: string[]): number; | ||
render(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<string | Redirect | Override>; | ||
constructor(root?: boolean); | ||
ingest(path: string | string[], module: RouteModule<any>): void; | ||
resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>; | ||
private resolveIndex; | ||
private resolveNext; | ||
private resolveWild; | ||
private resolveSlug; | ||
private resolveNative; | ||
private unwrap; | ||
} |
@@ -1,116 +0,118 @@ | ||
"use strict"; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
import * as endpoint from './util/endpoint.js'; | ||
import * as dynamic from './util/dynamic.js'; | ||
import * as mount from './client/mount.js'; | ||
import * as css from './util/css.js'; | ||
import { Parameterize } from './util/parameters.js'; | ||
import { Cookies } from './util/cookies.js'; | ||
export class GenericContext { | ||
request; | ||
headers; // response headers | ||
cookie; | ||
params; | ||
url; | ||
render; | ||
constructor(request, url, renderer) { | ||
this.cookie = new Cookies(request.headers); | ||
this.headers = new Headers(); | ||
this.request = request; | ||
this.params = {}; | ||
this.url = url; | ||
this.render = renderer; | ||
this.headers.set("x-powered-by", "htmx-router"); | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.RouteTree = exports.RouteLeaf = exports.IsAllowedExt = void 0; | ||
const shared_1 = require("./shared"); | ||
const render_args_1 = require("./render-args"); | ||
const BlankRoute = __importStar(require("./404-route")); | ||
function IsAllowedExt(ext) { | ||
if (ext[0] !== ".") | ||
return false; | ||
// js, jsx, tsx, ts | ||
if (ext[2] !== "s") | ||
return false; | ||
if (ext[1] !== "j" && ext[1] !== "t") | ||
return false; | ||
if (ext.length == 3) | ||
return true; | ||
if (ext.length != 4) | ||
return false; | ||
if (ext[3] !== "x") | ||
return false; | ||
return true; | ||
shape(shape) { | ||
return new RouteContext(this, shape); | ||
} | ||
} | ||
exports.IsAllowedExt = IsAllowedExt; | ||
class RouteLeaf { | ||
constructor(module, mask) { | ||
export class RouteContext { | ||
request; | ||
headers; // response headers | ||
cookie; | ||
params; | ||
url; | ||
render; | ||
constructor(base, shape) { | ||
this.params = Parameterize(base.params, shape); | ||
this.cookie = base.cookie; | ||
this.headers = base.headers; | ||
this.request = base.request; | ||
this.render = base.render; | ||
this.url = base.url; | ||
} | ||
} | ||
export class RouteLeaf { | ||
module; | ||
constructor(module) { | ||
this.module = module; | ||
this.mask = mask; | ||
} | ||
async render(args, mask, routeName) { | ||
async resolve(ctx) { | ||
const res = await this.renderWrapper(ctx); | ||
if (res === null) | ||
return null; | ||
if (res instanceof Response) | ||
return res; | ||
return ctx.render(res); | ||
} | ||
async error(ctx, e) { | ||
if (!this.module.error) | ||
return null; | ||
const res = await this.module.error(ctx, e); | ||
if (res === null) | ||
return null; | ||
if (res instanceof Response) | ||
return res; | ||
return ctx.render(res); | ||
} | ||
async renderWrapper(ctx) { | ||
try { | ||
// Always check auth | ||
// If auth error this function will throw | ||
if (this.module.Auth) | ||
await this.module.Auth(args); | ||
if (mask === render_args_1.MaskType.show) { | ||
if (this.module.Render) | ||
return await this.module.Render(routeName, args); | ||
if (!this.module.loader && !this.module.action) | ||
return null; | ||
const context = ctx.shape(this.module.parameters || {}); | ||
if (ctx.request.method === "HEAD" || ctx.request.method === "GET") { | ||
if (this.module.loader) | ||
return await this.module.loader(context); | ||
else | ||
return null; | ||
} | ||
else { | ||
return await args.Outlet(); | ||
} | ||
if (this.module.action) | ||
return await this.module.action(context); | ||
throw new Response("Method not Allowed", { status: 405, statusText: "Method not Allowed", headers: ctx.headers }); | ||
} | ||
catch (e) { | ||
if (e instanceof shared_1.Redirect || e instanceof shared_1.Override) | ||
if (this.module.error) | ||
return await this.module.error(ctx, e); | ||
else | ||
throw e; | ||
const err = (e instanceof shared_1.ErrorResponse) ? e : | ||
new shared_1.ErrorResponse(500, "Runtime Error", e); | ||
if (this.module.CatchError) | ||
return await this.module.CatchError(routeName, args, err); | ||
throw err; | ||
} | ||
return ""; | ||
return null; | ||
} | ||
} | ||
exports.RouteLeaf = RouteLeaf; | ||
const blankLeaf = new RouteLeaf(BlankRoute, []); | ||
class RouteTree { | ||
constructor() { | ||
export class RouteTree { | ||
root; | ||
nested; | ||
// Leaf nodes | ||
index; // about._index | ||
// Wild card route | ||
slug; // $ | ||
wild; // e.g. $userID | ||
wildCard; | ||
constructor(root = true) { | ||
this.root = root; | ||
this.nested = new Map(); | ||
this.wildCard = ""; | ||
this.slug = null; | ||
this.wild = null; | ||
this.default = null; | ||
this.route = null; | ||
this.index = null; | ||
} | ||
assignRoot(module) { | ||
if (!module.Render) | ||
throw new Error(`Root route is missing Render()`); | ||
if (!module.CatchError) | ||
throw new Error(`Root route is missing CatchError()`); | ||
this.route = new RouteLeaf(module, []); | ||
} | ||
ingest(path, module, override) { | ||
if (!Array.isArray(path)) { | ||
path = path.split(/[\./\\]/g); | ||
} | ||
if (path.length === 0) { | ||
override.push(false); | ||
this.route = new RouteLeaf(module, override); | ||
ingest(path, module) { | ||
if (!Array.isArray(path)) | ||
path = path.split("/"); | ||
if (path.length === 0 || (path.length == 1 && path[0] === "_index")) { | ||
this.index = new RouteLeaf(module); | ||
return; | ||
} | ||
if (path.length === 1 && path[0] === "_index") { | ||
override.push(false); | ||
this.default = new RouteLeaf(module, override); | ||
if (path[0] === "$") { | ||
this.slug = new RouteLeaf(module); | ||
return; | ||
} | ||
if (path[0].endsWith("_")) { | ||
path[0] = path[0].slice(0, -1); | ||
override.push(true); | ||
} | ||
else { | ||
override.push(false); | ||
} | ||
if (path[0][0] === "$") { | ||
@@ -121,3 +123,3 @@ const wildCard = path[0].slice(1); | ||
this.wildCard = wildCard; | ||
this.wild = new RouteTree(); | ||
this.wild = new RouteTree(false); | ||
} | ||
@@ -128,3 +130,3 @@ else if (wildCard !== this.wildCard) { | ||
path.splice(0, 1); | ||
this.wild.ingest(path, module, override); | ||
this.wild.ingest(path, module); | ||
return; | ||
@@ -134,151 +136,89 @@ } | ||
if (!next) { | ||
next = new RouteTree(); | ||
next = new RouteTree(false); | ||
this.nested.set(path[0], next); | ||
} | ||
path.splice(0, 1); | ||
next.ingest(path, module, override); | ||
next.ingest(path, module); | ||
} | ||
calculateDepth(from, to) { | ||
let depth = 0; | ||
if (from.length == 0 || to.length == 0) { | ||
depth = 1; | ||
} | ||
else { | ||
const segmentA = from.splice(0, 1)[0]; | ||
const segmentB = to.splice(0, 1)[0]; | ||
const subRoute = this.nested.get(segmentA); | ||
if (subRoute && segmentA === segmentB) { | ||
depth = subRoute.calculateDepth(from, to); | ||
} | ||
else if (this.wild) { | ||
depth = this.wild.calculateDepth(from, to); | ||
} | ||
else { | ||
return 1; | ||
} | ||
} | ||
depth++; | ||
return depth; | ||
async resolve(fragments, ctx) { | ||
let res = await this.resolveNative(fragments, ctx) | ||
|| await this.resolveIndex(fragments, ctx) | ||
|| await this.resolveNext(fragments, ctx) | ||
|| await this.resolveWild(fragments, ctx) | ||
|| await this.resolveSlug(fragments, ctx); | ||
return this.unwrap(ctx, res); | ||
} | ||
async render(req, res, url) { | ||
var _a; | ||
if (url.pathname.length != 1 && url.pathname.endsWith("/")) { | ||
return new shared_1.Redirect(url.pathname.slice(0, -1) + url.search); | ||
} | ||
const args = new render_args_1.RenderArgs(req, res, url); | ||
res.setHeader('Vary', "hx-current-url"); | ||
const from = req.headers['hx-current-url'] ? | ||
new URL(((_a = req.headers['hx-current-url']) === null || _a === void 0 ? void 0 : _a.toString()) || "/").pathname : | ||
""; | ||
try { | ||
const depth = BuildOutlet(this, args, from); | ||
if (from) { | ||
res.setHeader('HX-Push-Url', req.url || "/"); | ||
if (depth > 0) { | ||
res.setHeader('HX-Retarget', `#hx-route-${depth.toString(16)}`); | ||
} | ||
res.setHeader('HX-Reswap', "outerHTML"); | ||
} | ||
const out = await args.Outlet(); | ||
if (args.title) { | ||
const trigger = res.getHeader('HX-Trigger'); | ||
const entry = `{"setTitle":"${encodeURIComponent(args.title)}"}`; | ||
if (Array.isArray(trigger)) { | ||
res.setHeader('HX-Trigger', [...trigger, entry]); | ||
} | ||
else if (trigger) { | ||
res.setHeader('HX-Trigger', [trigger.toString(), entry]); | ||
} | ||
else { | ||
res.setHeader('HX-Trigger', [entry]); | ||
} | ||
} | ||
return out; | ||
} | ||
catch (e) { | ||
if (e instanceof shared_1.Redirect) | ||
return e; | ||
if (e instanceof shared_1.Override) | ||
return e; | ||
console.error(e); | ||
throw new Error(`Unhandled boil up type ${typeof (e)}: ${e}`); | ||
} | ||
; | ||
async resolveIndex(fragments, ctx) { | ||
if (fragments.length > 0) | ||
return null; | ||
if (!this.index) | ||
return null; | ||
return await this.index.resolve(ctx); | ||
} | ||
} | ||
exports.RouteTree = RouteTree; | ||
function BuildOutlet(start, args, fromPath) { | ||
const frags = args.url.pathname.split('/').slice(1); | ||
if (frags.length === 1 && frags[0] === "") { | ||
frags.splice(0, 1); | ||
async resolveNext(fragments, ctx) { | ||
if (fragments.length < 1) | ||
return null; | ||
const next = this.nested.get(fragments[0]); | ||
if (!next) | ||
return null; | ||
return await next.resolve(fragments.slice(1), ctx); | ||
} | ||
const from = fromPath.split('/').slice(1); | ||
if (from.length === 1 && from[0] === "") { | ||
from.splice(0, 1); | ||
async resolveWild(fragments, ctx) { | ||
if (!this.wild) | ||
return null; | ||
if (fragments.length < 1) | ||
return null; | ||
ctx.params[this.wildCard] = fragments[0]; | ||
return this.wild.resolve(fragments.slice(1), ctx); | ||
} | ||
let matching = fromPath.length > 0; | ||
let depth = -1; | ||
const stack = [start]; | ||
let mask = null; | ||
while (stack.length > 0) { | ||
const cursor = stack.pop(); | ||
if (!mask) { | ||
stack.push(cursor); | ||
if (frags.length === 0) { | ||
if (matching && from.length !== 0) { | ||
depth = args._outletChain.length + stack.length; | ||
matching = false; | ||
} | ||
; | ||
if (cursor.default) { | ||
args._addOutlet(cursor.default); | ||
mask = cursor.default.mask; | ||
} | ||
else { | ||
args._addOutlet(blankLeaf); | ||
mask = []; | ||
} | ||
} | ||
else { | ||
if (matching && from.length === 0) { | ||
depth = args._outletChain.length + stack.length; | ||
matching = false; | ||
} | ||
const segment = frags.splice(0, 1)[0]; | ||
const other = from.splice(0, 1)[0]; | ||
const subRoute = cursor.nested.get(segment); | ||
if (subRoute) { | ||
if (matching && segment !== other) { | ||
depth = args._outletChain.length + stack.length; | ||
matching = false; | ||
} | ||
; | ||
stack.push(subRoute); | ||
} | ||
else if (cursor.wild) { | ||
if (matching && cursor.nested.has(other)) { | ||
depth = args._outletChain.length + stack.length; | ||
matching = false; | ||
} | ||
; | ||
args.params[cursor.wildCard] = segment; | ||
stack.push(cursor.wild); | ||
} | ||
else { | ||
args._addOutlet(blankLeaf); | ||
mask = []; | ||
} | ||
} | ||
} | ||
else { | ||
if (cursor.route) { | ||
args._addOutlet(cursor.route); | ||
} | ||
} | ||
async resolveSlug(fragments, ctx) { | ||
if (!this.slug) | ||
return null; | ||
ctx.params["$"] = fragments.join("/"); | ||
const res = this.slug.resolve | ||
? await this.slug.resolve(ctx) | ||
: null; | ||
return res; | ||
} | ||
if (matching) { | ||
depth = args._outletChain.length - 1; | ||
async resolveNative(fragments, ctx) { | ||
if (!this.root) | ||
return null; | ||
if (fragments.length < 2) | ||
return null; | ||
if (fragments[0] != "_") | ||
return null; | ||
return await ResolveNatively(fragments, ctx); | ||
} | ||
args._applyMask(mask, depth); | ||
return depth; | ||
async unwrap(ctx, res) { | ||
if (!BadResponse(res)) | ||
return res; | ||
if (!this.slug) | ||
return res; | ||
if (res === null) | ||
res = new Response("Not Found", { status: 404, statusText: "Not Found", headers: ctx.headers }); | ||
if (res.headers.has("X-Caught")) | ||
return res; | ||
const caught = await this.slug.error(ctx, res); | ||
if (!caught) | ||
return res; | ||
caught.headers.set("X-Caught", "true"); | ||
return caught; | ||
} | ||
} | ||
function BadResponse(res) { | ||
if (res === null) | ||
return true; | ||
if (res.status < 200) | ||
return true; | ||
if (res.status > 299) | ||
return true; | ||
} | ||
async function ResolveNatively(fragments, ctx) { | ||
switch (fragments[1]) { | ||
case "dynamic": return dynamic._resolve(fragments, ctx); | ||
case "endpoint": return endpoint._resolve(fragments, ctx); | ||
case "mount": return mount._resolve(fragments); | ||
case "style": return css._resolve(fragments); | ||
} | ||
return null; | ||
} |
{ | ||
"name": "htmx-router", | ||
"version": "0.2.0", | ||
"description": "A remix.js style file path router for htmX websites", | ||
"version": "1.0.0-alpha.0", | ||
"description": "A simple SSR framework with dynamic+client islands", | ||
"main": "./bin/index.js", | ||
"type": "module", | ||
"scripts": { | ||
"build": "tsc" | ||
"build": "tsc && tsc-alias" | ||
}, | ||
@@ -22,11 +23,12 @@ "bin": { | ||
"homepage": "https://github.com/AjaniBilby/htmx-router#readme", | ||
"dependencies": { | ||
"vite": "^6.0.1" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.4.5", | ||
"es-module-lexer": "^1.5.4", | ||
"ts-node": "^10.9.1", | ||
"tsc-alias": "^1.8.10", | ||
"typescript": "^5.1.6" | ||
}, | ||
"dependencies": { | ||
"@kitajs/html": "^1.4.2", | ||
"csstype": "^3.1.2" | ||
} | ||
} |
352
readme.md
@@ -1,281 +0,217 @@ | ||
# htmX Router | ||
# htmx Router | ||
> A [remix.js](https://remix.run/docs/en/main/guides/routing) style file path router for htmX websites | ||
A simple file based router with support for dynamic + client islands, route-less endpoints, and built in CSS sheet generation. | ||
This library attempts to be as unopinionated as possible allowing for multiple escape hatches in-case there are certain times you want a different style of behaviour. | ||
- [htmx Router](#htmx-router) | ||
- [Setup](#setup) | ||
- [Routing](#routing) | ||
- [Route Module](#route-module) | ||
- [Nested Route Rendering](#nested-route-rendering) | ||
- [JSX Rendering](#jsx-rendering) | ||
- [Route Contexts](#route-contexts) | ||
- [Params](#params) | ||
- [Cookies](#cookies) | ||
- [Headers](#headers) | ||
- [Request](#request) | ||
- [URL](#url) | ||
- [Style Sheets](#style-sheets) | ||
- [Route-less Endpoint](#route-less-endpoint) | ||
- [Islands](#islands) | ||
- [Dynamic](#dynamic) | ||
- [Client](#client) | ||
This library does not rely on any heavy weight dependencies such as react, instead opting to be built on the lighter weight [kitajs/html](https://kitajs.github.io/html/) library for it's JSX rendering, and using [csstype](https://www.npmjs.com/package/csstype), just as a type interface to improve developer ergonomics. | ||
You can also see an example site running this library [here](https://github.com/AjaniBilby/predictable) with source code as an extra helpful example. Please be mindful the slow loading of this site is actually due to Discord APIs, and the rendering is taking less than 2ms on a raspberry pi on my floor. | ||
## Setup | ||
- [htmX Router](#htmx-router) | ||
- [Routes](#routes) | ||
- [Module Layout](#module-layout) | ||
- [Auth Function](#auth-function) | ||
- [Render Function](#render-function) | ||
- [CatchError Function](#catcherror-function) | ||
- [Router](#router) | ||
- [Types](#types) | ||
- [RenderArgs](#renderargs) | ||
- [Outlet](#outlet) | ||
- [setTitle](#settitle) | ||
- [addMeta](#addmeta) | ||
- [addLinks](#addlinks) | ||
- [renderHeadHTML](#renderheadhtml) | ||
- [shared](#shared) | ||
- [ErrorResponse](#errorresponse) | ||
- [Override](#override) | ||
- [Redirect](#redirect) | ||
- [Components](#components) | ||
- [Link](#link) | ||
- [StyleCSS](#stylecss) | ||
## Routes | ||
There are two requirements for this package behave correctly, you need a `root.jsx`/`.tsx`, in the same folder as a `routes` sub-folder. Plus any given route **must** have use the `route name` provided as the top element's `id` - which will be explained more in later. | ||
URLs are resolved based on the file structure of yur `routes` folder, you can choose to use nested folders or `.`s to have all of your files in a single folder if you choose - all examples will use the `.` layout for simplicity. | ||
If the url path `/user/1234/history` is requested, the router will create an outlet chain to your file tree (*this chain is actually just an array of modules, and the library works based on stack operations to reduce recursion and increase response times*). | ||
Create a `htmx-router.json` in the root of your project defining where you want your router file to be placed, and where your routes are. | ||
You can also define where you want to create your client component bindings, and for what target framework. | ||
```json | ||
{ | ||
"router": { | ||
"folder": "./source/routes", | ||
"output": "./source/router.tsx" | ||
}, | ||
"client": { | ||
"adapter": "react", | ||
"source": "./source/client.tsx" | ||
} | ||
} | ||
``` | ||
root.tsx | ||
routes | ||
├── _index.tsx | ||
├── user.tsx | ||
├── user.static-path.tsx | ||
└── user.$userID.tsx | ||
``` | ||
Given the file tree above, when the root function calls it's [Outlet](#outlet) function, that will trigger `user` to render, and when user calls it's [Outlet](#outlet) function, that will trigger `user.$userID.tsx` to render as this sub route didn't match with any of the static options available, so instead it matched with the wild card route. | ||
Once you have done this, run `npx htmx-router` to generate the artifacts to start development. | ||
We recommand you copy the setup from `examples/react` for your `server.js`, `entry.server.ts`, and `entry.client.ts`. | ||
Since there is no `user.$userID.history.tsx` file, if `user.$userID.tsx` calls [Outlet](#outlet) it will trigger a 404 error, which is actually generated by an internal hidden route placed at the end of an Outlet chain when it is not able to match the rest of a given URL. | ||
Don't forget that in all rendered routes you must include the `<RouterHeader/>` component in your head for hydration and `StyleClass`s to apply affectively. | ||
If we request `/user`, the root route will render, and the `user.tsx` route will render, with `user.tsx`'s [Outlet](#outlet) function actually returning a blank string. If we instead want this request to throw an error we should add a `user._index.tsx` file which on render always throws a `ErrorResponse`. | ||
### Module Layout | ||
## Routing | ||
The router will look for three functions when reading a module, [Render](#render), [CatchError](#catcherror), and [Auth](#auth). Any combination of all or none of these functions are allowed, with the only exception being your `root.tsx` which must have a [Render](#render) and a [CatchError](#catcherror) function. | ||
Routing applies in a depth first order, where it will match in order: | ||
1. The `_index` | ||
2. Static sub-routes | ||
3. Url path-param wildcards | ||
4. Catchall slug | ||
#### Auth Function | ||
```ts | ||
export async function Auth({shared}: RenderArgs) { | ||
if (!shared.auth?.isAdmin) throw new ErrorResponse(401, 'Unauthorised', "Unauthorised Access"); | ||
return; | ||
} | ||
This allows for easy overriding and fallback behaviour. For instance with the routes. | ||
``` | ||
This function is ran on all routes resolved by this file, and it's child routes - no matter if the route itself is masked or not rendered. This function must return nothing, and instead signals failure via throwing an error. With the successful case being nothing was thrown. | ||
#### Render Function | ||
```ts | ||
export async function Render(routeName: string, {}: RenderArgs): Promise<string> | ||
/user/$id.tsx | ||
/user/me.tsx | ||
/user/$.tsx | ||
``` | ||
The render function should follow the above function signature, the routeName string must be consumed if you're rendering a valid output, with the top most HTML element having the id assigned to this value. This helps the router dynamically insert new routes when using the [Link](#link) component. | ||
It will match on the `/user/me` route if applicable, and otherwise will fallback to attempt to match on `/user/$id`, and if the wildcard route fails, it will try the generic slug route `/user/$.tsx`. | ||
> **Note** that a render function may not always run for a given request, and may sometimes be ommited when the router determines the client already has this information in their DOM, and instead only response with the new data and where it should be inserted. | ||
> | ||
> For authorisation checks which must always run for a given URL, please use the [Auth](#auth) function | ||
If a route returns `null` the router will continue the depth first search, allowing for dynamic flow through of the routes. | ||
Optionally this function can also `throw`, in this case the thrown value will boil up until it hits a [CatchError](#catcherror), unless it throws certain types from this library such as `Redirect` or `Override` which will boil all the way to the top without triggering any [CatchError](#catcherror) functions. | ||
### Route Module | ||
This allows a given route to return an arbitrary response without being nested within it's parent routes. | ||
A route module must define a `parameters` export, which defines how the url path params should be parsed when attempting to match the route. | ||
You can use any function which takes a string, and returns something as the parser. You can also simply use JS-Builtin functions for this, and there is a special case with the `Number` function so it will reject on `NaN` values. | ||
```js | ||
export const parameters = { id: Number }; | ||
``` | ||
#### CatchError Function | ||
A route can additionally define a loader, which is called on `GET` and `HEAD` requests | ||
```ts | ||
export async function CatchError(rn: string, {}: RenderArgs, e: ErrorResponse): Promise<string> | ||
export async function loader({}: RouteContext<typeof parameters>); | ||
``` | ||
This function behaves almost identically to [Render](#render) in the way that it's results will be embed within it's parents unless an `Override` is thrown, and it takes a `routeName` which must be used in the root `HTML` element as the id. And it is given [RenderArgs](#renderargs). | ||
However this function **must not** call the [Outlet](#outlet) function within the [RenderArgs](#renderargs), as that will attempt to consume a failed child route as it's result. | ||
## Router | ||
The router itself can be generated via two different ways through the CLI from this library, dynamically or statically. | ||
```bash | ||
npx htmx-router ./source/website --dynamic | ||
``` | ||
When the command is ran it will generate a router based on the directory provided which should contain your root file and routes folder. This command will generate a `router.ts` which we recommend you git ignore from your project. | ||
- **Static**: The static build will read your directories and statically import all of your routes into itself, allowing for easy bundling with tools such as `esbuild` | ||
- **Dynamic**: Will instead generate a file will on startup will read your directory every time, and dynamically import your routes, which will make it unsuitable for use with webpackers, but allows for quick revisions and working well with tools such as `nodemon`. | ||
Once your router is generated you can simply import it and use it like the example below: | ||
With the `action` function being called for all other methods | ||
```ts | ||
const url = new URL(req.url || "/", "http://localhost"); | ||
const out = await Router.render(req, res, url); | ||
if (out instanceof Redirect) { | ||
res.statusCode = 302; | ||
res.setHeader('Location', out.location); | ||
return res.end(); | ||
} else if (out instanceof Override) { | ||
res.end(out.data); | ||
} else { | ||
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); | ||
res.end("<!DOCTYPE html>"+out); | ||
} | ||
export async function action({}: RouteContext<typeof parameters>); | ||
``` | ||
The `Router.render` function may output three possible types [Redirect](#redirect), [Override](#override), or a simple string. These two non-string types are to allow the boil up of overrides within routes, and allows you do handle them specific to your server environment. | ||
## Types | ||
### RenderArgs | ||
This class has been designed to work well with object unpacking for ease of use, typically it's used in a style like this for routes that only need information about the `req` and `res` objects. | ||
If any value is thrown by the parameter parsing, or the render functions (`loader`/`action`) it will boil up, attempting first to call an error function is supplied in the route, and otherwise boiling up to the nearest slug route's `error` function. | ||
```ts | ||
export async function Render(rn: string, {req, res}: RenderArgs) { | ||
return "Hello World"; | ||
} | ||
export async function error(ctx: GenericContext, error: unknown); | ||
``` | ||
However it also includes a bunch of functions to help with higher order features. | ||
### Nested Route Rendering | ||
#### Outlet | ||
The router will not do nested layouts, if that behaviour is required we recommend using the slug-shell pattern. | ||
Where you define a slug route, and export a `shell` function which takes the `JSX` rendered result from the sub route, and renders the upper route around it. | ||
The outlet function will call the child route to render, this function is asynchronous and will always return a string, or else it will throw. If there is nothing left in the outlet chain, it will return an empty string. | ||
This allows flexibility at runtime on how nested route rendering behaves, and can also allow you to ensure you are not reloading data from the db which is already loaded by a sub-route based on how you parse up data through your slug shells. | ||
#### setTitle | ||
We recommend you look at [Predictable Bot](https://github.com/AjaniBilby/predictable) as an example of this pattern performed simply. | ||
This function will update the title that will be generated by the [renderHeadHTML](#renderheadhtml) function, as well as the trigger value for the title updater when using the [Link](#link) component. | ||
You should consider when you call this function in conjunction to your [Outlet](#outlet) function, because if you run setTitle, after the outlet has been called it will override the title set by the child. | ||
### JSX Rendering | ||
```tsx | ||
export async function Render(rn: string, {setTitle, Outlet}: RenderArgs) { | ||
setTitle("Admin Panel"); | ||
htmx-router is jsx templating agnostic for SSR, instead only requiring a definition provided when creating your request handler, allowing you to BYO JSX templating. | ||
```js | ||
// @kitajs/html | ||
app.use('*', createRequestHandler.http({ | ||
build, | ||
viteDevServer, | ||
render: (res) => { | ||
const headers = new Headers(); | ||
headers.set("Content-Type", "text/html; charset=UTF-8"); | ||
return new Response(String(res), { headers }); | ||
} | ||
})); | ||
return <div id={rn}> | ||
<h1><Link to="/admin" style="color: inherit"> | ||
Admin Panel | ||
</Link></h1> | ||
{await Outlet()} | ||
</div>; | ||
} | ||
// React | ||
app.use('*', createRequestHandler.http({ | ||
build, | ||
viteDevServer, | ||
render: (res) => { | ||
const headers = new Headers(); | ||
headers.set("Content-Type", "text/html; charset=UTF-8"); | ||
return new Response(renderToString(res), { headers }); | ||
} | ||
})); | ||
``` | ||
#### addMeta | ||
## Route Contexts | ||
This function allows you to add meta tags which will be rendered by the [renderHeadHTML](#renderheadhtml) function. | ||
```ts | ||
addMeta([ | ||
{ property: "og:title", content: `${guild.name} - Predictions` }, | ||
{ property: "og:image", content: banner } | ||
], true); | ||
``` | ||
There are two kinds of route context, the `RouteContext<T>` which is the resolved route with parameterization, and the `GenericContext` which is used by error functions, and dynamic loads. | ||
If the second argument of this function is set to `true` this function will override any meta tags currently set, replacing them with the inputted tags instead. | ||
### Params | ||
#### addLinks | ||
In the `GenericContext` this will simply be an object with string key value pairs for the parameters, and only the `RouteContext<T>` for `loader` and `action` will have the parameters pre-parsed by your `parameters` definition. | ||
This function behaves identically to [addMeta](#addmeta) but instead designed for link tags. | ||
### Cookies | ||
#### renderHeadHTML | ||
The `RouteContext` and `GenericContext`s both provide a `cookie` object, with the cookie's pre-parsed from the request headers. | ||
It also has a built in `set(name, value, options)` function which will add the appropriate headers to the response for the cookie changes. | ||
This renders out the set meta and link tags for use in the `root.tsx` module, it also includes an embed script for updating the title for dynamic loads from the [Link](#link) component. | ||
### Headers | ||
#### shared | ||
This is a header object useful for adding response headers when you haven't fully finished generating your response yet. | ||
These headers will merge with the response object created by the provided `render` function, with response headers overriding any conflicting `ctx.headers` values. | ||
There is also a blank object attached to all [RenderArgs](#renderargs) for sharing information between routes. | ||
### Request | ||
This can be used for various purposes, but one example is to hold onto decoded cookie values so that each session doesn't need to recompute them if they already have. | ||
This is the original request object, including request headers. | ||
Such an example would look like this | ||
```ts | ||
import type { IncomingMessage } from "node:http"; | ||
import * as cookie from "cookie"; | ||
### URL | ||
export function GetCookies(req: IncomingMessage, shared: any): Record<string, string> { | ||
if (shared.cookie) return shared.cookie; | ||
The parsed `URL` object of the incoming request. | ||
shared.cookies = cookie.parse(req.headers.cookie || ""); | ||
return shared.cookies; | ||
} | ||
``` | ||
## Style Sheets | ||
```ts | ||
import type { GetCookies } from "../shared/cookie.ts"; | ||
htmx-router includes a `StyleClass` object, which can be used to define CSS classes without needing a unique name. | ||
StyleClasses should only be defined at the top level of a file, and not created within a function, or dynamically during runtime. | ||
export function GetCookies(rn: string, {shared}: RenderArgs) { | ||
const cookies = GetCookies(shared); | ||
// do stuff.... | ||
} | ||
``` | ||
### ErrorResponse | ||
This class is a way of HTTP-ifying error objects, and other error states, if an error is thrown by a [Render](#render) or an [Auth](#auth) function that isn't already wrapped by this type, the error will then become wrapped by this type. | ||
```ts | ||
export class ErrorResponse { | ||
code : number; | ||
status : string; | ||
data : any; | ||
const myClass = new StyleClass(`myClass`, ` | ||
.this:hover { | ||
background-color: red; | ||
} | ||
`).name; | ||
``` | ||
### Override | ||
## Route-less Endpoint | ||
If a render function throws a value of this type, it will boil all the way up to the original render call allowing it to bypass any parent manipulation. | ||
This should be defined at the top level of your file, these endpoints can optionally be given an name which will help for debugging network requests, but they do not need to be unique. | ||
```ts | ||
export class Override { | ||
data : string | Buffer | Uint8Array; | ||
} | ||
const endpoint_url = new Endpoint((ctx: GenericContext) => { | ||
return new Response("Hello World"); | ||
}, "hello-world").url; | ||
``` | ||
### Redirect | ||
## Islands | ||
This type behaves identically to override by is intended for boiling up specifically http redirect responses. | ||
> Tip: Don't forget to wrap your islands in a hx-preserve to prevent losing state. And use `display: contents;` to make that wrapping div transparent for grid and other layout features. | ||
```ts | ||
export class Redirect { | ||
location: string; | ||
} | ||
``` | ||
### Dynamic | ||
## Components | ||
A dynamic component takes params which will be converted into the props of the loader function, these props may only be string key string value pairs as they are encoded the the query string to allow for browser side caching. | ||
### Link | ||
The body of a dynamic component is the pre-rendered infill that will display while the client is loading the dynamic content. | ||
```ts | ||
export function Link(props: { | ||
to: string, | ||
target?: string, | ||
style?: string | ||
}, contents: string[]) | ||
``` | ||
```tsx | ||
<Link to={`/user/${id}`}>View Profile</Link> | ||
``` | ||
async function MyProfile(params: {}, ctx: GenericContext): Promise<JSX.Element> { | ||
ctx.headers.set('Cache-Control', "private, max-age=120"); | ||
const userID = ctx.cookie.get('userID'); | ||
if (!userID) return <></>; | ||
This component overrides a normal `<a>`, adding extra headers telling the server route it is coming from, based on this information the server can determine the minimal route that needs to be rendered to send back to the client and calculates just that sub route. | ||
const user = await GetUser(userID); | ||
if (!user) return <></>; | ||
Sending it back to the client telling htmX where to insert the new content. | ||
return <a href={`/user/${userID}`}> | ||
<div safe>{user.name}</div> | ||
</a> | ||
} | ||
This element will encode as a standard `<a>` with some extra html attributes, meaning it won't affect SEO and bots attempting to scrape your website. | ||
export async function loader({ params }: RouteContext<typeof parameters>) { | ||
return <Dynamic params={{}} loader={MyProfile}> | ||
put your ssr pre-rendered skeleton here | ||
</Dynamic> | ||
} | ||
``` | ||
### Client | ||
## StyleCSS | ||
Import all of the components you want to be able to use on the client side into your `client.tsx`, if you are running a dev server this file will automatically generate the clientized version, otherwise use the `npx htmx-router` command to regenerate these artifacts. | ||
> **DEPRECATED**: If you utilize `@kitajs/html` instead of `typed-html` this function is no longer needed | ||
Once a component has been clientized you can import it as use it like normal, however the body is now overwritten to it will render immediately on the server, and then all props will parsed to the client for it to be rendered properly in the browser. | ||
This is a helper function allowing you to give it a [CSS.Properties](https://www.npmjs.com/package/csstype) type, and render it into a string for [kitajs/html](https://kitajs.github.io/html/) to use. | ||
```tsx | ||
<div style={StyleCSS({ | ||
height: "100%", | ||
width: "100%", | ||
fontWeight: "bold", | ||
fontSize: "5em", | ||
})}> | ||
I AM BIG | ||
</div> | ||
``` | ||
<Client.Counter> | ||
<button>No yet hydrated...</button> {/* this will be overwritten in the browser once hydrated */} | ||
</Client.Counter> | ||
``` | ||
It is very important that you ensure your `Client` component has a single child element, if there are multiple child components the browser will only mount to the last child causing artifacting. |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
46805
1
44
1108
Yes
5
217
5
1
+ Addedvite@^6.0.1
+ Added@esbuild/aix-ppc64@0.24.2(transitive)
+ Added@esbuild/android-arm@0.24.2(transitive)
+ Added@esbuild/android-arm64@0.24.2(transitive)
+ Added@esbuild/android-x64@0.24.2(transitive)
+ Added@esbuild/darwin-arm64@0.24.2(transitive)
+ Added@esbuild/darwin-x64@0.24.2(transitive)
+ Added@esbuild/freebsd-arm64@0.24.2(transitive)
+ Added@esbuild/freebsd-x64@0.24.2(transitive)
+ Added@esbuild/linux-arm@0.24.2(transitive)
+ Added@esbuild/linux-arm64@0.24.2(transitive)
+ Added@esbuild/linux-ia32@0.24.2(transitive)
+ Added@esbuild/linux-loong64@0.24.2(transitive)
+ Added@esbuild/linux-mips64el@0.24.2(transitive)
+ Added@esbuild/linux-ppc64@0.24.2(transitive)
+ Added@esbuild/linux-riscv64@0.24.2(transitive)
+ Added@esbuild/linux-s390x@0.24.2(transitive)
+ Added@esbuild/linux-x64@0.24.2(transitive)
+ Added@esbuild/netbsd-arm64@0.24.2(transitive)
+ Added@esbuild/netbsd-x64@0.24.2(transitive)
+ Added@esbuild/openbsd-arm64@0.24.2(transitive)
+ Added@esbuild/openbsd-x64@0.24.2(transitive)
+ Added@esbuild/sunos-x64@0.24.2(transitive)
+ Added@esbuild/win32-arm64@0.24.2(transitive)
+ Added@esbuild/win32-ia32@0.24.2(transitive)
+ Added@esbuild/win32-x64@0.24.2(transitive)
+ Added@rollup/rollup-android-arm-eabi@4.34.6(transitive)
+ Added@rollup/rollup-android-arm64@4.34.6(transitive)
+ Added@rollup/rollup-darwin-arm64@4.34.6(transitive)
+ Added@rollup/rollup-darwin-x64@4.34.6(transitive)
+ Added@rollup/rollup-freebsd-arm64@4.34.6(transitive)
+ Added@rollup/rollup-freebsd-x64@4.34.6(transitive)
+ Added@rollup/rollup-linux-arm-gnueabihf@4.34.6(transitive)
+ Added@rollup/rollup-linux-arm-musleabihf@4.34.6(transitive)
+ Added@rollup/rollup-linux-arm64-gnu@4.34.6(transitive)
+ Added@rollup/rollup-linux-arm64-musl@4.34.6(transitive)
+ Added@rollup/rollup-linux-loongarch64-gnu@4.34.6(transitive)
+ Added@rollup/rollup-linux-powerpc64le-gnu@4.34.6(transitive)
+ Added@rollup/rollup-linux-riscv64-gnu@4.34.6(transitive)
+ Added@rollup/rollup-linux-s390x-gnu@4.34.6(transitive)
+ Added@rollup/rollup-linux-x64-gnu@4.34.6(transitive)
+ Added@rollup/rollup-linux-x64-musl@4.34.6(transitive)
+ Added@rollup/rollup-win32-arm64-msvc@4.34.6(transitive)
+ Added@rollup/rollup-win32-ia32-msvc@4.34.6(transitive)
+ Added@rollup/rollup-win32-x64-msvc@4.34.6(transitive)
+ Added@types/estree@1.0.6(transitive)
+ Addedesbuild@0.24.2(transitive)
+ Addedfsevents@2.3.3(transitive)
+ Addednanoid@3.3.8(transitive)
+ Addedpicocolors@1.1.1(transitive)
+ Addedpostcss@8.5.1(transitive)
+ Addedrollup@4.34.6(transitive)
+ Addedsource-map-js@1.2.1(transitive)
+ Addedvite@6.1.0(transitive)
- Removed@kitajs/html@^1.4.2
- Removedcsstype@^3.1.2
- Removed@kitajs/html@1.4.7(transitive)
- Removedcsstype@3.1.3(transitive)