@savvy-web/github-action-builder 0.1.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/123.js +646 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/github-action-builder.js +294 -0
- package/index.d.ts +1621 -0
- package/index.js +118 -0
- package/package.json +69 -0
- package/tsdoc-metadata.json +11 -0
package/123.js
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { Console, Context, Data, Effect, Layer, ManagedRuntime, Option, ParseResult, Schema } from "effect";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
|
+
const ConfigNotFoundBase = Data.TaggedError("ConfigNotFound");
|
|
8
|
+
class ConfigNotFound extends ConfigNotFoundBase {
|
|
9
|
+
}
|
|
10
|
+
const ConfigInvalidBase = Data.TaggedError("ConfigInvalid");
|
|
11
|
+
class ConfigInvalid extends ConfigInvalidBase {
|
|
12
|
+
}
|
|
13
|
+
const ConfigLoadFailedBase = Data.TaggedError("ConfigLoadFailed");
|
|
14
|
+
class ConfigLoadFailed extends ConfigLoadFailedBase {
|
|
15
|
+
}
|
|
16
|
+
const MainEntryMissingBase = Data.TaggedError("MainEntryMissing");
|
|
17
|
+
class MainEntryMissing extends MainEntryMissingBase {
|
|
18
|
+
}
|
|
19
|
+
const EntryFileMissingBase = Data.TaggedError("EntryFileMissing");
|
|
20
|
+
class EntryFileMissing extends EntryFileMissingBase {
|
|
21
|
+
}
|
|
22
|
+
const ActionYmlMissingBase = Data.TaggedError("ActionYmlMissing");
|
|
23
|
+
class ActionYmlMissing extends ActionYmlMissingBase {
|
|
24
|
+
}
|
|
25
|
+
const ActionYmlSyntaxErrorBase = Data.TaggedError("ActionYmlSyntaxError");
|
|
26
|
+
class ActionYmlSyntaxError extends ActionYmlSyntaxErrorBase {
|
|
27
|
+
}
|
|
28
|
+
const ActionYmlSchemaErrorBase = Data.TaggedError("ActionYmlSchemaError");
|
|
29
|
+
class ActionYmlSchemaError extends ActionYmlSchemaErrorBase {
|
|
30
|
+
}
|
|
31
|
+
const ValidationFailedBase = Data.TaggedError("ValidationFailed");
|
|
32
|
+
class ValidationFailed extends ValidationFailedBase {
|
|
33
|
+
}
|
|
34
|
+
const BundleFailedBase = Data.TaggedError("BundleFailed");
|
|
35
|
+
class BundleFailed extends BundleFailedBase {
|
|
36
|
+
}
|
|
37
|
+
const WriteErrorBase = Data.TaggedError("WriteError");
|
|
38
|
+
class WriteError extends WriteErrorBase {
|
|
39
|
+
}
|
|
40
|
+
const CleanErrorBase = Data.TaggedError("CleanError");
|
|
41
|
+
class CleanError extends CleanErrorBase {
|
|
42
|
+
}
|
|
43
|
+
const BuildFailedBase = Data.TaggedError("BuildFailed");
|
|
44
|
+
class BuildFailed extends BuildFailedBase {
|
|
45
|
+
}
|
|
46
|
+
function pathLikeToString(pathLike) {
|
|
47
|
+
if ("string" == typeof pathLike) return pathLike;
|
|
48
|
+
if (Buffer.isBuffer(pathLike)) return pathLike.toString("utf8");
|
|
49
|
+
if (pathLike instanceof URL) return fileURLToPath(pathLike);
|
|
50
|
+
return String(pathLike);
|
|
51
|
+
}
|
|
52
|
+
const PathLikeSchema = Schema.transform(Schema.Union(Schema.String, Schema.instanceOf(Buffer), Schema.instanceOf(URL)), Schema.String, {
|
|
53
|
+
strict: true,
|
|
54
|
+
decode: (pathLike)=>pathLikeToString(pathLike),
|
|
55
|
+
encode: (s)=>s
|
|
56
|
+
});
|
|
57
|
+
const OptionalPathLikeSchema = Schema.optional(PathLikeSchema);
|
|
58
|
+
const BuildRunnerOptionsSchema = Schema.Struct({
|
|
59
|
+
cwd: OptionalPathLikeSchema,
|
|
60
|
+
clean: Schema.optional(Schema.Boolean)
|
|
61
|
+
});
|
|
62
|
+
const BundleStatsSchema = Schema.Struct({
|
|
63
|
+
entry: Schema.String,
|
|
64
|
+
size: Schema.Number,
|
|
65
|
+
duration: Schema.Number,
|
|
66
|
+
outputPath: Schema.String
|
|
67
|
+
});
|
|
68
|
+
const BundleResultSchema = Schema.Struct({
|
|
69
|
+
success: Schema.Boolean,
|
|
70
|
+
stats: Schema.optional(BundleStatsSchema),
|
|
71
|
+
error: Schema.optional(Schema.String)
|
|
72
|
+
});
|
|
73
|
+
const BuildResultSchema = Schema.Struct({
|
|
74
|
+
success: Schema.Boolean,
|
|
75
|
+
entries: Schema.Array(BundleResultSchema),
|
|
76
|
+
duration: Schema.Number,
|
|
77
|
+
error: Schema.optional(Schema.String)
|
|
78
|
+
});
|
|
79
|
+
const BuildService = Context.GenericTag("BuildService");
|
|
80
|
+
const EntriesSchema = Schema.Struct({
|
|
81
|
+
main: Schema.optionalWith(Schema.String, {
|
|
82
|
+
default: ()=>"src/main.ts"
|
|
83
|
+
}),
|
|
84
|
+
pre: Schema.optional(Schema.String),
|
|
85
|
+
post: Schema.optional(Schema.String)
|
|
86
|
+
});
|
|
87
|
+
const EsTarget = Schema.Literal("es2020", "es2021", "es2022", "es2023", "es2024");
|
|
88
|
+
const BuildOptionsSchema = Schema.Struct({
|
|
89
|
+
minify: Schema.optionalWith(Schema.Boolean, {
|
|
90
|
+
default: ()=>true
|
|
91
|
+
}),
|
|
92
|
+
target: Schema.optionalWith(EsTarget, {
|
|
93
|
+
default: ()=>"es2022"
|
|
94
|
+
}),
|
|
95
|
+
sourceMap: Schema.optionalWith(Schema.Boolean, {
|
|
96
|
+
default: ()=>false
|
|
97
|
+
}),
|
|
98
|
+
externals: Schema.optionalWith(Schema.Array(Schema.String), {
|
|
99
|
+
default: ()=>[]
|
|
100
|
+
}),
|
|
101
|
+
quiet: Schema.optionalWith(Schema.Boolean, {
|
|
102
|
+
default: ()=>false
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
const ValidationOptionsSchema = Schema.Struct({
|
|
106
|
+
requireActionYml: Schema.optionalWith(Schema.Boolean, {
|
|
107
|
+
default: ()=>true
|
|
108
|
+
}),
|
|
109
|
+
maxBundleSize: Schema.optional(Schema.String),
|
|
110
|
+
strict: Schema.optional(Schema.Boolean)
|
|
111
|
+
});
|
|
112
|
+
const ConfigInputSchema = Schema.Struct({
|
|
113
|
+
entries: Schema.optional(Schema.Struct({
|
|
114
|
+
main: Schema.optional(Schema.String),
|
|
115
|
+
pre: Schema.optional(Schema.String),
|
|
116
|
+
post: Schema.optional(Schema.String)
|
|
117
|
+
})),
|
|
118
|
+
build: Schema.optional(Schema.Struct({
|
|
119
|
+
minify: Schema.optional(Schema.Boolean),
|
|
120
|
+
target: Schema.optional(EsTarget),
|
|
121
|
+
sourceMap: Schema.optional(Schema.Boolean),
|
|
122
|
+
externals: Schema.optional(Schema.Array(Schema.String)),
|
|
123
|
+
quiet: Schema.optional(Schema.Boolean)
|
|
124
|
+
})),
|
|
125
|
+
validation: Schema.optional(Schema.Struct({
|
|
126
|
+
requireActionYml: Schema.optional(Schema.Boolean),
|
|
127
|
+
maxBundleSize: Schema.optional(Schema.String),
|
|
128
|
+
strict: Schema.optional(Schema.Boolean)
|
|
129
|
+
}))
|
|
130
|
+
});
|
|
131
|
+
const ConfigSchema = Schema.Struct({
|
|
132
|
+
entries: EntriesSchema,
|
|
133
|
+
build: BuildOptionsSchema,
|
|
134
|
+
validation: ValidationOptionsSchema
|
|
135
|
+
});
|
|
136
|
+
function defineConfig(config = {}) {
|
|
137
|
+
return Schema.decodeUnknownSync(ConfigSchema)({
|
|
138
|
+
entries: config.entries ?? {},
|
|
139
|
+
build: config.build ?? {},
|
|
140
|
+
validation: config.validation ?? {}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const LoadConfigOptionsSchema = Schema.Struct({
|
|
144
|
+
cwd: OptionalPathLikeSchema,
|
|
145
|
+
configPath: OptionalPathLikeSchema
|
|
146
|
+
});
|
|
147
|
+
const EntryTypeSchema = Schema.Literal("main", "pre", "post");
|
|
148
|
+
const DetectedEntrySchema = Schema.Struct({
|
|
149
|
+
type: EntryTypeSchema,
|
|
150
|
+
path: Schema.String,
|
|
151
|
+
output: Schema.String
|
|
152
|
+
});
|
|
153
|
+
const DetectEntriesResultSchema = Schema.Struct({
|
|
154
|
+
success: Schema.Boolean,
|
|
155
|
+
entries: Schema.Array(DetectedEntrySchema)
|
|
156
|
+
});
|
|
157
|
+
Schema.Struct({
|
|
158
|
+
config: ConfigSchema,
|
|
159
|
+
configPath: Schema.optional(Schema.String),
|
|
160
|
+
usingDefaults: Schema.Boolean
|
|
161
|
+
});
|
|
162
|
+
const ConfigService = Context.GenericTag("ConfigService");
|
|
163
|
+
const build_live_require = createRequire(import.meta.url);
|
|
164
|
+
const ncc = build_live_require("@vercel/ncc");
|
|
165
|
+
async function bundle(entryPath, options = {}) {
|
|
166
|
+
return ncc(entryPath, {
|
|
167
|
+
minify: options.minify ?? true,
|
|
168
|
+
sourceMap: options.sourceMap ?? false,
|
|
169
|
+
target: options.target ?? "es2022",
|
|
170
|
+
quiet: options.quiet ?? true,
|
|
171
|
+
externals: options.externals ?? [],
|
|
172
|
+
...options
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function getBundleSize(code) {
|
|
176
|
+
return Buffer.byteLength(code, "utf8");
|
|
177
|
+
}
|
|
178
|
+
function formatBytes(bytes) {
|
|
179
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
180
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
181
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
182
|
+
}
|
|
183
|
+
function formatBuildResult(result) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
if (result.success) {
|
|
186
|
+
lines.push("Build Summary:");
|
|
187
|
+
for (const entry of result.entries)if (entry.success && entry.stats) {
|
|
188
|
+
const { entry: name, size, duration, outputPath } = entry.stats;
|
|
189
|
+
lines.push(` ✓ ${name}: ${formatBytes(size)} (${duration}ms) → ${outputPath}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push(`\nTotal time: ${result.duration}ms`);
|
|
192
|
+
} else {
|
|
193
|
+
lines.push("Build Failed:");
|
|
194
|
+
for (const entry of result.entries)if (!entry.success) lines.push(` ✗ ${entry.error}`);
|
|
195
|
+
}
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
function cleanDirectory(dir) {
|
|
199
|
+
return Effect["try"]({
|
|
200
|
+
try: ()=>{
|
|
201
|
+
if (existsSync(dir)) rmSync(dir, {
|
|
202
|
+
recursive: true,
|
|
203
|
+
force: true
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
catch: (error)=>new CleanError({
|
|
207
|
+
directory: dir,
|
|
208
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function writeFile(path, content) {
|
|
213
|
+
return Effect["try"]({
|
|
214
|
+
try: ()=>{
|
|
215
|
+
const dir = resolve(path, "..");
|
|
216
|
+
mkdirSync(dir, {
|
|
217
|
+
recursive: true
|
|
218
|
+
});
|
|
219
|
+
writeFileSync(path, content, "utf8");
|
|
220
|
+
},
|
|
221
|
+
catch: (error)=>new WriteError({
|
|
222
|
+
path,
|
|
223
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
224
|
+
})
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function bundleEntry(entry, config, cwd) {
|
|
228
|
+
return Effect.gen(function*() {
|
|
229
|
+
const startTime = Date.now();
|
|
230
|
+
const nccResult = yield* Effect.tryPromise({
|
|
231
|
+
try: ()=>bundle(entry.path, {
|
|
232
|
+
minify: config.build.minify,
|
|
233
|
+
sourceMap: config.build.sourceMap,
|
|
234
|
+
target: config.build.target,
|
|
235
|
+
externals: [
|
|
236
|
+
...config.build.externals
|
|
237
|
+
],
|
|
238
|
+
quiet: config.build.quiet
|
|
239
|
+
}),
|
|
240
|
+
catch: (error)=>new BundleFailed({
|
|
241
|
+
entry: entry.path,
|
|
242
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
const outputPath = resolve(cwd, entry.output);
|
|
246
|
+
yield* writeFile(outputPath, nccResult.code);
|
|
247
|
+
if (config.build.sourceMap && nccResult.map) yield* writeFile(`${outputPath}.map`, nccResult.map);
|
|
248
|
+
if (nccResult.assets) {
|
|
249
|
+
const outputDir = resolve(outputPath, "..");
|
|
250
|
+
for (const [assetName, assetData] of Object.entries(nccResult.assets)){
|
|
251
|
+
const assetPath = resolve(outputDir, assetName);
|
|
252
|
+
const content = "string" == typeof assetData.source ? assetData.source : assetData.source.toString("utf8");
|
|
253
|
+
yield* writeFile(assetPath, content);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const duration = Date.now() - startTime;
|
|
257
|
+
const size = getBundleSize(nccResult.code);
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
stats: {
|
|
261
|
+
entry: entry.type,
|
|
262
|
+
size,
|
|
263
|
+
duration,
|
|
264
|
+
outputPath: entry.output
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const BuildServiceLive = Layer.effect(BuildService, Effect.gen(function*() {
|
|
270
|
+
const configService = yield* ConfigService;
|
|
271
|
+
return {
|
|
272
|
+
build: (config, options = {})=>Effect.gen(function*() {
|
|
273
|
+
const cwd = options.cwd ?? process.cwd();
|
|
274
|
+
const shouldClean = options.clean ?? true;
|
|
275
|
+
const startTime = Date.now();
|
|
276
|
+
const entriesConfig = {
|
|
277
|
+
main: config.entries.main
|
|
278
|
+
};
|
|
279
|
+
if (config.entries.pre) entriesConfig.pre = config.entries.pre;
|
|
280
|
+
if (config.entries.post) entriesConfig.post = config.entries.post;
|
|
281
|
+
const entriesResult = yield* configService.detectEntries(cwd, entriesConfig);
|
|
282
|
+
if (shouldClean) yield* cleanDirectory(resolve(cwd, "dist"));
|
|
283
|
+
const entryResults = [];
|
|
284
|
+
for (const entry of entriesResult.entries){
|
|
285
|
+
const result = yield* Effect.either(bundleEntry(entry, config, cwd));
|
|
286
|
+
if ("Left" === result._tag) entryResults.push({
|
|
287
|
+
success: false,
|
|
288
|
+
error: result.left.cause
|
|
289
|
+
});
|
|
290
|
+
else entryResults.push(result.right);
|
|
291
|
+
}
|
|
292
|
+
yield* writeFile(resolve(cwd, "dist/package.json"), '{ "type": "module" }');
|
|
293
|
+
const duration = Date.now() - startTime;
|
|
294
|
+
const success = entryResults.every((r)=>r.success);
|
|
295
|
+
if (!success) return {
|
|
296
|
+
success,
|
|
297
|
+
entries: entryResults,
|
|
298
|
+
duration,
|
|
299
|
+
error: "One or more entries failed to build"
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
success,
|
|
303
|
+
entries: entryResults,
|
|
304
|
+
duration
|
|
305
|
+
};
|
|
306
|
+
}),
|
|
307
|
+
bundle: (entry, config)=>bundleEntry(entry, config, process.cwd()),
|
|
308
|
+
clean: (outputDir)=>cleanDirectory(outputDir),
|
|
309
|
+
formatResult: formatBuildResult,
|
|
310
|
+
formatBytes: formatBytes
|
|
311
|
+
};
|
|
312
|
+
}));
|
|
313
|
+
const CONFIG_FILENAMES = [
|
|
314
|
+
"action.config.ts",
|
|
315
|
+
"action.config.js",
|
|
316
|
+
"action.config.mjs"
|
|
317
|
+
];
|
|
318
|
+
const DEFAULT_ENTRIES = {
|
|
319
|
+
main: "src/main.ts",
|
|
320
|
+
pre: "src/pre.ts",
|
|
321
|
+
post: "src/post.ts"
|
|
322
|
+
};
|
|
323
|
+
function findConfigFile(cwd) {
|
|
324
|
+
for (const filename of CONFIG_FILENAMES){
|
|
325
|
+
const configPath = resolve(cwd, filename);
|
|
326
|
+
if (existsSync(configPath)) return configPath;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function detectOptionalEntry(cwd, type, explicitPath) {
|
|
330
|
+
const defaultPath = DEFAULT_ENTRIES[type];
|
|
331
|
+
const entryPath = explicitPath ?? defaultPath;
|
|
332
|
+
const absolutePath = resolve(cwd, entryPath);
|
|
333
|
+
if (existsSync(absolutePath)) return {
|
|
334
|
+
type,
|
|
335
|
+
path: absolutePath,
|
|
336
|
+
output: `dist/${type}.js`
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const ConfigServiceLive = Layer.succeed(ConfigService, {
|
|
340
|
+
load: (options = {})=>Effect.gen(function*() {
|
|
341
|
+
const cwd = options.cwd ?? process.cwd();
|
|
342
|
+
const configPath = options.configPath ?? findConfigFile(cwd);
|
|
343
|
+
if (!configPath) return {
|
|
344
|
+
config: defineConfig({}),
|
|
345
|
+
usingDefaults: true
|
|
346
|
+
};
|
|
347
|
+
if (!existsSync(configPath)) return yield* Effect.fail(new ConfigNotFound({
|
|
348
|
+
path: configPath,
|
|
349
|
+
message: "Specified config file does not exist"
|
|
350
|
+
}));
|
|
351
|
+
const absolutePath = resolve(cwd, configPath);
|
|
352
|
+
const configModule = yield* Effect.tryPromise({
|
|
353
|
+
try: async ()=>import(absolutePath),
|
|
354
|
+
catch: (error)=>new ConfigLoadFailed({
|
|
355
|
+
path: configPath,
|
|
356
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
const configInput = configModule.default;
|
|
360
|
+
if (!configInput || "object" != typeof configInput) return yield* Effect.fail(new ConfigInvalid({
|
|
361
|
+
path: configPath,
|
|
362
|
+
errors: [
|
|
363
|
+
"Config file must export a default configuration object"
|
|
364
|
+
]
|
|
365
|
+
}));
|
|
366
|
+
const config = defineConfig(configInput);
|
|
367
|
+
return {
|
|
368
|
+
config,
|
|
369
|
+
configPath,
|
|
370
|
+
usingDefaults: false
|
|
371
|
+
};
|
|
372
|
+
}),
|
|
373
|
+
resolve: (input = {})=>Effect.succeed(defineConfig(input)),
|
|
374
|
+
detectEntries: (cwd, entries)=>Effect.gen(function*() {
|
|
375
|
+
const detected = [];
|
|
376
|
+
const mainPath = entries?.main ?? DEFAULT_ENTRIES.main;
|
|
377
|
+
const absoluteMainPath = resolve(cwd, mainPath);
|
|
378
|
+
if (!existsSync(absoluteMainPath)) return yield* Effect.fail(new MainEntryMissing({
|
|
379
|
+
expectedPath: mainPath,
|
|
380
|
+
cwd
|
|
381
|
+
}));
|
|
382
|
+
detected.push({
|
|
383
|
+
type: "main",
|
|
384
|
+
path: absoluteMainPath,
|
|
385
|
+
output: "dist/main.js"
|
|
386
|
+
});
|
|
387
|
+
const preEntry = detectOptionalEntry(cwd, "pre", entries?.pre);
|
|
388
|
+
if (preEntry) detected.push(preEntry);
|
|
389
|
+
const postEntry = detectOptionalEntry(cwd, "post", entries?.post);
|
|
390
|
+
if (postEntry) detected.push(postEntry);
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
entries: detected
|
|
394
|
+
};
|
|
395
|
+
})
|
|
396
|
+
});
|
|
397
|
+
const BrandingIcon = Schema.Literal("activity", "airplay", "alert-circle", "alert-octagon", "alert-triangle", "align-center", "align-justify", "align-left", "align-right", "anchor", "aperture", "archive", "arrow-down-circle", "arrow-down-left", "arrow-down-right", "arrow-down", "arrow-left-circle", "arrow-left", "arrow-right-circle", "arrow-right", "arrow-up-circle", "arrow-up-left", "arrow-up-right", "arrow-up", "at-sign", "award", "bar-chart-2", "bar-chart", "battery-charging", "battery", "bell-off", "bell", "bluetooth", "bold", "book-open", "book", "bookmark", "box", "briefcase", "calendar", "camera-off", "camera", "cast", "check-circle", "check-square", "check", "chevron-down", "chevron-left", "chevron-right", "chevron-up", "chevrons-down", "chevrons-left", "chevrons-right", "chevrons-up", "circle", "clipboard", "clock", "cloud-drizzle", "cloud-lightning", "cloud-off", "cloud-rain", "cloud-snow", "cloud", "code", "command", "compass", "copy", "corner-down-left", "corner-down-right", "corner-left-down", "corner-left-up", "corner-right-down", "corner-right-up", "corner-up-left", "corner-up-right", "cpu", "credit-card", "crop", "crosshair", "database", "delete", "disc", "dollar-sign", "download-cloud", "download", "droplet", "edit-2", "edit-3", "edit", "external-link", "eye-off", "eye", "fast-forward", "feather", "file-minus", "file-plus", "file-text", "file", "film", "filter", "flag", "folder-minus", "folder-plus", "folder", "gift", "git-branch", "git-commit", "git-merge", "git-pull-request", "globe", "grid", "hard-drive", "hash", "headphones", "heart", "help-circle", "home", "image", "inbox", "info", "italic", "layers", "layout", "life-buoy", "link-2", "link", "list", "loader", "lock", "log-in", "log-out", "mail", "map-pin", "map", "maximize-2", "maximize", "menu", "message-circle", "message-square", "mic-off", "mic", "minimize-2", "minimize", "minus-circle", "minus-square", "minus", "monitor", "moon", "more-horizontal", "more-vertical", "move", "music", "navigation-2", "navigation", "octagon", "package", "paperclip", "pause-circle", "pause", "percent", "phone-call", "phone-forwarded", "phone-incoming", "phone-missed", "phone-off", "phone-outgoing", "phone", "pie-chart", "play-circle", "play", "plus-circle", "plus-square", "plus", "pocket", "power", "printer", "radio", "refresh-ccw", "refresh-cw", "repeat", "rewind", "rotate-ccw", "rotate-cw", "rss", "save", "scissors", "search", "send", "server", "settings", "share-2", "share", "shield-off", "shield", "shopping-bag", "shopping-cart", "shuffle", "sidebar", "skip-back", "skip-forward", "slash", "sliders", "smartphone", "speaker", "square", "star", "stop-circle", "sun", "sunrise", "sunset", "table", "tablet", "tag", "target", "terminal", "thermometer", "thumbs-down", "thumbs-up", "toggle-left", "toggle-right", "trash-2", "trash", "trending-down", "trending-up", "triangle", "truck", "tv", "type", "umbrella", "underline", "unlock", "upload-cloud", "upload", "user-check", "user-minus", "user-plus", "user-x", "user", "users", "video-off", "video", "voicemail", "volume-1", "volume-2", "volume-x", "volume", "watch", "wifi-off", "wifi", "wind", "x-circle", "x-square", "x", "zap-off", "zap", "zoom-in", "zoom-out");
|
|
398
|
+
const ActionInput = Schema.Struct({
|
|
399
|
+
description: Schema.String,
|
|
400
|
+
required: Schema.optional(Schema.Boolean),
|
|
401
|
+
default: Schema.optional(Schema.String),
|
|
402
|
+
deprecationMessage: Schema.optional(Schema.String)
|
|
403
|
+
});
|
|
404
|
+
const ActionOutput = Schema.Struct({
|
|
405
|
+
description: Schema.String
|
|
406
|
+
});
|
|
407
|
+
const Runs = Schema.Struct({
|
|
408
|
+
using: Schema.Literal("node24"),
|
|
409
|
+
main: Schema.String,
|
|
410
|
+
pre: Schema.optional(Schema.String),
|
|
411
|
+
"pre-if": Schema.optional(Schema.String),
|
|
412
|
+
post: Schema.optional(Schema.String),
|
|
413
|
+
"post-if": Schema.optional(Schema.String)
|
|
414
|
+
});
|
|
415
|
+
const BrandingColor = Schema.Literal("white", "black", "yellow", "blue", "green", "orange", "red", "purple", "gray-dark");
|
|
416
|
+
const Branding = Schema.Struct({
|
|
417
|
+
icon: Schema.optional(BrandingIcon),
|
|
418
|
+
color: Schema.optional(BrandingColor)
|
|
419
|
+
});
|
|
420
|
+
const ActionYml = Schema.Struct({
|
|
421
|
+
name: Schema.String,
|
|
422
|
+
description: Schema.String,
|
|
423
|
+
author: Schema.optional(Schema.String),
|
|
424
|
+
inputs: Schema.optional(Schema.Record({
|
|
425
|
+
key: Schema.String,
|
|
426
|
+
value: ActionInput
|
|
427
|
+
})),
|
|
428
|
+
outputs: Schema.optional(Schema.Record({
|
|
429
|
+
key: Schema.String,
|
|
430
|
+
value: ActionOutput
|
|
431
|
+
})),
|
|
432
|
+
runs: Runs,
|
|
433
|
+
branding: Schema.optional(Branding)
|
|
434
|
+
});
|
|
435
|
+
const ValidateOptionsSchema = Schema.Struct({
|
|
436
|
+
cwd: OptionalPathLikeSchema,
|
|
437
|
+
strict: Schema.optional(Schema.Boolean)
|
|
438
|
+
});
|
|
439
|
+
const ValidationErrorSchema = Schema.Struct({
|
|
440
|
+
code: Schema.String,
|
|
441
|
+
message: Schema.String,
|
|
442
|
+
file: Schema.optional(Schema.String),
|
|
443
|
+
suggestion: Schema.optional(Schema.String)
|
|
444
|
+
});
|
|
445
|
+
const ValidationWarningSchema = Schema.Struct({
|
|
446
|
+
code: Schema.String,
|
|
447
|
+
message: Schema.String,
|
|
448
|
+
file: Schema.optional(Schema.String),
|
|
449
|
+
suggestion: Schema.optional(Schema.String)
|
|
450
|
+
});
|
|
451
|
+
const ValidationResultSchema = Schema.Struct({
|
|
452
|
+
valid: Schema.Boolean,
|
|
453
|
+
errors: Schema.Array(ValidationErrorSchema),
|
|
454
|
+
warnings: Schema.Array(ValidationWarningSchema)
|
|
455
|
+
});
|
|
456
|
+
const ActionYmlResultSchema = Schema.Struct({
|
|
457
|
+
valid: Schema.Boolean,
|
|
458
|
+
content: Schema.optional(Schema.Any),
|
|
459
|
+
errors: Schema.Array(ValidationErrorSchema),
|
|
460
|
+
warnings: Schema.Array(ValidationWarningSchema)
|
|
461
|
+
});
|
|
462
|
+
const ValidationService = Context.GenericTag("ValidationService");
|
|
463
|
+
const isCI = ()=>"true" === process.env.CI || "1" === process.env.CI || "true" === process.env.GITHUB_ACTIONS;
|
|
464
|
+
const resolveStrict = (configStrict)=>configStrict ?? isCI();
|
|
465
|
+
const makeWarning = (code, message, suggestion, file)=>void 0 !== file ? {
|
|
466
|
+
code,
|
|
467
|
+
message,
|
|
468
|
+
suggestion,
|
|
469
|
+
file
|
|
470
|
+
} : {
|
|
471
|
+
code,
|
|
472
|
+
message,
|
|
473
|
+
suggestion
|
|
474
|
+
};
|
|
475
|
+
const formatSchemaErrors = (error, filePath)=>[
|
|
476
|
+
{
|
|
477
|
+
path: filePath,
|
|
478
|
+
message: ParseResult.TreeFormatter.formatErrorSync(error)
|
|
479
|
+
}
|
|
480
|
+
];
|
|
481
|
+
const ValidationServiceLive = Layer.effect(ValidationService, Effect.gen(function*() {
|
|
482
|
+
const configService = yield* ConfigService;
|
|
483
|
+
const readActionYml = (path)=>Effect.gen(function*() {
|
|
484
|
+
if (!existsSync(path)) return yield* new ActionYmlMissing({
|
|
485
|
+
cwd: path
|
|
486
|
+
});
|
|
487
|
+
const content = yield* Effect["try"]({
|
|
488
|
+
try: ()=>readFileSync(path, "utf8"),
|
|
489
|
+
catch: ()=>new ActionYmlSyntaxError({
|
|
490
|
+
path,
|
|
491
|
+
message: "Failed to read file"
|
|
492
|
+
})
|
|
493
|
+
});
|
|
494
|
+
const parsed = yield* Effect["try"]({
|
|
495
|
+
try: ()=>parse(content),
|
|
496
|
+
catch: (error)=>new ActionYmlSyntaxError({
|
|
497
|
+
path,
|
|
498
|
+
message: error instanceof Error ? error.message : "Invalid YAML syntax"
|
|
499
|
+
})
|
|
500
|
+
});
|
|
501
|
+
if (!parsed || "object" != typeof parsed) return yield* new ActionYmlSyntaxError({
|
|
502
|
+
path,
|
|
503
|
+
message: "action.yml must be an object"
|
|
504
|
+
});
|
|
505
|
+
return parsed;
|
|
506
|
+
});
|
|
507
|
+
const validateSchema = (parsed, path)=>Effect.gen(function*() {
|
|
508
|
+
const result = Schema.decodeUnknownEither(ActionYml)(parsed);
|
|
509
|
+
if ("Left" === result._tag) return yield* new ActionYmlSchemaError({
|
|
510
|
+
path,
|
|
511
|
+
errors: formatSchemaErrors(result.left, path)
|
|
512
|
+
});
|
|
513
|
+
return result.right;
|
|
514
|
+
});
|
|
515
|
+
const checkRecommendations = (content, filePath)=>{
|
|
516
|
+
const warnings = [];
|
|
517
|
+
if (content.branding) {
|
|
518
|
+
const branding = content.branding;
|
|
519
|
+
if (!branding.icon) warnings.push(makeWarning("ACTION_YML_NO_BRANDING_ICON", "Branding icon not specified", "Add branding.icon for better marketplace visibility", filePath));
|
|
520
|
+
if (!branding.color) warnings.push(makeWarning("ACTION_YML_NO_BRANDING_COLOR", "Branding color not specified", "Add branding.color for better marketplace visibility", filePath));
|
|
521
|
+
} else warnings.push(makeWarning("ACTION_YML_NO_BRANDING", "No branding configuration found", "Add branding.icon and branding.color for better marketplace visibility", filePath));
|
|
522
|
+
if (content.inputs) {
|
|
523
|
+
const inputs = content.inputs;
|
|
524
|
+
for (const [name, input] of Object.entries(inputs))if (!input.description) warnings.push(makeWarning("ACTION_YML_INPUT_NO_DESCRIPTION", `Input '${name}' has no description`, `Add a description for the '${name}' input`, filePath));
|
|
525
|
+
}
|
|
526
|
+
if (content.outputs) {
|
|
527
|
+
const outputs = content.outputs;
|
|
528
|
+
for (const [name, output] of Object.entries(outputs))if (!output.description) warnings.push(makeWarning("ACTION_YML_OUTPUT_NO_DESCRIPTION", `Output '${name}' has no description`, `Add a description for the '${name}' output`, filePath));
|
|
529
|
+
}
|
|
530
|
+
return warnings;
|
|
531
|
+
};
|
|
532
|
+
const validateActionYml = (path)=>Effect.gen(function*() {
|
|
533
|
+
const parsed = yield* readActionYml(path);
|
|
534
|
+
const content = yield* validateSchema(parsed, path);
|
|
535
|
+
const warnings = checkRecommendations(parsed, path);
|
|
536
|
+
return {
|
|
537
|
+
valid: true,
|
|
538
|
+
content,
|
|
539
|
+
errors: [],
|
|
540
|
+
warnings
|
|
541
|
+
};
|
|
542
|
+
});
|
|
543
|
+
const checkEntries = (config, cwd)=>Effect.gen(function*() {
|
|
544
|
+
const errors = [];
|
|
545
|
+
const entriesConfig = {
|
|
546
|
+
main: config.entries.main
|
|
547
|
+
};
|
|
548
|
+
if (config.entries.pre) entriesConfig.pre = config.entries.pre;
|
|
549
|
+
if (config.entries.post) entriesConfig.post = config.entries.post;
|
|
550
|
+
const result = yield* Effect.either(configService.detectEntries(cwd, entriesConfig));
|
|
551
|
+
if ("Left" === result._tag && result.left instanceof MainEntryMissing) errors.push({
|
|
552
|
+
code: "MAIN_ENTRY_MISSING",
|
|
553
|
+
message: `Main entry point not found: ${result.left.expectedPath}`,
|
|
554
|
+
file: result.left.expectedPath,
|
|
555
|
+
suggestion: "Create src/main.ts or specify a different path in config"
|
|
556
|
+
});
|
|
557
|
+
return errors;
|
|
558
|
+
});
|
|
559
|
+
const checkActionYml = (config, cwd)=>Effect.gen(function*() {
|
|
560
|
+
const errors = [];
|
|
561
|
+
const warnings = [];
|
|
562
|
+
if (!config.validation.requireActionYml) return {
|
|
563
|
+
errors,
|
|
564
|
+
warnings
|
|
565
|
+
};
|
|
566
|
+
const actionYmlPath = resolve(cwd, "action.yml");
|
|
567
|
+
const result = yield* Effect.either(validateActionYml(actionYmlPath));
|
|
568
|
+
if ("Left" === result._tag) {
|
|
569
|
+
const error = result.left;
|
|
570
|
+
if (error instanceof ActionYmlMissing) warnings.push({
|
|
571
|
+
code: "ACTION_YML_MISSING",
|
|
572
|
+
message: "action.yml not found",
|
|
573
|
+
file: actionYmlPath,
|
|
574
|
+
suggestion: "Create action.yml to define your action metadata"
|
|
575
|
+
});
|
|
576
|
+
else if (error instanceof ActionYmlSyntaxError) errors.push({
|
|
577
|
+
code: "ACTION_YML_SYNTAX_ERROR",
|
|
578
|
+
message: error.message,
|
|
579
|
+
file: error.path
|
|
580
|
+
});
|
|
581
|
+
else if (error instanceof ActionYmlSchemaError) for (const schemaError of error.errors)errors.push({
|
|
582
|
+
code: "ACTION_YML_SCHEMA_ERROR",
|
|
583
|
+
message: schemaError.message,
|
|
584
|
+
file: error.path
|
|
585
|
+
});
|
|
586
|
+
} else warnings.push(...result.right.warnings);
|
|
587
|
+
return {
|
|
588
|
+
errors,
|
|
589
|
+
warnings
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
return {
|
|
593
|
+
validate: (config, options = {})=>Effect.gen(function*() {
|
|
594
|
+
const cwd = options.cwd ?? process.cwd();
|
|
595
|
+
const strict = resolveStrict(options.strict ?? config.validation.strict);
|
|
596
|
+
const entryErrors = yield* checkEntries(config, cwd);
|
|
597
|
+
const actionYmlResult = yield* checkActionYml(config, cwd);
|
|
598
|
+
const errors = [
|
|
599
|
+
...entryErrors,
|
|
600
|
+
...actionYmlResult.errors
|
|
601
|
+
];
|
|
602
|
+
const warnings = [
|
|
603
|
+
...actionYmlResult.warnings
|
|
604
|
+
];
|
|
605
|
+
const valid = 0 === errors.length && (!strict || 0 === warnings.length);
|
|
606
|
+
if (strict && warnings.length > 0 && 0 === errors.length) return yield* new ValidationFailed({
|
|
607
|
+
errorCount: 0,
|
|
608
|
+
warningCount: warnings.length,
|
|
609
|
+
message: "Warnings treated as errors in strict mode"
|
|
610
|
+
});
|
|
611
|
+
return {
|
|
612
|
+
valid,
|
|
613
|
+
errors,
|
|
614
|
+
warnings
|
|
615
|
+
};
|
|
616
|
+
}),
|
|
617
|
+
validateActionYml,
|
|
618
|
+
formatResult: (result)=>{
|
|
619
|
+
const lines = [];
|
|
620
|
+
if (result.errors.length > 0) {
|
|
621
|
+
lines.push("Errors:");
|
|
622
|
+
for (const error of result.errors){
|
|
623
|
+
lines.push(` \u2717 ${error.message}`);
|
|
624
|
+
if (error.suggestion) lines.push(` \u2192 ${error.suggestion}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (result.warnings.length > 0) {
|
|
628
|
+
if (lines.length > 0) lines.push("");
|
|
629
|
+
lines.push("Warnings:");
|
|
630
|
+
for (const warning of result.warnings){
|
|
631
|
+
lines.push(` \u26A0 ${warning.message}`);
|
|
632
|
+
if (warning.suggestion) lines.push(` \u2192 ${warning.suggestion}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (result.valid && 0 === result.errors.length && 0 === result.warnings.length) lines.push("\u2713 All checks passed");
|
|
636
|
+
return lines.join("\n");
|
|
637
|
+
},
|
|
638
|
+
isCI: ()=>Effect.succeed(isCI()),
|
|
639
|
+
isStrict: (configStrict)=>Effect.succeed(resolveStrict(configStrict))
|
|
640
|
+
};
|
|
641
|
+
}));
|
|
642
|
+
const ConfigLayer = ConfigServiceLive;
|
|
643
|
+
const ValidationLayer = ValidationServiceLive.pipe(Layer.provide(ConfigServiceLive));
|
|
644
|
+
const BuildLayer = BuildServiceLive.pipe(Layer.provide(ConfigServiceLive));
|
|
645
|
+
const AppLayer = Layer.mergeAll(ConfigServiceLive, ValidationLayer, BuildLayer);
|
|
646
|
+
export { ActionYmlMissing, ActionYmlMissingBase, ActionYmlResultSchema, ActionYmlSchemaError, ActionYmlSchemaErrorBase, ActionYmlSyntaxError, ActionYmlSyntaxErrorBase, AppLayer, BuildFailed, BuildFailedBase, BuildLayer, BuildOptionsSchema, BuildResultSchema, BuildRunnerOptionsSchema, BuildService, BundleFailed, BundleFailedBase, BundleResultSchema, BundleStatsSchema, CleanError, CleanErrorBase, ConfigInputSchema, ConfigInvalid, ConfigInvalidBase, ConfigLayer, ConfigLoadFailed, ConfigLoadFailedBase, ConfigNotFound, ConfigNotFoundBase, ConfigSchema, ConfigService, Console, DetectEntriesResultSchema, DetectedEntrySchema, Effect, EntriesSchema, EntryFileMissing, EntryFileMissingBase, Layer, LoadConfigOptionsSchema, MainEntryMissing, MainEntryMissingBase, ManagedRuntime, Option, Schema, ValidateOptionsSchema, ValidationErrorSchema, ValidationFailed, ValidationFailedBase, ValidationLayer, ValidationOptionsSchema, ValidationResultSchema, ValidationService, ValidationWarningSchema, WriteError, WriteErrorBase, defineConfig, existsSync, mkdirSync, resolve, writeFileSync };
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Savvy Web Strategy, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|