@czfabrics/front-ready 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dist/index.cjs +129 -0
- package/.dist/index.d.ts +1 -0
- package/.dist/index.mjs +100 -0
- package/.dist/main.mjs +1337 -0
- package/.dist/src/config/schema.d.ts +45 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/package.json +96 -0
package/.dist/main.mjs
ADDED
|
@@ -0,0 +1,1337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/config/schema.ts
|
|
3
|
+
import z from "zod";
|
|
4
|
+
var NUMERIC = /* @__PURE__ */ new Set([
|
|
5
|
+
"max-age",
|
|
6
|
+
"s-maxage",
|
|
7
|
+
"stale-while-revalidate",
|
|
8
|
+
"stale-if-error"
|
|
9
|
+
]);
|
|
10
|
+
var FLAGS = /* @__PURE__ */ new Set([
|
|
11
|
+
"no-cache",
|
|
12
|
+
"no-store",
|
|
13
|
+
"no-transform",
|
|
14
|
+
"must-revalidate",
|
|
15
|
+
"proxy-revalidate",
|
|
16
|
+
"must-understand",
|
|
17
|
+
"private",
|
|
18
|
+
"public",
|
|
19
|
+
"immutable"
|
|
20
|
+
]);
|
|
21
|
+
var CacheControlSchema = z.string().superRefine((header, ctx) => {
|
|
22
|
+
for (const part of header.split(",")) {
|
|
23
|
+
const t = part.trim();
|
|
24
|
+
if (!t) continue;
|
|
25
|
+
const i = t.indexOf("=");
|
|
26
|
+
const name = (i === -1 ? t : t.slice(0, i)).trim().toLowerCase();
|
|
27
|
+
const value = i === -1 ? void 0 : t.slice(i + 1).trim().replace(/^"(.*)"$/, "$1");
|
|
28
|
+
if (NUMERIC.has(name)) {
|
|
29
|
+
if (value === void 0 || !/^\d+$/.test(value)) {
|
|
30
|
+
ctx.addIssue({
|
|
31
|
+
code: "custom",
|
|
32
|
+
message: `Directive "${name}" requires a non-negative integer`
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
} else if (FLAGS.has(name)) {
|
|
36
|
+
if (value !== void 0) {
|
|
37
|
+
ctx.addIssue({
|
|
38
|
+
code: "custom",
|
|
39
|
+
message: `Directive "${name}" does not take a value`
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: "custom",
|
|
45
|
+
message: `Unknown directive "${name}"`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
var ConfigSchema = z.object({
|
|
51
|
+
bucket: z.object({
|
|
52
|
+
namePrefix: z.string().nonempty().regex(/[a-z-]*/, "String should be kekab-case string"),
|
|
53
|
+
params: z.object({
|
|
54
|
+
region: z.string().nonempty(),
|
|
55
|
+
apiVersion: z.string().nonempty(),
|
|
56
|
+
endpoint: z.string().nonempty(),
|
|
57
|
+
forcePathStyle: z.union([z.stringbool(), z.boolean()]).optional(),
|
|
58
|
+
credentials: z.object({
|
|
59
|
+
accessKeyId: z.string().nonempty(),
|
|
60
|
+
secretAccessKey: z.string().nonempty()
|
|
61
|
+
})
|
|
62
|
+
}),
|
|
63
|
+
front: z.object({
|
|
64
|
+
cacheControlMapping: z.record(
|
|
65
|
+
z.templateLiteral([z.literal("^"), z.string(), z.literal("$")]),
|
|
66
|
+
CacheControlSchema
|
|
67
|
+
).default({
|
|
68
|
+
"^index.html$": "max-age=60, stale-while-revalidate=600, stale-if-error=86400",
|
|
69
|
+
"^assets/.+$": "max-age=86400, stale-while-revalidate=600, stale-if-error=86400",
|
|
70
|
+
"^translate/.+$": "max-age=14400, stale-while-revalidate=600, stale-if-error=86400",
|
|
71
|
+
"^.+$": "max-age=31536000, stale-while-revalidate=600, stale-if-error=86400"
|
|
72
|
+
}),
|
|
73
|
+
indexDocumentSuffix: z.string().nonempty().default("index.html"),
|
|
74
|
+
errorDocumentKey: z.string().nonempty().default("index.html")
|
|
75
|
+
}).prefault({}),
|
|
76
|
+
upload: z.object({
|
|
77
|
+
concurrency: z.number().positive().default(50)
|
|
78
|
+
}).prefault({})
|
|
79
|
+
}),
|
|
80
|
+
front: z.discriminatedUnion("type", [
|
|
81
|
+
z.object({
|
|
82
|
+
type: z.literal("angular"),
|
|
83
|
+
angular: z.object({
|
|
84
|
+
projectName: z.string().nonempty(),
|
|
85
|
+
angularJsonPath: z.string().nonempty(),
|
|
86
|
+
configurationName: z.string().nonempty().optional()
|
|
87
|
+
})
|
|
88
|
+
}),
|
|
89
|
+
z.object({
|
|
90
|
+
type: z.literal("custom"),
|
|
91
|
+
custom: z.object({
|
|
92
|
+
build: z.object({
|
|
93
|
+
command: z.string().nonempty(),
|
|
94
|
+
args: z.array(z.string().nonempty())
|
|
95
|
+
}),
|
|
96
|
+
environmentName: z.string().nonempty(),
|
|
97
|
+
buildOutputPath: z.string().nonempty()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
])
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// src/errors/config_format.ts
|
|
104
|
+
import { Data } from "effect";
|
|
105
|
+
import z2 from "zod";
|
|
106
|
+
var ConfigFormatError = class extends Data.TaggedError("ConfigFormatError") {
|
|
107
|
+
get message() {
|
|
108
|
+
return z2.prettifyError(this.cause);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/errors/interop/config_wrapper.ts
|
|
113
|
+
import { Data as Data2 } from "effect";
|
|
114
|
+
var ConfigWrapperError = class extends Data2.TaggedError("ConfigWrapperError") {
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/helpers/effect.ts
|
|
118
|
+
import { Effect } from "effect";
|
|
119
|
+
var toEffectSync = function(fn, errorClass, additionalData) {
|
|
120
|
+
return toEffect(
|
|
121
|
+
new Promise((resolve) => {
|
|
122
|
+
resolve(fn());
|
|
123
|
+
}),
|
|
124
|
+
errorClass,
|
|
125
|
+
additionalData
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
var toEffect = function(promise, errorClass, additionalData) {
|
|
129
|
+
return Effect.mapError(
|
|
130
|
+
Effect.tryPromise(() => promise),
|
|
131
|
+
(error) => new errorClass({
|
|
132
|
+
message: error.message,
|
|
133
|
+
cause: error,
|
|
134
|
+
...additionalData
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
var genFn = function(callback) {
|
|
139
|
+
return (...args) => {
|
|
140
|
+
return Effect.gen(() => callback(...args));
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/config/loader.ts
|
|
145
|
+
import { loadConfig as c12LoadConfig } from "c12";
|
|
146
|
+
import { Effect as Effect2 } from "effect";
|
|
147
|
+
var loadConfig = genFn(function* () {
|
|
148
|
+
const { config } = yield* toEffect(
|
|
149
|
+
c12LoadConfig({
|
|
150
|
+
name: "frontready",
|
|
151
|
+
configFileRequired: true
|
|
152
|
+
}),
|
|
153
|
+
ConfigWrapperError,
|
|
154
|
+
{}
|
|
155
|
+
);
|
|
156
|
+
const parsedConfig = ConfigSchema.safeParse(config);
|
|
157
|
+
if (parsedConfig.success) {
|
|
158
|
+
return parsedConfig.data;
|
|
159
|
+
}
|
|
160
|
+
return yield* Effect2.fail(
|
|
161
|
+
new ConfigFormatError({
|
|
162
|
+
cause: parsedConfig.error
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// src/contexts/cli_command.ts
|
|
168
|
+
import { Context } from "effect";
|
|
169
|
+
var CliCommandContext = class extends Context.Tag("CliCommandContext")() {
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// src/cli/cli_starter.ts
|
|
173
|
+
import { intro, log } from "@clack/prompts";
|
|
174
|
+
import { Effect as Effect3 } from "effect";
|
|
175
|
+
|
|
176
|
+
// package.json
|
|
177
|
+
var package_default = {
|
|
178
|
+
name: "@czfabrics/front-ready",
|
|
179
|
+
version: "0.1.0-beta.1",
|
|
180
|
+
author: {
|
|
181
|
+
name: "Dylan Valentin",
|
|
182
|
+
email: "dylan.valentin@ik.me",
|
|
183
|
+
url: "https://github.com/czyrok"
|
|
184
|
+
},
|
|
185
|
+
repository: {
|
|
186
|
+
type: "git",
|
|
187
|
+
url: "git+https://github.com/czfabrics/front-ready.git"
|
|
188
|
+
},
|
|
189
|
+
dependencies: {
|
|
190
|
+
"@aws-sdk/client-s3": "^3.1061.0",
|
|
191
|
+
"@clack/prompts": "^1.5.1",
|
|
192
|
+
"@effect/platform": "^0.96.1",
|
|
193
|
+
"@effect/platform-node": "^0.107.0",
|
|
194
|
+
c12: "^4.0.0-beta.5",
|
|
195
|
+
"cmd-ts": "^0.15.0",
|
|
196
|
+
effect: "^3.21.2",
|
|
197
|
+
mrmime: "^2.0.1",
|
|
198
|
+
zod: "^4.4.3"
|
|
199
|
+
},
|
|
200
|
+
devDependencies: {
|
|
201
|
+
"@appnest/readme": "^1.2.7",
|
|
202
|
+
"@types/pluralize": "^0.0.33",
|
|
203
|
+
"@typescript/analyze-trace": "^0.10.1",
|
|
204
|
+
esbuild: "^0.25.9",
|
|
205
|
+
prettier: "^3.6.2",
|
|
206
|
+
rimraf: "^6.0.1",
|
|
207
|
+
"tsc-alias": "^1.8.16",
|
|
208
|
+
tsx: "^4.21.0",
|
|
209
|
+
"type-fest": "^5.3.1",
|
|
210
|
+
typescript: "^5.9.2",
|
|
211
|
+
"vite-tsconfig-paths": "^6.1.0",
|
|
212
|
+
vitest: "^4.0.18",
|
|
213
|
+
"vscode-generate-index-standalone": "^1.7.1"
|
|
214
|
+
},
|
|
215
|
+
main: "./.dist/index.cjs",
|
|
216
|
+
module: "./.dist/index.mjs",
|
|
217
|
+
types: "./.dist/index.d.ts",
|
|
218
|
+
exports: {
|
|
219
|
+
"./package.json": "./package.json",
|
|
220
|
+
".": {
|
|
221
|
+
types: "./.dist/index.d.ts",
|
|
222
|
+
import: "./.dist/index.mjs",
|
|
223
|
+
require: "./.dist/index.cjs"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
bin: "./.dist/main.mjs",
|
|
227
|
+
bugs: {
|
|
228
|
+
url: "https://github.com/czfabrics/front-ready/issues"
|
|
229
|
+
},
|
|
230
|
+
description: "A framework-aware deployment tool for static frontends. It reads your project's build configuration (e.g. angular.json), runs the build with the configuration you choose, and pushes the compiled output to an S3 bucket \u2014 with correct content types and cache headers \u2014 so a single command takes you from source to a live, hosted site.",
|
|
231
|
+
files: [
|
|
232
|
+
"package.json",
|
|
233
|
+
"README.md",
|
|
234
|
+
"LICENSE",
|
|
235
|
+
".dist/**/*"
|
|
236
|
+
],
|
|
237
|
+
homepage: "https://github.com/czfabrics/front-ready#readme",
|
|
238
|
+
keywords: [
|
|
239
|
+
"cli",
|
|
240
|
+
"deploy",
|
|
241
|
+
"deployment",
|
|
242
|
+
"s3",
|
|
243
|
+
"aws",
|
|
244
|
+
"aws-s3",
|
|
245
|
+
"bucket",
|
|
246
|
+
"frontend",
|
|
247
|
+
"static-site",
|
|
248
|
+
"static-hosting",
|
|
249
|
+
"spa",
|
|
250
|
+
"build",
|
|
251
|
+
"angular",
|
|
252
|
+
"ci-cd",
|
|
253
|
+
"upload",
|
|
254
|
+
"typescript"
|
|
255
|
+
],
|
|
256
|
+
license: "MIT",
|
|
257
|
+
scripts: {
|
|
258
|
+
"format:all": "prettier . --write",
|
|
259
|
+
typecheck: "tsc --project ./tsconfig.typecheck.json",
|
|
260
|
+
dev: "tsx main.ts",
|
|
261
|
+
test: "vitest",
|
|
262
|
+
"test:coverage": "vitest --coverage.enabled",
|
|
263
|
+
"check:type-perf": "rimraf .tsc_traces && tsc --project ./tsconfig.typecheck.json --generateTrace .tsc_traces && analyze-trace .tsc_traces",
|
|
264
|
+
"prepare:index": "vscode-generate-index-standalone index.ts",
|
|
265
|
+
"prepare:dist": "tsx build.ts",
|
|
266
|
+
"prepare:declaration": "tsc --project ./tsconfig.declaration.json",
|
|
267
|
+
"prepare:imports": "tsc-alias --project tsconfig.declaration.json --outDir .dist",
|
|
268
|
+
build: "rimraf .dist && bun run prepare:index && bun run prepare:dist && bun run prepare:declaration && bun run prepare:imports",
|
|
269
|
+
"generate:readme": "readme generate --config .blueprint.json",
|
|
270
|
+
prepublishOnly: "bun run generate:readme && bun run build"
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/cli/cli_starter.ts
|
|
275
|
+
var startCli = function() {
|
|
276
|
+
return Effect3.gen(function* () {
|
|
277
|
+
const context = yield* CliCommandContext;
|
|
278
|
+
intro(`${package_default.name}@${package_default.version} - ${context.commandName}`);
|
|
279
|
+
const config = yield* loadConfig();
|
|
280
|
+
log.info("Configuration loaded");
|
|
281
|
+
return { config };
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/contexts/internal_config.ts
|
|
286
|
+
import { Context as Context2 } from "effect";
|
|
287
|
+
var InternalConfigContext = class extends Context2.Tag("InternalConfigContext")() {
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/contexts/front_deployment.ts
|
|
291
|
+
import { Context as Context3 } from "effect";
|
|
292
|
+
var FrontDeploymentContext = class extends Context3.Tag("FrontDeploymentContext")() {
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/errors/interop/tui_wrapper.ts
|
|
296
|
+
import { Data as Data3 } from "effect";
|
|
297
|
+
var TUiWrapperError = class extends Data3.TaggedError("TUiWrapperError") {
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/factories/bucket_name.ts
|
|
301
|
+
var makeDeploymentBucketName = function(config, bucketIdentifier) {
|
|
302
|
+
return `${config.bucket.namePrefix}-${bucketIdentifier}`;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/errors/angular.ts
|
|
306
|
+
import { Data as Data4 } from "effect";
|
|
307
|
+
import z3 from "zod";
|
|
308
|
+
var AngularJsonFormatError = class extends Data4.TaggedError("AngularJsonFormatError") {
|
|
309
|
+
get message() {
|
|
310
|
+
return z3.prettifyError(this.cause);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var AngularJsonMissingDataError = class extends Data4.TaggedError(
|
|
314
|
+
"AngularJsonMissingDataError"
|
|
315
|
+
) {
|
|
316
|
+
get message() {
|
|
317
|
+
return `Unable to resolve '${this.subject}' for '${this.projectName}'`;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/helpers/angular.ts
|
|
322
|
+
import { FileSystem } from "@effect/platform";
|
|
323
|
+
import { Effect as Effect4 } from "effect";
|
|
324
|
+
import z4 from "zod";
|
|
325
|
+
var OutputPathSchema = z4.union([
|
|
326
|
+
z4.string(),
|
|
327
|
+
z4.object({
|
|
328
|
+
base: z4.string(),
|
|
329
|
+
browser: z4.string().optional()
|
|
330
|
+
})
|
|
331
|
+
]);
|
|
332
|
+
var AngularTargetSchema = z4.object({
|
|
333
|
+
builder: z4.string().optional(),
|
|
334
|
+
options: z4.object({
|
|
335
|
+
outputPath: OutputPathSchema.optional()
|
|
336
|
+
}).optional(),
|
|
337
|
+
configurations: z4.record(z4.string(), z4.unknown()).optional()
|
|
338
|
+
});
|
|
339
|
+
var AngularProjectSchema = z4.object({
|
|
340
|
+
architect: z4.record(z4.string(), AngularTargetSchema).optional(),
|
|
341
|
+
targets: z4.record(z4.string(), AngularTargetSchema).optional()
|
|
342
|
+
});
|
|
343
|
+
var AngularJsonSchema = z4.object({
|
|
344
|
+
projects: z4.record(z4.string(), AngularProjectSchema).optional()
|
|
345
|
+
});
|
|
346
|
+
var getProjectTargets = (project) => {
|
|
347
|
+
return project.architect ?? project.targets ?? {};
|
|
348
|
+
};
|
|
349
|
+
var getAngularConfigurationNames = (angularJson) => {
|
|
350
|
+
const configurationNames = /* @__PURE__ */ new Set();
|
|
351
|
+
for (const project of Object.values(angularJson.projects ?? {})) {
|
|
352
|
+
for (const target of Object.values(getProjectTargets(project))) {
|
|
353
|
+
for (const name of Object.keys(target.configurations ?? {})) {
|
|
354
|
+
configurationNames.add(name);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return [...configurationNames];
|
|
359
|
+
};
|
|
360
|
+
var usesBrowserSubfolder = (builder) => builder?.endsWith(":application") ?? false;
|
|
361
|
+
var getAngularOutputPath = (angularJson, projectName, targetName = "build") => {
|
|
362
|
+
const projects = angularJson.projects ?? {};
|
|
363
|
+
const project = projects[projectName];
|
|
364
|
+
if (!project) return void 0;
|
|
365
|
+
const target = getProjectTargets(project)[targetName];
|
|
366
|
+
const outputPath = target?.options?.outputPath;
|
|
367
|
+
if (outputPath === void 0) return void 0;
|
|
368
|
+
if (typeof outputPath === "object") {
|
|
369
|
+
const browser = outputPath.browser ?? "browser";
|
|
370
|
+
return browser ? `${outputPath.base}/${browser}` : outputPath.base;
|
|
371
|
+
}
|
|
372
|
+
return usesBrowserSubfolder(target?.builder) ? `${outputPath}/browser` : outputPath;
|
|
373
|
+
};
|
|
374
|
+
var resolveAngularConfigurations = function(angularJsonPath, projectName) {
|
|
375
|
+
return Effect4.gen(function* () {
|
|
376
|
+
const fs = yield* FileSystem.FileSystem;
|
|
377
|
+
const content = yield* fs.readFileString(angularJsonPath);
|
|
378
|
+
const angularJson = yield* Effect4.try(() => JSON.parse(content));
|
|
379
|
+
const parsedJson = AngularJsonSchema.safeParse(angularJson);
|
|
380
|
+
if (!parsedJson.success) {
|
|
381
|
+
return yield* Effect4.fail(
|
|
382
|
+
new AngularJsonFormatError({
|
|
383
|
+
cause: parsedJson.error
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const outputPath = getAngularOutputPath(parsedJson.data, projectName);
|
|
388
|
+
if (!outputPath) {
|
|
389
|
+
return yield* Effect4.fail(
|
|
390
|
+
new AngularJsonMissingDataError({
|
|
391
|
+
subject: "output path",
|
|
392
|
+
projectName
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
configurations: getAngularConfigurationNames(parsedJson.data),
|
|
398
|
+
outputPath
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// src/factories/front_deployment_context.ts
|
|
404
|
+
import { log as log2, select } from "@clack/prompts";
|
|
405
|
+
import { Command } from "@effect/platform";
|
|
406
|
+
import { Effect as Effect5, Layer, Match } from "effect";
|
|
407
|
+
var makeFrontDeploymentContextLayer = function(rootConfig) {
|
|
408
|
+
return Layer.effect(
|
|
409
|
+
FrontDeploymentContext,
|
|
410
|
+
Match.value(rootConfig.front).pipe(
|
|
411
|
+
Match.when(
|
|
412
|
+
{ type: "angular" },
|
|
413
|
+
(config) => Effect5.gen(function* () {
|
|
414
|
+
const { configurations, outputPath } = yield* resolveAngularConfigurations(
|
|
415
|
+
config.angular.angularJsonPath,
|
|
416
|
+
config.angular.projectName
|
|
417
|
+
);
|
|
418
|
+
if (config.angular.configurationName) {
|
|
419
|
+
return {
|
|
420
|
+
command: Command.make("ng", "build", config.angular.configurationName),
|
|
421
|
+
bucketName: makeDeploymentBucketName(
|
|
422
|
+
rootConfig,
|
|
423
|
+
config.angular.configurationName
|
|
424
|
+
),
|
|
425
|
+
buildOutputPath: outputPath
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
log2.warning("Angular configuration name not found in configuration");
|
|
429
|
+
log2.message(
|
|
430
|
+
`Reading '${config.angular.angularJsonPath}' file to get available configurations`
|
|
431
|
+
);
|
|
432
|
+
const options = configurations.map((name) => ({
|
|
433
|
+
value: name,
|
|
434
|
+
label: name
|
|
435
|
+
}));
|
|
436
|
+
const configurationName = yield* toEffect(
|
|
437
|
+
select({
|
|
438
|
+
message: "Pick an Angular configuration.",
|
|
439
|
+
options
|
|
440
|
+
}),
|
|
441
|
+
TUiWrapperError,
|
|
442
|
+
{
|
|
443
|
+
uiFunction: "select"
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
return {
|
|
447
|
+
command: Command.make("ng", "build", configurationName.toString()),
|
|
448
|
+
bucketName: makeDeploymentBucketName(
|
|
449
|
+
rootConfig,
|
|
450
|
+
configurationName.toString()
|
|
451
|
+
),
|
|
452
|
+
buildOutputPath: outputPath
|
|
453
|
+
};
|
|
454
|
+
})
|
|
455
|
+
),
|
|
456
|
+
Match.when(
|
|
457
|
+
{ type: "custom" },
|
|
458
|
+
(config) => Effect5.gen(function* () {
|
|
459
|
+
return {
|
|
460
|
+
command: Command.make(
|
|
461
|
+
config.custom.build.command,
|
|
462
|
+
...config.custom.build.args
|
|
463
|
+
),
|
|
464
|
+
bucketName: makeDeploymentBucketName(
|
|
465
|
+
rootConfig,
|
|
466
|
+
config.custom.environmentName
|
|
467
|
+
),
|
|
468
|
+
buildOutputPath: config.custom.buildOutputPath
|
|
469
|
+
};
|
|
470
|
+
})
|
|
471
|
+
),
|
|
472
|
+
Match.exhaustive
|
|
473
|
+
)
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/helpers/runtime.ts
|
|
478
|
+
import { Effect as Effect6 } from "effect";
|
|
479
|
+
import * as readline from "node:readline";
|
|
480
|
+
var interruptOnCtrlC = function() {
|
|
481
|
+
return Effect6.async((resume) => {
|
|
482
|
+
readline.emitKeypressEvents(process.stdin);
|
|
483
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
484
|
+
const onKeypress = (_, key) => {
|
|
485
|
+
if (key.ctrl && key.name === "c") {
|
|
486
|
+
resume(Effect6.interrupt);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
process.stdin.on("keypress", onKeypress);
|
|
490
|
+
return Effect6.sync(() => {
|
|
491
|
+
process.stdin.off("keypress", onKeypress);
|
|
492
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
var runAndInterruptOnCtrlC = function(effect) {
|
|
497
|
+
return Effect6.raceFirst(interruptOnCtrlC(), effect);
|
|
498
|
+
};
|
|
499
|
+
var overrideProcessExit = (override) => Effect6.acquireRelease(
|
|
500
|
+
Effect6.sync(() => {
|
|
501
|
+
const original = process.exit;
|
|
502
|
+
process.exit = override;
|
|
503
|
+
return original;
|
|
504
|
+
}),
|
|
505
|
+
(original) => Effect6.sync(() => {
|
|
506
|
+
process.exit = original;
|
|
507
|
+
})
|
|
508
|
+
);
|
|
509
|
+
var interceptProcessExit = function(effect, callback) {
|
|
510
|
+
return Effect6.scoped(
|
|
511
|
+
Effect6.gen(function* () {
|
|
512
|
+
yield* overrideProcessExit((exitCode) => {
|
|
513
|
+
callback(exitCode ?? 130);
|
|
514
|
+
});
|
|
515
|
+
return yield* effect;
|
|
516
|
+
})
|
|
517
|
+
);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/contexts/file_object_bucket.ts
|
|
521
|
+
import { Context as Context4 } from "effect";
|
|
522
|
+
var FileObjectBucketContext = class extends Context4.Tag("FileObjectBucketContext")() {
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// src/contexts/file_repository.ts
|
|
526
|
+
import { Context as Context5 } from "effect";
|
|
527
|
+
var FileRepositoryContext = class extends Context5.Tag("FileRepositoryContext")() {
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/errors/file.ts
|
|
531
|
+
import { Data as Data5 } from "effect";
|
|
532
|
+
var FileError = class extends Data5.TaggedError("FileError") {
|
|
533
|
+
};
|
|
534
|
+
var FileNotFoundError = class extends Data5.TaggedError("FileNotFoundError") {
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/errors/interop/file_type_wrapper.ts
|
|
538
|
+
import { Data as Data6 } from "effect";
|
|
539
|
+
var FileTypeWrapperError = class extends Data6.TaggedError("FileTypeWrapperError") {
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// src/file/types.ts
|
|
543
|
+
import { Path } from "@effect/platform";
|
|
544
|
+
import { FileSystem as FileSystem2 } from "@effect/platform/FileSystem";
|
|
545
|
+
import { Effect as Effect7, Equal, Hash } from "effect";
|
|
546
|
+
import { lookup } from "mrmime";
|
|
547
|
+
var FileItem = {
|
|
548
|
+
new: function(data) {
|
|
549
|
+
return Effect7.gen(function* () {
|
|
550
|
+
const path = yield* Path.Path;
|
|
551
|
+
const fs = yield* FileSystem2;
|
|
552
|
+
const name = path.basename(data.relativePath);
|
|
553
|
+
const extension = path.extname(data.relativePath);
|
|
554
|
+
const absolutePath = path.resolve(data.cwd, data.relativePath);
|
|
555
|
+
const content = fs.readFile(absolutePath);
|
|
556
|
+
return {
|
|
557
|
+
name,
|
|
558
|
+
extension,
|
|
559
|
+
relativePath: data.relativePath,
|
|
560
|
+
absolutePath,
|
|
561
|
+
cwd: data.cwd,
|
|
562
|
+
content,
|
|
563
|
+
get contentType() {
|
|
564
|
+
return toEffectSync(
|
|
565
|
+
() => lookup(extension) ?? "application/octet-stream",
|
|
566
|
+
FileTypeWrapperError,
|
|
567
|
+
{
|
|
568
|
+
file: this
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
},
|
|
572
|
+
get length() {
|
|
573
|
+
return 1;
|
|
574
|
+
},
|
|
575
|
+
[Symbol.iterator]() {
|
|
576
|
+
let isFirst = true;
|
|
577
|
+
return {
|
|
578
|
+
next: () => {
|
|
579
|
+
if (isFirst) {
|
|
580
|
+
isFirst = false;
|
|
581
|
+
return { value: this, done: false };
|
|
582
|
+
}
|
|
583
|
+
return { value: void 0, done: true };
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
var FileTreeTypeId = Symbol.for("FileTree");
|
|
592
|
+
var isFileTree = function(thing) {
|
|
593
|
+
return typeof thing === "object" && thing !== null && FileTreeTypeId in thing;
|
|
594
|
+
};
|
|
595
|
+
var FileTree = {
|
|
596
|
+
new: function(data) {
|
|
597
|
+
return Effect7.gen(function* () {
|
|
598
|
+
const path = yield* Path.Path;
|
|
599
|
+
return {
|
|
600
|
+
[FileTreeTypeId]: FileTreeTypeId,
|
|
601
|
+
name: path.basename(data.relativePath),
|
|
602
|
+
relativePath: data.relativePath,
|
|
603
|
+
absolutePath: path.resolve(data.cwd, data.relativePath),
|
|
604
|
+
cwd: data.cwd,
|
|
605
|
+
items: data.items,
|
|
606
|
+
get length() {
|
|
607
|
+
return this.items.length;
|
|
608
|
+
},
|
|
609
|
+
append: function(newItems) {
|
|
610
|
+
return FileTree.new({
|
|
611
|
+
...this,
|
|
612
|
+
items: [...this.items, ...newItems]
|
|
613
|
+
});
|
|
614
|
+
},
|
|
615
|
+
[Symbol.iterator]() {
|
|
616
|
+
let currentIndex = 0;
|
|
617
|
+
return {
|
|
618
|
+
next: () => {
|
|
619
|
+
if (currentIndex < this.items.length) {
|
|
620
|
+
const currentItem = this.items[currentIndex];
|
|
621
|
+
currentIndex++;
|
|
622
|
+
return { value: currentItem, done: false };
|
|
623
|
+
}
|
|
624
|
+
return { value: void 0, done: true };
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
[Equal.symbol](that) {
|
|
629
|
+
return isFileTree(that) && this.absolutePath === that.absolutePath;
|
|
630
|
+
},
|
|
631
|
+
[Hash.symbol]() {
|
|
632
|
+
return Hash.string(this.absolutePath);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
is: function(thing) {
|
|
638
|
+
return isFileTree(thing);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// src/file/repository.ts
|
|
643
|
+
import { Path as Path2 } from "@effect/platform";
|
|
644
|
+
import { FileSystem as FileSystem3 } from "@effect/platform/FileSystem";
|
|
645
|
+
import { Effect as Effect8 } from "effect";
|
|
646
|
+
var FileRepository = class extends Effect8.Service()("FileRepository", {
|
|
647
|
+
effect: Effect8.gen(function* () {
|
|
648
|
+
const fs = yield* FileSystem3;
|
|
649
|
+
const path = yield* Path2.Path;
|
|
650
|
+
const context = yield* FileRepositoryContext;
|
|
651
|
+
return {
|
|
652
|
+
listFiles: () => Effect8.gen(function* () {
|
|
653
|
+
const paths = yield* fs.readDirectory(context.cwd, {
|
|
654
|
+
recursive: true
|
|
655
|
+
});
|
|
656
|
+
const filePaths = yield* Effect8.filter(
|
|
657
|
+
paths,
|
|
658
|
+
(relativePath) => fs.stat(path.resolve(context.cwd, relativePath)).pipe(Effect8.map((info) => info.type === "File")),
|
|
659
|
+
{ concurrency: "unbounded" }
|
|
660
|
+
);
|
|
661
|
+
return yield* Effect8.forEach(filePaths, (filePath) => {
|
|
662
|
+
return FileItem.new({
|
|
663
|
+
relativePath: filePath,
|
|
664
|
+
cwd: context.cwd
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
})
|
|
668
|
+
};
|
|
669
|
+
}),
|
|
670
|
+
dependencies: []
|
|
671
|
+
}) {
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// src/errors/interop/file_object_wrapper.ts
|
|
675
|
+
import { Data as Data7 } from "effect";
|
|
676
|
+
var FileObjectWrapperError = class extends Data7.TaggedError("FileObjectWrapperError") {
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/file_object/api_instance.ts
|
|
680
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
681
|
+
import { Context as Context6, Effect as Effect9, Layer as Layer2 } from "effect";
|
|
682
|
+
var FileObjectApiInstance = class extends Context6.Tag("FileObjectApiInstance")() {
|
|
683
|
+
};
|
|
684
|
+
var FileObjectApiInstanceLive = Layer2.effect(
|
|
685
|
+
FileObjectApiInstance,
|
|
686
|
+
Effect9.gen(function* () {
|
|
687
|
+
const config = yield* InternalConfigContext;
|
|
688
|
+
return new S3Client({
|
|
689
|
+
region: config.bucket.params.region,
|
|
690
|
+
apiVersion: config.bucket.params.apiVersion,
|
|
691
|
+
endpoint: config.bucket.params.endpoint,
|
|
692
|
+
forcePathStyle: config.bucket.params.forcePathStyle,
|
|
693
|
+
credentials: {
|
|
694
|
+
accessKeyId: config.bucket.params.credentials.accessKeyId,
|
|
695
|
+
secretAccessKey: config.bucket.params.credentials.secretAccessKey
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
})
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
// src/file_object/repository.ts
|
|
702
|
+
import {
|
|
703
|
+
CreateBucketCommand,
|
|
704
|
+
HeadBucketCommand,
|
|
705
|
+
PutBucketAclCommand,
|
|
706
|
+
PutBucketWebsiteCommand,
|
|
707
|
+
PutObjectCommand
|
|
708
|
+
} from "@aws-sdk/client-s3";
|
|
709
|
+
import { Effect as Effect10 } from "effect";
|
|
710
|
+
var FileObjectRepository = class extends Effect10.Service()(
|
|
711
|
+
"FileObjectRepository",
|
|
712
|
+
{
|
|
713
|
+
effect: Effect10.gen(function* () {
|
|
714
|
+
const apiInstance = yield* FileObjectApiInstance;
|
|
715
|
+
const context = yield* FileObjectBucketContext;
|
|
716
|
+
return {
|
|
717
|
+
createBucket: genFn(function* () {
|
|
718
|
+
const command3 = new CreateBucketCommand({
|
|
719
|
+
Bucket: context.bucketName,
|
|
720
|
+
CreateBucketConfiguration: {
|
|
721
|
+
LocationConstraint: context.region
|
|
722
|
+
},
|
|
723
|
+
ObjectOwnership: "BucketOwnerEnforced"
|
|
724
|
+
});
|
|
725
|
+
yield* toEffect(apiInstance.send(command3), FileObjectWrapperError, {});
|
|
726
|
+
}),
|
|
727
|
+
doesBucketExist: genFn(function* () {
|
|
728
|
+
const command3 = new HeadBucketCommand({
|
|
729
|
+
Bucket: context.bucketName
|
|
730
|
+
});
|
|
731
|
+
const doesExist = yield* Effect10.matchEffect(
|
|
732
|
+
toEffect(apiInstance.send(command3), FileObjectWrapperError, {}),
|
|
733
|
+
{
|
|
734
|
+
onFailure: (error) => {
|
|
735
|
+
if (error.cause.name === "NotFound") {
|
|
736
|
+
return Effect10.succeed(false);
|
|
737
|
+
}
|
|
738
|
+
return Effect10.fail(error);
|
|
739
|
+
},
|
|
740
|
+
onSuccess: () => Effect10.succeed(true)
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
return doesExist;
|
|
744
|
+
}),
|
|
745
|
+
setPublicReadAclOnBucket: genFn(function* () {
|
|
746
|
+
const command3 = new PutBucketAclCommand({
|
|
747
|
+
Bucket: context.bucketName,
|
|
748
|
+
ACL: "public-read"
|
|
749
|
+
});
|
|
750
|
+
yield* toEffect(apiInstance.send(command3), FileObjectWrapperError, {});
|
|
751
|
+
}),
|
|
752
|
+
setWebsiteConfigurationOnBucket: genFn(function* (indexFileKeySuffix, errorFileKey) {
|
|
753
|
+
const command3 = new PutBucketWebsiteCommand({
|
|
754
|
+
Bucket: context.bucketName,
|
|
755
|
+
WebsiteConfiguration: {
|
|
756
|
+
IndexDocument: {
|
|
757
|
+
Suffix: indexFileKeySuffix
|
|
758
|
+
},
|
|
759
|
+
ErrorDocument: {
|
|
760
|
+
Key: errorFileKey
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
yield* toEffect(apiInstance.send(command3), FileObjectWrapperError, {});
|
|
765
|
+
}),
|
|
766
|
+
putObject: genFn(function* (file) {
|
|
767
|
+
const command3 = new PutObjectCommand({
|
|
768
|
+
ACL: "public-read",
|
|
769
|
+
Bucket: context.bucketName,
|
|
770
|
+
Key: file.key,
|
|
771
|
+
Body: file.content,
|
|
772
|
+
ContentEncoding: "binary",
|
|
773
|
+
ContentType: file.contentType,
|
|
774
|
+
CacheControl: file.cacheControlValue
|
|
775
|
+
});
|
|
776
|
+
yield* toEffect(apiInstance.send(command3), FileObjectWrapperError, {});
|
|
777
|
+
})
|
|
778
|
+
};
|
|
779
|
+
}),
|
|
780
|
+
dependencies: [FileObjectApiInstanceLive]
|
|
781
|
+
}
|
|
782
|
+
) {
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// src/helpers/file.ts
|
|
786
|
+
import { Path as Path3 } from "@effect/platform";
|
|
787
|
+
import { Array as Array2, Effect as Effect11, Option } from "effect";
|
|
788
|
+
var fileIntoObject = function(file) {
|
|
789
|
+
return Effect11.gen(function* () {
|
|
790
|
+
const content = yield* file.content;
|
|
791
|
+
const contentType = yield* file.contentType;
|
|
792
|
+
return {
|
|
793
|
+
key: file.relativePath,
|
|
794
|
+
content,
|
|
795
|
+
contentType,
|
|
796
|
+
cacheControlValue: void 0
|
|
797
|
+
};
|
|
798
|
+
});
|
|
799
|
+
};
|
|
800
|
+
var detectAndFillCacheControl = function(object, cacheControlMapping) {
|
|
801
|
+
for (const [keyRawRegExp, cacheControlValue] of Object.entries(cacheControlMapping)) {
|
|
802
|
+
const isCorrectCacheControl = new RegExp(keyRawRegExp).test(object.key);
|
|
803
|
+
if (isCorrectCacheControl) {
|
|
804
|
+
return {
|
|
805
|
+
...object,
|
|
806
|
+
cacheControlValue
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return object;
|
|
811
|
+
};
|
|
812
|
+
var extractFirstFolderFromPath = function(relativePath) {
|
|
813
|
+
return Effect11.gen(function* () {
|
|
814
|
+
const path = yield* Path3.Path;
|
|
815
|
+
const segments = path.normalize(relativePath).replace(/\\/g, "/").split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
816
|
+
if (segments.length <= 1) {
|
|
817
|
+
return Option.none();
|
|
818
|
+
}
|
|
819
|
+
return Array2.head(segments);
|
|
820
|
+
});
|
|
821
|
+
};
|
|
822
|
+
var extractRootFileTree = genFn(function* (file, cwd) {
|
|
823
|
+
const firstFolder = yield* extractFirstFolderFromPath(file.relativePath);
|
|
824
|
+
if (Option.isNone(firstFolder)) {
|
|
825
|
+
return Option.none();
|
|
826
|
+
}
|
|
827
|
+
return Option.some(
|
|
828
|
+
yield* FileTree.new({ relativePath: firstFolder.value, cwd, items: [file] })
|
|
829
|
+
);
|
|
830
|
+
});
|
|
831
|
+
var hasMinOneFile = function(components) {
|
|
832
|
+
for (const component of components) {
|
|
833
|
+
if (component.length > 0) {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return false;
|
|
838
|
+
};
|
|
839
|
+
var excludeRootFile = function(components, filenameToExclude) {
|
|
840
|
+
return Effect11.sync(function* () {
|
|
841
|
+
for (const component of components) {
|
|
842
|
+
if (!FileTree.is(component) && component.name === filenameToExclude) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
yield component;
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
};
|
|
849
|
+
var extractRootFile = genFn(function* (components, filenameToPick) {
|
|
850
|
+
for (const component of components) {
|
|
851
|
+
if (!FileTree.is(component) && component.name === filenameToPick) {
|
|
852
|
+
return component;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return yield* Effect11.fail(
|
|
856
|
+
new FileNotFoundError({ message: `File not found`, file: { name: filenameToPick } })
|
|
857
|
+
);
|
|
858
|
+
});
|
|
859
|
+
var pickIndexHtml = genFn(function* (components) {
|
|
860
|
+
const alteredComponents = yield* excludeRootFile(components, "index.html");
|
|
861
|
+
const indexHtml = yield* extractRootFile(components, "index.html");
|
|
862
|
+
return {
|
|
863
|
+
components: alteredComponents,
|
|
864
|
+
indexHtml
|
|
865
|
+
};
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// src/front/service.ts
|
|
869
|
+
import { Effect as Effect12, HashMap, Layer as Layer3, Option as Option2 } from "effect";
|
|
870
|
+
var FileRepositoryContextLive = Layer3.effect(
|
|
871
|
+
FileRepositoryContext,
|
|
872
|
+
Effect12.gen(function* () {
|
|
873
|
+
const front = yield* FrontDeploymentContext;
|
|
874
|
+
return { cwd: front.buildOutputPath };
|
|
875
|
+
})
|
|
876
|
+
);
|
|
877
|
+
var FileObjectBucketContextLive = Layer3.effect(
|
|
878
|
+
FileObjectBucketContext,
|
|
879
|
+
Effect12.gen(function* () {
|
|
880
|
+
const front = yield* FrontDeploymentContext;
|
|
881
|
+
const config = yield* InternalConfigContext;
|
|
882
|
+
return {
|
|
883
|
+
bucketName: front.bucketName,
|
|
884
|
+
region: config.bucket.params.region
|
|
885
|
+
};
|
|
886
|
+
})
|
|
887
|
+
);
|
|
888
|
+
var FrontFileObjectService = class extends Effect12.Service()(
|
|
889
|
+
"FrontFileObjectService",
|
|
890
|
+
{
|
|
891
|
+
effect: Effect12.gen(function* () {
|
|
892
|
+
const configContext = yield* InternalConfigContext;
|
|
893
|
+
const fileRepository = yield* FileRepository;
|
|
894
|
+
const fileObjectRepository = yield* FileObjectRepository;
|
|
895
|
+
const frontContext = yield* FrontDeploymentContext;
|
|
896
|
+
return {
|
|
897
|
+
doesFrontBucketExist: () => {
|
|
898
|
+
return fileObjectRepository.doesBucketExist();
|
|
899
|
+
},
|
|
900
|
+
listFrontBuildFiles: () => {
|
|
901
|
+
return fileRepository.listFiles();
|
|
902
|
+
},
|
|
903
|
+
listFrontBuildFileByRootComponents: genFn(function* () {
|
|
904
|
+
const files = yield* fileRepository.listFiles();
|
|
905
|
+
let map = HashMap.empty();
|
|
906
|
+
for (const file of files) {
|
|
907
|
+
const resolvedFileTree = yield* extractRootFileTree(
|
|
908
|
+
file,
|
|
909
|
+
frontContext.buildOutputPath
|
|
910
|
+
);
|
|
911
|
+
if (Option2.isNone(resolvedFileTree)) {
|
|
912
|
+
map = HashMap.set(map, file.absolutePath, file);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const existingFileTree = HashMap.get(map, resolvedFileTree.value.absolutePath);
|
|
916
|
+
if (Option2.isNone(existingFileTree)) {
|
|
917
|
+
map = HashMap.set(
|
|
918
|
+
map,
|
|
919
|
+
resolvedFileTree.value.absolutePath,
|
|
920
|
+
resolvedFileTree.value
|
|
921
|
+
);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (!FileTree.is(existingFileTree.value)) {
|
|
925
|
+
return yield* Effect12.fail(
|
|
926
|
+
new FileError({
|
|
927
|
+
message: `The same path was resolved both file item & file tree`,
|
|
928
|
+
file: existingFileTree.value
|
|
929
|
+
})
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
const updatedFileTree = yield* existingFileTree.value.append([file]);
|
|
933
|
+
map = HashMap.set(map, existingFileTree.value.absolutePath, updatedFileTree);
|
|
934
|
+
}
|
|
935
|
+
return HashMap.values(map);
|
|
936
|
+
}),
|
|
937
|
+
createFrontBucket: genFn(function* () {
|
|
938
|
+
yield* fileObjectRepository.createBucket();
|
|
939
|
+
yield* fileObjectRepository.setPublicReadAclOnBucket();
|
|
940
|
+
yield* fileObjectRepository.setWebsiteConfigurationOnBucket(
|
|
941
|
+
configContext.bucket.front.indexDocumentSuffix,
|
|
942
|
+
configContext.bucket.front.errorDocumentKey
|
|
943
|
+
);
|
|
944
|
+
}),
|
|
945
|
+
uploadFrontFileToBucket: genFn(function* (file) {
|
|
946
|
+
const object = detectAndFillCacheControl(
|
|
947
|
+
yield* fileIntoObject(file),
|
|
948
|
+
configContext.bucket.front.cacheControlMapping
|
|
949
|
+
);
|
|
950
|
+
yield* fileObjectRepository.putObject(object);
|
|
951
|
+
})
|
|
952
|
+
};
|
|
953
|
+
}),
|
|
954
|
+
dependencies: [
|
|
955
|
+
FileRepository.Default.pipe(Layer3.provide(FileRepositoryContextLive)),
|
|
956
|
+
FileObjectRepository.Default.pipe(Layer3.provide(FileObjectBucketContextLive))
|
|
957
|
+
]
|
|
958
|
+
}
|
|
959
|
+
) {
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
// src/ui/confirm.ts
|
|
963
|
+
import { confirm } from "@clack/prompts";
|
|
964
|
+
var genConfirmUi = function({
|
|
965
|
+
question,
|
|
966
|
+
initialValue
|
|
967
|
+
}) {
|
|
968
|
+
return toEffect(
|
|
969
|
+
confirm({
|
|
970
|
+
message: question,
|
|
971
|
+
initialValue
|
|
972
|
+
}),
|
|
973
|
+
TUiWrapperError,
|
|
974
|
+
{
|
|
975
|
+
uiFunction: "confirm"
|
|
976
|
+
}
|
|
977
|
+
);
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
// src/ui/loader.ts
|
|
981
|
+
import { spinner } from "@clack/prompts";
|
|
982
|
+
import { Effect as Effect13 } from "effect";
|
|
983
|
+
var genLoaderUi = function({
|
|
984
|
+
process: process2,
|
|
985
|
+
message
|
|
986
|
+
}) {
|
|
987
|
+
return Effect13.gen(function* () {
|
|
988
|
+
const spin = spinner({
|
|
989
|
+
indicator: "timer",
|
|
990
|
+
frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
991
|
+
delay: 80,
|
|
992
|
+
styleFrame: (frame) => `\x1B[35m${frame}\x1B[0m`
|
|
993
|
+
});
|
|
994
|
+
return yield* interceptProcessExit(
|
|
995
|
+
Effect13.gen(function* () {
|
|
996
|
+
spin.start(message.resolveStart());
|
|
997
|
+
const [duration, result] = yield* Effect13.timed(
|
|
998
|
+
process2(spin.message).pipe(
|
|
999
|
+
Effect13.catchAll(
|
|
1000
|
+
(error) => Effect13.gen(function* () {
|
|
1001
|
+
spin.error(message.resolveError(error));
|
|
1002
|
+
return yield* Effect13.fail(error);
|
|
1003
|
+
})
|
|
1004
|
+
)
|
|
1005
|
+
)
|
|
1006
|
+
);
|
|
1007
|
+
spin.stop(message.resolveEnd(duration));
|
|
1008
|
+
return result;
|
|
1009
|
+
}),
|
|
1010
|
+
(exitCode) => spin.cancel(message.resolveCancel(exitCode))
|
|
1011
|
+
);
|
|
1012
|
+
});
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// src/use_cases/create_front_bucket.ts
|
|
1016
|
+
import { log as log3 } from "@clack/prompts";
|
|
1017
|
+
import { Duration as Duration2, Effect as Effect14 } from "effect";
|
|
1018
|
+
var CreateFrontBucketUseCase = class extends Effect14.Service()(
|
|
1019
|
+
"CreateFrontBucketUseCase",
|
|
1020
|
+
{
|
|
1021
|
+
effect: Effect14.gen(function* () {
|
|
1022
|
+
const deploymentContext = yield* FrontDeploymentContext;
|
|
1023
|
+
const frontService = yield* FrontFileObjectService;
|
|
1024
|
+
return {
|
|
1025
|
+
run: genFn(function* () {
|
|
1026
|
+
const doesBucketExist = yield* frontService.doesFrontBucketExist();
|
|
1027
|
+
if (doesBucketExist) {
|
|
1028
|
+
log3.info(`The bucket '${deploymentContext.bucketName}' already exist`);
|
|
1029
|
+
return yield* Effect14.interrupt;
|
|
1030
|
+
}
|
|
1031
|
+
const shouldContinue = yield* genConfirmUi({
|
|
1032
|
+
question: `Do you want to create the bucket '${deploymentContext.bucketName}'?`,
|
|
1033
|
+
initialValue: true
|
|
1034
|
+
});
|
|
1035
|
+
if (!shouldContinue) {
|
|
1036
|
+
return yield* Effect14.interrupt;
|
|
1037
|
+
}
|
|
1038
|
+
yield* genLoaderUi({
|
|
1039
|
+
process: frontService.createFrontBucket,
|
|
1040
|
+
message: {
|
|
1041
|
+
resolveStart: () => "Creating front bucket",
|
|
1042
|
+
resolveError: (error) => `Creation failed: ${error.message}`,
|
|
1043
|
+
resolveCancel: () => "Creation canceled",
|
|
1044
|
+
resolveEnd: (duration) => `Creation finished in ${Duration2.toMillis(duration)}ms`
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
})
|
|
1048
|
+
};
|
|
1049
|
+
}),
|
|
1050
|
+
dependencies: [FrontFileObjectService.Default]
|
|
1051
|
+
}
|
|
1052
|
+
) {
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// src/cli/create_front_bucket_command.ts
|
|
1056
|
+
import { cancel, log as log4, outro } from "@clack/prompts";
|
|
1057
|
+
import { command } from "cmd-ts";
|
|
1058
|
+
import { Effect as Effect15, Inspectable, Layer as Layer4 } from "effect";
|
|
1059
|
+
var createFrontBucketCommand = command({
|
|
1060
|
+
name: "create",
|
|
1061
|
+
description: "TODO",
|
|
1062
|
+
args: {},
|
|
1063
|
+
handler: function() {
|
|
1064
|
+
const CommandContextLayer = Layer4.succeed(CliCommandContext, {
|
|
1065
|
+
commandName: this.name
|
|
1066
|
+
});
|
|
1067
|
+
return Effect15.gen(function* () {
|
|
1068
|
+
const { config } = yield* startCli();
|
|
1069
|
+
const ConfigContextLayer = Layer4.succeed(InternalConfigContext, config);
|
|
1070
|
+
const DeploymentContextLayer = makeFrontDeploymentContextLayer(config);
|
|
1071
|
+
yield* Effect15.gen(function* () {
|
|
1072
|
+
const useCase = yield* CreateFrontBucketUseCase;
|
|
1073
|
+
yield* runAndInterruptOnCtrlC(useCase.run());
|
|
1074
|
+
outro(`Creation finished`);
|
|
1075
|
+
}).pipe(
|
|
1076
|
+
Effect15.provide(CreateFrontBucketUseCase.Default),
|
|
1077
|
+
Effect15.provide(ConfigContextLayer),
|
|
1078
|
+
Effect15.provide(DeploymentContextLayer),
|
|
1079
|
+
Effect15.onInterrupt(() => Effect15.sync(() => cancel(`Creation canceled`))),
|
|
1080
|
+
Effect15.catchAll(
|
|
1081
|
+
(error) => Effect15.gen(function* () {
|
|
1082
|
+
log4.error(error.message);
|
|
1083
|
+
log4.message(Inspectable.toStringUnknown(error));
|
|
1084
|
+
outro(`Creation aborted`);
|
|
1085
|
+
})
|
|
1086
|
+
)
|
|
1087
|
+
);
|
|
1088
|
+
}).pipe(Effect15.provide(CommandContextLayer));
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
// src/errors/front.ts
|
|
1093
|
+
import { Data as Data8 } from "effect";
|
|
1094
|
+
var FrontError = class extends Data8.TaggedError("FrontError") {
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// src/errors/command.ts
|
|
1098
|
+
import { Data as Data9 } from "effect";
|
|
1099
|
+
var CommandError = class extends Data9.TaggedError("CommandError") {
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
// src/helpers/command.ts
|
|
1103
|
+
import { Command as Command2 } from "@effect/platform";
|
|
1104
|
+
import { Effect as Effect16, pipe, Stream } from "effect";
|
|
1105
|
+
var runCommand = function(command3, logMessage) {
|
|
1106
|
+
return Effect16.scoped(
|
|
1107
|
+
pipe(
|
|
1108
|
+
Command2.start(command3.pipe(Command2.runInShell(true))),
|
|
1109
|
+
Effect16.flatMap(
|
|
1110
|
+
(process2) => Effect16.all(
|
|
1111
|
+
[
|
|
1112
|
+
process2.exitCode,
|
|
1113
|
+
process2.stdout.pipe(
|
|
1114
|
+
Stream.decodeText(),
|
|
1115
|
+
Stream.runForEach((chunk) => Effect16.sync(() => logMessage(chunk)))
|
|
1116
|
+
),
|
|
1117
|
+
process2.stderr.pipe(
|
|
1118
|
+
Stream.decodeText(),
|
|
1119
|
+
Stream.runForEach((chunk) => Effect16.sync(() => logMessage(chunk)))
|
|
1120
|
+
)
|
|
1121
|
+
],
|
|
1122
|
+
{ concurrency: 3 }
|
|
1123
|
+
)
|
|
1124
|
+
),
|
|
1125
|
+
Effect16.map((result) => result[0])
|
|
1126
|
+
)
|
|
1127
|
+
);
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
// src/ui/command.ts
|
|
1131
|
+
import { Effect as Effect17 } from "effect";
|
|
1132
|
+
var genCommandUi = function({
|
|
1133
|
+
command: command3,
|
|
1134
|
+
message
|
|
1135
|
+
}) {
|
|
1136
|
+
return genLoaderUi({
|
|
1137
|
+
process: (logMessage) => Effect17.gen(function* () {
|
|
1138
|
+
const exitCode = yield* runCommand(command3, logMessage);
|
|
1139
|
+
if (exitCode !== 0) {
|
|
1140
|
+
return yield* Effect17.fail(
|
|
1141
|
+
new CommandError({
|
|
1142
|
+
message: `Command exited with code '${exitCode}'`,
|
|
1143
|
+
code: exitCode
|
|
1144
|
+
})
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
return exitCode;
|
|
1148
|
+
}),
|
|
1149
|
+
message
|
|
1150
|
+
});
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
// src/ui/tasks.ts
|
|
1154
|
+
import { taskLog } from "@clack/prompts";
|
|
1155
|
+
import { Effect as Effect18 } from "effect";
|
|
1156
|
+
var genTaskLogsUi = ({
|
|
1157
|
+
title,
|
|
1158
|
+
itemGroups,
|
|
1159
|
+
processItem,
|
|
1160
|
+
message,
|
|
1161
|
+
subTaskConcurrency
|
|
1162
|
+
}) => {
|
|
1163
|
+
return Effect18.gen(function* () {
|
|
1164
|
+
const log7 = taskLog({
|
|
1165
|
+
title,
|
|
1166
|
+
retainLog: false
|
|
1167
|
+
});
|
|
1168
|
+
const tasks = Array.from(itemGroups).map((group) => {
|
|
1169
|
+
return Effect18.gen(function* () {
|
|
1170
|
+
const groupLog = log7.group(message.resolveGroupTitle(group));
|
|
1171
|
+
const [duration2] = yield* Effect18.timed(
|
|
1172
|
+
Effect18.gen(function* () {
|
|
1173
|
+
const itemProcesses = Array.from(group).map(
|
|
1174
|
+
(item) => Effect18.gen(function* () {
|
|
1175
|
+
yield* processItem(item);
|
|
1176
|
+
groupLog.message(message.resolveItem(item));
|
|
1177
|
+
})
|
|
1178
|
+
);
|
|
1179
|
+
yield* Effect18.all(itemProcesses, {
|
|
1180
|
+
concurrency: subTaskConcurrency
|
|
1181
|
+
});
|
|
1182
|
+
}).pipe(
|
|
1183
|
+
Effect18.catchAll(
|
|
1184
|
+
(error) => Effect18.gen(function* () {
|
|
1185
|
+
groupLog.error(message.resolveGroupError(group, error));
|
|
1186
|
+
return yield* Effect18.fail(error);
|
|
1187
|
+
})
|
|
1188
|
+
)
|
|
1189
|
+
)
|
|
1190
|
+
);
|
|
1191
|
+
groupLog.success(message.resolveGroupSuccess(group, duration2));
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
const [duration] = yield* Effect18.timed(
|
|
1195
|
+
Effect18.all(tasks, {
|
|
1196
|
+
concurrency: "unbounded"
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
log7.success(message.resolveSuccess(duration));
|
|
1200
|
+
});
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
// src/use_cases/deploy_on_bucket.ts
|
|
1204
|
+
import { log as log5 } from "@clack/prompts";
|
|
1205
|
+
import { Duration as Duration5, Effect as Effect19 } from "effect";
|
|
1206
|
+
var DeployOnBucketUseCase = class extends Effect19.Service()(
|
|
1207
|
+
"DeployOnBucketUseCase",
|
|
1208
|
+
{
|
|
1209
|
+
effect: Effect19.gen(function* () {
|
|
1210
|
+
const frontService = yield* FrontFileObjectService;
|
|
1211
|
+
const deploymentContext = yield* FrontDeploymentContext;
|
|
1212
|
+
return {
|
|
1213
|
+
run: genFn(function* () {
|
|
1214
|
+
const shouldContinue = yield* genConfirmUi({
|
|
1215
|
+
question: `Do you want to build and upload '${deploymentContext.buildOutputPath}' to bucket '${deploymentContext.bucketName}'?`,
|
|
1216
|
+
initialValue: false
|
|
1217
|
+
});
|
|
1218
|
+
if (!shouldContinue) {
|
|
1219
|
+
return yield* Effect19.interrupt;
|
|
1220
|
+
}
|
|
1221
|
+
const doesBucketExist = yield* frontService.doesFrontBucketExist();
|
|
1222
|
+
if (!doesBucketExist) {
|
|
1223
|
+
log5.error(`Bucket '${deploymentContext.bucketName}' should exist`);
|
|
1224
|
+
return yield* Effect19.interrupt;
|
|
1225
|
+
}
|
|
1226
|
+
yield* genCommandUi({
|
|
1227
|
+
command: deploymentContext.command,
|
|
1228
|
+
message: {
|
|
1229
|
+
resolveStart: () => "Building front",
|
|
1230
|
+
resolveError: (error) => `Build failed: ${error.message}`,
|
|
1231
|
+
resolveCancel: () => "Build canceled",
|
|
1232
|
+
resolveEnd: (duration) => `Build finished in ${Duration5.toMillis(duration)}ms`
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
const configContext = yield* InternalConfigContext;
|
|
1236
|
+
const frontBuildFileComponents = yield* frontService.listFrontBuildFileByRootComponents();
|
|
1237
|
+
const hasOneFile = hasMinOneFile(frontBuildFileComponents);
|
|
1238
|
+
if (!hasOneFile) {
|
|
1239
|
+
log5.error("Front files should contain minimum one file");
|
|
1240
|
+
return yield* Effect19.interrupt;
|
|
1241
|
+
}
|
|
1242
|
+
const { components: frontBuildFileComponentRests, indexHtml } = yield* pickIndexHtml(frontBuildFileComponents).pipe(
|
|
1243
|
+
Effect19.catchTag(
|
|
1244
|
+
"FileNotFoundError",
|
|
1245
|
+
() => new FrontError({
|
|
1246
|
+
message: "'index.html' file should exist in the front build folder",
|
|
1247
|
+
config: configContext.front
|
|
1248
|
+
})
|
|
1249
|
+
)
|
|
1250
|
+
);
|
|
1251
|
+
yield* genTaskLogsUi({
|
|
1252
|
+
title: "Uploading common files",
|
|
1253
|
+
itemGroups: Array.from(frontBuildFileComponentRests),
|
|
1254
|
+
processItem: frontService.uploadFrontFileToBucket,
|
|
1255
|
+
message: {
|
|
1256
|
+
resolveGroupTitle: (fileComponent) => `Uploading ${fileComponent.name}`,
|
|
1257
|
+
resolveGroupSuccess: (fileComponent, duration) => `${fileComponent.name}: ${fileComponent.length} files uploaded in ${Duration5.toMillis(duration)}ms`,
|
|
1258
|
+
resolveGroupError: (fileComponent, error) => `Upload ${fileComponent.name} failed: ${error.message}`,
|
|
1259
|
+
resolveItem: (fileItem) => `File ${fileItem.relativePath} uploaded`,
|
|
1260
|
+
resolveSuccess: (duration) => `Common files uploaded in ${Duration5.toMillis(duration)}ms`
|
|
1261
|
+
},
|
|
1262
|
+
subTaskConcurrency: configContext.bucket.upload.concurrency
|
|
1263
|
+
});
|
|
1264
|
+
yield* genLoaderUi({
|
|
1265
|
+
process: () => frontService.uploadFrontFileToBucket(indexHtml),
|
|
1266
|
+
message: {
|
|
1267
|
+
resolveStart: () => "Uploading index.html",
|
|
1268
|
+
resolveError: (error) => `Upload index.html failed: ${error.message}`,
|
|
1269
|
+
resolveCancel: () => "Upload index.html canceled",
|
|
1270
|
+
resolveEnd: (duration) => `Upload index.html finished in ${Duration5.toMillis(duration)}ms`
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
})
|
|
1274
|
+
};
|
|
1275
|
+
}),
|
|
1276
|
+
dependencies: [FrontFileObjectService.Default]
|
|
1277
|
+
}
|
|
1278
|
+
) {
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
// src/cli/deploy_on_bucket_command.ts
|
|
1282
|
+
import { cancel as cancel2, log as log6, outro as outro2 } from "@clack/prompts";
|
|
1283
|
+
import { command as command2 } from "cmd-ts";
|
|
1284
|
+
import { Effect as Effect20, Inspectable as Inspectable2, Layer as Layer5 } from "effect";
|
|
1285
|
+
var deployOnBucketCommand = command2({
|
|
1286
|
+
name: "deploy",
|
|
1287
|
+
description: "TODO",
|
|
1288
|
+
args: {},
|
|
1289
|
+
handler: function() {
|
|
1290
|
+
const CommandContextLayer = Layer5.succeed(CliCommandContext, {
|
|
1291
|
+
commandName: this.name
|
|
1292
|
+
});
|
|
1293
|
+
return Effect20.gen(function* () {
|
|
1294
|
+
const { config } = yield* startCli();
|
|
1295
|
+
const ConfigContextLayer = Layer5.succeed(InternalConfigContext, config);
|
|
1296
|
+
const DeploymentContextLayer = makeFrontDeploymentContextLayer(config);
|
|
1297
|
+
yield* Effect20.gen(function* () {
|
|
1298
|
+
const useCase = yield* DeployOnBucketUseCase;
|
|
1299
|
+
yield* runAndInterruptOnCtrlC(useCase.run());
|
|
1300
|
+
outro2(`Deployment finished`);
|
|
1301
|
+
}).pipe(
|
|
1302
|
+
Effect20.provide(DeployOnBucketUseCase.Default),
|
|
1303
|
+
Effect20.provide(ConfigContextLayer),
|
|
1304
|
+
Effect20.provide(DeploymentContextLayer),
|
|
1305
|
+
Effect20.onInterrupt(() => Effect20.sync(() => cancel2(`Deployment canceled`))),
|
|
1306
|
+
Effect20.catchAll(
|
|
1307
|
+
(error) => Effect20.gen(function* () {
|
|
1308
|
+
log6.error(error.message);
|
|
1309
|
+
log6.message(Inspectable2.toStringUnknown(error));
|
|
1310
|
+
outro2(`Deployment aborted`);
|
|
1311
|
+
})
|
|
1312
|
+
)
|
|
1313
|
+
);
|
|
1314
|
+
}).pipe(Effect20.provide(CommandContextLayer));
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// src/cli/cli.ts
|
|
1319
|
+
import { subcommands } from "cmd-ts";
|
|
1320
|
+
var cli = subcommands({
|
|
1321
|
+
name: package_default.name,
|
|
1322
|
+
cmds: { deploy: deployOnBucketCommand, create: createFrontBucketCommand }
|
|
1323
|
+
});
|
|
1324
|
+
|
|
1325
|
+
// src/errors/internal.ts
|
|
1326
|
+
import { Data as Data10 } from "effect";
|
|
1327
|
+
var InternalError = class extends Data10.TaggedError("InternalError") {
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
// main.ts
|
|
1331
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
1332
|
+
import { run } from "cmd-ts";
|
|
1333
|
+
import { Effect as Effect21 } from "effect";
|
|
1334
|
+
var runCli = toEffect(run(cli, process.argv.slice(2)), InternalError, {}).pipe(
|
|
1335
|
+
Effect21.flatMap(({ value }) => value)
|
|
1336
|
+
);
|
|
1337
|
+
NodeRuntime.runMain(runCli.pipe(Effect21.provide(NodeContext.layer)));
|