@voxgig/model 10.0.1 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-model +4 -4
- package/dist/model.js +29 -1
- package/dist/model.js.map +1 -1
- package/dist/producer/msg.d.ts +4 -0
- package/dist/producer/msg.js +170 -0
- package/dist/producer/msg.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/src/build.ts +254 -0
- package/src/config.ts +49 -0
- package/src/init.ts +65 -0
- package/src/model.ts +330 -0
- package/src/producer/local.ts +116 -0
- package/src/producer/model.ts +122 -0
- package/src/producer/msg.ts +207 -0
- package/src/tsconfig.json +19 -0
- package/src/types.ts +162 -0
- package/src/watch.ts +335 -0
package/src/build.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/* Copyright © 2021-2024 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { Aontu } from 'aontu'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
Build,
|
|
9
|
+
BuildResult,
|
|
10
|
+
BuildContext,
|
|
11
|
+
BuildSpec,
|
|
12
|
+
RunSpec,
|
|
13
|
+
Log,
|
|
14
|
+
ProducerDef
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BuildImpl implements Build {
|
|
19
|
+
id
|
|
20
|
+
base
|
|
21
|
+
path
|
|
22
|
+
opts: any
|
|
23
|
+
pdef: ProducerDef[]
|
|
24
|
+
spec: BuildSpec
|
|
25
|
+
model: any
|
|
26
|
+
use = {}
|
|
27
|
+
errs: any[] = []
|
|
28
|
+
ctx: BuildContext
|
|
29
|
+
log: Log
|
|
30
|
+
fs: any
|
|
31
|
+
dryrun: boolean
|
|
32
|
+
args: any
|
|
33
|
+
aontu: Aontu
|
|
34
|
+
deps: any
|
|
35
|
+
|
|
36
|
+
// Signature of the last successful generate: path -> mtimeMs (-1 = missing).
|
|
37
|
+
// When every tracked file still matches, resolveModel() reuses this.model.
|
|
38
|
+
cacheSig: Map<string, number> | null = null
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
constructor(spec: BuildSpec, log: Log) {
|
|
42
|
+
this.id = String(Math.random()).substring(3, 9)
|
|
43
|
+
this.log = log
|
|
44
|
+
|
|
45
|
+
this.spec = spec
|
|
46
|
+
|
|
47
|
+
this.dryrun = !!spec.dryrun
|
|
48
|
+
this.args = spec.buildargs
|
|
49
|
+
this.fs = spec.fs
|
|
50
|
+
this.base = null == spec.base ? '' : spec.base
|
|
51
|
+
this.path = null == spec.path ? '' : spec.path
|
|
52
|
+
this.opts = {}
|
|
53
|
+
this.ctx = { step: 'pre', watch: false, state: {} }
|
|
54
|
+
|
|
55
|
+
if (null != spec.base) {
|
|
56
|
+
this.opts.base = spec.base
|
|
57
|
+
this.opts.path = spec.path
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (null != spec.require) {
|
|
61
|
+
this.opts.require = spec.require
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.pdef = spec.res || []
|
|
65
|
+
|
|
66
|
+
Object.assign(this.use, spec.use || {})
|
|
67
|
+
|
|
68
|
+
this.deps = {}
|
|
69
|
+
this.aontu = new Aontu()
|
|
70
|
+
|
|
71
|
+
// Aontu comments are `#` only. The npm engine's jsonic parser also
|
|
72
|
+
// enables `//` and `/* */` by default, which the Go engine does not
|
|
73
|
+
// support — disable them so both implementations reject the same sources.
|
|
74
|
+
this.aontu.lang.jsonic.options({
|
|
75
|
+
comment: { def: { slash: null, multi: null } }
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async run(rspec: RunSpec): Promise<BuildResult> {
|
|
81
|
+
let hasErr = false
|
|
82
|
+
let runlog = []
|
|
83
|
+
|
|
84
|
+
// Reset per-run error state. The BuildImpl is reused across watch
|
|
85
|
+
// rebuilds, so without this a single failure would stick to every
|
|
86
|
+
// later build. Reassign (don't clear in place) so a previously
|
|
87
|
+
// returned BuildResult keeps its own errors.
|
|
88
|
+
this.errs = []
|
|
89
|
+
|
|
90
|
+
this.ctx = { step: 'pre', state: {}, watch: rspec.watch }
|
|
91
|
+
const plog: any[] = []
|
|
92
|
+
|
|
93
|
+
if (!hasErr) {
|
|
94
|
+
runlog.push('model:initial')
|
|
95
|
+
hasErr = await this.resolveModel()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let forceReload = false
|
|
99
|
+
|
|
100
|
+
if (!hasErr) {
|
|
101
|
+
for (let producer of this.pdef) {
|
|
102
|
+
try {
|
|
103
|
+
runlog.push('producer:pre:' + producer.build.name)
|
|
104
|
+
let pr = await producer.build(this, this.ctx)
|
|
105
|
+
forceReload = forceReload || pr.reload
|
|
106
|
+
plog.push(pr)
|
|
107
|
+
if (!pr.ok) {
|
|
108
|
+
hasErr = true
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err: any) {
|
|
113
|
+
hasErr = true
|
|
114
|
+
this.errs.push(err)
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Only reload when a pre-producer actually modified model sources
|
|
121
|
+
// (signalled via pr.reload). Previously this always ran on success.
|
|
122
|
+
const reload = forceReload && !hasErr
|
|
123
|
+
|
|
124
|
+
if (reload) {
|
|
125
|
+
runlog.push('model:full')
|
|
126
|
+
hasErr = await this.resolveModel()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!hasErr) {
|
|
130
|
+
this.ctx.step = 'post'
|
|
131
|
+
|
|
132
|
+
for (let producer of this.pdef) {
|
|
133
|
+
try {
|
|
134
|
+
runlog.push('producer:post:' + producer.build.name)
|
|
135
|
+
let pr = await producer.build(this, this.ctx)
|
|
136
|
+
plog.push(pr)
|
|
137
|
+
if (!pr.ok) {
|
|
138
|
+
hasErr = true
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err: any) {
|
|
143
|
+
hasErr = true
|
|
144
|
+
this.errs.push(err)
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const br: BuildResult =
|
|
152
|
+
{
|
|
153
|
+
// TODO: remove need for this
|
|
154
|
+
build: () => this,
|
|
155
|
+
|
|
156
|
+
ok: !hasErr,
|
|
157
|
+
producers: plog,
|
|
158
|
+
errs: this.errs,
|
|
159
|
+
runlog
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return br
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async resolveModel() {
|
|
167
|
+
if (this.model && this.cacheSig && this.cacheHit()) {
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let hasErr = false
|
|
172
|
+
|
|
173
|
+
let src: string = ''
|
|
174
|
+
if (!hasErr) {
|
|
175
|
+
try {
|
|
176
|
+
src = this.fs.readFileSync(this.path, 'utf8')
|
|
177
|
+
}
|
|
178
|
+
catch (err: any) {
|
|
179
|
+
hasErr = true
|
|
180
|
+
this.errs.push(err)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!hasErr) {
|
|
185
|
+
// Collect this generation's errors into a fresh array. The option key
|
|
186
|
+
// is `err` (not `errs`) — that is what aontu reads, and providing it
|
|
187
|
+
// puts aontu in collect mode so model errors are gathered rather than
|
|
188
|
+
// thrown. A per-call array avoids leaking errors into later builds.
|
|
189
|
+
const modelErrs: any[] = []
|
|
190
|
+
this.opts.err = modelErrs
|
|
191
|
+
this.opts.deps = this.deps
|
|
192
|
+
this.opts.fs = this.fs
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
this.model = this.aontu.generate(src, this.opts)
|
|
196
|
+
}
|
|
197
|
+
catch (err: any) {
|
|
198
|
+
// collect mode normally prevents throws, but guard the rare cases
|
|
199
|
+
// (e.g. unresolved imports) so they surface as build errors.
|
|
200
|
+
modelErrs.push(err)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (0 < modelErrs.length) {
|
|
204
|
+
hasErr = true
|
|
205
|
+
this.errs.push(...modelErrs)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.cacheSig = hasErr ? null : this.snapshotSig()
|
|
210
|
+
|
|
211
|
+
return hasErr
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
// Collect mtimeMs for the root file and every file aontu recorded as a dep.
|
|
216
|
+
snapshotSig(): Map<string, number> {
|
|
217
|
+
const sig = new Map<string, number>()
|
|
218
|
+
sig.set(this.path, mtime(this.fs, this.path))
|
|
219
|
+
for (const parent of Object.keys(this.deps)) {
|
|
220
|
+
for (const child of Object.keys(this.deps[parent])) {
|
|
221
|
+
if (!sig.has(child)) sig.set(child, mtime(this.fs, child))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return sig
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
cacheHit(): boolean {
|
|
229
|
+
if (!this.cacheSig) return false
|
|
230
|
+
for (const [path, prev] of this.cacheSig) {
|
|
231
|
+
if (mtime(this.fs, path) !== prev) return false
|
|
232
|
+
}
|
|
233
|
+
return true
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
function mtime(fs: any, path: string): number {
|
|
239
|
+
try { return fs.statSync(path).mtimeMs }
|
|
240
|
+
catch { return -1 }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
function makeBuild(spec: BuildSpec, log: Log) {
|
|
245
|
+
return new BuildImpl(spec, log)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export {
|
|
249
|
+
makeBuild,
|
|
250
|
+
BuildSpec,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import type { BuildResult, BuildSpec, Log } from './types'
|
|
5
|
+
|
|
6
|
+
import { Watch } from './watch'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Config {
|
|
11
|
+
build: BuildSpec
|
|
12
|
+
watch: Watch
|
|
13
|
+
log: Log
|
|
14
|
+
|
|
15
|
+
constructor(spec: BuildSpec, log: Log) {
|
|
16
|
+
this.log = log
|
|
17
|
+
|
|
18
|
+
this.build = {
|
|
19
|
+
path: spec.path,
|
|
20
|
+
base: spec.base,
|
|
21
|
+
res: [
|
|
22
|
+
...(spec.res || [])
|
|
23
|
+
],
|
|
24
|
+
require: spec.require,
|
|
25
|
+
log: this.log,
|
|
26
|
+
fs: spec.fs
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.watch = new Watch(this.build, this.log)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async run(watch: boolean): Promise<BuildResult> {
|
|
33
|
+
return this.watch.run('config', watch, '<config>')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async start(initial: boolean = true) {
|
|
37
|
+
return this.watch.start(initial)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async stop() {
|
|
41
|
+
return this.watch.stop()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export { Config, BuildSpec }
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
package/src/init.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
import Path from 'node:path'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const STARTER_MODEL = `# Voxgig model. Edit this file, then build it:
|
|
7
|
+
# voxgig-model model/model.aon
|
|
8
|
+
#
|
|
9
|
+
# Models are unified .aon - add types, defaults, references, imports.
|
|
10
|
+
# Tutorial: https://github.com/voxgig/model/blob/main/docs/tutorial.md
|
|
11
|
+
|
|
12
|
+
name: 'my-model'
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
const STARTER_CONFIG = `# Model configuration. Declare build actions and their order here.
|
|
16
|
+
#
|
|
17
|
+
# Example (TypeScript loads the module; Go binds the name to a
|
|
18
|
+
# registered action func):
|
|
19
|
+
# sys: model: action: { example: load: 'build/example' }
|
|
20
|
+
# sys: model: order: action: 'example'
|
|
21
|
+
|
|
22
|
+
sys: model: action: {}
|
|
23
|
+
sys: model: order: action: *''
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
type InitResult = {
|
|
28
|
+
created: string[]
|
|
29
|
+
skipped: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// Scaffold a starter model and config under <dir>/model. Existing files are
|
|
34
|
+
// left untouched.
|
|
35
|
+
function initModel(dir: string, fs: any): InitResult {
|
|
36
|
+
const d = dir || '.'
|
|
37
|
+
const files: [string, string][] = [
|
|
38
|
+
[Path.join(d, 'model', 'model.aon'), STARTER_MODEL],
|
|
39
|
+
[Path.join(d, 'model', '.model-config', 'model-config.aon'), STARTER_CONFIG],
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const created: string[] = []
|
|
43
|
+
const skipped: string[] = []
|
|
44
|
+
|
|
45
|
+
for (const [p, content] of files) {
|
|
46
|
+
if (fs.existsSync(p)) {
|
|
47
|
+
skipped.push(p)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
fs.mkdirSync(Path.dirname(p), { recursive: true })
|
|
51
|
+
fs.writeFileSync(p, content)
|
|
52
|
+
created.push(p)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { created, skipped }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
initModel,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type {
|
|
64
|
+
InitResult,
|
|
65
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import * as NodeFs from 'node:fs'
|
|
5
|
+
|
|
6
|
+
import { memfs as MemFs } from 'memfs'
|
|
7
|
+
|
|
8
|
+
import { prettyPino } from '@voxgig/util'
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
Build,
|
|
12
|
+
BuildResult,
|
|
13
|
+
ProducerDef,
|
|
14
|
+
BuildContext,
|
|
15
|
+
BuildSpec,
|
|
16
|
+
ModelSpec,
|
|
17
|
+
Log,
|
|
18
|
+
FST,
|
|
19
|
+
ProducerResult,
|
|
20
|
+
} from './types'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
import { Config } from './config'
|
|
24
|
+
import { Watch } from './watch'
|
|
25
|
+
|
|
26
|
+
import { model_producer } from './producer/model'
|
|
27
|
+
import { local_producer } from './producer/local'
|
|
28
|
+
import { msg_producer } from './producer/msg'
|
|
29
|
+
|
|
30
|
+
import { initModel } from './init'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Model {
|
|
34
|
+
config?: Config
|
|
35
|
+
build: BuildSpec
|
|
36
|
+
watch: Watch
|
|
37
|
+
|
|
38
|
+
trigger_model = false
|
|
39
|
+
|
|
40
|
+
log: Log
|
|
41
|
+
fs: any
|
|
42
|
+
|
|
43
|
+
constructor(mspec: ModelSpec) {
|
|
44
|
+
const self = this
|
|
45
|
+
|
|
46
|
+
this.fs = { ...(mspec.fs || NodeFs) }
|
|
47
|
+
|
|
48
|
+
if (mspec.dryrun) {
|
|
49
|
+
makeReadOnly(this.fs)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pino = prettyPino('model', mspec as any)
|
|
53
|
+
|
|
54
|
+
this.log = pino.child({ cmp: 'model' })
|
|
55
|
+
|
|
56
|
+
this.log.info({ point: 'model-init' })
|
|
57
|
+
if (this.log.isLevelEnabled('debug')) {
|
|
58
|
+
this.log.debug({
|
|
59
|
+
point: 'model-spec', mspec, note: '\n' +
|
|
60
|
+
JSON.stringify({ ...mspec, src: '<NOT-SHOWN>' }, null, 2)
|
|
61
|
+
.replace(/"/g, '')
|
|
62
|
+
.replaceAll(process.cwd(), '.')
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Config is a special Watch to handle model config. It is optional: when
|
|
67
|
+
// mspec.config is false, the .model-config/ build is skipped entirely and
|
|
68
|
+
// the model runs on its own (see run/start below).
|
|
69
|
+
const useConfig = false !== mspec.config
|
|
70
|
+
|
|
71
|
+
this.config = !useConfig ? undefined : makeConfig(mspec, this.log, this.fs, {
|
|
72
|
+
path: '/',
|
|
73
|
+
build: async function trigger_model(build: Build, ctx: BuildContext) {
|
|
74
|
+
let pres: ProducerResult = {
|
|
75
|
+
ok: false, name: 'config', step: '', active: true, reload: false, errs: [], runlog: []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ('post' !== ctx.step) {
|
|
79
|
+
pres.ok = true
|
|
80
|
+
return pres
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if (self.trigger_model) {
|
|
85
|
+
|
|
86
|
+
// TODO: better design
|
|
87
|
+
// Point the config's last result at the current build so the model
|
|
88
|
+
// producer reads fresh config state. It must be a thunk to satisfy
|
|
89
|
+
// BuildResult.build's `() => Build` contract (consumers call it).
|
|
90
|
+
const lastConfig = self.build.use?.config?.watch?.last
|
|
91
|
+
if (lastConfig) {
|
|
92
|
+
lastConfig.build = () => build
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const br = await self.watch.run('model', true)
|
|
96
|
+
pres.ok = br.ok
|
|
97
|
+
pres.errs = br.errs
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
self.trigger_model = true
|
|
101
|
+
pres.ok = true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ctx.watch) {
|
|
105
|
+
const watchmap = build.model?.sys?.model?.watch
|
|
106
|
+
|
|
107
|
+
if (watchmap) {
|
|
108
|
+
Object.keys(watchmap).forEach((file: string) => {
|
|
109
|
+
self.watch.add(file)
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return pres
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// The actual model.
|
|
119
|
+
this.build = {
|
|
120
|
+
path: mspec.path,
|
|
121
|
+
base: mspec.base,
|
|
122
|
+
debug: mspec.debug,
|
|
123
|
+
dryrun: mspec.dryrun,
|
|
124
|
+
buildargs: mspec.buildargs,
|
|
125
|
+
use: self.config ? { config: self.config } : {},
|
|
126
|
+
res: [
|
|
127
|
+
// Validates message declarations (pre phase), so an inconsistent
|
|
128
|
+
// main.msg fails the build before any output is written.
|
|
129
|
+
{
|
|
130
|
+
path: '/',
|
|
131
|
+
build: msg_producer
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
path: '/',
|
|
135
|
+
build: model_producer
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
path: '/',
|
|
139
|
+
build: local_producer
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
require: mspec.require,
|
|
143
|
+
log: this.log,
|
|
144
|
+
fs: this.fs,
|
|
145
|
+
watch: mspec.watch,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.watch = new Watch(self.build, this.log)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
// Run once. With config enabled, the config build runs first and triggers
|
|
153
|
+
// the model build; without it, the model build runs directly.
|
|
154
|
+
async run(): Promise<BuildResult> {
|
|
155
|
+
this.trigger_model = false
|
|
156
|
+
if (!this.config) {
|
|
157
|
+
return this.watch.run('model', false, '<start>')
|
|
158
|
+
}
|
|
159
|
+
const br = await this.config.run(false)
|
|
160
|
+
return br.ok ? this.watch.run('model', false, '<start>') : br
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// Start watching for file changes. Runs an initial build, then watches
|
|
165
|
+
// both the model files and (when enabled) the config files for ongoing
|
|
166
|
+
// changes.
|
|
167
|
+
async start() {
|
|
168
|
+
this.trigger_model = false
|
|
169
|
+
if (!this.config) {
|
|
170
|
+
return this.watch.start()
|
|
171
|
+
}
|
|
172
|
+
const br = await this.config.run(true)
|
|
173
|
+
if (!br.ok) {
|
|
174
|
+
return br
|
|
175
|
+
}
|
|
176
|
+
// Watch config files too. The initial config build is already done
|
|
177
|
+
// above, so start without forcing another one; a later config change
|
|
178
|
+
// rebuilds the config and re-triggers the model build.
|
|
179
|
+
this.config.start(false)
|
|
180
|
+
return this.watch.start()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async stop() {
|
|
185
|
+
// start() also spins up a config-file watcher; stop both so no
|
|
186
|
+
// chokidar handle is left open keeping the process alive.
|
|
187
|
+
await this.config?.stop()
|
|
188
|
+
return this.watch.stop()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
function makeConfig(mspec: ModelSpec, log: Log, fs: any, trigger_model_build: ProducerDef) {
|
|
194
|
+
let cbase = mspec.base + '/.model-config'
|
|
195
|
+
let cpath = cbase + '/model-config.aon'
|
|
196
|
+
|
|
197
|
+
// MIGRATE A LEGACY .aontu CONFIG RATHER THAN WRITING OVER IT. This block
|
|
198
|
+
// CREATES the config when it finds none, so looking only for `.aon` in a
|
|
199
|
+
// project that has a `model-config.aontu` would not read the old file — it
|
|
200
|
+
// would decide there is no config and write a fresh default beside it,
|
|
201
|
+
// silently discarding whatever the project had declared there.
|
|
202
|
+
const legacycpath = cbase + '/model-config.aontu'
|
|
203
|
+
if (!fs.existsSync(cpath) && fs.existsSync(legacycpath)) {
|
|
204
|
+
// NOT A VERBATIM COPY. This package's OWN config moved to .aon in v10, so
|
|
205
|
+
// a legacy config's import of it names a file that no longer ships and the
|
|
206
|
+
// migrated config fails to resolve - `aontu/multisource_not_found:
|
|
207
|
+
// @voxgig/model/model/.model-config/model-config.aontu` - on the first
|
|
208
|
+
// build after upgrading. Renaming the file without retargeting that import
|
|
209
|
+
// just moves the breakage.
|
|
210
|
+
//
|
|
211
|
+
// Only THIS package's import is retargeted, and only where it is an
|
|
212
|
+
// IMPORT. Two things are deliberately left alone:
|
|
213
|
+
//
|
|
214
|
+
// - a project's own `.aontu` imports, which still name real files on
|
|
215
|
+
// its disk that nothing here renamed;
|
|
216
|
+
// - this same pathname held as ordinary string DATA (a note, a
|
|
217
|
+
// compatibility path in action metadata).
|
|
218
|
+
//
|
|
219
|
+
// Hence the match is anchored to aontu's `@"..."` import syntax, closing
|
|
220
|
+
// quote included, rather than to the bare pathname. Both are declarations
|
|
221
|
+
// the migration exists to preserve, and silently editing one during a
|
|
222
|
+
// one-time migration is precisely the failure this whole block guards
|
|
223
|
+
// against.
|
|
224
|
+
const legacy = fs.readFileSync(legacycpath, 'utf8')
|
|
225
|
+
fs.writeFileSync(cpath, legacy.replace(
|
|
226
|
+
/@(\s*)"(@voxgig\/model\/[^"]*model-config)\.aontu"/g, '@$1"$2.aon"'))
|
|
227
|
+
try { fs.unlinkSync(legacycpath) } catch (_err: any) { }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!fs.existsSync(cpath)) {
|
|
231
|
+
fs.mkdirSync(cbase, { recursive: true })
|
|
232
|
+
fs.writeFileSync(cpath, `
|
|
233
|
+
@"@voxgig/model/model/.model-config/model-config.aon"
|
|
234
|
+
|
|
235
|
+
sys: model: action: {}
|
|
236
|
+
`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let cspec: BuildSpec = {
|
|
240
|
+
name: 'config',
|
|
241
|
+
path: cpath,
|
|
242
|
+
base: cbase,
|
|
243
|
+
debug: mspec.debug,
|
|
244
|
+
res: [
|
|
245
|
+
|
|
246
|
+
// Generate full config model and save as a file.
|
|
247
|
+
{
|
|
248
|
+
path: '/',
|
|
249
|
+
build: model_producer
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// Trigger main model build.
|
|
253
|
+
trigger_model_build
|
|
254
|
+
],
|
|
255
|
+
require: mspec.require,
|
|
256
|
+
log,
|
|
257
|
+
fs,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return new Config(cspec, log)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
function makeReadOnly(fsm: FST) {
|
|
265
|
+
|
|
266
|
+
// NOTE: NOT COMPLETE!
|
|
267
|
+
// Just for internal use,
|
|
268
|
+
const writers = [
|
|
269
|
+
'writeFile',
|
|
270
|
+
'writeFileSync',
|
|
271
|
+
'appendFile',
|
|
272
|
+
'appendFileSync',
|
|
273
|
+
'chmod',
|
|
274
|
+
'chmodSync',
|
|
275
|
+
'chown',
|
|
276
|
+
'chownSync',
|
|
277
|
+
'cp',
|
|
278
|
+
'cpSync',
|
|
279
|
+
'createWriteStream',
|
|
280
|
+
'mkdir',
|
|
281
|
+
'mkdirSync',
|
|
282
|
+
'rename',
|
|
283
|
+
'renameSync',
|
|
284
|
+
'rm',
|
|
285
|
+
'rmSync',
|
|
286
|
+
'rmdir',
|
|
287
|
+
'rmdirSync',
|
|
288
|
+
'symlink',
|
|
289
|
+
'symlinkSync',
|
|
290
|
+
'truncate',
|
|
291
|
+
'truncateSync',
|
|
292
|
+
'unlink',
|
|
293
|
+
'unlinkSync',
|
|
294
|
+
'write',
|
|
295
|
+
'writev',
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
const { fs } = MemFs({ [process.cwd()]: {} })
|
|
299
|
+
|
|
300
|
+
for (let w of writers) {
|
|
301
|
+
if ((fs as any)[w]) {
|
|
302
|
+
(fsm as any)[w] = (fs as any)[w].bind(fs)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Also redirect the promise-based writers. fsm.promises is shared by
|
|
307
|
+
// reference with the real fs module, so replace it with a copy rather
|
|
308
|
+
// than mutating the caller's fs.
|
|
309
|
+
const memPromises = (fs as any).promises
|
|
310
|
+
if ((fsm as any).promises && memPromises) {
|
|
311
|
+
const promises: any = { ...(fsm as any).promises }
|
|
312
|
+
for (let w of writers) {
|
|
313
|
+
if ('function' === typeof memPromises[w]) {
|
|
314
|
+
promises[w] = memPromises[w].bind(memPromises)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
;(fsm as any).promises = promises
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return fsm
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
export {
|
|
325
|
+
Model,
|
|
326
|
+
BuildSpec,
|
|
327
|
+
initModel,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|