@jay-framework/compiler-jay-stack 0.9.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/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # @jay-framework/compiler-jay-stack
2
+
3
+ Vite/Rollup plugin for Jay Stack that provides **bidirectional code splitting** between client and server environments.
4
+
5
+ ## What It Does
6
+
7
+ This plugin automatically splits Jay Stack component builder chains into environment-specific code:
8
+
9
+ - **Client builds**: Strips server-only code (`withServices`, `withLoadParams`, `withSlowlyRender`, `withFastRender`)
10
+ - **Server builds**: Strips client-only code (`withInteractive`, `withContexts`)
11
+
12
+ This prevents:
13
+
14
+ - ❌ Server secrets leaking to client bundles
15
+ - ❌ Browser APIs crashing Node.js server
16
+ - ❌ Unnecessary code bloat in both environments
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ yarn add -D @jay-framework/compiler-jay-stack
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### For Jay Stack Applications
27
+
28
+ Replace `jayRuntime()` with `jayStackCompiler()`:
29
+
30
+ ```typescript
31
+ // vite.config.ts
32
+ import { defineConfig } from 'vite';
33
+ import { jayStackCompiler } from '@jay-framework/compiler-jay-stack';
34
+
35
+ export default defineConfig({
36
+ plugins: [
37
+ ...jayStackCompiler({
38
+ tsConfigFilePath: './tsconfig.json',
39
+ }),
40
+ ],
41
+ });
42
+ ```
43
+
44
+ The plugin internally composes the `jay:runtime` plugin, so you only need one import.
45
+
46
+ ### For Jay Stack Packages (Reusable Components)
47
+
48
+ Build both server and client bundles:
49
+
50
+ ```typescript
51
+ // vite.config.ts for a Jay Stack package
52
+ import { resolve } from 'path';
53
+ import { defineConfig } from 'vitest/config';
54
+ import { jayStackCompiler, JayRollupConfig } from '@jay-framework/compiler-jay-stack';
55
+
56
+ const jayOptions: JayRollupConfig = {
57
+ tsConfigFilePath: resolve(__dirname, 'tsconfig.json'),
58
+ };
59
+
60
+ export default defineConfig({
61
+ plugins: [...jayStackCompiler(jayOptions)],
62
+ build: {
63
+ lib: {
64
+ entry: {
65
+ // Server build (client code stripped)
66
+ index: resolve(__dirname, 'lib/index.ts?jay-server'),
67
+ // Client build (server code stripped)
68
+ 'index.client': resolve(__dirname, 'lib/index.ts?jay-client'),
69
+ },
70
+ formats: ['es'],
71
+ },
72
+ },
73
+ });
74
+ ```
75
+
76
+ Update `package.json` exports:
77
+
78
+ ```json
79
+ {
80
+ "exports": {
81
+ ".": "./dist/index.js",
82
+ "./client": "./dist/index.client.js"
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## How It Works
88
+
89
+ ### Query Parameters
90
+
91
+ - `?jay-client` - Transform file to client-only code
92
+ - `?jay-server` - Transform file to server-only code
93
+ - No query - Use original code (not recommended for Jay Stack components)
94
+
95
+ ### Example Transformation
96
+
97
+ **Input (page.ts):**
98
+
99
+ ```typescript
100
+ import { DATABASE } from './database';
101
+ import { Interactive } from './interactive';
102
+
103
+ export const page = makeJayStackComponent()
104
+ .withServices(DATABASE)
105
+ .withSlowlyRender(async () => {
106
+ /* ... */
107
+ })
108
+ .withInteractive(Interactive);
109
+ ```
110
+
111
+ **Server Build (`?jay-server`):**
112
+
113
+ ```typescript
114
+ import { DATABASE } from './database';
115
+
116
+ export const page = makeJayStackComponent()
117
+ .withServices(DATABASE)
118
+ .withSlowlyRender(async () => {
119
+ /* ... */
120
+ });
121
+ // ✅ No withInteractive - prevents browser API crashes
122
+ ```
123
+
124
+ **Client Build (`?jay-client`):**
125
+
126
+ ```typescript
127
+ import { Interactive } from './interactive';
128
+
129
+ export const page = makeJayStackComponent().withInteractive(Interactive);
130
+ // ✅ No server code - smaller bundle, no secrets
131
+ ```
132
+
133
+ ## Method Classification
134
+
135
+ | Method | Server | Client | Shared |
136
+ | -------------------- | ------ | ------ | ------ |
137
+ | `withProps()` | ✅ | ✅ | ✅ |
138
+ | `withServices()` | ✅ | ❌ | |
139
+ | `withContexts()` | ❌ | ✅ | |
140
+ | `withLoadParams()` | ✅ | ❌ | |
141
+ | `withSlowlyRender()` | ✅ | ❌ | |
142
+ | `withFastRender()` | ✅ | ❌ | |
143
+ | `withInteractive()` | ❌ | ✅ | |
144
+
145
+ ## Architecture
146
+
147
+ This plugin is a composite of two plugins:
148
+
149
+ 1. **jay-stack:code-split** (runs first, `enforce: 'pre'`)
150
+
151
+ - Strips environment-specific builder methods
152
+ - Removes unused imports
153
+ - Uses TypeScript AST transformation
154
+
155
+ 2. **jay:runtime** (runs second)
156
+ - Handles `.jay-html` and `.jay-contract` compilation
157
+ - Standard Jay runtime compilation
158
+
159
+ ## Benefits
160
+
161
+ ### For Developers
162
+
163
+ - ✅ Write components in one place
164
+ - ✅ Full TypeScript type safety
165
+ - ✅ No manual code organization needed
166
+
167
+ ### For Applications
168
+
169
+ - ✅ Prevents runtime crashes (no browser APIs on server)
170
+ - ✅ Smaller client bundles (no server code)
171
+ - ✅ Smaller server bundles (no client code)
172
+ - ✅ Better security (server secrets can't leak)
173
+
174
+ ### For Package Authors
175
+
176
+ - ✅ One plugin handles both builds
177
+ - ✅ Standard npm export patterns
178
+ - ✅ Automatic optimization for consumers
179
+
180
+ ## Migration
181
+
182
+ ### From `jayRuntime()`
183
+
184
+ ```diff
185
+ - import { jayRuntime } from '@jay-framework/vite-plugin';
186
+ + import { jayStackCompiler } from '@jay-framework/compiler-jay-stack';
187
+
188
+ export default defineConfig({
189
+ plugins: [
190
+ - jayRuntime(config),
191
+ + ...jayStackCompiler(config),
192
+ ],
193
+ });
194
+ ```
195
+
196
+ ### Package.json Dependencies
197
+
198
+ ```diff
199
+ {
200
+ "dependencies": {
201
+ - "@jay-framework/vite-plugin": "workspace:^",
202
+ + "@jay-framework/compiler-jay-stack": "workspace:^",
203
+ }
204
+ }
205
+ ```
206
+
207
+ ## Technical Details
208
+
209
+ ### AST Transformation
210
+
211
+ The plugin uses:
212
+
213
+ - `SourceFileBindingResolver` - Tracks identifier origins
214
+ - `SourceFileStatementDependencies` - Builds dependency graph
215
+ - TypeScript compiler API - Safe AST transformations
216
+
217
+ These utilities are battle-tested from Jay's security transformations.
218
+
219
+ ### Import Detection
220
+
221
+ For headless components:
222
+
223
+ - **Local files** (`./`, `../`): Use `?jay-client` query
224
+ - **npm packages**: Use `/client` export path
225
+
226
+ Example:
227
+
228
+ ```typescript
229
+ // Local component
230
+ import { comp } from './my-component?jay-client';
231
+
232
+ // npm package
233
+ import { comp } from 'my-plugin/client';
234
+ ```
235
+
236
+ ## Debugging
237
+
238
+ If transformation fails, check:
239
+
240
+ 1. Are you using method chaining? (Conditional composition not supported yet)
241
+ 2. Are your imports used elsewhere? (They won't be removed)
242
+ 3. Check console for transformation errors
243
+
244
+ ## See Also
245
+
246
+ - [Design Log #52](../../../design-log/52%20-%20jay-stack%20client-server%20code%20splitting.md) - Full design documentation
247
+ - [@jay-framework/fullstack-component](../full-stack-component/README.md) - Jay Stack component builder
248
+ - [@jay-framework/dev-server](../dev-server/README.md) - Jay Stack dev server
249
+
250
+ ## License
251
+
252
+ Apache-2.0
@@ -0,0 +1,41 @@
1
+ import { Plugin } from 'vite';
2
+ import { JayRollupConfig } from '@jay-framework/vite-plugin';
3
+ export { JayRollupConfig } from '@jay-framework/vite-plugin';
4
+
5
+ type BuildEnvironment = 'client' | 'server';
6
+ /**
7
+ * Transform Jay Stack component builder chains to strip environment-specific code
8
+ *
9
+ * @param code - Source code to transform
10
+ * @param filePath - File path (for source file creation)
11
+ * @param environment - Target environment ('client' or 'server')
12
+ * @returns Transformed code
13
+ */
14
+ declare function transformJayStackBuilder(code: string, filePath: string, environment: BuildEnvironment): {
15
+ code: string;
16
+ map?: any;
17
+ };
18
+
19
+ /**
20
+ * Jay Stack Compiler - Handles both Jay runtime compilation and Jay Stack code splitting
21
+ *
22
+ * This plugin internally uses the jay:runtime plugin and adds Jay Stack-specific
23
+ * transformations for client/server code splitting.
24
+ *
25
+ * Usage:
26
+ * ```typescript
27
+ * import { jayStackCompiler } from '@jay-framework/compiler-jay-stack';
28
+ *
29
+ * export default defineConfig({
30
+ * plugins: [
31
+ * ...jayStackCompiler({ tsConfigFilePath: './tsconfig.json' })
32
+ * ]
33
+ * });
34
+ * ```
35
+ *
36
+ * @param jayOptions - Configuration for Jay runtime (passed to jay:runtime plugin)
37
+ * @returns Array of Vite plugins [codeSplitPlugin, jayRuntimePlugin]
38
+ */
39
+ declare function jayStackCompiler(jayOptions?: JayRollupConfig): Plugin[];
40
+
41
+ export { type BuildEnvironment, jayStackCompiler, transformJayStackBuilder };
package/dist/index.js ADDED
@@ -0,0 +1,300 @@
1
+ import { jayRuntime } from "@jay-framework/vite-plugin";
2
+ import tsBridge from "@jay-framework/typescript-bridge";
3
+ import { flattenVariable, isImportModuleVariableRoot, mkTransformer, SourceFileBindingResolver, areFlattenedAccessChainsEqual } from "@jay-framework/compiler";
4
+ const SERVER_METHODS = /* @__PURE__ */ new Set([
5
+ "withServices",
6
+ "withLoadParams",
7
+ "withSlowlyRender",
8
+ "withFastRender"
9
+ ]);
10
+ const CLIENT_METHODS = /* @__PURE__ */ new Set([
11
+ "withInteractive",
12
+ "withContexts"
13
+ ]);
14
+ function shouldRemoveMethod(methodName, environment) {
15
+ return environment === "client" && SERVER_METHODS.has(methodName) || environment === "server" && CLIENT_METHODS.has(methodName);
16
+ }
17
+ const { isCallExpression: isCallExpression$1, isPropertyAccessExpression: isPropertyAccessExpression$1, isIdentifier: isIdentifier$2, isStringLiteral } = tsBridge;
18
+ function findBuilderMethodsToRemove(sourceFile, bindingResolver, environment) {
19
+ const callsToRemove = [];
20
+ const removedVariables = /* @__PURE__ */ new Set();
21
+ const visit = (node) => {
22
+ if (isCallExpression$1(node) && isPropertyAccessExpression$1(node.expression) && isPartOfJayStackChain(node, bindingResolver)) {
23
+ const methodName = node.expression.name.text;
24
+ if (shouldRemoveMethod(methodName, environment)) {
25
+ const variable = bindingResolver.explain(node.expression);
26
+ const flattened = flattenVariable(variable);
27
+ callsToRemove.push(flattened);
28
+ collectVariablesFromArguments(node.arguments, bindingResolver, removedVariables);
29
+ }
30
+ }
31
+ node.forEachChild(visit);
32
+ };
33
+ sourceFile.forEachChild(visit);
34
+ return { callsToRemove, removedVariables };
35
+ }
36
+ function isPartOfJayStackChain(callExpr, bindingResolver) {
37
+ let current = callExpr.expression;
38
+ while (true) {
39
+ if (isPropertyAccessExpression$1(current)) {
40
+ current = current.expression;
41
+ } else if (isCallExpression$1(current)) {
42
+ if (isIdentifier$2(current.expression)) {
43
+ const variable = bindingResolver.explain(current.expression);
44
+ const flattened = flattenVariable(variable);
45
+ if (flattened.path.length === 1 && flattened.path[0] === "makeJayStackComponent" && isImportModuleVariableRoot(flattened.root) && isStringLiteral(flattened.root.module) && flattened.root.module.text === "@jay-framework/fullstack-component")
46
+ return true;
47
+ }
48
+ if (isPropertyAccessExpression$1(current.expression)) {
49
+ current = current.expression.expression;
50
+ continue;
51
+ } else {
52
+ break;
53
+ }
54
+ } else {
55
+ break;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ function collectVariablesFromArguments(args, bindingResolver, variables) {
61
+ const visitor = (node) => {
62
+ if (isIdentifier$2(node)) {
63
+ const variable = bindingResolver.explain(node);
64
+ if (variable && (variable.name || variable.root)) {
65
+ variables.add(variable);
66
+ }
67
+ }
68
+ node.forEachChild(visitor);
69
+ };
70
+ args.forEach((arg) => visitor(arg));
71
+ }
72
+ const {
73
+ isIdentifier: isIdentifier$1,
74
+ isImportDeclaration: isImportDeclaration$1,
75
+ isFunctionDeclaration: isFunctionDeclaration$1,
76
+ isVariableStatement: isVariableStatement$1,
77
+ isInterfaceDeclaration: isInterfaceDeclaration$1,
78
+ isTypeAliasDeclaration: isTypeAliasDeclaration$1,
79
+ isClassDeclaration,
80
+ isEnumDeclaration,
81
+ SyntaxKind
82
+ } = tsBridge;
83
+ function analyzeUnusedStatements(sourceFile, bindingResolver) {
84
+ const statementsToRemove = /* @__PURE__ */ new Set();
85
+ const collectUsedIdentifiers = () => {
86
+ const used = /* @__PURE__ */ new Set();
87
+ for (const statement of sourceFile.statements) {
88
+ if (isImportDeclaration$1(statement) || statementsToRemove.has(statement)) {
89
+ continue;
90
+ }
91
+ const definedName = getStatementDefinedName(statement);
92
+ const visitor = (node, parent) => {
93
+ if (isIdentifier$1(node)) {
94
+ if (node.text !== definedName) {
95
+ used.add(node.text);
96
+ }
97
+ }
98
+ node.forEachChild((child) => visitor(child));
99
+ };
100
+ statement.forEachChild((child) => visitor(child));
101
+ }
102
+ return used;
103
+ };
104
+ let changed = true;
105
+ while (changed) {
106
+ changed = false;
107
+ const stillUsedIdentifiers = collectUsedIdentifiers();
108
+ for (const statement of sourceFile.statements) {
109
+ if (statementsToRemove.has(statement) || isImportDeclaration$1(statement) || isExportStatement(statement)) {
110
+ continue;
111
+ }
112
+ const definedName = getStatementDefinedName(statement);
113
+ if (definedName && !stillUsedIdentifiers.has(definedName)) {
114
+ statementsToRemove.add(statement);
115
+ changed = true;
116
+ }
117
+ }
118
+ }
119
+ const finalUsedIdentifiers = collectUsedIdentifiers();
120
+ const unusedImports = /* @__PURE__ */ new Set();
121
+ for (const statement of sourceFile.statements) {
122
+ if (isImportDeclaration$1(statement) && statement.importClause?.namedBindings) {
123
+ const namedBindings = statement.importClause.namedBindings;
124
+ if ("elements" in namedBindings) {
125
+ for (const element of namedBindings.elements) {
126
+ const importName = element.name.text;
127
+ if (!finalUsedIdentifiers.has(importName)) {
128
+ unusedImports.add(importName);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return { statementsToRemove, unusedImports };
135
+ }
136
+ function isExportStatement(statement) {
137
+ const modifiers = "modifiers" in statement ? statement.modifiers : void 0;
138
+ if (modifiers) {
139
+ return modifiers.some((mod) => mod.kind === SyntaxKind.ExportKeyword);
140
+ }
141
+ return false;
142
+ }
143
+ function getStatementDefinedName(statement) {
144
+ if (isFunctionDeclaration$1(statement) && statement.name) {
145
+ return statement.name.text;
146
+ }
147
+ if (isVariableStatement$1(statement)) {
148
+ const firstDecl = statement.declarationList.declarations[0];
149
+ if (firstDecl && isIdentifier$1(firstDecl.name)) {
150
+ return firstDecl.name.text;
151
+ }
152
+ }
153
+ if (isInterfaceDeclaration$1(statement) && statement.name) {
154
+ return statement.name.text;
155
+ }
156
+ if (isTypeAliasDeclaration$1(statement) && statement.name) {
157
+ return statement.name.text;
158
+ }
159
+ if (isClassDeclaration(statement) && statement.name) {
160
+ return statement.name.text;
161
+ }
162
+ if (isEnumDeclaration(statement) && statement.name) {
163
+ return statement.name.text;
164
+ }
165
+ return void 0;
166
+ }
167
+ const {
168
+ createPrinter,
169
+ createSourceFile,
170
+ ScriptTarget,
171
+ visitEachChild,
172
+ isCallExpression,
173
+ isPropertyAccessExpression,
174
+ isImportDeclaration,
175
+ isNamedImports,
176
+ isIdentifier,
177
+ isFunctionDeclaration,
178
+ isVariableStatement,
179
+ isInterfaceDeclaration,
180
+ isTypeAliasDeclaration
181
+ } = tsBridge;
182
+ function transformJayStackBuilder(code, filePath, environment) {
183
+ const sourceFile = createSourceFile(
184
+ filePath,
185
+ code,
186
+ ScriptTarget.Latest,
187
+ true
188
+ );
189
+ const transformers = [mkTransformer(mkJayStackCodeSplitTransformer, { environment })];
190
+ const printer = createPrinter();
191
+ const result = tsBridge.transform(sourceFile, transformers);
192
+ const transformedFile = result.transformed[0];
193
+ const transformedCode = printer.printFile(transformedFile);
194
+ result.dispose();
195
+ return {
196
+ code: transformedCode
197
+ };
198
+ }
199
+ function isCallToRemove(flattened, callsToRemove) {
200
+ return callsToRemove.some((call) => areFlattenedAccessChainsEqual(flattened, call));
201
+ }
202
+ function mkJayStackCodeSplitTransformer({
203
+ factory,
204
+ sourceFile,
205
+ context,
206
+ environment
207
+ }) {
208
+ const bindingResolver = new SourceFileBindingResolver(sourceFile);
209
+ const { callsToRemove, removedVariables } = findBuilderMethodsToRemove(
210
+ sourceFile,
211
+ bindingResolver,
212
+ environment
213
+ );
214
+ const transformVisitor = (node) => {
215
+ if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
216
+ const variable = bindingResolver.explain(node.expression);
217
+ const flattened = flattenVariable(variable);
218
+ if (isCallToRemove(flattened, callsToRemove)) {
219
+ const receiver = node.expression.expression;
220
+ return transformVisitor(receiver);
221
+ }
222
+ }
223
+ return visitEachChild(node, transformVisitor, context);
224
+ };
225
+ let transformedSourceFile = visitEachChild(sourceFile, transformVisitor, context);
226
+ new SourceFileBindingResolver(transformedSourceFile);
227
+ const { statementsToRemove, unusedImports } = analyzeUnusedStatements(
228
+ transformedSourceFile
229
+ );
230
+ const transformedStatements = transformedSourceFile.statements.map((statement) => {
231
+ if (statementsToRemove.has(statement)) {
232
+ return void 0;
233
+ }
234
+ if (isImportDeclaration(statement)) {
235
+ return filterImportDeclaration(statement, unusedImports, factory);
236
+ }
237
+ return statement;
238
+ }).filter((s) => s !== void 0);
239
+ return factory.updateSourceFile(transformedSourceFile, transformedStatements);
240
+ }
241
+ function filterImportDeclaration(statement, unusedImports, factory) {
242
+ const importClause = statement.importClause;
243
+ if (!importClause?.namedBindings || !isNamedImports(importClause.namedBindings)) {
244
+ return statement;
245
+ }
246
+ const usedElements = importClause.namedBindings.elements.filter(
247
+ (element) => !unusedImports.has(element.name.text)
248
+ );
249
+ if (usedElements.length === 0) {
250
+ return void 0;
251
+ }
252
+ return factory.updateImportDeclaration(
253
+ statement,
254
+ statement.modifiers,
255
+ factory.updateImportClause(
256
+ importClause,
257
+ importClause.isTypeOnly,
258
+ importClause.name,
259
+ factory.updateNamedImports(
260
+ importClause.namedBindings,
261
+ usedElements
262
+ )
263
+ ),
264
+ statement.moduleSpecifier,
265
+ statement.assertClause
266
+ );
267
+ }
268
+ function jayStackCompiler(jayOptions = {}) {
269
+ return [
270
+ // First: Jay Stack code splitting transformation
271
+ {
272
+ name: "jay-stack:code-split",
273
+ enforce: "pre",
274
+ // Run before jay:runtime
275
+ transform(code, id) {
276
+ const isClientBuild = id.includes("?jay-client");
277
+ const isServerBuild = id.includes("?jay-server");
278
+ if (!isClientBuild && !isServerBuild) {
279
+ return null;
280
+ }
281
+ const environment = isClientBuild ? "client" : "server";
282
+ if (!id.endsWith(".ts") && !id.includes(".ts?")) {
283
+ return null;
284
+ }
285
+ try {
286
+ return transformJayStackBuilder(code, id, environment);
287
+ } catch (error) {
288
+ console.error(`[jay-stack:code-split] Error transforming ${id}:`, error);
289
+ return null;
290
+ }
291
+ }
292
+ },
293
+ // Second: Jay runtime compilation (existing plugin)
294
+ jayRuntime(jayOptions)
295
+ ];
296
+ }
297
+ export {
298
+ jayStackCompiler,
299
+ transformJayStackBuilder
300
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@jay-framework/compiler-jay-stack",
3
+ "version": "0.9.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "npm run build:js && npm run build:types",
20
+ "build:watch": "npm run build:js -- --watch & npm run build:types -- --watch",
21
+ "build:js": "vite build",
22
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
23
+ "build:check-types": "tsc",
24
+ "clean": "rimraf dist",
25
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest"
28
+ },
29
+ "dependencies": {
30
+ "@jay-framework/compiler": "^0.9.0",
31
+ "@jay-framework/typescript-bridge": "^0.4.0",
32
+ "@jay-framework/vite-plugin": "^0.9.0",
33
+ "vite": "^5.0.11"
34
+ },
35
+ "devDependencies": {
36
+ "@jay-framework/dev-environment": "^0.9.0",
37
+ "rimraf": "^5.0.5",
38
+ "tsup": "^8.0.1",
39
+ "typescript": "^5.3.3",
40
+ "vitest": "^1.2.1"
41
+ }
42
+ }