@master/css-server 0.0.0-site.1793659883.g69c9aebcf628
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 +118 -0
- package/dist/create-server-renderer.d.ts +23 -0
- package/dist/create-server-renderer.js +86 -0
- package/dist/decode-html.d.ts +3 -0
- package/dist/decode-html.js +12 -0
- package/dist/html-render-session.d.ts +18 -0
- package/dist/html-render-session.js +71 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/native-stylesheets.d.ts +15 -0
- package/dist/native-stylesheets.js +27 -0
- package/dist/parse-html.d.ts +14 -0
- package/dist/parse-html.js +41 -0
- package/dist/render-html.d.ts +7 -0
- package/dist/render-html.js +34 -0
- package/dist/render.d.ts +28 -0
- package/dist/render.js +117 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<br>
|
|
2
|
+
<div align="center">
|
|
3
|
+
|
|
4
|
+
<p align="center">
|
|
5
|
+
<a href="https://css.master.co">
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/33840671/201701649-3bb7d698-abec-4d5f-ac30-ccc4d7bafcd4.svg">
|
|
8
|
+
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/33840671/201703010-77bf2373-9899-40cc-98f5-30cf9b546941.svg">
|
|
9
|
+
<img alt="Master CSS" src="https://user-images.githubusercontent.com/33840671/201703010-77bf2373-9899-40cc-98f5-30cf9b546941.svg" width="100%">
|
|
10
|
+
</picture>
|
|
11
|
+
</a>
|
|
12
|
+
</p>
|
|
13
|
+
<p align="center">Server-side HTML renderer for Master CSS</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a aria-label="GitHub release (latest by date including pre-releases)" href="https://github.com/master-co/css/releases">
|
|
17
|
+
<picture>
|
|
18
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/github/v/release/master-co/css?include_prereleases&color=212022&label=&style=for-the-badge&logo=github&logoColor=fff">
|
|
19
|
+
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/github/v/release/master-co/css?include_prereleases&color=f6f7f8&label=&style=for-the-badge&logo=github&logoColor=%23000">
|
|
20
|
+
<img alt="NPM Version" src="https://img.shields.io/github/v/release/master-co/css?include_prereleases&color=f6f7f8&label=&style=for-the-badge&logo=github">
|
|
21
|
+
</picture>
|
|
22
|
+
</a>
|
|
23
|
+
<a aria-label="NPM Package" href="https://www.npmjs.com/package/@master/css-server">
|
|
24
|
+
<picture>
|
|
25
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/npm/dm/@master/css-server?color=212022&label=%20&logo=npm&style=for-the-badge">
|
|
26
|
+
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/npm/dm/@master/css-server?color=f6f7f8&label=%20&logo=npm&style=for-the-badge">
|
|
27
|
+
<img alt="NPM package ( download / month )" src="https://img.shields.io/npm/dm/@master/css-server?color=f6f7f8&label=%20&logo=npm&style=for-the-badge">
|
|
28
|
+
</picture>
|
|
29
|
+
</a>
|
|
30
|
+
<a aria-label="Discord Community" href="https://discord.gg/sZNKpAAAw6">
|
|
31
|
+
<picture>
|
|
32
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/discord/917780624314613760?color=212022&label=%20&logo=discord&style=for-the-badge">
|
|
33
|
+
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/discord/917780624314613760?color=f6f7f8&label=%20&logo=discord&style=for-the-badge">
|
|
34
|
+
<img alt="Discord online" src="https://img.shields.io/discord/917780624314613760?color=f6f7f8&label=%20&logo=discord&style=for-the-badge">
|
|
35
|
+
</picture>
|
|
36
|
+
</a>
|
|
37
|
+
<a aria-label="Follow @mastercorg" href="https://twitter.com/mastercorg">
|
|
38
|
+
<picture>
|
|
39
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/static/v1?label=%20&message=twitter&color=212022&logo=twitter&style=for-the-badge">
|
|
40
|
+
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/static/v1?label=%20&message=twitter&color=f6f7f8&logo=twitter&style=for-the-badge">
|
|
41
|
+
<img alt="Follow @mastercorg" src="https://img.shields.io/static/v1?label=%20&message=twitter&color=f6f7f8&logo=twitter&style=for-the-badge">
|
|
42
|
+
</picture>
|
|
43
|
+
</a>
|
|
44
|
+
<a aria-label="Github Actions" href="https://github.com/master-co/css/actions/workflows/ci-release.yml">
|
|
45
|
+
<picture>
|
|
46
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/github/actions/workflow/status/master-co/css/ci-release.yml?branch=rc&label=%20&message=twitter&color=212022&logo=githubactions&style=for-the-badge">
|
|
47
|
+
<source media="(prefers-color-scheme: light)" srcset="https://img.shields.io/github/actions/workflow/status/master-co/css/ci-release.yml?branch=rc&label=%20&message=twitter&color=f6f7f8&logo=githubactions&style=for-the-badge&logoColor=%23000">
|
|
48
|
+
<img alt="Github release actions" src="https://img.shields.io/github/actions/workflow/status/master-co/css/ci-release.yml?branch=rc&label=%20&message=twitter&color=f6f7f8&logo=githubactions&style=for-the-badge&logoColor=%23000">
|
|
49
|
+
</picture>
|
|
50
|
+
</a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @master/css-server
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`@master/css-server` owns HTML parsing and style/hydration injection. Every entry
|
|
62
|
+
requires an explicit `MasterCSSManifest`; class-only rendering belongs to
|
|
63
|
+
`@master/css/node`.
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### `renderHTML()`
|
|
68
|
+
|
|
69
|
+
The one-shot API owns and releases its render session. Its result is an immutable
|
|
70
|
+
snapshot and does not expose DOM nodes, bindings, or disposable CSS state.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { renderHTML } from '@master/css-server'
|
|
74
|
+
|
|
75
|
+
const result = renderHTML('<div class="text-center"></div>', {
|
|
76
|
+
manifest,
|
|
77
|
+
hydrationManifest: 'inline'
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
console.log(result.html)
|
|
81
|
+
console.log(result.cssText)
|
|
82
|
+
console.log(result.classNames)
|
|
83
|
+
console.log(result.diagnostics)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `createServerRenderer()`
|
|
87
|
+
|
|
88
|
+
Create a manifest-scoped renderer when one process renders multiple documents.
|
|
89
|
+
Generated rules are cached across calls while every call returns an independent
|
|
90
|
+
snapshot.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { createServerRenderer } from '@master/css-server'
|
|
94
|
+
|
|
95
|
+
const renderer = createServerRenderer({ manifest })
|
|
96
|
+
const first = renderer.renderHTML('<div class="text-center"></div>')
|
|
97
|
+
const second = renderer.renderHTML('<div class="fg-red"></div>')
|
|
98
|
+
|
|
99
|
+
renderer.dispose()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Long-lived renderers cache up to 8192 unique classes before starting a new cache generation. Finite build jobs can pass `{ maxCachedClasses: Infinity }` and dispose the renderer when the build ends. Recreate the renderer whenever its manifest changes.
|
|
103
|
+
|
|
104
|
+
### `createHTMLRenderSession()`
|
|
105
|
+
|
|
106
|
+
Use one session per streaming document. `write()` accepts successive chunks and
|
|
107
|
+
`end()` returns the final transformed chunk plus the complete immutable snapshot.
|
|
108
|
+
The session owns its renderer and releases it after `end()`, failure, or `dispose()`.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { createHTMLRenderSession } from '@master/css-server'
|
|
112
|
+
|
|
113
|
+
const session = createHTMLRenderSession({ manifest })
|
|
114
|
+
const firstChunk = session.write('<html><head>')
|
|
115
|
+
const final = session.end('</head><body class="block"></body></html>')
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use official framework integrations when possible; they compose this package with manifest loading, source scanning, and runtime hydration.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MasterCSSRenderSessionOptions } from "@master/css/node";
|
|
2
|
+
import { MasterCSSHTMLDocumentOptions, MasterCSSHTMLRenderResult } from "./render.js";
|
|
3
|
+
interface MasterCSSServerRendererOptions extends MasterCSSRenderSessionOptions {
|
|
4
|
+
readonly maxCachedClasses?: number;
|
|
5
|
+
}
|
|
6
|
+
declare class MasterCSSServerRenderer implements Disposable {
|
|
7
|
+
private readonly options;
|
|
8
|
+
readonly maxCachedClasses: number;
|
|
9
|
+
private readonly cachedClassNames;
|
|
10
|
+
private renderSession;
|
|
11
|
+
private disposed;
|
|
12
|
+
private constructor();
|
|
13
|
+
renderHTML(html: string, options?: MasterCSSHTMLDocumentOptions): MasterCSSHTMLRenderResult;
|
|
14
|
+
createHTMLRenderSession(options?: MasterCSSHTMLDocumentOptions): import("./html-render-session.js").MasterCSSHTMLRenderSession;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
[Symbol.dispose](): void;
|
|
17
|
+
private renderClassNames;
|
|
18
|
+
private renderOptions;
|
|
19
|
+
private createRenderSession;
|
|
20
|
+
private assertActive;
|
|
21
|
+
}
|
|
22
|
+
declare function createServerRenderer(options: MasterCSSServerRendererOptions): MasterCSSServerRenderer;
|
|
23
|
+
export { MasterCSSServerRenderer, MasterCSSServerRendererOptions, createServerRenderer };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { bindHTMLRenderSessionInternal } from "./html-render-session.js";
|
|
2
|
+
import parseHTML from "./parse-html.js";
|
|
3
|
+
import { collectNativeStylesheets } from "./native-stylesheets.js";
|
|
4
|
+
import { renderHTMLWithSnapshot } from "./render.js";
|
|
5
|
+
import { createRenderSessionSync, renderClassNamesSync } from "@master/css/node";
|
|
6
|
+
import { MasterCSSError } from "@master/css-schema";
|
|
7
|
+
// src/create-server-renderer.ts
|
|
8
|
+
const DEFAULT_MAX_CACHED_CLASSES = 8192;
|
|
9
|
+
let bindServerRenderer;
|
|
10
|
+
var MasterCSSServerRenderer = class MasterCSSServerRenderer {
|
|
11
|
+
options;
|
|
12
|
+
maxCachedClasses;
|
|
13
|
+
cachedClassNames = /* @__PURE__ */ new Set();
|
|
14
|
+
renderSession;
|
|
15
|
+
disposed = false;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.maxCachedClasses = options.maxCachedClasses ?? DEFAULT_MAX_CACHED_CLASSES;
|
|
19
|
+
if (this.maxCachedClasses !== Infinity && (!Number.isInteger(this.maxCachedClasses) || this.maxCachedClasses <= 0)) throw new TypeError("maxCachedClasses must be a positive integer or Infinity.");
|
|
20
|
+
this.renderSession = this.createRenderSession();
|
|
21
|
+
}
|
|
22
|
+
renderHTML(html, options = {}) {
|
|
23
|
+
this.assertActive();
|
|
24
|
+
const document = parseHTML(html);
|
|
25
|
+
const native = collectNativeStylesheets(document, options.stylesheets, options.documentURL);
|
|
26
|
+
if (native.length) {
|
|
27
|
+
using session = this.createRenderSession();
|
|
28
|
+
for (const css of native) session.ensureStylesheetResources(css);
|
|
29
|
+
session.ensureClassRules(document.classes);
|
|
30
|
+
return renderHTMLWithSnapshot(html, document, session.snapshot(), options);
|
|
31
|
+
}
|
|
32
|
+
const snapshot = this.renderClassNames(document.classes);
|
|
33
|
+
return renderHTMLWithSnapshot(html, document, snapshot, options);
|
|
34
|
+
}
|
|
35
|
+
createHTMLRenderSession(options = {}) {
|
|
36
|
+
this.assertActive();
|
|
37
|
+
return bindHTMLRenderSessionInternal(this, options);
|
|
38
|
+
}
|
|
39
|
+
dispose() {
|
|
40
|
+
if (this.disposed) return;
|
|
41
|
+
this.renderSession.dispose();
|
|
42
|
+
this.cachedClassNames.clear();
|
|
43
|
+
this.disposed = true;
|
|
44
|
+
}
|
|
45
|
+
[Symbol.dispose]() {
|
|
46
|
+
this.dispose();
|
|
47
|
+
}
|
|
48
|
+
renderClassNames(classNames) {
|
|
49
|
+
const classes = [...new Set(classNames.filter(Boolean))];
|
|
50
|
+
if (classes.length > this.maxCachedClasses) return renderClassNamesSync(classes, this.renderOptions());
|
|
51
|
+
const newClassNames = classes.filter((className) => !this.cachedClassNames.has(className));
|
|
52
|
+
let classesToEnsure = newClassNames;
|
|
53
|
+
if (this.cachedClassNames.size + newClassNames.length > this.maxCachedClasses) {
|
|
54
|
+
this.renderSession.dispose();
|
|
55
|
+
this.renderSession = this.createRenderSession();
|
|
56
|
+
this.cachedClassNames.clear();
|
|
57
|
+
classesToEnsure = classes;
|
|
58
|
+
}
|
|
59
|
+
if (classesToEnsure.length) this.renderSession.ensureClassRules(classesToEnsure);
|
|
60
|
+
for (const className of classes) this.cachedClassNames.add(className);
|
|
61
|
+
return this.renderSession.snapshotForClassNames(classes);
|
|
62
|
+
}
|
|
63
|
+
renderOptions() {
|
|
64
|
+
return {
|
|
65
|
+
manifest: this.options.manifest,
|
|
66
|
+
emittedGlobals: this.options.emittedGlobals
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
createRenderSession() {
|
|
70
|
+
return createRenderSessionSync(this.renderOptions());
|
|
71
|
+
}
|
|
72
|
+
assertActive() {
|
|
73
|
+
if (this.disposed) throw new MasterCSSError({
|
|
74
|
+
code: "SESSION_DISPOSED",
|
|
75
|
+
domain: "server",
|
|
76
|
+
message: "The Master CSS server renderer has been disposed."
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
static {
|
|
80
|
+
bindServerRenderer = (options) => new MasterCSSServerRenderer(options);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
function createServerRenderer(options) {
|
|
84
|
+
return bindServerRenderer(options);
|
|
85
|
+
}
|
|
86
|
+
export { MasterCSSServerRenderer, createServerRenderer };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Parser } from "htmlparser2";
|
|
2
|
+
// src/decode-html.ts
|
|
3
|
+
/** Decode as an HTML attribute while preserving the document's original markup. */
|
|
4
|
+
function decodeHTML(html) {
|
|
5
|
+
if (!html.includes("&")) return html;
|
|
6
|
+
let decoded = html;
|
|
7
|
+
new Parser({ onattribute(name, value) {
|
|
8
|
+
if (name === "class") decoded = value;
|
|
9
|
+
} }, { decodeEntities: true }).end(`<span class="${html.replaceAll("\"", """)}">`);
|
|
10
|
+
return decoded;
|
|
11
|
+
}
|
|
12
|
+
export { decodeHTML as default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MasterCSSHTMLDocumentOptions, MasterCSSHTMLRenderResult } from "./render.js";
|
|
2
|
+
import { MasterCSSServerRenderer } from "./create-server-renderer.js";
|
|
3
|
+
interface MasterCSSHTMLStreamEndResult {
|
|
4
|
+
readonly chunk: string;
|
|
5
|
+
readonly result: MasterCSSHTMLRenderResult;
|
|
6
|
+
}
|
|
7
|
+
declare class MasterCSSHTMLRenderSession implements Disposable {
|
|
8
|
+
#private;
|
|
9
|
+
private constructor();
|
|
10
|
+
write(chunk: string): string;
|
|
11
|
+
end(finalChunk?: string): MasterCSSHTMLStreamEndResult;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
[Symbol.dispose](): void;
|
|
14
|
+
private assertWritable;
|
|
15
|
+
}
|
|
16
|
+
/** @internal */
|
|
17
|
+
declare function bindHTMLRenderSessionInternal(renderer: MasterCSSServerRenderer, options: MasterCSSHTMLDocumentOptions, ownsRenderer?: boolean): MasterCSSHTMLRenderSession;
|
|
18
|
+
export { MasterCSSHTMLRenderSession, MasterCSSHTMLStreamEndResult, bindHTMLRenderSessionInternal };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { MasterCSSError } from "@master/css-schema";
|
|
2
|
+
// src/html-render-session.ts
|
|
3
|
+
let bindHTMLRenderSession;
|
|
4
|
+
var MasterCSSHTMLRenderSession = class MasterCSSHTMLRenderSession {
|
|
5
|
+
#sourceHTML = "";
|
|
6
|
+
#emittedPrefix = "";
|
|
7
|
+
#ended = false;
|
|
8
|
+
#renderer;
|
|
9
|
+
#options;
|
|
10
|
+
#ownsRenderer = false;
|
|
11
|
+
constructor() {}
|
|
12
|
+
write(chunk) {
|
|
13
|
+
this.assertWritable();
|
|
14
|
+
this.#sourceHTML += chunk;
|
|
15
|
+
if (this.#emittedPrefix) return "";
|
|
16
|
+
const prefix = this.#sourceHTML.match(/^(?:(?:<!doctype html>|<!DOCTYPE html>)\s*)?<html>\s*<head>/)?.[0];
|
|
17
|
+
if (!prefix) return "";
|
|
18
|
+
this.#emittedPrefix = prefix;
|
|
19
|
+
return prefix;
|
|
20
|
+
}
|
|
21
|
+
end(finalChunk = "") {
|
|
22
|
+
this.assertWritable();
|
|
23
|
+
const finalPrefix = finalChunk ? this.write(finalChunk) : "";
|
|
24
|
+
try {
|
|
25
|
+
const result = this.#renderer.renderHTML(this.#sourceHTML, this.#options);
|
|
26
|
+
if (this.#emittedPrefix && !result.html.startsWith(this.#emittedPrefix)) throw new MasterCSSError({
|
|
27
|
+
code: "STREAM_PREFIX_MISMATCH",
|
|
28
|
+
domain: "server",
|
|
29
|
+
message: "The rendered document no longer matches the emitted HTML stream prefix."
|
|
30
|
+
});
|
|
31
|
+
const chunk = finalPrefix + result.html.slice(this.#emittedPrefix.length);
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
chunk,
|
|
34
|
+
result
|
|
35
|
+
});
|
|
36
|
+
} finally {
|
|
37
|
+
this.dispose();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
dispose() {
|
|
41
|
+
if (this.#ended) return;
|
|
42
|
+
this.#sourceHTML = "";
|
|
43
|
+
this.#emittedPrefix = "";
|
|
44
|
+
this.#ended = true;
|
|
45
|
+
if (this.#ownsRenderer) this.#renderer.dispose();
|
|
46
|
+
}
|
|
47
|
+
[Symbol.dispose]() {
|
|
48
|
+
this.dispose();
|
|
49
|
+
}
|
|
50
|
+
assertWritable() {
|
|
51
|
+
if (this.#ended) throw new MasterCSSError({
|
|
52
|
+
code: "SESSION_DISPOSED",
|
|
53
|
+
domain: "server",
|
|
54
|
+
message: "The Master CSS HTML render session has ended."
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
static {
|
|
58
|
+
bindHTMLRenderSession = (renderer, options, ownsRenderer) => {
|
|
59
|
+
const session = new MasterCSSHTMLRenderSession();
|
|
60
|
+
session.#renderer = renderer;
|
|
61
|
+
session.#options = options;
|
|
62
|
+
session.#ownsRenderer = ownsRenderer;
|
|
63
|
+
return session;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/** @internal */
|
|
68
|
+
function bindHTMLRenderSessionInternal(renderer, options, ownsRenderer = false) {
|
|
69
|
+
return bindHTMLRenderSession(renderer, options, ownsRenderer);
|
|
70
|
+
}
|
|
71
|
+
export { MasterCSSHTMLRenderSession, bindHTMLRenderSessionInternal };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { MasterCSSExternalHydrationManifestOptions, MasterCSSExternalHydrationManifestSource, MasterCSSHTMLDocumentOptions, MasterCSSHTMLRenderResult, MasterCSSHydrationManifestRenderMode } from "./render.js";
|
|
2
|
+
import { MasterCSSServerRenderer, MasterCSSServerRendererOptions, createServerRenderer } from "./create-server-renderer.js";
|
|
3
|
+
import { MasterCSSHTMLRenderSession, MasterCSSHTMLStreamEndResult } from "./html-render-session.js";
|
|
4
|
+
import { MasterCSSHTMLRenderOptions, createHTMLRenderSession, renderHTML } from "./render-html.js";
|
|
5
|
+
import { MasterCSSServerStylesheet } from "./native-stylesheets.js";
|
|
6
|
+
export { type MasterCSSExternalHydrationManifestOptions, type MasterCSSExternalHydrationManifestSource, type MasterCSSHTMLDocumentOptions, type MasterCSSHTMLRenderOptions, type MasterCSSHTMLRenderResult, MasterCSSHTMLRenderSession, type MasterCSSHTMLStreamEndResult, type MasterCSSHydrationManifestRenderMode, MasterCSSServerRenderer, type MasterCSSServerRendererOptions, type MasterCSSServerStylesheet, createHTMLRenderSession, createServerRenderer, renderHTML };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MasterCSSHTMLRenderSession } from "./html-render-session.js";
|
|
2
|
+
import { MasterCSSServerRenderer, createServerRenderer } from "./create-server-renderer.js";
|
|
3
|
+
import { createHTMLRenderSession, renderHTML } from "./render-html.js";
|
|
4
|
+
export { MasterCSSHTMLRenderSession, MasterCSSServerRenderer, createHTMLRenderSession, createServerRenderer, renderHTML };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import parseHTML from "./parse-html.js";
|
|
2
|
+
/** A final stylesheet a rendered page may link. */
|
|
3
|
+
interface MasterCSSServerStylesheet {
|
|
4
|
+
readonly href: string;
|
|
5
|
+
readonly css: string;
|
|
6
|
+
/** Hrefs of the stylesheets it imports, in order, relative to `href`. */
|
|
7
|
+
readonly imports?: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The CSS a page delivers natively, in document order: its inline styles, then each
|
|
11
|
+
* linked stylesheet followed by the ones it imports. Theme variables these declare
|
|
12
|
+
* are not generated again.
|
|
13
|
+
*/
|
|
14
|
+
declare function collectNativeStylesheets(document: ReturnType<typeof parseHTML>, stylesheets?: readonly MasterCSSServerStylesheet[], documentURL?: string): string[];
|
|
15
|
+
export { MasterCSSServerStylesheet, collectNativeStylesheets };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/native-stylesheets.ts
|
|
2
|
+
/**
|
|
3
|
+
* The CSS a page delivers natively, in document order: its inline styles, then each
|
|
4
|
+
* linked stylesheet followed by the ones it imports. Theme variables these declare
|
|
5
|
+
* are not generated again.
|
|
6
|
+
*/
|
|
7
|
+
function collectNativeStylesheets(document, stylesheets = [], documentURL = "https://master-css-render.invalid/") {
|
|
8
|
+
const byHref = new Map(stylesheets.map((stylesheet) => [new URL(stylesheet.href, documentURL).href, stylesheet]));
|
|
9
|
+
const sources = document.nativeStyleElements.map((style) => style.childNodes.map((node) => "data" in node ? node.data : "").join(""));
|
|
10
|
+
const visited = /* @__PURE__ */ new Set();
|
|
11
|
+
const collect = (href) => {
|
|
12
|
+
const stylesheet = byHref.get(href);
|
|
13
|
+
if (!stylesheet || visited.has(href)) return;
|
|
14
|
+
visited.add(href);
|
|
15
|
+
sources.push(stylesheet.css);
|
|
16
|
+
for (const imported of stylesheet.imports ?? []) collect(new URL(imported, href).href);
|
|
17
|
+
};
|
|
18
|
+
const visit = (nodes) => {
|
|
19
|
+
for (const node of nodes) {
|
|
20
|
+
if ("attribs" in node && node.name === "link" && node.attribs.rel === "stylesheet" && node.attribs.href && node.attribs.disabled === void 0) collect(new URL(node.attribs.href, documentURL).href);
|
|
21
|
+
if ("childNodes" in node) visit(node.childNodes);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
visit(document.nodes);
|
|
25
|
+
return sources;
|
|
26
|
+
}
|
|
27
|
+
export { collectNativeStylesheets };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ChildNode, Element } from "domhandler";
|
|
2
|
+
/**
|
|
3
|
+
* @param html
|
|
4
|
+
* @returns {classes from html, root element}
|
|
5
|
+
*/
|
|
6
|
+
declare function parseHTML(html: string): {
|
|
7
|
+
classes: string[];
|
|
8
|
+
nodes: ChildNode[];
|
|
9
|
+
htmlElement: Element | null;
|
|
10
|
+
headElement: Element | null;
|
|
11
|
+
styleElement: Element | null;
|
|
12
|
+
nativeStyleElements: Element[];
|
|
13
|
+
};
|
|
14
|
+
export { parseHTML as default };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import decodeHTML from "./decode-html.js";
|
|
2
|
+
import { DomHandler, Parser } from "htmlparser2";
|
|
3
|
+
import { MASTER_CSS_RUNTIME_STYLE_ID } from "@master/css-schema/runtime-style";
|
|
4
|
+
// src/parse-html.ts
|
|
5
|
+
/**
|
|
6
|
+
* @param html
|
|
7
|
+
* @returns {classes from html, root element}
|
|
8
|
+
*/
|
|
9
|
+
function parseHTML(html) {
|
|
10
|
+
const classes = /* @__PURE__ */ new Set();
|
|
11
|
+
let htmlElement = null;
|
|
12
|
+
let headElement = null;
|
|
13
|
+
let styleElement = null;
|
|
14
|
+
const nativeStyleElements = [];
|
|
15
|
+
const handler = new DomHandler(void 0, {}, (element) => {
|
|
16
|
+
if (element.type === "tag") switch (element.name) {
|
|
17
|
+
case "html":
|
|
18
|
+
htmlElement = element;
|
|
19
|
+
break;
|
|
20
|
+
case "head": headElement = element;
|
|
21
|
+
}
|
|
22
|
+
else if (element.type === "style") {
|
|
23
|
+
if (element.attribs.id === MASTER_CSS_RUNTIME_STYLE_ID) styleElement = element;
|
|
24
|
+
else if (!element.attribs.type || element.attribs.type === "text/css") nativeStyleElements.push(element);
|
|
25
|
+
}
|
|
26
|
+
if (element.attribs.class) decodeHTML(element.attribs.class).split(/[\t\n\f\r ]+/).forEach((className) => {
|
|
27
|
+
if (!className) return;
|
|
28
|
+
classes.add(className);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
new Parser(handler, { decodeEntities: false }).end(html);
|
|
32
|
+
return {
|
|
33
|
+
classes: [...classes],
|
|
34
|
+
nodes: handler.root.childNodes,
|
|
35
|
+
htmlElement,
|
|
36
|
+
headElement,
|
|
37
|
+
styleElement,
|
|
38
|
+
nativeStyleElements
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export { parseHTML as default };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MasterCSSHTMLDocumentOptions, MasterCSSHTMLRenderResult } from "./render.js";
|
|
2
|
+
import { MasterCSSServerRendererOptions } from "./create-server-renderer.js";
|
|
3
|
+
import { MasterCSSHTMLRenderSession } from "./html-render-session.js";
|
|
4
|
+
interface MasterCSSHTMLRenderOptions extends MasterCSSServerRendererOptions, MasterCSSHTMLDocumentOptions {}
|
|
5
|
+
declare function renderHTML(html: string, options: MasterCSSHTMLRenderOptions): MasterCSSHTMLRenderResult;
|
|
6
|
+
declare function createHTMLRenderSession(options: MasterCSSHTMLRenderOptions): MasterCSSHTMLRenderSession;
|
|
7
|
+
export { MasterCSSHTMLRenderOptions, createHTMLRenderSession, renderHTML };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { bindHTMLRenderSessionInternal } from "./html-render-session.js";
|
|
2
|
+
import { createServerRenderer } from "./create-server-renderer.js";
|
|
3
|
+
// src/render-html.ts
|
|
4
|
+
function renderHTML(html, options) {
|
|
5
|
+
const { hydrationManifest, stylesheets, documentURL, manifest, emittedGlobals, maxCachedClasses } = options;
|
|
6
|
+
const renderer = createServerRenderer({
|
|
7
|
+
manifest,
|
|
8
|
+
emittedGlobals,
|
|
9
|
+
maxCachedClasses
|
|
10
|
+
});
|
|
11
|
+
try {
|
|
12
|
+
return renderer.renderHTML(html, {
|
|
13
|
+
hydrationManifest,
|
|
14
|
+
stylesheets,
|
|
15
|
+
documentURL
|
|
16
|
+
});
|
|
17
|
+
} finally {
|
|
18
|
+
renderer.dispose();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function createHTMLRenderSession(options) {
|
|
22
|
+
const { hydrationManifest, stylesheets, documentURL, manifest, emittedGlobals, maxCachedClasses } = options;
|
|
23
|
+
const renderer = createServerRenderer({
|
|
24
|
+
manifest,
|
|
25
|
+
emittedGlobals,
|
|
26
|
+
maxCachedClasses
|
|
27
|
+
});
|
|
28
|
+
return bindHTMLRenderSessionInternal(renderer, {
|
|
29
|
+
hydrationManifest,
|
|
30
|
+
stylesheets,
|
|
31
|
+
documentURL
|
|
32
|
+
}, true);
|
|
33
|
+
}
|
|
34
|
+
export { createHTMLRenderSession, renderHTML };
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MasterCSSRenderSnapshot } from "@master/css/node";
|
|
2
|
+
import { MasterCSSDiagnostic } from "@master/css-schema";
|
|
3
|
+
import { MasterCSSHydrationManifest } from "@master/css-schema/hydration-manifest";
|
|
4
|
+
import { MasterCSSServerStylesheet } from "./native-stylesheets.js";
|
|
5
|
+
import parseHTML from "./parse-html.js";
|
|
6
|
+
type MasterCSSExternalHydrationManifestSource = string | ((json: string, hydrationManifest: MasterCSSHydrationManifest) => string);
|
|
7
|
+
interface MasterCSSExternalHydrationManifestOptions {
|
|
8
|
+
readonly type: 'external';
|
|
9
|
+
readonly source: MasterCSSExternalHydrationManifestSource;
|
|
10
|
+
}
|
|
11
|
+
type MasterCSSHydrationManifestRenderMode = 'return' | 'inject' | false | MasterCSSExternalHydrationManifestOptions;
|
|
12
|
+
interface MasterCSSHTMLDocumentOptions {
|
|
13
|
+
readonly hydrationManifest?: MasterCSSHydrationManifestRenderMode;
|
|
14
|
+
/** Final stylesheets the page may link, whose theme variables it does not repeat. */
|
|
15
|
+
readonly stylesheets?: readonly MasterCSSServerStylesheet[];
|
|
16
|
+
/** Resolves the page's stylesheet links. */
|
|
17
|
+
readonly documentURL?: string;
|
|
18
|
+
}
|
|
19
|
+
interface MasterCSSHTMLRenderResult {
|
|
20
|
+
readonly html: string;
|
|
21
|
+
readonly cssText: string;
|
|
22
|
+
readonly classNames: readonly string[];
|
|
23
|
+
readonly invalidClassNames: readonly string[];
|
|
24
|
+
readonly hydrationManifest?: MasterCSSHydrationManifest;
|
|
25
|
+
readonly diagnostics: readonly MasterCSSDiagnostic[];
|
|
26
|
+
}
|
|
27
|
+
declare function renderHTMLWithSnapshot(html: string, context: ReturnType<typeof parseHTML>, snapshot: MasterCSSRenderSnapshot | undefined, options: MasterCSSHTMLDocumentOptions): MasterCSSHTMLRenderResult;
|
|
28
|
+
export { MasterCSSExternalHydrationManifestOptions, MasterCSSExternalHydrationManifestSource, MasterCSSHTMLDocumentOptions, MasterCSSHTMLRenderResult, MasterCSSHydrationManifestRenderMode, renderHTMLWithSnapshot };
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { MASTER_CSS_RUNTIME_STYLE_ID } from "@master/css-schema/runtime-style";
|
|
2
|
+
import { MASTER_CSS_HYDRATION_MANIFEST_ATTR, MASTER_CSS_HYDRATION_MANIFEST_SCRIPT_ID, serializeMasterCSSHydrationManifest } from "@master/css-schema/hydration-manifest";
|
|
3
|
+
import { Element, Text } from "domhandler";
|
|
4
|
+
import serialize from "dom-serializer";
|
|
5
|
+
// src/render.ts
|
|
6
|
+
function hydrationScript(hydrationManifest) {
|
|
7
|
+
return new Element("script", {
|
|
8
|
+
type: "application/json",
|
|
9
|
+
id: MASTER_CSS_HYDRATION_MANIFEST_SCRIPT_ID
|
|
10
|
+
}, [new Text(serializeMasterCSSHydrationManifest(hydrationManifest))]);
|
|
11
|
+
}
|
|
12
|
+
function isHydrationScript(node) {
|
|
13
|
+
return node.type === "script" && node.name === "script" && node.attribs.id === MASTER_CSS_HYDRATION_MANIFEST_SCRIPT_ID;
|
|
14
|
+
}
|
|
15
|
+
function findHydrationScripts(childNodes, matches = []) {
|
|
16
|
+
for (let index = 0; index < childNodes.length; index++) {
|
|
17
|
+
const node = childNodes[index];
|
|
18
|
+
if (isHydrationScript(node)) matches.push({
|
|
19
|
+
element: node,
|
|
20
|
+
childNodes,
|
|
21
|
+
index
|
|
22
|
+
});
|
|
23
|
+
if ("childNodes" in node) findHydrationScripts(node.childNodes, matches);
|
|
24
|
+
}
|
|
25
|
+
return matches;
|
|
26
|
+
}
|
|
27
|
+
function removeHydrationScripts(nodes) {
|
|
28
|
+
const scripts = findHydrationScripts(nodes);
|
|
29
|
+
for (let index = scripts.length - 1; index >= 0; index--) {
|
|
30
|
+
const script = scripts[index];
|
|
31
|
+
script.childNodes.splice(script.index, 1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function removeNode(nodes, target) {
|
|
35
|
+
const index = nodes.indexOf(target);
|
|
36
|
+
if (index !== -1) {
|
|
37
|
+
nodes.splice(index, 1);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
for (const node of nodes) if ("childNodes" in node && removeNode(node.childNodes, target)) return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function injectHydrationScript(nodes, htmlElement, headElement, styleElement, hydrationManifest) {
|
|
44
|
+
if (styleElement) delete styleElement.attribs[MASTER_CSS_HYDRATION_MANIFEST_ATTR];
|
|
45
|
+
const scripts = findHydrationScripts(nodes);
|
|
46
|
+
if (scripts.length) {
|
|
47
|
+
const [first, ...duplicates] = scripts;
|
|
48
|
+
first.element.attribs.type = "application/json";
|
|
49
|
+
first.element.childNodes = [new Text(serializeMasterCSSHydrationManifest(hydrationManifest))];
|
|
50
|
+
for (let index = duplicates.length - 1; index >= 0; index--) {
|
|
51
|
+
const duplicate = duplicates[index];
|
|
52
|
+
duplicate.childNodes.splice(duplicate.index, 1);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const script = hydrationScript(hydrationManifest);
|
|
57
|
+
if (headElement) headElement.childNodes.push(script);
|
|
58
|
+
else if (htmlElement) htmlElement.childNodes.unshift(new Element("head", {}, [script]));
|
|
59
|
+
else nodes.unshift(script);
|
|
60
|
+
}
|
|
61
|
+
function externalSource(option, hydrationManifest) {
|
|
62
|
+
const json = serializeMasterCSSHydrationManifest(hydrationManifest);
|
|
63
|
+
return typeof option.source === "function" ? option.source(json, hydrationManifest) : option.source;
|
|
64
|
+
}
|
|
65
|
+
function result(html, classNames, snapshot, hydrationManifest) {
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
html,
|
|
68
|
+
cssText: snapshot?.cssText ?? "",
|
|
69
|
+
classNames: Object.freeze([...classNames]),
|
|
70
|
+
invalidClassNames: Object.freeze([...snapshot?.invalidClassNames ?? []]),
|
|
71
|
+
...hydrationManifest ? { hydrationManifest } : {},
|
|
72
|
+
diagnostics: Object.freeze([])
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function styleText(css) {
|
|
76
|
+
return css.replace(/(\\*)<\/style/gi, (match, escapes) => {
|
|
77
|
+
return escapes.slice(0, escapes.length - escapes.length % 2) + "\\3c " + match.slice(escapes.length + 1);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function renderHTMLWithSnapshot(html, context, snapshot, options) {
|
|
81
|
+
const { nodes, htmlElement, classes: classNames } = context;
|
|
82
|
+
let { headElement, styleElement } = context;
|
|
83
|
+
const hydrationOption = options.hydrationManifest ?? "return";
|
|
84
|
+
const hydrationManifest = hydrationOption === false ? void 0 : snapshot?.hydrationManifest;
|
|
85
|
+
const external = typeof hydrationOption === "object" ? hydrationOption : void 0;
|
|
86
|
+
if (!snapshot?.cssText) {
|
|
87
|
+
if ((hydrationOption === "inject" || external) && styleElement) {
|
|
88
|
+
removeNode(nodes, styleElement);
|
|
89
|
+
styleElement = null;
|
|
90
|
+
}
|
|
91
|
+
if (external) removeHydrationScripts(nodes);
|
|
92
|
+
return result(external || styleElement !== context.styleElement ? serialize(nodes, {
|
|
93
|
+
decodeEntities: false,
|
|
94
|
+
encodeEntities: false
|
|
95
|
+
}) : html, classNames, snapshot, hydrationManifest);
|
|
96
|
+
}
|
|
97
|
+
const safeStyleText = styleText(snapshot.cssText);
|
|
98
|
+
if (styleElement) styleElement.childNodes = [new Text(safeStyleText)];
|
|
99
|
+
else {
|
|
100
|
+
styleElement = new Element("style", { id: MASTER_CSS_RUNTIME_STYLE_ID }, [new Text(safeStyleText)]);
|
|
101
|
+
if (headElement) headElement.childNodes.push(styleElement);
|
|
102
|
+
else if (htmlElement) {
|
|
103
|
+
headElement = new Element("head", {}, [styleElement]);
|
|
104
|
+
htmlElement.childNodes.unshift(headElement);
|
|
105
|
+
} else nodes.unshift(styleElement);
|
|
106
|
+
}
|
|
107
|
+
if (hydrationOption === "inject" && hydrationManifest && snapshot.rules.length) injectHydrationScript(nodes, htmlElement, headElement, styleElement, hydrationManifest);
|
|
108
|
+
else if (external && hydrationManifest && snapshot.rules.length) {
|
|
109
|
+
removeHydrationScripts(nodes);
|
|
110
|
+
styleElement.attribs[MASTER_CSS_HYDRATION_MANIFEST_ATTR] = externalSource(external, hydrationManifest);
|
|
111
|
+
} else delete styleElement.attribs[MASTER_CSS_HYDRATION_MANIFEST_ATTR];
|
|
112
|
+
return result(serialize(nodes, {
|
|
113
|
+
decodeEntities: false,
|
|
114
|
+
encodeEntities: false
|
|
115
|
+
}), classNames, snapshot, hydrationManifest);
|
|
116
|
+
}
|
|
117
|
+
export { renderHTMLWithSnapshot };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@master/css-server",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": "^24"
|
|
6
|
+
},
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"description": "Server-side HTML renderer for Master CSS",
|
|
9
|
+
"author": "Aoyue Design LLC.",
|
|
10
|
+
"homepage": "https://css.master.co",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/master-co/css/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/master-co/css.git",
|
|
17
|
+
"directory": "packages/server"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"server",
|
|
21
|
+
"static",
|
|
22
|
+
"html",
|
|
23
|
+
"markup",
|
|
24
|
+
"dom",
|
|
25
|
+
"css",
|
|
26
|
+
"mastercss"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"default": "./dist/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@master/css": "0.0.0-site.1793659883.g69c9aebcf628",
|
|
46
|
+
"@master/css-tooling": "0.0.0-site.1793659883.g69c9aebcf628",
|
|
47
|
+
"dom-serializer": "^3.1.1",
|
|
48
|
+
"domhandler": "^6.0.1",
|
|
49
|
+
"htmlparser2": "^12.0.0",
|
|
50
|
+
"@master/css-schema": "0.0.0-site.1793659883.g69c9aebcf628"
|
|
51
|
+
},
|
|
52
|
+
"version": "0.0.0-site.1793659883.g69c9aebcf628",
|
|
53
|
+
"gitHead": "69c9aebcf628eb529001b9fa1b3536306f1b139e"
|
|
54
|
+
}
|