@bleedingdev/modern-js-plugin-bff-extensions 3.9.0-ultramodern.10 → 3.9.0-ultramodern.12

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 (50) hide show
  1. package/README.md +7 -0
  2. package/dist/cjs/backend-federation/identity.js +23 -22
  3. package/dist/cjs/backend-federation-manifest/load.js +28 -5
  4. package/dist/cjs/backend-federation-manifest/validation.js +11 -7
  5. package/dist/cjs/cross-project-policy/node.js +8 -28
  6. package/dist/cjs/effect-adapter/cross-project-policy.js +2 -2
  7. package/dist/cjs/effect-adapter/entry.js +8 -44
  8. package/dist/cjs/effect-adapter/index.js +10 -2
  9. package/dist/cjs/effect-source-loader/built-entry.js +4 -5
  10. package/dist/cjs/effect-source-loader/loader.js +2 -1
  11. package/dist/cjs/effect-source-loader/operation-contracts.js +8 -31
  12. package/dist/cjs/effect-source-loader/paths.js +38 -10
  13. package/dist/esm/backend-federation/identity.mjs +18 -23
  14. package/dist/esm/backend-federation-manifest/load.mjs +28 -5
  15. package/dist/esm/backend-federation-manifest/validation.mjs +12 -8
  16. package/dist/esm/cross-project-policy/node.mjs +9 -19
  17. package/dist/esm/effect-adapter/cross-project-policy.mjs +2 -2
  18. package/dist/esm/effect-adapter/entry.mjs +9 -45
  19. package/dist/esm/effect-adapter/index.mjs +10 -2
  20. package/dist/esm/effect-source-loader/built-entry.mjs +5 -6
  21. package/dist/esm/effect-source-loader/loader.mjs +2 -1
  22. package/dist/esm/effect-source-loader/operation-contracts.mjs +10 -33
  23. package/dist/esm/effect-source-loader/paths.mjs +30 -11
  24. package/dist/esm-node/backend-federation/identity.mjs +18 -23
  25. package/dist/esm-node/backend-federation-manifest/load.mjs +28 -5
  26. package/dist/esm-node/backend-federation-manifest/validation.mjs +12 -8
  27. package/dist/esm-node/cross-project-policy/node.mjs +9 -19
  28. package/dist/esm-node/effect-adapter/cross-project-policy.mjs +2 -2
  29. package/dist/esm-node/effect-adapter/entry.mjs +9 -45
  30. package/dist/esm-node/effect-adapter/index.mjs +10 -2
  31. package/dist/esm-node/effect-source-loader/built-entry.mjs +5 -6
  32. package/dist/esm-node/effect-source-loader/loader.mjs +2 -1
  33. package/dist/esm-node/effect-source-loader/operation-contracts.mjs +10 -33
  34. package/dist/esm-node/effect-source-loader/paths.mjs +30 -11
  35. package/dist/types/backend-federation/identity.d.ts +3 -6
  36. package/dist/types/cross-project-policy/node.d.ts +2 -2
  37. package/dist/types/effect-adapter/cross-project-policy.d.ts +2 -1
  38. package/dist/types/effect-source-loader/paths.d.ts +20 -2
  39. package/package.json +10 -10
  40. package/src/backend-federation/identity.ts +39 -47
  41. package/src/backend-federation-manifest/load.ts +75 -27
  42. package/src/backend-federation-manifest/validation.ts +22 -29
  43. package/src/cross-project-policy/node.ts +7 -38
  44. package/src/effect-adapter/cross-project-policy.ts +7 -1
  45. package/src/effect-adapter/entry.ts +10 -88
  46. package/src/effect-adapter/index.ts +7 -1
  47. package/src/effect-source-loader/built-entry.ts +11 -9
  48. package/src/effect-source-loader/loader.ts +10 -5
  49. package/src/effect-source-loader/operation-contracts.ts +7 -41
  50. package/src/effect-source-loader/paths.ts +55 -21
@@ -10,6 +10,11 @@ import {
10
10
  type BackendFederatedEffectApiModule,
11
11
  type BackendFederationRemote,
12
12
  } from '../backend-federation';
