@voxgig/model 10.0.0 → 10.0.2
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/model.js +22 -1
- package/dist/model.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/build.ts +254 -0
- package/src/config.ts +49 -0
- package/src/init.ts +65 -0
- package/src/model.ts +323 -0
- package/src/producer/local.ts +116 -0
- package/src/producer/model.ts +122 -0
- package/src/tsconfig.json +19 -0
- package/src/types.ts +162 -0
- package/src/watch.ts +335 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
|
|
2
|
+
import Path from 'path'
|
|
3
|
+
|
|
4
|
+
import type { Build, Producer, BuildContext, ProducerResult } from '../types'
|
|
5
|
+
|
|
6
|
+
const ORDERING_SPLIT_RE = /\s*,+\s*/
|
|
7
|
+
|
|
8
|
+
// Runs any producers local to the repo.
|
|
9
|
+
const local_producer: Producer = async (build: Build, ctx: BuildContext) => {
|
|
10
|
+
|
|
11
|
+
ctx.state.local = (ctx.state.local || {})
|
|
12
|
+
let actionDefs = ctx.state.local.actionDefs
|
|
13
|
+
|
|
14
|
+
if (null == actionDefs) {
|
|
15
|
+
actionDefs = ctx.state.local.actionDefs = []
|
|
16
|
+
|
|
17
|
+
// TODO: need to provide project root via build
|
|
18
|
+
let root = Path.resolve(build.path, '..', '..')
|
|
19
|
+
|
|
20
|
+
// TODO: build should do this
|
|
21
|
+
// Config is optional: with no .model-config build linked in, there are no
|
|
22
|
+
// declared actions and the model runs on its own.
|
|
23
|
+
let configBuildResult = build.use.config?.watch?.last
|
|
24
|
+
let configBuild = configBuildResult?.build()
|
|
25
|
+
let config = configBuild?.model || {}
|
|
26
|
+
|
|
27
|
+
let actions = config.sys?.model?.action ||
|
|
28
|
+
// NOTE: backwards compat
|
|
29
|
+
config.sys?.model?.builders ||
|
|
30
|
+
{}
|
|
31
|
+
|
|
32
|
+
let ordering = config.sys?.model?.order?.action
|
|
33
|
+
ordering = null == ordering ? Object.keys(actions) :
|
|
34
|
+
ordering.split(ORDERING_SPLIT_RE).filter((n: string) => null != n && '' != n)
|
|
35
|
+
|
|
36
|
+
// load actions
|
|
37
|
+
for (let name of ordering) {
|
|
38
|
+
let actiondef = actions[name]
|
|
39
|
+
|
|
40
|
+
if (null == actiondef) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'Unknown model action "' + name +
|
|
43
|
+
'" referenced in sys.model.order.action')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (null == actiondef.load) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Model action "' + name + '" is missing a "load" path')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let actionpath = Path.join(root, actiondef.load)
|
|
52
|
+
|
|
53
|
+
let action = require(actionpath)
|
|
54
|
+
|
|
55
|
+
if (action instanceof Promise) {
|
|
56
|
+
action = await action
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const step = action.step || 'post'
|
|
60
|
+
|
|
61
|
+
actionDefs.push({ name, actiondef, action, step })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const runActionDefs = actionDefs.filter((ad: any) => ctx.step === ad.step || 'all' === ad.step)
|
|
66
|
+
|
|
67
|
+
build.log.info({
|
|
68
|
+
point: ctx.step + '-actions', step: ctx.step, actions: runActionDefs,
|
|
69
|
+
note: runActionDefs.map((ad: any) => ad.name).join(';')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
let ok = true
|
|
73
|
+
let areslog = []
|
|
74
|
+
let reload = false
|
|
75
|
+
|
|
76
|
+
for (let actionDef of runActionDefs) {
|
|
77
|
+
try {
|
|
78
|
+
// TODO: this call signature needs to be well-defined as it is an external interface
|
|
79
|
+
let ares = await actionDef.action(build.model, build, ctx)
|
|
80
|
+
ok = ok && (null == ares || !!ares.ok)
|
|
81
|
+
reload = reload || ares?.reload
|
|
82
|
+
|
|
83
|
+
areslog.push(ares)
|
|
84
|
+
|
|
85
|
+
if (!ok) { break }
|
|
86
|
+
}
|
|
87
|
+
catch (err: any) {
|
|
88
|
+
if (!err.__logged__) {
|
|
89
|
+
build.log.error({
|
|
90
|
+
point: ctx.step + '-action', step: ctx.step, action: actionDef,
|
|
91
|
+
note: actionDef.name,
|
|
92
|
+
err
|
|
93
|
+
})
|
|
94
|
+
err.__logged__ = true
|
|
95
|
+
}
|
|
96
|
+
throw err
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let pr: ProducerResult = {
|
|
101
|
+
ok,
|
|
102
|
+
reload,
|
|
103
|
+
name: 'local',
|
|
104
|
+
step: ctx.step,
|
|
105
|
+
active: true,
|
|
106
|
+
errs: [],
|
|
107
|
+
runlog: []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return pr
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
local_producer
|
|
116
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
|
|
2
|
+
import Path from 'path'
|
|
3
|
+
|
|
4
|
+
import type { Build, Producer, BuildContext, ProducerResult } from '../types'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// Serialize the model to two-space-indented JSON with object keys in strictly
|
|
8
|
+
// lexical (UTF-8 byte) order, byte-for-byte identical to the Go
|
|
9
|
+
// implementation's encoding/json. JSON.stringify cannot express this: JS
|
|
10
|
+
// objects iterate integer-like keys ("9", "10") in numeric order ahead of the
|
|
11
|
+
// other keys regardless of insertion order, so the order must be imposed
|
|
12
|
+
// during serialization, and the default JS string sort compares UTF-16 code
|
|
13
|
+
// units, which disagrees with Go's byte order for astral-plane keys. Arrays
|
|
14
|
+
// keep their order. Values only producer mutation can introduce mirror
|
|
15
|
+
// JSON.stringify: undefined, functions, and symbols are dropped from objects
|
|
16
|
+
// and become null in arrays (sparse holes too), and toJSON results are fed
|
|
17
|
+
// back through the canonical serializer.
|
|
18
|
+
function jsonify(value: any, indent: string): string {
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
if (0 === value.length) {
|
|
21
|
+
return '[]'
|
|
22
|
+
}
|
|
23
|
+
const inner = indent + ' '
|
|
24
|
+
// Array.from visits holes (as undefined); map/join would skip them.
|
|
25
|
+
return '[\n' +
|
|
26
|
+
Array.from(value, (item) => inner + jsonify(item, inner)).join(',\n') +
|
|
27
|
+
'\n' + indent + ']'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (null != value && 'object' === typeof value) {
|
|
31
|
+
if ('function' === typeof value.toJSON) {
|
|
32
|
+
return jsonify(value.toJSON(), indent)
|
|
33
|
+
}
|
|
34
|
+
const keys = Object.keys(value)
|
|
35
|
+
.filter((key) => {
|
|
36
|
+
const item = value[key]
|
|
37
|
+
return undefined !== item &&
|
|
38
|
+
'function' !== typeof item && 'symbol' !== typeof item
|
|
39
|
+
})
|
|
40
|
+
.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))
|
|
41
|
+
if (0 === keys.length) {
|
|
42
|
+
return '{}'
|
|
43
|
+
}
|
|
44
|
+
const inner = indent + ' '
|
|
45
|
+
return '{\n' +
|
|
46
|
+
keys.map((key) =>
|
|
47
|
+
inner + jstr(key) + ': ' + jsonify(value[key], inner))
|
|
48
|
+
.join(',\n') +
|
|
49
|
+
'\n' + indent + '}'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const scalar = jstr(value)
|
|
53
|
+
return undefined === scalar ? 'null' : scalar
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
// JSON.stringify, plus the U+2028/U+2029 escapes Go's encoding/json always
|
|
58
|
+
// applies even with HTML escaping off. JSON.stringify emits the separators
|
|
59
|
+
// literally (both forms are valid JSON), so escape them here for byte parity.
|
|
60
|
+
function jstr(value: any): string | undefined {
|
|
61
|
+
const out = JSON.stringify(value)
|
|
62
|
+
return undefined === out ? undefined :
|
|
63
|
+
out.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
// Builds the main model file, after unification.
|
|
68
|
+
const model_producer: Producer = async (build: Build, ctx: BuildContext) => {
|
|
69
|
+
let pr: ProducerResult = {
|
|
70
|
+
ok: true,
|
|
71
|
+
name: 'model',
|
|
72
|
+
reload: false,
|
|
73
|
+
step: ctx.step,
|
|
74
|
+
active: true,
|
|
75
|
+
errs: [],
|
|
76
|
+
runlog: []
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ('post' !== ctx.step) {
|
|
80
|
+
return pr
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let json = jsonify(build.model, '')
|
|
84
|
+
|
|
85
|
+
let filename = Path.basename(build.path)
|
|
86
|
+
let filenameparts = filename.match(/^(.*)\.[^.]+$/)
|
|
87
|
+
if (filenameparts) {
|
|
88
|
+
filename = filenameparts[1]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let file = build.opts.base + '/' + filename + '.json'
|
|
92
|
+
|
|
93
|
+
// Skip write when output is unchanged — avoids mtime churn that would
|
|
94
|
+
// invalidate caches (here and in downstream watchers).
|
|
95
|
+
let existing: string | undefined
|
|
96
|
+
try { existing = build.fs.readFileSync(file, 'utf8') } catch { }
|
|
97
|
+
|
|
98
|
+
if (existing === json) {
|
|
99
|
+
build.log.debug({
|
|
100
|
+
point: 'write-model-skip',
|
|
101
|
+
path: file,
|
|
102
|
+
note: file.replace(process.cwd(), '.') + ' (unchanged)'
|
|
103
|
+
})
|
|
104
|
+
return pr
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
build.log.info({
|
|
108
|
+
point: 'write-model',
|
|
109
|
+
path: file,
|
|
110
|
+
note: file.replace(process.cwd(), '.')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
build.fs.mkdirSync(Path.dirname(file), { recursive: true })
|
|
114
|
+
build.fs.writeFileSync(file, json)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
return pr
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
model_producer
|
|
122
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"composite": true,
|
|
4
|
+
"tsBuildInfoFile": "../dist/tsconfig.tsbuildinfo",
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"module": "nodenext",
|
|
7
|
+
"noEmitOnError": true,
|
|
8
|
+
"outDir":"../dist",
|
|
9
|
+
"rootDir":".",
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"target": "es2021",
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"declarationDir": "../dist"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* Copyright © 2021-2024 Voxgig Ltd, MIT License. */
|
|
2
|
+
|
|
3
|
+
import Fs from 'node:fs'
|
|
4
|
+
|
|
5
|
+
import Pino from 'pino'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import type { Aontu } from 'aontu'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
type FST = typeof Fs
|
|
12
|
+
|
|
13
|
+
type Log = ReturnType<typeof Pino>
|
|
14
|
+
|
|
15
|
+
interface Build {
|
|
16
|
+
id: string
|
|
17
|
+
base: string
|
|
18
|
+
path: string
|
|
19
|
+
opts: { [key: string]: any }
|
|
20
|
+
pdef: ProducerDef[]
|
|
21
|
+
spec: BuildSpec
|
|
22
|
+
model: any
|
|
23
|
+
use: { [name: string]: any }
|
|
24
|
+
errs: any[]
|
|
25
|
+
ctx: BuildContext
|
|
26
|
+
deps: any
|
|
27
|
+
run: (rspec: RunSpec) => Promise<BuildResult>
|
|
28
|
+
log: Log
|
|
29
|
+
fs: FST
|
|
30
|
+
dryrun: boolean
|
|
31
|
+
args: any
|
|
32
|
+
aontu: Aontu
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
interface BuildResult {
|
|
38
|
+
ok: boolean
|
|
39
|
+
builder?: string
|
|
40
|
+
path?: string
|
|
41
|
+
producers?: ProducerResult[]
|
|
42
|
+
step?: string
|
|
43
|
+
errs: any[]
|
|
44
|
+
runlog: string[]
|
|
45
|
+
build?: () => Build
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
interface BuildContext {
|
|
50
|
+
step: 'pre' | 'post'
|
|
51
|
+
watch: boolean,
|
|
52
|
+
state: Record<string, any>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
interface BuildSpec {
|
|
57
|
+
path?: string
|
|
58
|
+
base?: string
|
|
59
|
+
res?: ProducerDef[]
|
|
60
|
+
require?: any
|
|
61
|
+
use?: { [name: string]: any }
|
|
62
|
+
log?: Log
|
|
63
|
+
idle?: number
|
|
64
|
+
name?: string
|
|
65
|
+
debug?: boolean | string
|
|
66
|
+
dryrun?: boolean,
|
|
67
|
+
buildargs?: any,
|
|
68
|
+
watch?: {
|
|
69
|
+
mod?: boolean // file modification
|
|
70
|
+
add?: boolean // file addition
|
|
71
|
+
rem?: boolean // file deletion
|
|
72
|
+
}
|
|
73
|
+
fs: FST
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
interface ProducerDef {
|
|
78
|
+
path: string
|
|
79
|
+
build: Producer
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type Producer = (
|
|
83
|
+
build: Build,
|
|
84
|
+
ctx: BuildContext,
|
|
85
|
+
) => Promise<ProducerResult>
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
interface ProducerResult {
|
|
89
|
+
ok: boolean
|
|
90
|
+
name: string
|
|
91
|
+
active: boolean
|
|
92
|
+
errs: any[]
|
|
93
|
+
runlog: string[]
|
|
94
|
+
step: string
|
|
95
|
+
reload: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
type Run = {
|
|
100
|
+
canon: string
|
|
101
|
+
path: string
|
|
102
|
+
start: number
|
|
103
|
+
end: number
|
|
104
|
+
result?: BuildResult
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
type RunSpec = {
|
|
109
|
+
watch: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type Canon = {
|
|
113
|
+
path: string
|
|
114
|
+
isFolder: boolean
|
|
115
|
+
when: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
type ChangeItem = {
|
|
120
|
+
path: string
|
|
121
|
+
when: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface ModelSpec {
|
|
125
|
+
path?: string
|
|
126
|
+
base?: string
|
|
127
|
+
require?: any
|
|
128
|
+
log?: Log
|
|
129
|
+
idle?: number
|
|
130
|
+
debug?: boolean | string
|
|
131
|
+
dryrun?: boolean
|
|
132
|
+
buildargs?: any
|
|
133
|
+
fs?: any
|
|
134
|
+
// Resolve a .model-config/model-config.aon (auto-created when missing)
|
|
135
|
+
// that declares the build actions and their order. Defaults to true; set
|
|
136
|
+
// false to skip the config entirely and run the model on its own.
|
|
137
|
+
config?: boolean
|
|
138
|
+
watch?: {
|
|
139
|
+
mod?: boolean // file modification
|
|
140
|
+
add?: boolean // file addition
|
|
141
|
+
rem?: boolean // file deletion
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
export type {
|
|
148
|
+
Build,
|
|
149
|
+
BuildResult,
|
|
150
|
+
ProducerDef,
|
|
151
|
+
Producer,
|
|
152
|
+
ProducerResult,
|
|
153
|
+
BuildContext,
|
|
154
|
+
BuildSpec,
|
|
155
|
+
Log,
|
|
156
|
+
Run,
|
|
157
|
+
Canon,
|
|
158
|
+
ChangeItem,
|
|
159
|
+
RunSpec,
|
|
160
|
+
ModelSpec,
|
|
161
|
+
FST,
|
|
162
|
+
}
|