@sshkeda/task 0.0.0-stage → 1.0.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.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/package.json +41 -4
  4. package/src/index.ts +2 -0
  5. package/src/lib/errors/index.ts +15 -0
  6. package/src/lib/errors/internal/errors.ts +147 -0
  7. package/src/lib/kill-leftover-processes/index.ts +17 -0
  8. package/src/lib/kill-leftover-processes/internal/command-words.ts +16 -0
  9. package/src/lib/kill-leftover-processes/internal/find-process-ids.ts +36 -0
  10. package/src/lib/kill-leftover-processes/internal/kill-process-groups.ts +270 -0
  11. package/src/lib/kill-leftover-processes/internal/list-linux-processes-from-proc.ts +191 -0
  12. package/src/lib/kill-leftover-processes/internal/list-macos-processes-with-sysctl.ts +335 -0
  13. package/src/lib/kill-leftover-processes/internal/list-processes.ts +58 -0
  14. package/src/lib/kill-leftover-processes/internal/mark-run-processes.ts +367 -0
  15. package/src/lib/kill-leftover-processes/internal/process-cleanup-errors.ts +17 -0
  16. package/src/lib/kill-leftover-processes/internal/send-signal.ts +32 -0
  17. package/src/lib/kill-leftover-processes/internal/signal-run-processes.ts +48 -0
  18. package/src/lib/kill-leftover-processes/internal/started-process-groups.ts +107 -0
  19. package/src/lib/main-thread/index.ts +3 -0
  20. package/src/lib/main-thread/internal/listen-for-stop-requests.ts +102 -0
  21. package/src/lib/main-thread/internal/run-main-thread.ts +81 -0
  22. package/src/lib/main-thread/internal/run-root-task-fiber.ts +191 -0
  23. package/src/lib/main-thread/internal/stops-at-first-failure.ts +20 -0
  24. package/src/lib/main-thread/internal/trace-the-run.ts +107 -0
  25. package/src/lib/main-thread/lib/find-code-each-task-depends-on/index.ts +7 -0
  26. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/follow-imports-from-entry-files.ts +306 -0
  27. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/load-all-reporting-first-failure.ts +11 -0
  28. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/make-module-graph.ts +48 -0
  29. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/read-configured-loaders.ts +64 -0
  30. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/resolve-module-specifiers.ts +60 -0
  31. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/rewrite-worker-imports.ts +118 -0
  32. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/worker-import-errors.ts +19 -0
  33. package/src/lib/main-thread/lib/run-tasks-on-workers/index.ts +7 -0
  34. package/src/lib/main-thread/lib/run-tasks-on-workers/internal/handle-worker-events.ts +348 -0
  35. package/src/lib/main-thread/lib/run-tasks-on-workers/internal/make-task-runs.ts +207 -0
  36. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/index.ts +1 -0
  37. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/await-within-counted-limit.ts +29 -0
  38. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/execute-task.ts +272 -0
  39. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/make-task-setup-clock.ts +129 -0
  40. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/run-requested-tasks.ts +135 -0
  41. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/define-task-run-state.ts +288 -0
  42. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/make-stopwatch.ts +39 -0
  43. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/share-standard-input-with-root-task.ts +36 -0
  44. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/stop-task.ts +166 -0
  45. package/src/lib/main-thread/lib/scan-module-source/index.ts +1 -0
  46. package/src/lib/main-thread/lib/scan-module-source/internal/find-lexical-bindings.ts +154 -0
  47. package/src/lib/main-thread/lib/scan-module-source/internal/find-task-declarations.ts +205 -0
  48. package/src/lib/main-thread/lib/scan-module-source/internal/parse-module-text.ts +34 -0
  49. package/src/lib/main-thread/lib/scan-module-source/internal/read-literal-string.ts +15 -0
  50. package/src/lib/main-thread/lib/scan-module-source/internal/read-module-loads.ts +316 -0
  51. package/src/lib/main-thread/lib/scan-module-source/internal/scan-module-text.ts +70 -0
  52. package/src/lib/main-thread/lib/scan-module-source/internal/unwrap-type-assertions.ts +10 -0
  53. package/src/lib/main-thread/lib/scan-module-source/internal/visit-non-reference-identifiers.ts +21 -0
  54. package/src/lib/main-thread/shared/fail-run.ts +36 -0
  55. package/src/lib/main-thread/shared/find-failure-reason.ts +43 -0
  56. package/src/lib/main-thread/shared/find-package-directories.ts +34 -0
  57. package/src/lib/main-thread/shared/find-tasks-reachable-from-root-task-file.ts +246 -0
  58. package/src/lib/main-thread/shared/list-directory-paths-up-to-root.ts +13 -0
  59. package/src/lib/main-thread/shared/main-thread-errors.ts +373 -0
  60. package/src/lib/main-thread/shared/package-json-errors.ts +93 -0
  61. package/src/lib/main-thread/shared/provide-discovered-tasks.ts +26 -0
  62. package/src/lib/main-thread/shared/provide-process-groups.ts +63 -0
  63. package/src/lib/main-thread/shared/provide-run-root-task-config.ts +66 -0
  64. package/src/lib/main-thread/shared/provide-stop-state.ts +150 -0
  65. package/src/lib/main-thread/shared/provide-worker-pool.ts +607 -0
  66. package/src/lib/main-thread/shared/read-bare-package-name.ts +3 -0
  67. package/src/lib/main-thread/shared/read-package-json.ts +149 -0
  68. package/src/lib/main-thread/shared/report-task-failure.ts +31 -0
  69. package/src/lib/preload-rewritten-imports-into-each-worker/index.ts +5 -0
  70. package/src/lib/preload-rewritten-imports-into-each-worker/internal/load-rewritten-imports.ts +34 -0
  71. package/src/lib/supervise-the-run/index.ts +15 -0
  72. package/src/lib/supervise-the-run/internal/adopt-orphans-on-linux.ts +73 -0
  73. package/src/lib/supervise-the-run/internal/check-root-task-file.ts +8 -0
  74. package/src/lib/supervise-the-run/internal/report-ready.ts +12 -0
  75. package/src/lib/supervise-the-run/internal/share-process-groups.ts +25 -0
  76. package/src/lib/supervise-the-run/internal/start-watching-the-supervisor.ts +6 -0
  77. package/src/lib/supervise-the-run/internal/supervise-run.ts +155 -0
  78. package/src/lib/supervise-the-run/internal/talk-to-the-supervisor.ts +53 -0
  79. package/src/lib/supervise-the-run/internal/watch-the-supervisor.ts +61 -0
  80. package/src/lib/task/index.ts +5 -0
  81. package/src/lib/task/internal/make-tasks.ts +82 -0
  82. package/src/lib/task/internal/run-command.ts +194 -0
  83. package/src/lib/task/internal/run-root-task.ts +263 -0
  84. package/src/lib/task/internal/run-task-lists.ts +51 -0
  85. package/src/lib/task/internal/trust-output-mode-names.ts +18 -0
  86. package/src/lib/task/internal/trust-task-results.ts +35 -0
  87. package/src/lib/task/internal/use-task-services.ts +4 -0
  88. package/src/lib/worker-thread/index.ts +3 -0
  89. package/src/lib/worker-thread/internal/answer-main-thread-messages.ts +141 -0
  90. package/src/lib/worker-thread/internal/describe-task-body-result.ts +91 -0
  91. package/src/lib/worker-thread/internal/name-config-key-not-set.ts +57 -0
  92. package/src/lib/worker-thread/internal/run-task-body.ts +117 -0
  93. package/src/lib/worker-thread/internal/serve-on-the-parent-port.ts +63 -0
  94. package/src/lib/worker-thread/internal/start-worker-thread.ts +41 -0
  95. package/src/lib/worker-thread/lib/provide-task-services/index.ts +1 -0
  96. package/src/lib/worker-thread/lib/provide-task-services/internal/make-task-logger.ts +34 -0
  97. package/src/lib/worker-thread/lib/provide-task-services/internal/provide-task-services.ts +86 -0
  98. package/src/lib/worker-thread/lib/provide-task-services/internal/read-standard-input-from-main-thread.ts +58 -0
  99. package/src/lib/worker-thread/lib/provide-task-services/internal/resolve-paths-from-package-directory.ts +61 -0
  100. package/src/lib/worker-thread/lib/provide-task-services/internal/run-commands-in-package-directory.ts +24 -0
  101. package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-commands-with-bun.ts +545 -0
  102. package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-reported-process.ts +151 -0
  103. package/src/lib/worker-thread/shared/await-main-thread-answers.ts +76 -0
  104. package/src/lib/worker-thread/shared/decode-task-result-json.ts +114 -0
  105. package/src/lib/worker-thread/shared/define-task-body-record.ts +11 -0
  106. package/src/lib/worker-thread/shared/describe-issue-input.ts +52 -0
  107. package/src/lib/worker-thread/shared/plain-object.ts +8 -0
  108. package/src/lib/worker-thread/shared/provide-worker-state.ts +160 -0
  109. package/src/lib/worker-thread/shared/worker-thread-errors.ts +41 -0
  110. package/src/shared/attribute-reported-failures.ts +25 -0
  111. package/src/shared/claim-processes-to-report.ts +22 -0
  112. package/src/shared/command-errors.ts +44 -0
  113. package/src/shared/complete-discarded-calls-once-sent.ts +12 -0
  114. package/src/shared/cut-to-graphemes.ts +12 -0
  115. package/src/shared/environment-config-errors.ts +74 -0
  116. package/src/shared/error-fields.ts +13 -0
  117. package/src/shared/find-bun-loader-for-script.ts +32 -0
  118. package/src/shared/find-node-modules-package-directory.ts +35 -0
  119. package/src/shared/find-user-locations-in-stack-traces.ts +64 -0
  120. package/src/shared/format-error-lines.ts +53 -0
  121. package/src/shared/format-task-lines.ts +21 -0
  122. package/src/shared/hold-task-logs.ts +24 -0
  123. package/src/shared/invariant-errors.ts +28 -0
  124. package/src/shared/mark-task-worker-thread-contexts.ts +29 -0
  125. package/src/shared/module-graph-errors.ts +49 -0
  126. package/src/shared/name-config-key.ts +42 -0
  127. package/src/shared/names-from-package-json.ts +7 -0
  128. package/src/shared/print-to-standard-error.ts +48 -0
  129. package/src/shared/process-errors.ts +191 -0
  130. package/src/shared/read-environment-config.ts +239 -0
  131. package/src/shared/record-tasks-made.ts +10 -0
  132. package/src/shared/report-cause.ts +140 -0
  133. package/src/shared/report-errors.ts +121 -0
  134. package/src/shared/resolve-task-options.ts +20 -0
  135. package/src/shared/rpc.ts +161 -0
  136. package/src/shared/run-in-child-fiber-and-join.ts +16 -0
  137. package/src/shared/run-marker.ts +7 -0
  138. package/src/shared/save-standard-stream-flags.ts +57 -0
  139. package/src/shared/select-cause-reasons.ts +14 -0
  140. package/src/shared/signal-run-called-twice.ts +11 -0
  141. package/src/shared/standard-input-streams.ts +41 -0
  142. package/src/shared/stop-signals.ts +41 -0
  143. package/src/shared/task-errors.ts +224 -0
  144. package/src/shared/task-package.ts +5 -0
  145. package/src/shared/task-results.ts +55 -0
  146. package/src/shared/task-services.ts +30 -0
  147. package/src/shared/write-trace-file.ts +112 -0
