@nextwebwg/htmlkit 1.0.0-alpha.52 → 1.0.0-alpha.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/dist/application.d.ts +15 -1
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +152 -311
- package/dist/application.js.map +1 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +116 -18
- package/dist/build.js.map +1 -1
- package/dist/bundle-worker.js +17 -7
- package/dist/bundle-worker.js.map +1 -1
- package/dist/bundle.d.ts +7 -1
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +6 -4
- package/dist/bundle.js.map +1 -1
- package/dist/cli.js +38 -10
- package/dist/cli.js.map +1 -1
- package/dist/client.js +11 -11
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -3
- package/dist/config.js.map +1 -1
- package/dist/document.d.ts +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js.map +1 -1
- package/dist/head.d.ts +19 -2
- package/dist/head.d.ts.map +1 -1
- package/dist/head.js +26 -21
- package/dist/head.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +8 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +90 -0
- package/dist/links.js.map +1 -0
- package/dist/resource.d.ts +2 -0
- package/dist/resource.d.ts.map +1 -1
- package/dist/resource.js +8 -3
- package/dist/resource.js.map +1 -1
- package/dist/routes.d.ts +13 -5
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +92 -47
- package/dist/routes.js.map +1 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +26 -78
- package/dist/serve.js.map +1 -1
- package/dist/server.d.ts +162 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +599 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +92 -35
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# HTMLKit
|
|
2
2
|
|
|
3
3
|
HTMLKit (`@nextwebwg/htmlkit`) builds applications with HTML Next components. It provides routes,
|
|
4
|
-
layouts, server loaders, browser delivery, and development tools.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
layouts, server loaders, endpoints, middleware, browser delivery, and development tools. A build
|
|
5
|
+
prerenders pages for any static host and writes a request server for pages that render on each
|
|
6
|
+
request, which `htmlkit start` runs on Node and Deno and Bun run through its `fetch`.
|
|
7
|
+
[The server-rendering design](./docs/server-rendering.md) records how requests are answered.
|
|
7
8
|
|
|
8
9
|
Use Node `>=22.22.2 <23 || >=24.15 <25`. Install `@nextwebwg/htmlkit` and add its commands to your
|
|
9
10
|
`package.json` scripts:
|
|
@@ -13,15 +14,17 @@ Use Node `>=22.22.2 <23 || >=24.15 <25`. Install `@nextwebwg/htmlkit` and add it
|
|
|
13
14
|
"scripts": {
|
|
14
15
|
"dev": "htmlkit dev",
|
|
15
16
|
"build": "htmlkit build",
|
|
16
|
-
"preview": "htmlkit preview"
|
|
17
|
+
"preview": "htmlkit preview",
|
|
18
|
+
"start": "htmlkit start"
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
Then run `pnpm dev`, `pnpm build`, or `pnpm
|
|
23
|
+
Then run `pnpm dev`, `pnpm build`, `pnpm preview`, or `pnpm start` (`npm run dev` and the others work too).
|
|
22
24
|
|
|
23
25
|
**Guide:** [nextwebwg.org/htmlkit](https://nextwebwg.org/htmlkit/) covers pages and routing,
|
|
24
|
-
loading data, navigation, moving between pages, and
|
|
26
|
+
loading data, endpoints and middleware, navigation, moving between pages, deployment, and
|
|
27
|
+
configuration. Its source is
|
|
25
28
|
[`docs/guide`](./docs/guide/) in this package.
|
|
26
29
|
[The client-navigation design](./docs/client-navigation.md) records how pages swap in place.
|
|
27
30
|
|
package/dist/application.d.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
+
import { type ComponentDefinition } from "@nextwebwg/html-next";
|
|
1
2
|
import { type ViteDevServer } from "vite";
|
|
2
3
|
import { type PageAssets } from "./document.js";
|
|
3
|
-
import
|
|
4
|
+
import { type RenderLayer } from "./server.js";
|
|
5
|
+
import type { Application, ApplicationOptions, ApplicationRoute, BrowserDefinition, RenderedHead, RenderedPage } from "./types.js";
|
|
4
6
|
export declare function headScripts(options: ApplicationOptions, root: string): Promise<readonly string[]>;
|
|
5
7
|
export declare function notFoundPage(scripts: readonly string[]): {
|
|
6
8
|
head: RenderedHead;
|
|
7
9
|
body: string;
|
|
8
10
|
html: string;
|
|
9
11
|
};
|
|
12
|
+
/** A route's components, ready to render: its layers, every component they use, and their browser delivery. */
|
|
13
|
+
export interface RouteGraph {
|
|
14
|
+
readonly layers: readonly RenderLayer[];
|
|
15
|
+
readonly definitions: readonly ComponentDefinition[];
|
|
16
|
+
readonly components: readonly BrowserDefinition[];
|
|
17
|
+
}
|
|
18
|
+
/** The application, plus what a build reads from it: each route's component graph and the concrete page URLs. */
|
|
19
|
+
export declare function createApplicationInternals(input?: ApplicationOptions, moduleServer?: ViteDevServer, assets?: (page: RenderedPage) => PageAssets): Promise<{
|
|
20
|
+
application: Application;
|
|
21
|
+
graph: (route: ApplicationRoute) => Promise<RouteGraph>;
|
|
22
|
+
catalog: () => Promise<ReadonlyMap<string, ApplicationRoute>>;
|
|
23
|
+
}>;
|
|
10
24
|
/** assets supplies a serving adapter's browser modules and stylesheets for fetch(). */
|
|
11
25
|
export declare function createApplication(input?: ApplicationOptions, moduleServer?: ViteDevServer, assets?: (page: RenderedPage) => PageAssets): Promise<Application>;
|
|
12
26
|
//# sourceMappingURL=application.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../src/application.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../src/application.ts"],"names":[],"mappings":"AAKA,OAAO,EAA0D,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAExH,OAAO,EAA0C,KAAK,aAAa,EAAE,MAAM,MAAM,CAAC;AAGlF,OAAO,EAA8B,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAG5E,OAAO,EAAiH,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC9J,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,iBAAiB,EAA8C,YAAY,EAAE,YAAY,EAC9H,MAAM,YAAY,CAAC;AAoB/C,wBAAsB,WAAW,CAAC,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAYvG;AAGD,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE;;;;EAItD;AAED,+GAA+G;AAC/G,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACrD,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACnD;AAED,iHAAiH;AACjH,wBAAsB,0BAA0B,CAAC,KAAK,GAAE,kBAAuB,EAAE,YAAY,CAAC,EAAE,aAAa,EAC3G,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,UAAU;;mBAyEf,gBAAgB,KAAG,OAAO,CAAC,UAAU,CAAC;;GAiJnE;AAED,uFAAuF;AACvF,wBAAsB,iBAAiB,CAAC,KAAK,GAAE,kBAAuB,EAAE,YAAY,CAAC,EAAE,aAAa,EAClG,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAEnE"}
|
package/dist/application.js
CHANGED
|
@@ -2,47 +2,14 @@ import { dirname, join } from "node:path";
|
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { compileComponentStylesForBuild, parseComponentResource
|
|
5
|
+
import { compileComponentStylesForBuild, parseComponentResource } from "@nextwebwg/html-next";
|
|
6
6
|
import { loadNodeComponents } from "@nextwebwg/html-next/node-loader";
|
|
7
|
-
import { renderComponents } from "@nextwebwg/html-next/server";
|
|
8
7
|
import { createServer, isRunnableDevEnvironment } from "vite";
|
|
9
8
|
import { configure, HtmlKitError, rootAlias, withPlugins } from "./config.js";
|
|
10
|
-
import { documentHTML,
|
|
11
|
-
import { breadth, discover, matchRoute, parameter,
|
|
9
|
+
import { documentHTML, redirectPage } from "./document.js";
|
|
10
|
+
import { breadth, discover, discoverServerFiles, matchRoute, parameter, redirectTable, validSegment } from "./routes.js";
|
|
12
11
|
import { applicationResource, pageDefinition } from "./resource.js";
|
|
13
|
-
import {
|
|
14
|
-
/** What a loader's redirect() throws, so render answers with the redirect instead of the page. */
|
|
15
|
-
class Redirect {
|
|
16
|
-
location;
|
|
17
|
-
status;
|
|
18
|
-
constructor(location, status) {
|
|
19
|
-
this.location = location;
|
|
20
|
-
this.status = status;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/** A loader's props as invocation attributes, in their HTML form, defaults included. */
|
|
24
|
-
function invocationAttributes(definition, result) {
|
|
25
|
-
const props = result.props ?? {};
|
|
26
|
-
for (const name of Object.keys(props)) {
|
|
27
|
-
if (!Object.hasOwn(definition.contract.props, name))
|
|
28
|
-
throw new HtmlKitError(`Undeclared prop ${name}.`, definition.source.file);
|
|
29
|
-
}
|
|
30
|
-
const attributes = {};
|
|
31
|
-
for (const [name, contract] of Object.entries(definition.contract.props)) {
|
|
32
|
-
serializePropTarget(contract, props[name], name, definition.contract.tag);
|
|
33
|
-
// The contract target describes lowering onto native DOM. Invocation attributes carry
|
|
34
|
-
// the public prop name; writing the target name here silently loses remapped props.
|
|
35
|
-
const provided = props[name] === undefined ? contract.default : props[name];
|
|
36
|
-
const value = provided == null ? null : serializeTypedValue(provided, contract.type);
|
|
37
|
-
if (value !== null)
|
|
38
|
-
attributes[name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()] = value;
|
|
39
|
-
}
|
|
40
|
-
return attributes;
|
|
41
|
-
}
|
|
42
|
-
/** The browser builds the same invocation from a page payload (client.ts). */
|
|
43
|
-
function invocation(tag, attributes, id, child, nested) {
|
|
44
|
-
return `<${tag} id="${id}"${nested ? ' slot="page"' : ""}${Object.entries(attributes).map(([name, value]) => ` ${name}="${escapeHTML(value)}"`).join("")}>${child}</${tag}>`;
|
|
45
|
-
}
|
|
12
|
+
import { createNavigation, failurePage, pageResponse, renderRoute, respond, serverFailure } from "./server.js";
|
|
46
13
|
// Built-in components need no link: a resource that uses one gets it added after its content, so
|
|
47
14
|
// diagnostics keep their line numbers. The import map below resolves it from this package.
|
|
48
15
|
const builtins = [["hk-nav", "@nextwebwg/htmlkit/components/nav.html"], ["hk-breadcrumbs", "@nextwebwg/htmlkit/components/breadcrumbs.html"],
|
|
@@ -79,12 +46,15 @@ export function notFoundPage(scripts) {
|
|
|
79
46
|
const body = "<main><h1>Page not found</h1></main>";
|
|
80
47
|
return { head, body, html: documentHTML(body, head) };
|
|
81
48
|
}
|
|
82
|
-
/**
|
|
83
|
-
export async function
|
|
49
|
+
/** The application, plus what a build reads from it: each route's component graph and the concrete page URLs. */
|
|
50
|
+
export async function createApplicationInternals(input = {}, moduleServer, assets) {
|
|
84
51
|
const options = await withPlugins(input);
|
|
85
52
|
const config = configure(options);
|
|
86
53
|
const redirects = redirectTable(config.base, options.redirects);
|
|
87
|
-
const { routes, notFound: notFoundRoute, sources, files,
|
|
54
|
+
const { routes, notFound: notFoundRoute, error: errorRoute, sources, files, watched } = await discover(config.root, { ...options, base: config.base });
|
|
55
|
+
// Alias pages name their own URL canonical, absolute when the site sets its origin.
|
|
56
|
+
const canonicalOrigin = options.origin === undefined ? "" : config.origin;
|
|
57
|
+
const { endpoints, middleware } = await discoverServerFiles(config.root);
|
|
88
58
|
// A dynamic page's entries are checked the same way when they are listed.
|
|
89
59
|
const shadowed = routes.find(route => route.params.length === 0 && redirects.has(config.base + route.pattern.slice(1)));
|
|
90
60
|
if (shadowed !== undefined)
|
|
@@ -112,8 +82,10 @@ export async function createApplication(input = {}, moduleServer, assets) {
|
|
|
112
82
|
const paths = new Map();
|
|
113
83
|
for (const route of routes) {
|
|
114
84
|
const loaded = await module(route);
|
|
85
|
+
// A server-rendered dynamic page renders whatever its URL names, so it needs no entries().
|
|
115
86
|
if (route.params.length > 0 && typeof loaded.entries !== "function") {
|
|
116
|
-
|
|
87
|
+
if (route.render !== "server")
|
|
88
|
+
unenumerated.push(route);
|
|
117
89
|
continue;
|
|
118
90
|
}
|
|
119
91
|
const entries = loaded.entries === undefined ? [{}] : await loaded.entries();
|
|
@@ -152,304 +124,173 @@ export async function createApplication(input = {}, moduleServer, assets) {
|
|
|
152
124
|
}
|
|
153
125
|
return paths;
|
|
154
126
|
})();
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const aRank = aParts[i].rank;
|
|
192
|
-
const bRank = bParts[i].rank;
|
|
193
|
-
if (aRank !== bRank) {
|
|
194
|
-
if (aRank === null)
|
|
195
|
-
return 1;
|
|
196
|
-
if (bRank === null)
|
|
197
|
-
return -1;
|
|
198
|
-
const difference = BigInt(aRank) - BigInt(bRank);
|
|
199
|
-
if (difference !== 0n)
|
|
200
|
-
return difference < 0n ? -1 : 1;
|
|
127
|
+
const navigation = createNavigation(config.base, routes, concreteRoutes);
|
|
128
|
+
// Vite owns module caching and dependency invalidation. Per-render results never enter
|
|
129
|
+
// a shared cache; HTML Next owns the DOM worker and declarative component semantics.
|
|
130
|
+
const graph = async (route) => {
|
|
131
|
+
const layers = [...route.layouts, route];
|
|
132
|
+
const packages = new Map();
|
|
133
|
+
const resources = new Map();
|
|
134
|
+
const layerURLs = new Set(layers.map(layer => pathToFileURL(layer.component).href));
|
|
135
|
+
const resolveCSS = server.config.createResolver({ extensions: [".css"], mainFields: ["style"], conditions: ["style", "development|production"], preferRelative: true, tryIndex: false });
|
|
136
|
+
const loaded = await loadNodeComponents(layers.map(layer => pathToFileURL(layer.component).href), {
|
|
137
|
+
readStylesheet: async (url) => ({ url, source: await readFile(fileURLToPath(url), "utf8") }),
|
|
138
|
+
resolveStylesheet: async (specifier, parentURL) => {
|
|
139
|
+
const resolved = await resolveCSS(specifier, fileURLToPath(parentURL));
|
|
140
|
+
return resolved === undefined ? undefined
|
|
141
|
+
: pathToFileURL(resolved.replace(/[?#].*$/, "")).href + (/[?#].*$/.exec(resolved)?.[0] ?? "");
|
|
142
|
+
},
|
|
143
|
+
stylesheetAssetURL: url => url.startsWith("file:") ? `/@fs/${fileURLToPath(url)}${new URL(url).search}${new URL(url).hash}` : url,
|
|
144
|
+
// The graph resolver is synchronous. Prepare its bare imports while asynchronously
|
|
145
|
+
// reading each carrier, using Vite's ESM resolution from the consuming application.
|
|
146
|
+
importMap: map,
|
|
147
|
+
readComponent: async (url) => {
|
|
148
|
+
let source = sources.get(fileURLToPath(url)) ?? await readFile(fileURLToPath(url), "utf8");
|
|
149
|
+
for (const [tag, href] of builtins)
|
|
150
|
+
if (new RegExp(`<${tag}[\\s/>]`).test(source))
|
|
151
|
+
source += `\n<link rel="component" href="${href}">`;
|
|
152
|
+
const parsed = layerURLs.has(url) ? applicationResource(source, url) : parseComponentResource(source, url);
|
|
153
|
+
if ("configuration" in parsed)
|
|
154
|
+
resources.set(url, parsed);
|
|
155
|
+
const imports = [...parsed.dependencies, ...parsed.definitions.flatMap(definition => definition.controller === undefined ? [] : [definition.controller])];
|
|
156
|
+
for (const specifier of imports) {
|
|
157
|
+
if (/^(?:[A-Za-z][A-Za-z\d+.-]*:|\/|\.\.?\/)/.test(specifier) || Object.keys(map.imports).some(key => specifier.startsWith(key)))
|
|
158
|
+
continue;
|
|
159
|
+
const resolved = await server.environments.client.pluginContainer.resolveId(specifier, fileURLToPath(url));
|
|
160
|
+
if (resolved === null || resolved.external)
|
|
161
|
+
throw new HtmlKitError(`Cannot resolve component import ${specifier}.`, url);
|
|
162
|
+
packages.set(`${url}\0${specifier}`, packageResource(resolved.id));
|
|
201
163
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
164
|
+
return { url, source };
|
|
165
|
+
},
|
|
166
|
+
resolvePackage: (specifier, parentURL) => {
|
|
167
|
+
const resolved = packages.get(`${parentURL}\0${specifier}`);
|
|
168
|
+
if (resolved === undefined)
|
|
169
|
+
throw new HtmlKitError(`Cannot resolve component import ${specifier}.`, parentURL);
|
|
170
|
+
return resolved;
|
|
171
|
+
},
|
|
207
172
|
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const start = from.split("/").filter(Boolean).length;
|
|
219
|
-
const trail = [];
|
|
220
|
-
for (let i = start; i <= parts.length; i++) {
|
|
221
|
-
const href = config.base + parts.slice(0, i).map(part => part + "/").join("");
|
|
222
|
-
// The page being rendered exists even when no entries() lists it, as for a dynamic page at request time.
|
|
223
|
-
const route = paths.get(href) ?? (i === parts.length ? matchRoute(routes, href, config.base)?.route : undefined);
|
|
224
|
-
if (route !== undefined && route.canonical === undefined)
|
|
225
|
-
trail.push(item(href, route, i - start, current));
|
|
173
|
+
const definitions = layers.map(layer => {
|
|
174
|
+
if (layer === route)
|
|
175
|
+
return pageDefinition(resources.get(pathToFileURL(layer.component).href), layer.component);
|
|
176
|
+
const roots = loaded.roots.filter(id => loaded.nodes.get(id).url === pathToFileURL(layer.component).href);
|
|
177
|
+
if (roots.length !== 1)
|
|
178
|
+
throw new HtmlKitError("A page or layout must declare exactly one root component.", layer.component);
|
|
179
|
+
return loaded.nodes.get(roots[0]).definition;
|
|
180
|
+
});
|
|
181
|
+
if (definitions.at(-1).contract.tag !== route.pageName) {
|
|
182
|
+
throw new HtmlKitError("Page component name changed; rediscover routes before rendering.", route.component);
|
|
226
183
|
}
|
|
227
|
-
|
|
184
|
+
for (const definition of definitions.slice(0, -1)) {
|
|
185
|
+
if (!definition.slots?.some(slot => slot.name === "page"))
|
|
186
|
+
throw new HtmlKitError('Layout requires a slot named "page".', definition.source.file);
|
|
187
|
+
}
|
|
188
|
+
const components = [...loaded.nodes.values()].map(node => ({ definition: node.definition,
|
|
189
|
+
styles: compileComponentStylesForBuild(node.definition.css, node.definition),
|
|
190
|
+
...(node.controller === undefined ? {} : { controller: fileURLToPath(node.controller.url) }) }));
|
|
191
|
+
return { components, definitions: components.map(component => component.definition), layers: layers.map((layer, i) => ({ definition: definitions[i],
|
|
192
|
+
metadata: resources.get(pathToFileURL(layer.component).href).components.get(definitions[i].contract.tag), module: () => module(layer), origin: origin(layer) })) };
|
|
228
193
|
};
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
nodes.set(key, node);
|
|
249
|
-
siblings.push(node);
|
|
250
|
-
}
|
|
251
|
-
siblings = node.children;
|
|
252
|
-
}
|
|
253
|
-
if (node === undefined) {
|
|
254
|
-
node = { label: item.label, order: null, children: [] };
|
|
255
|
-
top.unshift(node);
|
|
256
|
-
}
|
|
257
|
-
node.item = item;
|
|
258
|
-
node.route = route;
|
|
194
|
+
const builtinNotFound = (pathname) => ({ status: 404, pathname, ...notFoundPage(scripts), css: "", components: [], layers: [] });
|
|
195
|
+
const redirected = (pathname, location, status, headers) => ({ status, pathname, location, ...redirectPage(location), css: "", components: [], layers: [], ...(headers === undefined ? {} : { headers }) });
|
|
196
|
+
const builtinFailure = (pathname) => ({ pathname, ...failurePage(serverFailure, scripts), css: "", components: [], layers: [] });
|
|
197
|
+
// A matched route; without one, the not-found page, or for a failure the error page, through its layouts.
|
|
198
|
+
// request makes it a request-time render.
|
|
199
|
+
const renderPage = async (matched, url, request, signal, failure) => {
|
|
200
|
+
const fallback = failure === undefined ? notFoundRoute : errorRoute;
|
|
201
|
+
if (matched === undefined && fallback === undefined)
|
|
202
|
+
return failure === undefined ? builtinNotFound(url.pathname) : builtinFailure(url.pathname);
|
|
203
|
+
const { route, params } = matched ?? { route: fallback, params: {} };
|
|
204
|
+
const { layers, definitions, components } = await graph(route);
|
|
205
|
+
const rendered = await renderRoute({ base: config.base, canonicalOrigin, route, params, url, ...(request === undefined ? {} : { request }), signal, layers,
|
|
206
|
+
definitions, scripts, navigation, ...(failure === undefined ? {} : { failure }) });
|
|
207
|
+
if (rendered.kind === "redirect")
|
|
208
|
+
return redirected(url.pathname, rendered.location, rendered.status, rendered.headers);
|
|
209
|
+
if (rendered.kind === "notFound") {
|
|
210
|
+
// The not-found and error pages' own notFound() falls back to the built-in page.
|
|
211
|
+
const page = matched === undefined ? builtinNotFound(url.pathname) : await renderPage(undefined, url, undefined, signal);
|
|
212
|
+
return { ...page, headers: rendered.headers };
|
|
259
213
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const { item, route } = draft;
|
|
263
|
-
// A folder's index page labels the folder by its hk:label; then a plugin's folder() label does, then the page's <title>.
|
|
264
|
-
const label = item === undefined ? draft.label : children.length > 0 && route.label === undefined ? draft.folder ?? route.title ?? draft.label : item.label;
|
|
265
|
-
const current = item?.current ?? "false";
|
|
266
|
-
return Object.freeze({ label, ...(item === undefined ? {} : { href: item.href, pageName: item.pageName }), current,
|
|
267
|
-
open: current === "page" || children.some(child => child.open), depth, order: draft.order, children: Object.freeze(children) });
|
|
268
|
-
};
|
|
269
|
-
return Object.freeze(top.map(draft => finish(draft, 0)));
|
|
214
|
+
return { status: failure?.status ?? (matched === undefined ? 404 : rendered.status), pathname: url.pathname, body: rendered.body, css: rendered.css, head: rendered.head,
|
|
215
|
+
html: documentHTML(rendered.body, rendered.head), components, layers: rendered.layers, headers: rendered.headers };
|
|
270
216
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
217
|
+
/**
|
|
218
|
+
* A failure answers with app/error.html and a safe message; the error itself goes to the console. The
|
|
219
|
+
* development server, which supplies the module server, shows the error instead.
|
|
220
|
+
*/
|
|
221
|
+
const failed = async (error, request) => {
|
|
222
|
+
const report = (error) => { if (!ownsServer || request.signal.aborted)
|
|
223
|
+
throw error; console.error(error); };
|
|
224
|
+
report(error);
|
|
225
|
+
const { pathname } = new URL(request.url);
|
|
226
|
+
// A failed page has no payload, so the browser loads its document, the error page.
|
|
227
|
+
if (pathname.startsWith(config.base + "_htmlkit/pages/"))
|
|
228
|
+
return new Response(null, { status: 500 });
|
|
229
|
+
// The error page shares the site's layouts, so a failing layout fails it too; the built-in page answers then.
|
|
230
|
+
const page = pathname.startsWith("//") ? builtinFailure(pathname) : await renderPage(undefined, new URL(pathname, config.origin), undefined, request.signal, serverFailure)
|
|
231
|
+
.catch((again) => { report(again); return builtinFailure(pathname); });
|
|
232
|
+
return pageResponse(page, request, false, assets?.(page));
|
|
233
|
+
};
|
|
234
|
+
const pipeline = {
|
|
235
|
+
base: config.base, routes, endpoints,
|
|
236
|
+
redirects: new Map([...redirects].map(([from, location]) => [from, { location, status: 308 }])),
|
|
237
|
+
endpoint: async (route) => await environment.runner.import(route.file),
|
|
238
|
+
...(middleware === undefined ? {} : { middleware: async () => {
|
|
239
|
+
const handler = (await environment.runner.import(middleware)).default;
|
|
240
|
+
if (typeof handler !== "function")
|
|
241
|
+
throw new HtmlKitError("Middleware must export a default function.", middleware);
|
|
242
|
+
return handler;
|
|
243
|
+
} }),
|
|
244
|
+
async page(route, params, request, payload) {
|
|
245
|
+
const url = new URL(request.url);
|
|
246
|
+
// A prerendered page renders as the build renders it: at its canonical URL, without the request.
|
|
247
|
+
const server = route.render === "server";
|
|
248
|
+
const page = await renderPage({ route, params }, server ? url : new URL(url.pathname, config.origin), server ? request : undefined, request.signal);
|
|
249
|
+
return pageResponse(page, request, payload, assets?.(page));
|
|
250
|
+
},
|
|
251
|
+
async notFound(request, payload) {
|
|
252
|
+
const { pathname } = new URL(request.url);
|
|
253
|
+
if (payload)
|
|
254
|
+
return new Response(null, { status: 404 });
|
|
255
|
+
const page = pathname.startsWith("//") ? builtinNotFound(pathname) : await renderPage(undefined, new URL(pathname, config.origin), undefined, request.signal);
|
|
256
|
+
return pageResponse(page, request, false, assets?.(page));
|
|
257
|
+
},
|
|
278
258
|
};
|
|
279
259
|
const application = {
|
|
280
|
-
...config, routes, ...(notFoundRoute === undefined ? {} : { notFound: notFoundRoute }),
|
|
260
|
+
...config, routes, ...(notFoundRoute === undefined ? {} : { notFound: notFoundRoute }), ...(errorRoute === undefined ? {} : { error: errorRoute }),
|
|
261
|
+
navigation: navigation.navigation, navigationTree: navigation.navigationTree,
|
|
262
|
+
files, watchFiles: watched, endpoints, ...(middleware === undefined ? {} : { middleware }),
|
|
281
263
|
async entries() {
|
|
282
264
|
const paths = await concreteRoutes();
|
|
283
265
|
const missing = unenumerated[0];
|
|
284
266
|
if (missing !== undefined)
|
|
285
267
|
throw new HtmlKitError(`Static entries() must enumerate parameters: ${missing.params.join(", ")}.`, origin(missing));
|
|
286
|
-
|
|
268
|
+
// Server-rendered pages render on request, so a build lists only prerendered ones.
|
|
269
|
+
return [...paths].filter(([, route]) => route.render !== "server").map(([path]) => path).sort();
|
|
287
270
|
},
|
|
288
271
|
async render(pathname, signal = new AbortController().signal) {
|
|
289
272
|
signal.throwIfAborted();
|
|
290
|
-
const notFound = () => ({ status: 404, pathname, ...notFoundPage(scripts), css: "", components: [], layers: [] });
|
|
291
273
|
// A path starting with // would name another origin; no page lives there, for documents or payloads.
|
|
292
274
|
if (pathname.startsWith("//"))
|
|
293
|
-
return
|
|
275
|
+
return builtinNotFound(pathname);
|
|
294
276
|
const url = new URL(pathname, config.origin);
|
|
295
277
|
if (url.origin !== config.origin || url.search || url.hash)
|
|
296
278
|
throw new HtmlKitError("Static rendering requires an application pathname without a query or fragment.");
|
|
297
|
-
const redirected = (location, status) => ({ status, pathname: url.pathname, location, ...redirectPage(location), css: "", components: [], layers: [] });
|
|
298
279
|
// The redirects option precedes pages, with or without the trailing slash.
|
|
299
280
|
const configured = redirects.get(url.pathname.endsWith("/") ? url.pathname : url.pathname + "/");
|
|
300
281
|
if (configured !== undefined)
|
|
301
|
-
return redirected(configured, 308);
|
|
302
|
-
|
|
303
|
-
// Unmatched paths render app/pages/404.html through its layouts, or the built-in page.
|
|
304
|
-
if (matched === undefined && notFoundRoute === undefined)
|
|
305
|
-
return notFound();
|
|
306
|
-
const { route, params } = matched ?? { route: notFoundRoute, params: {} };
|
|
307
|
-
const status = matched === undefined ? 404 : 200;
|
|
308
|
-
const layers = [...route.layouts, route];
|
|
309
|
-
// Vite owns module caching and dependency invalidation. Per-render results never enter
|
|
310
|
-
// a shared cache; HTML Next owns the DOM worker and declarative component semantics.
|
|
311
|
-
const packages = new Map();
|
|
312
|
-
const resources = new Map();
|
|
313
|
-
const layerURLs = new Set(layers.map(layer => pathToFileURL(layer.component).href));
|
|
314
|
-
const resolveCSS = server.config.createResolver({ extensions: [".css"], mainFields: ["style"], conditions: ["style", "development|production"], preferRelative: true, tryIndex: false });
|
|
315
|
-
const graph = await loadNodeComponents(layers.map(layer => pathToFileURL(layer.component).href), {
|
|
316
|
-
readStylesheet: async (url) => ({ url, source: await readFile(fileURLToPath(url), "utf8") }),
|
|
317
|
-
resolveStylesheet: async (specifier, parentURL) => {
|
|
318
|
-
const resolved = await resolveCSS(specifier, fileURLToPath(parentURL));
|
|
319
|
-
return resolved === undefined ? undefined
|
|
320
|
-
: pathToFileURL(resolved.replace(/[?#].*$/, "")).href + (/[?#].*$/.exec(resolved)?.[0] ?? "");
|
|
321
|
-
},
|
|
322
|
-
stylesheetAssetURL: url => url.startsWith("file:") ? `/@fs/${fileURLToPath(url)}${new URL(url).search}${new URL(url).hash}` : url,
|
|
323
|
-
// The graph resolver is synchronous. Prepare its bare imports while asynchronously
|
|
324
|
-
// reading each carrier, using Vite's ESM resolution from the consuming application.
|
|
325
|
-
importMap: map,
|
|
326
|
-
readComponent: async (url) => {
|
|
327
|
-
let source = sources.get(fileURLToPath(url)) ?? await readFile(fileURLToPath(url), "utf8");
|
|
328
|
-
for (const [tag, href] of builtins)
|
|
329
|
-
if (new RegExp(`<${tag}[\\s/>]`).test(source))
|
|
330
|
-
source += `\n<link rel="component" href="${href}">`;
|
|
331
|
-
const parsed = layerURLs.has(url) ? applicationResource(source, url) : parseComponentResource(source, url);
|
|
332
|
-
if ("configuration" in parsed)
|
|
333
|
-
resources.set(url, parsed);
|
|
334
|
-
const imports = [...parsed.dependencies, ...parsed.definitions.flatMap(definition => definition.controller === undefined ? [] : [definition.controller])];
|
|
335
|
-
for (const specifier of imports) {
|
|
336
|
-
if (/^(?:[A-Za-z][A-Za-z\d+.-]*:|\/|\.\.?\/)/.test(specifier) || Object.keys(map.imports).some(key => specifier.startsWith(key)))
|
|
337
|
-
continue;
|
|
338
|
-
const resolved = await server.environments.client.pluginContainer.resolveId(specifier, fileURLToPath(url));
|
|
339
|
-
if (resolved === null || resolved.external)
|
|
340
|
-
throw new HtmlKitError(`Cannot resolve component import ${specifier}.`, url);
|
|
341
|
-
packages.set(`${url}\0${specifier}`, packageResource(resolved.id));
|
|
342
|
-
}
|
|
343
|
-
return { url, source };
|
|
344
|
-
},
|
|
345
|
-
resolvePackage: (specifier, parentURL) => {
|
|
346
|
-
const resolved = packages.get(`${parentURL}\0${specifier}`);
|
|
347
|
-
if (resolved === undefined)
|
|
348
|
-
throw new HtmlKitError(`Cannot resolve component import ${specifier}.`, parentURL);
|
|
349
|
-
return resolved;
|
|
350
|
-
},
|
|
351
|
-
});
|
|
352
|
-
const layerDefinitions = layers.map(layer => {
|
|
353
|
-
if (layer === route)
|
|
354
|
-
return pageDefinition(resources.get(pathToFileURL(layer.component).href), layer.component);
|
|
355
|
-
const roots = graph.roots.filter(id => graph.nodes.get(id).url === pathToFileURL(layer.component).href);
|
|
356
|
-
if (roots.length !== 1)
|
|
357
|
-
throw new HtmlKitError("A page or layout must declare exactly one root component.", layer.component);
|
|
358
|
-
return graph.nodes.get(roots[0]).definition;
|
|
359
|
-
});
|
|
360
|
-
if (layerDefinitions.at(-1).contract.tag !== route.pageName) {
|
|
361
|
-
throw new HtmlKitError("Page component name changed; rediscover routes before rendering.", route.component);
|
|
362
|
-
}
|
|
363
|
-
for (const definition of layerDefinitions.slice(0, -1)) {
|
|
364
|
-
if (!definition.slots?.some(slot => slot.name === "page"))
|
|
365
|
-
throw new HtmlKitError('Layout requires a slot named "page".', definition.source.file);
|
|
366
|
-
}
|
|
367
|
-
const results = [];
|
|
368
|
-
const attributes = [];
|
|
369
|
-
let parent = Object.freeze({});
|
|
370
|
-
let head = {};
|
|
371
|
-
// An alias marks its page's own entry current.
|
|
372
|
-
const current = route.canonical === undefined ? url.pathname : config.base + route.canonical.slice(1);
|
|
373
|
-
for (const layer of layers) {
|
|
374
|
-
signal.throwIfAborted();
|
|
375
|
-
const loaded = await module(layer);
|
|
376
|
-
const context = { phase: "prerender", url: new URL(url), base: config.base, params, parent,
|
|
377
|
-
propNames: Object.freeze(Object.keys(layerDefinitions[results.length].contract.props)),
|
|
378
|
-
fetch: globalThis.fetch, signal,
|
|
379
|
-
navigation: (query) => navigation({ current, ...query }),
|
|
380
|
-
navigationTree: (query) => navigationTree({ current, ...query }),
|
|
381
|
-
breadcrumbs: (query) => breadcrumbs({ current, ...query }),
|
|
382
|
-
pager: (query) => pager({ current, ...query }),
|
|
383
|
-
redirect(location, status = 303) {
|
|
384
|
-
if (!redirectStatuses.includes(status))
|
|
385
|
-
throw new HtmlKitError(`Redirect status ${String(status)} must be 301, 302, 303, 307, or 308.`, origin(layer));
|
|
386
|
-
throw new Redirect(redirectLocation(config.base, location, origin(layer)), status);
|
|
387
|
-
},
|
|
388
|
-
get request() { throw new HtmlKitError("request is unavailable during static generation.", origin(layer)); } };
|
|
389
|
-
let result;
|
|
390
|
-
try {
|
|
391
|
-
result = loaded.load === undefined ? {} : await loaded.load(context);
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
if (error instanceof Redirect)
|
|
395
|
-
return redirected(error.location, error.status);
|
|
396
|
-
throw error;
|
|
397
|
-
}
|
|
398
|
-
if (result === null || typeof result !== "object" || Array.isArray(result))
|
|
399
|
-
throw new HtmlKitError("load() must return a LoaderResult object.", origin(layer));
|
|
400
|
-
results.push(result);
|
|
401
|
-
parent = Object.freeze({ ...parent, ...result.data });
|
|
402
|
-
const index = results.length - 1;
|
|
403
|
-
// Checked against the layer's own component, which a prop's diagnostic names; its head reuses them.
|
|
404
|
-
attributes.push(invocationAttributes(layerDefinitions[index], result));
|
|
405
|
-
head = await renderHead(resources.get(pathToFileURL(layer.component).href), layerDefinitions[index], result, head, url.href, definition => invocation(definition.contract.tag, attributes[index], "hk-head", "", false));
|
|
406
|
-
}
|
|
407
|
-
if (scripts.length > 0)
|
|
408
|
-
head = { ...head, scripts };
|
|
409
|
-
const state = {};
|
|
410
|
-
const rendered = layers.map((_layer, i) => ({ component: layerDefinitions[i].contract.tag,
|
|
411
|
-
attributes: attributes[i], ...(results[i].state === undefined ? {} : { state: results[i].state }) }));
|
|
412
|
-
let body = "";
|
|
413
|
-
for (let i = layers.length - 1; i >= 0; i--) {
|
|
414
|
-
const id = `hk-layer-${i}`;
|
|
415
|
-
const layer = rendered[i];
|
|
416
|
-
if (layer.state !== undefined)
|
|
417
|
-
state[`#${id}`] = layer.state;
|
|
418
|
-
body = invocation(layer.component, layer.attributes, id, body, i > 0);
|
|
419
|
-
}
|
|
420
|
-
const components = [...graph.nodes.values()].map(node => ({ definition: node.definition,
|
|
421
|
-
styles: compileComponentStylesForBuild(node.definition.css, node.definition),
|
|
422
|
-
...(node.controller === undefined ? {} : { controller: fileURLToPath(node.controller.url) }) }));
|
|
423
|
-
const output = await renderComponents(body, { definitions: components.map(value => value.definition), url: url.href, state });
|
|
424
|
-
signal.throwIfAborted();
|
|
425
|
-
return { status, pathname: url.pathname, body: output.html, css: output.css, head,
|
|
426
|
-
html: documentHTML(output.html, head), components, layers: rendered };
|
|
282
|
+
return redirected(url.pathname, configured, 308);
|
|
283
|
+
return renderPage(matchRoute(routes, url.pathname, config.base), url, undefined, signal);
|
|
427
284
|
},
|
|
428
285
|
// Not a method of `this`: runtimes are handed the function itself, as in Deno.serve(application.fetch).
|
|
429
|
-
|
|
430
|
-
if (request.method !== "GET" && request.method !== "HEAD")
|
|
431
|
-
return new Response(null, { status: 405, headers: { allow: "GET, HEAD" } });
|
|
432
|
-
const { pathname } = new URL(request.url);
|
|
433
|
-
// A page's payload, through the same render as its document.
|
|
434
|
-
const pages = config.base + "_htmlkit/pages/";
|
|
435
|
-
const payload = pathname.startsWith(pages) ? /^(.*\/)?payload\.json$/.exec(pathname.slice(pages.length)) : null;
|
|
436
|
-
if (payload !== null) {
|
|
437
|
-
const page = await application.render(config.base + (payload[1] ?? ""), request.signal);
|
|
438
|
-
const data = page.status === 200 && payloadPath(config.base, page.pathname) === pathname
|
|
439
|
-
? pagePayload(page, assets?.(page) ?? { styles: [], modules: [] }) : undefined;
|
|
440
|
-
return new Response(data === undefined || request.method === "HEAD" ? null : JSON.stringify(data), { status: data === undefined ? 404 : 200, headers: { "content-type": "application/json" } });
|
|
441
|
-
}
|
|
442
|
-
const page = await application.render(pathname, request.signal);
|
|
443
|
-
if (page.location !== undefined) {
|
|
444
|
-
return new Response(request.method === "HEAD" ? null : page.html, { status: page.status, headers: { location: page.location, "content-type": "text/html; charset=utf-8" } });
|
|
445
|
-
}
|
|
446
|
-
if (page.status === 200 && !pathname.endsWith("/"))
|
|
447
|
-
return new Response(null, { status: 308, headers: { location: pathname + "/" } });
|
|
448
|
-
return new Response(request.method === "HEAD" ? null : documentHTML(page.body, page.head, assets?.(page)), { status: page.status, headers: { "content-type": "text/html; charset=utf-8" } });
|
|
449
|
-
},
|
|
286
|
+
fetch: request => respond(pipeline, request).catch((error) => failed(error, request)),
|
|
450
287
|
async close() { if (ownsServer)
|
|
451
288
|
await server.close(); },
|
|
452
289
|
};
|
|
453
|
-
return application;
|
|
290
|
+
return { application, graph, catalog: concreteRoutes };
|
|
291
|
+
}
|
|
292
|
+
/** assets supplies a serving adapter's browser modules and stylesheets for fetch(). */
|
|
293
|
+
export async function createApplication(input = {}, moduleServer, assets) {
|
|
294
|
+
return (await createApplicationInternals(input, moduleServer, assets)).application;
|
|
454
295
|
}
|
|
455
296
|
//# sourceMappingURL=application.js.map
|