13
+ import {
14
+ backendFederationCompatibility,
15
+ formatBackendFederationIdentityIssues,
16
+ validateBackendFederationCompatibility,
17
+ } from '../backend-federation/identity';
13
18
  import {
14
19
  assertConsistentValue,
15
20
  assertManifestAdapter,
@@ -19,7 +24,6 @@ import {
19
24
  import {
20
25
  backendFederationMetadata,
21
26
  manifestDeliveryUnit,
22
- recordField,
23
27
  stringValue,
24
28
  versionBoundaryMetadata,
25
29
  } from './metadata';
@@ -166,7 +170,7 @@ export function validateLoadedBackendFederationContract(
166
170
  const loadedContract = loaded.backendFederationContract;
167
171
  const backendFederation = backendFederationMetadata(manifest);
168
172
  const boundary = versionBoundaryMetadata(manifest);
169
- const compatibility = recordField(loadedContract, 'compatibility');
173
+ const compatibility = backendFederationCompatibility(loaded);
170
174
 
171
175
  assertManifestAdapter(
172
176
  compatibility,
@@ -186,26 +190,6 @@ export function validateLoadedBackendFederationContract(
186
190
  remote.name,
187
191
  'expose name',
188
192
  );
189
- assertVersionValue(
190
- stringValue(compatibility.contractVersion),
191
- stringValue(backendFederation?.contractVersion),
192
- 'expose contractVersion',
193
- );
194
- assertVersionValue(
195
- stringValue(compatibility.nodeAdapterVersion),
196
- stringValue(backendFederation?.nodeAdapterVersion),
197
- 'expose nodeAdapterVersion',
198
- );
199
- assertVersionValue(
200
- stringValue(compatibility.packageName),
201
- stringValue(boundary?.packageName),
202
- 'expose packageName',
203
- );
204
- assertVersionValue(
205
- stringValue(compatibility.build),
206
- stringValue(boundary?.buildVersion),
207
- 'expose buildVersion',
208
- );
209
193
 
210
194
  const deliveryUnit = manifestDeliveryUnit(backendFederation, boundary);
211
195
  const manifestUnitId =
@@ -213,15 +197,24 @@ export function validateLoadedBackendFederationContract(
213
197
  stringValue(deliveryUnit.top?.unitId);
214
198
  const manifestBuildMarker = stringValue(deliveryUnit.boundary?.buildMarker);
215
199
 
216
- assertVersionValue(
217
- stringValue(compatibility.unitId),
218
- manifestUnitId,
219
- 'deliveryUnit.unitId vs expose compatibility.unitId',
220
- );
200
+ const issues = validateBackendFederationCompatibility(compatibility, {
201
+ contractVersion: stringValue(backendFederation?.contractVersion),
202
+ nodeAdapterVersion: stringValue(backendFederation?.nodeAdapterVersion),
203
+ packageName: stringValue(boundary?.packageName),
204
+ unitId: manifestUnitId,
205
+ build: manifestBuildMarker ?? stringValue(boundary?.buildVersion),
206
+ });
207
+ // Manifest validation establishes buildVersion/buildMarker consistency; the
208
+ // executed expose is still checked independently against that trusted record.
221
209
  assertVersionValue(
222
210
  stringValue(compatibility.build),
223
- manifestBuildMarker,
224
- 'deliveryUnit.buildMarker vs expose compatibility.build',
211
+ stringValue(boundary?.buildVersion),
212
+ 'expose buildVersion',
213
+ );
214
+ assertManifestAdapter(
215
+ issues.length === 0,
216
+ 'version_mismatch',
217
+ `[BFF][Effect] ${formatBackendFederationIdentityIssues(issues)}.`,
225
218
  );
226
219
  }
227
220
 
@@ -1,48 +1,17 @@
1
- import { readFileSync } from 'node:fs';
2
- import path from 'node:path';
3
1
  import '@modern-js/server-runtime-extensions/server-config';
4
2
  import type { ServerPluginAPI } from '@modern-js/server-core';
5
3
  import {
6
- deriveOperationVersion,
7
4
  type OperationContractSource,
8
5
  resolveCrossProjectPolicy,
6
+ resolveOperationProducer,
9
7
  } from '@modern-js/server-runtime-extensions/bff-policy/node';
10
8
 
11
9
  import type { ResolvedCrossProjectPolicy } from './evaluation';
12
10
 
13
- const readNearestPackageVersion = (
14
- startDir: string | undefined,
15
- ): string | undefined => {
16
- if (!startDir) {
17
- return undefined;
18
- }
19
-
20
- let current = path.resolve(startDir);
21
- for (let depth = 0; depth < 10; depth += 1) {
22
- try {
23
- const packageJson = JSON.parse(
24
- readFileSync(path.join(current, 'package.json'), 'utf8'),
25
- ) as { version?: unknown };
26
- if (typeof packageJson.version === 'string') {
27
- return packageJson.version;
28
- }
29
- } catch {
30
- // The producer package may be nested below its package.json.
31
- }
32
-
33
- const parent = path.dirname(current);
34
- if (parent === current) {
35
- return undefined;
36
- }
37
- current = parent;
38
- }
39
-
40
- return undefined;
41
- };
42
-
43
11
  export const resolveAdapterCrossProjectPolicy = (
44
12
  api: ServerPluginAPI,
45
13
  handlers: OperationContractSource[],
14
+ producer?: ReturnType<typeof resolveOperationProducer>,
46
15
  ): ResolvedCrossProjectPolicy | undefined => {
47
16
  const bff = api.getServerConfig()?.bff;
48
17
  const { apiDirectory, appDirectory } = api.getServerContext() as {
@@ -53,11 +22,11 @@ export const resolveAdapterCrossProjectPolicy = (
53
22
  return resolveCrossProjectPolicy({
54
23
  crossProjectPolicy: bff?.crossProjectPolicy,
55
24
  handlers,
56
- requestId: bff?.requestId,
57
25
  isCrossProjectServer: bff?.isCrossProjectServer,
58
- operationVersion: deriveOperationVersion(
59
- readNearestPackageVersion(apiDirectory) ??
60
- readNearestPackageVersion(appDirectory),
61
- ),
26
+ ...(producer ??
27
+ resolveOperationProducer({
28
+ directories: [apiDirectory, appDirectory],
29
+ requestId: bff?.requestId || 'default',
30
+ })),
62
31
  });
63
32
  };
@@ -6,6 +6,7 @@ import {
6
6
  toOperationContractSources,
7
7
  } from '@modern-js/bff-effect/effect';
8
8
  import type { ServerPluginAPI } from '@modern-js/server-core';
9
+ import type { resolveOperationProducer } from '@modern-js/server-runtime-extensions/bff-policy/node';
9
10
  import { logger } from '@modern-js/utils';
10
11
  import { HttpApi } from 'effect/unstable/httpapi';
11
12
 
@@ -16,6 +17,7 @@ export function resolveEffectAdapterCrossProjectPolicy(
16
17
  api: ServerPluginAPI,
17
18
  prefix: string,
18
19
  mod: EffectApiModule | null,
20
+ producer?: ReturnType<typeof resolveOperationProducer>,
19
21
  ): Promise<ResolvedCrossProjectPolicy | undefined> {
20
22
  const contractSourcesPromise =
21
23
  mod === null
@@ -52,7 +54,11 @@ export function resolveEffectAdapterCrossProjectPolicy(
52
54
  });
53
55
 
54
56
  return contractSourcesPromise.then(contractSources => {
55
- const policy = resolveAdapterCrossProjectPolicy(api, contractSources);
57
+ const policy = resolveAdapterCrossProjectPolicy(
58
+ api,
59
+ contractSources,
60
+ producer,
61
+ );
56
62
  if (policy?.enabled === true && contractSources.length === 0) {
57
63
  logger.warn(
58
64
  '[BFF][Effect] Cross-project policy enabled but no HttpApi endpoints could be reflected; requests fail operation-contract matching unless allowUnknownOperations is enabled.',
@@ -1,96 +1,18 @@
1
1
  // @effect-diagnostics nodeBuiltinImport:off strictBooleanExpressions:off
2
-
3
2
  import path from 'node:path';
4
3
  import '@modern-js/server-runtime-extensions/server-config';
5
4
  import type { ServerPluginAPI } from '@modern-js/server-core';
6
- import { API_DIR, findExists, fs, isProd } from '@modern-js/utils';
7
-
8
- const JS_OR_TS_EXTS = [
9
- '.js',
10
- '.jsx',
11
- '.ts',
12
- '.tsx',
13
- '.mjs',
14
- '.mts',
15
- '.cjs',
16
- '.cts',
17
- ] as const;
18
-
19
- type JsOrTsExtension = (typeof JS_OR_TS_EXTS)[number];
20
-
21
- function resolveJsOrTsEntry(entryWithoutOrWithExt: string) {
22
- const extension = path.extname(entryWithoutOrWithExt) as JsOrTsExtension;
23
- if (JS_OR_TS_EXTS.includes(extension)) {
24
- return fs.existsSync(entryWithoutOrWithExt)
25
- ? entryWithoutOrWithExt
26
- : undefined;
27
- }
28
-
29
- return (
30
- findExists(JS_OR_TS_EXTS.map(ext => `${entryWithoutOrWithExt}${ext}`)) ||
31
- undefined
32
- );
33
- }
34
-
35
- function builtEntryPath(entryPath: string) {
36
- return entryPath.replace(/\.(?:[cm]?ts|tsx|jsx)$/u, '.js');
37
- }
38
-
39
- function relativeAppPath(appDirectory: string, entryPath: string) {
40
- const relativePath = path.relative(appDirectory, entryPath);
41
- return relativePath === '..' || relativePath.startsWith(`..${path.sep}`)
42
- ? undefined
43
- : relativePath;
44
- }
45
-
46
- function isDependencyPath(relativePath: string | undefined) {
47
- return relativePath?.split(/[\\/]/u).includes('node_modules') ?? false;
48
- }
5
+ import { API_DIR, isProd } from '@modern-js/utils';
6
+ import { resolveEffectEntry } from '../effect-source-loader/paths';
49
7
 
50
8
  export function resolveEffectAdapterEntryFile(api: ServerPluginAPI) {
51
9
  const { appDirectory, apiDirectory, distDirectory } = api.getServerContext();
52
- const appRoot = path.resolve(appDirectory || process.cwd());
53
- const productionRoot =
54
- isProd() && distDirectory ? path.resolve(distDirectory) : undefined;
55
- const configuredEntry = api.getServerConfig()?.bff?.effect?.entry;
56
-
57
- if (configuredEntry) {
58
- const sourceEntry = path.isAbsolute(configuredEntry)
59
- ? configuredEntry
60
- : path.resolve(appRoot, configuredEntry);
61
- if (productionRoot) {
62
- const relativeEntry = relativeAppPath(appRoot, sourceEntry);
63
- if (relativeEntry === undefined || isDependencyPath(relativeEntry)) {
64
- return resolveJsOrTsEntry(sourceEntry);
65
- }
66
- return relativeEntry
67
- ? resolveJsOrTsEntry(
68
- builtEntryPath(path.resolve(productionRoot, relativeEntry)),
69
- )
70
- : undefined;
71
- }
72
- return resolveJsOrTsEntry(sourceEntry);
73
- }
74
-
75
- const resolvedApiDirectory = path.resolve(appRoot, apiDirectory || API_DIR);
76
- const productionApiDirectory = productionRoot
77
- ? relativeAppPath(productionRoot, resolvedApiDirectory) !== undefined
78
- ? resolvedApiDirectory
79
- : (() => {
80
- const relativeApiDirectory = relativeAppPath(
81
- appRoot,
82
- resolvedApiDirectory,
83
- );
84
- if (isDependencyPath(relativeApiDirectory)) {
85
- return resolvedApiDirectory;
86
- }
87
- return relativeApiDirectory
88
- ? path.resolve(productionRoot, relativeApiDirectory)
89
- : undefined;
90
- })()
91
- : resolvedApiDirectory;
92
-
93
- return productionApiDirectory
94
- ? resolveJsOrTsEntry(path.resolve(productionApiDirectory, 'index'))
95
- : undefined;
10
+ const appDir = path.resolve(appDirectory || process.cwd());
11
+ return resolveEffectEntry({
12
+ appDir,
13
+ apiDir: path.resolve(appDir, apiDirectory || API_DIR),
14
+ effectEntry: api.getServerConfig()?.bff?.effect?.entry,
15
+ distDir:
16
+ isProd() && distDirectory ? path.resolve(distDirectory) : undefined,
17
+ }).path;
96
18
  }
@@ -10,6 +10,7 @@ import {
10
10
  useEffectContext,
11
11
  } from '@modern-js/bff-effect/effect';
12
12
  import type { Context, Next, ServerPluginAPI } from '@modern-js/server-core';
13
+ import { resolveOperationProducer } from '@modern-js/server-runtime-extensions/bff-policy/node';
13
14
  import {
14
15
  createDisposableServerRuntimeHandle,
15
16
  type DisposableServerRuntimeHandle,
@@ -161,9 +162,9 @@ export class EffectAdapter {
161
162
  return;
162
163
  }
163
164
 
165
+ const { appDirectory } = this.api.getServerContext();
164
166
  let mod: EffectApiModule;
165
167
  try {
166
- const { appDirectory } = this.api.getServerContext();
167
168
  mod = (await (isProd()
168
169
  ? loadEffectBuiltModule(entryFile)
169
170
  : loadEffectSourceModule({
@@ -177,6 +178,10 @@ export class EffectAdapter {
177
178
  throw error;
178
179
  }
179
180
 
181
+ const producer = resolveOperationProducer({
182
+ directories: [path.dirname(entryFile), appDirectory],
183
+ requestId: this.api.getServerConfig()?.bff?.requestId,
184
+ });
180
185
  const crossProjectPolicies = new Map(
181
186
  await Promise.all(
182
187
  this.prefixes.map(
@@ -187,6 +192,7 @@ export class EffectAdapter {
187
192
  this.api,
188
193
  prefix,
189
194
  mod,
195
+ producer,
190
196
  ),
191
197
  ] as const,
192
198
  ),
@@ -1,8 +1,10 @@
1
1
  import { fs, upath as path } from '@modern-js/utils';
2
2
 
3
- import { resolveEffectEntryFile } from './paths';
4
-
5
- const BUILT_ENTRY_EXT = /\.(?:[cm]?ts|tsx|jsx)$/u;
3
+ import {
4
+ emittedEffectEntry,
5
+ relativeEffectAppPath,
6
+ resolveEffectEntryFile,
7
+ } from './paths';
6
8
 
7
9
  export function resolveBuiltEffectEntry(
8
10
  appDirectory: string,
@@ -12,15 +14,15 @@ export function resolveBuiltEffectEntry(
12
14
  if (sourceEntry === undefined || sourceEntry.length === 0) {
13
15
  return undefined;
14
16
  }
15
- const relativeEntry = path.relative(appDirectory, sourceEntry);
16
- if (relativeEntry === '..' || relativeEntry.startsWith(`..${path.sep}`)) {
17
+ const relativeEntry = relativeEffectAppPath(appDirectory, sourceEntry);
18
+ if (relativeEntry === undefined) {
17
19
  throw new Error(
18
20
  `Effect BFF entry must be inside the application directory: ${sourceEntry}`,
19
21
  );
20
22
  }
21
- const builtEntry = path
22
- .resolve(distDirectory, relativeEntry)
23
- .replace(BUILT_ENTRY_EXT, '.js');
23
+ const builtEntry = emittedEffectEntry(
24
+ path.resolve(distDirectory, relativeEntry),
25
+ );
24
26
  return fs.existsSync(builtEntry) ? builtEntry : undefined;
25
27
  }
26
28
 
@@ -62,7 +64,7 @@ export function resolveEffectEntryPaths(options: {
62
64
  });
63
65
  const relativeEffectEntry =
64
66
  sourceEffectEntry !== undefined && sourceEffectEntry.length > 0
65
- ? path.relative(appDir, sourceEffectEntry).replace(BUILT_ENTRY_EXT, '.js')
67
+ ? emittedEffectEntry(path.relative(appDir, sourceEffectEntry))
66
68
  : '';
67
69
  return { sourceEffectEntry, relativeEffectEntry };
68
70
  }
@@ -125,10 +125,9 @@ function isBareSpecifier(specifier: string) {
125
125
  }
126
126
 
127
127
  /**
128
- * Keep installed registry dependencies external while bundling symlinked
129
- * workspace source packages. Registry packages remain executable from the
130
- * deployment node_modules tree; workspace TypeScript is bundled so the
131
- * generated entry is relocatable and directly executable by Node 26.7.
128
+ * Keep installed executable dependencies external to preserve runtime identity.
129
+ * Source requiring compilation is bundled even when pnpm injects a workspace
130
+ * package into node_modules: Node does not strip types inside that directory.
132
131
  */
133
132
  function externalizeInstalledDependencies(options: {
134
133
  runtimeResolveDir: string;
@@ -167,7 +166,13 @@ function externalizeInstalledDependencies(options: {
167
166
  const realPath = await fs.promises
168
167
  .realpath(resolution.path)
169
168
  .catch(() => resolution.path);
170
- if (realPath.includes(`${path.sep}node_modules${path.sep}`)) {
169
+ const loader = SOURCE_LOADERS.get(path.extname(realPath).toLowerCase());
170
+ if (
171
+ realPath.includes(`${path.sep}node_modules${path.sep}`) &&
172
+ loader !== 'ts' &&
173
+ loader !== 'tsx' &&
174
+ loader !== 'jsx'
175
+ ) {
171
176
  const runtimeResolution = await buildApi.resolve(args.path, {
172
177
  importer: '',
173
178
  kind: args.kind,
@@ -4,42 +4,11 @@ import {
4
4
  } from '@modern-js/bff-effect/effect';
5
5
  import {
6
6
  buildOperationContractMap,
7
- deriveOperationVersion,
7
+ resolveOperationProducer,
8
8
  } from '@modern-js/server-runtime-extensions/bff-policy/node';
9
- import { fs, upath as path } from '@modern-js/utils';
9
+ import { upath as path } from '@modern-js/utils';
10
10
  import { getHttpApiRuntime, loadEffectApi } from './http-api-runtime';
11
11
 
12
- function getPackageInfo(
13
- resourcePath: string,
14
- appDir: string,
15
- onDependency?: (dependency: string) => void,
16
- ): { name?: string; version?: string } {
17
- for (const startDir of [path.dirname(resourcePath), appDir]) {
18
- let current = path.resolve(startDir);
19
- for (let depth = 0; depth < 32; depth += 1) {
20
- const packageJsonPath = path.join(current, 'package.json');
21
- if (fs.existsSync(packageJsonPath)) {
22
- onDependency?.(packageJsonPath);
23
- try {
24
- const packageJson = fs.readJSONSync(packageJsonPath) as {
25
- name?: string;
26
- version?: string;
27
- };
28
- return { name: packageJson.name, version: packageJson.version };
29
- } catch {
30
- return {};
31
- }
32
- }
33
- const parent = path.dirname(current);
34
- if (parent === current) {
35
- break;
36
- }
37
- current = parent;
38
- }
39
- }
40
- return {};
41
- }
42
-
43
12
  /** Derives server policy metadata without emitting or typing a client. */
44
13
  export async function resolveEffectOperationContracts(options: {
45
14
  appDir: string;
@@ -58,15 +27,12 @@ export async function resolveEffectOperationContracts(options: {
58
27
  api,
59
28
  options.prefix,
60
29
  );
61
- const packageInfo = getPackageInfo(
62
- options.resourcePath,
63
- options.appDir,
64
- options.onDependency,
65
- );
66
- const requestId = options.requestId?.trim() || packageInfo.name || 'default';
67
30
  return buildOperationContractMap({
68
31
  handlers: toOperationContractSources(endpoints),
69
- requestId,
70
- operationVersion: deriveOperationVersion(packageInfo.version),
32
+ ...resolveOperationProducer({
33
+ directories: [path.dirname(options.resourcePath), options.appDir],
34
+ requestId: options.requestId,
35
+ onDependency: options.onDependency,
36
+ }),
71
37
  });
72
38
  }
@@ -9,32 +9,66 @@ const JS_OR_TS_EXTS = [
9
9
  '.mts',
10
10
  '.cjs',
11
11
  '.cts',
12
- ] as const;
12
+ ];
13
13
 
14
- export function resolveEffectEntryFile(options: {
14
+ export const emittedEffectEntry = (entry: string) =>
15
+ entry.replace(/\.(?:[cm]?ts|tsx|jsx)$/u, '.js');
16
+
17
+ export function relativeEffectAppPath(appDir: string, entry: string) {
18
+ const relative = path.relative(appDir, entry);
19
+ return relative === '..' ||
20
+ relative.startsWith('../') ||
21
+ relative.split('/').includes('node_modules')
22
+ ? undefined
23
+ : relative;
24
+ }
25
+
26
+ function existingEntry(entry: string): string | undefined {
27
+ return path.extname(entry)
28
+ ? fs.existsSync(entry)
29
+ ? entry
30
+ : undefined
31
+ : findExists(JS_OR_TS_EXTS.map(ext => `${entry}${ext}`)) || undefined;
32
+ }
33
+
34
+ type EffectEntryOptions = {
15
35
  appDir: string;
16
36
  apiDir: string;
17
37
  effectEntry?: string;
18
- }): string | undefined {
19
- const { appDir, apiDir, effectEntry } = options;
38
+ };
39
+ export type EffectEntry =
40
+ | { kind: 'app-source'; path: string | undefined }
41
+ | { kind: 'built-output'; path: string | undefined }
42
+ | { kind: 'external-sdk'; path: string | undefined };
20
43
 
21
- const resolveEntry = (entryWithoutExt: string) => {
22
- if (path.extname(entryWithoutExt) !== '') {
23
- return fs.existsSync(entryWithoutExt) ? entryWithoutExt : undefined;
24
- }
25
-
26
- return (
27
- findExists(JS_OR_TS_EXTS.map(ext => `${entryWithoutExt}${ext}`)) ||
28
- undefined
29
- );
30
- };
31
-
32
- if (effectEntry !== undefined && effectEntry !== '') {
33
- const entryWithoutExt = path.isAbsolute(effectEntry)
34
- ? effectEntry
35
- : path.resolve(appDir, effectEntry);
36
- return resolveEntry(entryWithoutExt);
44
+ /** Classify ownership before resolving files: production works without source. */
45
+ export function resolveEffectEntry(
46
+ options: EffectEntryOptions & { distDir?: string },
47
+ ): EffectEntry {
48
+ const { appDir, apiDir, effectEntry, distDir } = options;
49
+ const entry = path.resolve(appDir, effectEntry || path.join(apiDir, 'index'));
50
+ if (distDir && relativeEffectAppPath(distDir, entry) !== undefined) {
51
+ return {
52
+ kind: 'built-output',
53
+ path: existingEntry(emittedEffectEntry(entry)),
54
+ };
37
55
  }
56
+ const relative = relativeEffectAppPath(appDir, entry);
57
+ if (relative === undefined) {
58
+ return { kind: 'external-sdk', path: existingEntry(entry) };
59
+ }
60
+ return distDir
61
+ ? {
62
+ kind: 'built-output',
63
+ path: existingEntry(
64
+ emittedEffectEntry(path.resolve(distDir, relative)),
65
+ ),
66
+ }
67
+ : { kind: 'app-source', path: existingEntry(entry) };
68
+ }
38
69
 
39
- return resolveEntry(path.resolve(apiDir, 'index'));
70
+ export function resolveEffectEntryFile(
71
+ options: EffectEntryOptions,
72
+ ): string | undefined {
73
+ return resolveEffectEntry(options).path;
40
74
  }