@voxgig/system 1.3.1 → 1.6.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/README.md +23 -0
- package/cmd.ts +87 -7
- package/dist/cmd.js +80 -4
- package/dist/cmd.js.map +1 -1
- package/dist/lib/add.d.ts +4 -1
- package/dist/lib/add.js +84 -0
- package/dist/lib/add.js.map +1 -1
- package/dist/lib/template.d.ts +24 -0
- package/dist/lib/template.js +245 -0
- package/dist/lib/template.js.map +1 -0
- package/dist/system.d.ts +10 -2
- package/dist/system.js +10 -1
- package/dist/system.js.map +1 -1
- package/lib/add.ts +98 -1
- package/lib/template.ts +323 -0
- package/package.json +1 -1
- package/system.ts +11 -1
package/lib/template.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/* Copyright © 2026 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
// Template customization tooling: list / eject / diff for the generation
|
|
4
|
+
// templates provided by @voxgig/build.
|
|
5
|
+
//
|
|
6
|
+
// Layered resolution (first hit wins):
|
|
7
|
+
// 1. backend/src/gen/<name>.ts compiled generator override (deep custom)
|
|
8
|
+
// 2. backend/tm/<area>/<frag> project fragment (text-level custom)
|
|
9
|
+
// 3. @voxgig/build package defaults
|
|
10
|
+
//
|
|
11
|
+
// Fragment names are area-qualified (lambda/srv.yml.frag,
|
|
12
|
+
// env/aws/serverless.yml.frag); bare names default to the lambda area.
|
|
13
|
+
//
|
|
14
|
+
// eject copies a package fragment (or, with code=true, a template source
|
|
15
|
+
// rewired to the package's public API) into the project, recording
|
|
16
|
+
// provenance in tm/lambda/.ejected.json so diff can show upstream drift
|
|
17
|
+
// after package upgrades.
|
|
18
|
+
|
|
19
|
+
import Crypto from 'node:crypto'
|
|
20
|
+
import Fs from 'node:fs'
|
|
21
|
+
import Path from 'node:path'
|
|
22
|
+
import { spawnSync } from 'node:child_process'
|
|
23
|
+
|
|
24
|
+
import { resolveModelFiles } from './add'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// The code-level template generators (env/lambda/<name>.ts in the package).
|
|
28
|
+
const GENERATORS = ['srv_yml', 'srv_handler', 'res_yml']
|
|
29
|
+
|
|
30
|
+
// Generator shorthand -> default fragment.
|
|
31
|
+
const GENERATOR_FRAG: Record<string, string> = {
|
|
32
|
+
srv_yml: 'lambda/srv.yml.frag',
|
|
33
|
+
srv_handler: 'lambda/srv_handler.ts.frag',
|
|
34
|
+
res_yml: 'lambda/res.role.yml.frag',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export type Project = {
|
|
39
|
+
backend: string // the project backend folder (holds model/, tm/, src/)
|
|
40
|
+
tm: string // backend/tm (project fragment root)
|
|
41
|
+
gen: string // backend/src/gen
|
|
42
|
+
pkg: string // resolved @voxgig/build package folder
|
|
43
|
+
pkgtm: string // package tm root
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
function resolveProject(start: string): Project {
|
|
48
|
+
const files = resolveModelFiles(start)
|
|
49
|
+
const backend = Path.dirname(files.folder)
|
|
50
|
+
|
|
51
|
+
let pkgjson: string
|
|
52
|
+
try {
|
|
53
|
+
pkgjson = require.resolve('@voxgig/build/package.json', {
|
|
54
|
+
paths: [backend, files.folder, start],
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
catch (e: any) {
|
|
58
|
+
throw new Error('@voxgig/build not found from ' + backend +
|
|
59
|
+
' - is it installed? (npm install)')
|
|
60
|
+
}
|
|
61
|
+
const pkg = Path.dirname(pkgjson)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
backend,
|
|
65
|
+
tm: Path.join(backend, 'tm'),
|
|
66
|
+
gen: Path.join(backend, 'src', 'gen'),
|
|
67
|
+
pkg,
|
|
68
|
+
pkgtm: Path.join(pkg, 'tm'),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
function pkgVersion(project: Project): string {
|
|
74
|
+
return JSON.parse(
|
|
75
|
+
Fs.readFileSync(Path.join(project.pkg, 'package.json'), 'utf8')).version
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sha256(content: string | Buffer): string {
|
|
79
|
+
return Crypto.createHash('sha256').update(content).digest('hex')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
type Provenance = Record<string, {
|
|
84
|
+
package: string, version: string, sha256: string
|
|
85
|
+
}>
|
|
86
|
+
|
|
87
|
+
// Provenance lives at tm/.ejected.json; the pre-area location
|
|
88
|
+
// (tm/lambda/.ejected.json) and bare fragment keys are still read, with
|
|
89
|
+
// bare keys normalized to the lambda area.
|
|
90
|
+
function readProvenance(project: Project): Provenance {
|
|
91
|
+
const out: Provenance = {}
|
|
92
|
+
for (const p of [
|
|
93
|
+
Path.join(project.tm, 'lambda', '.ejected.json'),
|
|
94
|
+
Path.join(project.tm, '.ejected.json'),
|
|
95
|
+
]) {
|
|
96
|
+
if (Fs.existsSync(p)) {
|
|
97
|
+
const prov = JSON.parse(Fs.readFileSync(p, 'utf8'))
|
|
98
|
+
for (const key of Object.keys(prov)) {
|
|
99
|
+
const norm = (key.includes('/') || key.startsWith('code:')) ?
|
|
100
|
+
key : 'lambda/' + key
|
|
101
|
+
out[norm] = prov[key]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeProvenance(project: Project, prov: Provenance) {
|
|
109
|
+
Fs.mkdirSync(project.tm, { recursive: true })
|
|
110
|
+
Fs.writeFileSync(Path.join(project.tm, '.ejected.json'),
|
|
111
|
+
JSON.stringify(prov, null, 2) + '\n')
|
|
112
|
+
const old = Path.join(project.tm, 'lambda', '.ejected.json')
|
|
113
|
+
if (Fs.existsSync(old)) {
|
|
114
|
+
Fs.rmSync(old)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
// All package fragments, as area-qualified relative names.
|
|
120
|
+
function pkgFragments(project: Project): string[] {
|
|
121
|
+
const out: string[] = []
|
|
122
|
+
const walk = (rel: string) => {
|
|
123
|
+
const dir = Path.join(project.pkgtm, rel)
|
|
124
|
+
if (!Fs.existsSync(dir)) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
for (const e of Fs.readdirSync(dir, { withFileTypes: true })) {
|
|
128
|
+
const relpath = '' === rel ? e.name : rel + '/' + e.name
|
|
129
|
+
if (e.isDirectory()) {
|
|
130
|
+
walk(relpath)
|
|
131
|
+
}
|
|
132
|
+
else if (e.name.endsWith('.frag')) {
|
|
133
|
+
out.push(relpath)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
walk('')
|
|
138
|
+
return out.sort()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
export type TemplateRow = {
|
|
143
|
+
name: string // fragment or generator name
|
|
144
|
+
kind: 'fragment' | 'generator'
|
|
145
|
+
layer: string // which layer currently provides it
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// List each template with the layer that currently provides it.
|
|
149
|
+
function listTemplates(start: string): TemplateRow[] {
|
|
150
|
+
const project = resolveProject(start)
|
|
151
|
+
|
|
152
|
+
const rows: TemplateRow[] = []
|
|
153
|
+
|
|
154
|
+
for (const gen of GENERATORS) {
|
|
155
|
+
const override = Path.join(project.gen, gen + '.ts')
|
|
156
|
+
rows.push({
|
|
157
|
+
name: gen,
|
|
158
|
+
kind: 'generator',
|
|
159
|
+
layer: Fs.existsSync(override) ?
|
|
160
|
+
'project (src/gen/' + gen + '.ts)' : 'package',
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const frag of pkgFragments(project)) {
|
|
165
|
+
rows.push({
|
|
166
|
+
name: frag,
|
|
167
|
+
kind: 'fragment',
|
|
168
|
+
layer: Fs.existsSync(Path.join(project.tm, frag)) ?
|
|
169
|
+
'project (tm/' + frag + ')' : 'package',
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return rows
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
// Eject a fragment (text template) into backend/tm/lambda/.
|
|
178
|
+
function ejectFragment(start: string, name: string): string {
|
|
179
|
+
const project = resolveProject(start)
|
|
180
|
+
|
|
181
|
+
let frag = GENERATOR_FRAG[name] || name
|
|
182
|
+
if (!frag.includes('/')) {
|
|
183
|
+
frag = 'lambda/' + frag
|
|
184
|
+
}
|
|
185
|
+
const srcpath = Path.join(project.pkgtm, frag)
|
|
186
|
+
if (!Fs.existsSync(srcpath)) {
|
|
187
|
+
throw new Error('unknown fragment: ' + frag +
|
|
188
|
+
' (available: ' + pkgFragments(project).join(', ') + ')')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const dest = Path.join(project.tm, frag)
|
|
192
|
+
if (Fs.existsSync(dest)) {
|
|
193
|
+
throw new Error('already ejected: ' + Path.relative(start, dest))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const content = Fs.readFileSync(srcpath, 'utf8')
|
|
197
|
+
Fs.mkdirSync(Path.dirname(dest), { recursive: true })
|
|
198
|
+
Fs.writeFileSync(dest, content)
|
|
199
|
+
|
|
200
|
+
const prov = readProvenance(project)
|
|
201
|
+
prov[frag] = {
|
|
202
|
+
package: '@voxgig/build',
|
|
203
|
+
version: pkgVersion(project),
|
|
204
|
+
sha256: sha256(content),
|
|
205
|
+
}
|
|
206
|
+
writeProvenance(project, prov)
|
|
207
|
+
|
|
208
|
+
return dest
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
// Eject a generator source (compiled component override) into
|
|
213
|
+
// backend/src/gen/<name>.ts, rewiring package-internal imports to the
|
|
214
|
+
// public '@voxgig/build' API.
|
|
215
|
+
function ejectCode(start: string, name: string): string {
|
|
216
|
+
const project = resolveProject(start)
|
|
217
|
+
|
|
218
|
+
if (!GENERATORS.includes(name)) {
|
|
219
|
+
throw new Error('unknown generator: ' + name +
|
|
220
|
+
' (available: ' + GENERATORS.join(', ') + ')')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const srcpath = Path.join(project.pkg, 'env', 'lambda', name + '.ts')
|
|
224
|
+
if (!Fs.existsSync(srcpath)) {
|
|
225
|
+
throw new Error('template source not shipped by installed ' +
|
|
226
|
+
'@voxgig/build (' + pkgVersion(project) + ') - needs >= 4.1.0')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const dest = Path.join(project.gen, name + '.ts')
|
|
230
|
+
if (Fs.existsSync(dest)) {
|
|
231
|
+
throw new Error('already ejected: ' + Path.relative(start, dest))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let src = Fs.readFileSync(srcpath, 'utf8')
|
|
235
|
+
|
|
236
|
+
// Package-internal imports -> public API.
|
|
237
|
+
src = src
|
|
238
|
+
.replace(/from '\.\/generate'/g, "from '@voxgig/build'")
|
|
239
|
+
.replace(/from '\.\.\/\.\.\/shape\/msg'/g, "from '@voxgig/build'")
|
|
240
|
+
.replace(/from '\.\.\/\.\.\/shape\/conf'/g, "from '@voxgig/build'")
|
|
241
|
+
.replace(/from '\.\.\/\.\.\/yml\/res_dynamo_yml'/g, "from '@voxgig/build'")
|
|
242
|
+
|
|
243
|
+
const version = pkgVersion(project)
|
|
244
|
+
src = '// Ejected from @voxgig/build ' + version +
|
|
245
|
+
' (env/lambda/' + name + '.ts).\n' +
|
|
246
|
+
'// This project copy now OWNS this generator: the build action uses\n' +
|
|
247
|
+
'// it instead of the package default. After editing, run:\n' +
|
|
248
|
+
'// npm run build && npm run model-build\n' +
|
|
249
|
+
'// Compare with the package version: voxgig-system template diff\n' +
|
|
250
|
+
src
|
|
251
|
+
|
|
252
|
+
Fs.mkdirSync(project.gen, { recursive: true })
|
|
253
|
+
Fs.writeFileSync(dest, src)
|
|
254
|
+
|
|
255
|
+
const prov = readProvenance(project)
|
|
256
|
+
prov['code:' + name] = {
|
|
257
|
+
package: '@voxgig/build',
|
|
258
|
+
version,
|
|
259
|
+
sha256: sha256(Fs.readFileSync(srcpath)),
|
|
260
|
+
}
|
|
261
|
+
writeProvenance(project, prov)
|
|
262
|
+
|
|
263
|
+
return dest
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
export type DiffRow = {
|
|
268
|
+
name: string
|
|
269
|
+
upstream: 'unchanged' | 'changed' | 'missing'
|
|
270
|
+
diff?: string // unified diff of package vs project copy
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// For each ejected template, report upstream drift (package version now
|
|
274
|
+
// differs from what was ejected) and show package-vs-project diffs.
|
|
275
|
+
function diffTemplates(start: string): DiffRow[] {
|
|
276
|
+
const project = resolveProject(start)
|
|
277
|
+
const prov = readProvenance(project)
|
|
278
|
+
|
|
279
|
+
const rows: DiffRow[] = []
|
|
280
|
+
|
|
281
|
+
for (const key of Object.keys(prov).sort()) {
|
|
282
|
+
const rec = prov[key]
|
|
283
|
+
const isCode = key.startsWith('code:')
|
|
284
|
+
const name = isCode ? key.substring(5) : key
|
|
285
|
+
|
|
286
|
+
const pkgfile = isCode ?
|
|
287
|
+
Path.join(project.pkg, 'env', 'lambda', name + '.ts') :
|
|
288
|
+
Path.join(project.pkgtm, name)
|
|
289
|
+
const projfile = isCode ?
|
|
290
|
+
Path.join(project.gen, name + '.ts') :
|
|
291
|
+
Path.join(project.tm, name)
|
|
292
|
+
|
|
293
|
+
if (!Fs.existsSync(pkgfile)) {
|
|
294
|
+
rows.push({ name: key, upstream: 'missing' })
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const upstream =
|
|
299
|
+
sha256(Fs.readFileSync(pkgfile)) === rec.sha256 ?
|
|
300
|
+
'unchanged' : 'changed'
|
|
301
|
+
|
|
302
|
+
let diff: string | undefined = undefined
|
|
303
|
+
if (Fs.existsSync(projfile)) {
|
|
304
|
+
const res = spawnSync('diff', ['-u', pkgfile, projfile],
|
|
305
|
+
{ encoding: 'utf8' })
|
|
306
|
+
diff = 0 === res.status ? '' : res.stdout
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
rows.push({ name: key, upstream, diff })
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return rows
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
export {
|
|
317
|
+
resolveProject,
|
|
318
|
+
listTemplates,
|
|
319
|
+
ejectFragment,
|
|
320
|
+
ejectCode,
|
|
321
|
+
diffTemplates,
|
|
322
|
+
GENERATORS,
|
|
323
|
+
}
|
package/package.json
CHANGED
package/system.ts
CHANGED
|
@@ -13,7 +13,8 @@ import { Utility } from './lib/utility'
|
|
|
13
13
|
|
|
14
14
|
import { MakeSrv } from './srv/make'
|
|
15
15
|
|
|
16
|
-
import { addEntity, addSrv, addMsg, addFields, resolveModelFiles } from './lib/add'
|
|
16
|
+
import { addEntity, addSrv, addMsg, addFields, addEnv, resolveModelFiles } from './lib/add'
|
|
17
|
+
import { listTemplates, ejectFragment, ejectCode, diffTemplates } from './lib/template'
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
|
|
@@ -221,11 +222,20 @@ const Add = {
|
|
|
221
222
|
srv: addSrv,
|
|
222
223
|
msg: addMsg,
|
|
223
224
|
fields: addFields,
|
|
225
|
+
env: addEnv,
|
|
224
226
|
resolveModelFiles,
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
const Template = {
|
|
230
|
+
list: listTemplates,
|
|
231
|
+
eject: ejectFragment,
|
|
232
|
+
ejectCode,
|
|
233
|
+
diff: diffTemplates,
|
|
234
|
+
}
|
|
235
|
+
|
|
227
236
|
export {
|
|
228
237
|
Add,
|
|
238
|
+
Template,
|
|
229
239
|
gubuify,
|
|
230
240
|
System,
|
|
231
241
|
MakeSrv,
|