@savvy-web/github-action-builder 0.7.4 → 0.7.5
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.d.ts +1 -0
- package/bin/github-action-builder.js +39 -307
- package/cli/commands/build.js +91 -0
- package/cli/commands/index.js +5 -0
- package/cli/commands/init.js +247 -0
- package/cli/commands/validate.js +42 -0
- package/errors.js +284 -0
- package/github-action.js +302 -0
- package/index.d.ts +1567 -1820
- package/index.js +10 -134
- package/layers/app.js +83 -0
- package/package.json +74 -90
- package/schemas/action-yml.js +110 -0
- package/schemas/config.js +190 -0
- package/schemas/path.js +43 -0
- package/services/build-live.js +223 -0
- package/services/build.js +63 -0
- package/services/config-live.js +111 -0
- package/services/config.js +63 -0
- package/services/persist-local-live.js +210 -0
- package/services/persist-local.js +37 -0
- package/services/validation-live.js +216 -0
- package/services/validation.js +77 -0
- package/tsdoc-metadata.json +11 -11
- package/231.js +0 -5
- package/612.js +0 -5
- package/948.js +0 -931
- /package/{tsconfig → public/tsconfig}/action.json +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/config.ts
|
|
4
|
+
/**
|
|
5
|
+
* Configuration schemas for GitHub Action Builder using Effect Schema.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This module defines the schemas for validating configuration
|
|
9
|
+
* and provides the {@link defineConfig} helper for type-safe configuration files.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Schema for entry point paths.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* GitHub Actions support three entry points:
|
|
18
|
+
* - `main`: The primary action entry point (required)
|
|
19
|
+
* - `pre`: Runs before the main action (optional)
|
|
20
|
+
* - `post`: Runs after the main action for cleanup (optional)
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
const EntriesSchema = Schema.Struct({
|
|
25
|
+
/** Path to the main action entry point. Defaults to "src/main.ts". */
|
|
26
|
+
main: Schema.optionalWith(Schema.String, { default: () => "src/main.ts" }),
|
|
27
|
+
/** Path to the pre-action hook entry point. */
|
|
28
|
+
pre: Schema.optional(Schema.String),
|
|
29
|
+
/** Path to the post-action hook entry point. */
|
|
30
|
+
post: Schema.optional(Schema.String)
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Schema for build options.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Build options control how the TypeScript source is bundled using rsbuild.
|
|
37
|
+
* The bundler creates a single JavaScript file with all dependencies inlined.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
const BuildOptionsSchema = Schema.Struct({
|
|
42
|
+
/** Enable minification to reduce bundle size. Defaults to true. */
|
|
43
|
+
minify: Schema.optionalWith(Schema.Boolean, { default: () => true }),
|
|
44
|
+
/** Generate source maps for debugging. Defaults to false. */
|
|
45
|
+
sourceMap: Schema.optionalWith(Schema.Boolean, { default: () => false }),
|
|
46
|
+
/** Packages to exclude from the bundle (in addition to node: builtins). Defaults to []. */
|
|
47
|
+
externals: Schema.optionalWith(Schema.Array(Schema.String), { default: () => [] }),
|
|
48
|
+
/** Packages to exclude from the bundle and replace with a stub that throws if loaded at runtime. Use for optional transitive dependencies the action never exercises (e.g. native modules). Defaults to []. */
|
|
49
|
+
ignore: Schema.optionalWith(Schema.Array(Schema.String), { default: () => [] })
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Schema for validation options.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* Validation options control how strictly the build process validates
|
|
56
|
+
* the project structure and configuration before building.
|
|
57
|
+
*
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
const ValidationOptionsSchema = Schema.Struct({
|
|
61
|
+
/** Require action.yml to exist and be valid. Defaults to true. */
|
|
62
|
+
requireActionYml: Schema.optionalWith(Schema.Boolean, { default: () => true }),
|
|
63
|
+
/** Maximum bundle size before warning/error (e.g., "5mb", "500kb"). */
|
|
64
|
+
maxBundleSize: Schema.optional(Schema.String),
|
|
65
|
+
/** Treat warnings as errors. Auto-detects from CI when undefined. */
|
|
66
|
+
strict: Schema.optional(Schema.Boolean)
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Schema for persist-local options.
|
|
70
|
+
*
|
|
71
|
+
* @remarks
|
|
72
|
+
* Controls automatic copying of build output to a local action directory
|
|
73
|
+
* for testing with nektos/act.
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
const PersistLocalOptionsSchema = Schema.Struct({
|
|
78
|
+
/** Enable persisting build output locally. Defaults to true. */
|
|
79
|
+
enabled: Schema.optionalWith(Schema.Boolean, { default: () => true }),
|
|
80
|
+
/** Path for the local action directory, relative to cwd. Defaults to ".github/actions/local". */
|
|
81
|
+
path: Schema.optionalWith(Schema.String, { default: () => ".github/actions/local" }),
|
|
82
|
+
/** Generate act boilerplate files (.actrc, act-test.yml) if they don't exist. Defaults to true. */
|
|
83
|
+
actTemplate: Schema.optionalWith(Schema.Boolean, { default: () => true })
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* User-provided configuration input (all fields optional).
|
|
87
|
+
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* This schema is used for parsing user-provided configuration.
|
|
90
|
+
* All sections are optional; defaults are applied via {@link defineConfig}.
|
|
91
|
+
*
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
const ConfigInputSchema = Schema.Struct({
|
|
95
|
+
entries: Schema.optional(Schema.Struct({
|
|
96
|
+
main: Schema.optional(Schema.String),
|
|
97
|
+
pre: Schema.optional(Schema.String),
|
|
98
|
+
post: Schema.optional(Schema.String)
|
|
99
|
+
})),
|
|
100
|
+
build: Schema.optional(Schema.Struct({
|
|
101
|
+
minify: Schema.optional(Schema.Boolean),
|
|
102
|
+
sourceMap: Schema.optional(Schema.Boolean),
|
|
103
|
+
externals: Schema.optional(Schema.Array(Schema.String)),
|
|
104
|
+
ignore: Schema.optional(Schema.Array(Schema.String))
|
|
105
|
+
})),
|
|
106
|
+
validation: Schema.optional(Schema.Struct({
|
|
107
|
+
requireActionYml: Schema.optional(Schema.Boolean),
|
|
108
|
+
maxBundleSize: Schema.optional(Schema.String),
|
|
109
|
+
strict: Schema.optional(Schema.Boolean)
|
|
110
|
+
})),
|
|
111
|
+
persistLocal: Schema.optional(Schema.Struct({
|
|
112
|
+
enabled: Schema.optional(Schema.Boolean),
|
|
113
|
+
path: Schema.optional(Schema.String),
|
|
114
|
+
actTemplate: Schema.optional(Schema.Boolean)
|
|
115
|
+
}))
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* Fully resolved configuration with all defaults applied.
|
|
119
|
+
*
|
|
120
|
+
* @internal
|
|
121
|
+
*/
|
|
122
|
+
const ConfigSchema = Schema.Struct({
|
|
123
|
+
entries: EntriesSchema,
|
|
124
|
+
build: BuildOptionsSchema,
|
|
125
|
+
validation: ValidationOptionsSchema,
|
|
126
|
+
persistLocal: PersistLocalOptionsSchema
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* Define a configuration with full TypeScript support.
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* This function validates the configuration and applies all defaults.
|
|
133
|
+
* Use it in your `action.config.ts` file for autocomplete and type checking.
|
|
134
|
+
*
|
|
135
|
+
* @param config - Partial configuration object
|
|
136
|
+
* @returns Fully resolved configuration with defaults applied
|
|
137
|
+
*
|
|
138
|
+
* @example Basic configuration file
|
|
139
|
+
* ```typescript
|
|
140
|
+
* // action.config.ts
|
|
141
|
+
* import { defineConfig } from "@savvy-web/github-action-builder";
|
|
142
|
+
*
|
|
143
|
+
* export default defineConfig({
|
|
144
|
+
* entries: {
|
|
145
|
+
* main: "src/main.ts",
|
|
146
|
+
* },
|
|
147
|
+
* build: {
|
|
148
|
+
* minify: true,
|
|
149
|
+
* },
|
|
150
|
+
* });
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* @example Full configuration with all options
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // action.config.ts
|
|
156
|
+
* import { defineConfig } from "@savvy-web/github-action-builder";
|
|
157
|
+
*
|
|
158
|
+
* export default defineConfig({
|
|
159
|
+
* entries: {
|
|
160
|
+
* main: "src/action.ts",
|
|
161
|
+
* pre: "src/setup.ts",
|
|
162
|
+
* post: "src/cleanup.ts",
|
|
163
|
+
* },
|
|
164
|
+
* build: {
|
|
165
|
+
* minify: true,
|
|
166
|
+
* sourceMap: true,
|
|
167
|
+
* externals: ["@aws-sdk/client-s3"],
|
|
168
|
+
* ignore: ["libxmljs2"],
|
|
169
|
+
* },
|
|
170
|
+
* validation: {
|
|
171
|
+
* requireActionYml: true,
|
|
172
|
+
* maxBundleSize: "10mb",
|
|
173
|
+
* strict: true,
|
|
174
|
+
* },
|
|
175
|
+
* });
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* @public
|
|
179
|
+
*/
|
|
180
|
+
function defineConfig(config = {}) {
|
|
181
|
+
return Schema.decodeUnknownSync(ConfigSchema)({
|
|
182
|
+
entries: config.entries ?? {},
|
|
183
|
+
build: config.build ?? {},
|
|
184
|
+
validation: config.validation ?? {},
|
|
185
|
+
persistLocal: config.persistLocal ?? {}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
//#endregion
|
|
190
|
+
export { BuildOptionsSchema, ConfigInputSchema, ConfigSchema, EntriesSchema, PersistLocalOptionsSchema, ValidationOptionsSchema, defineConfig };
|
package/schemas/path.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
//#region src/schemas/path.ts
|
|
5
|
+
/**
|
|
6
|
+
* Convert a PathLike value to a string.
|
|
7
|
+
*
|
|
8
|
+
* @param pathLike - A string, Buffer, or URL path
|
|
9
|
+
* @returns The path as a string
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
/* v8 ignore start - edge case branches for Buffer/URL paths */
|
|
14
|
+
function pathLikeToString(pathLike) {
|
|
15
|
+
if (typeof pathLike === "string") return pathLike;
|
|
16
|
+
if (Buffer.isBuffer(pathLike)) return pathLike.toString("utf8");
|
|
17
|
+
if (pathLike instanceof URL) return fileURLToPath(pathLike);
|
|
18
|
+
return String(pathLike);
|
|
19
|
+
}
|
|
20
|
+
/* v8 ignore stop */
|
|
21
|
+
/**
|
|
22
|
+
* Schema that accepts PathLike (string, Buffer, or URL) and normalizes to string.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* This schema is designed for user-facing inputs where flexibility is important.
|
|
26
|
+
* Internally, paths are stored as strings for serialization compatibility.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
const PathLikeSchema = Schema.transform(Schema.Union(Schema.String, Schema.instanceOf(Buffer), Schema.instanceOf(URL)), Schema.String, {
|
|
31
|
+
strict: true,
|
|
32
|
+
decode: (pathLike) => pathLikeToString(pathLike),
|
|
33
|
+
/* v8 ignore next */
|
|
34
|
+
encode: (s) => s
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Optional PathLike schema.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
const OptionalPathLikeSchema = Schema.optional(PathLikeSchema);
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { OptionalPathLikeSchema };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { BundleFailed, CleanError, WriteError } from "../errors.js";
|
|
2
|
+
import { BuildService } from "./build.js";
|
|
3
|
+
import { ConfigService } from "./config.js";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { createRsbuild } from "@rsbuild/core";
|
|
8
|
+
|
|
9
|
+
//#region src/services/build-live.ts
|
|
10
|
+
/* v8 ignore start - build service requires actual bundling for integration testing */
|
|
11
|
+
/**
|
|
12
|
+
* BuildService Layer implementation.
|
|
13
|
+
*
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Source of the stub module that replaces packages listed in `build.ignore`.
|
|
17
|
+
* It is bundled in place of the real module and throws if ever loaded.
|
|
18
|
+
*/
|
|
19
|
+
const IGNORE_STUB_SOURCE = `throw new Error("A module excluded via the build 'ignore' option was loaded at runtime.");\n`;
|
|
20
|
+
/**
|
|
21
|
+
* Format bytes as a human-readable string.
|
|
22
|
+
*/
|
|
23
|
+
function formatBytes(bytes) {
|
|
24
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
25
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Format build result for terminal output.
|
|
30
|
+
*/
|
|
31
|
+
/* v8 ignore start - formatting function tested via integration */
|
|
32
|
+
function formatBuildResult(result) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
if (result.success) {
|
|
35
|
+
lines.push("Build Summary:");
|
|
36
|
+
for (const entry of result.entries) if (entry.success && entry.stats) {
|
|
37
|
+
const { entry: name, size, duration, outputPath } = entry.stats;
|
|
38
|
+
lines.push(` ✓ ${name}: ${formatBytes(size)} (${duration}ms) → ${outputPath}`);
|
|
39
|
+
}
|
|
40
|
+
lines.push(`\nTotal time: ${result.duration}ms`);
|
|
41
|
+
} else {
|
|
42
|
+
lines.push("Build Failed:");
|
|
43
|
+
for (const entry of result.entries) if (!entry.success) lines.push(` ✗ ${entry.error}`);
|
|
44
|
+
}
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
47
|
+
/* v8 ignore stop */
|
|
48
|
+
/**
|
|
49
|
+
* Clean output directory.
|
|
50
|
+
*/
|
|
51
|
+
function cleanDirectory(dir) {
|
|
52
|
+
return Effect.try({
|
|
53
|
+
try: () => {
|
|
54
|
+
if (existsSync(dir)) rmSync(dir, {
|
|
55
|
+
recursive: true,
|
|
56
|
+
force: true
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
60
|
+
catch: (error) => new CleanError({
|
|
61
|
+
directory: dir,
|
|
62
|
+
cause: error
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write file with directory creation.
|
|
68
|
+
*/
|
|
69
|
+
function writeFile(path, content) {
|
|
70
|
+
return Effect.try({
|
|
71
|
+
try: () => {
|
|
72
|
+
mkdirSync(resolve(path, ".."), { recursive: true });
|
|
73
|
+
writeFileSync(path, content, "utf8");
|
|
74
|
+
},
|
|
75
|
+
/* v8 ignore next 5 - error branch requires fs permission failures */
|
|
76
|
+
catch: (error) => new WriteError({
|
|
77
|
+
path,
|
|
78
|
+
cause: error
|
|
79
|
+
})
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Bundle a single entry with rsbuild.
|
|
84
|
+
*/
|
|
85
|
+
/* v8 ignore start - bundling requires actual rsbuild execution */
|
|
86
|
+
function bundleEntry(entry, config, cwd) {
|
|
87
|
+
return Effect.gen(function* () {
|
|
88
|
+
const startTime = Date.now();
|
|
89
|
+
const outputDir = resolve(cwd, "dist");
|
|
90
|
+
const externalsSet = new Set(config.build.externals);
|
|
91
|
+
const ignoreSet = new Set(config.build.ignore);
|
|
92
|
+
const ignoreAlias = {};
|
|
93
|
+
if (config.build.ignore.length > 0) {
|
|
94
|
+
const stubPath = resolve(cwd, "node_modules", ".cache", "github-action-builder", "ignore-stub.mjs");
|
|
95
|
+
yield* writeFile(stubPath, IGNORE_STUB_SOURCE);
|
|
96
|
+
for (const moduleName of config.build.ignore) ignoreAlias[`${moduleName}$`] = stubPath;
|
|
97
|
+
}
|
|
98
|
+
const rsbuild = yield* Effect.tryPromise({
|
|
99
|
+
try: () => createRsbuild({ rsbuildConfig: {
|
|
100
|
+
source: { entry: { [entry.type]: entry.path } },
|
|
101
|
+
resolve: { alias: ignoreAlias },
|
|
102
|
+
output: {
|
|
103
|
+
target: "node",
|
|
104
|
+
module: true,
|
|
105
|
+
distPath: { root: outputDir },
|
|
106
|
+
filename: { js: "[name].js" },
|
|
107
|
+
externals: (data) => {
|
|
108
|
+
const request = data.request;
|
|
109
|
+
if (!request) return false;
|
|
110
|
+
if (request.startsWith("node:")) return `node-commonjs ${request}`;
|
|
111
|
+
if (externalsSet.has(request) && !ignoreSet.has(request)) return request;
|
|
112
|
+
return false;
|
|
113
|
+
},
|
|
114
|
+
cleanDistPath: false,
|
|
115
|
+
legalComments: "inline",
|
|
116
|
+
minify: config.build.minify,
|
|
117
|
+
sourceMap: config.build.sourceMap ? { js: "source-map" } : false
|
|
118
|
+
},
|
|
119
|
+
performance: { chunkSplit: { strategy: "all-in-one" } },
|
|
120
|
+
tools: { rspack: {
|
|
121
|
+
node: {
|
|
122
|
+
__dirname: "node-module",
|
|
123
|
+
__filename: "node-module"
|
|
124
|
+
},
|
|
125
|
+
output: { asyncChunks: false }
|
|
126
|
+
} }
|
|
127
|
+
} }),
|
|
128
|
+
catch: (error) => new BundleFailed({
|
|
129
|
+
entry: entry.path,
|
|
130
|
+
cause: error
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
const buildResult = yield* Effect.tryPromise({
|
|
134
|
+
try: () => rsbuild.build(),
|
|
135
|
+
catch: (error) => new BundleFailed({
|
|
136
|
+
entry: entry.path,
|
|
137
|
+
cause: error
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
yield* Effect.tryPromise({
|
|
141
|
+
try: () => buildResult.close(),
|
|
142
|
+
catch: (error) => new BundleFailed({
|
|
143
|
+
entry: entry.path,
|
|
144
|
+
cause: /* @__PURE__ */ new Error(`rsbuild close() failed: ${error}`)
|
|
145
|
+
})
|
|
146
|
+
});
|
|
147
|
+
const outputPath = resolve(outputDir, `${entry.type}.js`);
|
|
148
|
+
const size = yield* Effect.try({
|
|
149
|
+
try: () => statSync(outputPath).size,
|
|
150
|
+
catch: (error) => new BundleFailed({
|
|
151
|
+
entry: entry.path,
|
|
152
|
+
cause: error
|
|
153
|
+
})
|
|
154
|
+
});
|
|
155
|
+
const duration = Date.now() - startTime;
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
stats: {
|
|
159
|
+
entry: entry.type,
|
|
160
|
+
size,
|
|
161
|
+
duration,
|
|
162
|
+
outputPath: entry.output
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/* v8 ignore stop */
|
|
168
|
+
/**
|
|
169
|
+
* Live implementation of BuildService.
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* Uses rsbuild for bundling.
|
|
173
|
+
*/
|
|
174
|
+
const BuildServiceLive = Layer.effect(BuildService, Effect.gen(function* () {
|
|
175
|
+
const configService = yield* ConfigService;
|
|
176
|
+
return {
|
|
177
|
+
/* v8 ignore start - build execution requires actual rsbuild bundling */
|
|
178
|
+
build: (config, options = {}) => Effect.gen(function* () {
|
|
179
|
+
const cwd = options.cwd ?? process.cwd();
|
|
180
|
+
const shouldClean = options.clean ?? true;
|
|
181
|
+
const startTime = Date.now();
|
|
182
|
+
const entriesConfig = { main: config.entries.main };
|
|
183
|
+
if (config.entries.pre) entriesConfig.pre = config.entries.pre;
|
|
184
|
+
if (config.entries.post) entriesConfig.post = config.entries.post;
|
|
185
|
+
const entriesResult = yield* configService.detectEntries(cwd, entriesConfig);
|
|
186
|
+
if (shouldClean) yield* cleanDirectory(resolve(cwd, "dist"));
|
|
187
|
+
const entryResults = [];
|
|
188
|
+
for (const entry of entriesResult.entries) {
|
|
189
|
+
const result = yield* Effect.either(bundleEntry(entry, config, cwd));
|
|
190
|
+
if (result._tag === "Left") {
|
|
191
|
+
const err = result.left;
|
|
192
|
+
entryResults.push({
|
|
193
|
+
success: false,
|
|
194
|
+
error: err.cause instanceof Error ? err.cause.message : String(err.cause)
|
|
195
|
+
});
|
|
196
|
+
} else entryResults.push(result.right);
|
|
197
|
+
}
|
|
198
|
+
yield* writeFile(resolve(cwd, "dist/package.json"), "{ \"type\": \"module\" }");
|
|
199
|
+
const duration = Date.now() - startTime;
|
|
200
|
+
const success = entryResults.every((r) => r.success);
|
|
201
|
+
if (!success) return {
|
|
202
|
+
success,
|
|
203
|
+
entries: entryResults,
|
|
204
|
+
duration,
|
|
205
|
+
error: "One or more entries failed to build"
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
success,
|
|
209
|
+
entries: entryResults,
|
|
210
|
+
duration
|
|
211
|
+
};
|
|
212
|
+
}),
|
|
213
|
+
/* v8 ignore stop */
|
|
214
|
+
bundle: (entry, config) => bundleEntry(entry, config, process.cwd()),
|
|
215
|
+
clean: (outputDir) => cleanDirectory(outputDir),
|
|
216
|
+
formatResult: formatBuildResult,
|
|
217
|
+
formatBytes
|
|
218
|
+
};
|
|
219
|
+
}));
|
|
220
|
+
/* v8 ignore stop */
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
export { BuildServiceLive };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { OptionalPathLikeSchema } from "../schemas/path.js";
|
|
2
|
+
import { Context, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/services/build.ts
|
|
5
|
+
/**
|
|
6
|
+
* Options for the build process.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const BuildRunnerOptionsSchema = Schema.Struct({
|
|
10
|
+
/** Working directory for the build. Accepts string, Buffer, or URL. */
|
|
11
|
+
cwd: OptionalPathLikeSchema,
|
|
12
|
+
/** Clean output directory before building. Defaults to true. */
|
|
13
|
+
clean: Schema.optional(Schema.Boolean)
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Statistics for a single bundled entry.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
const BundleStatsSchema = Schema.Struct({
|
|
20
|
+
/** Entry type (main, pre, or post). */
|
|
21
|
+
entry: Schema.String,
|
|
22
|
+
/** Bundle size in bytes. */
|
|
23
|
+
size: Schema.Number,
|
|
24
|
+
/** Build duration in milliseconds. */
|
|
25
|
+
duration: Schema.Number,
|
|
26
|
+
/** Output path relative to working directory. */
|
|
27
|
+
outputPath: Schema.String
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Result of bundling a single entry.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
const BundleResultSchema = Schema.Struct({
|
|
34
|
+
/** Whether bundling succeeded. */
|
|
35
|
+
success: Schema.Boolean,
|
|
36
|
+
/** Bundle statistics if successful. */
|
|
37
|
+
stats: Schema.optional(BundleStatsSchema),
|
|
38
|
+
/** Error message if failed. */
|
|
39
|
+
error: Schema.optional(Schema.String)
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Result of the complete build process.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
const BuildResultSchema = Schema.Struct({
|
|
46
|
+
/** Whether the overall build succeeded. */
|
|
47
|
+
success: Schema.Boolean,
|
|
48
|
+
/** Results for each entry that was built. */
|
|
49
|
+
entries: Schema.Array(BundleResultSchema),
|
|
50
|
+
/** Total build duration in milliseconds. */
|
|
51
|
+
duration: Schema.Number,
|
|
52
|
+
/** Error message if build failed. */
|
|
53
|
+
error: Schema.optional(Schema.String)
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* BuildService tag for dependency injection.
|
|
57
|
+
*
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
const BuildService = Context.GenericTag("BuildService");
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { BuildResultSchema, BuildRunnerOptionsSchema, BuildService, BundleResultSchema, BundleStatsSchema };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ConfigInvalid, ConfigLoadFailed, ConfigNotFound, MainEntryMissing } from "../errors.js";
|
|
2
|
+
import { defineConfig } from "../schemas/config.js";
|
|
3
|
+
import { ConfigService } from "./config.js";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { createJiti } from "jiti";
|
|
8
|
+
|
|
9
|
+
//#region src/services/config-live.ts
|
|
10
|
+
/**
|
|
11
|
+
* ConfigService Layer implementation.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
const CONFIG_FILENAMES = [
|
|
15
|
+
"action.config.ts",
|
|
16
|
+
"action.config.js",
|
|
17
|
+
"action.config.mjs"
|
|
18
|
+
];
|
|
19
|
+
const DEFAULT_ENTRIES = {
|
|
20
|
+
main: "src/main.ts",
|
|
21
|
+
pre: "src/pre.ts",
|
|
22
|
+
post: "src/post.ts"
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Find config file in the given directory.
|
|
26
|
+
*/
|
|
27
|
+
function findConfigFile(cwd) {
|
|
28
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
29
|
+
const configPath = resolve(cwd, filename);
|
|
30
|
+
if (existsSync(configPath)) return configPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Detect a single optional entry.
|
|
35
|
+
*/
|
|
36
|
+
function detectOptionalEntry(cwd, type, explicitPath) {
|
|
37
|
+
const defaultPath = DEFAULT_ENTRIES[type];
|
|
38
|
+
const absolutePath = resolve(cwd, explicitPath ?? defaultPath);
|
|
39
|
+
if (existsSync(absolutePath)) return {
|
|
40
|
+
type,
|
|
41
|
+
path: absolutePath,
|
|
42
|
+
output: `dist/${type}.js`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Live implementation of ConfigService.
|
|
47
|
+
*/
|
|
48
|
+
const ConfigServiceLive = Layer.succeed(ConfigService, {
|
|
49
|
+
load: (options = {}) => Effect.gen(function* () {
|
|
50
|
+
const cwd = options.cwd ?? process.cwd();
|
|
51
|
+
const configPath = options.configPath ?? findConfigFile(cwd);
|
|
52
|
+
if (!configPath) return {
|
|
53
|
+
config: defineConfig({}),
|
|
54
|
+
usingDefaults: true
|
|
55
|
+
};
|
|
56
|
+
/* v8 ignore start - requires explicit configPath to non-existent file */
|
|
57
|
+
if (!existsSync(configPath)) return yield* Effect.fail(new ConfigNotFound({
|
|
58
|
+
path: configPath,
|
|
59
|
+
message: "Specified config file does not exist"
|
|
60
|
+
}));
|
|
61
|
+
/* v8 ignore stop */
|
|
62
|
+
const absolutePath = resolve(cwd, configPath);
|
|
63
|
+
/* v8 ignore stop */
|
|
64
|
+
const configInput = (yield* Effect.tryPromise({
|
|
65
|
+
try: async () => {
|
|
66
|
+
if (absolutePath.endsWith(".ts")) return createJiti(absolutePath, { interopDefault: true }).import(absolutePath);
|
|
67
|
+
return import(absolutePath);
|
|
68
|
+
},
|
|
69
|
+
catch: (error) => new ConfigLoadFailed({
|
|
70
|
+
path: configPath,
|
|
71
|
+
cause: error
|
|
72
|
+
})
|
|
73
|
+
})).default;
|
|
74
|
+
/* v8 ignore start - requires config file with non-object default export */
|
|
75
|
+
if (!configInput || typeof configInput !== "object") return yield* Effect.fail(new ConfigInvalid({
|
|
76
|
+
path: configPath,
|
|
77
|
+
errors: ["Config file must export a default configuration object"]
|
|
78
|
+
}));
|
|
79
|
+
return {
|
|
80
|
+
config: defineConfig(configInput),
|
|
81
|
+
configPath,
|
|
82
|
+
usingDefaults: false
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
resolve: (input = {}) => Effect.succeed(defineConfig(input)),
|
|
86
|
+
detectEntries: (cwd, entries) => Effect.gen(function* () {
|
|
87
|
+
const detected = [];
|
|
88
|
+
const mainPath = entries?.main ?? DEFAULT_ENTRIES.main;
|
|
89
|
+
const absoluteMainPath = resolve(cwd, mainPath);
|
|
90
|
+
if (!existsSync(absoluteMainPath)) return yield* Effect.fail(new MainEntryMissing({
|
|
91
|
+
expectedPath: mainPath,
|
|
92
|
+
cwd
|
|
93
|
+
}));
|
|
94
|
+
detected.push({
|
|
95
|
+
type: "main",
|
|
96
|
+
path: absoluteMainPath,
|
|
97
|
+
output: "dist/main.js"
|
|
98
|
+
});
|
|
99
|
+
const preEntry = detectOptionalEntry(cwd, "pre", entries?.pre);
|
|
100
|
+
if (preEntry) detected.push(preEntry);
|
|
101
|
+
const postEntry = detectOptionalEntry(cwd, "post", entries?.post);
|
|
102
|
+
if (postEntry) detected.push(postEntry);
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
entries: detected
|
|
106
|
+
};
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
export { ConfigServiceLive };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { OptionalPathLikeSchema } from "../schemas/path.js";
|
|
2
|
+
import { ConfigSchema } from "../schemas/config.js";
|
|
3
|
+
import { Context, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/services/config.ts
|
|
6
|
+
/**
|
|
7
|
+
* Options for loading configuration.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
const LoadConfigOptionsSchema = Schema.Struct({
|
|
11
|
+
/** Working directory to search for config. Accepts string, Buffer, or URL. */
|
|
12
|
+
cwd: OptionalPathLikeSchema,
|
|
13
|
+
/** Explicit path to config file. Accepts string, Buffer, or URL. */
|
|
14
|
+
configPath: OptionalPathLikeSchema
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Entry point type.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const EntryTypeSchema = Schema.Literal("main", "pre", "post");
|
|
21
|
+
/**
|
|
22
|
+
* Detected entry point information.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
const DetectedEntrySchema = Schema.Struct({
|
|
26
|
+
/** Entry type (main, pre, or post). */
|
|
27
|
+
type: EntryTypeSchema,
|
|
28
|
+
/** Absolute path to the entry file. */
|
|
29
|
+
path: Schema.String,
|
|
30
|
+
/** Output path for the bundled file. */
|
|
31
|
+
output: Schema.String
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Result of entry detection.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
const DetectEntriesResultSchema = Schema.Struct({
|
|
38
|
+
/** Whether detection was successful. */
|
|
39
|
+
success: Schema.Boolean,
|
|
40
|
+
/** Detected entries. */
|
|
41
|
+
entries: Schema.Array(DetectedEntrySchema)
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Result of configuration loading.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
const LoadConfigResultSchema = Schema.Struct({
|
|
48
|
+
/** The resolved configuration. */
|
|
49
|
+
config: ConfigSchema,
|
|
50
|
+
/** Path to the config file that was loaded, if any. */
|
|
51
|
+
configPath: Schema.optional(Schema.String),
|
|
52
|
+
/** Whether defaults were used (no config file found). */
|
|
53
|
+
usingDefaults: Schema.Boolean
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* ConfigService tag for dependency injection.
|
|
57
|
+
*
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
const ConfigService = Context.GenericTag("ConfigService");
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { ConfigService, DetectEntriesResultSchema, DetectedEntrySchema, LoadConfigOptionsSchema };
|