@llui/compiler-ssr 0.3.0

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 Franco Ponticelli
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.
@@ -0,0 +1,37 @@
1
+ export interface UseClientTransformResult {
2
+ output: string;
3
+ warnings: string[];
4
+ }
5
+ /**
6
+ * If `source` begins with a `'use client'` directive, generate a stub
7
+ * replacement for the SSR build. Every `export const X = <expr>` becomes
8
+ * `export const X = __clientOnlyStub('X')`, every `export function X`
9
+ * becomes a stub, and `export default <expr>` becomes a default stub.
10
+ * Returns `null` if the directive is absent (caller should fall through
11
+ * to the normal compiler pass).
12
+ *
13
+ * The client build is expected to skip this path entirely — Vite passes
14
+ * `{ ssr: false }` there, and the plugin checks that before invoking
15
+ * this function.
16
+ *
17
+ * Shapes this v1 does NOT handle (emits a warning + leaves them out of
18
+ * the stub output):
19
+ *
20
+ * - `export function foo() {}` and `export class Foo {}` — rewritten
21
+ * as stubs but the caller may be surprised that `foo` and `Foo` are
22
+ * ComponentDef-shaped objects during SSR.
23
+ * - `export { a, b } from './other.js'` — re-export forms are not
24
+ * detected; they pass through and will still pull `./other` into
25
+ * the SSR graph.
26
+ * - `export * from './other.js'` — same as above.
27
+ * - `export type ...` — type exports are erased by TS so nothing to
28
+ * stub; left untouched.
29
+ */
30
+ export declare function transformUseClientSsr(source: string, _filename: string): UseClientTransformResult | null;
31
+ /**
32
+ * Check whether `source`'s first statement is a `'use client'` directive.
33
+ * Cheap string scan so the caller can decide which transform to run
34
+ * without parsing the whole file twice.
35
+ */
36
+ export declare function hasUseClientDirective(source: string): boolean;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,wBAAwB,GAAG,IAAI,CAoGjC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CA4B7D"}
package/dist/index.js ADDED
@@ -0,0 +1,160 @@
1
+ // @llui/compiler-ssr — SSR support (opt-in).
2
+ //
3
+ // Handles the `'use client'` directive: scans for it cheaply with
4
+ // `hasUseClientDirective`, then rewrites client-only modules into
5
+ // stubs that the SSR build can ship via `transformUseClientSsr`.
6
+ //
7
+ // `@llui/vike` calls these directly from its Vite plugin's transform
8
+ // hook when `ssr: true`. The plugin gates on `hasUseClientDirective`
9
+ // to avoid the parse cost when the directive isn't present.
10
+ //
11
+ // Owned by this package since v2c/decomp-25 (moved verbatim from
12
+ // @llui/compiler's transform.ts).
13
+ import ts from 'typescript';
14
+ /**
15
+ * If `source` begins with a `'use client'` directive, generate a stub
16
+ * replacement for the SSR build. Every `export const X = <expr>` becomes
17
+ * `export const X = __clientOnlyStub('X')`, every `export function X`
18
+ * becomes a stub, and `export default <expr>` becomes a default stub.
19
+ * Returns `null` if the directive is absent (caller should fall through
20
+ * to the normal compiler pass).
21
+ *
22
+ * The client build is expected to skip this path entirely — Vite passes
23
+ * `{ ssr: false }` there, and the plugin checks that before invoking
24
+ * this function.
25
+ *
26
+ * Shapes this v1 does NOT handle (emits a warning + leaves them out of
27
+ * the stub output):
28
+ *
29
+ * - `export function foo() {}` and `export class Foo {}` — rewritten
30
+ * as stubs but the caller may be surprised that `foo` and `Foo` are
31
+ * ComponentDef-shaped objects during SSR.
32
+ * - `export { a, b } from './other.js'` — re-export forms are not
33
+ * detected; they pass through and will still pull `./other` into
34
+ * the SSR graph.
35
+ * - `export * from './other.js'` — same as above.
36
+ * - `export type ...` — type exports are erased by TS so nothing to
37
+ * stub; left untouched.
38
+ */
39
+ export function transformUseClientSsr(source, _filename) {
40
+ const sourceFile = ts.createSourceFile('input.ts', source, ts.ScriptTarget.Latest, true);
41
+ // Find the first non-comment, non-directive-whitespace statement.
42
+ // 'use client' should be the literal first statement in the file.
43
+ const first = sourceFile.statements[0];
44
+ if (!first)
45
+ return null;
46
+ if (!ts.isExpressionStatement(first))
47
+ return null;
48
+ if (!ts.isStringLiteral(first.expression))
49
+ return null;
50
+ if (first.expression.text !== 'use client')
51
+ return null;
52
+ const warnings = [];
53
+ const namedExports = [];
54
+ let hasDefaultExport = false;
55
+ for (const stmt of sourceFile.statements) {
56
+ // The `'use client'` directive itself — skip.
57
+ if (stmt === first)
58
+ continue;
59
+ // `export const NAME = ...` and `export let NAME = ...`
60
+ if (ts.isVariableStatement(stmt) &&
61
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
62
+ for (const decl of stmt.declarationList.declarations) {
63
+ if (ts.isIdentifier(decl.name)) {
64
+ namedExports.push(decl.name.text);
65
+ }
66
+ else {
67
+ warnings.push('[llui/use-client] destructured `export const { ... }` is not supported; each binding would have to be stubbed individually. Refactor to one `export const` per value.');
68
+ }
69
+ }
70
+ continue;
71
+ }
72
+ // `export function NAME() {}`
73
+ if (ts.isFunctionDeclaration(stmt) &&
74
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&
75
+ stmt.name) {
76
+ namedExports.push(stmt.name.text);
77
+ continue;
78
+ }
79
+ // `export class NAME {}`
80
+ if (ts.isClassDeclaration(stmt) &&
81
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&
82
+ stmt.name) {
83
+ namedExports.push(stmt.name.text);
84
+ continue;
85
+ }
86
+ // `export default ...`
87
+ if (ts.isExportAssignment(stmt) ||
88
+ (ts.isFunctionDeclaration(stmt) &&
89
+ stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword))) {
90
+ hasDefaultExport = true;
91
+ continue;
92
+ }
93
+ // `export { a, b }` / `export { a } from './x.js'` / `export * from './x.js'`
94
+ if (ts.isExportDeclaration(stmt)) {
95
+ if (stmt.moduleSpecifier) {
96
+ warnings.push("[llui/use-client] `export ... from '...'` re-export forms still pull the source module into the SSR graph and bypass stubbing. Either drop the re-export or move the 'use client' directive to the source module.");
97
+ }
98
+ else if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
99
+ for (const spec of stmt.exportClause.elements) {
100
+ namedExports.push((spec.name ?? spec.propertyName).text);
101
+ }
102
+ }
103
+ continue;
104
+ }
105
+ // Type-only statements are erased at runtime — nothing to stub.
106
+ if (ts.isTypeAliasDeclaration(stmt) || ts.isInterfaceDeclaration(stmt))
107
+ continue;
108
+ // Imports, `import type`, enum declarations, plain (non-export)
109
+ // variable statements — dropped from the stub output.
110
+ }
111
+ // Build the generated module source.
112
+ const lines = ["import { __clientOnlyStub } from '@llui/dom'", ''];
113
+ for (const name of namedExports) {
114
+ lines.push(`export const ${name} = __clientOnlyStub(${JSON.stringify(name)})`);
115
+ }
116
+ if (hasDefaultExport) {
117
+ lines.push('export default __clientOnlyStub("default")');
118
+ }
119
+ return {
120
+ output: lines.join('\n') + '\n',
121
+ warnings,
122
+ };
123
+ }
124
+ /**
125
+ * Check whether `source`'s first statement is a `'use client'` directive.
126
+ * Cheap string scan so the caller can decide which transform to run
127
+ * without parsing the whole file twice.
128
+ */
129
+ export function hasUseClientDirective(source) {
130
+ // Skip leading whitespace and block/line comments; look for the
131
+ // first token. A full parse is overkill here — users who write
132
+ // `'use client'` in any other position (inside a function, after
133
+ // imports) aren't using the directive as React/Vercel define it.
134
+ let i = 0;
135
+ const len = source.length;
136
+ while (i < len) {
137
+ const ch = source[i];
138
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
139
+ i++;
140
+ continue;
141
+ }
142
+ if (source.startsWith('//', i)) {
143
+ const nl = source.indexOf('\n', i);
144
+ if (nl === -1)
145
+ return false;
146
+ i = nl + 1;
147
+ continue;
148
+ }
149
+ if (source.startsWith('/*', i)) {
150
+ const end = source.indexOf('*/', i + 2);
151
+ if (end === -1)
152
+ return false;
153
+ i = end + 2;
154
+ continue;
155
+ }
156
+ break;
157
+ }
158
+ return source.startsWith("'use client'", i) || source.startsWith('"use client"', i);
159
+ }
160
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["// @llui/compiler-ssr — SSR support (opt-in).\n//\n// Handles the `'use client'` directive: scans for it cheaply with\n// `hasUseClientDirective`, then rewrites client-only modules into\n// stubs that the SSR build can ship via `transformUseClientSsr`.\n//\n// `@llui/vike` calls these directly from its Vite plugin's transform\n// hook when `ssr: true`. The plugin gates on `hasUseClientDirective`\n// to avoid the parse cost when the directive isn't present.\n//\n// Owned by this package since v2c/decomp-25 (moved verbatim from\n// @llui/compiler's transform.ts).\n\nimport ts from 'typescript'\n\n// ── 'use client' directive ───────────────────────────────────────\n\nexport interface UseClientTransformResult {\n output: string\n warnings: string[]\n}\n\n/**\n * If `source` begins with a `'use client'` directive, generate a stub\n * replacement for the SSR build. Every `export const X = <expr>` becomes\n * `export const X = __clientOnlyStub('X')`, every `export function X`\n * becomes a stub, and `export default <expr>` becomes a default stub.\n * Returns `null` if the directive is absent (caller should fall through\n * to the normal compiler pass).\n *\n * The client build is expected to skip this path entirely — Vite passes\n * `{ ssr: false }` there, and the plugin checks that before invoking\n * this function.\n *\n * Shapes this v1 does NOT handle (emits a warning + leaves them out of\n * the stub output):\n *\n * - `export function foo() {}` and `export class Foo {}` — rewritten\n * as stubs but the caller may be surprised that `foo` and `Foo` are\n * ComponentDef-shaped objects during SSR.\n * - `export { a, b } from './other.js'` — re-export forms are not\n * detected; they pass through and will still pull `./other` into\n * the SSR graph.\n * - `export * from './other.js'` — same as above.\n * - `export type ...` — type exports are erased by TS so nothing to\n * stub; left untouched.\n */\nexport function transformUseClientSsr(\n source: string,\n _filename: string,\n): UseClientTransformResult | null {\n const sourceFile = ts.createSourceFile('input.ts', source, ts.ScriptTarget.Latest, true)\n\n // Find the first non-comment, non-directive-whitespace statement.\n // 'use client' should be the literal first statement in the file.\n const first = sourceFile.statements[0]\n if (!first) return null\n if (!ts.isExpressionStatement(first)) return null\n if (!ts.isStringLiteral(first.expression)) return null\n if (first.expression.text !== 'use client') return null\n\n const warnings: string[] = []\n const namedExports: string[] = []\n let hasDefaultExport = false\n\n for (const stmt of sourceFile.statements) {\n // The `'use client'` directive itself — skip.\n if (stmt === first) continue\n\n // `export const NAME = ...` and `export let NAME = ...`\n if (\n ts.isVariableStatement(stmt) &&\n stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)\n ) {\n for (const decl of stmt.declarationList.declarations) {\n if (ts.isIdentifier(decl.name)) {\n namedExports.push(decl.name.text)\n } else {\n warnings.push(\n '[llui/use-client] destructured `export const { ... }` is not supported; each binding would have to be stubbed individually. Refactor to one `export const` per value.',\n )\n }\n }\n continue\n }\n\n // `export function NAME() {}`\n if (\n ts.isFunctionDeclaration(stmt) &&\n stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&\n stmt.name\n ) {\n namedExports.push(stmt.name.text)\n continue\n }\n\n // `export class NAME {}`\n if (\n ts.isClassDeclaration(stmt) &&\n stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) &&\n stmt.name\n ) {\n namedExports.push(stmt.name.text)\n continue\n }\n\n // `export default ...`\n if (\n ts.isExportAssignment(stmt) ||\n (ts.isFunctionDeclaration(stmt) &&\n stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword))\n ) {\n hasDefaultExport = true\n continue\n }\n\n // `export { a, b }` / `export { a } from './x.js'` / `export * from './x.js'`\n if (ts.isExportDeclaration(stmt)) {\n if (stmt.moduleSpecifier) {\n warnings.push(\n \"[llui/use-client] `export ... from '...'` re-export forms still pull the source module into the SSR graph and bypass stubbing. Either drop the re-export or move the 'use client' directive to the source module.\",\n )\n } else if (stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {\n for (const spec of stmt.exportClause.elements) {\n namedExports.push((spec.name ?? spec.propertyName!).text)\n }\n }\n continue\n }\n\n // Type-only statements are erased at runtime — nothing to stub.\n if (ts.isTypeAliasDeclaration(stmt) || ts.isInterfaceDeclaration(stmt)) continue\n\n // Imports, `import type`, enum declarations, plain (non-export)\n // variable statements — dropped from the stub output.\n }\n\n // Build the generated module source.\n const lines: string[] = [\"import { __clientOnlyStub } from '@llui/dom'\", '']\n for (const name of namedExports) {\n lines.push(`export const ${name} = __clientOnlyStub(${JSON.stringify(name)})`)\n }\n if (hasDefaultExport) {\n lines.push('export default __clientOnlyStub(\"default\")')\n }\n\n return {\n output: lines.join('\\n') + '\\n',\n warnings,\n }\n}\n\n/**\n * Check whether `source`'s first statement is a `'use client'` directive.\n * Cheap string scan so the caller can decide which transform to run\n * without parsing the whole file twice.\n */\nexport function hasUseClientDirective(source: string): boolean {\n // Skip leading whitespace and block/line comments; look for the\n // first token. A full parse is overkill here — users who write\n // `'use client'` in any other position (inside a function, after\n // imports) aren't using the directive as React/Vercel define it.\n let i = 0\n const len = source.length\n while (i < len) {\n const ch = source[i]!\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++\n continue\n }\n if (source.startsWith('//', i)) {\n const nl = source.indexOf('\\n', i)\n if (nl === -1) return false\n i = nl + 1\n continue\n }\n if (source.startsWith('/*', i)) {\n const end = source.indexOf('*/', i + 2)\n if (end === -1) return false\n i = end + 2\n continue\n }\n break\n }\n return source.startsWith(\"'use client'\", i) || source.startsWith('\"use client\"', i)\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@llui/compiler-ssr",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "typescript": "^6.0.2",
19
+ "@llui/compiler": "0.3.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0"
23
+ },
24
+ "description": "LLui compiler — SSR support (opt-in). 'use client' directive handling and SSR-specific emission paths consumed by @llui/vike.",
25
+ "keywords": [
26
+ "llui",
27
+ "compiler",
28
+ "ssr"
29
+ ],
30
+ "author": "Franco Ponticelli <franco.ponticelli@gmail.com>",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/fponticelli/llui.git",
35
+ "directory": "packages/compiler-ssr"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/fponticelli/llui/issues"
39
+ },
40
+ "homepage": "https://github.com/fponticelli/llui/tree/main/packages/compiler-ssr#readme",
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json",
43
+ "check": "tsc --noEmit",
44
+ "lint": "eslint src",
45
+ "test": "vitest run"
46
+ }
47
+ }