@savvy-web/github-action-builder 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,7 +24,7 @@ const rootCommand = Command.make("github-action-builder").pipe(Command.withSubco
24
24
  /**
25
25
  * CLI application: reads argv from the Stdio service provided by NodeServices.
26
26
  */
27
- const cli = Command.run(rootCommand, { version: "2.1.1" });
27
+ const cli = Command.run(rootCommand, { version: "2.2.1" });
28
28
  /**
29
29
  * Combined layer: AppLayer + the Node implementations of the CLI
30
30
  * environment (FileSystem, Path, Terminal, Stdio, ChildProcessSpawner).
@@ -1,6 +1,6 @@
1
1
  import { BuildFailed, ValidationFailed } from "../../errors.js";
2
- import { BuildService } from "../../services/build.js";
3
2
  import { ConfigService } from "../../services/config.js";
3
+ import { BuildService } from "../../services/build.js";
4
4
  import { PersistLocalService } from "../../services/persist-local.js";
5
5
  import { ValidationService } from "../../services/validation.js";
6
6
  import { Console, Effect, Option } from "effect";
@@ -20,7 +20,7 @@ const forceOption = Flag.boolean("force").pipe(Flag.withAlias("f"), Flag.withDes
20
20
  * Get current package version (replaced at build time).
21
21
  */
22
22
  const getPackageVersion = () => {
23
- return "2.1.1";
23
+ return "2.2.1";
24
24
  };
25
25
  /**
26
26
  * Generate package.json content.
@@ -206,7 +206,8 @@ const writeFile = (path, content, force, createdFiles, skippedFiles) => {
206
206
  * Init command handler.
207
207
  */
208
208
  const initHandler = ({ actionName, force }) => Effect.gen(function* () {
209
- const projectDir = resolve(process.cwd(), actionName);
209
+ const cwd = process.cwd();
210
+ const projectDir = resolve(cwd, actionName);
210
211
  const createdFiles = [];
211
212
  const skippedFiles = [];
212
213
  if (existsSync(projectDir) && !force) {
package/github-action.js CHANGED
@@ -1,6 +1,6 @@
1
- import { BuildResultSchema, BuildService } from "./services/build.js";
2
1
  import { defineConfig } from "./schemas/config.js";
3
2
  import { ConfigService } from "./services/config.js";
3
+ import { BuildResultSchema, BuildService } from "./services/build.js";
4
4
  import { PersistLocalResultSchema, PersistLocalService } from "./services/persist-local.js";
5
5
  import { ValidationResultSchema, ValidationService } from "./services/validation.js";
6
6
  import { AppLayer } from "./layers/app.js";
package/index.d.ts CHANGED
@@ -925,7 +925,14 @@ declare const ConfigService_base: Context.ServiceClass<ConfigService, "ConfigSer
925
925
  *
926
926
  * @public
927
927
  */
928
- declare class ConfigService extends ConfigService_base {}
928
+ declare class ConfigService extends ConfigService_base {
929
+ /**
930
+ * Production implementation of {@link ConfigService}.
931
+ *
932
+ * @public
933
+ */
934
+ static readonly layer: Layer.Layer<ConfigService>;
935
+ }
929
936
  //#endregion
930
937
  //#region src/services/build.d.ts
931
938
  /**
@@ -1103,7 +1110,17 @@ declare const BuildService_base: Context.ServiceClass<BuildService, "BuildServic
1103
1110
  *
1104
1111
  * @public
1105
1112
  */
1106
- declare class BuildService extends BuildService_base {}
1113
+ declare class BuildService extends BuildService_base {
1114
+ /**
1115
+ * Production implementation of {@link BuildService}.
1116
+ *
1117
+ * @remarks
1118
+ * Uses rsbuild for bundling. Depends on {@link ConfigService}.
1119
+ *
1120
+ * @public
1121
+ */
1122
+ static readonly layer: Layer.Layer<BuildService, never, ConfigService>;
1123
+ }
1107
1124
  //#endregion
1108
1125
  //#region src/services/persist-local.d.ts
1109
1126
  /**
@@ -1181,7 +1198,14 @@ declare const PersistLocalService_base: Context.ServiceClass<PersistLocalService
1181
1198
  *
1182
1199
  * @public
1183
1200
  */
1184
- declare class PersistLocalService extends PersistLocalService_base {}
1201
+ declare class PersistLocalService extends PersistLocalService_base {
1202
+ /**
1203
+ * Production implementation of {@link PersistLocalService}.
1204
+ *
1205
+ * @public
1206
+ */
1207
+ static readonly layer: Layer.Layer<PersistLocalService>;
1208
+ }
1185
1209
  //#endregion
1186
1210
  //#region src/services/validation.d.ts
1187
1211
  /**
@@ -1387,7 +1411,17 @@ declare const ValidationService_base: Context.ServiceClass<ValidationService, "V
1387
1411
  *
1388
1412
  * @public
1389
1413
  */
1390
- declare class ValidationService extends ValidationService_base {}
1414
+ declare class ValidationService extends ValidationService_base {
1415
+ /**
1416
+ * Production implementation of {@link ValidationService}.
1417
+ *
1418
+ * @remarks
1419
+ * Depends on {@link ConfigService}.
1420
+ *
1421
+ * @public
1422
+ */
1423
+ static readonly layer: Layer.Layer<ValidationService, never, ConfigService>;
1424
+ }
1391
1425
  //#endregion
1392
1426
  //#region src/github-action.d.ts
1393
1427
  /**
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ActionYmlMissing, ActionYmlMissingBase, ActionYmlPathError, ActionYmlPathErrorBase, ActionYmlSchemaError, ActionYmlSchemaErrorBase, ActionYmlSyntaxError, ActionYmlSyntaxErrorBase, BuildFailed, BuildFailedBase, BundleFailed, BundleFailedBase, CleanError, CleanErrorBase, ConfigInvalid, ConfigInvalidBase, ConfigLoadFailed, ConfigLoadFailedBase, ConfigNotFound, ConfigNotFoundBase, EntryFileMissing, EntryFileMissingBase, MainEntryMissing, MainEntryMissingBase, PersistLocalError, PersistLocalErrorBase, ValidationFailed, ValidationFailedBase, WorkerEntryInvalidName, WorkerEntryInvalidNameBase, WorkerEntryMissing, WorkerEntryMissingBase, WriteError, WriteErrorBase } from "./errors.js";
2
- import { BuildResultSchema, BuildRunnerOptionsSchema, BuildService, BundleResultSchema, BundleStatsSchema } from "./services/build.js";
3
2
  import { BuildOptionsSchema, ConfigInputSchema, ConfigSchema, EntriesSchema, PersistLocalOptionsSchema, ValidationOptionsSchema, defineConfig } from "./schemas/config.js";
4
3
  import { ConfigService, DetectEntriesResultSchema, DetectedEntrySchema, LoadConfigOptionsSchema } from "./services/config.js";
4
+ import { BuildResultSchema, BuildRunnerOptionsSchema, BuildService, BundleResultSchema, BundleStatsSchema } from "./services/build.js";
5
5
  import { PersistLocalResultSchema, PersistLocalRunnerOptionsSchema, PersistLocalService } from "./services/persist-local.js";
6
6
  import { ActionYmlResultSchema, ValidateOptionsSchema, ValidationErrorSchema, ValidationResultSchema, ValidationService, ValidationWarningSchema } from "./services/validation.js";
7
7
  import { AppLayer, BuildLayer, ConfigLayer, PersistLocalLayer, ValidationLayer } from "./layers/app.js";
package/layers/app.js CHANGED
@@ -1,7 +1,7 @@
1
- import { BuildServiceLive } from "../services/build-live.js";
2
- import { ConfigServiceLive } from "../services/config-live.js";
3
- import { PersistLocalServiceLive } from "../services/persist-local-live.js";
4
- import { ValidationServiceLive } from "../services/validation-live.js";
1
+ import { ConfigService } from "../services/config.js";
2
+ import { BuildService } from "../services/build.js";
3
+ import { PersistLocalService } from "../services/persist-local.js";
4
+ import { ValidationService } from "../services/validation.js";
5
5
  import { Layer } from "effect";
6
6
 
7
7
  //#region src/layers/app.ts
@@ -21,7 +21,7 @@ import { Layer } from "effect";
21
21
  *
22
22
  * @public
23
23
  */
24
- const ConfigLayer = ConfigServiceLive;
24
+ const ConfigLayer = ConfigService.layer;
25
25
  /**
26
26
  * Layer providing ValidationService (depends on ConfigService).
27
27
  *
@@ -30,7 +30,7 @@ const ConfigLayer = ConfigServiceLive;
30
30
  *
31
31
  * @public
32
32
  */
33
- const ValidationLayer = ValidationServiceLive.pipe(Layer.provide(ConfigServiceLive));
33
+ const ValidationLayer = ValidationService.layer.pipe(Layer.provide(ConfigService.layer));
34
34
  /**
35
35
  * Layer providing BuildService (depends on ConfigService).
36
36
  *
@@ -39,7 +39,7 @@ const ValidationLayer = ValidationServiceLive.pipe(Layer.provide(ConfigServiceLi
39
39
  *
40
40
  * @public
41
41
  */
42
- const BuildLayer = BuildServiceLive.pipe(Layer.provide(ConfigServiceLive));
42
+ const BuildLayer = BuildService.layer.pipe(Layer.provide(ConfigService.layer));
43
43
  /**
44
44
  * Layer providing PersistLocalService (no dependencies).
45
45
  *
@@ -48,7 +48,7 @@ const BuildLayer = BuildServiceLive.pipe(Layer.provide(ConfigServiceLive));
48
48
  *
49
49
  * @public
50
50
  */
51
- const PersistLocalLayer = PersistLocalServiceLive;
51
+ const PersistLocalLayer = PersistLocalService.layer;
52
52
  /**
53
53
  * Combined layer providing all services.
54
54
  *
@@ -77,7 +77,7 @@ const PersistLocalLayer = PersistLocalServiceLive;
77
77
  *
78
78
  * @public
79
79
  */
80
- const AppLayer = Layer.mergeAll(ConfigServiceLive, ValidationLayer, BuildLayer, PersistLocalLayer);
80
+ const AppLayer = Layer.mergeAll(ConfigService.layer, ValidationLayer, BuildLayer, PersistLocalLayer);
81
81
 
82
82
  //#endregion
83
83
  export { AppLayer, BuildLayer, ConfigLayer, PersistLocalLayer, ValidationLayer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/github-action-builder",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
4
4
  "private": false,
5
5
  "description": "A zero-config build tool for creating GitHub Actions from TypeScript. Bundles with rsbuild, validates action.yml against GitHub's schema, and outputs production-ready Node.js 24 actions.",
6
6
  "keywords": [
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@effect/platform-node": "4.0.0-beta.101",
52
52
  "@effected/yaml": "^0.6.0",
53
- "@rsbuild/core": "^2.1.8",
53
+ "@rsbuild/core": "^2.1.9",
54
54
  "effect": "4.0.0-beta.101",
55
55
  "jiti": "^2.7.0",
56
56
  "picocolors": "^1.1.1"
package/services/build.js CHANGED
@@ -1,7 +1,24 @@
1
+ import { BundleFailed, CleanError, WriteError } from "../errors.js";
1
2
  import { OptionalPathLikeSchema } from "../schemas/path.js";
2
- import { Context, Schema } from "effect";
3
+ import { ConfigService } from "./config.js";
4
+ import { buildNativeDynamicImportRules } from "./native-dynamic-imports.js";
5
+ import { Context, Effect, Layer, Result, Schema } from "effect";
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
+ import { basename, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { createRsbuild } from "@rsbuild/core";
3
10
 
4
11
  //#region src/services/build.ts
12
+ /* v8 ignore start - build service requires actual bundling for integration testing */
13
+ /**
14
+ * BuildService - Effect service for building GitHub Actions.
15
+ *
16
+ * @remarks
17
+ * Provides bundling and build orchestration capabilities
18
+ * using Effect's service pattern with Context.Service.
19
+ *
20
+ * @internal
21
+ */
5
22
  /**
6
23
  * Options for the build process.
7
24
  * @public
@@ -53,6 +70,216 @@ const BuildResultSchema = Schema.Struct({
53
70
  error: Schema.optional(Schema.String)
54
71
  });
55
72
  /**
73
+ * Format bytes as a human-readable string.
74
+ */
75
+ function formatBytes(bytes) {
76
+ if (bytes < 1024) return `${bytes} B`;
77
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
78
+ return `${(bytes / 1048576).toFixed(1)} MB`;
79
+ }
80
+ /**
81
+ * Format build result for terminal output.
82
+ */
83
+ /* v8 ignore start - formatting function tested via integration */
84
+ function formatBuildResult(result) {
85
+ const lines = [];
86
+ if (result.success) {
87
+ lines.push("Build Summary:");
88
+ for (const entry of result.entries) if (entry.success && entry.stats) {
89
+ const { entry: name, size, duration, outputPath } = entry.stats;
90
+ lines.push(` ✓ ${name}: ${formatBytes(size)} (${duration}ms) → ${outputPath}`);
91
+ }
92
+ lines.push(`\nTotal time: ${result.duration}ms`);
93
+ } else {
94
+ lines.push("Build Failed:");
95
+ for (const entry of result.entries) if (!entry.success) lines.push(` ✗ ${entry.error}`);
96
+ }
97
+ return lines.join("\n");
98
+ }
99
+ /* v8 ignore stop */
100
+ /**
101
+ * Source of the stub module that replaces packages listed in `build.ignore`.
102
+ * It is bundled in place of the real module and throws if ever loaded.
103
+ */
104
+ const IGNORE_STUB_SOURCE = `throw new Error("A module excluded via the build 'ignore' option was loaded at runtime.");\n`;
105
+ /**
106
+ * Self-referencing specifier for the `webpackIgnore`-injecting loader
107
+ * shipped from `public/loaders/webpack-ignore-dynamic-imports.cjs` (see
108
+ * `package.json` `exports`). Resolved through the package's own `exports`
109
+ * map via `import.meta.resolve`, which stays correct whether this module is
110
+ * running from `src` (the map points at `./public/loaders/...`) or from a
111
+ * built `dist` (the map points at the flattened `./loaders/...`, since the
112
+ * `public/` copy step drops the `public/` prefix both on disk and in the
113
+ * built manifest) — no relative-path assumption needed either way.
114
+ */
115
+ const WEBPACK_IGNORE_LOADER_SPECIFIER = "@savvy-web/github-action-builder/loaders/webpack-ignore-dynamic-imports.cjs";
116
+ /**
117
+ * Resolve the absolute on-disk path to the `webpackIgnore`-injecting loader.
118
+ */
119
+ function resolveWebpackIgnoreLoaderPath() {
120
+ return fileURLToPath(import.meta.resolve(WEBPACK_IGNORE_LOADER_SPECIFIER));
121
+ }
122
+ /**
123
+ * Clean output directory.
124
+ */
125
+ function cleanDirectory(dir) {
126
+ return Effect.try({
127
+ try: () => {
128
+ if (existsSync(dir)) rmSync(dir, {
129
+ recursive: true,
130
+ force: true
131
+ });
132
+ },
133
+ /* v8 ignore next 5 - error branch requires fs permission failures */
134
+ catch: (error) => new CleanError({
135
+ directory: dir,
136
+ cause: error
137
+ })
138
+ });
139
+ }
140
+ /**
141
+ * Write file with directory creation.
142
+ */
143
+ function writeFile(path, content) {
144
+ return Effect.try({
145
+ try: () => {
146
+ const dir = resolve(path, "..");
147
+ mkdirSync(dir, { recursive: true });
148
+ writeFileSync(path, content, "utf8");
149
+ },
150
+ /* v8 ignore next 5 - error branch requires fs permission failures */
151
+ catch: (error) => new WriteError({
152
+ path,
153
+ cause: error
154
+ })
155
+ });
156
+ }
157
+ /**
158
+ * Fold an extracted `*.LICENSE.txt` sidecar back into its bundle.
159
+ *
160
+ * `legalComments: "linked"` is the only mode whose extraction actually sees
161
+ * bundled license banners — the "inline" mode's SWC comment-preservation path
162
+ * never receives them and silently drops attribution (verified against
163
+ * rsbuild 2.1.8). A committed action still must not carry sidecar files
164
+ * (issue #94), so the sidecar's verbatim comment blocks replace the
165
+ * `LICENSE:` reference banner in the bundle and the sidecar is deleted —
166
+ * attribution inline, no extra dist file.
167
+ */
168
+ function inlineLicenseSidecar(outputPath) {
169
+ return Effect.try({
170
+ try: () => {
171
+ const sidecarPath = `${outputPath}.LICENSE.txt`;
172
+ if (!existsSync(sidecarPath)) return;
173
+ const licenses = readFileSync(sidecarPath, "utf8").trim();
174
+ const bundle = readFileSync(outputPath, "utf8");
175
+ const reference = `/*! LICENSE: ${basename(sidecarPath)} */`;
176
+ const afterReference = bundle.startsWith(reference) && bundle.charAt(reference.length) === "\n" ? reference.length + 1 : bundle.startsWith(reference) ? reference.length : 0;
177
+ const folded = `${licenses}\n${bundle.slice(afterReference)}`;
178
+ writeFileSync(outputPath, folded, "utf8");
179
+ unlinkSync(sidecarPath);
180
+ },
181
+ /* v8 ignore next 5 - error branch requires fs permission failures */
182
+ catch: (error) => new WriteError({
183
+ path: outputPath,
184
+ cause: error
185
+ })
186
+ });
187
+ }
188
+ /**
189
+ * Bundle a single entry with rsbuild.
190
+ */
191
+ /* v8 ignore start - bundling requires actual rsbuild execution */
192
+ function bundleEntry(entry, config, cwd) {
193
+ return Effect.gen(function* () {
194
+ const startTime = Date.now();
195
+ const outputDir = resolve(cwd, "dist");
196
+ const externalsSet = new Set(config.build.externals);
197
+ const ignoreSet = new Set(config.build.ignore);
198
+ const ignoreAlias = {};
199
+ // webpackIgnore-injecting loader below leaves those calls as native
200
+ const nativeDynamicImportRules = config.build.nativeDynamicImports.length > 0 ? buildNativeDynamicImportRules(config.build.nativeDynamicImports, resolveWebpackIgnoreLoaderPath()) : [];
201
+ if (config.build.ignore.length > 0) {
202
+ const stubPath = resolve(cwd, "node_modules", ".cache", "github-action-builder", "ignore-stub.mjs");
203
+ yield* writeFile(stubPath, IGNORE_STUB_SOURCE);
204
+ for (const moduleName of config.build.ignore) ignoreAlias[`${moduleName}$`] = stubPath;
205
+ }
206
+ const rsbuild = yield* Effect.tryPromise({
207
+ try: () => createRsbuild({ rsbuildConfig: {
208
+ mode: "production",
209
+ source: { entry: { [entry.type]: entry.path } },
210
+ resolve: { alias: ignoreAlias },
211
+ output: {
212
+ target: "node",
213
+ module: true,
214
+ distPath: { root: outputDir },
215
+ filename: { js: "[name].js" },
216
+ externals: (data) => {
217
+ const request = data.request;
218
+ if (!request) return false;
219
+ if (request.startsWith("node:")) return `node-commonjs ${request}`;
220
+ if (externalsSet.has(request) && !ignoreSet.has(request)) return request;
221
+ return false;
222
+ },
223
+ cleanDistPath: false,
224
+ legalComments: "linked",
225
+ minify: config.build.minify,
226
+ sourceMap: config.build.sourceMap ? { js: "source-map" } : false
227
+ },
228
+ performance: { chunkSplit: { strategy: "all-in-one" } },
229
+ tools: { rspack: {
230
+ node: {
231
+ __dirname: "node-module",
232
+ __filename: "node-module"
233
+ },
234
+ module: {
235
+ parser: { javascript: { importMeta: false } },
236
+ // webpackIgnore-injecting loader (empty when the option is unset).
237
+ rules: nativeDynamicImportRules
238
+ },
239
+ output: { asyncChunks: false }
240
+ } }
241
+ } }),
242
+ catch: (error) => new BundleFailed({
243
+ entry: entry.path,
244
+ cause: error
245
+ })
246
+ });
247
+ const buildResult = yield* Effect.tryPromise({
248
+ try: () => rsbuild.build(),
249
+ catch: (error) => new BundleFailed({
250
+ entry: entry.path,
251
+ cause: error
252
+ })
253
+ });
254
+ yield* Effect.tryPromise({
255
+ try: () => buildResult.close(),
256
+ catch: (error) => new BundleFailed({
257
+ entry: entry.path,
258
+ cause: /* @__PURE__ */ new Error(`rsbuild close() failed: ${error}`)
259
+ })
260
+ });
261
+ const outputPath = resolve(outputDir, `${entry.type}.js`);
262
+ yield* inlineLicenseSidecar(outputPath);
263
+ const size = yield* Effect.try({
264
+ try: () => statSync(outputPath).size,
265
+ catch: (error) => new BundleFailed({
266
+ entry: entry.path,
267
+ cause: error
268
+ })
269
+ });
270
+ const duration = Date.now() - startTime;
271
+ return {
272
+ success: true,
273
+ stats: {
274
+ entry: entry.type,
275
+ size,
276
+ duration,
277
+ outputPath: entry.output
278
+ }
279
+ };
280
+ });
281
+ }
282
+ /**
56
283
  * BuildService key for dependency injection.
57
284
  *
58
285
  * @example Using BuildService with Effect
@@ -77,7 +304,64 @@ const BuildResultSchema = Schema.Struct({
77
304
  *
78
305
  * @public
79
306
  */
80
- var BuildService = class extends Context.Service()("BuildService") {};
307
+ var BuildService = class extends Context.Service()("BuildService") {
308
+ /**
309
+ * Production implementation of {@link BuildService}.
310
+ *
311
+ * @remarks
312
+ * Uses rsbuild for bundling. Depends on {@link ConfigService}.
313
+ *
314
+ * @public
315
+ */
316
+ static layer = Layer.effect(this, Effect.gen(function* () {
317
+ const configService = yield* ConfigService;
318
+ return {
319
+ /* v8 ignore start - build execution requires actual rsbuild bundling */
320
+ build: (config, options = {}) => Effect.gen(function* () {
321
+ const cwd = options.cwd ?? process.cwd();
322
+ const shouldClean = options.clean ?? true;
323
+ const startTime = Date.now();
324
+ const entriesConfig = { main: config.entries.main };
325
+ if (config.entries.pre) entriesConfig.pre = config.entries.pre;
326
+ if (config.entries.post) entriesConfig.post = config.entries.post;
327
+ if (config.entries.workers) entriesConfig.workers = config.entries.workers;
328
+ const entriesResult = yield* configService.detectEntries(cwd, entriesConfig);
329
+ if (shouldClean) yield* cleanDirectory(resolve(cwd, "dist"));
330
+ const entryResults = [];
331
+ for (const entry of entriesResult.entries) {
332
+ const result = yield* Effect.result(bundleEntry(entry, config, cwd));
333
+ if (Result.isFailure(result)) {
334
+ const err = result.failure;
335
+ entryResults.push({
336
+ success: false,
337
+ error: err.cause instanceof Error ? err.cause.message : String(err.cause)
338
+ });
339
+ } else entryResults.push(result.success);
340
+ }
341
+ yield* writeFile(resolve(cwd, "dist/package.json"), "{ \"type\": \"module\" }");
342
+ const duration = Date.now() - startTime;
343
+ const success = entryResults.every((r) => r.success);
344
+ if (!success) return {
345
+ success,
346
+ entries: entryResults,
347
+ duration,
348
+ error: "One or more entries failed to build"
349
+ };
350
+ return {
351
+ success,
352
+ entries: entryResults,
353
+ duration
354
+ };
355
+ }),
356
+ /* v8 ignore stop */
357
+ bundle: (entry, config) => bundleEntry(entry, config, process.cwd()),
358
+ clean: (outputDir) => cleanDirectory(outputDir),
359
+ formatResult: formatBuildResult,
360
+ formatBytes
361
+ };
362
+ }));
363
+ };
364
+ /* v8 ignore stop */
81
365
 
82
366
  //#endregion
83
367
  export { BuildResultSchema, BuildRunnerOptionsSchema, BuildService, BundleResultSchema, BundleStatsSchema };
@@ -1,9 +1,59 @@
1
+ import { ConfigInvalid, ConfigLoadFailed, ConfigNotFound, MainEntryMissing, WorkerEntryInvalidName, WorkerEntryMissing } from "../errors.js";
1
2
  import { OptionalPathLikeSchema } from "../schemas/path.js";
2
- import { ConfigSchema } from "../schemas/config.js";
3
- import { Context, Schema } from "effect";
3
+ import { ConfigSchema, defineConfig } from "../schemas/config.js";
4
+ import { Context, Effect, Layer, Schema } from "effect";
5
+ import { existsSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { createJiti } from "jiti";
4
8
 
5
9
  //#region src/services/config.ts
6
10
  /**
11
+ * ConfigService - Effect service for configuration management.
12
+ *
13
+ * @remarks
14
+ * Provides configuration loading, resolution, and entry point detection
15
+ * using Effect's service pattern with Context.Service.
16
+ *
17
+ * @internal
18
+ */
19
+ const CONFIG_FILENAMES = [
20
+ "action.config.ts",
21
+ "action.config.js",
22
+ "action.config.mjs"
23
+ ];
24
+ const DEFAULT_ENTRIES = {
25
+ main: "src/main.ts",
26
+ pre: "src/pre.ts",
27
+ post: "src/post.ts"
28
+ };
29
+ /** Lifecycle bundle names a worker entry must not reuse — they own `dist/main.js` etc. */
30
+ const RESERVED_ENTRY_NAMES = /* @__PURE__ */ new Set([
31
+ "main",
32
+ "pre",
33
+ "post"
34
+ ]);
35
+ /**
36
+ * Find config file in the given directory.
37
+ */
38
+ function findConfigFile(cwd) {
39
+ for (const filename of CONFIG_FILENAMES) {
40
+ const configPath = resolve(cwd, filename);
41
+ if (existsSync(configPath)) return configPath;
42
+ }
43
+ }
44
+ /**
45
+ * Detect a single optional entry.
46
+ */
47
+ function detectOptionalEntry(cwd, type, explicitPath) {
48
+ const defaultPath = DEFAULT_ENTRIES[type];
49
+ const absolutePath = resolve(cwd, explicitPath ?? defaultPath);
50
+ if (existsSync(absolutePath)) return {
51
+ type,
52
+ path: absolutePath,
53
+ output: `dist/${type}.js`
54
+ };
55
+ }
56
+ /**
7
57
  * Options for loading configuration.
8
58
  * @public
9
59
  */
@@ -75,7 +125,95 @@ const LoadConfigResultSchema = Schema.Struct({
75
125
  *
76
126
  * @public
77
127
  */
78
- var ConfigService = class extends Context.Service()("ConfigService") {};
128
+ var ConfigService = class extends Context.Service()("ConfigService") {
129
+ /**
130
+ * Production implementation of {@link ConfigService}.
131
+ *
132
+ * @public
133
+ */
134
+ static layer = Layer.succeed(this, {
135
+ load: (options = {}) => Effect.gen(function* () {
136
+ const cwd = options.cwd ?? process.cwd();
137
+ const configPath = options.configPath ?? findConfigFile(cwd);
138
+ if (!configPath) return {
139
+ config: defineConfig({}),
140
+ usingDefaults: true
141
+ };
142
+ /* v8 ignore start - requires explicit configPath to non-existent file */
143
+ if (!existsSync(configPath)) return yield* Effect.fail(new ConfigNotFound({
144
+ path: configPath,
145
+ message: "Specified config file does not exist"
146
+ }));
147
+ /* v8 ignore stop */
148
+ const absolutePath = resolve(cwd, configPath);
149
+ /* v8 ignore stop */
150
+ const configInput = (yield* Effect.tryPromise({
151
+ try: async () => {
152
+ if (absolutePath.endsWith(".ts")) return createJiti(absolutePath, { interopDefault: true }).import(absolutePath);
153
+ return import(absolutePath);
154
+ },
155
+ catch: (error) => new ConfigLoadFailed({
156
+ path: configPath,
157
+ cause: error
158
+ })
159
+ })).default;
160
+ /* v8 ignore start - requires config file with non-object default export */
161
+ if (!configInput || typeof configInput !== "object") return yield* Effect.fail(new ConfigInvalid({
162
+ path: configPath,
163
+ errors: ["Config file must export a default configuration object"]
164
+ }));
165
+ return {
166
+ config: defineConfig(configInput),
167
+ configPath,
168
+ usingDefaults: false
169
+ };
170
+ }),
171
+ resolve: (input = {}) => Effect.succeed(defineConfig(input)),
172
+ detectEntries: (cwd, entries) => Effect.gen(function* () {
173
+ const detected = [];
174
+ const mainPath = entries?.main ?? DEFAULT_ENTRIES.main;
175
+ const absoluteMainPath = resolve(cwd, mainPath);
176
+ if (!existsSync(absoluteMainPath)) return yield* Effect.fail(new MainEntryMissing({
177
+ expectedPath: mainPath,
178
+ cwd
179
+ }));
180
+ detected.push({
181
+ type: "main",
182
+ path: absoluteMainPath,
183
+ output: "dist/main.js"
184
+ });
185
+ const preEntry = detectOptionalEntry(cwd, "pre", entries?.pre);
186
+ if (preEntry) detected.push(preEntry);
187
+ const postEntry = detectOptionalEntry(cwd, "post", entries?.post);
188
+ if (postEntry) detected.push(postEntry);
189
+ for (const [name, workerPath] of Object.entries(entries?.workers ?? {})) {
190
+ if (RESERVED_ENTRY_NAMES.has(name)) return yield* Effect.fail(new WorkerEntryInvalidName({
191
+ workerName: name,
192
+ reason: `"${name}" is a reserved lifecycle bundle name (main/pre/post)`
193
+ }));
194
+ if (name.length === 0 || name.includes("/") || name.includes("\\") || name.includes("..")) return yield* Effect.fail(new WorkerEntryInvalidName({
195
+ workerName: name,
196
+ reason: "worker names must be non-empty and free of path separators"
197
+ }));
198
+ const absoluteWorkerPath = resolve(cwd, workerPath);
199
+ if (!existsSync(absoluteWorkerPath)) return yield* Effect.fail(new WorkerEntryMissing({
200
+ workerName: name,
201
+ expectedPath: workerPath,
202
+ cwd
203
+ }));
204
+ detected.push({
205
+ type: name,
206
+ path: absoluteWorkerPath,
207
+ output: `dist/${name}.js`
208
+ });
209
+ }
210
+ return {
211
+ success: true,
212
+ entries: detected
213
+ };
214
+ })
215
+ });
216
+ };
79
217
 
80
218
  //#endregion
81
219
  export { ConfigService, DetectEntriesResultSchema, DetectedEntrySchema, LoadConfigOptionsSchema };