@nifrajs/web-vanilla 0.1.0-alpha.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nifra Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @nifrajs/web-vanilla
2
+
3
+ Zero-framework render adapter for @nifrajs/web — auto-escaping tagged-template HTML, ~0 KB client JS, interactivity via @nifrajs/web/islands.
4
+
5
+ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework — one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ bun add @nifrajs/web-vanilla
11
+ ```
12
+
13
+ ## Docs
14
+
15
+ - Reference: <https://nifra.dev/docs>
16
+ - AI-readable: <https://nifra.dev/llms.txt>
17
+
18
+ MIT
@@ -0,0 +1,17 @@
1
+ import type { RenderProps } from "@nifrajs/web";
2
+ import type { Template } from "./html.js";
3
+ /**
4
+ * A vanilla "component": a plain function from props to a {@link Template}. The page (innermost
5
+ * chain element) receives the loader {@link RenderProps}; a layout receives `{ children }` — the
6
+ * already-rendered inner fragment — plus the same render props, mirroring the React/Preact
7
+ * adapters' `children` contract.
8
+ */
9
+ export type VanillaComponent = (props: RenderProps & {
10
+ children?: Template;
11
+ }) => Template;
12
+ /**
13
+ * Fold a layout chain (outermost layout → page) into one {@link Template}: render the page with
14
+ * the loader props, then wrap upward, each layout receiving the inner fragment as `children`.
15
+ */
16
+ export declare function compose(chain: readonly unknown[], props: RenderProps): Template;
17
+ //# sourceMappingURL=compose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEzC;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG;IAAE,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAE,KAAK,QAAQ,CAAA;AAEzF;;;GAGG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,QAAQ,CAO/E"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Fold a layout chain (outermost layout → page) into one {@link Template}: render the page with
3
+ * the loader props, then wrap upward, each layout receiving the inner fragment as `children`.
4
+ */
5
+ export function compose(chain, props) {
6
+ const last = chain.length - 1;
7
+ let node = chain[last](props);
8
+ for (let i = last - 1; i >= 0; i--) {
9
+ node = chain[i]({ ...props, children: node });
10
+ }
11
+ return node;
12
+ }
13
+ //# sourceMappingURL=compose.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB;IACnE,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAC7B,IAAI,IAAI,GAAI,KAAK,CAAC,IAAI,CAAsB,CAAC,KAAK,CAAC,CAAA;IACnD,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,IAAI,GAAI,KAAK,CAAC,CAAC,CAAsB,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACrE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
package/dist/html.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The tagged-template HTML engine — `html` builds an escaped-by-construction {@link Template}.
3
+ *
4
+ * Security model (the part that matters): every interpolated value is HTML-escaped by default.
5
+ * The ONLY ways to emit unescaped markup are (1) nesting another `html` template — already escaped
6
+ * by construction — and (2) an explicit {@link raw} wrapper, which is the audit-greppable opt-out.
7
+ * The escaper covers text and double-quoted attribute contexts (`& < > " '`); interpolations into
8
+ * unquoted attributes or inline scripts are NOT safe by design — the docs say "always quote".
9
+ */
10
+ /** Branded wrapper marking a string as pre-trusted markup. Construct only via {@link raw}. */
11
+ export declare class RawHtml {
12
+ readonly value: string;
13
+ constructor(value: string);
14
+ }
15
+ /**
16
+ * Mark a string as trusted, pre-escaped markup — it is emitted verbatim. The deliberate escape
17
+ * hatch (CMS-sanitized HTML, pre-rendered markdown): every call site is greppable, exactly like
18
+ * React's dangerouslySetInnerHTML, without the JSX.
19
+ */
20
+ export declare function raw(trusted: string): RawHtml;
21
+ /** A rendered HTML fragment — what `html` returns and components produce. Stringified once. */
22
+ export declare class Template {
23
+ /** The final markup. Built eagerly at tag time (interpolations are already values by then). */
24
+ readonly html: string;
25
+ constructor(htmlString: string);
26
+ toString(): string;
27
+ }
28
+ /** What an interpolation may be: escaped primitives, nested templates/raw, arrays of the same.
29
+ * `null`/`undefined`/`false` render as nothing (conditional rendering: `cond && html\`…\``). */
30
+ export type HtmlValue = string | number | bigint | boolean | null | undefined | Template | RawHtml | ReadonlyArray<HtmlValue>;
31
+ /** The tag: `` html`<p>${user.name}</p>` `` → an escaped {@link Template}. */
32
+ export declare function html(strings: TemplateStringsArray, ...values: HtmlValue[]): Template;
33
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8FAA8F;AAC9F,qBAAa,OAAO;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;gBACV,KAAK,EAAE,MAAM;CAG1B;AAED;;;;GAIG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5C;AAmCD,+FAA+F;AAC/F,qBAAa,QAAQ;IACnB,+FAA+F;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;gBACT,UAAU,EAAE,MAAM;IAG9B,QAAQ,IAAI,MAAM;CAGnB;AAED;gGACgG;AAChG,MAAM,MAAM,SAAS,GACjB,MAAM,GACN,MAAM,GACN,MAAM,GACN,OAAO,GACP,IAAI,GACJ,SAAS,GACT,QAAQ,GACR,OAAO,GACP,aAAa,CAAC,SAAS,CAAC,CAAA;AAoB5B,8EAA8E;AAC9E,wBAAgB,IAAI,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,GAAG,QAAQ,CAMpF"}
package/dist/html.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The tagged-template HTML engine — `html` builds an escaped-by-construction {@link Template}.
3
+ *
4
+ * Security model (the part that matters): every interpolated value is HTML-escaped by default.
5
+ * The ONLY ways to emit unescaped markup are (1) nesting another `html` template — already escaped
6
+ * by construction — and (2) an explicit {@link raw} wrapper, which is the audit-greppable opt-out.
7
+ * The escaper covers text and double-quoted attribute contexts (`& < > " '`); interpolations into
8
+ * unquoted attributes or inline scripts are NOT safe by design — the docs say "always quote".
9
+ */
10
+ /** Branded wrapper marking a string as pre-trusted markup. Construct only via {@link raw}. */
11
+ export class RawHtml {
12
+ value;
13
+ constructor(value) {
14
+ this.value = value;
15
+ }
16
+ }
17
+ /**
18
+ * Mark a string as trusted, pre-escaped markup — it is emitted verbatim. The deliberate escape
19
+ * hatch (CMS-sanitized HTML, pre-rendered markdown): every call site is greppable, exactly like
20
+ * React's dangerouslySetInnerHTML, without the JSX.
21
+ */
22
+ export function raw(trusted) {
23
+ return new RawHtml(trusted);
24
+ }
25
+ const ESCAPE_RE = /[&<>"']/;
26
+ const escapeHtml = (s) => {
27
+ // Fast path: most text has nothing to escape — one regex probe, zero allocation.
28
+ if (!ESCAPE_RE.test(s))
29
+ return s;
30
+ let out = "";
31
+ let last = 0;
32
+ for (let i = 0; i < s.length; i++) {
33
+ let entity;
34
+ switch (s.charCodeAt(i)) {
35
+ case 38:
36
+ entity = "&amp;";
37
+ break;
38
+ case 60:
39
+ entity = "&lt;";
40
+ break;
41
+ case 62:
42
+ entity = "&gt;";
43
+ break;
44
+ case 34:
45
+ entity = "&quot;";
46
+ break;
47
+ case 39:
48
+ entity = "&#39;";
49
+ break;
50
+ default:
51
+ continue;
52
+ }
53
+ out += s.slice(last, i) + entity;
54
+ last = i + 1;
55
+ }
56
+ return out + s.slice(last);
57
+ };
58
+ /** A rendered HTML fragment — what `html` returns and components produce. Stringified once. */
59
+ export class Template {
60
+ /** The final markup. Built eagerly at tag time (interpolations are already values by then). */
61
+ html;
62
+ constructor(htmlString) {
63
+ this.html = htmlString;
64
+ }
65
+ toString() {
66
+ return this.html;
67
+ }
68
+ }
69
+ function renderValue(value) {
70
+ if (value == null || value === false)
71
+ return "";
72
+ if (typeof value === "string")
73
+ return escapeHtml(value);
74
+ if (typeof value === "number" || typeof value === "bigint")
75
+ return String(value);
76
+ if (value === true)
77
+ return "true";
78
+ if (value instanceof Template)
79
+ return value.html;
80
+ if (value instanceof RawHtml)
81
+ return value.value;
82
+ if (Array.isArray(value)) {
83
+ let out = "";
84
+ for (const item of value)
85
+ out += renderValue(item);
86
+ return out;
87
+ }
88
+ // Objects/functions are a bug at the call site — render loudly rather than "[object Object]".
89
+ throw new TypeError(`[nifra/web-vanilla] html: unsupported interpolation of type ${typeof value} — interpolate strings, numbers, nested html\`…\`, raw(), or arrays of those`);
90
+ }
91
+ /** The tag: `` html`<p>${user.name}</p>` `` → an escaped {@link Template}. */
92
+ export function html(strings, ...values) {
93
+ let out = strings[0];
94
+ for (let i = 0; i < values.length; i++) {
95
+ out += renderValue(values[i]) + strings[i + 1];
96
+ }
97
+ return new Template(out);
98
+ }
99
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @nifrajs/web-vanilla — the zero-framework render adapter for @nifrajs/web.
3
+ *
4
+ * Pages are plain functions returning auto-escaping `html` tagged templates; the client ships
5
+ * **no framework runtime at all**. Use it for content/SEO surfaces (landing pages, listings,
6
+ * docs, comparison tables) where HTML is the product — interactivity comes from
7
+ * `@nifrajs/web/islands` entries (`islandScripts`), each a hand-sized vanilla module.
8
+ *
9
+ * Routes using this adapter are server-rendered documents, not hydrated apps — set
10
+ * `export const hydrate = false` on the route (there is no client runtime to hydrate with);
11
+ * `renderPage` then omits the framework bootstrap entirely. Loaders, actions, ISR, SSG, head
12
+ * management, and streaming all work unchanged — they live in @nifrajs/web, not the view layer.
13
+ *
14
+ * import { html, vanillaAdapter } from "@nifrajs/web-vanilla"
15
+ *
16
+ * export default function Hotels({ data }) {
17
+ * const { hotels } = data as { hotels: Array<{ name: string; pricePaise: number }> }
18
+ * return html`<ul>${hotels.map((h) => html`<li>${h.name} — ₹${h.pricePaise / 100}</li>`)}</ul>`
19
+ * }
20
+ */
21
+ import type { RenderAdapter } from "@nifrajs/web";
22
+ export { compose, type VanillaComponent } from "./compose.js";
23
+ export { type HtmlValue, html, RawHtml, raw, Template } from "./html.js";
24
+ /** The zero-framework server render adapter — pass to @nifrajs/web's `renderPage`/`createWebApp`. */
25
+ export declare const vanillaAdapter: RenderAdapter;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,cAAc,CAAA;AAG9D,OAAO,EAAE,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAE,KAAK,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAIxE,qGAAqG;AACrG,eAAO,MAAM,cAAc,EAAE,aAsB5B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @nifrajs/web-vanilla — the zero-framework render adapter for @nifrajs/web.
3
+ *
4
+ * Pages are plain functions returning auto-escaping `html` tagged templates; the client ships
5
+ * **no framework runtime at all**. Use it for content/SEO surfaces (landing pages, listings,
6
+ * docs, comparison tables) where HTML is the product — interactivity comes from
7
+ * `@nifrajs/web/islands` entries (`islandScripts`), each a hand-sized vanilla module.
8
+ *
9
+ * Routes using this adapter are server-rendered documents, not hydrated apps — set
10
+ * `export const hydrate = false` on the route (there is no client runtime to hydrate with);
11
+ * `renderPage` then omits the framework bootstrap entirely. Loaders, actions, ISR, SSG, head
12
+ * management, and streaming all work unchanged — they live in @nifrajs/web, not the view layer.
13
+ *
14
+ * import { html, vanillaAdapter } from "@nifrajs/web-vanilla"
15
+ *
16
+ * export default function Hotels({ data }) {
17
+ * const { hotels } = data as { hotels: Array<{ name: string; pricePaise: number }> }
18
+ * return html`<ul>${hotels.map((h) => html`<li>${h.name} — ₹${h.pricePaise / 100}</li>`)}</ul>`
19
+ * }
20
+ */
21
+ import { compose } from "./compose.js";
22
+ export { compose } from "./compose.js";
23
+ export { html, RawHtml, raw, Template } from "./html.js";
24
+ const ENCODER = new TextEncoder();
25
+ /** The zero-framework server render adapter — pass to @nifrajs/web's `renderPage`/`createWebApp`. */
26
+ export const vanillaAdapter = {
27
+ // One-pass string render — renderPage's buffered fast path uses this for every non-deferred
28
+ // page, which for vanilla routes is the norm.
29
+ renderToString(chain, props) {
30
+ return compose(chain, props).html;
31
+ },
32
+ // Deferred pages need the streaming seam; a synchronous renderer satisfies it with a one-chunk
33
+ // stream (the shell IS the document — vanilla has no Suspense boundaries of its own; @nifrajs/web's
34
+ // defer()/<no-js fallback> machinery handles progressive data above this seam).
35
+ renderToStream(chain, props) {
36
+ const body = compose(chain, props).html;
37
+ return new ReadableStream({
38
+ start(controller) {
39
+ controller.enqueue(ENCODER.encode(body));
40
+ controller.close();
41
+ },
42
+ });
43
+ },
44
+ // No client runtime ⇒ no per-document bootstrap markup.
45
+ hydrationHead() {
46
+ return "";
47
+ },
48
+ };
49
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,OAAO,EAAyB,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAkB,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAExE,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC,qGAAqG;AACrG,MAAM,CAAC,MAAM,cAAc,GAAkB;IAC3C,4FAA4F;IAC5F,8CAA8C;IAC9C,cAAc,CAAC,KAAyB,EAAE,KAAkB;QAC1D,OAAO,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,CAAA;IACnC,CAAC;IACD,+FAA+F;IAC/F,oGAAoG;IACpG,gFAAgF;IAChF,cAAc,CAAC,KAAyB,EAAE,KAAkB;QAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,CAAA;QACvC,OAAO,IAAI,cAAc,CAAa;YACpC,KAAK,CAAC,UAAU;gBACd,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;gBACxC,UAAU,CAAC,KAAK,EAAE,CAAA;YACpB,CAAC;SACF,CAAC,CAAA;IACJ,CAAC;IACD,wDAAwD;IACxD,aAAa;QACX,OAAO,EAAE,CAAA;IACX,CAAC;CACF,CAAA"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@nifrajs/web-vanilla",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Zero-framework render adapter for @nifrajs/web — auto-escaping tagged-template HTML, ~0 KB client JS, interactivity via @nifrajs/web/islands.",
5
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "web-framework",
10
+ "full-stack",
11
+ "edge",
12
+ "ssr"
13
+ ],
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "bun": "./src/index.ts",
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "sideEffects": false,
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist",
32
+ "test": "bun test",
33
+ "typecheck": "tsc --noEmit"
34
+ },
35
+ "peerDependencies": {
36
+ "@nifrajs/web": "workspace:*"
37
+ },
38
+ "license": "MIT",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/nifrajs/nifra.git",
45
+ "directory": "packages/web-vanilla"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/nifrajs/nifra/issues"
49
+ },
50
+ "homepage": "https://github.com/nifrajs/nifra#readme"
51
+ }
package/src/compose.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { RenderProps } from "@nifrajs/web"
2
+ import type { Template } from "./html.ts"
3
+
4
+ /**
5
+ * A vanilla "component": a plain function from props to a {@link Template}. The page (innermost
6
+ * chain element) receives the loader {@link RenderProps}; a layout receives `{ children }` — the
7
+ * already-rendered inner fragment — plus the same render props, mirroring the React/Preact
8
+ * adapters' `children` contract.
9
+ */
10
+ export type VanillaComponent = (props: RenderProps & { children?: Template }) => Template
11
+
12
+ /**
13
+ * Fold a layout chain (outermost layout → page) into one {@link Template}: render the page with
14
+ * the loader props, then wrap upward, each layout receiving the inner fragment as `children`.
15
+ */
16
+ export function compose(chain: readonly unknown[], props: RenderProps): Template {
17
+ const last = chain.length - 1
18
+ let node = (chain[last] as VanillaComponent)(props)
19
+ for (let i = last - 1; i >= 0; i--) {
20
+ node = (chain[i] as VanillaComponent)({ ...props, children: node })
21
+ }
22
+ return node
23
+ }
package/src/html.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * The tagged-template HTML engine — `html` builds an escaped-by-construction {@link Template}.
3
+ *
4
+ * Security model (the part that matters): every interpolated value is HTML-escaped by default.
5
+ * The ONLY ways to emit unescaped markup are (1) nesting another `html` template — already escaped
6
+ * by construction — and (2) an explicit {@link raw} wrapper, which is the audit-greppable opt-out.
7
+ * The escaper covers text and double-quoted attribute contexts (`& < > " '`); interpolations into
8
+ * unquoted attributes or inline scripts are NOT safe by design — the docs say "always quote".
9
+ */
10
+
11
+ /** Branded wrapper marking a string as pre-trusted markup. Construct only via {@link raw}. */
12
+ export class RawHtml {
13
+ readonly value: string
14
+ constructor(value: string) {
15
+ this.value = value
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Mark a string as trusted, pre-escaped markup — it is emitted verbatim. The deliberate escape
21
+ * hatch (CMS-sanitized HTML, pre-rendered markdown): every call site is greppable, exactly like
22
+ * React's dangerouslySetInnerHTML, without the JSX.
23
+ */
24
+ export function raw(trusted: string): RawHtml {
25
+ return new RawHtml(trusted)
26
+ }
27
+
28
+ const ESCAPE_RE = /[&<>"']/
29
+ const escapeHtml = (s: string): string => {
30
+ // Fast path: most text has nothing to escape — one regex probe, zero allocation.
31
+ if (!ESCAPE_RE.test(s)) return s
32
+ let out = ""
33
+ let last = 0
34
+ for (let i = 0; i < s.length; i++) {
35
+ let entity: string
36
+ switch (s.charCodeAt(i)) {
37
+ case 38:
38
+ entity = "&amp;"
39
+ break
40
+ case 60:
41
+ entity = "&lt;"
42
+ break
43
+ case 62:
44
+ entity = "&gt;"
45
+ break
46
+ case 34:
47
+ entity = "&quot;"
48
+ break
49
+ case 39:
50
+ entity = "&#39;"
51
+ break
52
+ default:
53
+ continue
54
+ }
55
+ out += s.slice(last, i) + entity
56
+ last = i + 1
57
+ }
58
+ return out + s.slice(last)
59
+ }
60
+
61
+ /** A rendered HTML fragment — what `html` returns and components produce. Stringified once. */
62
+ export class Template {
63
+ /** The final markup. Built eagerly at tag time (interpolations are already values by then). */
64
+ readonly html: string
65
+ constructor(htmlString: string) {
66
+ this.html = htmlString
67
+ }
68
+ toString(): string {
69
+ return this.html
70
+ }
71
+ }
72
+
73
+ /** What an interpolation may be: escaped primitives, nested templates/raw, arrays of the same.
74
+ * `null`/`undefined`/`false` render as nothing (conditional rendering: `cond && html\`…\``). */
75
+ export type HtmlValue =
76
+ | string
77
+ | number
78
+ | bigint
79
+ | boolean
80
+ | null
81
+ | undefined
82
+ | Template
83
+ | RawHtml
84
+ | ReadonlyArray<HtmlValue>
85
+
86
+ function renderValue(value: HtmlValue): string {
87
+ if (value == null || value === false) return ""
88
+ if (typeof value === "string") return escapeHtml(value)
89
+ if (typeof value === "number" || typeof value === "bigint") return String(value)
90
+ if (value === true) return "true"
91
+ if (value instanceof Template) return value.html
92
+ if (value instanceof RawHtml) return value.value
93
+ if (Array.isArray(value)) {
94
+ let out = ""
95
+ for (const item of value) out += renderValue(item)
96
+ return out
97
+ }
98
+ // Objects/functions are a bug at the call site — render loudly rather than "[object Object]".
99
+ throw new TypeError(
100
+ `[nifra/web-vanilla] html: unsupported interpolation of type ${typeof value} — interpolate strings, numbers, nested html\`…\`, raw(), or arrays of those`,
101
+ )
102
+ }
103
+
104
+ /** The tag: `` html`<p>${user.name}</p>` `` → an escaped {@link Template}. */
105
+ export function html(strings: TemplateStringsArray, ...values: HtmlValue[]): Template {
106
+ let out = strings[0] as string
107
+ for (let i = 0; i < values.length; i++) {
108
+ out += renderValue(values[i] as HtmlValue) + (strings[i + 1] as string)
109
+ }
110
+ return new Template(out)
111
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @nifrajs/web-vanilla — the zero-framework render adapter for @nifrajs/web.
3
+ *
4
+ * Pages are plain functions returning auto-escaping `html` tagged templates; the client ships
5
+ * **no framework runtime at all**. Use it for content/SEO surfaces (landing pages, listings,
6
+ * docs, comparison tables) where HTML is the product — interactivity comes from
7
+ * `@nifrajs/web/islands` entries (`islandScripts`), each a hand-sized vanilla module.
8
+ *
9
+ * Routes using this adapter are server-rendered documents, not hydrated apps — set
10
+ * `export const hydrate = false` on the route (there is no client runtime to hydrate with);
11
+ * `renderPage` then omits the framework bootstrap entirely. Loaders, actions, ISR, SSG, head
12
+ * management, and streaming all work unchanged — they live in @nifrajs/web, not the view layer.
13
+ *
14
+ * import { html, vanillaAdapter } from "@nifrajs/web-vanilla"
15
+ *
16
+ * export default function Hotels({ data }) {
17
+ * const { hotels } = data as { hotels: Array<{ name: string; pricePaise: number }> }
18
+ * return html`<ul>${hotels.map((h) => html`<li>${h.name} — ₹${h.pricePaise / 100}</li>`)}</ul>`
19
+ * }
20
+ */
21
+
22
+ import type { RenderAdapter, RenderProps } from "@nifrajs/web"
23
+ import { compose } from "./compose.ts"
24
+
25
+ export { compose, type VanillaComponent } from "./compose.ts"
26
+ export { type HtmlValue, html, RawHtml, raw, Template } from "./html.ts"
27
+
28
+ const ENCODER = new TextEncoder()
29
+
30
+ /** The zero-framework server render adapter — pass to @nifrajs/web's `renderPage`/`createWebApp`. */
31
+ export const vanillaAdapter: RenderAdapter = {
32
+ // One-pass string render — renderPage's buffered fast path uses this for every non-deferred
33
+ // page, which for vanilla routes is the norm.
34
+ renderToString(chain: readonly unknown[], props: RenderProps): string {
35
+ return compose(chain, props).html
36
+ },
37
+ // Deferred pages need the streaming seam; a synchronous renderer satisfies it with a one-chunk
38
+ // stream (the shell IS the document — vanilla has no Suspense boundaries of its own; @nifrajs/web's
39
+ // defer()/<no-js fallback> machinery handles progressive data above this seam).
40
+ renderToStream(chain: readonly unknown[], props: RenderProps): ReadableStream<Uint8Array> {
41
+ const body = compose(chain, props).html
42
+ return new ReadableStream<Uint8Array>({
43
+ start(controller) {
44
+ controller.enqueue(ENCODER.encode(body))
45
+ controller.close()
46
+ },
47
+ })
48
+ },
49
+ // No client runtime ⇒ no per-document bootstrap markup.
50
+ hydrationHead(): string {
51
+ return ""
52
+ },
53
+ }