@@ -0,0 +1,306 @@
1
+ import { Cache, Effect, FileSystem, Option, Path } from "effect";
2
+
3
+ import {
4
+ findBunLoaderForScript,
5
+ isDataLoader,
6
+ } from "../../../../../shared/find-bun-loader-for-script.ts";
7
+ import type { makeNodeModulesPackageFinder } from "../../../../../shared/find-node-modules-package-directory.ts";
8
+ import { CodeFileUnreadable } from "../../../../../shared/module-graph-errors.ts";
9
+ import { TASK_PACKAGE } from "../../../../../shared/task-package.ts";
10
+ import { findPackageJsonDirectoryPath } from "../../../shared/find-package-directories.ts";
11
+ import { listDirectoryPathsUpToRoot } from "../../../shared/list-directory-paths-up-to-root.ts";
12
+ import { ImportNotFound } from "../../../shared/main-thread-errors.ts";
13
+ import type { ModuleRefusal } from "../../../shared/main-thread-errors.ts";
14
+ import {
15
+ PackageJsonUnreadable,
16
+ PackageJsonWithoutName,
17
+ } from "../../../shared/package-json-errors.ts";
18
+ import { readBarePackageName } from "../../../shared/read-bare-package-name.ts";
19
+ import {
20
+ decodePackageJsonName,
21
+ decodePackageJsonText,
22
+ parsePackageJsonObject,
23
+ readPackageJsonText,
24
+ toPackageJsonUnreadable,
25
+ } from "../../../shared/read-package-json.ts";
26
+ import { scanProjectModule } from "../../scan-module-source/index.ts";
27
+ import { loadAllReportingFirstFailure } from "./load-all-reporting-first-failure.ts";
28
+ import { resolveSpecifierToRealFilePath } from "./resolve-module-specifiers.ts";
29
+
30
+ const findPackageDirectoryNamed = Effect.fn("findPackageDirectoryNamed")(function* (
31
+ start: string,
32
+ name: string,
33
+ ) {
34
+ const path = yield* Path.Path;
35
+ for (const searchDirectoryPath of listDirectoryPathsUpToRoot(path, start)) {
36
+ const packageJsonFilePath = `${searchDirectoryPath}/package.json`;
37
+ const packageJsonText = yield* readPackageJsonText(packageJsonFilePath).pipe(
38
+ Effect.asSome,
39
+ Effect.catchReason("PlatformError", "NotFound", () => Effect.succeedNone),
40
+ Effect.mapError(toPackageJsonUnreadable(packageJsonFilePath)),
41
+ );
42
+ if (Option.isNone(packageJsonText)) {
43
+ continue;
44
+ }
45
+ const packageJsonObject = yield* parsePackageJsonObject(
46
+ packageJsonFilePath,
47
+ packageJsonText.value,
48
+ );
49
+ const packageName = yield* decodePackageJsonName(packageJsonFilePath, packageJsonObject);
50
+ if (Option.contains(packageName, name)) {
51
+ return searchDirectoryPath;
52
+ }
53
+ }
54
+ return null;
55
+ });
56
+
57
+ const resolveProjectImport = (
58
+ specifier: ReturnType<typeof scanProjectModule>["specifiers"][number],
59
+ importingFilePath: string,
60
+ ) =>
61
+ resolveSpecifierToRealFilePath(importingFilePath, specifier).pipe(
62
+ Effect.map(
63
+ ({
64
+ found,
65
+ realFilePath,
66
+ }): {
67
+ readonly resolvedFilePath: string | null;
68
+ readonly refusal: typeof ModuleRefusal.Type | null;
69
+ } =>
70
+ found
71
+ ? { resolvedFilePath: realFilePath, refusal: null }
72
+ : {
73
+ resolvedFilePath: null,
74
+ refusal: new ImportNotFound({ importingFilePath, specifier: specifier.value }),
75
+ },
76
+ ),
77
+ Effect.catchTag("ImportResolutionThrew", (refusal) =>
78
+ Effect.succeed({ resolvedFilePath: null, refusal }),
79
+ ),
80
+ );
81
+
82
+ const listDependencies = (
83
+ packageJson: Effect.Success<ReturnType<ReturnType<typeof decodePackageJsonText>>>,
84
+ ) =>
85
+ Object.keys({
86
+ ...packageJson.dependencies,
87
+ ...packageJson.devDependencies,
88
+ ...packageJson.peerDependencies,
89
+ ...packageJson.optionalDependencies,
90
+ });
91
+
92
+ const mayMakeTasks = (
93
+ packageJson: Effect.Success<ReturnType<ReturnType<typeof decodePackageJsonText>>>,
94
+ ) =>
95
+ packageJson.name !== TASK_PACKAGE.name &&
96
+ listDependencies(packageJson).includes(TASK_PACKAGE.name);
97
+
98
+ export const makeImportFollower = (input: {
99
+ readonly configuredLoaderOfExtension: ReadonlyMap<string, string>;
100
+ readonly taskSourceDirectoryPath: string;
101
+ readonly findNodeModulesPackageDirectoryPath: ReturnType<typeof makeNodeModulesPackageFinder>;
102
+ readonly concurrency: number;
103
+ }) =>
104
+ Effect.gen(function* () {
105
+ const fileSystem = yield* FileSystem.FileSystem;
106
+ const path = yield* Path.Path;
107
+ const { taskSourceDirectoryPath, findNodeModulesPackageDirectoryPath, concurrency } = input;
108
+
109
+ const packageJsonCache = yield* Cache.make({
110
+ capacity: Number.POSITIVE_INFINITY,
111
+ lookup: (packageDirectoryPath: string) => {
112
+ const packageJsonFilePath = path.join(packageDirectoryPath, "package.json");
113
+ return fileSystem.readFileString(packageJsonFilePath).pipe(
114
+ Effect.mapError(
115
+ (cause) => new PackageJsonUnreadable({ filePath: packageJsonFilePath, cause }),
116
+ ),
117
+ Effect.flatMap(decodePackageJsonText(packageJsonFilePath)),
118
+ Effect.flatMap((packageJson) => {
119
+ const { name } = packageJson;
120
+ return name === undefined
121
+ ? Effect.fail(new PackageJsonWithoutName({ filePath: packageJsonFilePath }))
122
+ : Effect.succeed({ ...packageJson, name });
123
+ }),
124
+ );
125
+ },
126
+ });
127
+
128
+ const packageJsonDirectoryPathCache = yield* Cache.make({
129
+ capacity: Number.POSITIVE_INFINITY,
130
+ lookup: findPackageJsonDirectoryPath,
131
+ });
132
+
133
+ const parsedModuleFileCache = yield* Cache.make({
134
+ capacity: Number.POSITIVE_INFINITY,
135
+ lookup: Effect.fn("readAndParseModuleFile")(function* (moduleFilePath: string) {
136
+ const script = findBunLoaderForScript(moduleFilePath, input.configuredLoaderOfExtension);
137
+ if (script === null) {
138
+ return null;
139
+ }
140
+ const text = yield* fileSystem
141
+ .readFileString(moduleFilePath)
142
+ .pipe(
143
+ Effect.mapError((cause) => new CodeFileUnreadable({ filePath: moduleFilePath, cause })),
144
+ );
145
+ const projectModuleScan = scanProjectModule(
146
+ moduleFilePath,
147
+ text,
148
+ script.isConfigured ? script.loader : null,
149
+ );
150
+ const resolvedSpecifiers = yield* Effect.forEach(
151
+ projectModuleScan.specifiers,
152
+ (specifier) =>
153
+ resolveProjectImport(specifier, moduleFilePath).pipe(
154
+ Effect.map((resolution) => ({ specifier, ...resolution })),
155
+ ),
156
+ { concurrency },
157
+ );
158
+ return {
159
+ text,
160
+ loader: script.loader,
161
+ resolvedSpecifiers,
162
+ madeTasks: projectModuleScan.madeTasks,
163
+ refusals: projectModuleScan.refusals.map((refusal): typeof ModuleRefusal.Type => ({
164
+ filePath: moduleFilePath,
165
+ refusal,
166
+ })),
167
+ };
168
+ }),
169
+ });
170
+
171
+ const MODULE_WITHOUT_CODE = { imports: [], madeTasks: [], refusals: [] };
172
+
173
+ const loadModule = Effect.fn("loadModule")(function* (importedModule: {
174
+ readonly filePath: string;
175
+ readonly packageDirectoryPathOutsideNodeModules: string | null;
176
+ }) {
177
+ const moduleFilePath = importedModule.filePath;
178
+ if (moduleFilePath.startsWith(taskSourceDirectoryPath)) {
179
+ return MODULE_WITHOUT_CODE;
180
+ }
181
+ const nodeModulesPackageDirectoryPath = findNodeModulesPackageDirectoryPath(moduleFilePath);
182
+ const isInstalled = nodeModulesPackageDirectoryPath !== null;
183
+ const isUnfollowedInstalledModule =
184
+ isInstalled &&
185
+ !mayMakeTasks(yield* Cache.get(packageJsonCache, nodeModulesPackageDirectoryPath));
186
+ if (isUnfollowedInstalledModule) {
187
+ return MODULE_WITHOUT_CODE;
188
+ }
189
+ const packageDirectoryPath =
190
+ nodeModulesPackageDirectoryPath ?? importedModule.packageDirectoryPathOutsideNodeModules;
191
+
192
+ const parsed = yield* Cache.get(parsedModuleFileCache, moduleFilePath);
193
+ if (parsed === null) {
194
+ return MODULE_WITHOUT_CODE;
195
+ }
196
+
197
+ const followsBarePackagesOutsideNodeModules =
198
+ !isInstalled &&
199
+ (importedModule.packageDirectoryPathOutsideNodeModules !== null ||
200
+ (yield* Cache.get(packageJsonDirectoryPathCache, path.dirname(moduleFilePath))) === null);
201
+ const findPackageDirectoryPathOutsideNodeModules = (
202
+ specifier: ReturnType<typeof scanProjectModule>["specifiers"][number],
203
+ importedFilePath: string,
204
+ ) => {
205
+ const packageName = readBarePackageName(specifier.value);
206
+ if (findNodeModulesPackageDirectoryPath(importedFilePath) !== null) {
207
+ return Effect.succeed(null);
208
+ }
209
+ const followsBarePackage =
210
+ followsBarePackagesOutsideNodeModules && packageName !== undefined;
211
+ if (followsBarePackage) {
212
+ return findPackageDirectoryNamed(path.dirname(importedFilePath), packageName);
213
+ }
214
+ const isInsideTheImportingPackage =
215
+ !isInstalled &&
216
+ packageDirectoryPath !== null &&
217
+ importedFilePath.startsWith(`${packageDirectoryPath}/`);
218
+ return Effect.succeed(isInsideTheImportingPackage ? packageDirectoryPath : null);
219
+ };
220
+ const refusals = [...parsed.refusals];
221
+ const imports: (typeof importedModule)[] = [];
222
+ for (const { specifier, resolvedFilePath, refusal } of parsed.resolvedSpecifiers) {
223
+ if (refusal !== null) {
224
+ refusals.push(refusal);
225
+ continue;
226
+ }
227
+ const isFollowedCodeImport =
228
+ resolvedFilePath !== null &&
229
+ specifier.kind !== "require.resolve" &&
230
+ !isDataLoader(specifier.loader);
231
+ if (isFollowedCodeImport) {
232
+ imports.push({
233
+ filePath: resolvedFilePath,
234
+ packageDirectoryPathOutsideNodeModules:
235
+ yield* findPackageDirectoryPathOutsideNodeModules(specifier, resolvedFilePath),
236
+ });
237
+ }
238
+ }
239
+ return { imports, madeTasks: parsed.madeTasks, refusals };
240
+ });
241
+
242
+ const walkReachableModules = Effect.fn("findReachableModules")(function* (
243
+ entryFilePath: string,
244
+ ) {
245
+ const reachable = new Map<string, Effect.Success<ReturnType<typeof loadModule>>>();
246
+ const loadWave = (wave: readonly Parameters<typeof loadModule>[0][]) =>
247
+ loadAllReportingFirstFailure(
248
+ wave,
249
+ (importedModule) =>
250
+ loadModule(importedModule).pipe(
251
+ Effect.map((module) => [importedModule.filePath, module] as const),
252
+ ),
253
+ concurrency,
254
+ );
255
+ const walkWave = (
256
+ wave: readonly Parameters<typeof loadModule>[0][],
257
+ ): Effect.Effect<
258
+ void,
259
+ Effect.Error<ReturnType<typeof loadWave>>,
260
+ Effect.Services<ReturnType<typeof loadWave>>
261
+ > =>
262
+ wave.length === 0
263
+ ? Effect.void
264
+ : loadWave(wave).pipe(
265
+ Effect.flatMap((loadedWave) => {
266
+ for (const [moduleFilePath, module] of loadedWave) {
267
+ reachable.set(moduleFilePath, module);
268
+ }
269
+ const nextWave = new Map<string, Parameters<typeof loadModule>[0]>();
270
+ for (const [, module] of loadedWave) {
271
+ for (const importedModule of module.imports) {
272
+ const queuedPackageDirectoryPath = nextWave.get(
273
+ importedModule.filePath,
274
+ )?.packageDirectoryPathOutsideNodeModules;
275
+ const importedPackageDirectoryPath =
276
+ importedModule.packageDirectoryPathOutsideNodeModules;
277
+ const isNewOrDeeperPackageImport =
278
+ !reachable.has(importedModule.filePath) &&
279
+ (queuedPackageDirectoryPath === undefined ||
280
+ queuedPackageDirectoryPath === null ||
281
+ (importedPackageDirectoryPath !== null &&
282
+ importedPackageDirectoryPath.length > queuedPackageDirectoryPath.length));
283
+ if (isNewOrDeeperPackageImport) {
284
+ nextWave.set(importedModule.filePath, importedModule);
285
+ }
286
+ }
287
+ }
288
+ return walkWave([...nextWave.values()]);
289
+ }),
290
+ );
291
+ yield* walkWave([{ filePath: entryFilePath, packageDirectoryPathOutsideNodeModules: null }]);
292
+ return reachable;
293
+ });
294
+
295
+ const reachableModulesCache = yield* Cache.make({
296
+ capacity: Number.POSITIVE_INFINITY,
297
+ lookup: walkReachableModules,
298
+ });
299
+
300
+ return {
301
+ findReachableModules: (entryFilePath: string) =>
302
+ Cache.get(reachableModulesCache, entryFilePath),
303
+ readParsedModuleFile: (moduleFilePath: string) =>
304
+ Cache.get(parsedModuleFileCache, moduleFilePath),
305
+ };
306
+ });
@@ -0,0 +1,11 @@
1
+ import { Effect, Result } from "effect";
2
+
3
+ export const loadAllReportingFirstFailure = <Item, Loaded, Failure, Requirements>(
4
+ items: readonly Item[],
5
+ load: (item: Item) => Effect.Effect<Loaded, Failure, Requirements>,
6
+ concurrency: number,
7
+ ) =>
8
+ Effect.all(
9
+ items.map((item) => load(item)),
10
+ { concurrency, mode: "result" },
11
+ ).pipe(Effect.flatMap((results) => Effect.fromResult(Result.all(results))));
@@ -0,0 +1,48 @@
1
+ import { Context, Effect, Path } from "effect";
2
+ import type { FileSystem } from "effect";
3
+
4
+ import { makeNodeModulesPackageFinder } from "../../../../../shared/find-node-modules-package-directory.ts";
5
+ import { dieOnInvariantFailure } from "../../../../../shared/invariant-errors.ts";
6
+ import { availableParallelism } from "../../../../../shared/read-environment-config.ts";
7
+ import { TASK_PACKAGE } from "../../../../../shared/task-package.ts";
8
+ import { RunRootTaskConfig } from "../../../shared/provide-run-root-task-config.ts";
9
+ import { makeImportFollower } from "./follow-imports-from-entry-files.ts";
10
+ import { readConfiguredLoaders } from "./read-configured-loaders.ts";
11
+ import { makeWorkerImportRewriter } from "./rewrite-worker-imports.ts";
12
+
13
+ export const makeModuleGraph = Effect.gen(function* () {
14
+ const path = yield* Path.Path;
15
+ const runRootTaskConfig = yield* RunRootTaskConfig;
16
+ const context = yield* Effect.context<FileSystem.FileSystem | Path.Path>();
17
+ const taskSourceDirectoryPath = yield* path
18
+ .fromFileUrl(new URL("../../../../../", import.meta.url))
19
+ .pipe(dieOnInvariantFailure("module graph", "the runtime source URL is a file URL"));
20
+ const findNodeModulesPackageDirectoryPath = makeNodeModulesPackageFinder(
21
+ runRootTaskConfig.rootTaskPackageJsonDirectoryPath,
22
+ );
23
+ const importFollower = yield* makeImportFollower({
24
+ configuredLoaderOfExtension: yield* readConfiguredLoaders({
25
+ labelCodeFile: (filePath) =>
26
+ path.relative(runRootTaskConfig.rootTaskPackageJsonDirectoryPath, filePath),
27
+ workingDirectoryPathAtStart: runRootTaskConfig.workingDirectoryPathAtStart,
28
+ }),
29
+ taskSourceDirectoryPath,
30
+ findNodeModulesPackageDirectoryPath,
31
+ concurrency: yield* availableParallelism,
32
+ });
33
+ const rewriteBareImports = makeWorkerImportRewriter(
34
+ importFollower,
35
+ findNodeModulesPackageDirectoryPath,
36
+ );
37
+ return {
38
+ rewriteBareImports: (roots: readonly string[]) =>
39
+ rewriteBareImports(roots).pipe(Effect.provide(context)),
40
+ findReachableModules: (entryFilePath: string) =>
41
+ importFollower.findReachableModules(entryFilePath).pipe(Effect.provide(context)),
42
+ };
43
+ });
44
+
45
+ export class ModuleGraph extends Context.Service<
46
+ ModuleGraph,
47
+ Effect.Success<typeof makeModuleGraph>
48
+ >()(`${TASK_PACKAGE.name}/ModuleGraph`) {}
@@ -0,0 +1,64 @@
1
+ import { Array as Arrays, Effect, FileSystem, Option, Path, Schema } from "effect";
2
+
3
+ import { BunfigUnparsable, CodeFileUnreadable } from "../../../../../shared/module-graph-errors.ts";
4
+ import { bunArguments } from "../../../../../shared/read-environment-config.ts";
5
+
6
+ const listFlagValues = (
7
+ argumentList: readonly string[],
8
+ flag: { readonly name: string; readonly takesNextArgument: boolean },
9
+ ) =>
10
+ argumentList.flatMap((argument, index) => {
11
+ if (argument.startsWith(`${flag.name}=`)) {
12
+ return [argument.slice(flag.name.length + 1)];
13
+ }
14
+ const nextArgument = argumentList[index + 1];
15
+ const isFlagFollowedByValue =
16
+ flag.takesNextArgument && argument === flag.name && nextArgument !== undefined;
17
+ return isFlagFollowedByValue ? [nextArgument] : [];
18
+ });
19
+
20
+ const decodeBunfigLoaders = Schema.decodeUnknownEffect(
21
+ Schema.Struct({ loader: Schema.optional(Schema.Record(Schema.String, Schema.String)) }),
22
+ );
23
+
24
+ export const readConfiguredLoaders = Effect.fn("readConfiguredLoaders")(function* (input: {
25
+ readonly labelCodeFile: (filePath: string) => string;
26
+ readonly workingDirectoryPathAtStart: string;
27
+ }) {
28
+ const fileSystem = yield* FileSystem.FileSystem;
29
+ const path = yield* Path.Path;
30
+ const startArguments = yield* bunArguments;
31
+ const configFilePath = Arrays.last(
32
+ listFlagValues(startArguments, { name: "--config", takesNextArgument: false }),
33
+ );
34
+ const bunfigFilePath = Option.isSome(configFilePath)
35
+ ? path.resolve(input.workingDirectoryPathAtStart, configFilePath.value)
36
+ : path.join(input.workingDirectoryPathAtStart, "bunfig.toml");
37
+ const bunfigText = yield* fileSystem.readFileString(bunfigFilePath).pipe(
38
+ Effect.asSome,
39
+ Effect.catchReason("PlatformError", "NotFound", () => Effect.succeedNone),
40
+ Effect.mapError((cause) => new CodeFileUnreadable({ filePath: bunfigFilePath, cause })),
41
+ );
42
+ const bunfigLoaders = Option.isNone(bunfigText)
43
+ ? {}
44
+ : ((yield* Effect.try(() => Bun.TOML.parse(bunfigText.value)).pipe(
45
+ Effect.flatMap(decodeBunfigLoaders),
46
+ Effect.mapError(
47
+ (cause) => new BunfigUnparsable({ filePath: input.labelCodeFile(bunfigFilePath), cause }),
48
+ ),
49
+ )).loader ?? {});
50
+ const argumentLoaders = ["--loader", "-l"]
51
+ .flatMap((name) => listFlagValues(startArguments, { name, takesNextArgument: true }))
52
+ .flatMap((extensionAndLoader) => {
53
+ const separatorIndex = extensionAndLoader.indexOf(":");
54
+ return separatorIndex === -1
55
+ ? []
56
+ : [
57
+ [
58
+ extensionAndLoader.slice(0, separatorIndex),
59
+ extensionAndLoader.slice(separatorIndex + 1),
60
+ ] as const,
61
+ ];
62
+ });
63
+ return new Map([...Object.entries(bunfigLoaders), ...argumentLoaders]);
64
+ });
@@ -0,0 +1,60 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import { Effect, FileSystem, Option, Path, Schema } from "effect";
4
+
5
+ import { ImportResolutionThrew } from "../../../../../shared/module-graph-errors.ts";
6
+ import type { scanProjectModule } from "../../scan-module-source/index.ts";
7
+
8
+ const isModuleNotFound = Schema.is(
9
+ Schema.Struct({ code: Schema.Literals(["ERR_MODULE_NOT_FOUND", "MODULE_NOT_FOUND"]) }),
10
+ );
11
+
12
+ const resolveSpecifier = (
13
+ importingFilePath: string,
14
+ specifier: Pick<ReturnType<typeof scanProjectModule>["specifiers"][number], "kind" | "value">,
15
+ ) =>
16
+ Path.Path.use((path) =>
17
+ specifier.kind === "import"
18
+ ? Effect.tryPromise({
19
+ try: () => Bun.resolve(specifier.value, path.dirname(importingFilePath)),
20
+ catch: (cause) =>
21
+ new ImportResolutionThrew({ importingFilePath, specifier: specifier.value, cause }),
22
+ })
23
+ : Effect.try({
24
+ try: () => createRequire(importingFilePath).resolve(specifier.value),
25
+ catch: (cause) =>
26
+ new ImportResolutionThrew({ importingFilePath, specifier: specifier.value, cause }),
27
+ }),
28
+ ).pipe(
29
+ Effect.asSome,
30
+ Effect.catchTag("ImportResolutionThrew", (failure) =>
31
+ isModuleNotFound(failure.cause) ? Effect.succeedNone : Effect.fail(failure),
32
+ ),
33
+ );
34
+
35
+ export const resolveSpecifierToRealFilePath = (
36
+ importingFilePath: string,
37
+ specifier: Pick<ReturnType<typeof scanProjectModule>["specifiers"][number], "kind" | "value">,
38
+ ) =>
39
+ Effect.gen(function* () {
40
+ const fileSystem = yield* FileSystem.FileSystem;
41
+ const path = yield* Path.Path;
42
+ const resolvedPath = yield* resolveSpecifier(importingFilePath, specifier);
43
+ if (Option.isNone(resolvedPath)) {
44
+ return { found: false, realFilePath: null };
45
+ }
46
+ if (!path.isAbsolute(resolvedPath.value)) {
47
+ return { found: true, realFilePath: null };
48
+ }
49
+ return {
50
+ found: true,
51
+ realFilePath: yield* fileSystem
52
+ .realPath(resolvedPath.value)
53
+ .pipe(
54
+ Effect.mapError(
55
+ (cause) =>
56
+ new ImportResolutionThrew({ importingFilePath, specifier: specifier.value, cause }),
57
+ ),
58
+ ),
59
+ };
60
+ });
@@ -0,0 +1,118 @@
1
+ import { Array as Arrays, Chunk, Effect, Schema } from "effect";
2
+
3
+ import { isDataLoader } from "../../../../../shared/find-bun-loader-for-script.ts";
4
+ import type { makeNodeModulesPackageFinder } from "../../../../../shared/find-node-modules-package-directory.ts";
5
+ import { dieOnInvariantFailure } from "../../../../../shared/invariant-errors.ts";
6
+ import { readBarePackageName } from "../../../shared/read-bare-package-name.ts";
7
+ import type { makeImportFollower } from "./follow-imports-from-entry-files.ts";
8
+ import { WorkerImportRefused } from "./worker-import-errors.ts";
9
+
10
+ const encodeQuotedString = Schema.encodeEffect(Schema.fromJsonString(Schema.String));
11
+
12
+ export const makeWorkerImportRewriter = (
13
+ importFollower: Effect.Success<ReturnType<typeof makeImportFollower>>,
14
+ findNodeModulesPackageDirectoryPath: ReturnType<typeof makeNodeModulesPackageFinder>,
15
+ ) => {
16
+ const { findReachableModules, readParsedModuleFile } = importFollower;
17
+ return Effect.fn("rewriteBareImports")(function* (roots: readonly string[]) {
18
+ const reachableModulesOfRoots = yield* Effect.forEach(roots, findReachableModules);
19
+ for (const reachableModules of reachableModulesOfRoots) {
20
+ for (const module of reachableModules.values()) {
21
+ const [firstRefusal] = module.refusals;
22
+ if (firstRefusal !== undefined) {
23
+ return yield* new WorkerImportRefused({ refusal: firstRefusal });
24
+ }
25
+ }
26
+ }
27
+ const followsPackageOutsideNodeModules = reachableModulesOfRoots.some((reachableModules) =>
28
+ [...reachableModules.values()].some((module) =>
29
+ module.imports.some(
30
+ (importedModule) => importedModule.packageDirectoryPathOutsideNodeModules !== null,
31
+ ),
32
+ ),
33
+ );
34
+ if (!followsPackageOutsideNodeModules) {
35
+ return {};
36
+ }
37
+
38
+ const visited = new Set<string>();
39
+ const pending = [...roots];
40
+ const rewriteImportsOfFile = (importingFilePath: string) =>
41
+ Effect.gen(function* () {
42
+ const parsed = yield* readParsedModuleFile(importingFilePath);
43
+ if (parsed === null) {
44
+ return [];
45
+ }
46
+ const replacements: {
47
+ readonly start: number;
48
+ readonly end: number;
49
+ readonly text: string;
50
+ }[] = [];
51
+ for (const { specifier, resolvedFilePath } of parsed.resolvedSpecifiers.toSorted(
52
+ (left, right) => right.specifier.start - left.specifier.start,
53
+ )) {
54
+ if (resolvedFilePath === null) {
55
+ continue;
56
+ }
57
+ const loadsCode = specifier.kind !== "require.resolve" && !isDataLoader(specifier.loader);
58
+ if (loadsCode) {
59
+ pending.push(resolvedFilePath);
60
+ }
61
+ const importsBarePackageOutsideNodeModules =
62
+ readBarePackageName(specifier.value) !== undefined &&
63
+ findNodeModulesPackageDirectoryPath(resolvedFilePath) === null;
64
+ if (importsBarePackageOutsideNodeModules) {
65
+ const quotedResolvedPath = yield* encodeQuotedString(resolvedFilePath).pipe(
66
+ dieOnInvariantFailure("module graph", "a resolved import path encodes to JSON text"),
67
+ );
68
+ replacements.push({
69
+ start: specifier.start,
70
+ end: specifier.end,
71
+ text: quotedResolvedPath,
72
+ });
73
+ }
74
+ }
75
+ const replacementsInTextOrder = replacements.toReversed();
76
+ const keptTextStarts = Arrays.prepend(
77
+ replacementsInTextOrder.map((replacement) => replacement.end),
78
+ 0,
79
+ );
80
+ const rewrittenText = [
81
+ ...Arrays.zipWith(
82
+ keptTextStarts,
83
+ replacementsInTextOrder,
84
+ (keptTextStart, replacement) =>
85
+ `${parsed.text.slice(keptTextStart, replacement.start)}${replacement.text}`,
86
+ ),
87
+ parsed.text.slice(Arrays.lastNonEmpty(keptTextStarts)),
88
+ ].join("");
89
+ return rewrittenText === parsed.text
90
+ ? []
91
+ : [[importingFilePath, { contents: rewrittenText, loader: parsed.loader }] as const];
92
+ });
93
+ const visitNext = (
94
+ rewrittenSoFar: Chunk.Chunk<Effect.Success<ReturnType<typeof rewriteImportsOfFile>>[number]>,
95
+ ): Effect.Effect<
96
+ typeof rewrittenSoFar,
97
+ Effect.Error<ReturnType<typeof rewriteImportsOfFile>>
98
+ > => {
99
+ const importingFilePath = pending.pop();
100
+ if (importingFilePath === undefined) {
101
+ return Effect.succeed(rewrittenSoFar);
102
+ }
103
+ const isSkippedImportingFile =
104
+ visited.has(importingFilePath) ||
105
+ findNodeModulesPackageDirectoryPath(importingFilePath) !== null;
106
+ if (isSkippedImportingFile) {
107
+ return Effect.suspend(() => visitNext(rewrittenSoFar));
108
+ }
109
+ visited.add(importingFilePath);
110
+ return rewriteImportsOfFile(importingFilePath).pipe(
111
+ Effect.flatMap((rewrittenFile) =>
112
+ visitNext(Chunk.appendAll(rewrittenSoFar, Chunk.fromIterable(rewrittenFile))),
113
+ ),
114
+ );
115
+ };
116
+ return Object.fromEntries(yield* Effect.suspend(() => visitNext(Chunk.empty())));
117
+ });
118
+ };
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+
3
+ import { TASK_ERROR_FIELDS } from "../../../../../shared/error-fields.ts";
4
+ import {
5
+ ModuleRefusal,
6
+ reportTaskDefinitionViolation,
7
+ } from "../../../shared/main-thread-errors.ts";
8
+
9
+ export class WorkerImportRefused extends Schema.TaggedError<WorkerImportRefused>()(
10
+ "WorkerImportRefused",
11
+ { ...TASK_ERROR_FIELDS, refusal: ModuleRefusal },
12
+ ) {
13
+ public get location() {
14
+ return reportTaskDefinitionViolation(this.refusal).location;
15
+ }
16
+ public override get message() {
17
+ return `could not rewrite the imports of worker modules because ${reportTaskDefinitionViolation(this.refusal).message}`;
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ import { Layer } from "effect";
2
+
3
+ import { makeTaskRuns, TaskRuns } from "./internal/make-task-runs.ts";
4
+
5
+ export { TaskRuns } from "./internal/make-task-runs.ts";
6
+
7
+ export const TaskRunsLive = Layer.effect(TaskRuns, makeTaskRuns);