@voxgig/create-sdkgen 0.30.9 → 0.31.1

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.
@@ -13,6 +13,13 @@ import Pkg from '../package.json'
13
13
 
14
14
  import { TARGETS, forwardingError } from './args'
15
15
 
16
+ import { gitVersions, withCopyMerge } from './copymerge'
17
+
18
+ import {
19
+ SCAFFOLD_BASE, SCAFFOLD_CONFLICT,
20
+ fingerprint, scaffoldMode, readScaffold, recordScaffold,
21
+ } from './scaffold-record'
22
+
16
23
 
17
24
  type FullCreateSdkGenOptions = {
18
25
  fs?: any
@@ -36,6 +43,8 @@ type GenerateSpec = {
36
43
  target?: string[]
37
44
  feature?: string[]
38
45
  install?: boolean // perform installation - run npm install
46
+ force?: boolean // take the incoming scaffold over local changes
47
+ keep?: boolean // keep local changes whose merge stops
39
48
  folder?: string // Target folder for project
40
49
 
41
50
  }
@@ -135,7 +144,40 @@ function CreateSdkGen(opts: FullCreateSdkGenOptions) {
135
144
  note: JSON.stringify(jopts, null, 2)
136
145
  })
137
146
 
138
- const jres = await jostraca.generate(jopts, () => CreateRoot({ model, spec }))
147
+ const kept = new Set<string>([...(rootModule.KEPT ?? []), ...(rootModule.SEEDS ?? [])]
148
+ .map((rel: string) => SDK_FOLDER + '/' + rel))
149
+
150
+ const manage = (rel: string) => !kept.has(rel) &&
151
+ !rel.startsWith(SDK_FOLDER + '/def/') && !rel.startsWith(SDK_FOLDER + '/log/') &&
152
+ !rel.split('/').includes('.jostraca') && !rel.endsWith('.aon')
153
+
154
+ const record = readScaffold(fs, folder)
155
+
156
+ const jres = await withCopyMerge({
157
+ fs,
158
+ folder,
159
+ log,
160
+ mode: scaffoldMode(fs, folder),
161
+ what: 'create-sdkgen ' + name,
162
+ force: !!spec.force,
163
+ keep: !!spec.keep,
164
+ dryrun,
165
+ manage,
166
+ recorded: (rel: string) => record[rel],
167
+ fingerprint,
168
+ basedir: SCAFFOLD_BASE,
169
+ conflictdir: SCAFFOLD_CONFLICT,
170
+ versions: gitVersions,
171
+ }, async (gfs: any) => {
172
+ const res = await jostraca.generate({ ...jopts, fs: () => gfs },
173
+ () => CreateRoot({ model, spec }))
174
+
175
+ if (!dryrun) {
176
+ recordScaffold(gfs, folder, res, Pkg.version, manage, record)
177
+ }
178
+
179
+ return res
180
+ })
139
181
 
140
182
  showChanges(jopts.log, 'generate-result', jres, process.cwd())
141
183
 
@@ -88,6 +88,12 @@ const PROJECT_STUB = `# Project overlay — YOURS. The scaffold creates this fil
88
88
  # A published package name that does not follow the derivation:
89
89
  #
90
90
  # main: kit: target: ts: publish: registry: package: '@scope/name'
91
+ #
92
+ # How an add, or a re-scaffold, treats a file in .sdk the project changed:
93
+ # merged with the update (the default), or overwritten, for a project
94
+ # regenerated in full from the toolchain:
95
+ #
96
+ # main: kit: copies: 'overwrite'
91
97
  `
92
98
 
93
99
 
@@ -126,6 +132,17 @@ const KEPT = [
126
132
  ].map((rel: string[]) => rel.join('/'))
127
133
 
128
134
 
