@voxgig/build 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.ts +46 -0
- package/dist/build.d.ts +31 -1
- package/dist/build.js +33 -1
- package/dist/build.js.map +1 -1
- package/dist/env/env_gen.d.ts +16 -0
- package/dist/env/env_gen.js +143 -0
- package/dist/env/env_gen.js.map +1 -0
- package/dist/env/lambda/generate.d.ts +8 -1
- package/dist/env/lambda/generate.js +59 -0
- package/dist/env/lambda/generate.js.map +1 -1
- package/dist/env/lambda/res_yml.d.ts +1 -0
- package/dist/env/lambda/res_yml.js +12 -41
- package/dist/env/lambda/res_yml.js.map +1 -1
- package/dist/env/lambda/srv_handler.d.ts +1 -0
- package/dist/env/lambda/srv_handler.js +10 -21
- package/dist/env/lambda/srv_handler.js.map +1 -1
- package/dist/env/lambda/srv_yml.d.ts +1 -0
- package/dist/env/lambda/srv_yml.js +11 -9
- package/dist/env/lambda/srv_yml.js.map +1 -1
- package/env/env_gen.ts +165 -0
- package/env/lambda/generate.ts +112 -0
- package/env/lambda/res_yml.ts +140 -0
- package/env/lambda/srv_handler.ts +132 -0
- package/env/lambda/srv_yml.ts +201 -0
- package/package.json +6 -4
- package/tm/env/aws/README.md.frag +9 -0
- package/tm/env/aws/lambda.entry.ts.frag +89 -0
- package/tm/env/aws/serverless.yml.frag +23 -0
- package/tm/env/azure/README.md.frag +7 -0
- package/tm/env/azure/entry.ts.frag +49 -0
- package/tm/env/azure/host.json.frag +8 -0
- package/tm/env/basic/README.md.frag +7 -0
- package/tm/env/basic/backend.service.frag +14 -0
- package/tm/env/basic/deploy.sh.frag +13 -0
- package/tm/env/cloudflare/README.md.frag +7 -0
- package/tm/env/cloudflare/worker.ts.frag +13 -0
- package/tm/env/cloudflare/wrangler.toml.frag +7 -0
- package/tm/env/docker/Dockerfile.frag +10 -0
- package/tm/env/docker/README.md.frag +7 -0
- package/tm/env/docker/compose.yml.frag +10 -0
- package/tm/env/local/README.md.frag +7 -0
- package/tm/env/local/run.sh.frag +6 -0
- package/tm/env/server.entry.ts.frag +48 -0
- package/tm/env/vm/README.md.frag +5 -0
- package/tm/env/vm/cloud-init.yaml.frag +23 -0
- package/tm/lambda/res.queue.yml.frag +6 -0
- package/tm/lambda/res.role.yml.frag +35 -0
- package/tm/lambda/srv.yml.frag +6 -0
- package/tm/lambda/srv_handler.js.frag +17 -0
- package/tm/lambda/srv_handler.ts.frag +17 -0
|
@@ -16,6 +16,7 @@ const srv_yml = async (model, spec) => {
|
|
|
16
16
|
const core = (0, conf_1.CoreConfShape)(model.main.conf.core);
|
|
17
17
|
let appname = core.name;
|
|
18
18
|
let AppName = (0, util_1.camelify)(appname);
|
|
19
|
+
const frag = (0, generate_1.loadFragment)('srv.yml.frag', spec);
|
|
19
20
|
let srv_yml_prefix_path = path_1.default.join(spec.folder, 'srv.prefix.yml');
|
|
20
21
|
let srv_yml_suffix_path = path_1.default.join(spec.folder, 'srv.suffix.yml');
|
|
21
22
|
let prefixContent = fs_1.default.existsSync(srv_yml_prefix_path) ?
|
|
@@ -37,13 +38,6 @@ const srv_yml = async (model, spec) => {
|
|
|
37
38
|
if ((_c = (_b = (_a = srv.gen) === null || _a === void 0 ? void 0 : _a.custom) === null || _b === void 0 ? void 0 : _b.lambda) === null || _c === void 0 ? void 0 : _c.srv_yml) {
|
|
38
39
|
return srv.gen.custom.lambda.srv_yml;
|
|
39
40
|
}
|
|
40
|
-
// TODO: this should be a JSON structure exported as YAML
|
|
41
|
-
let srvyml = `${name}:
|
|
42
|
-
handler: ${handler.path.prefix}${name}${handler.path.suffix}
|
|
43
|
-
role: Basic${AppName}LambdaRole
|
|
44
|
-
timeout: ${lambda.timeout}
|
|
45
|
-
memorySize: ${lambda.memory || 1024}
|
|
46
|
-
`;
|
|
47
41
|
const web = srv.api.web;
|
|
48
42
|
let events = '';
|
|
49
43
|
let onEvents = srv.on;
|
|
@@ -148,13 +142,21 @@ ${corsprops}
|
|
|
148
142
|
}
|
|
149
143
|
}
|
|
150
144
|
}
|
|
145
|
+
let eventsBlock = '';
|
|
151
146
|
if ('' !== events) {
|
|
152
|
-
|
|
147
|
+
eventsBlock = (0, generate_1.TM)(`
|
|
153
148
|
events:
|
|
154
149
|
${events}
|
|
155
150
|
`);
|
|
156
151
|
}
|
|
157
|
-
return
|
|
152
|
+
return (0, generate_1.renderFragment)(frag, {
|
|
153
|
+
name,
|
|
154
|
+
handler: handler.path.prefix + name + handler.path.suffix,
|
|
155
|
+
AppName,
|
|
156
|
+
timeout: lambda.timeout,
|
|
157
|
+
memory: lambda.memory || 1024,
|
|
158
|
+
events: eventsBlock,
|
|
159
|
+
});
|
|
158
160
|
}).join('\n\n\n') +
|
|
159
161
|
suffixContent;
|
|
160
162
|
await (0, generate_1.generate)(spec.folder, [{ name: 'srv.yml', content }]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"srv_yml.js","sourceRoot":"","sources":["../../../env/lambda/srv_yml.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;;AAEpD,0EAA0E;AAC1E,iDAAiD;AAEjD,4CAAmB;AACnB,gDAAuB;AAEvB,uCAAuC;AAEvC,2CAAgD;AAEhD,
|
|
1
|
+
{"version":3,"file":"srv_yml.js","sourceRoot":"","sources":["../../../env/lambda/srv_yml.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;;AAEpD,0EAA0E;AAC1E,iDAAiD;AAEjD,4CAAmB;AACnB,gDAAuB;AAEvB,uCAAuC;AAEvC,2CAAgD;AAEhD,yCAA8E;AAG9E,MAAM,OAAO,GAAG,KAAK,EAAE,KAAU,EAAE,IAGlC,EAAE,EAAE;IAEH,MAAM,IAAI,GAAG,IAAA,oBAAa,EAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAEhD,IAAI,OAAO,GAAG,IAAI,CAAC,IAAI,CAAA;IACvB,IAAI,OAAO,GAAG,IAAA,eAAQ,EAAC,OAAO,CAAC,CAAA;IAE/B,MAAM,IAAI,GAAG,IAAA,uBAAY,EAAC,cAAc,EAAE,IAAI,CAAC,CAAA;IAE/C,IAAI,mBAAmB,GAAG,cAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IAClE,IAAI,mBAAmB,GAAG,cAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IAElE,IAAI,aAAa,GAAG,YAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC,CAAC;QACtD,YAAE,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC3C,IAAI,aAAa,GAAG,YAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,CAAC,CAAC;QACtD,YAAE,CAAC,YAAY,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAE3C,IAAI,OAAO,GAET,aAAa;QACb,MAAM;aACH,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;aACvB,MAAM,CAAC,CAAC,KAAU,EAAE,EAAE,eAAC,OAAA,MAAA,MAAA,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,0CAAE,MAAM,0CAAE,MAAM,CAAA,EAAA,CAAC;aACpD,GAAG,CAAC,CAAC,KAAU,EAAE,EAAE;;YAClB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YACrB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;YAEpB,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAA;YAC7B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;YAE9B,6DAA6D;YAC7D,uBAAuB;YACvB,IAAI,MAAA,MAAA,MAAA,GAAG,CAAC,GAAG,0CAAE,MAAM,0CAAE,MAAM,0CAAE,OAAO,EAAE,CAAC;gBACrC,OAAO,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAA;YACtC,CAAC;YAED,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAA;YAEvB,IAAI,MAAM,GAAG,EAAE,CAAA;YAEf,IAAI,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAA;YAErB,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,KAAY,EAAE,EAAE;oBAChD,sBAAsB;oBACtB,IAAI,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;oBAEnB,IAAI,KAAK,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;wBAC5B,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,EAAO,EAAE,EAAE;4BAC9B,IAAI,IAAI,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC;gCACvB,MAAM,IAAI,IAAA,aAAE,EAAC;;kBAEb,EAAE,CAAC,MAAM;iBACV,EAAE,CAAC,KAAK;;CAExB,CAAC,CAAA;gCACgB,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC;oCACb,MAAM,IAAI,IAAA,aAAE,EAAC;;CAEhC,CAAC,CAAA;oCACkB,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;wCACpB,MAAM,IAAI,IAAA,aAAE,EAAC;sBACb,EAAE,CAAC,KAAK,CAAC,MAAM;CACpC,CAAC,CAAA;oCACkB,CAAC;oCAED,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;wCACpB,MAAM,IAAI,IAAA,aAAE,EAAC;sBACb,EAAE,CAAC,KAAK,CAAC,MAAM;CACpC,CAAC,CAAA;oCACkB,CAAC;gCACH,CAAC;4BACH,CAAC;iCACI,IAAI,UAAU,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC;gCAClC,IAAI,OAAO,GACT,QAAQ,KAAK,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;gCAC9D,IAAI,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAa,EAAE,EAAE;oCACxC,IAAI,QAAQ,GAAG;;gBAEnB,KAAK,EAAE,CAAA;oCACH,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC;wCACX,QAAQ,IAAI;;iBAEjB,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAA;oCACtB,CAAC;oCACD,OAAO,QAAQ,CAAA;gCACjB,CAAC,CAAC,CAAA;gCAEF,MAAM,IAAI,IAAA,aAAE,EAAC;EAC7B,KAAK;CACN,CAAC,CAAA;4BAEc,CAAC;iCACI,IAAI,KAAK,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC;gCAC7B,MAAM,IAAI,IAAA,aAAE,EAAC;;;;gBAIf,EAAE,CAAC,GAAG;;;CAGrB,CAAC,CAAA;4BAEc,CAAC;wBACH,CAAC,CAAC,CAAA;oBACJ,CAAC;gBACH,CAAC,CAAC,CAAA;YACJ,CAAC;YAED,qBAAqB;YACrB,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;gBACf,IAAI,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAA;gBAC5B,IAAI,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,CAAA;gBAC5B,IAAI,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAA;gBACxB,IAAI,MAAM,GAAG,GAAG,CAAC,MAAM,CAAA;gBACvB,IAAI,QAAQ,GAAG,OAAO,CAAA;gBACtB,IAAI,SAAS,GAAG,EAAE,CAAA;gBAElB,IAAI,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;gBAC/B,kCAAkC;gBAElC,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;oBACpB,QAAQ,GAAG,MAAM,CAAA;oBACjB,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAA,gBAAK,EAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;wBAC7C,QAAQ,GAAG,EAAE,CAAA;wBACb,SAAS,GAAG,MAAM;6BACf,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;6BACvB,MAAM,CAAC,CAAC,CAAC,CAAM,EAAE,EAAO,EAAE,EAAE,CAAC,CAC5B,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI;8BACnC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;oBAChB,CAAC;gBACH,CAAC;gBAED,IAAI,IAAI,MAAK,MAAA,GAAG,CAAC,MAAM,0CAAE,OAAO,CAAA,EAAE,CAAC;oBACjC,KAAK,IAAI,MAAM,IAAI,OAAO,EAAE,CAAC;wBAC3B,MAAM,IAAI,IAAA,aAAE,EAAC;;iBAEV,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM;kBAC5B,MAAM;CACvB,CAAC,CAAA;oBACU,CAAC;gBACH,CAAC;qBACI,CAAC;oBACJ,KAAK,IAAI,MAAM,IAAI,OAAO,EAAE,CAAC;wBAC3B,MAAM,IAAI,IAAA,aAAE,EAAC;;iBAEV,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM;kBAC5B,MAAM;gBACR,QAAQ;EACtB,SAAS;CACV,CAAC,CAAA;oBACU,CAAC;gBACH,CAAC;YACH,CAAC;YAID,IAAI,WAAW,GAAG,EAAE,CAAA;YACpB,IAAI,EAAE,KAAK,MAAM,EAAE,CAAC;gBAClB,WAAW,GAAG,IAAA,aAAE,EAAC;;EAEzB,MAAM;CACP,CAAC,CAAA;YACM,CAAC;YAED,OAAO,IAAA,yBAAc,EAAC,IAAI,EAAE;gBAC1B,IAAI;gBACJ,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM;gBACzD,OAAO;gBACP,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,IAAI;gBAC7B,MAAM,EAAE,WAAW;aACpB,CAAC,CAAA;QACJ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;QACnB,aAAa,CAAA;IAEf,MAAM,IAAA,mBAAQ,EAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;AAC7D,CAAC,CAAA;AAIC,0BAAO"}
|
package/env/env_gen.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* Copyright © 2026 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
// EnvGen: per-environment deployment artifact generation. The project
|
|
4
|
+
// model declares its target environments under `main: env:`:
|
|
5
|
+
//
|
|
6
|
+
// main: env: local: { active: true }
|
|
7
|
+
// main: env: aws: { active: true, region: 'eu-west-1', stage: 'dev' }
|
|
8
|
+
//
|
|
9
|
+
// For each active environment this generates gen/env/<name>/ from the
|
|
10
|
+
// fragment set tm/env/<kind>/ (kind defaults to the env name, so a
|
|
11
|
+
// project can declare e.g. `aws2: { kind: 'aws', region: ... }`).
|
|
12
|
+
// Fragments are project-shadowable via spec.tm (the project's tm/env
|
|
13
|
+
// folder), like the lambda templates.
|
|
14
|
+
|
|
15
|
+
import Fs from 'fs'
|
|
16
|
+
import Path from 'path'
|
|
17
|
+
|
|
18
|
+
import { camelify } from '@voxgig/util'
|
|
19
|
+
|
|
20
|
+
import { CoreConfShape } from '../shape/conf'
|
|
21
|
+
|
|
22
|
+
import { generate, loadFragment, renderFragment } from './lambda/generate'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// The supported environment kinds and the files each generates.
|
|
26
|
+
// `out` may contain $$slots$$ (rendered with the same slot values).
|
|
27
|
+
const ENV_FILES: Record<string, { frag: string, out: string }[]> = {
|
|
28
|
+
local: [
|
|
29
|
+
{ frag: 'local/run.sh.frag', out: 'run.sh' },
|
|
30
|
+
{ frag: 'local/README.md.frag', out: 'README.md' },
|
|
31
|
+
],
|
|
32
|
+
basic: [
|
|
33
|
+
{ frag: 'basic/backend.service.frag', out: '$$name$$-backend.service' },
|
|
34
|
+
{ frag: 'basic/deploy.sh.frag', out: 'deploy.sh' },
|
|
35
|
+
{ frag: 'basic/README.md.frag', out: 'README.md' },
|
|
36
|
+
],
|
|
37
|
+
docker: [
|
|
38
|
+
{ frag: 'docker/Dockerfile.frag', out: 'Dockerfile' },
|
|
39
|
+
{ frag: 'docker/compose.yml.frag', out: 'compose.yml' },
|
|
40
|
+
{ frag: 'docker/README.md.frag', out: 'README.md' },
|
|
41
|
+
],
|
|
42
|
+
vm: [
|
|
43
|
+
{ frag: 'vm/cloud-init.yaml.frag', out: 'cloud-init.yaml' },
|
|
44
|
+
{ frag: 'vm/README.md.frag', out: 'README.md' },
|
|
45
|
+
],
|
|
46
|
+
aws: [
|
|
47
|
+
{ frag: 'aws/serverless.yml.frag', out: 'serverless.yml' },
|
|
48
|
+
{ frag: 'aws/README.md.frag', out: 'README.md' },
|
|
49
|
+
],
|
|
50
|
+
azure: [
|
|
51
|
+
{ frag: 'azure/host.json.frag', out: 'host.json' },
|
|
52
|
+
{ frag: 'azure/README.md.frag', out: 'README.md' },
|
|
53
|
+
],
|
|
54
|
+
cloudflare: [
|
|
55
|
+
{ frag: 'cloudflare/wrangler.toml.frag', out: 'wrangler.toml' },
|
|
56
|
+
{ frag: 'cloudflare/README.md.frag', out: 'README.md' },
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The runtime entry point + Seneca config setup each environment needs
|
|
61
|
+
// under src/env/. These are generated ONCE (only when missing): after
|
|
62
|
+
// creation the project owns them, like the scaffold's local entry. All
|
|
63
|
+
// entries route Seneca configuration through src/env/shared/basic.ts.
|
|
64
|
+
// `dir` is relative to the project src/env folder.
|
|
65
|
+
const ENV_SRC: Record<string, { frag: string, dir: string, out: string }[]> = {
|
|
66
|
+
local: [], // scaffold provides src/env/local/local.ts
|
|
67
|
+
basic: [
|
|
68
|
+
{ frag: 'server.entry.ts.frag', dir: 'basic', out: 'basic.ts' },
|
|
69
|
+
],
|
|
70
|
+
docker: [
|
|
71
|
+
{ frag: 'server.entry.ts.frag', dir: 'docker', out: 'docker.ts' },
|
|
72
|
+
],
|
|
73
|
+
vm: [
|
|
74
|
+
{ frag: 'server.entry.ts.frag', dir: 'vm', out: 'vm.ts' },
|
|
75
|
+
],
|
|
76
|
+
aws: [
|
|
77
|
+
// The Lambda bootstrap the generated handlers import.
|
|
78
|
+
{ frag: 'aws/lambda.entry.ts.frag', dir: 'lambda', out: 'lambda.ts' },
|
|
79
|
+
],
|
|
80
|
+
azure: [
|
|
81
|
+
{ frag: 'azure/entry.ts.frag', dir: 'azure', out: 'azure.ts' },
|
|
82
|
+
],
|
|
83
|
+
cloudflare: [
|
|
84
|
+
{ frag: 'cloudflare/worker.ts.frag', dir: 'cloudflare', out: 'worker.ts' },
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const KINDS = Object.keys(ENV_FILES)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
const env_gen = async (model: any, spec: {
|
|
92
|
+
folder: string // gen/env output root; each env writes <folder>/<name>/
|
|
93
|
+
tm?: string // project tm/env folder (fragment shadowing)
|
|
94
|
+
src?: string // project src/env folder: runtime entries generated
|
|
95
|
+
// here ONCE (existing files are never overwritten)
|
|
96
|
+
}) => {
|
|
97
|
+
|
|
98
|
+
const core = CoreConfShape(model.main.conf.core)
|
|
99
|
+
const envs = model.main.env || {}
|
|
100
|
+
|
|
101
|
+
for (const entry of Object.entries(envs)) {
|
|
102
|
+
const name = entry[0]
|
|
103
|
+
const def: any = entry[1] || {}
|
|
104
|
+
|
|
105
|
+
if (false === def.active) {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const kind = def.kind || name
|
|
110
|
+
const filedefs = ENV_FILES[kind]
|
|
111
|
+
if (null == filedefs) {
|
|
112
|
+
throw new Error('@voxgig/build: unknown environment kind: ' + kind +
|
|
113
|
+
' (env ' + name + '; known kinds: ' + KINDS.join(', ') + ')')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Slot values: project identity, ports, and per-env config with
|
|
117
|
+
// sensible defaults. Additional scalar config keys on the env
|
|
118
|
+
// definition become slots too.
|
|
119
|
+
const slots: Record<string, any> = {
|
|
120
|
+
name: core.name,
|
|
121
|
+
Name: camelify(core.name),
|
|
122
|
+
env: name,
|
|
123
|
+
port: model.main.conf.port?.backend || 8080,
|
|
124
|
+
node: def.node || '22',
|
|
125
|
+
region: def.region || 'us-east-1',
|
|
126
|
+
stage: def.stage || 'dev',
|
|
127
|
+
}
|
|
128
|
+
for (const key of Object.keys(def)) {
|
|
129
|
+
if ('object' !== typeof def[key] && null == slots[key]) {
|
|
130
|
+
slots[key] = def[key]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Slot for the env name inside entry fragments.
|
|
135
|
+
slots.env = name
|
|
136
|
+
|
|
137
|
+
const files = filedefs.map((fd) => ({
|
|
138
|
+
name: renderFragment(fd.out, slots),
|
|
139
|
+
content: renderFragment(loadFragment(fd.frag, spec, 'env'), slots),
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
await generate(spec.folder + '/' + name, files)
|
|
143
|
+
|
|
144
|
+
// Runtime entries: create-once into src/env (user-owned afterwards).
|
|
145
|
+
if (null != spec.src) {
|
|
146
|
+
for (const sd of (ENV_SRC[kind] || [])) {
|
|
147
|
+
const dest = Path.join(spec.src, sd.dir, renderFragment(sd.out, slots))
|
|
148
|
+
if (!Fs.existsSync(dest)) {
|
|
149
|
+
const content =
|
|
150
|
+
renderFragment(loadFragment(sd.frag, spec, 'env'), slots)
|
|
151
|
+
Fs.mkdirSync(Path.dirname(dest), { recursive: true })
|
|
152
|
+
Fs.writeFileSync(dest, content)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
env_gen,
|
|
162
|
+
ENV_FILES,
|
|
163
|
+
ENV_SRC,
|
|
164
|
+
KINDS,
|
|
165
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Copyright © 2022-2026 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
// Shared generation utilities for the lambda templates. File output goes
|
|
4
|
+
// through the jostraca templating library (Project/File/Content components),
|
|
5
|
+
// which also maintains .jostraca/ build metadata next to the outputs.
|
|
6
|
+
|
|
7
|
+
import Fs from 'fs'
|
|
8
|
+
import Path from 'path'
|
|
9
|
+
|
|
10
|
+
import { Jostraca, Project, File, Content } from 'jostraca'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// The text shape of each generated output lives in a jostraca-style
|
|
14
|
+
// fragment file with $$slot$$ placeholders. Fragments ship with this
|
|
15
|
+
// package under tm/lambda/, and a project can shadow any of them by
|
|
16
|
+
// placing a same-named file in its own tm folder (passed as spec.tm by
|
|
17
|
+
// the project's build actions). The fragment file's final newline is
|
|
18
|
+
// dropped at load, so slot values fully control trailing bytes.
|
|
19
|
+
|
|
20
|
+
// Package tm root: compiled code lives at <pkg>/dist/env/lambda (three
|
|
21
|
+
// levels down), but test transforms run the source at <pkg>/env/lambda
|
|
22
|
+
// (two levels down) - probe both. Areas (tm/<area>/...) group fragment
|
|
23
|
+
// sets: 'lambda' (the EnvLambda templates) and 'env' (per-environment
|
|
24
|
+
// deployment templates).
|
|
25
|
+
const PKG_TM_ROOT = [
|
|
26
|
+
Path.join(__dirname, '..', '..', '..', 'tm'),
|
|
27
|
+
Path.join(__dirname, '..', '..', 'tm'),
|
|
28
|
+
].find((p) => Fs.existsSync(p)) ||
|
|
29
|
+
Path.join(__dirname, '..', '..', '..', 'tm')
|
|
30
|
+
|
|
31
|
+
const PKG_TM = Path.join(PKG_TM_ROOT, 'lambda')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
function loadFragment(
|
|
35
|
+
name: string, spec?: { tm?: string }, area: string = 'lambda'
|
|
36
|
+
): string {
|
|
37
|
+
const candidates = []
|
|
38
|
+
if (spec?.tm) {
|
|
39
|
+
candidates.push(Path.join(spec.tm, name))
|
|
40
|
+
}
|
|
41
|
+
candidates.push(Path.join(PKG_TM_ROOT, area, name))
|
|
42
|
+
|
|
43
|
+
for (const c of candidates) {
|
|
44
|
+
if (Fs.existsSync(c)) {
|
|
45
|
+
let src = Fs.readFileSync(c, 'utf8')
|
|
46
|
+
if (src.endsWith('\n')) {
|
|
47
|
+
src = src.substring(0, src.length - 1)
|
|
48
|
+
}
|
|
49
|
+
return src
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
throw new Error('@voxgig/build: fragment not found: ' + name +
|
|
53
|
+
' (looked in ' + candidates.join(', ') + ')')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// Literal $$slot$$ substitution (split/join - values are never
|
|
58
|
+
// interpreted, so ${...}, $&, etc. pass through untouched).
|
|
59
|
+
function renderFragment(src: string, slots: Record<string, any>): string {
|
|
60
|
+
let out = src
|
|
61
|
+
for (const key of Object.keys(slots)) {
|
|
62
|
+
out = out.split('$$' + key + '$$').join(String(slots[key]))
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// Fragment names shipped by this package (for tooling: list/eject).
|
|
69
|
+
function listFragments(): string[] {
|
|
70
|
+
return Fs.readdirSync(PKG_TM).filter((f) => f.endsWith('.frag')).sort()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
// Generate one or more files in folder, each with exact content.
|
|
75
|
+
async function generate(
|
|
76
|
+
folder: string,
|
|
77
|
+
files: { name: string, content: string }[]
|
|
78
|
+
) {
|
|
79
|
+
const jostraca = Jostraca()
|
|
80
|
+
|
|
81
|
+
await jostraca.generate({ folder }, () => {
|
|
82
|
+
Project({}, () => {
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
File({ name: f.name }, () => {
|
|
85
|
+
Content(f.content)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
function empty(o: any) {
|
|
94
|
+
return null == o ? true : 0 === Object.keys(o).length
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Strip inital newline
|
|
98
|
+
function TM(str: string) {
|
|
99
|
+
return str.replace(/^\n/, '')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
generate,
|
|
105
|
+
empty,
|
|
106
|
+
TM,
|
|
107
|
+
loadFragment,
|
|
108
|
+
renderFragment,
|
|
109
|
+
listFragments,
|
|
110
|
+
PKG_TM,
|
|
111
|
+
PKG_TM_ROOT,
|
|
112
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* Copyright © 2022-2026 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
// gen/serverless resources template: SQS queues, DynamoDB tables (via
|
|
4
|
+
// yml/res_dynamo_yml), and the basic Lambda IAM role.
|
|
5
|
+
|
|
6
|
+
import Fs from 'fs'
|
|
7
|
+
import Path from 'path'
|
|
8
|
+
|
|
9
|
+
import { dive, camelify } from '@voxgig/util'
|
|
10
|
+
|
|
11
|
+
import { MsgMetaShape } from '../../shape/msg'
|
|
12
|
+
import { CoreConfShape, CloudConfShape } from '../../shape/conf'
|
|
13
|
+
|
|
14
|
+
import { res_dynamo_yml } from '../../yml/res_dynamo_yml'
|
|
15
|
+
|
|
16
|
+
import { generate, loadFragment, renderFragment } from './generate'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const resources_yml = async (model: any, spec: {
|
|
20
|
+
folder: string,
|
|
21
|
+
filename: string
|
|
22
|
+
custom: string,
|
|
23
|
+
tm?: string
|
|
24
|
+
}) => {
|
|
25
|
+
|
|
26
|
+
const queueFrag = loadFragment('res.queue.yml.frag', spec)
|
|
27
|
+
const roleFrag = loadFragment('res.role.yml.frag', spec)
|
|
28
|
+
|
|
29
|
+
const core = CoreConfShape(model.main.conf.core)
|
|
30
|
+
|
|
31
|
+
const appname = core.name
|
|
32
|
+
const AppName = camelify(appname)
|
|
33
|
+
|
|
34
|
+
const cloud = CloudConfShape(model.main.conf.cloud)
|
|
35
|
+
|
|
36
|
+
const region = cloud.aws.region
|
|
37
|
+
const accountid = cloud.aws.accountid
|
|
38
|
+
|
|
39
|
+
let filename = spec.filename || 'resources.yml'
|
|
40
|
+
|
|
41
|
+
let resources_yml_prefix_path = Path.join(spec.folder, 'res.prefix.yml')
|
|
42
|
+
let resources_yml_suffix_path = Path.join(spec.folder, 'res.suffix.yml')
|
|
43
|
+
|
|
44
|
+
let prefixContent = Fs.existsSync(resources_yml_prefix_path) ?
|
|
45
|
+
Fs.readFileSync(resources_yml_prefix_path) : ''
|
|
46
|
+
let suffixContent = Fs.existsSync(resources_yml_suffix_path) ?
|
|
47
|
+
Fs.readFileSync(resources_yml_suffix_path) : ''
|
|
48
|
+
|
|
49
|
+
const dynamoResources: { arn: string }[] = []
|
|
50
|
+
|
|
51
|
+
let content =
|
|
52
|
+
`# START
|
|
53
|
+
`
|
|
54
|
+
|
|
55
|
+
content +=
|
|
56
|
+
prefixContent +
|
|
57
|
+
|
|
58
|
+
await res_dynamo_yml(model, { dynamoResources, region, accountid })
|
|
59
|
+
|
|
60
|
+
// content +=
|
|
61
|
+
let queueDefs = dive(model.main.msg, 128).map((entry: any) => {
|
|
62
|
+
let path = entry[0]
|
|
63
|
+
let msgMeta = MsgMetaShape(entry[1])
|
|
64
|
+
|
|
65
|
+
let pathname = path
|
|
66
|
+
.map((p: string) =>
|
|
67
|
+
(p[0] + '').toUpperCase() + p.substring(1))
|
|
68
|
+
.join('')
|
|
69
|
+
|
|
70
|
+
// console.log('MQ', pathname, msgMeta)
|
|
71
|
+
|
|
72
|
+
if (msgMeta.transport?.queue?.active) {
|
|
73
|
+
// console.log('MM', path, msgMeta)
|
|
74
|
+
let queue = msgMeta.transport.queue
|
|
75
|
+
let name = queue.name || pathname
|
|
76
|
+
|
|
77
|
+
// TODO: aontu should do this, but needs recursive child conjuncts
|
|
78
|
+
let stage_suffix =
|
|
79
|
+
(false === queue.stage?.active) ? '' : '-${self:provider.stage,"dev"}'
|
|
80
|
+
|
|
81
|
+
let queue_suffix = (null == queue.suffix || '' == queue.suffix) ? '' :
|
|
82
|
+
'-' + queue.suffix
|
|
83
|
+
|
|
84
|
+
let resname = 'Queue' + name
|
|
85
|
+
|
|
86
|
+
let queueName =
|
|
87
|
+
(queue.prefix || '') +
|
|
88
|
+
path.reduce((s: string, p: string, i: number) =>
|
|
89
|
+
(s += p + (i % 2 ?
|
|
90
|
+
(i == path.length - 1 ? '' : '-') : '_')), '') +
|
|
91
|
+
(queue_suffix || '') +
|
|
92
|
+
(stage_suffix || '')
|
|
93
|
+
|
|
94
|
+
let queueTimeout = queue.timeout || 30
|
|
95
|
+
|
|
96
|
+
return renderFragment(queueFrag, {
|
|
97
|
+
resname,
|
|
98
|
+
queueName,
|
|
99
|
+
queueTimeout,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
return ''
|
|
103
|
+
}).filter(n => '' !== n).join('')
|
|
104
|
+
|
|
105
|
+
// console.log('queueDefs', queueDefs)
|
|
106
|
+
|
|
107
|
+
content += '\n\n' + queueDefs
|
|
108
|
+
|
|
109
|
+
let customLambdaPolicyStatementPath =
|
|
110
|
+
Path.join(spec.folder, 'res.lambda.policy.statements.yml')
|
|
111
|
+
let customLambdaPolicyStatementContent = Fs.existsSync(customLambdaPolicyStatementPath) ?
|
|
112
|
+
Fs.readFileSync(customLambdaPolicyStatementPath) : ''
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
content += renderFragment(roleFrag, {
|
|
117
|
+
AppName,
|
|
118
|
+
dynamoArns: dynamoResources.map(r => ' - ' + r.arn).join('\n'),
|
|
119
|
+
customPolicy: customLambdaPolicyStatementContent,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if (spec.custom) {
|
|
125
|
+
content = Fs.readFileSync(spec.custom).toString() + '\n\n\n' + content
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
content += suffixContent
|
|
129
|
+
|
|
130
|
+
content += `
|
|
131
|
+
# END
|
|
132
|
+
`
|
|
133
|
+
|
|
134
|
+
await generate(spec.folder, [{ name: filename, content }])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
resources_yml,
|
|
140
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* Copyright © 2022-2026 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
// Lambda handler template: one handler source file per lambda service,
|
|
4
|
+
// bootstrapping Seneca via the env folder and delegating to gateway-lambda.
|
|
5
|
+
|
|
6
|
+
import { dive, get, pinify } from '@voxgig/util'
|
|
7
|
+
|
|
8
|
+
import { MsgMetaShape } from '../../shape/msg'
|
|
9
|
+
|
|
10
|
+
import { generate, loadFragment, renderFragment } from './generate'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// Only create if does not exist
|
|
14
|
+
const srv_handler = async (model: any, spec: {
|
|
15
|
+
folder: string
|
|
16
|
+
start?: string
|
|
17
|
+
env?: {
|
|
18
|
+
folder: string
|
|
19
|
+
}
|
|
20
|
+
lang?: string
|
|
21
|
+
tm?: string
|
|
22
|
+
}) => {
|
|
23
|
+
let lang = spec.lang || 'js'
|
|
24
|
+
|
|
25
|
+
const frag = loadFragment('srv_handler.' + lang + '.frag', spec)
|
|
26
|
+
|
|
27
|
+
const files: { name: string, content: string }[] = []
|
|
28
|
+
|
|
29
|
+
Object
|
|
30
|
+
.entries(model.main.srv)
|
|
31
|
+
.filter((entry: any) => entry[1].env?.lambda)
|
|
32
|
+
.forEach((entry: any) => {
|
|
33
|
+
const name = entry[0]
|
|
34
|
+
const srv = entry[1]
|
|
35
|
+
|
|
36
|
+
if ('custom' === srv.env.lambda.kind) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let start = spec.start || 'setup'
|
|
41
|
+
let envFolder = spec.env?.folder || '../../../env/lambda'
|
|
42
|
+
|
|
43
|
+
let handler = 'handler'
|
|
44
|
+
let modify = ''
|
|
45
|
+
|
|
46
|
+
// if (!srv.api.web.active) {
|
|
47
|
+
// if (srv.on && 0 < Object.keys(srv.on).length) {
|
|
48
|
+
// handler = 'eventhandler'
|
|
49
|
+
// modify = `
|
|
50
|
+
// event = {
|
|
51
|
+
// ...event,
|
|
52
|
+
// // TODO: @voxgig/system? util needed to handle this dynamically
|
|
53
|
+
// seneca$: { msg: '${srv.on[Object.keys(srv.on)[0]].events[0].msg}' },
|
|
54
|
+
// }
|
|
55
|
+
// `
|
|
56
|
+
// }
|
|
57
|
+
// }
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
let prepare = ''
|
|
61
|
+
let complete = ''
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
dive(model.main.msg.aim[name], 128).map((entry: any) => {
|
|
65
|
+
let path = ['aim', name, ...entry[0]]
|
|
66
|
+
let msgMeta = MsgMetaShape(entry[1])
|
|
67
|
+
let pin = pinify(path)
|
|
68
|
+
if (msgMeta.transport?.queue?.active) {
|
|
69
|
+
complete += `
|
|
70
|
+
seneca.listen({type:'sqs',pin:'${pin}'})`
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
dive(model.main.srv[name].out, 128).map((entry: any) => {
|
|
76
|
+
let path = entry[0]
|
|
77
|
+
let msgMetaMaybe = get(model.main.msg, path)
|
|
78
|
+
// console.log(name, path, msgMetaMaybe)
|
|
79
|
+
if (msgMetaMaybe?.$) {
|
|
80
|
+
let msgMeta = MsgMetaShape(msgMetaMaybe?.$)
|
|
81
|
+
let pin = pinify(path)
|
|
82
|
+
|
|
83
|
+
if (msgMeta.transport?.queue?.active) {
|
|
84
|
+
complete += `
|
|
85
|
+
seneca.client({type:'sqs',pin:'${pin}'})`
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
let makeGatewayHandler = false
|
|
91
|
+
let onlist = model.main.srv[name].on || {}
|
|
92
|
+
Object.entries(onlist).map((onitem: any) => {
|
|
93
|
+
onitem[1].events.map((event: any) => {
|
|
94
|
+
if ('s3' === event.source) {
|
|
95
|
+
if (!makeGatewayHandler) {
|
|
96
|
+
complete += `
|
|
97
|
+
|
|
98
|
+
const makeGatewayHandler = seneca.export('s3-store/makeGatewayHandler')`
|
|
99
|
+
makeGatewayHandler = true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
complete += `
|
|
103
|
+
seneca
|
|
104
|
+
.act('sys:gateway,kind:lambda,add:hook,hook:handler', {
|
|
105
|
+
handler: makeGatewayHandler('${event.msg}') })`
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
const content = renderFragment(frag, {
|
|
112
|
+
envFolder,
|
|
113
|
+
start,
|
|
114
|
+
name,
|
|
115
|
+
complete,
|
|
116
|
+
modify,
|
|
117
|
+
prepare,
|
|
118
|
+
handler,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
files.push({ name: name + '.' + lang, content })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (0 < files.length) {
|
|
125
|
+
await generate(spec.folder, files)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
srv_handler,
|
|
132
|
+
}
|