@reckona/mreact-router 0.0.73 → 0.0.75

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.
Files changed (47) hide show
  1. package/README.md +3 -1
  2. package/dist/actions.d.ts +14 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +55 -44
  5. package/dist/actions.js.map +1 -1
  6. package/dist/build.d.ts +12 -0
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +151 -52
  9. package/dist/build.js.map +1 -1
  10. package/dist/cli-options.d.ts +13 -0
  11. package/dist/cli-options.d.ts.map +1 -1
  12. package/dist/cli-options.js +73 -0
  13. package/dist/cli-options.js.map +1 -1
  14. package/dist/cli.js +4 -1
  15. package/dist/cli.js.map +1 -1
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +17 -8
  18. package/dist/client.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/render.d.ts +2 -1
  23. package/dist/render.d.ts.map +1 -1
  24. package/dist/render.js +4 -0
  25. package/dist/render.js.map +1 -1
  26. package/dist/route-source.d.ts +8 -0
  27. package/dist/route-source.d.ts.map +1 -1
  28. package/dist/route-source.js +15 -1
  29. package/dist/route-source.js.map +1 -1
  30. package/dist/serve.d.ts.map +1 -1
  31. package/dist/serve.js +6 -0
  32. package/dist/serve.js.map +1 -1
  33. package/dist/server-action-inference.d.ts +50 -0
  34. package/dist/server-action-inference.d.ts.map +1 -0
  35. package/dist/server-action-inference.js +488 -0
  36. package/dist/server-action-inference.js.map +1 -0
  37. package/package.json +12 -11
  38. package/src/actions.ts +92 -62
  39. package/src/build.ts +231 -71
  40. package/src/cli-options.ts +103 -0
  41. package/src/cli.ts +6 -0
  42. package/src/client.ts +14 -11
  43. package/src/index.ts +1 -0
  44. package/src/render.ts +9 -0
  45. package/src/route-source.ts +25 -0
  46. package/src/serve.ts +22 -0
  47. package/src/server-action-inference.ts +804 -0
