@moonsjs/vite 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +9 -2
- package/dist/index-8xq8fcgq.js +222 -0
- package/dist/index-8xq8fcgq.js.map +14 -0
- package/dist/index-fg6nqart.js +776 -0
- package/dist/index-fg6nqart.js.map +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +424 -0
- package/dist/index.js.map +11 -0
- package/dist/leak-check.d.ts +14 -0
- package/dist/leak-check.d.ts.map +1 -0
- package/dist/manifest.d.ts +21 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/server/dev.d.ts +9 -0
- package/dist/server/dev.d.ts.map +1 -0
- package/dist/server/handler.d.ts +18 -0
- package/dist/server/handler.d.ts.map +1 -0
- package/dist/server/index.d.ts +13 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +311 -0
- package/dist/server/index.js.map +13 -0
- package/dist/server/node-server.d.ts +17 -0
- package/dist/server/node-server.d.ts.map +1 -0
- package/dist/server/node.d.ts +6 -0
- package/dist/server/node.d.ts.map +1 -0
- package/dist/server/shell.d.ts +18 -0
- package/dist/server/shell.d.ts.map +1 -0
- package/dist/server/shutdown.d.ts +17 -0
- package/dist/server/shutdown.d.ts.map +1 -0
- package/dist/server/test-server.d.ts +21 -0
- package/dist/server/test-server.d.ts.map +1 -0
- package/dist/transform.d.ts +101 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +34 -0
- package/dist/transform.js.map +9 -0
- package/dist/virtual-modules.d.ts +12 -0
- package/dist/virtual-modules.d.ts.map +1 -0
- package/package.json +51 -4
- package/src/ctx-fallback.d.ts +9 -0
- package/src/index.ts +2 -0
- package/src/leak-check.ts +36 -0
- package/src/manifest.ts +79 -0
- package/src/plugin.ts +479 -0
- package/src/server/dev.ts +54 -0
- package/src/server/handler.ts +249 -0
- package/src/server/index.ts +12 -0
- package/src/server/node-server.ts +115 -0
- package/src/server/node.ts +61 -0
- package/src/server/shell.ts +77 -0
- package/src/server/shutdown.ts +64 -0
- package/src/server/test-server.ts +44 -0
- package/src/transform.ts +1073 -0
- package/src/virtual-modules.ts +23 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
// src/transform.ts
|
|
2
|
+
import { MoonsError, computeFnHash, fnRegistryKey } from "@moonsjs/core";
|
|
3
|
+
import { parse } from "@babel/parser";
|
|
4
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
var SERVER_SPECIFIER = /\.server(\.(ts|tsx|js|jsx|mts|cts|mjs|cjs))?$/i;
|
|
7
|
+
var CONTRACT_SERVER_FILE = /\.server\.(ts|tsx)$/i;
|
|
8
|
+
var NON_CONTRACT_SERVER_FILE = /\.server\.(js|jsx|mts|cts|mjs|cjs)$/i;
|
|
9
|
+
function isServerFilePath2(path) {
|
|
10
|
+
return CONTRACT_SERVER_FILE.test(stripQuery2(path));
|
|
11
|
+
}
|
|
12
|
+
function isNonContractServerFile2(path) {
|
|
13
|
+
return NON_CONTRACT_SERVER_FILE.test(stripQuery2(path));
|
|
14
|
+
}
|
|
15
|
+
function isServerSpecifier2(specifier) {
|
|
16
|
+
return SERVER_SPECIFIER.test(specifier);
|
|
17
|
+
}
|
|
18
|
+
function resolveServerSpecifierSync2(importerFile, specifier) {
|
|
19
|
+
if (!isServerSpecifier2(specifier)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const base = specifier.startsWith(".") ? resolve(dirname(importerFile), specifier) : isAbsolute(specifier) ? specifier : null;
|
|
23
|
+
if (base === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
for (const candidate of [base, `${base}.ts`, `${base}.tsx`]) {
|
|
27
|
+
if (existsSync(candidate)) {
|
|
28
|
+
return candidate;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function stripQuery2(id) {
|
|
34
|
+
const queryIndex = id.indexOf("?");
|
|
35
|
+
return queryIndex === -1 ? id : id.slice(0, queryIndex);
|
|
36
|
+
}
|
|
37
|
+
function parseModule(code, filename) {
|
|
38
|
+
return parse(code, {
|
|
39
|
+
sourceType: "module",
|
|
40
|
+
plugins: filename.endsWith(".tsx") ? ["typescript", "jsx"] : ["typescript"],
|
|
41
|
+
errorRecovery: false
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function walk(node, visit, ancestors = []) {
|
|
45
|
+
if (node === null || typeof node !== "object") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(node)) {
|
|
49
|
+
for (const item of node) {
|
|
50
|
+
walk(item, visit, ancestors);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const candidate = node;
|
|
55
|
+
if (typeof candidate.type !== "string" || typeof candidate.start !== "number") {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
visit(candidate, ancestors);
|
|
59
|
+
const nextAncestors = [...ancestors, candidate];
|
|
60
|
+
for (const key of Object.keys(candidate)) {
|
|
61
|
+
if (key === "loc" || key === "start" || key === "end" || key === "extra") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
walk(candidate[key], visit, nextAncestors);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function topLevelStatements(ast) {
|
|
68
|
+
const program = ast["program"];
|
|
69
|
+
return program?.body ?? [];
|
|
70
|
+
}
|
|
71
|
+
function keyName(node) {
|
|
72
|
+
const key = node;
|
|
73
|
+
if (key?.type === "Identifier") {
|
|
74
|
+
return key["name"];
|
|
75
|
+
}
|
|
76
|
+
if (key?.type === "StringLiteral") {
|
|
77
|
+
return key["value"];
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function extractServerFnInit(init) {
|
|
82
|
+
const call = init;
|
|
83
|
+
if (call?.type !== "CallExpression") {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const callee = call["callee"];
|
|
87
|
+
if (callee?.type !== "Identifier" || callee["name"] !== "serverFn") {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const args = call["arguments"] ?? [];
|
|
91
|
+
const first = args[0];
|
|
92
|
+
if (first?.type !== "ObjectExpression") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const properties = new Map;
|
|
96
|
+
for (const property of first["properties"] ?? []) {
|
|
97
|
+
if (property.type !== "ObjectProperty") {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const name = keyName(property["key"]);
|
|
101
|
+
if (name !== null) {
|
|
102
|
+
properties.set(name, property);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const input = properties.get("input")?.["value"];
|
|
106
|
+
const output = properties.get("output")?.["value"];
|
|
107
|
+
if (input?.start == null || input.end == null || output?.start == null || output.end == null) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
input: { start: input.start, end: input.end },
|
|
112
|
+
output: { start: output.start, end: output.end },
|
|
113
|
+
line: call.loc?.start.line ?? 1,
|
|
114
|
+
objectStart: first.start ?? 0
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function analyzeServerFile2(source, filename, projectRoot) {
|
|
118
|
+
const ast = parseModule(source, filename);
|
|
119
|
+
const serverFns = [];
|
|
120
|
+
const exportNames = [];
|
|
121
|
+
const reExports = [];
|
|
122
|
+
const exportAllFrom = [];
|
|
123
|
+
for (const statement of topLevelStatements(ast)) {
|
|
124
|
+
if (statement.type === "ExportNamedDeclaration") {
|
|
125
|
+
const declaration = statement["declaration"];
|
|
126
|
+
if (declaration?.type === "VariableDeclaration") {
|
|
127
|
+
for (const declarator of declaration["declarations"] ?? []) {
|
|
128
|
+
const id = declarator["id"];
|
|
129
|
+
if (id?.type !== "Identifier") {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const name = id["name"];
|
|
133
|
+
exportNames.push(name);
|
|
134
|
+
const info = extractServerFnInit(declarator["init"]);
|
|
135
|
+
if (info !== null) {
|
|
136
|
+
serverFns.push({
|
|
137
|
+
name,
|
|
138
|
+
hash: computeFnHash(projectRoot, filename, name),
|
|
139
|
+
line: info.line,
|
|
140
|
+
input: info.input,
|
|
141
|
+
output: info.output,
|
|
142
|
+
objectStart: info.objectStart
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else if (declaration?.type === "FunctionDeclaration") {
|
|
147
|
+
const id = declaration["id"];
|
|
148
|
+
if (id?.type === "Identifier") {
|
|
149
|
+
exportNames.push(id["name"]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const specifier of statement["specifiers"] ?? []) {
|
|
153
|
+
const exported = specifier["exported"];
|
|
154
|
+
if (exported?.type === "Identifier") {
|
|
155
|
+
exportNames.push(exported["name"]);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const sourceNode = statement["source"];
|
|
159
|
+
if (sourceNode?.type === "StringLiteral" && isServerSpecifier2(sourceNode["value"])) {
|
|
160
|
+
reExports.push({
|
|
161
|
+
source: sourceNode["value"],
|
|
162
|
+
names: (statement["specifiers"] ?? []).map((specifier) => ({
|
|
163
|
+
local: specifier["local"]?.["name"] ?? "",
|
|
164
|
+
exported: specifier["exported"]?.["name"] ?? ""
|
|
165
|
+
}))
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} else if (statement.type === "ExportDefaultDeclaration") {
|
|
169
|
+
exportNames.push("default");
|
|
170
|
+
} else if (statement.type === "ExportAllDeclaration") {
|
|
171
|
+
const sourceNode = statement["source"];
|
|
172
|
+
if (sourceNode?.type === "StringLiteral" && isServerSpecifier2(sourceNode["value"])) {
|
|
173
|
+
exportAllFrom.push(sourceNode["value"]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { file: filename, serverFns, exportNames, reExports, exportAllFrom };
|
|
178
|
+
}
|
|
179
|
+
function serverFnExportNames2(analysis, analyzeTarget, seen = new Set) {
|
|
180
|
+
const names = new Set(analysis.serverFns.map((fn) => fn.name));
|
|
181
|
+
for (const reExport of analysis.reExports) {
|
|
182
|
+
const target = analyzeTarget(reExport.source);
|
|
183
|
+
if (target === null || seen.has(reExport.source)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
seen.add(reExport.source);
|
|
187
|
+
const targetNames = serverFnExportNames2(target, analyzeTarget, seen);
|
|
188
|
+
for (const { local, exported } of reExport.names) {
|
|
189
|
+
if (local === "*" || targetNames.has(local)) {
|
|
190
|
+
names.add(exported);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const specifier of analysis.exportAllFrom) {
|
|
195
|
+
const target = analyzeTarget(specifier);
|
|
196
|
+
if (target === null || seen.has(specifier)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
seen.add(specifier);
|
|
200
|
+
for (const name of serverFnExportNames2(target, analyzeTarget, seen)) {
|
|
201
|
+
names.add(name);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return names;
|
|
205
|
+
}
|
|
206
|
+
function collectRetainedText(source, analysis) {
|
|
207
|
+
return analysis.serverFns.map((fn) => `${source.slice(fn.input.start, fn.input.end)}
|
|
208
|
+
${source.slice(fn.output.start, fn.output.end)}`).join(`
|
|
209
|
+
`);
|
|
210
|
+
}
|
|
211
|
+
function isUsed(name, retainedText) {
|
|
212
|
+
return new RegExp(`(^|[^\\w$])${escapeRegExp(name)}([^\\w$]|$)`).test(retainedText);
|
|
213
|
+
}
|
|
214
|
+
function escapeRegExp(text) {
|
|
215
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
216
|
+
}
|
|
217
|
+
function renderServerFnStub2(source, filename, analysis, dependencies) {
|
|
218
|
+
const ast = parseModule(source, filename);
|
|
219
|
+
const retainedText = collectRetainedText(source, analysis);
|
|
220
|
+
const importLines = [];
|
|
221
|
+
const referenced = [];
|
|
222
|
+
let needsCreateStub = analysis.serverFns.length > 0;
|
|
223
|
+
for (const statement of topLevelStatements(ast)) {
|
|
224
|
+
if (statement.type !== "ImportDeclaration") {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const sourceNode = statement["source"];
|
|
228
|
+
const specifier = sourceNode["value"];
|
|
229
|
+
if (/^@moons\/server(\/|$)/.test(specifier)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (specifier.startsWith("node:") || specifier.startsWith("bun:")) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const specifiers = statement["specifiers"] ?? [];
|
|
236
|
+
const kept = [];
|
|
237
|
+
if (isServerSpecifier2(specifier)) {
|
|
238
|
+
referenced.push(specifier);
|
|
239
|
+
for (const item of specifiers) {
|
|
240
|
+
if (item.type === "ImportDefaultSpecifier" || item.type === "ImportNamespaceSpecifier") {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const local = item["local"]?.["name"] ?? "";
|
|
244
|
+
if (local.length > 0 && isUsed(local, retainedText)) {
|
|
245
|
+
kept.push(item);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
for (const item of specifiers) {
|
|
250
|
+
const local = item["local"]?.["name"] ?? "";
|
|
251
|
+
if (local.length > 0 && isUsed(local, retainedText)) {
|
|
252
|
+
kept.push(item);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (kept.length === 0) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const keptText = kept.map((item) => source.slice(item.start ?? 0, item.end ?? 0)).join(", ");
|
|
260
|
+
importLines.push(`import { ${keptText} } from ${JSON.stringify(specifier)};`);
|
|
261
|
+
}
|
|
262
|
+
if (needsCreateStub) {
|
|
263
|
+
importLines.push('import { __moons_createStub } from "@moonsjs/server/client";');
|
|
264
|
+
}
|
|
265
|
+
const exportLines = [];
|
|
266
|
+
for (const fn of analysis.serverFns) {
|
|
267
|
+
const input = source.slice(fn.input.start, fn.input.end);
|
|
268
|
+
const output = source.slice(fn.output.start, fn.output.end);
|
|
269
|
+
exportLines.push(`export const ${fn.name} = __moons_createStub({ hash: ${JSON.stringify(fn.hash)}, input: ${input}, output: ${output} });`);
|
|
270
|
+
}
|
|
271
|
+
for (const reExport of analysis.reExports) {
|
|
272
|
+
const target = dependencies.analyzeSpecifier(filename, reExport.source);
|
|
273
|
+
if (target === null) {
|
|
274
|
+
throw new MoonsError({
|
|
275
|
+
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
276
|
+
file: filename,
|
|
277
|
+
message: `tidak bisa me-resolve \`${reExport.source}\` untuk re-export`,
|
|
278
|
+
details: "Re-export dari `*.server.ts` harus bisa di-resolve statis (PRD §12.6).",
|
|
279
|
+
suggestions: ["Perbaiki path re-export", "Hindari re-export dinamis dari file server"]
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const targetNames = serverFnExportNames2(target, (specifier) => dependencies.analyzeSpecifier(filename, specifier));
|
|
283
|
+
const keptNames = reExport.names.filter(({ local }) => local !== "" && targetNames.has(local));
|
|
284
|
+
if (keptNames.length > 0) {
|
|
285
|
+
const specifiers = keptNames.map(({ local, exported }) => local === exported ? local : `${local} as ${exported}`).join(", ");
|
|
286
|
+
exportLines.push(`export { ${specifiers} } from ${JSON.stringify(reExport.source)};`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const specifier of analysis.exportAllFrom) {
|
|
290
|
+
const target = dependencies.analyzeSpecifier(filename, specifier);
|
|
291
|
+
if (target === null) {
|
|
292
|
+
throw new MoonsError({
|
|
293
|
+
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
294
|
+
file: filename,
|
|
295
|
+
message: `\`export *\` dari \`${specifier}\` tidak bisa di-resolve`,
|
|
296
|
+
details: "`export *` dari `*.server.ts` wajib bisa di-expand statis (PRD §12.6).",
|
|
297
|
+
suggestions: ["Perbaiki path re-export", "Ganti menjadi named re-export"]
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const targetNames = serverFnExportNames2(target, (nested) => dependencies.analyzeSpecifier(filename, nested));
|
|
301
|
+
if (targetNames.size > 0) {
|
|
302
|
+
exportLines.push(`export { ${[...targetNames].join(", ")} } from ${JSON.stringify(specifier)};`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (importLines.length === 0 && exportLines.length === 0) {
|
|
306
|
+
return { code: `// @moons: tidak ada serverFn yang di-export
|
|
307
|
+
export {};
|
|
308
|
+
`, referenced };
|
|
309
|
+
}
|
|
310
|
+
return { code: `${[...importLines, "", ...exportLines].join(`
|
|
311
|
+
`)}
|
|
312
|
+
`, referenced };
|
|
313
|
+
}
|
|
314
|
+
function injectServerFnMeta2(source, analysis, projectRoot) {
|
|
315
|
+
let output = source;
|
|
316
|
+
const inserts = [...analysis.serverFns].sort((a, b) => b.objectStart - a.objectStart);
|
|
317
|
+
for (const fn of inserts) {
|
|
318
|
+
const meta = ` __hash: ${JSON.stringify(fn.hash)},` + ` __file: ${JSON.stringify(fnRegistryKey(projectRoot, analysis.file, fn.name).split("#")[0])},` + ` __line: ${fn.line},` + ` __exportName: ${JSON.stringify(fn.name)},`;
|
|
319
|
+
const at = fn.objectStart + 1;
|
|
320
|
+
output = output.slice(0, at) + meta + output.slice(at);
|
|
321
|
+
}
|
|
322
|
+
return output;
|
|
323
|
+
}
|
|
324
|
+
async function serverFnExportNamesAsync(analysis, analyzeSpecifier, seen = new Set) {
|
|
325
|
+
const names = new Set(analysis.serverFns.map((fn) => fn.name));
|
|
326
|
+
for (const reExport of analysis.reExports) {
|
|
327
|
+
const key = `${analysis.file}\x00${reExport.source}`;
|
|
328
|
+
if (seen.has(key)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
seen.add(key);
|
|
332
|
+
const target = await analyzeSpecifier(reExport.source, analysis.file);
|
|
333
|
+
if (target === null) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const targetNames = await serverFnExportNamesAsync(target, analyzeSpecifier, seen);
|
|
337
|
+
for (const { local, exported } of reExport.names) {
|
|
338
|
+
if (local === "*" || targetNames.has(local)) {
|
|
339
|
+
names.add(exported);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const specifier of analysis.exportAllFrom) {
|
|
344
|
+
const key = `${analysis.file}\x00${specifier}`;
|
|
345
|
+
if (seen.has(key)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
seen.add(key);
|
|
349
|
+
const target = await analyzeSpecifier(specifier, analysis.file);
|
|
350
|
+
if (target === null) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
for (const name of await serverFnExportNamesAsync(target, analyzeSpecifier, seen)) {
|
|
354
|
+
names.add(name);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return names;
|
|
358
|
+
}
|
|
359
|
+
function nonContractServerFileError2(file, projectRoot) {
|
|
360
|
+
return new MoonsError({
|
|
361
|
+
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
362
|
+
file: toRelative(projectRoot, file),
|
|
363
|
+
message: "file server memakai ekstensi di luar kontrak §12",
|
|
364
|
+
details: "Transform zero-trust hanya menangani `*.server.ts`/`*.server.tsx`; file server dengan ekstensi lain tidak bisa di-stub dan tidak boleh masuk graph client.",
|
|
365
|
+
suggestions: [
|
|
366
|
+
"Ganti nama file menjadi `*.server.ts`/`*.server.tsx` dan taruh `serverFn` di dalamnya",
|
|
367
|
+
"Atau pindahkan nilai yang dibutuhkan ke file non-server"
|
|
368
|
+
]
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async function checkClientModuleImports2(context) {
|
|
372
|
+
const { file, code, projectRoot, resolve, analyze } = context;
|
|
373
|
+
const ast = parseModule(code, file);
|
|
374
|
+
const relativeFile = toRelative(projectRoot, file);
|
|
375
|
+
const resolveSafe = async (specifier, importer) => {
|
|
376
|
+
try {
|
|
377
|
+
const result = await resolve(specifier, importer);
|
|
378
|
+
if (result === null) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const clean = stripQuery2(result);
|
|
382
|
+
try {
|
|
383
|
+
return realpathSync(clean);
|
|
384
|
+
} catch {
|
|
385
|
+
return clean;
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const analyzeResolved = async (specifier, importer) => {
|
|
392
|
+
const absolute = await resolveSafe(specifier, importer);
|
|
393
|
+
return absolute === null ? null : analyze(absolute);
|
|
394
|
+
};
|
|
395
|
+
const isAllowedServerFnImport = async (resolvedTarget, specifier, name) => {
|
|
396
|
+
const target = resolvedTarget === null ? await analyzeResolved(specifier, file) : analyze(resolvedTarget);
|
|
397
|
+
if (target === null) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
const names = await serverFnExportNamesAsync(target, (nested, importer) => analyzeResolved(nested, importer));
|
|
401
|
+
return names.has(name);
|
|
402
|
+
};
|
|
403
|
+
const reject = (line, message, details) => {
|
|
404
|
+
throw new MoonsError({
|
|
405
|
+
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
406
|
+
file: relativeFile,
|
|
407
|
+
line,
|
|
408
|
+
message,
|
|
409
|
+
details,
|
|
410
|
+
suggestions: [
|
|
411
|
+
"Buat server function yang mengembalikan data yang dibutuhkan, atau",
|
|
412
|
+
"Pindahkan nilai itu ke file yang tidak berakhiran .server.ts"
|
|
413
|
+
]
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
const rejectForbidden = (line, specifier) => {
|
|
417
|
+
throw new MoonsError({
|
|
418
|
+
code: "E_FORBIDDEN_SERVER_IMPORT",
|
|
419
|
+
file: relativeFile,
|
|
420
|
+
line,
|
|
421
|
+
message: `mengimpor \`${specifier}\` di modul client`,
|
|
422
|
+
details: "Modul server-only hanya boleh di-import dari *.server.ts.",
|
|
423
|
+
suggestions: [
|
|
424
|
+
"Pindahkan operasi tersebut ke serverFn, atau",
|
|
425
|
+
"Hapus import tersebut"
|
|
426
|
+
]
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
for (const statement of topLevelStatements(ast)) {
|
|
430
|
+
if (statement.type === "ImportDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportAllDeclaration") {
|
|
431
|
+
const sourceNode = statement["source"];
|
|
432
|
+
if (sourceNode?.type !== "StringLiteral") {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const rawSpecifier = sourceNode["value"];
|
|
436
|
+
const specifier = stripQuery2(rawSpecifier);
|
|
437
|
+
const line = statement.loc?.start.line ?? 1;
|
|
438
|
+
if (specifier.startsWith("node:") || specifier.startsWith("bun:")) {
|
|
439
|
+
rejectForbidden(line, specifier);
|
|
440
|
+
}
|
|
441
|
+
const resolvedTarget = await resolveSafe(specifier, file);
|
|
442
|
+
if (resolvedTarget !== null && isNonContractServerFile2(resolvedTarget)) {
|
|
443
|
+
throw nonContractServerFileError2(resolvedTarget, projectRoot);
|
|
444
|
+
}
|
|
445
|
+
const serverBySpecifier = isServerSpecifier2(specifier);
|
|
446
|
+
const serverByResolve = resolvedTarget !== null && isServerFilePath2(resolvedTarget);
|
|
447
|
+
if (!serverBySpecifier && !serverByResolve) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const targetRelative = resolvedTarget === null ? rawSpecifier : toRelative(projectRoot, resolvedTarget);
|
|
451
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
452
|
+
if (resolvedTarget === null) {
|
|
453
|
+
reject(line, `\`export *\` dari \`${targetRelative}\` tidak bisa di-resolve`, "Re-export dari `*.server.ts` wajib bisa di-resolve statis (PRD §12.6).");
|
|
454
|
+
}
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const specifiers = statement["specifiers"] ?? [];
|
|
458
|
+
for (const item of specifiers) {
|
|
459
|
+
if (item.type === "ImportDefaultSpecifier") {
|
|
460
|
+
reject(line, `mengimpor default dari ${targetRelative}`, "Default export dari file server tidak boleh menyeberang ke client.");
|
|
461
|
+
}
|
|
462
|
+
if (item.type === "ImportNamespaceSpecifier") {
|
|
463
|
+
reject(line, `mengimpor namespace (\`* as\`) dari ${targetRelative}`, "Import namespace dari file server tidak boleh menyeberang ke client.");
|
|
464
|
+
}
|
|
465
|
+
const name = item["local"]?.["name"] ?? "";
|
|
466
|
+
const kind = item["importKind"] ?? statement["importKind"];
|
|
467
|
+
if (kind === "type") {
|
|
468
|
+
reject(line, `mengimpor type \`${name}\` dari ${targetRelative}`, "Import dari file server tidak diizinkan di modul client (PRD §12.4).");
|
|
469
|
+
}
|
|
470
|
+
if (name.length > 0 && !await isAllowedServerFnImport(resolvedTarget, specifier, name)) {
|
|
471
|
+
reject(line, `mengimpor \`${name}\` dari ${targetRelative}`, "Kode server tidak boleh muncul di client.\nHanya `serverFn` yang diizinkan menyeberang.");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const envBindings = new Set;
|
|
477
|
+
for (const statement of topLevelStatements(ast)) {
|
|
478
|
+
if (statement.type !== "ImportDeclaration") {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const sourceNode = statement["source"];
|
|
482
|
+
const specifier = sourceNode?.type === "StringLiteral" ? sourceNode["value"] : "";
|
|
483
|
+
if (specifier !== "@moonsjs/server" && !specifier.startsWith("@moonsjs/server/")) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
for (const item of statement["specifiers"] ?? []) {
|
|
487
|
+
if (item.type !== "ImportSpecifier") {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const imported = item["imported"];
|
|
491
|
+
const local = item["local"];
|
|
492
|
+
if (imported?.type === "Identifier" && imported["name"] === "env" && local?.type === "Identifier") {
|
|
493
|
+
envBindings.add(local["name"]);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (envBindings.size > 0) {
|
|
498
|
+
walk(ast, (node) => {
|
|
499
|
+
if (node.type !== "CallExpression") {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const callee = node["callee"];
|
|
503
|
+
if (callee?.type !== "Identifier" || !envBindings.has(callee["name"])) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const callText = code.slice(node.start ?? 0, node.end ?? 0).replace(/\s+/g, " ").slice(0, 80);
|
|
507
|
+
throw new MoonsError({
|
|
508
|
+
code: "E_ENV_IN_CLIENT",
|
|
509
|
+
file: relativeFile,
|
|
510
|
+
line: node.loc?.start.line ?? 1,
|
|
511
|
+
message: `memanggil \`${callText}\` di modul yang masuk client`,
|
|
512
|
+
details: "`env()` hanya legal di server.",
|
|
513
|
+
suggestions: [
|
|
514
|
+
"Pindahkan pemanggilan ke dalam serverFn, atau",
|
|
515
|
+
"Gunakan import.meta.env untuk nilai publik"
|
|
516
|
+
]
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
const dynamicRequests = [];
|
|
521
|
+
walk(ast, (node, ancestors) => {
|
|
522
|
+
const request = dynamicImportRequest(node);
|
|
523
|
+
if (request !== null) {
|
|
524
|
+
dynamicRequests.push({
|
|
525
|
+
request,
|
|
526
|
+
guarded: ancestors.some((ancestor) => isDevGuard(ancestor))
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
for (const { request, guarded } of dynamicRequests) {
|
|
531
|
+
if (guarded) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const specifier = stripQuery2(request.text);
|
|
535
|
+
const resolvedTarget = await resolveSafe(specifier, file);
|
|
536
|
+
if (resolvedTarget !== null && isNonContractServerFile2(resolvedTarget)) {
|
|
537
|
+
throw nonContractServerFileError2(resolvedTarget, projectRoot);
|
|
538
|
+
}
|
|
539
|
+
const serverBySpecifier = isServerSpecifier2(specifier);
|
|
540
|
+
const serverByResolve = resolvedTarget !== null && isServerFilePath2(resolvedTarget);
|
|
541
|
+
if (!serverBySpecifier && !serverByResolve) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const targetRelative = resolvedTarget === null ? request.text : toRelative(projectRoot, resolvedTarget);
|
|
545
|
+
reject(request.node.loc?.start.line ?? 1, `mengimpor (dynamic) \`${request.text}\` dari ${targetRelative}`, "Client tidak boleh melakukan dynamic import ke `*.server.ts` (PRD §12.7).");
|
|
546
|
+
}
|
|
547
|
+
walk(ast, (node) => {
|
|
548
|
+
if (node.type !== "CallExpression") {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const callee = node["callee"];
|
|
552
|
+
if (callee?.type !== "MemberExpression") {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const object = callee["object"];
|
|
556
|
+
const property = callee["property"];
|
|
557
|
+
if (object?.type !== "MetaProperty" || property?.type !== "Identifier" || property["name"] !== "glob") {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
for (const pattern of globPatterns((node["arguments"] ?? [])[0])) {
|
|
561
|
+
if (!isServerSpecifier2(pattern)) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
throw new MoonsError({
|
|
565
|
+
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
566
|
+
file: relativeFile,
|
|
567
|
+
line: node.loc?.start.line ?? 1,
|
|
568
|
+
message: `mengimpor (glob) \`${pattern}\` di modul client`,
|
|
569
|
+
details: "`import.meta.glob` yang mencakup `*.server.ts` dilarang di client (PRD §12.7).",
|
|
570
|
+
suggestions: [
|
|
571
|
+
"Import statis `serverFn` dari `*.server.ts` (hanya itu yang boleh menyeberang), atau",
|
|
572
|
+
"Pindahkan kebutuhan data ke dalam server function"
|
|
573
|
+
]
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function dynamicImportRequest(node) {
|
|
579
|
+
let sourceNode = null;
|
|
580
|
+
if (node.type === "ImportExpression") {
|
|
581
|
+
sourceNode = node["source"] ?? null;
|
|
582
|
+
} else if (node.type === "CallExpression") {
|
|
583
|
+
const callee = node["callee"];
|
|
584
|
+
if (callee?.type !== "Import") {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
sourceNode = (node["arguments"] ?? [])[0] ?? null;
|
|
588
|
+
}
|
|
589
|
+
if (sourceNode === null) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
if (sourceNode.type === "StringLiteral") {
|
|
593
|
+
return { text: sourceNode["value"], node: sourceNode };
|
|
594
|
+
}
|
|
595
|
+
if (sourceNode.type === "TemplateLiteral") {
|
|
596
|
+
const text = (sourceNode["quasis"] ?? []).map((quasi) => quasi["value"]?.cooked ?? "").join("*");
|
|
597
|
+
return { text, node: sourceNode };
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
function globPatterns(node) {
|
|
602
|
+
if (node?.type === "StringLiteral") {
|
|
603
|
+
return [node["value"]];
|
|
604
|
+
}
|
|
605
|
+
if (node?.type === "ArrayExpression") {
|
|
606
|
+
return (node["elements"] ?? []).filter((element) => element?.type === "StringLiteral").map((element) => element["value"]);
|
|
607
|
+
}
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
function isDevGuard(node) {
|
|
611
|
+
if (node.type !== "IfStatement") {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
const test = node["test"];
|
|
615
|
+
return isImportMetaFlag(test, "DEV") || isNegatedImportMetaFlag(test, "PROD");
|
|
616
|
+
}
|
|
617
|
+
function isImportMetaFlag(node, flag) {
|
|
618
|
+
const member = node;
|
|
619
|
+
if (member?.type !== "MemberExpression") {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const property = member["property"];
|
|
623
|
+
if (property?.type !== "Identifier" || property["name"] !== flag) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
const object = member["object"];
|
|
627
|
+
return isImportMetaEnv(object);
|
|
628
|
+
}
|
|
629
|
+
function isNegatedImportMetaFlag(node, flag) {
|
|
630
|
+
if (node?.type !== "UnaryExpression" || node["operator"] !== "!") {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
return isImportMetaFlag(node["argument"], flag);
|
|
634
|
+
}
|
|
635
|
+
function isImportMetaEnv(node) {
|
|
636
|
+
if (node?.type !== "MemberExpression") {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
const property = node["property"];
|
|
640
|
+
if (property?.type !== "Identifier" || property["name"] !== "env") {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
const object = node["object"];
|
|
644
|
+
return object?.type === "MetaProperty";
|
|
645
|
+
}
|
|
646
|
+
function toRelative(projectRoot, file) {
|
|
647
|
+
const relative = file.startsWith(projectRoot) ? file.slice(projectRoot.length).replace(/^[/\\]/, "") : file;
|
|
648
|
+
return relative.split("\\").join("/");
|
|
649
|
+
}
|
|
650
|
+
function checkRouteModuleExports2(context) {
|
|
651
|
+
const { file, code, routesDir, projectRoot } = context;
|
|
652
|
+
if (!file.startsWith(routesDir) || !file.endsWith(".tsx")) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const base = file.slice(file.lastIndexOf("/") + 1);
|
|
656
|
+
if (base.startsWith("_") && base !== "_layout.tsx" && base !== "_error.tsx" && base !== "_404.tsx") {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const allowed = base === "_layout.tsx" ? ["default", "loader"] : base === "_error.tsx" || base === "_404.tsx" ? ["default"] : ["default", "loader", "meta"];
|
|
660
|
+
const allowedText = `Hanya ${allowed.map((name) => `\`${name}\``).join(", ")} yang boleh di-export.`;
|
|
661
|
+
const relativeFile = toRelative(projectRoot, file);
|
|
662
|
+
const ast = parseModule(code, file);
|
|
663
|
+
const declarations = [];
|
|
664
|
+
let hasDefault = false;
|
|
665
|
+
for (const statement of topLevelStatements(ast)) {
|
|
666
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
667
|
+
hasDefault = true;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
671
|
+
throw new MoonsError({
|
|
672
|
+
code: "E_ROUTE_EXPORT_INVALID",
|
|
673
|
+
file: relativeFile,
|
|
674
|
+
line: statement.loc?.start.line ?? 1,
|
|
675
|
+
message: "`export *` tidak diizinkan di modul route",
|
|
676
|
+
details: allowedText,
|
|
677
|
+
suggestions: [
|
|
678
|
+
`Pindahkan isi file ke luar ${toRelative(projectRoot, routesDir)}/, atau`,
|
|
679
|
+
"Hapus `export *` tersebut"
|
|
680
|
+
]
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
if (statement.type !== "ExportNamedDeclaration") {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const declaration = statement["declaration"];
|
|
687
|
+
if (declaration !== null && declaration !== undefined) {
|
|
688
|
+
for (const name of declarationNames(declaration)) {
|
|
689
|
+
declarations.push({ name, line: declaration.loc?.start.line ?? 1 });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const item of statement["specifiers"] ?? []) {
|
|
693
|
+
const exported = item["exported"];
|
|
694
|
+
const name = exported?.type === "Identifier" ? exported["name"] : exported?.type === "StringLiteral" ? exported["value"] : null;
|
|
695
|
+
if (name !== null) {
|
|
696
|
+
declarations.push({ name, line: item.loc?.start.line ?? statement.loc?.start.line ?? 1 });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
for (const entry of declarations) {
|
|
701
|
+
if (!allowed.includes(entry.name)) {
|
|
702
|
+
throw new MoonsError({
|
|
703
|
+
code: "E_ROUTE_EXPORT_INVALID",
|
|
704
|
+
file: relativeFile,
|
|
705
|
+
line: entry.line,
|
|
706
|
+
message: `export \`${entry.name}\` tidak diizinkan di modul route`,
|
|
707
|
+
details: allowedText,
|
|
708
|
+
suggestions: [
|
|
709
|
+
`Pindahkan \`${entry.name}\` ke file di luar ${toRelative(projectRoot, routesDir)}/, atau`,
|
|
710
|
+
"Hapus export tersebut"
|
|
711
|
+
]
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (!hasDefault) {
|
|
716
|
+
throw new MoonsError({
|
|
717
|
+
code: "E_ROUTE_EXPORT_INVALID",
|
|
718
|
+
file: relativeFile,
|
|
719
|
+
message: "modul route wajib mengekspor `default`",
|
|
720
|
+
details: "Setiap file route harus punya `export default` berupa komponen React (PRD §11.2).",
|
|
721
|
+
suggestions: ["Tambahkan `export default function Page() { ... }`"]
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function declarationNames(declaration) {
|
|
726
|
+
switch (declaration.type) {
|
|
727
|
+
case "VariableDeclaration":
|
|
728
|
+
return (declaration["declarations"] ?? []).map((declarator) => declarator["id"]?.["name"] ?? "").filter((name) => name.length > 0);
|
|
729
|
+
case "FunctionDeclaration":
|
|
730
|
+
case "ClassDeclaration":
|
|
731
|
+
case "TSInterfaceDeclaration":
|
|
732
|
+
case "TSTypeAliasDeclaration":
|
|
733
|
+
case "TSEnumDeclaration":
|
|
734
|
+
return [declaration["id"]?.["name"] ?? ""].filter((name) => name.length > 0);
|
|
735
|
+
default:
|
|
736
|
+
return [];
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function scanServerFiles2(projectRoot) {
|
|
740
|
+
const excluded = new Set(["node_modules", "dist", ".moons", ".git"]);
|
|
741
|
+
const result = [];
|
|
742
|
+
const walkDir = (dir) => {
|
|
743
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
744
|
+
if (entry.isDirectory()) {
|
|
745
|
+
if (excluded.has(entry.name)) {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
walkDir(join(dir, entry.name));
|
|
749
|
+
} else if (entry.isFile() && isServerFilePath2(entry.name)) {
|
|
750
|
+
result.push(join(dir, entry.name));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
walkDir(projectRoot);
|
|
755
|
+
return result.sort();
|
|
756
|
+
}
|
|
757
|
+
function buildFnRegistry2(projectRoot, files) {
|
|
758
|
+
const registry = {};
|
|
759
|
+
for (const file of files) {
|
|
760
|
+
let source;
|
|
761
|
+
try {
|
|
762
|
+
source = readFileSync(file, "utf8");
|
|
763
|
+
} catch {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const analysis = analyzeServerFile2(source, file, projectRoot);
|
|
767
|
+
for (const fn of analysis.serverFns) {
|
|
768
|
+
registry[fnRegistryKey(projectRoot, file, fn.name)] = fn.hash;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return registry;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export { isServerFilePath2, isNonContractServerFile2, isServerSpecifier2, resolveServerSpecifierSync2, stripQuery2, analyzeServerFile2, serverFnExportNames2, renderServerFnStub2, injectServerFnMeta2, nonContractServerFileError2, checkClientModuleImports2, checkRouteModuleExports2, scanServerFiles2, buildFnRegistry2 };
|
|
775
|
+
|
|
776
|
+
//# debugId=A2B4D23CAFD2B11164756E2164756E21
|