@savvy-web/github-action-builder 2.1.1 → 2.2.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.
- package/bin/github-action-builder.js +1 -1
- package/cli/commands/build.js +1 -1
- package/cli/commands/init.js +3 -2
- package/github-action.js +1 -1
- package/index.d.ts +38 -4
- package/index.js +1 -1
- package/layers/app.js +9 -9
- package/package.json +1 -1
- package/services/build.js +286 -2
- package/services/config.js +141 -3
- package/services/persist-local.js +214 -2
- package/services/validation.js +223 -2
- package/services/build-live.js +0 -282
- package/services/config-live.js +0 -138
- package/services/persist-local-live.js +0 -211
- package/services/validation-live.js +0 -217
|
@@ -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.
|
|
27
|
+
const cli = Command.run(rootCommand, { version: "2.2.0" });
|
|
28
28
|
/**
|
|
29
29
|
* Combined layer: AppLayer + the Node implementations of the CLI
|
|
30
30
|
* environment (FileSystem, Path, Terminal, Stdio, ChildProcessSpawner).
|
package/cli/commands/build.js
CHANGED
|
@@ -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";
|
package/cli/commands/init.js
CHANGED
|
@@ -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.
|
|
23
|
+
return "2.2.0";
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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.
|
|
3
|
+
"version": "2.2.0",
|
|
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": [
|
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 {
|
|
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 };
|
package/services/config.js
CHANGED
|
@@ -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 };
|