@@ -0,0 +1,804 @@
1
+ import { createHash } from "node:crypto";
2
+ import { dirname, relative, sep } from "node:path";
3
+ import {
4
+ collectFormActionExpressionReferences,
5
+ hasModuleDirective,
6
+ type FormActionExpressionReference,
7
+ } from "@reckona/mreact-compiler";
8
+ import type * as Ts from "typescript";
9
+
10
+ let ts = undefined as unknown as typeof Ts;
11
+ let typescriptLoaded = false;
12
+
13
+ export interface InferredServerActionReference {
14
+ exportName: string;
15
+ inferred: boolean;
16
+ moduleId: string;
17
+ }
18
+
19
+ export interface InferredServerActionExpressionReference
20
+ extends InferredServerActionReference {
21
+ end: number;
22
+ expression: string;
23
+ expressionEnd: number;
24
+ expressionStart: number;
25
+ sourceHash: string;
26
+ start: number;
27
+ }
28
+
29
+ export interface ServerActionInferenceDiagnostic {
30
+ code: typeof dynamicFormActionInferenceCode;
31
+ filename: string;
32
+ level: "warn";
33
+ message: string;
34
+ }
35
+
36
+ export interface RuntimeServerActionInferenceFileSystem {
37
+ isUseServerFile(file: string): Promise<boolean>;
38
+ resolveSourceFile(directory: string, source: string): Promise<string | undefined>;
39
+ }
40
+
41
+ export const dynamicFormActionInferenceCode =
42
+ "MR_SERVER_ACTION_INFERENCE_DYNAMIC_FORM_ACTION";
43
+
44
+ const dynamicFormActionInferenceMessage =
45
+ "mreact could not infer a single server action from this form action expression. Pass the action function directly or use an explicit escape hatch.";
46
+
47
+ export async function collectRuntimeInferredServerActions(options: {
48
+ appDir: string;
49
+ code: string;
50
+ fileSystem: RuntimeServerActionInferenceFileSystem;
51
+ pageFile: string;
52
+ }): Promise<{
53
+ diagnostics: ServerActionInferenceDiagnostic[];
54
+ references: Map<string, InferredServerActionReference>;
55
+ }> {
56
+ const formReferences = collectFormActionExpressionReferences({
57
+ code: options.code,
58
+ filename: options.pageFile,
59
+ });
60
+
61
+ if (formReferences.length === 0) {
62
+ return { diagnostics: [], references: new Map() };
63
+ }
64
+
65
+ await loadTypeScript();
66
+ const program = ts.createProgram({
67
+ host: createRuntimeProgramHost({
68
+ code: options.code,
69
+ compilerOptions: defaultCompilerOptions(),
70
+ pageFile: options.pageFile,
71
+ }),
72
+ options: {
73
+ allowJs: true,
74
+ jsx: ts.JsxEmit.ReactJSX,
75
+ module: ts.ModuleKind.NodeNext,
76
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
77
+ noEmit: true,
78
+ target: ts.ScriptTarget.ES2022,
79
+ },
80
+ rootNames: [options.pageFile],
81
+ });
82
+ const checker = program.getTypeChecker();
83
+ const sourceFile = program.getSourceFile(options.pageFile);
84
+
85
+ if (sourceFile === undefined) {
86
+ return { diagnostics: [], references: new Map() };
87
+ }
88
+
89
+ const diagnostics: ServerActionInferenceDiagnostic[] = [];
90
+ const references = new Map<string, InferredServerActionReference>();
91
+
92
+ for (const formReference of formReferences) {
93
+ const expression = findExpressionAt(sourceFile, formReference);
94
+
95
+ if (expression === undefined) {
96
+ continue;
97
+ }
98
+
99
+ const resolved = await resolveRuntimeActionExpression({
100
+ appDir: options.appDir,
101
+ checker,
102
+ expression,
103
+ fileSystem: options.fileSystem,
104
+ pageFile: options.pageFile,
105
+ seen: new Set(),
106
+ });
107
+
108
+ if (resolved.kind === "resolved") {
109
+ references.set(formActionOccurrenceKey(formReference), resolved.reference);
110
+ continue;
111
+ }
112
+
113
+ if (resolved.kind === "dynamic") {
114
+ diagnostics.push({
115
+ code: dynamicFormActionInferenceCode,
116
+ filename: options.pageFile,
117
+ level: "warn",
118
+ message: dynamicFormActionInferenceMessage,
119
+ });
120
+ }
121
+ }
122
+
123
+ return { diagnostics, references };
124
+ }
125
+
126
+ export async function collectBuildInferredServerActions(options: {
127
+ file: string;
128
+ files: Record<string, string>;
129
+ relativeRoutesDir: string;
130
+ resolveSourceImport: (importer: string, source: string) => string | undefined;
131
+ source: string;
132
+ }): Promise<{
133
+ diagnostics: ServerActionInferenceDiagnostic[];
134
+ references: InferredServerActionExpressionReference[];
135
+ }> {
136
+ const formReferences = collectFormActionExpressionReferences({
137
+ code: options.source,
138
+ filename: options.file,
139
+ });
140
+
141
+ if (formReferences.length === 0) {
142
+ return { diagnostics: [], references: [] };
143
+ }
144
+
145
+ await loadTypeScript();
146
+ const program = createBuildProgram(options.file, options.files, options.source);
147
+ const checker = program.getTypeChecker();
148
+ const sourceFile = program.getSourceFile(options.file);
149
+
150
+ if (sourceFile === undefined) {
151
+ return { diagnostics: [], references: [] };
152
+ }
153
+
154
+ const diagnostics: ServerActionInferenceDiagnostic[] = [];
155
+ const references: InferredServerActionExpressionReference[] = [];
156
+ const sourceHash = formActionSourceHash(options.source);
157
+
158
+ for (const formReference of formReferences) {
159
+ const expression = findExpressionAt(sourceFile, formReference);
160
+
161
+ if (expression === undefined) {
162
+ continue;
163
+ }
164
+
165
+ const resolved = resolveBuildActionExpression({
166
+ checker,
167
+ expression,
168
+ file: options.file,
169
+ files: options.files,
170
+ relativeRoutesDir: options.relativeRoutesDir,
171
+ resolveSourceImport: options.resolveSourceImport,
172
+ seen: new Set(),
173
+ });
174
+
175
+ if (resolved.kind === "resolved") {
176
+ references.push({
177
+ ...resolved.reference,
178
+ end: formReference.end,
179
+ expression: formReference.expression,
180
+ expressionEnd: formReference.expressionEnd,
181
+ expressionStart: formReference.expressionStart,
182
+ sourceHash,
183
+ start: formReference.start,
184
+ });
185
+ continue;
186
+ }
187
+
188
+ if (resolved.kind === "dynamic") {
189
+ diagnostics.push({
190
+ code: dynamicFormActionInferenceCode,
191
+ filename: options.file,
192
+ level: "warn",
193
+ message: dynamicFormActionInferenceMessage,
194
+ });
195
+ }
196
+ }
197
+
198
+ return { diagnostics, references };
199
+ }
200
+
201
+ type RuntimeResolveResult =
202
+ | { kind: "dynamic" }
203
+ | { kind: "resolved"; reference: InferredServerActionReference }
204
+ | { kind: "unresolved" };
205
+
206
+ type BuildResolveResult =
207
+ | { kind: "dynamic" }
208
+ | { kind: "resolved"; reference: InferredServerActionReference }
209
+ | { kind: "unresolved" };
210
+
211
+ async function resolveRuntimeActionExpression(options: {
212
+ appDir: string;
213
+ checker: Ts.TypeChecker;
214
+ expression: Ts.Expression;
215
+ fileSystem: RuntimeServerActionInferenceFileSystem;
216
+ pageFile: string;
217
+ seen: Set<Ts.Node>;
218
+ }): Promise<RuntimeResolveResult> {
219
+ const expression = unwrapExpression(options.expression);
220
+
221
+ if (options.seen.has(expression)) {
222
+ return { kind: "unresolved" };
223
+ }
224
+
225
+ options.seen.add(expression);
226
+
227
+ if (ts.isIdentifier(expression)) {
228
+ return await resolveRuntimeIdentifier({ ...options, expression });
229
+ }
230
+
231
+ if (ts.isPropertyAccessExpression(expression)) {
232
+ return await resolveRuntimePropertyAccess({ ...options, expression });
233
+ }
234
+
235
+ if (ts.isConditionalExpression(expression)) {
236
+ return { kind: "dynamic" };
237
+ }
238
+
239
+ return { kind: "unresolved" };
240
+ }
241
+
242
+ async function resolveRuntimeIdentifier(options: {
243
+ appDir: string;
244
+ checker: Ts.TypeChecker;
245
+ expression: Ts.Identifier;
246
+ fileSystem: RuntimeServerActionInferenceFileSystem;
247
+ pageFile: string;
248
+ seen: Set<Ts.Node>;
249
+ }): Promise<RuntimeResolveResult> {
250
+ const symbol = options.checker.getSymbolAtLocation(options.expression);
251
+
252
+ if (symbol === undefined) {
253
+ return { kind: "unresolved" };
254
+ }
255
+
256
+ for (const declaration of symbol.declarations ?? []) {
257
+ if (ts.isImportSpecifier(declaration)) {
258
+ return await referenceFromRuntimeImportSpecifier(options, declaration);
259
+ }
260
+
261
+ if (ts.isVariableDeclaration(declaration)) {
262
+ if (declaration.initializer === undefined) {
263
+ continue;
264
+ }
265
+
266
+ return await resolveRuntimeActionExpression({
267
+ ...options,
268
+ expression: declaration.initializer,
269
+ });
270
+ }
271
+ }
272
+
273
+ const aliased = aliasTargetSymbol(options.checker, symbol);
274
+
275
+ if (aliased !== undefined) {
276
+ const reference = await referenceFromRuntimeSymbolDeclaration(options, aliased);
277
+
278
+ if (reference !== undefined) {
279
+ return { kind: "resolved", reference };
280
+ }
281
+ }
282
+
283
+ return { kind: "unresolved" };
284
+ }
285
+
286
+ async function resolveRuntimePropertyAccess(options: {
287
+ appDir: string;
288
+ checker: Ts.TypeChecker;
289
+ expression: Ts.PropertyAccessExpression;
290
+ fileSystem: RuntimeServerActionInferenceFileSystem;
291
+ pageFile: string;
292
+ seen: Set<Ts.Node>;
293
+ }): Promise<RuntimeResolveResult> {
294
+ const object = unwrapExpression(options.expression.expression);
295
+
296
+ if (!ts.isIdentifier(object)) {
297
+ return { kind: "unresolved" };
298
+ }
299
+
300
+ const symbol = options.checker.getSymbolAtLocation(object);
301
+
302
+ for (const declaration of symbol?.declarations ?? []) {
303
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined) {
304
+ continue;
305
+ }
306
+
307
+ const initializer = unwrapExpression(declaration.initializer);
308
+
309
+ if (!ts.isObjectLiteralExpression(initializer)) {
310
+ continue;
311
+ }
312
+
313
+ const property = initializer.properties.find((candidate) =>
314
+ objectLiteralPropertyName(candidate) === options.expression.name.text,
315
+ );
316
+
317
+ if (property === undefined) {
318
+ continue;
319
+ }
320
+
321
+ if (ts.isShorthandPropertyAssignment(property)) {
322
+ return await resolveRuntimeSymbol(
323
+ options,
324
+ options.checker.getShorthandAssignmentValueSymbol(property),
325
+ );
326
+ }
327
+
328
+ if (ts.isPropertyAssignment(property)) {
329
+ return await resolveRuntimeActionExpression({
330
+ ...options,
331
+ expression: property.initializer,
332
+ });
333
+ }
334
+
335
+ return { kind: "dynamic" };
336
+ }
337
+
338
+ return { kind: "unresolved" };
339
+ }
340
+
341
+ async function resolveRuntimeSymbol(
342
+ options: {
343
+ appDir: string;
344
+ checker: Ts.TypeChecker;
345
+ fileSystem: RuntimeServerActionInferenceFileSystem;
346
+ pageFile: string;
347
+ },
348
+ symbol: Ts.Symbol | undefined,
349
+ ): Promise<RuntimeResolveResult> {
350
+ if (symbol === undefined) {
351
+ return { kind: "unresolved" };
352
+ }
353
+
354
+ for (const declaration of symbol.declarations ?? []) {
355
+ if (ts.isImportSpecifier(declaration)) {
356
+ return await referenceFromRuntimeImportSpecifier(options, declaration);
357
+ }
358
+ }
359
+
360
+ const aliased = aliasTargetSymbol(options.checker, symbol);
361
+
362
+ if (aliased === undefined) {
363
+ return { kind: "unresolved" };
364
+ }
365
+
366
+ const reference = await referenceFromRuntimeSymbolDeclaration(options, aliased);
367
+
368
+ return reference === undefined ? { kind: "unresolved" } : { kind: "resolved", reference };
369
+ }
370
+
371
+ async function referenceFromRuntimeImportSpecifier(
372
+ options: {
373
+ appDir: string;
374
+ fileSystem: RuntimeServerActionInferenceFileSystem;
375
+ pageFile: string;
376
+ },
377
+ declaration: Ts.ImportSpecifier,
378
+ ): Promise<RuntimeResolveResult> {
379
+ const importDeclaration = declaration.parent.parent.parent;
380
+ const source = importDeclaration.moduleSpecifier;
381
+
382
+ if (!ts.isStringLiteral(source) || !source.text.startsWith(".")) {
383
+ return { kind: "unresolved" };
384
+ }
385
+
386
+ const file = await options.fileSystem.resolveSourceFile(dirname(options.pageFile), source.text);
387
+
388
+ if (file === undefined) {
389
+ return { kind: "unresolved" };
390
+ }
391
+
392
+ return {
393
+ kind: "resolved",
394
+ reference: {
395
+ exportName: declaration.propertyName?.text ?? declaration.name.text,
396
+ inferred: !(await options.fileSystem.isUseServerFile(file)),
397
+ moduleId: moduleIdForFile(options.appDir, file),
398
+ },
399
+ };
400
+ }
401
+
402
+ async function referenceFromRuntimeSymbolDeclaration(
403
+ options: {
404
+ appDir: string;
405
+ fileSystem: RuntimeServerActionInferenceFileSystem;
406
+ pageFile: string;
407
+ },
408
+ symbol: Ts.Symbol,
409
+ ): Promise<InferredServerActionReference | undefined> {
410
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
411
+
412
+ if (declaration === undefined) {
413
+ return undefined;
414
+ }
415
+
416
+ const file = declaration.getSourceFile().fileName;
417
+
418
+ if (file === options.pageFile || !isInsideAppDir(options.appDir, file)) {
419
+ return undefined;
420
+ }
421
+
422
+ return {
423
+ exportName: symbol.getName(),
424
+ inferred: !(await options.fileSystem.isUseServerFile(file)),
425
+ moduleId: moduleIdForFile(options.appDir, file),
426
+ };
427
+ }
428
+
429
+ function resolveBuildActionExpression(options: {
430
+ checker: Ts.TypeChecker;
431
+ expression: Ts.Expression;
432
+ file: string;
433
+ files: Record<string, string>;
434
+ relativeRoutesDir: string;
435
+ resolveSourceImport: (importer: string, source: string) => string | undefined;
436
+ seen: Set<Ts.Node>;
437
+ }): BuildResolveResult {
438
+ const expression = unwrapExpression(options.expression);
439
+
440
+ if (options.seen.has(expression)) {
441
+ return { kind: "unresolved" };
442
+ }
443
+
444
+ options.seen.add(expression);
445
+
446
+ if (ts.isIdentifier(expression)) {
447
+ return resolveBuildIdentifier({ ...options, expression });
448
+ }
449
+
450
+ if (ts.isPropertyAccessExpression(expression)) {
451
+ return resolveBuildPropertyAccess({ ...options, expression });
452
+ }
453
+
454
+ if (ts.isConditionalExpression(expression)) {
455
+ return { kind: "dynamic" };
456
+ }
457
+
458
+ return { kind: "unresolved" };
459
+ }
460
+
461
+ function resolveBuildIdentifier(options: {
462
+ checker: Ts.TypeChecker;
463
+ expression: Ts.Identifier;
464
+ file: string;
465
+ files: Record<string, string>;
466
+ relativeRoutesDir: string;
467
+ resolveSourceImport: (importer: string, source: string) => string | undefined;
468
+ seen: Set<Ts.Node>;
469
+ }): BuildResolveResult {
470
+ const symbol = options.checker.getSymbolAtLocation(options.expression);
471
+
472
+ if (symbol === undefined) {
473
+ return { kind: "unresolved" };
474
+ }
475
+
476
+ for (const declaration of symbol.declarations ?? []) {
477
+ if (ts.isImportSpecifier(declaration)) {
478
+ return referenceFromBuildImportSpecifier(options, declaration);
479
+ }
480
+
481
+ if (ts.isVariableDeclaration(declaration)) {
482
+ if (declaration.initializer === undefined) {
483
+ continue;
484
+ }
485
+
486
+ return resolveBuildActionExpression({
487
+ ...options,
488
+ expression: declaration.initializer,
489
+ });
490
+ }
491
+ }
492
+
493
+ return { kind: "unresolved" };
494
+ }
495
+
496
+ function resolveBuildPropertyAccess(options: {
497
+ checker: Ts.TypeChecker;
498
+ expression: Ts.PropertyAccessExpression;
499
+ file: string;
500
+ files: Record<string, string>;
501
+ relativeRoutesDir: string;
502
+ resolveSourceImport: (importer: string, source: string) => string | undefined;
503
+ seen: Set<Ts.Node>;
504
+ }): BuildResolveResult {
505
+ const object = unwrapExpression(options.expression.expression);
506
+
507
+ if (!ts.isIdentifier(object)) {
508
+ return { kind: "unresolved" };
509
+ }
510
+
511
+ const symbol = options.checker.getSymbolAtLocation(object);
512
+
513
+ for (const declaration of symbol?.declarations ?? []) {
514
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined) {
515
+ continue;
516
+ }
517
+
518
+ const initializer = unwrapExpression(declaration.initializer);
519
+
520
+ if (!ts.isObjectLiteralExpression(initializer)) {
521
+ continue;
522
+ }
523
+
524
+ const property = initializer.properties.find((candidate) =>
525
+ objectLiteralPropertyName(candidate) === options.expression.name.text,
526
+ );
527
+
528
+ if (property === undefined) {
529
+ continue;
530
+ }
531
+
532
+ if (ts.isShorthandPropertyAssignment(property)) {
533
+ return resolveBuildSymbol(options, options.checker.getShorthandAssignmentValueSymbol(property));
534
+ }
535
+
536
+ if (ts.isPropertyAssignment(property)) {
537
+ return resolveBuildActionExpression({
538
+ ...options,
539
+ expression: property.initializer,
540
+ });
541
+ }
542
+
543
+ return { kind: "dynamic" };
544
+ }
545
+
546
+ return { kind: "unresolved" };
547
+ }
548
+
549
+ function resolveBuildSymbol(
550
+ options: {
551
+ checker: Ts.TypeChecker;
552
+ file: string;
553
+ files: Record<string, string>;
554
+ relativeRoutesDir: string;
555
+ resolveSourceImport: (importer: string, source: string) => string | undefined;
556
+ },
557
+ symbol: Ts.Symbol | undefined,
558
+ ): BuildResolveResult {
559
+ if (symbol === undefined) {
560
+ return { kind: "unresolved" };
561
+ }
562
+
563
+ for (const declaration of symbol.declarations ?? []) {
564
+ if (ts.isImportSpecifier(declaration)) {
565
+ return referenceFromBuildImportSpecifier(options, declaration);
566
+ }
567
+ }
568
+
569
+ return { kind: "unresolved" };
570
+ }
571
+
572
+ function referenceFromBuildImportSpecifier(
573
+ options: {
574
+ file: string;
575
+ files: Record<string, string>;
576
+ relativeRoutesDir: string;
577
+ resolveSourceImport: (importer: string, source: string) => string | undefined;
578
+ },
579
+ declaration: Ts.ImportSpecifier,
580
+ ): BuildResolveResult {
581
+ const importDeclaration = declaration.parent.parent.parent;
582
+ const source = importDeclaration.moduleSpecifier;
583
+
584
+ if (!ts.isStringLiteral(source) || !source.text.startsWith(".")) {
585
+ return { kind: "unresolved" };
586
+ }
587
+
588
+ const localFile = options.resolveSourceImport(options.file, source.text);
589
+
590
+ if (localFile === undefined) {
591
+ return { kind: "unresolved" };
592
+ }
593
+
594
+ const sourceCode = options.files[localFile];
595
+
596
+ if (sourceCode === undefined) {
597
+ return { kind: "unresolved" };
598
+ }
599
+
600
+ return {
601
+ kind: "resolved",
602
+ reference: {
603
+ exportName: declaration.propertyName?.text ?? declaration.name.text,
604
+ inferred: !hasUseServerDirectiveInSource({ code: sourceCode, filename: localFile }),
605
+ moduleId: moduleIdForBuildFile(localFile, options.relativeRoutesDir),
606
+ },
607
+ };
608
+ }
609
+
610
+ function aliasTargetSymbol(checker: Ts.TypeChecker, symbol: Ts.Symbol): Ts.Symbol | undefined {
611
+ if ((symbol.flags & ts.SymbolFlags.Alias) === 0) {
612
+ return undefined;
613
+ }
614
+
615
+ return checker.getAliasedSymbol(symbol);
616
+ }
617
+
618
+ function findExpressionAt(
619
+ sourceFile: Ts.SourceFile,
620
+ reference: FormActionExpressionReference,
621
+ ): Ts.Expression | undefined {
622
+ let found: Ts.Expression | undefined;
623
+
624
+ const visit = (node: Ts.Node): void => {
625
+ if (found !== undefined) {
626
+ return;
627
+ }
628
+
629
+ if (
630
+ ts.isExpression(node) &&
631
+ node.getStart(sourceFile) === reference.expressionStart &&
632
+ node.getEnd() === reference.expressionEnd
633
+ ) {
634
+ found = node;
635
+ return;
636
+ }
637
+
638
+ ts.forEachChild(node, visit);
639
+ };
640
+
641
+ visit(sourceFile);
642
+ return found;
643
+ }
644
+
645
+ function objectLiteralPropertyName(property: Ts.ObjectLiteralElementLike): string | undefined {
646
+ if (
647
+ ts.isShorthandPropertyAssignment(property) ||
648
+ ts.isPropertyAssignment(property) ||
649
+ ts.isMethodDeclaration(property)
650
+ ) {
651
+ return propertyNameText(property.name);
652
+ }
653
+
654
+ return undefined;
655
+ }
656
+
657
+ function propertyNameText(name: Ts.PropertyName): string | undefined {
658
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
659
+ return name.text;
660
+ }
661
+
662
+ return undefined;
663
+ }
664
+
665
+ function unwrapExpression(expression: Ts.Expression): Ts.Expression {
666
+ let current = expression;
667
+
668
+ while (
669
+ ts.isParenthesizedExpression(current) ||
670
+ ts.isAsExpression(current) ||
671
+ ts.isSatisfiesExpression(current) ||
672
+ ts.isTypeAssertionExpression(current) ||
673
+ ts.isNonNullExpression(current)
674
+ ) {
675
+ current = current.expression;
676
+ }
677
+
678
+ return current;
679
+ }
680
+
681
+ function moduleIdForFile(appDir: string, file: string): string {
682
+ return relative(appDir, file).split(sep).join("/");
683
+ }
684
+
685
+ function isInsideAppDir(appDir: string, file: string): boolean {
686
+ const relativePath = relative(appDir, file);
687
+
688
+ return relativePath === "" || (!relativePath.startsWith("..") && !relativePath.startsWith("/"));
689
+ }
690
+
691
+ function moduleIdForBuildFile(file: string, relativeRoutesDir: string): string {
692
+ return relativeRoutesDir === "" ? file : file.slice(relativeRoutesDir.length + 1);
693
+ }
694
+
695
+ function createBuildProgram(
696
+ file: string,
697
+ files: Record<string, string>,
698
+ source: string,
699
+ ): Ts.Program {
700
+ const compilerOptions = defaultCompilerOptions();
701
+ const defaultHost = ts.createCompilerHost(compilerOptions, true);
702
+ const host: Ts.CompilerHost = {
703
+ ...defaultHost,
704
+ fileExists: (filename) => filename === file || files[filename] !== undefined,
705
+ getSourceFile: (filename, languageVersion) => {
706
+ const fileSource = filename === file ? source : files[filename];
707
+
708
+ if (fileSource !== undefined) {
709
+ return ts.createSourceFile(filename, fileSource, languageVersion, true);
710
+ }
711
+
712
+ return defaultHost.getSourceFile(filename, languageVersion);
713
+ },
714
+ readFile: (filename) => (filename === file ? source : files[filename]),
715
+ };
716
+
717
+ return ts.createProgram({
718
+ host,
719
+ options: compilerOptions,
720
+ rootNames: [file],
721
+ });
722
+ }
723
+
724
+ function createRuntimeProgramHost(options: {
725
+ code: string;
726
+ compilerOptions: Ts.CompilerOptions;
727
+ pageFile: string;
728
+ }): Ts.CompilerHost {
729
+ const defaultHost = ts.createCompilerHost(options.compilerOptions, true);
730
+
731
+ return {
732
+ ...defaultHost,
733
+ fileExists: (filename) => filename === options.pageFile || defaultHost.fileExists(filename),
734
+ getSourceFile: (filename, languageVersion) => {
735
+ if (filename === options.pageFile) {
736
+ return ts.createSourceFile(filename, options.code, languageVersion, true);
737
+ }
738
+
739
+ return defaultHost.getSourceFile(filename, languageVersion);
740
+ },
741
+ readFile: (filename) =>
742
+ filename === options.pageFile ? options.code : defaultHost.readFile(filename),
743
+ };
744
+ }
745
+
746
+ function defaultCompilerOptions(): Ts.CompilerOptions {
747
+ return {
748
+ allowJs: true,
749
+ jsx: ts.JsxEmit.ReactJSX,
750
+ module: ts.ModuleKind.NodeNext,
751
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
752
+ noEmit: true,
753
+ target: ts.ScriptTarget.ES2022,
754
+ };
755
+ }
756
+
757
+ async function loadTypeScript(): Promise<void> {
758
+ if (typescriptLoaded) {
759
+ return;
760
+ }
761
+
762
+ ts = await import("typescript");
763
+ typescriptLoaded = true;
764
+ }
765
+
766
+ export function __readServerActionInferenceTypeScriptLoadedForTests(): boolean {
767
+ return typescriptLoaded;
768
+ }
769
+
770
+ export function __resetServerActionInferenceTypeScriptForTests(): void {
771
+ ts = undefined as unknown as typeof Ts;
772
+ typescriptLoaded = false;
773
+ }
774
+
775
+ function formActionOccurrenceKey(reference: {
776
+ end: number;
777
+ expression: string;
778
+ expressionEnd: number;
779
+ expressionStart: number;
780
+ start: number;
781
+ }): string {
782
+ return [
783
+ reference.start,
784
+ reference.end,
785
+ reference.expressionStart,
786
+ reference.expressionEnd,
787
+ reference.expression,
788
+ ].join(":");
789
+ }
790
+
791
+ function formActionSourceHash(code: string): string {
792
+ return createHash("sha256").update(code).digest("base64url");
793
+ }
794
+
795
+ export function hasUseServerDirectiveInSource(input: {
796
+ code: string;
797
+ filename: string;
798
+ }): boolean {
799
+ return hasModuleDirective({
800
+ code: input.code,
801
+ directive: "use server",
802
+ filename: input.filename,
803
+ });
804
+ }