@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 +252 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +300 -0
- package/package.json +42 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|