@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.
@@ -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
+ }