135
+ // Starting points that apidef or the model build rewrites, so a difference
136
+ // is the build's, not the project's: a re-scaffold overwrites them.
137
+ const SEEDS = [
138
+ 'model/api/api-info.aontu',
139
+ 'model/entity/entity-index.aontu',
140
+ 'model/flow/flow-index.aontu',
141
+ 'test/.model-config/model-config.json',
142
+ 'test/test.json',
143
+ ]
144
+
145
+
129
146
  function mergeGuide(existing: string | null, template: string): string {
130
147
  if (null == existing) {
131
148
  return template
@@ -303,5 +320,7 @@ const CreateRoot = cmp(function CreateRoot(props: any) {
303
320
 
304
321
  export {
305
322
  CreateRoot,
323
+ KEPT,
324
+ SEEDS,
306
325
  sanitizeDefName,
307
326
  }
@@ -0,0 +1,119 @@
1
+ /* Copyright (c) 2026 Richard Rodger, MIT License */
2
+
3
+ import Path from 'node:path'
4
+ import { createHash } from 'node:crypto'
5
+
6
+ import { copyMode } from './copymerge'
7
+ import type { CopyMode } from './copymerge'
8
+
9
+
10
+ // Relative to the project folder.
11
+ const SCAFFOLD_LOG = '.sdk/log/scaffold.jsonl'
12
+ const SCAFFOLD_BASE = '.sdk/log/scaffold-base'
13
+ const SCAFFOLD_CONFLICT = '.sdk/log/scaffold-conflict'
14
+ const PROJECT_MODEL = '.sdk/model/project.aontu'
15
+
16
+ const COPIES_RE = /^\s*main\s*:\s*kit\s*:\s*copies\s*:\s*['"]?(merge|overwrite)\b/m
17
+
18
+
19
+ // As @voxgig/sdkgen fingerprints its copies: CRLF and LF read the same.
20
+ function fingerprint(content: any): string {
21
+ const bytes = Buffer.isBuffer(content) ? content : Buffer.from(String(content))
22
+ const lf = Buffer.from(bytes.toString('latin1').replace(/\r\n/g, '\n'), 'latin1')
23
+ return createHash('sha256').update(lf).digest('hex').slice(0, 16)
24
+ }
25
+
26
+
27
+ function at(folder: string, rel: string): string {
28
+ return Path.join(folder, ...rel.split('/'))
29
+ }
30
+
31
+
32
+ // `main.kit.copies`, read from the project overlay without building the model.
33
+ function scaffoldMode(fs: any, folder: string): CopyMode {
34
+ const path = at(folder, PROJECT_MODEL)
35
+ const text = fs.existsSync(path) ? String(fs.readFileSync(path, 'utf8')) : ''
36
+ return copyMode(COPIES_RE.exec(text)?.[1])
37
+ }
38
+
39
+
40
+ // Replays the append-only log: each line holds what one scaffold changed.
41
+ function readScaffold(fs: any, folder: string): Record<string, string> {
42
+ const files: Record<string, string> = {}
43
+ const path = at(folder, SCAFFOLD_LOG)
44
+
45
+ if (!fs.existsSync(path)) {
46
+ return files
47
+ }
48
+
49
+ for (const line of String(fs.readFileSync(path, 'utf8')).split('\n')) {
50
+ let entry: any
51
+ try {
52
+ entry = '' === line.trim() ? undefined : JSON.parse(line)
53
+ }
54
+ catch (err: any) {
55
+ continue
56
+ }
57
+
58
+ for (const [rel, print] of Object.entries(entry?.files ?? {})) {
59
+ if ('string' === typeof print) {
60
+ files[rel] = print
61
+ }
62
+ else if (null === print) {
63
+ delete files[rel]
64
+ }
65
+ }
66
+ }
67
+
68
+ return files
69
+ }
70
+
71
+
72
+ function recordScaffold(
73
+ fs: any, folder: string, jres: any, version: string,
74
+ manage: (rel: string) => boolean, record: Record<string, string>,
75
+ ) {
76
+ const written: string[] = [
77
+ ...(jres?.files?.written ?? []), ...(jres?.files?.unchanged ?? []),
78
+ ]
79
+
80
+ const files: Record<string, string> = {}
81
+
82
+ for (const path of written) {
83
+ const rel = Path.relative(Path.resolve(folder), Path.resolve(path))
84
+ .split(Path.sep).join('/')
85
+
86
+ if ('' === rel || rel.startsWith('../') || !manage(rel) || !fs.existsSync(path)) {
87
+ continue
88
+ }
89
+
90
+ const print = fingerprint(fs.readFileSync(path))
91
+ if (print !== record[rel]) {
92
+ files[rel] = print
93
+ }
94
+ }
95
+
96
+ if (0 === Object.keys(files).length) {
97
+ return
98
+ }
99
+
100
+ const sorted = Object.fromEntries(Object.keys(files).sort()
101
+ .map((rel: string) => [rel, files[rel]]))
102
+
103
+ const log = at(folder, SCAFFOLD_LOG)
104
+ fs.mkdirSync(Path.dirname(log), { recursive: true })
105
+ fs.appendFileSync(log, JSON.stringify({
106
+ at: new Date().toISOString(), op: 'scaffold', version, files: sorted,
107
+ }) + '\n')
108
+ }
109
+
110
+
111
+ export {
112
+ SCAFFOLD_LOG,
113
+ SCAFFOLD_BASE,
114
+ SCAFFOLD_CONFLICT,
115
+ fingerprint,
116
+ scaffoldMode,
117
+ readScaffold,
118
+ recordScaffold,
119
+ }