| ... | @@ -8,7 +8,6 @@ import * as Effect from "effect/Effect"; | ... | @@ -8,7 +8,6 @@ import * as Effect from "effect/Effect"; |
| 8 | import * as FileSystem from "effect/FileSystem"; | 8 | import * as FileSystem from "effect/FileSystem"; |
| 9 | import * as Logger from "effect/Logger"; | 9 | import * as Logger from "effect/Logger"; |
| 10 | import * as Path from "effect/Path"; | 10 | import * as Path from "effect/Path"; |
| 11 | import * as PlatformError from "effect/PlatformError"; | | |
| 12 | import * as Schema from "effect/Schema"; | 11 | import * as Schema from "effect/Schema"; |
| 13 | import { Command } from "effect/unstable/cli"; | 12 | import { Command } from "effect/unstable/cli"; |
| 14 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 13 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| ... | @@ -18,12 +17,44 @@ interface NativeStaticTool { | ... | @@ -18,12 +17,44 @@ interface NativeStaticTool { |
| 18 | readonly installHint: string; | 17 | readonly installHint: string; |
| 19 | } | 18 | } |
| 20 | | 19 | |
| | 20 | const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)); |
| | 21 | |
| | 22 | export class NativeStaticCheckSourceDiscoveryError extends Schema.TaggedErrorClass<NativeStaticCheckSourceDiscoveryError>()( |
| | 23 | "NativeStaticCheckSourceDiscoveryError", |
| | 24 | { |
| | 25 | operation: Schema.Literals(["resolve-root", "read-directory", "stat-entry"]), |
| | 26 | path: Schema.String, |
| | 27 | cause: Schema.Defect(), |
| | 28 | }, |
| | 29 | ) { |
| | 30 | override get message(): string { |
| | 31 | return `Native source discovery operation '${this.operation}' failed.`; |
| | 32 | } |
| | 33 | } |
| | 34 | |
| | 35 | export class NativeStaticCheckProcessError extends Schema.TaggedErrorClass<NativeStaticCheckProcessError>()( |
| | 36 | "NativeStaticCheckProcessError", |
| | 37 | { |
| | 38 | operation: Schema.Literals(["spawn", "wait-for-exit"]), |
| | 39 | command: Schema.String, |
| | 40 | argumentCount: NonNegativeInt, |
| | 41 | cwd: Schema.String, |
| | 42 | shell: Schema.Boolean, |
| | 43 | cause: Schema.Defect(), |
| | 44 | }, |
| | 45 | ) { |
| | 46 | override get message(): string { |
| | 47 | return `Native static check process operation '${this.operation}' failed for command '${this.command}'.`; |
| | 48 | } |
| | 49 | } |
| | 50 | |
| 21 | export class NativeStaticCheckCommandError extends Schema.TaggedErrorClass<NativeStaticCheckCommandError>()( | 51 | export class NativeStaticCheckCommandError extends Schema.TaggedErrorClass<NativeStaticCheckCommandError>()( |
| 22 | "NativeStaticCheckCommandError", | 52 | "NativeStaticCheckCommandError", |
| 23 | { | 53 | { |
| 24 | command: Schema.String, | 54 | command: Schema.String, |
| 25 | args: Schema.Array(Schema.String), | 55 | argumentCount: NonNegativeInt, |
| 26 | cwd: Schema.String, | 56 | cwd: Schema.String, |
| | 57 | shell: Schema.Boolean, |
| 27 | exitCode: Schema.Int, | 58 | exitCode: Schema.Int, |
| 28 | }, | 59 | }, |
| 29 | ) { | 60 | ) { |
| ... | @@ -59,8 +90,17 @@ const excludedDirectories = new Set([ | ... | @@ -59,8 +90,17 @@ const excludedDirectories = new Set([ |
| 59 | ]); | 90 | ]); |
| 60 | const generatedNativeProjectDirectories = new Set(["android", "ios"]); | 91 | const generatedNativeProjectDirectories = new Set(["android", "ios"]); |
| 61 | | 92 | |
| | 93 | const mobileAppRootUrl = new URL("../apps/mobile", import.meta.url); |
| 62 | const appRoot = Effect.service(Path.Path).pipe( | 94 | const appRoot = Effect.service(Path.Path).pipe( |
| 63 | Effect.flatMap((path) => path.fromFileUrl(new URL("../apps/mobile", import.meta.url))), | 95 | Effect.flatMap((path) => path.fromFileUrl(mobileAppRootUrl)), |
| | 96 | Effect.mapError( |
| | 97 | (cause) => |
| | 98 | new NativeStaticCheckSourceDiscoveryError({ |
| | 99 | operation: "resolve-root", |
| | 100 | path: mobileAppRootUrl.pathname, |
| | 101 | cause, |
| | 102 | }), |
| | 103 | ), |
| 64 | ); | 104 | ); |
| 65 | | 105 | |
| 66 | const commandOutputOptions = { | 106 | const commandOutputOptions = { |
| ... | @@ -77,7 +117,7 @@ const warnMissingTool = (tool: NativeStaticTool, checkName: string) => | ... | @@ -77,7 +117,7 @@ const warnMissingTool = (tool: NativeStaticTool, checkName: string) => |
| 77 | `${tool.command} is not installed; skipping ${checkName}. Install it with '${tool.installHint}' or run 'brew bundle install --file apps/mobile/Brewfile'.`, | 117 | `${tool.command} is not installed; skipping ${checkName}. Install it with '${tool.installHint}' or run 'brew bundle install --file apps/mobile/Brewfile'.`, |
| 78 | ); | 118 | ); |
| 79 | | 119 | |
| 80 | const runCommand = Effect.fn("runCommand")(function* ( | 120 | export const runCommand = Effect.fn("runCommand")(function* ( |
| 81 | command: string, | 121 | command: string, |
| 82 | args: ReadonlyArray<string>, | 122 | args: ReadonlyArray<string>, |
| 83 | cwd: string, | 123 | cwd: string, |
| ... | @@ -85,42 +125,86 @@ const runCommand = Effect.fn("runCommand")(function* ( | ... | @@ -85,42 +125,86 @@ const runCommand = Effect.fn("runCommand")(function* ( |
| 85 | yield* Console.log(`$ ${[command, ...args].join(" ")}`); | 125 | yield* Console.log(`$ ${[command, ...args].join(" ")}`); |
| 86 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 126 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 87 | const spawnCommand = yield* resolveSpawnCommand(command, args); | 127 | const spawnCommand = yield* resolveSpawnCommand(command, args); |
| 88 | const child = yield* spawner.spawn( | 128 | const processContext = { |
| 89 | ChildProcess.make(spawnCommand.command, spawnCommand.args, { | 129 | command, |
| 90 | cwd, | 130 | argumentCount: spawnCommand.args.length, |
| 91 | ...commandOutputOptions, | 131 | cwd, |
| 92 | shell: spawnCommand.shell, | 132 | shell: spawnCommand.shell, |
| 93 | }), | 133 | } as const; |
| | 134 | const child = yield* spawner |
| | 135 | .spawn( |
| | 136 | ChildProcess.make(spawnCommand.command, spawnCommand.args, { |
| | 137 | cwd, |
| | 138 | ...commandOutputOptions, |
| | 139 | shell: spawnCommand.shell, |
| | 140 | }), |
| | 141 | ) |
| | 142 | .pipe( |
| | 143 | Effect.mapError( |
| | 144 | (cause) => |
| | 145 | new NativeStaticCheckProcessError({ |
| | 146 | ...processContext, |
| | 147 | operation: "spawn", |
| | 148 | cause, |
| | 149 | }), |
| | 150 | ), |
| | 151 | ); |
| | 152 | const exitCode = Number( |
| | 153 | yield* child.exitCode.pipe( |
| | 154 | Effect.mapError( |
| | 155 | (cause) => |
| | 156 | new NativeStaticCheckProcessError({ |
| | 157 | ...processContext, |
| | 158 | operation: "wait-for-exit", |
| | 159 | cause, |
| | 160 | }), |
| | 161 | ), |
| | 162 | ), |
| 94 | ); | 163 | ); |
| 95 | const exitCode = Number(yield* child.exitCode); | | |
| 96 | | 164 | |
| 97 | if (exitCode !== 0) { | 165 | if (exitCode !== 0) { |
| 98 | return yield* new NativeStaticCheckCommandError({ | 166 | return yield* new NativeStaticCheckCommandError({ |
| 99 | command, | 167 | ...processContext, |
| 100 | args, | | |
| 101 | cwd, | | |
| 102 | exitCode, | 168 | exitCode, |
| 103 | }); | 169 | }); |
| 104 | } | 170 | } |
| 105 | }); | 171 | }); |
| 106 | | 172 | |
| 107 | function collectSources( | 173 | export function collectSources( |
| 108 | directory: string, | 174 | directory: string, |
| 109 | root: string, | 175 | root: string, |
| 110 | ): Effect.Effect< | 176 | ): Effect.Effect< |
| 111 | ReadonlyArray<string>, | 177 | ReadonlyArray<string>, |
| 112 | PlatformError.PlatformError, | 178 | NativeStaticCheckSourceDiscoveryError, |
| 113 | FileSystem.FileSystem | Path.Path | 179 | FileSystem.FileSystem | Path.Path |
| 114 | > { | 180 | > { |
| 115 | return Effect.gen(function* () { | 181 | return Effect.gen(function* () { |
| 116 | const fs = yield* FileSystem.FileSystem; | 182 | const fs = yield* FileSystem.FileSystem; |
| 117 | const path = yield* Path.Path; | 183 | const path = yield* Path.Path; |
| 118 | const entries = yield* fs.readDirectory(directory); | 184 | const entries = yield* fs.readDirectory(directory).pipe( |
| | 185 | Effect.mapError( |
| | 186 | (cause) => |
| | 187 | new NativeStaticCheckSourceDiscoveryError({ |
| | 188 | operation: "read-directory", |
| | 189 | path: directory, |
| | 190 | cause, |
| | 191 | }), |
| | 192 | ), |
| | 193 | ); |
| 119 | const sources: Array<string> = []; | 194 | const sources: Array<string> = []; |
| 120 | | 195 | |
| 121 | for (const entry of entries) { | 196 | for (const entry of entries) { |
| 122 | const entryPath = path.join(directory, entry); | 197 | const entryPath = path.join(directory, entry); |
| 123 | const stat = yield* fs.stat(entryPath); | 198 | const stat = yield* fs.stat(entryPath).pipe( |
| | 199 | Effect.mapError( |
| | 200 | (cause) => |
| | 201 | new NativeStaticCheckSourceDiscoveryError({ |
| | 202 | operation: "stat-entry", |
| | 203 | path: entryPath, |
| | 204 | cause, |
| | 205 | }), |
| | 206 | ), |
| | 207 | ); |
| 124 | | 208 | |
| 125 | if (stat.type === "Directory") { | 209 | if (stat.type === "Directory") { |
| 126 | const isGeneratedNativeProjectDirectory = | 210 | const isGeneratedNativeProjectDirectory = |