@anaemia/bundler 0.3.7 → 0.5.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 +1 -1
- package/README.md +1 -1
- package/dist/aliases.js +1 -1
- package/dist/analyzer/ast-utils.d.ts +5 -0
- package/dist/analyzer/ast-utils.d.ts.map +1 -0
- package/dist/analyzer/ast-utils.js +16 -0
- package/dist/analyzer/ast-walker.d.ts +15 -0
- package/dist/analyzer/ast-walker.d.ts.map +1 -0
- package/dist/analyzer/ast-walker.js +43 -0
- package/dist/analyzer/checks/env-access.d.ts +3 -0
- package/dist/analyzer/checks/env-access.d.ts.map +1 -0
- package/dist/analyzer/checks/env-access.js +63 -0
- package/dist/analyzer/checks/route-metadata.d.ts +12 -0
- package/dist/analyzer/checks/route-metadata.d.ts.map +1 -0
- package/dist/analyzer/checks/route-metadata.js +74 -0
- package/dist/analyzer/checks/server-functions.d.ts +3 -0
- package/dist/analyzer/checks/server-functions.d.ts.map +1 -0
- package/dist/analyzer/checks/server-functions.js +75 -0
- package/dist/analyzer/index.d.ts +7 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +49 -0
- package/dist/analyzer/parser.d.ts +4 -0
- package/dist/analyzer/parser.d.ts.map +1 -0
- package/dist/analyzer/parser.js +96 -0
- package/dist/analyzer/types.d.ts +48 -0
- package/dist/analyzer/types.d.ts.map +1 -0
- package/dist/analyzer/types.js +1 -0
- package/dist/env-loader.d.ts +2 -0
- package/dist/env-loader.d.ts.map +1 -0
- package/dist/env-loader.js +10 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -15
- package/dist/optimization.d.ts.map +1 -1
- package/dist/optimization.js +17 -4
- package/dist/plugins/babel-transform-server.d.ts.map +1 -1
- package/dist/plugins/babel-transform-server.js +1 -4
- package/dist/router/generate-entry.d.ts.map +1 -1
- package/dist/router/generate-entry.js +3 -3
- package/dist/router/generate-server-routes.d.ts.map +1 -1
- package/dist/router/generate-server-routes.js +15 -5
- package/dist/router/manifest.d.ts +3 -2
- package/dist/router/manifest.d.ts.map +1 -1
- package/dist/router/manifest.js +20 -20
- package/dist/router/scan.d.ts.map +1 -1
- package/dist/router/scan.js +5 -6
- package/dist/rules.d.ts +16 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +37 -5
- package/package.json +11 -3
- package/src/aliases.ts +2 -2
- package/src/analyzer/ast-utils.ts +22 -0
- package/src/analyzer/ast-walker.ts +63 -0
- package/src/analyzer/checks/env-access.ts +77 -0
- package/src/analyzer/checks/route-metadata.ts +91 -0
- package/src/analyzer/checks/server-functions.ts +85 -0
- package/src/analyzer/index.ts +70 -0
- package/src/analyzer/parser.ts +103 -0
- package/src/analyzer/types.ts +55 -0
- package/src/env-loader.ts +13 -0
- package/src/index.ts +119 -19
- package/src/optimization.ts +18 -5
- package/src/plugins/babel-transform-server.ts +1 -4
- package/src/plugins/rspack-manifest-hydration.ts +3 -3
- package/src/router/generate-entry.ts +15 -6
- package/src/router/generate-server-routes.ts +16 -5
- package/src/router/manifest.ts +24 -38
- package/src/router/scan.ts +9 -10
- package/src/rules.ts +48 -8
- package/test/analyzer.test.mjs +308 -0
- package/test/rspack-config.test.mjs +5 -2
- package/test/server-functions.test.mjs +25 -22
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import { analyzeApp, walkAst } from "../dist/analyzer/index.js";
|
|
8
|
+
|
|
9
|
+
function createTmpProject() {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-analyzer-test-"));
|
|
11
|
+
fs.mkdirSync(path.join(dir, "src/routes/users/[id]"), { recursive: true });
|
|
12
|
+
fs.mkdirSync(path.join(dir, "src/features/auth/components"), { recursive: true });
|
|
13
|
+
fs.mkdirSync(path.join(dir, "src/shared/utils"), { recursive: true });
|
|
14
|
+
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(dir, "anaemia.config.ts"),
|
|
17
|
+
`
|
|
18
|
+
import { defineConfig } from "@anaemia/core";
|
|
19
|
+
export default defineConfig({ port: 3005 });
|
|
20
|
+
`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(path.join(dir, "src/root.tsx"), `export default function Root(props) { return props.children; }`);
|
|
24
|
+
|
|
25
|
+
// route with PUBLIC_ env - valid
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(dir, "src/routes/users/[id]/index.tsx"),
|
|
28
|
+
`export default function UserPage() { return <h1>{import.meta.env.PUBLIC_API_URL}</h1>; }`,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// route with non-PUBLIC_ env - should warn
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(dir, "src/routes/index.tsx"),
|
|
34
|
+
`export default function Home() { return <div>{import.meta.env.SECRET_KEY}</div>; }`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// route with process.env - should warn
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(dir, "src/routes/about.tsx"),
|
|
40
|
+
`export default function About() { return <div>{process.env.API_KEY}</div>; }`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// server route - env access should not warn
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(dir, "src/routes/users/[id]/_route.ts"),
|
|
46
|
+
`export const GET = () => new Response(import.meta.env.SECRET_KEY);`,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// .server file - env access should not warn
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
path.join(dir, "src/features/auth/components/auth.server.ts"),
|
|
52
|
+
`export const getToken = () => import.meta.env.SECRET_TOKEN;`,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// feature component with non-PUBLIC_ env - should warn
|
|
56
|
+
fs.writeFileSync(
|
|
57
|
+
path.join(dir, "src/features/auth/components/Login.tsx"),
|
|
58
|
+
`export function Login() { return <form action={import.meta.env.AUTH_URL} />; }`,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// server function definition
|
|
62
|
+
fs.writeFileSync(
|
|
63
|
+
path.join(dir, "src/features/auth/components/actions.server.ts"),
|
|
64
|
+
`
|
|
65
|
+
import { runOnServer } from "@anaemia/core";
|
|
66
|
+
export const getUser = runOnServer(async (id) => ({ id }), "getUser");
|
|
67
|
+
export const unusedFn = runOnServer(async () => {}, "unusedFn");
|
|
68
|
+
`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// only imports getUser, not unusedFn
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(dir, "src/routes/users/[id]/index.tsx"),
|
|
74
|
+
`
|
|
75
|
+
import { getUser } from "../../features/auth/components/actions.server";
|
|
76
|
+
export default function UserPage() { return <h1>{import.meta.env.PUBLIC_API_URL}</h1>; }
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return dir;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// file classification
|
|
84
|
+
|
|
85
|
+
test("analyzeApp classifies file kinds correctly", async () => {
|
|
86
|
+
const dir = createTmpProject();
|
|
87
|
+
try {
|
|
88
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
89
|
+
const kinds = new Map(result.files.map((f) => [f.relativePath, f.kind]));
|
|
90
|
+
|
|
91
|
+
assert.equal(kinds.get("anaemia.config.ts"), "config");
|
|
92
|
+
assert.equal(kinds.get("src/root.tsx"), "root");
|
|
93
|
+
assert.equal(kinds.get("src/routes/users/[id]/index.tsx"), "route");
|
|
94
|
+
assert.equal(kinds.get("src/routes/users/[id]/_route.ts"), "server-route");
|
|
95
|
+
assert.equal(result.build.mode, "test");
|
|
96
|
+
} finally {
|
|
97
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// defineConfig
|
|
102
|
+
|
|
103
|
+
test("analyzeApp handles defineConfig in anaemia.config.ts without errors", async () => {
|
|
104
|
+
const dir = createTmpProject();
|
|
105
|
+
try {
|
|
106
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
107
|
+
const configFile = result.files.find((f) => f.relativePath === "anaemia.config.ts");
|
|
108
|
+
|
|
109
|
+
assert.ok(configFile, "config file should be present");
|
|
110
|
+
assert.ok(configFile.program !== null, "config file should parse without error");
|
|
111
|
+
|
|
112
|
+
const configErrors = configFile.diagnostics.filter((d) => d.severity === "error");
|
|
113
|
+
assert.equal(configErrors.length, 0, "config file should have no parse errors");
|
|
114
|
+
} finally {
|
|
115
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// env access
|
|
120
|
+
|
|
121
|
+
test("env check: PUBLIC_ prefix passes without warning", async () => {
|
|
122
|
+
const dir = createTmpProject();
|
|
123
|
+
try {
|
|
124
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
125
|
+
const warnings = result.diagnostics.filter(
|
|
126
|
+
(d) => d.code === "ENV_NOT_PUBLIC" && d.file?.includes("users/[id]/index.tsx"),
|
|
127
|
+
);
|
|
128
|
+
assert.equal(warnings.length, 0);
|
|
129
|
+
} finally {
|
|
130
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("env check: non-PUBLIC_ prefix in route warns", async () => {
|
|
135
|
+
const dir = createTmpProject();
|
|
136
|
+
try {
|
|
137
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
138
|
+
const warnings = result.diagnostics.filter(
|
|
139
|
+
(d) => d.code === "ENV_NOT_PUBLIC" && d.filePath?.includes("routes/index.tsx"),
|
|
140
|
+
);
|
|
141
|
+
assert.equal(warnings.length, 1);
|
|
142
|
+
assert.ok(warnings[0].message.includes("SECRET_KEY"));
|
|
143
|
+
} finally {
|
|
144
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("env check: non-PUBLIC_ prefix in feature component warns", async () => {
|
|
149
|
+
const dir = createTmpProject();
|
|
150
|
+
try {
|
|
151
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
152
|
+
const warnings = result.diagnostics.filter((d) => d.code === "ENV_NOT_PUBLIC" && d.filePath?.includes("Login.tsx"));
|
|
153
|
+
assert.equal(warnings.length, 1);
|
|
154
|
+
assert.ok(warnings[0].message.includes("AUTH_URL"));
|
|
155
|
+
} finally {
|
|
156
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("env check: server route does not warn for non-PUBLIC_ env", async () => {
|
|
161
|
+
const dir = createTmpProject();
|
|
162
|
+
try {
|
|
163
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
164
|
+
const warnings = result.diagnostics.filter((d) => d.code === "ENV_NOT_PUBLIC" && d.file?.includes("_route.ts"));
|
|
165
|
+
assert.equal(warnings.length, 0);
|
|
166
|
+
} finally {
|
|
167
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("env check: .server.ts file does not warn for non-PUBLIC_ env", async () => {
|
|
172
|
+
const dir = createTmpProject();
|
|
173
|
+
try {
|
|
174
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
175
|
+
const warnings = result.diagnostics.filter(
|
|
176
|
+
(d) => d.code === "ENV_NOT_PUBLIC" && d.file?.includes("auth.server.ts"),
|
|
177
|
+
);
|
|
178
|
+
assert.equal(warnings.length, 0);
|
|
179
|
+
} finally {
|
|
180
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("env check: process.env usage warns", async () => {
|
|
185
|
+
const dir = createTmpProject();
|
|
186
|
+
try {
|
|
187
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
188
|
+
const warnings = result.diagnostics.filter(
|
|
189
|
+
(d) => d.code === "PROCESS_ENV_ACCESS" && d.filePath?.includes("about.tsx"),
|
|
190
|
+
);
|
|
191
|
+
assert.equal(warnings.length, 1);
|
|
192
|
+
} finally {
|
|
193
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("env check: ALWAYS_SAFE keys do not warn", async () => {
|
|
198
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-analyzer-test-"));
|
|
199
|
+
try {
|
|
200
|
+
fs.mkdirSync(path.join(dir, "src/routes"), { recursive: true });
|
|
201
|
+
fs.writeFileSync(
|
|
202
|
+
path.join(dir, "src/routes/index.tsx"),
|
|
203
|
+
`
|
|
204
|
+
export default function Page() {
|
|
205
|
+
return <div>{import.meta.env.NODE_ENV}{import.meta.env.MODE}{import.meta.env.DEV}{import.meta.env.PROD}</div>;
|
|
206
|
+
}
|
|
207
|
+
`,
|
|
208
|
+
);
|
|
209
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
210
|
+
const warnings = result.diagnostics.filter((d) => d.code === "ENV_NOT_PUBLIC");
|
|
211
|
+
assert.equal(warnings.length, 0);
|
|
212
|
+
} finally {
|
|
213
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// unused server functions
|
|
218
|
+
|
|
219
|
+
test("unused server functions: imported function does not warn", async () => {
|
|
220
|
+
const dir = createTmpProject();
|
|
221
|
+
try {
|
|
222
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
223
|
+
const warnings = result.diagnostics.filter(
|
|
224
|
+
(d) => d.code === "UNUSED_SERVER_FUNCTION" && d.message?.includes("getUser"),
|
|
225
|
+
);
|
|
226
|
+
assert.equal(warnings.length, 0);
|
|
227
|
+
} finally {
|
|
228
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("unused server functions: unimported function warns", async () => {
|
|
233
|
+
const dir = createTmpProject();
|
|
234
|
+
try {
|
|
235
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
236
|
+
const warnings = result.diagnostics.filter(
|
|
237
|
+
(d) => d.code === "UNUSED_SERVER_FUNCTION" && d.message?.includes("unusedFn"),
|
|
238
|
+
);
|
|
239
|
+
assert.equal(warnings.length, 1);
|
|
240
|
+
} finally {
|
|
241
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// route metadata
|
|
246
|
+
|
|
247
|
+
test("route metadata: static route detected correctly", async () => {
|
|
248
|
+
const dir = createTmpProject();
|
|
249
|
+
try {
|
|
250
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
251
|
+
const aboutMeta = result.routeMetadata?.find((m) => m.filePath.includes("about.tsx"));
|
|
252
|
+
assert.ok(aboutMeta);
|
|
253
|
+
assert.equal(aboutMeta.isStatic, true);
|
|
254
|
+
assert.equal(aboutMeta.hasLoader, false);
|
|
255
|
+
assert.equal(aboutMeta.hasGuard, false);
|
|
256
|
+
} finally {
|
|
257
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("route metadata: route with server function import is not static", async () => {
|
|
262
|
+
const dir = createTmpProject();
|
|
263
|
+
try {
|
|
264
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
265
|
+
const userMeta = result.routeMetadata?.find((m) => m.filePath.includes("users/[id]/index.tsx"));
|
|
266
|
+
assert.ok(userMeta);
|
|
267
|
+
assert.equal(userMeta.isStatic, false);
|
|
268
|
+
assert.equal(userMeta.hasServerFunctions, true);
|
|
269
|
+
} finally {
|
|
270
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("route metadata: params extracted from file path", async () => {
|
|
275
|
+
const dir = createTmpProject();
|
|
276
|
+
try {
|
|
277
|
+
const result = await analyzeApp(dir, { mode: "test" });
|
|
278
|
+
const userMeta = result.routeMetadata?.find((m) => m.filePath.includes("users/[id]"));
|
|
279
|
+
assert.ok(userMeta);
|
|
280
|
+
assert.ok(userMeta.params.includes("id"));
|
|
281
|
+
} finally {
|
|
282
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// walkAst
|
|
287
|
+
|
|
288
|
+
test("walkAst visits all expected node types", async () => {
|
|
289
|
+
const dir = createTmpProject();
|
|
290
|
+
try {
|
|
291
|
+
const result = await analyzeApp(dir, { include: ["src/routes/**/*.tsx"] });
|
|
292
|
+
const routeFile = result.files.find((f) => f.relativePath === "src/routes/users/[id]/index.tsx");
|
|
293
|
+
|
|
294
|
+
assert.ok(routeFile);
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
walkAst(routeFile.program, {
|
|
297
|
+
enter(node) {
|
|
298
|
+
seen.add(node.type);
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
assert.ok(seen.has("Program"));
|
|
303
|
+
assert.ok(seen.has("ImportDeclaration"));
|
|
304
|
+
assert.ok(seen.has("ExportDefaultDeclaration"));
|
|
305
|
+
} finally {
|
|
306
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
@@ -9,7 +9,10 @@ async function createTmpProject(isTs = true) {
|
|
|
9
9
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "anaemia-bundler-test-"));
|
|
10
10
|
|
|
11
11
|
fs.mkdirSync(path.join(dir, "src/routes"), { recursive: true });
|
|
12
|
-
fs.writeFileSync(
|
|
12
|
+
fs.writeFileSync(
|
|
13
|
+
path.join(dir, "src/routes/index.tsx"),
|
|
14
|
+
`export default function Index() { return <div>hello</div>; }`,
|
|
15
|
+
);
|
|
13
16
|
fs.writeFileSync(path.join(dir, "index.html"), `<html><body><div anaemia-entry></div></body></html>`);
|
|
14
17
|
|
|
15
18
|
if (isTs) {
|
|
@@ -106,4 +109,4 @@ test("server config aliases point to dist not src", async () => {
|
|
|
106
109
|
} finally {
|
|
107
110
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
108
111
|
}
|
|
109
|
-
});
|
|
112
|
+
});
|
|
@@ -15,12 +15,14 @@ const source = `
|
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
17
|
function transform(plugin) {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
return (
|
|
19
|
+
transformSync(source, {
|
|
20
|
+
filename,
|
|
21
|
+
plugins: [plugin],
|
|
22
|
+
configFile: false,
|
|
23
|
+
babelrc: false,
|
|
24
|
+
})?.code ?? ""
|
|
25
|
+
);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
test("client and server transforms generate the same server function id", () => {
|
|
@@ -42,36 +44,37 @@ test("client transform forwards call arguments to the RPC wrapper", () => {
|
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
test("client transform preserves explicit server function ids", () => {
|
|
45
|
-
const code =
|
|
46
|
-
|
|
47
|
+
const code =
|
|
48
|
+
transformSync(
|
|
49
|
+
`
|
|
47
50
|
import { runOnServer } from "@anaemia/core";
|
|
48
51
|
export const ping = runOnServer(async () => "pong", "custom-id");
|
|
49
52
|
`,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
{
|
|
54
|
+
filename,
|
|
55
|
+
plugins: [clientServerFnTransform],
|
|
56
|
+
configFile: false,
|
|
57
|
+
babelrc: false,
|
|
58
|
+
},
|
|
59
|
+
)?.code ?? "";
|
|
57
60
|
|
|
58
61
|
assert.match(code, /\$\$executeClientRpc\("custom-id"\)/);
|
|
59
62
|
});
|
|
60
63
|
|
|
61
64
|
test("should guarantee server-side logic never leaks to client assets", async () => {
|
|
62
65
|
const clientAssetDir = path.resolve(process.cwd(), "dist/client/assets");
|
|
63
|
-
|
|
66
|
+
|
|
64
67
|
if (!fs.existsSync(clientAssetDir)) return;
|
|
65
68
|
|
|
66
|
-
const files = fs.readdirSync(clientAssetDir).filter(f => f.endsWith(".js"));
|
|
69
|
+
const files = fs.readdirSync(clientAssetDir).filter((f) => f.endsWith(".js"));
|
|
67
70
|
|
|
68
71
|
for (const file of files) {
|
|
69
72
|
const content = fs.readFileSync(path.join(clientAssetDir, file), "utf-8");
|
|
70
|
-
|
|
73
|
+
|
|
71
74
|
assert.equal(
|
|
72
|
-
content.includes("SELECT * FROM users"),
|
|
73
|
-
false,
|
|
74
|
-
`CRITICAL SECURITY LEAK: server logic found inside client asset: ${file}
|
|
75
|
+
content.includes("SELECT * FROM users"),
|
|
76
|
+
false,
|
|
77
|
+
`CRITICAL SECURITY LEAK: server logic found inside client asset: ${file}`,
|
|
75
78
|
);
|
|
76
79
|
}
|
|
77
|
-
});
|
|
80
|
+
});
|
package/tsconfig.json
CHANGED