@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 +21 -0
- package/README.md +18 -0
- package/dist/compose.d.ts +17 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +13 -0
- package/dist/compose.js.map +1 -0
- package/dist/html.d.ts +33 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +99 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/compose.ts +23 -0
- package/src/html.ts +111 -0
- package/src/index.ts +53 -0
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"}
|
package/dist/compose.js
ADDED
|
@@ -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 = "&";
|
|
37
|
+
break;
|
|
38
|
+
case 60:
|
|
39
|
+
entity = "<";
|
|
40
|
+
break;
|
|
41
|
+
case 62:
|
|
42
|
+
entity = ">";
|
|
43
|
+
break;
|
|
44
|
+
case 34:
|
|
45
|
+
entity = """;
|
|
46
|
+
break;
|
|
47
|
+
case 39:
|
|
48
|
+
entity = "'";
|
|
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
|
package/dist/html.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8FAA8F;AAC9F,MAAM,OAAO,OAAO;IACT,KAAK,CAAQ;IACtB,YAAY,KAAa;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,OAAe;IACjC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,SAAS,GAAG,SAAS,CAAA;AAC3B,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE;IACvC,iFAAiF;IACjF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAA;IAChC,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,MAAc,CAAA;QAClB,QAAQ,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YACxB,KAAK,EAAE;gBACL,MAAM,GAAG,OAAO,CAAA;gBAChB,MAAK;YACP,KAAK,EAAE;gBACL,MAAM,GAAG,MAAM,CAAA;gBACf,MAAK;YACP,KAAK,EAAE;gBACL,MAAM,GAAG,MAAM,CAAA;gBACf,MAAK;YACP,KAAK,EAAE;gBACL,MAAM,GAAG,QAAQ,CAAA;gBACjB,MAAK;YACP,KAAK,EAAE;gBACL,MAAM,GAAG,OAAO,CAAA;gBAChB,MAAK;YACP;gBACE,SAAQ;QACZ,CAAC;QACD,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,MAAM,CAAA;QAChC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;IACd,CAAC;IACD,OAAO,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;AAC5B,CAAC,CAAA;AAED,+FAA+F;AAC/F,MAAM,OAAO,QAAQ;IACnB,+FAA+F;IACtF,IAAI,CAAQ;IACrB,YAAY,UAAkB;QAC5B,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;IACxB,CAAC;IACD,QAAQ;QACN,OAAO,IAAI,CAAC,IAAI,CAAA;IAClB,CAAC;CACF;AAeD,SAAS,WAAW,CAAC,KAAgB;IACnC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,EAAE,CAAA;IAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAC,KAAK,CAAC,CAAA;IACvD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IAChF,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAA;IACjC,IAAI,KAAK,YAAY,QAAQ;QAAE,OAAO,KAAK,CAAC,IAAI,CAAA;IAChD,IAAI,KAAK,YAAY,OAAO;QAAE,OAAO,KAAK,CAAC,KAAK,CAAA;IAChD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,GAAG,GAAG,EAAE,CAAA;QACZ,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,CAAA;QAClD,OAAO,GAAG,CAAA;IACZ,CAAC;IACD,8FAA8F;IAC9F,MAAM,IAAI,SAAS,CACjB,+DAA+D,OAAO,KAAK,8EAA8E,CAC1J,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,IAAI,CAAC,OAA6B,EAAE,GAAG,MAAmB;IACxE,IAAI,GAAG,GAAG,OAAO,CAAC,CAAC,CAAW,CAAA;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC,CAAc,CAAC,GAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAY,CAAA;IACzE,CAAC;IACD,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAA;AAC1B,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 = "&"
|
|
39
|
+
break
|
|
40
|
+
case 60:
|
|
41
|
+
entity = "<"
|
|
42
|
+
break
|
|
43
|
+
case 62:
|
|
44
|
+
entity = ">"
|
|
45
|
+
break
|
|
46
|
+
case 34:
|
|
47
|
+
entity = """
|
|
48
|
+
break
|
|
49
|
+
case 39:
|
|
50
|
+
entity = "'"
|
|
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
|
+
}
|