@voxgig/sdkgen 3.4.7 → 3.5.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-sdkgen +78 -10
- package/dist/action/action.d.ts +5 -2
- package/dist/action/action.js +113 -6
- package/dist/action/action.js.map +1 -1
- package/dist/action/check.d.ts +15 -0
- package/dist/action/check.js +347 -0
- package/dist/action/check.js.map +1 -0
- package/dist/action/dispatch.d.ts +8 -0
- package/dist/action/dispatch.js +74 -0
- package/dist/action/dispatch.js.map +1 -0
- package/dist/action/docs.d.ts +4 -0
- package/dist/action/docs.js +236 -0
- package/dist/action/docs.js.map +1 -0
- package/dist/action/doctor.d.ts +5 -2
- package/dist/action/doctor.js +526 -74
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/feature.js +96 -17
- package/dist/action/feature.js.map +1 -1
- package/dist/action/kind.d.ts +30 -0
- package/dist/action/kind.js +258 -0
- package/dist/action/kind.js.map +1 -0
- package/dist/action/package.d.ts +25 -0
- package/dist/action/package.js +852 -0
- package/dist/action/package.js.map +1 -0
- package/dist/action/resolve.d.ts +18 -0
- package/dist/action/resolve.js +280 -0
- package/dist/action/resolve.js.map +1 -0
- package/dist/action/target.d.ts +7 -1
- package/dist/action/target.js +240 -97
- package/dist/action/target.js.map +1 -1
- package/dist/cmp/Docs.d.ts +4 -0
- package/dist/cmp/Docs.js +98 -0
- package/dist/cmp/Docs.js.map +1 -0
- package/dist/cmp/ExternalDocs.d.ts +2 -0
- package/dist/cmp/ExternalDocs.js +43 -0
- package/dist/cmp/ExternalDocs.js.map +1 -0
- package/dist/helpers/definition.d.ts +5 -0
- package/dist/helpers/definition.js +72 -0
- package/dist/helpers/definition.js.map +1 -0
- package/dist/helpers/featureSource.d.ts +8 -2
- package/dist/helpers/featureSource.js +61 -12
- package/dist/helpers/featureSource.js.map +1 -1
- package/dist/helpers/junk.d.ts +11 -0
- package/dist/helpers/junk.js +149 -0
- package/dist/helpers/junk.js.map +1 -0
- package/dist/helpers/manifest.d.ts +50 -0
- package/dist/helpers/manifest.js +328 -0
- package/dist/helpers/manifest.js.map +1 -0
- package/dist/helpers/modelcheck.d.ts +22 -0
- package/dist/helpers/modelcheck.js +203 -0
- package/dist/helpers/modelcheck.js.map +1 -0
- package/dist/helpers/semver.d.ts +4 -0
- package/dist/helpers/semver.js +174 -0
- package/dist/helpers/semver.js.map +1 -0
- package/dist/helpers/shipped.d.ts +4 -0
- package/dist/helpers/shipped.js +36 -0
- package/dist/helpers/shipped.js.map +1 -0
- package/dist/helpers/stdrep.d.ts +9 -1
- package/dist/helpers/stdrep.js +22 -0
- package/dist/helpers/stdrep.js.map +1 -1
- package/dist/sdkgen.d.ts +15 -3
- package/dist/sdkgen.js +233 -67
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/utility.js +17 -0
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aontu +69 -0
- package/package.json +3 -2
- package/project/.sdk/model/feature/audit.aontu +1 -0
- package/project/.sdk/model/feature/cache.aontu +1 -0
- package/project/.sdk/model/feature/clienttrack.aontu +1 -0
- package/project/.sdk/model/feature/debug.aontu +1 -0
- package/project/.sdk/model/feature/idempotency.aontu +1 -0
- package/project/.sdk/model/feature/log.aontu +1 -0
- package/project/.sdk/model/feature/metrics.aontu +1 -0
- package/project/.sdk/model/feature/netsim.aontu +1 -0
- package/project/.sdk/model/feature/paging.aontu +1 -0
- package/project/.sdk/model/feature/proxy.aontu +1 -0
- package/project/.sdk/model/feature/ratelimit.aontu +1 -0
- package/project/.sdk/model/feature/rbac.aontu +1 -0
- package/project/.sdk/model/feature/retry.aontu +1 -0
- package/project/.sdk/model/feature/streaming.aontu +1 -0
- package/project/.sdk/model/feature/telemetry.aontu +1 -0
- package/project/.sdk/model/feature/test.aontu +1 -0
- package/project/.sdk/model/feature/timeout.aontu +1 -0
- package/project/.sdk/model/target/c.aontu +1 -0
- package/project/.sdk/model/target/clojure.aontu +1 -0
- package/project/.sdk/model/target/cpp.aontu +1 -0
- package/project/.sdk/model/target/dart.aontu +1 -0
- package/project/.sdk/model/target/elixir.aontu +1 -0
- package/project/.sdk/model/target/go-cli.aontu +1 -0
- package/project/.sdk/model/target/go-mcp.aontu +1 -0
- package/project/.sdk/model/target/go.aontu +1 -0
- package/project/.sdk/model/target/haskell.aontu +1 -0
- package/project/.sdk/model/target/java.aontu +1 -0
- package/project/.sdk/model/target/js.aontu +1 -0
- package/project/.sdk/model/target/kotlin.aontu +1 -0
- package/project/.sdk/model/target/lean.aontu +1 -0
- package/project/.sdk/model/target/lua.aontu +1 -0
- package/project/.sdk/model/target/ocaml.aontu +1 -0
- package/project/.sdk/model/target/perl.aontu +1 -0
- package/project/.sdk/model/target/php.aontu +1 -0
- package/project/.sdk/model/target/py-data.aontu +1 -0
- package/project/.sdk/model/target/py.aontu +1 -0
- package/project/.sdk/model/target/rb.aontu +1 -0
- package/project/.sdk/model/target/rust.aontu +1 -0
- package/project/.sdk/model/target/scala.aontu +1 -0
- package/project/.sdk/model/target/seneca-provider.aontu +1 -0
- package/project/.sdk/model/target/zig.aontu +1 -0
- package/project/.sdk/src/cmp/c/Config_c.ts +44 -0
- package/project/.sdk/src/cmp/c/fragment/Main.fragment.c +3 -1
- package/project/.sdk/src/cmp/clojure/Config_clojure.ts +37 -42
- package/project/.sdk/src/cmp/clojure/utility_clojure.ts +42 -0
- package/project/.sdk/src/cmp/cpp/Config_cpp.ts +18 -0
- package/project/.sdk/src/cmp/csharp/Config_csharp.ts +125 -52
- package/project/.sdk/src/cmp/csharp/fragment/Main.fragment.cs +3 -1
- package/project/.sdk/src/cmp/csharp/utility_csharp.ts +94 -6
- package/project/.sdk/src/cmp/dart/Config_dart.ts +43 -5
- package/project/.sdk/src/cmp/dart/fragment/Config.data.fragment.dart +74 -0
- package/project/.sdk/src/cmp/dart/fragment/Config.fragment.dart +6 -9
- package/project/.sdk/src/cmp/dart/fragment/Main.fragment.dart +16 -5
- package/project/.sdk/src/cmp/dart/utility_dart.ts +18 -0
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +128 -13
- package/project/.sdk/src/cmp/elixir/fragment/Main.fragment.ex +3 -1
- package/project/.sdk/src/cmp/elixir/utility_elixir.ts +6 -0
- package/project/.sdk/src/cmp/haskell/Config_haskell.ts +35 -52
- package/project/.sdk/src/cmp/haskell/Package_haskell.ts +1 -0
- package/project/.sdk/src/cmp/haskell/Test_haskell.ts +1 -1
- package/project/.sdk/src/cmp/haskell/utility_haskell.ts +15 -5
- package/project/.sdk/src/cmp/java/Config_java.ts +21 -0
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +15 -0
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +42 -39
- package/project/.sdk/src/cmp/perl/Config_perl.ts +15 -0
- package/project/.sdk/src/cmp/perl/fragment/Main.fragment.pm +3 -1
- package/project/.sdk/src/cmp/rust/Config_rust.ts +44 -0
- package/project/.sdk/src/cmp/rust/fragment/Main.fragment.rs +4 -2
- package/project/.sdk/src/cmp/scala/Config_scala.ts +15 -0
- package/project/.sdk/src/cmp/swift/Config_swift.ts +17 -0
- package/project/.sdk/src/cmp/swift/fragment/Main.fragment.swift +3 -1
- package/project/.sdk/src/cmp/zig/Config_zig.ts +44 -0
- package/project/.sdk/src/cmp/zig/TestEntity_zig.ts +9 -2
- package/project/.sdk/src/cmp/zig/fragment/Main.fragment.zig +3 -1
- package/project/.sdk/tm/c/core/sdk.h +1 -0
- package/project/.sdk/tm/c/utility/make_options.c +5 -1
- package/project/.sdk/tm/clojure/src/sdk/core.clj +51 -3
- package/project/.sdk/tm/cpp/core/types.hpp +4 -1
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +5 -1
- package/project/.sdk/tm/csharp/test/PipelineTest.cs +15 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +67 -1
- package/project/.sdk/tm/dart/lib/utility/MakeOptionsUtility.dart +43 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +58 -2
- package/project/.sdk/tm/haskell/{test/TestJson.hs → src/SdkJson.hs} +8 -4
- package/project/.sdk/tm/haskell/src/SdkRuntime.hs +55 -2
- package/project/.sdk/tm/haskell/test/Runner.hs +1 -1
- package/project/.sdk/tm/haskell/test/StructCorpus.hs +1 -1
- package/project/.sdk/tm/java/core/SdkClient.java +3 -1
- package/project/.sdk/tm/java/utility/MakeOptions.java +5 -1
- package/project/.sdk/tm/kotlin/core/SdkClient.kt +3 -1
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +5 -1
- package/project/.sdk/tm/ocaml/Makefile +3 -2
- package/project/.sdk/tm/ocaml/sdk_json.ml +106 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +56 -0
- package/project/.sdk/tm/ocaml/test/struct_corpus.ml +4 -86
- package/project/.sdk/tm/perl/utility/make_options.pm +5 -1
- package/project/.sdk/tm/rust/utility/make_options.rs +79 -1
- package/project/.sdk/tm/scala/core/SdkClient.scala +3 -1
- package/project/.sdk/tm/scala/utility/Make.scala +5 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +5 -1
- package/project/.sdk/tm/zig/core/utility.zig +85 -1
- package/project/sdkgen-package.json +57 -0
- package/src/action/action.ts +136 -6
- package/src/action/check.ts +495 -0
- package/src/action/dispatch.ts +105 -0
- package/src/action/docs.ts +310 -0
- package/src/action/doctor.ts +692 -81
- package/src/action/feature.ts +109 -17
- package/src/action/kind.ts +375 -0
- package/src/action/package.ts +1173 -0
- package/src/action/resolve.ts +362 -0
- package/src/action/target.ts +290 -108
- package/src/cmp/Docs.ts +125 -0
- package/src/cmp/ExternalDocs.ts +54 -0
- package/src/helpers/definition.ts +82 -0
- package/src/helpers/featureSource.ts +83 -13
- package/src/helpers/junk.ts +168 -0
- package/src/helpers/manifest.ts +474 -0
- package/src/helpers/modelcheck.ts +262 -0
- package/src/helpers/semver.ts +211 -0
- package/src/helpers/shipped.ts +41 -0
- package/src/helpers/stdrep.ts +70 -0
- package/src/sdkgen.ts +294 -66
- package/src/types.ts +33 -3
- package/src/utility.ts +17 -0
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
// `package add` / `package list` — the whole-package verbs.
|
|
2
|
+
//
|
|
3
|
+
// See docs/design/sdkgen-packages.md §9.
|
|
4
|
+
//
|
|
5
|
+
// WHAT `package add` IS
|
|
6
|
+
//
|
|
7
|
+
// Not a new copy pipeline. It resolves a package ROOT, validates its manifest,
|
|
8
|
+
// and then runs the SAME per-kind add that `target add` / `feature add` run,
|
|
9
|
+
// once per provided item. Index handling, provenance stamping, feature
|
|
10
|
+
// fan-out, dry run and logging all come along unchanged, because they are not
|
|
11
|
+
// reimplemented. The value it adds over typing the individual adds is:
|
|
12
|
+
//
|
|
13
|
+
// - the manifest is REQUIRED and validated first, so a package that lies
|
|
14
|
+
// about what it provides fails before anything is written rather than
|
|
15
|
+
// halfway through;
|
|
16
|
+
// - the engine range is checked once, for the package, rather than never;
|
|
17
|
+
// - ordering: targets before features, because `feature add` fans a
|
|
18
|
+
// feature's source out across the targets already in the model, so a
|
|
19
|
+
// feature installed first would find none of the package's own targets.
|
|
20
|
+
//
|
|
21
|
+
// WHY VALIDATION COMES FIRST, ALL OF IT
|
|
22
|
+
//
|
|
23
|
+
// The items are installed in a loop. If the fourth one turns out not to be in
|
|
24
|
+
// the package, the first three are already written and the project is left
|
|
25
|
+
// half-installed with a partial index. Validating the whole claim up front is
|
|
26
|
+
// what makes the loop safe to run at all.
|
|
27
|
+
|
|
28
|
+
import Path from 'node:path'
|
|
29
|
+
|
|
30
|
+
import { KIT } from '../types'
|
|
31
|
+
|
|
32
|
+
import type {
|
|
33
|
+
ActionContext,
|
|
34
|
+
ActionResult,
|
|
35
|
+
} from '../types'
|
|
36
|
+
|
|
37
|
+
import { SdkGenError } from '../utility'
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
MANIFEST,
|
|
41
|
+
ITEM_NAME_RE,
|
|
42
|
+
probePackage,
|
|
43
|
+
readManifest,
|
|
44
|
+
validateManifest,
|
|
45
|
+
} from '../helpers/manifest'
|
|
46
|
+
|
|
47
|
+
import type { Manifest, Finding } from '../helpers/manifest'
|
|
48
|
+
|
|
49
|
+
import { satisfies } from '../helpers/semver'
|
|
50
|
+
|
|
51
|
+
import { KINDS, kindDef } from './kind'
|
|
52
|
+
|
|
53
|
+
import { cmd_package_check } from './check'
|
|
54
|
+
|
|
55
|
+
import { doctor } from './doctor'
|
|
56
|
+
|
|
57
|
+
import { resolveSource, registerInstalled, nameConflict } from './resolve'
|
|
58
|
+
import type { Source } from './resolve'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
const CMD_MAP: any = Object.assign(Object.create(null), {
|
|
62
|
+
add: cmd_package_add,
|
|
63
|
+
check: cmd_package_check,
|
|
64
|
+
list: cmd_package_list,
|
|
65
|
+
update: cmd_package_update,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// The kinds `package add` installs, IN ORDER.
|
|
70
|
+
//
|
|
71
|
+
// Targets first. `feature add` copies a feature's per-target source into every
|
|
72
|
+
// target already in the model, so a feature installed before the package's own
|
|
73
|
+
// targets would silently ship no source for them — the `feature-source-missing`
|
|
74
|
+
// warning, once per target, and a feature that does nothing.
|
|
75
|
+
//
|
|
76
|
+
// Docs LAST, for the mirror-image reason: a docs item is ABOUT the targets and
|
|
77
|
+
// features a project has (a page per SDK, a page per feature), and while it
|
|
78
|
+
// reads them at generate time rather than at add time, installing it last
|
|
79
|
+
// keeps the add log in the order a reader would expect and leaves the model
|
|
80
|
+
// complete before anything reads it.
|
|
81
|
+
const ADD_ORDER = ['target', 'feature', 'docs']
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async function action_package(
|
|
85
|
+
args: string[], actx: ActionContext,
|
|
86
|
+
): Promise<ActionResult> {
|
|
87
|
+
const cmdname = args[1]
|
|
88
|
+
const cmd = CMD_MAP[cmdname]
|
|
89
|
+
|
|
90
|
+
if (null == cmd) {
|
|
91
|
+
throw new SdkGenError(
|
|
92
|
+
'Unknown package cmd: ' + cmdname + ' (expected: ' +
|
|
93
|
+
Object.keys(CMD_MAP).sort().join(', ') + ')')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return await cmd(args, actx)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// Where a package ROOT is, given a ref.
|
|
101
|
+
//
|
|
102
|
+
// The same probe chain `resolveSource` uses for an item, one level up: an
|
|
103
|
+
// installed npm package first, then a path relative to the project, then an
|
|
104
|
+
// absolute path. A package root is the folder HOLDING `.sdk`, which is what
|
|
105
|
+
// makes `<root>/<item>` the item ref — so the two resolvers agree by
|
|
106
|
+
// construction about what a package is.
|
|
107
|
+
type PackageSource = {
|
|
108
|
+
ref: string
|
|
109
|
+
root: string
|
|
110
|
+
sdk: string
|
|
111
|
+
manifest: Manifest
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
function resolvePackage(ref: string, actx: ActionContext): PackageSource {
|
|
116
|
+
const { found, search } = probePackage(actx.fs(), actx.folder ?? '.', ref)
|
|
117
|
+
|
|
118
|
+
if (null == found) {
|
|
119
|
+
throw new SdkGenError(
|
|
120
|
+
'Package not found: ' + ref + '\n looked for a `.sdk` folder in:\n ' +
|
|
121
|
+
search.join('\n '))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { root, sdk, read } = found
|
|
125
|
+
|
|
126
|
+
// A `.sdk` folder with no manifest is a legal source for a DIRECT ref and
|
|
127
|
+
// is what every pre-manifest fixture is — but `package add` is the verb
|
|
128
|
+
// that acts on a manifest, so here its absence is the error, and it says
|
|
129
|
+
// which of the two commands the user wants.
|
|
130
|
+
if (null == read.manifest) {
|
|
131
|
+
throw new SdkGenError(
|
|
132
|
+
'No package manifest: ' + read.file +
|
|
133
|
+
(null == read.err ? '' : '\n ' + read.err) +
|
|
134
|
+
'\n `package add` installs what a manifest declares. For a folder' +
|
|
135
|
+
' without one, add its items directly:' +
|
|
136
|
+
'\n voxgig-sdkgen target add ' + ref + '/<name>')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ref, root, sdk, manifest: read.manifest }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
// Everything wrong with the package, or nothing. Reported as ONE message
|
|
144
|
+
// rather than one-per-throw, because an author fixing a manifest wants the
|
|
145
|
+
// whole list, not the first line of it.
|
|
146
|
+
function refuse(src: PackageSource, found: Finding[], log: any) {
|
|
147
|
+
// The non-errors are still worth saying — an unclaimed extra is nearly
|
|
148
|
+
// always a forgotten manifest edit — but they do not stop the install.
|
|
149
|
+
for (const f of found) {
|
|
150
|
+
if ('error' !== f.level) {
|
|
151
|
+
log[f.level](f)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const errors = found.filter((f: Finding) => 'error' === f.level)
|
|
156
|
+
|
|
157
|
+
if (0 === errors.length) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new SdkGenError(
|
|
162
|
+
src.ref + ': package manifest does not match the package (' +
|
|
163
|
+
Path.join(src.root, MANIFEST) + ')\n ' +
|
|
164
|
+
errors.map((f: Finding) => f.note).join('\n '))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
// Is this generator new enough for the package?
|
|
169
|
+
//
|
|
170
|
+
// `undefined` from `satisfies` means the range is outside the subset it
|
|
171
|
+
// understands — see helpers/semver. That is reported and ALLOWED: refusing on
|
|
172
|
+
// a range nobody could parse would block a package that works, which is worse
|
|
173
|
+
// than the incompatibility being guarded against.
|
|
174
|
+
function checkEngine(src: PackageSource, actx: ActionContext) {
|
|
175
|
+
const range = src.manifest.engines?.sdkgen
|
|
176
|
+
|
|
177
|
+
if (null == range || '' === range) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const ok = satisfies(SDKGEN_VERSION, range)
|
|
182
|
+
|
|
183
|
+
if (false === ok) {
|
|
184
|
+
throw new SdkGenError(
|
|
185
|
+
src.ref + ': needs @voxgig/sdkgen ' + range +
|
|
186
|
+
', this is ' + SDKGEN_VERSION +
|
|
187
|
+
'\n upgrade @voxgig/sdkgen, or install an earlier version of ' + src.ref)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (null == ok) {
|
|
191
|
+
actx.log.warn({
|
|
192
|
+
point: 'package-engine-unparsed', package: src.manifest.name,
|
|
193
|
+
range, version: SDKGEN_VERSION,
|
|
194
|
+
note: src.ref + ': cannot compare `engines.sdkgen` range ' +
|
|
195
|
+
JSON.stringify(range) + ' against ' + SDKGEN_VERSION +
|
|
196
|
+
'; proceeding — see helpers/semver for the supported subset'
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
// This generator's own version, read from the package.json beside `dist`.
|
|
203
|
+
// Not from the consumer's model: `engines.sdkgen` is about the GENERATOR
|
|
204
|
+
// doing the installing, and the model describes the SDK being generated.
|
|
205
|
+
const SDKGEN_VERSION: string = (() => {
|
|
206
|
+
try {
|
|
207
|
+
return require('../../package.json').version
|
|
208
|
+
}
|
|
209
|
+
catch (err: any) {
|
|
210
|
+
return '0.0.0'
|
|
211
|
+
}
|
|
212
|
+
})()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
// The items to install, as `<kind>` -> names, after `--only` is applied.
|
|
216
|
+
//
|
|
217
|
+
// `--only target:iot-go,feature:circuitbreaker` — a subset of what the
|
|
218
|
+
// manifest provides, named the same way the manifest keys them. A name that
|
|
219
|
+
// the package does not provide is an ERROR listing what it does, rather than
|
|
220
|
+
// a silent no-op: a typo'd `--only` that installed nothing and reported
|
|
221
|
+
// success is the failure this whole verb exists to remove.
|
|
222
|
+
function selectItems(
|
|
223
|
+
src: PackageSource, only: string | undefined, log: any,
|
|
224
|
+
): Record<string, string[]> {
|
|
225
|
+
const provides = src.manifest.provides ?? {}
|
|
226
|
+
|
|
227
|
+
// ABSENT means "everything"; EXPLICITLY EMPTY does not.
|
|
228
|
+
//
|
|
229
|
+
// `--only=` and `{ only: '' }` are what a script gets when it builds the
|
|
230
|
+
// flag from an empty variable, and treating that as absent installs the
|
|
231
|
+
// whole package — the opposite of what the operator asked for, at the one
|
|
232
|
+
// moment nobody is watching.
|
|
233
|
+
if (null == only) {
|
|
234
|
+
return provides
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const wanted: Record<string, string[]> = Object.create(null)
|
|
238
|
+
const missing: string[] = []
|
|
239
|
+
const specs = only.split(',').map((s) => s.trim()).filter(Boolean)
|
|
240
|
+
|
|
241
|
+
if (0 === specs.length) {
|
|
242
|
+
throw new SdkGenError(
|
|
243
|
+
'--only was given but selects nothing: ' + JSON.stringify(only) +
|
|
244
|
+
'\n omit the flag to install everything the package provides')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const spec of specs) {
|
|
248
|
+
const colon = spec.indexOf(':')
|
|
249
|
+
|
|
250
|
+
if (colon < 0) {
|
|
251
|
+
throw new SdkGenError(
|
|
252
|
+
'--only expects <kind>:<name> entries, got: ' + spec +
|
|
253
|
+
'\n for example: --only target:iot-go,feature:circuitbreaker')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const kind = spec.slice(0, colon)
|
|
257
|
+
const name = spec.slice(colon + 1)
|
|
258
|
+
|
|
259
|
+
if (!(provides[kind] ?? []).includes(name)) {
|
|
260
|
+
missing.push(spec)
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
(wanted[kind] = wanted[kind] ?? []).push(name)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (0 < missing.length) {
|
|
268
|
+
throw new SdkGenError(
|
|
269
|
+
src.ref + ': does not provide ' + missing.join(', ') +
|
|
270
|
+
'\n it provides: ' + describeProvides(provides))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return wanted
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
function describeProvides(provides: Record<string, string[]>): string {
|
|
278
|
+
const parts: string[] = []
|
|
279
|
+
|
|
280
|
+
for (const kind of Object.keys(provides).sort()) {
|
|
281
|
+
for (const name of provides[kind]) {
|
|
282
|
+
parts.push(kind + ' `' + name + '`')
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return 0 === parts.length ? '(nothing)' : parts.join(', ')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
// `--alias iot-go=acme-go,other=thing` — the install-time renames, by ORIGIN
|
|
291
|
+
// name. Only kinds that permit aliasing may appear; a feature alias is
|
|
292
|
+
// refused here with the same explanation `feature add` gives, so the two
|
|
293
|
+
// entry points cannot disagree about what is allowed.
|
|
294
|
+
function parseAliases(
|
|
295
|
+
alias: string | undefined, wanted: Record<string, string[]>,
|
|
296
|
+
): Record<string, string> {
|
|
297
|
+
// NULL-PROTOTYPE. A manifest may legally provide an item called
|
|
298
|
+
// `constructor` or `toString` — the name grammar admits them — and on a
|
|
299
|
+
// plain object `aliases['constructor']` is Object.prototype.constructor,
|
|
300
|
+
// which is truthy, so an unaliased item got `~function Object() { … }`
|
|
301
|
+
// appended and installed under that as a name.
|
|
302
|
+
const out: Record<string, string> = Object.create(null)
|
|
303
|
+
|
|
304
|
+
if (null == alias || '' === alias) {
|
|
305
|
+
return out
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const spec of alias.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
309
|
+
const eq = spec.indexOf('=')
|
|
310
|
+
|
|
311
|
+
if (eq < 0) {
|
|
312
|
+
throw new SdkGenError(
|
|
313
|
+
'--alias expects <name>=<alias> entries, got: ' + spec +
|
|
314
|
+
'\n for example: --alias iot-go=acme-go')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const from = spec.slice(0, eq)
|
|
318
|
+
const to = spec.slice(eq + 1)
|
|
319
|
+
|
|
320
|
+
// Checked HERE as well as in the resolver, because the two catch
|
|
321
|
+
// different things. The resolver sees whatever survives ref parsing, so
|
|
322
|
+
// it catches `iotgo=..`; but `iotgo=../../elsewhere` is concatenated into
|
|
323
|
+
// `<root>/iotgo~../../elsewhere`, whose last segment is `elsewhere` and
|
|
324
|
+
// which therefore stops looking like an alias at all — it resolves as a
|
|
325
|
+
// ref to a different item and fails confusingly instead. Same grammar in
|
|
326
|
+
// both places, so they cannot disagree about what a name is.
|
|
327
|
+
if (!ITEM_NAME_RE.test(to)) {
|
|
328
|
+
throw new SdkGenError(
|
|
329
|
+
'Invalid alias in --alias ' + JSON.stringify(spec) + ': ' +
|
|
330
|
+
JSON.stringify(to) + ' is not a name (matching ' +
|
|
331
|
+
ITEM_NAME_RE.source + ')' +
|
|
332
|
+
'\n an alias becomes the directory the item is installed into')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const kind = Object.keys(wanted)
|
|
336
|
+
.find((k: string) => (wanted[k] ?? []).includes(from))
|
|
337
|
+
|
|
338
|
+
if (null == kind) {
|
|
339
|
+
throw new SdkGenError(
|
|
340
|
+
'--alias names ' + JSON.stringify(from) +
|
|
341
|
+
', which is not among the items being installed: ' +
|
|
342
|
+
describeProvides(wanted))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!kindDef(kind).alias) {
|
|
346
|
+
throw new SdkGenError(
|
|
347
|
+
capitalise(kind) + ' aliasing is not supported: ' + spec +
|
|
348
|
+
'\n A ' + kind + ' name is part of the generated config ' +
|
|
349
|
+
'(options.' + kind + '.<name>) and of the hook wiring in every ' +
|
|
350
|
+
'target, so it cannot be renamed at install time.')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
out[from] = to
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return out
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async function cmd_package_add(
|
|
361
|
+
args: string[], actx: ActionContext,
|
|
362
|
+
): Promise<ActionResult> {
|
|
363
|
+
const refs = args.slice(2).flatMap(
|
|
364
|
+
(a: any) => 'string' === typeof a ? a.split(',') : a)
|
|
365
|
+
.filter((r: any) => null != r && '' !== r)
|
|
366
|
+
|
|
367
|
+
if (0 === refs.length) {
|
|
368
|
+
throw new SdkGenError('package add: no package given')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return package_add(refs, actx)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async function package_add(
|
|
376
|
+
refs: string[], actx: ActionContext,
|
|
377
|
+
): Promise<ActionResult> {
|
|
378
|
+
const log = actx.log
|
|
379
|
+
const flags = actx.flags ?? {}
|
|
380
|
+
|
|
381
|
+
// `--only` and `--alias` name items, so they only make sense for ONE
|
|
382
|
+
// package. Silently applying them to each of several would install the same
|
|
383
|
+
// alias twice.
|
|
384
|
+
if (1 < refs.length && (null != flags.only || null != flags.alias)) {
|
|
385
|
+
throw new SdkGenError(
|
|
386
|
+
'--only and --alias apply to a single package; ' + refs.length +
|
|
387
|
+
' were given: ' + refs.join(', '))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// PREFLIGHT EVERY PACKAGE BEFORE INSTALLING ANY OF THEM.
|
|
391
|
+
//
|
|
392
|
+
// The same argument that makes this verb validate a manifest in full before
|
|
393
|
+
// writing anything applies across refs: `package add good,bad` that
|
|
394
|
+
// installed `good` and then failed would leave exactly the half-completed
|
|
395
|
+
// command the guarantee is about. Resolution, the engine gate, manifest
|
|
396
|
+
// validation, `--only` selection and the name-collision check all happen
|
|
397
|
+
// here, for all of them, before the first file is written.
|
|
398
|
+
const plan = refs.map((ref: string) => plan_one(ref, flags, actx))
|
|
399
|
+
|
|
400
|
+
checkCollisions(plan, actx)
|
|
401
|
+
|
|
402
|
+
const results: any[] = []
|
|
403
|
+
|
|
404
|
+
for (const { src, wanted, items } of plan) {
|
|
405
|
+
log.info({
|
|
406
|
+
point: 'package-add-start', package: src.manifest.name, ref: src.ref,
|
|
407
|
+
version: src.manifest.version, root: src.root,
|
|
408
|
+
note: src.manifest.name +
|
|
409
|
+
(null == src.manifest.version ? '' : '@' + src.manifest.version) +
|
|
410
|
+
' <- ' + src.root
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
for (const kind of orderedKinds(wanted)) {
|
|
414
|
+
const add = adderFor(kind)
|
|
415
|
+
const itemrefs = items[kind] ?? []
|
|
416
|
+
|
|
417
|
+
if (0 === itemrefs.length) {
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (null == add) {
|
|
422
|
+
// Validation already rejected an unknown kind, so this is a kind the
|
|
423
|
+
// registry knows and nothing can install yet — a `docs` entry before
|
|
424
|
+
// its action exists. Say so rather than skipping in silence.
|
|
425
|
+
log.warn({
|
|
426
|
+
point: 'package-kind-unsupported', package: src.manifest.name, kind,
|
|
427
|
+
names: wanted[kind],
|
|
428
|
+
note: src.manifest.name + ': nothing can install `' + kind +
|
|
429
|
+
'` items yet; skipped ' + (wanted[kind] ?? []).join(', ')
|
|
430
|
+
})
|
|
431
|
+
continue
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
results.push(await add(itemrefs, actx))
|
|
435
|
+
|
|
436
|
+
registerInstalled(kind, itemrefs, actx)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
log.info({
|
|
440
|
+
point: 'package-add-end', package: src.manifest.name, ref: src.ref,
|
|
441
|
+
note: src.manifest.name + ': added ' + describeProvides(wanted)
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// The LAST jostraca result, for the CLI's change summary. Each per-kind add
|
|
446
|
+
// already reported its own changes as it ran.
|
|
447
|
+
return { jres: results[results.length - 1]?.jres }
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
type Planned = {
|
|
452
|
+
src: PackageSource
|
|
453
|
+
wanted: Record<string, string[]>
|
|
454
|
+
// Item refs, by kind, in the grammar the per-kind add already takes:
|
|
455
|
+
// `<package-root>/<name>` with `~alias` appended when renamed. Building a
|
|
456
|
+
// ref rather than calling an internal entry point is what keeps
|
|
457
|
+
// `package add` and a hand-typed `target add` on exactly the same path.
|
|
458
|
+
items: Record<string, string[]>
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
function plan_one(
|
|
463
|
+
ref: string, flags: Record<string, any>, actx: ActionContext,
|
|
464
|
+
): Planned {
|
|
465
|
+
const src = resolvePackage(ref, actx)
|
|
466
|
+
|
|
467
|
+
checkEngine(src, actx)
|
|
468
|
+
refuse(src, validateManifest(actx.fs(), src.sdk, src.manifest, KINDS),
|
|
469
|
+
actx.log)
|
|
470
|
+
|
|
471
|
+
const wanted = selectItems(src, flags.only, actx.log)
|
|
472
|
+
const aliases = parseAliases(flags.alias, wanted)
|
|
473
|
+
|
|
474
|
+
const items: Record<string, string[]> = Object.create(null)
|
|
475
|
+
|
|
476
|
+
for (const kind of Object.keys(wanted)) {
|
|
477
|
+
items[kind] = (wanted[kind] ?? []).map((name: string) =>
|
|
478
|
+
Path.join(src.root, name) +
|
|
479
|
+
(Object.prototype.hasOwnProperty.call(aliases, name) ?
|
|
480
|
+
'~' + aliases[name] : ''))
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { src, wanted, items }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
// Would any of this REPLACE something the project got from elsewhere?
|
|
488
|
+
//
|
|
489
|
+
// `add` is overwrite, deliberately — that is how a resync works. But
|
|
490
|
+
// overwriting one package's `go` with a different package's `go` is not a
|
|
491
|
+
// resync: it silently replaces a working target's model, components and
|
|
492
|
+
// templates. The project asked for a package, not for that.
|
|
493
|
+
//
|
|
494
|
+
// Checked across the WHOLE plan, so two packages in one command claiming the
|
|
495
|
+
// same name are caught too — the second would otherwise conflict with nothing,
|
|
496
|
+
// because the first is not installed yet either.
|
|
497
|
+
function checkCollisions(plan: Planned[], actx: ActionContext) {
|
|
498
|
+
const claimed = new Map<string, string>()
|
|
499
|
+
const clashes: string[] = []
|
|
500
|
+
|
|
501
|
+
for (const { src, items } of plan) {
|
|
502
|
+
for (const kind of Object.keys(items)) {
|
|
503
|
+
for (const ref of items[kind]) {
|
|
504
|
+
let source: Source
|
|
505
|
+
try {
|
|
506
|
+
source = resolveSource(ref, kind, actx)
|
|
507
|
+
}
|
|
508
|
+
catch (err: any) {
|
|
509
|
+
// Unresolvable here means the add will fail too, with a better
|
|
510
|
+
// message than this check could give. Let it.
|
|
511
|
+
continue
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const key = kind + ':' + source.name
|
|
515
|
+
|
|
516
|
+
const earlier = claimed.get(key)
|
|
517
|
+
if (null != earlier && earlier !== src.manifest.name) {
|
|
518
|
+
clashes.push(kind + ' `' + source.name + '`: both ' + earlier +
|
|
519
|
+
' and ' + src.manifest.name + ' provide it')
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
claimed.set(key, src.manifest.name)
|
|
523
|
+
|
|
524
|
+
const conflict = nameConflict(kind, source, actx)
|
|
525
|
+
|
|
526
|
+
if (null != conflict) {
|
|
527
|
+
clashes.push(
|
|
528
|
+
kind + ' `' + source.name + '`: already installed from ' +
|
|
529
|
+
(conflict.package || conflict.base) +
|
|
530
|
+
', and ' + src.manifest.name + ' provides it too')
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (0 === clashes.length) {
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
throw new SdkGenError(
|
|
541
|
+
'Name collision, nothing installed:\n ' + clashes.join('\n ') +
|
|
542
|
+
'\n\n Either install the one you want by its own ref:' +
|
|
543
|
+
'\n voxgig-sdkgen target add <package>/<name>' +
|
|
544
|
+
'\n or install this package\'s under a different name:' +
|
|
545
|
+
'\n voxgig-sdkgen package add <package> --alias <name>=<alias>')
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
// `ADD_ORDER` first, then anything else the registry knows, so a kind added
|
|
550
|
+
// later installs without editing this list.
|
|
551
|
+
function orderedKinds(wanted: Record<string, string[]>): string[] {
|
|
552
|
+
const rest = Object.keys(wanted)
|
|
553
|
+
.filter((k: string) => !ADD_ORDER.includes(k)).sort()
|
|
554
|
+
|
|
555
|
+
return [...ADD_ORDER.filter((k: string) => null != wanted[k]), ...rest]
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
// Registered by `dispatch`, to keep this module out of a require cycle with
|
|
560
|
+
// `target.ts` (which imports `feature.ts`, which imports `kind.ts`).
|
|
561
|
+
const ADDERS: Record<string, (refs: string[], actx: ActionContext) => Promise<any>> =
|
|
562
|
+
Object.create(null)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
function registerAdder(
|
|
566
|
+
kind: string, add: (refs: string[], actx: ActionContext) => Promise<any>,
|
|
567
|
+
) {
|
|
568
|
+
ADDERS[kind] = add
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
// The adder for a kind, LOADING THE REGISTRATIONS IF NOBODY HAS YET.
|
|
573
|
+
//
|
|
574
|
+
// `dispatch` is what calls `registerAdder`, and until this, whether it had
|
|
575
|
+
// been loaded depended on the importer: `sdkgen.ts` pulls it in, so the CLI
|
|
576
|
+
// worked, but anything importing this module directly got an empty table —
|
|
577
|
+
// and `package add` / `package update` then skipped every item with a
|
|
578
|
+
// `package-kind-unsupported` warning and reported success. It silently did
|
|
579
|
+
// nothing.
|
|
580
|
+
//
|
|
581
|
+
// Found the hard way: a throwaway script written to reproduce a review
|
|
582
|
+
// finding imported this module alone, and the "update" it performed was a
|
|
583
|
+
// no-op that looked like a passing result. A capability that depends on some
|
|
584
|
+
// other module having been imported first is not a capability.
|
|
585
|
+
//
|
|
586
|
+
// Required lazily rather than at the top, because `dispatch` imports THIS
|
|
587
|
+
// module; by call time the cycle has resolved.
|
|
588
|
+
function adderFor(
|
|
589
|
+
kind: string,
|
|
590
|
+
): ((refs: string[], actx: ActionContext) => Promise<any>) | undefined {
|
|
591
|
+
if (0 === Object.keys(ADDERS).length) {
|
|
592
|
+
require('./dispatch')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return ADDERS[kind]
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
// `package update <pkg>` — refresh everything a package supplied.
|
|
600
|
+
//
|
|
601
|
+
// See docs/design/sdkgen-packages.md §13.
|
|
602
|
+
//
|
|
603
|
+
// THE ORDER IS THE SAFETY PROPERTY, which is why this command owns the fetch
|
|
604
|
+
// instead of telling the operator to run `npm update` first:
|
|
605
|
+
//
|
|
606
|
+
// 1. CHECK the project's copies against the source AS CURRENTLY INSTALLED
|
|
607
|
+
// 2. FETCH the new version
|
|
608
|
+
// 3. RE-ADD each item
|
|
609
|
+
//
|
|
610
|
+
// Measured at step 1, a copy that differs from its source means the project
|
|
611
|
+
// changed it. Run the other way round — fetch first, then check — every item
|
|
612
|
+
// legitimately differs from the new source, the gate fires on all of them,
|
|
613
|
+
// and the operator learns to pass `--force` every time. That ordering bug
|
|
614
|
+
// would make the gate worse than useless, because the same signal (copy
|
|
615
|
+
// differs from source) carries both meanings and only sequence separates
|
|
616
|
+
// them.
|
|
617
|
+
//
|
|
618
|
+
// WHAT THE GATE CANNOT DO, and says so
|
|
619
|
+
//
|
|
620
|
+
// It cannot prove which meaning applies. If the operator already ran
|
|
621
|
+
// `npm update` in another shell, step 1 is measuring against the NEW source
|
|
622
|
+
// and a difference means "stale", not "forked" — and nothing recorded in the
|
|
623
|
+
// project distinguishes the two. So the refusal states both readings and
|
|
624
|
+
// gives a runnable out for each, rather than asserting a fork it cannot
|
|
625
|
+
// diagnose. (A per-file digest recorded at add time would make it exact;
|
|
626
|
+
// deferred, design §17.9.)
|
|
627
|
+
async function cmd_package_update(
|
|
628
|
+
args: string[], actx: ActionContext,
|
|
629
|
+
): Promise<ActionResult> {
|
|
630
|
+
const names = args.slice(2).flatMap(
|
|
631
|
+
(a: any) => 'string' === typeof a ? a.split(',') : a)
|
|
632
|
+
.filter((r: any) => null != r && '' !== r)
|
|
633
|
+
|
|
634
|
+
if (0 === names.length) {
|
|
635
|
+
throw new SdkGenError(
|
|
636
|
+
'package update: no package given' +
|
|
637
|
+
'\n `voxgig-sdkgen package list` shows what this project has installed')
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return package_update(names, actx)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
// One item this project got from the package being updated.
|
|
645
|
+
type Installed = {
|
|
646
|
+
kind: string
|
|
647
|
+
name: string
|
|
648
|
+
origname: string
|
|
649
|
+
base: string
|
|
650
|
+
aliased: boolean
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
// Everything the model says came from `pkgname`.
|
|
655
|
+
//
|
|
656
|
+
// By RECORDED PROVENANCE, not by asking the package what it provides: what
|
|
657
|
+
// this refreshes is what the project actually installed, which may be a
|
|
658
|
+
// subset (`--only`) or carry aliases the package never mentions. Asking the
|
|
659
|
+
// package would refresh things the project does not have and miss the ones it
|
|
660
|
+
// renamed.
|
|
661
|
+
function installedFrom(pkgname: string, actx: ActionContext): Installed[] {
|
|
662
|
+
const kit: any = (actx.model as any)?.main?.[KIT] ?? {}
|
|
663
|
+
const found: Installed[] = []
|
|
664
|
+
|
|
665
|
+
for (const kind of Object.keys(KINDS).sort()) {
|
|
666
|
+
const items = kit[kind] ?? {}
|
|
667
|
+
|
|
668
|
+
for (const name of Object.keys(items).sort()) {
|
|
669
|
+
const item = items[name]
|
|
670
|
+
|
|
671
|
+
if (null == item || 'object' !== typeof item ||
|
|
672
|
+
item.package !== pkgname) {
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const origname = item.origname || name
|
|
677
|
+
|
|
678
|
+
found.push({
|
|
679
|
+
kind, name, origname,
|
|
680
|
+
base: item.base || '',
|
|
681
|
+
aliased: kindDef(kind).alias && origname !== name,
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return found
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
async function package_update(
|
|
691
|
+
names: string[], actx: ActionContext,
|
|
692
|
+
): Promise<ActionResult> {
|
|
693
|
+
const log = actx.log
|
|
694
|
+
const flags = actx.flags ?? {}
|
|
695
|
+
|
|
696
|
+
const results: any[] = []
|
|
697
|
+
|
|
698
|
+
// EVERY package's items resolved and CHECKED before any of them is
|
|
699
|
+
// fetched. `package update A,B` that finished A and then refused B would
|
|
700
|
+
// exit as failed having already changed A's dependencies and `.sdk` — the
|
|
701
|
+
// partial command the gate exists to prevent, one level up from where it
|
|
702
|
+
// was already prevented.
|
|
703
|
+
const plan = names.map((pkgname: string) => {
|
|
704
|
+
const installed = installedFrom(pkgname, actx)
|
|
705
|
+
|
|
706
|
+
if (0 === installed.length) {
|
|
707
|
+
throw new SdkGenError(
|
|
708
|
+
'Nothing installed from ' + pkgname +
|
|
709
|
+
'\n `voxgig-sdkgen package list` shows which packages this project ' +
|
|
710
|
+
'has, and what each supplied')
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return { pkgname, installed }
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
// STEP 1 — before anything moves, for all of them.
|
|
717
|
+
for (const { pkgname, installed } of plan) {
|
|
718
|
+
await preCheck(pkgname, installed, actx)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
for (const { pkgname, installed } of plan) {
|
|
722
|
+
log.info({
|
|
723
|
+
point: 'package-update-start', package: pkgname,
|
|
724
|
+
items: installed.length,
|
|
725
|
+
note: pkgname + ': updating ' + installed.length + ' item(s)'
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// STEP 2 — now the source may change.
|
|
729
|
+
await fetchPackage(pkgname, installed, actx)
|
|
730
|
+
|
|
731
|
+
// STEP 2b — the fetched version is a DIFFERENT package from the one
|
|
732
|
+
// step 1 measured, and nothing has validated it. `package add` refuses a
|
|
733
|
+
// package whose manifest lies or whose `engines.sdkgen` is beyond this
|
|
734
|
+
// generator; an update that skipped those checks would overwrite `.sdk`
|
|
735
|
+
// with components written for a generator this is not.
|
|
736
|
+
validateFetched(pkgname, installed, actx)
|
|
737
|
+
|
|
738
|
+
// STEP 3.
|
|
739
|
+
results.push(...await reAdd(pkgname, installed, actx))
|
|
740
|
+
|
|
741
|
+
log.info({
|
|
742
|
+
point: 'package-update-end', package: pkgname,
|
|
743
|
+
note: pkgname + ': updated'
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return { jres: results[results.length - 1]?.jres }
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
// EVERYTHING THE RE-ADD WILL WRITE, not just the package's own items.
|
|
752
|
+
//
|
|
753
|
+
// The gate is only worth having if it covers what step 3 actually touches,
|
|
754
|
+
// and step 3 touches more than it is asked to: `target_add` re-runs
|
|
755
|
+
// `feature_add` for EVERY active feature in the model, whoever supplied it.
|
|
756
|
+
// So updating a target package rewrites the model file of a feature that came
|
|
757
|
+
// from somewhere else — and a scope of "this package's items" never looked at
|
|
758
|
+
// it, so a local edit there was overwritten without `--force`, by the command
|
|
759
|
+
// whose whole promise is that it asks first.
|
|
760
|
+
//
|
|
761
|
+
// The features are added to the scope, not the fan-out narrowed: what the
|
|
762
|
+
// re-add does is `target add`'s long-standing behaviour, and changing it here
|
|
763
|
+
// would make `package update` write something different from what a
|
|
764
|
+
// hand-typed add writes — the equivalence every other part of this verb is
|
|
765
|
+
// built on.
|
|
766
|
+
function blastRadius(
|
|
767
|
+
installed: Installed[], actx: ActionContext,
|
|
768
|
+
): Set<string> {
|
|
769
|
+
const wanted = new Set(installed.map((i: Installed) => i.kind + ':' + i.name))
|
|
770
|
+
|
|
771
|
+
if (!installed.some((i: Installed) => 'target' === i.kind)) {
|
|
772
|
+
return wanted
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const features: any = (actx.model as any)?.main?.[KIT]?.feature ?? {}
|
|
776
|
+
|
|
777
|
+
for (const name of Object.keys(features)) {
|
|
778
|
+
if (false !== features[name]?.active) {
|
|
779
|
+
wanted.add('feature:' + name)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return wanted
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
// STEP 1: is the project's copy of this package's items unmodified?
|
|
788
|
+
//
|
|
789
|
+
// Runs the SAME comparison `doctor` runs, scoped to these items — a gate that
|
|
790
|
+
// decides whether to overwrite a project's files must not have its own idea
|
|
791
|
+
// of what counts as a difference.
|
|
792
|
+
async function preCheck(
|
|
793
|
+
pkgname: string, installed: Installed[], actx: ActionContext,
|
|
794
|
+
) {
|
|
795
|
+
const flags = actx.flags ?? {}
|
|
796
|
+
|
|
797
|
+
const wanted = blastRadius(installed, actx)
|
|
798
|
+
|
|
799
|
+
const res: any = await doctor(
|
|
800
|
+
actx, (kind: string, name: string) => wanted.has(kind + ':' + name))
|
|
801
|
+
|
|
802
|
+
const report = res.report
|
|
803
|
+
|
|
804
|
+
// `forked` and `edited` only. `missing` means the project is short of what
|
|
805
|
+
// add would write, which an update FIXES; `resyncPending` is provenance
|
|
806
|
+
// catching up, which an update also fixes; `aliasedDiff` is the project's
|
|
807
|
+
// own differentiation of an alias, which step 3 does not touch anyway.
|
|
808
|
+
const changed = [...report.forked, ...report.edited]
|
|
809
|
+
|
|
810
|
+
if (0 === changed.length) {
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (true === flags.force) {
|
|
815
|
+
actx.log.warn({
|
|
816
|
+
point: 'package-update-forced', package: pkgname, files: changed,
|
|
817
|
+
note: pkgname + ': --force, overwriting ' + changed.length +
|
|
818
|
+
' locally-changed file(s): ' + changed.join(', ')
|
|
819
|
+
})
|
|
820
|
+
return
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
throw new SdkGenError(
|
|
824
|
+
pkgname + ': ' + changed.length + ' file(s) differ from the installed ' +
|
|
825
|
+
'source, so updating would overwrite them:\n ' + changed.join('\n ') +
|
|
826
|
+
'\n\n This means one of two things, and nothing recorded in the project ' +
|
|
827
|
+
'tells them apart:' +
|
|
828
|
+
'\n - they are LOCAL EDITS, and `--force` will discard them;' +
|
|
829
|
+
'\n - or ' + pkgname + ' was already updated out of band (an ' +
|
|
830
|
+
'`npm update` in another shell), in which case they are merely STALE ' +
|
|
831
|
+
'and nothing is at risk.' +
|
|
832
|
+
'\n\n If you did not update it: copy anything you want to keep into ' +
|
|
833
|
+
'.sdk/model/, then re-run with --force.' +
|
|
834
|
+
'\n If you did: reinstall the version you had, re-run this command, ' +
|
|
835
|
+
'and it will check against the right source.')
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
// STEP 2: fetch. Injectable, so tests do not shell out and a caller with its
|
|
840
|
+
// own dependency management can supply one.
|
|
841
|
+
//
|
|
842
|
+
// `--no-fetch` covers the operator who has already fetched deliberately and
|
|
843
|
+
// accepts that step 1 measured against the new source. It is not the default
|
|
844
|
+
// because then this command would only ever re-apply the source it already
|
|
845
|
+
// has, which is `package add`.
|
|
846
|
+
async function fetchPackage(
|
|
847
|
+
pkgname: string, installed: Installed[], actx: ActionContext,
|
|
848
|
+
) {
|
|
849
|
+
const flags = actx.flags ?? {}
|
|
850
|
+
|
|
851
|
+
if (true === flags.nofetch) {
|
|
852
|
+
actx.log.info({
|
|
853
|
+
point: 'package-update-nofetch', package: pkgname,
|
|
854
|
+
note: pkgname + ': --no-fetch, using the source already installed'
|
|
855
|
+
})
|
|
856
|
+
return
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// A DRY RUN MUST NOT FETCH. The adders honour `actx.opts.dryrun` and write
|
|
860
|
+
// nothing, but an unconditional `npm install --save-dev` rewrites
|
|
861
|
+
// package.json, the lockfile and node_modules — mutating dependency state
|
|
862
|
+
// in the one mode whose entire promise is that nothing changes. This
|
|
863
|
+
// workstream has already fixed one dry-run defect (pruneStaleTemplates
|
|
864
|
+
// deleting files during a dry run); the fetch is the same failure in a
|
|
865
|
+
// louder place, because it reaches outside the project.
|
|
866
|
+
if (true === actx.opts?.dryrun) {
|
|
867
|
+
actx.log.info({
|
|
868
|
+
point: 'package-update-dryrun-fetch', package: pkgname,
|
|
869
|
+
note: pkgname + ': ** DRY RUN ** not fetching; the check and the ' +
|
|
870
|
+
're-add below run against the source already installed'
|
|
871
|
+
})
|
|
872
|
+
return
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const fetch = actx.fetchPackage ?? npmFetch
|
|
876
|
+
|
|
877
|
+
// npm CAN ONLY UPDATE WHAT NPM INSTALLED. A package added by a local path
|
|
878
|
+
// or an absolute checkout records that base, and the re-add reads from it —
|
|
879
|
+
// so an `npm install` would write a fresh copy into `node_modules`, leave
|
|
880
|
+
// the recorded source untouched, and the command would then recopy the OLD
|
|
881
|
+
// content while reporting success and having changed the project's
|
|
882
|
+
// dependencies. Refused rather than half-done, naming the way to do it.
|
|
883
|
+
if (null == actx.fetchPackage) {
|
|
884
|
+
const local = installed.find((i: Installed) =>
|
|
885
|
+
'' !== i.base && !isNodeModules(i.base))
|
|
886
|
+
|
|
887
|
+
if (null != local) {
|
|
888
|
+
throw new SdkGenError(
|
|
889
|
+
pkgname + ': installed from ' + local.base + ', which npm does not ' +
|
|
890
|
+
'manage, so fetching would update a different copy and change ' +
|
|
891
|
+
'nothing here.' +
|
|
892
|
+
'\n Update that source yourself (git pull, rebuild, …) and re-run ' +
|
|
893
|
+
'with --no-fetch.')
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
await fetch(pkgname, actx)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
// Is this base inside a `node_modules` directory — i.e. is it npm's to
|
|
902
|
+
// update? Checked on the '/'-normalised recorded value, which is how `base`
|
|
903
|
+
// is written (see helpers/stdrep), plus the platform separator for an
|
|
904
|
+
// absolute base recorded on Windows.
|
|
905
|
+
function isNodeModules(base: string): boolean {
|
|
906
|
+
const norm = base.split(Path.sep).join('/')
|
|
907
|
+
return norm.startsWith('node_modules/') || norm.includes('/node_modules/')
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
// The default fetch: hand it to npm, in the project's own directory.
|
|
912
|
+
//
|
|
913
|
+
// SHELLING OUT IS DELIBERATE and is the one place this generator runs another
|
|
914
|
+
// tool. The alternative — telling the operator to fetch first — is what makes
|
|
915
|
+
// the pre-check unable to distinguish a fork from a stale copy, which is the
|
|
916
|
+
// entire point of the ordering above.
|
|
917
|
+
async function npmFetch(pkgname: string, actx: ActionContext) {
|
|
918
|
+
const { execFile } = require('node:child_process')
|
|
919
|
+
const { promisify } = require('node:util')
|
|
920
|
+
|
|
921
|
+
const run = promisify(execFile)
|
|
922
|
+
const cwd = actx.folder ?? '.'
|
|
923
|
+
|
|
924
|
+
actx.log.info({
|
|
925
|
+
point: 'package-update-fetch', package: pkgname, cwd,
|
|
926
|
+
note: pkgname + ': npm install ' + pkgname + '@latest'
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
try {
|
|
930
|
+
// `npm.cmd` ON WINDOWS. `execFile` does no PATHEXT resolution, so plain
|
|
931
|
+
// `npm` — which is a `.cmd` shim there — fails with ENOENT. Not caught by
|
|
932
|
+
// CI, because every test injects its own fetcher and this function never
|
|
933
|
+
// runs; found by reading it rather than by it breaking.
|
|
934
|
+
//
|
|
935
|
+
// Arguments as an ARRAY and no `shell: true`, so a package name is never
|
|
936
|
+
// interpreted by a shell.
|
|
937
|
+
//
|
|
938
|
+
// maxBuffer raised well past execFile's 1MB default: `npm install` output
|
|
939
|
+
// for a large tree exceeds it, and the resulting ENOBUFS would be caught
|
|
940
|
+
// below and reported as a failed fetch — after npm had in fact SUCCEEDED,
|
|
941
|
+
// leaving the project's dependencies updated and its `.sdk` not.
|
|
942
|
+
const out = await run(
|
|
943
|
+
'win32' === process.platform ? 'npm.cmd' : 'npm',
|
|
944
|
+
['install', '--save-dev', pkgname + '@latest'],
|
|
945
|
+
{ cwd, maxBuffer: 64 * 1024 * 1024 })
|
|
946
|
+
|
|
947
|
+
actx.log.debug({
|
|
948
|
+
point: 'package-update-fetched', package: pkgname,
|
|
949
|
+
stdout: out.stdout, stderr: out.stderr
|
|
950
|
+
})
|
|
951
|
+
}
|
|
952
|
+
catch (err: any) {
|
|
953
|
+
throw new SdkGenError(
|
|
954
|
+
pkgname + ': fetch failed — ' + (err.message || String(err)) +
|
|
955
|
+
'\n nothing has been overwritten. Fetch it yourself and re-run with ' +
|
|
956
|
+
'--no-fetch, or fix the install and try again.' +
|
|
957
|
+
(null == err.stderr ? '' : '\n\n' + err.stderr))
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
// STEP 2b: the fetched package is a DIFFERENT package from the one checked.
|
|
963
|
+
//
|
|
964
|
+
// Resolved from a recorded base rather than by name, because that is where
|
|
965
|
+
// the re-add will read from — validating some other copy of the package would
|
|
966
|
+
// be validating the wrong thing.
|
|
967
|
+
//
|
|
968
|
+
// A source with no manifest is not an error here, unlike in `package add`:
|
|
969
|
+
// the project demonstrably installed from it once, and refusing to refresh it
|
|
970
|
+
// now would strand a project whose package predates the manifest. Nothing is
|
|
971
|
+
// validated in that case, which is the same amount as before this command
|
|
972
|
+
// existed.
|
|
973
|
+
function validateFetched(
|
|
974
|
+
pkgname: string, installed: Installed[], actx: ActionContext,
|
|
975
|
+
) {
|
|
976
|
+
const base = installed.find((i: Installed) => '' !== i.base)?.base
|
|
977
|
+
|
|
978
|
+
if (null == base) {
|
|
979
|
+
return
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const root = Path.join(
|
|
983
|
+
Path.isAbsolute(base) ? base : Path.join(actx.folder ?? '.', base), '..')
|
|
984
|
+
|
|
985
|
+
const read = readManifest(actx.fs(), Path.join(root, '.sdk'))
|
|
986
|
+
|
|
987
|
+
if (null == read.manifest) {
|
|
988
|
+
actx.log.info({
|
|
989
|
+
point: 'package-update-unmanifested', package: pkgname, file: read.file,
|
|
990
|
+
note: pkgname + ': the source declares no manifest, so the fetched ' +
|
|
991
|
+
'version could not be validated'
|
|
992
|
+
})
|
|
993
|
+
return
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const src: PackageSource = {
|
|
997
|
+
ref: pkgname, root, sdk: Path.join(root, '.sdk'), manifest: read.manifest,
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
checkEngine(src, actx)
|
|
1001
|
+
refuse(src, validateManifest(actx.fs(), src.sdk, src.manifest, KINDS),
|
|
1002
|
+
actx.log)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
// STEP 3: re-add each item from its recorded base.
|
|
1007
|
+
//
|
|
1008
|
+
// An ALIASED item's model file is left alone, and that is not special-cased
|
|
1009
|
+
// here: `kindModel` already creates it `exclude: true` for a kind whose
|
|
1010
|
+
// aliased definition is project-owned, so a re-add refreshes `src/cmp` and
|
|
1011
|
+
// `tm` from the new origin and leaves the file the project is MEANT to edit
|
|
1012
|
+
// untouched. Reported, so the author knows to port upstream model changes by
|
|
1013
|
+
// hand rather than discovering later that they were never applied.
|
|
1014
|
+
async function reAdd(
|
|
1015
|
+
pkgname: string, installed: Installed[], actx: ActionContext,
|
|
1016
|
+
): Promise<any[]> {
|
|
1017
|
+
const log = actx.log
|
|
1018
|
+
const results: any[] = []
|
|
1019
|
+
|
|
1020
|
+
const skipped = installed.filter((i: Installed) => i.aliased)
|
|
1021
|
+
|
|
1022
|
+
if (0 < skipped.length) {
|
|
1023
|
+
log.info({
|
|
1024
|
+
point: 'package-update-alias-model-kept', package: pkgname,
|
|
1025
|
+
items: skipped.map((i: Installed) => i.kind + '/' + i.name),
|
|
1026
|
+
note: pkgname + ': keeping the model file of ' + skipped.length +
|
|
1027
|
+
' aliased item(s) — that file is where an alias is differentiated, ' +
|
|
1028
|
+
'so upstream model changes to ' +
|
|
1029
|
+
skipped.map((i: Installed) => i.origname).join(', ') +
|
|
1030
|
+
' must be ported by hand: ' +
|
|
1031
|
+
skipped.map((i: Installed) =>
|
|
1032
|
+
'model/' + i.kind + '/' + i.name + '.aontu').join(', ')
|
|
1033
|
+
})
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Same order as `package add`, for the same reason: `feature add` fans a
|
|
1037
|
+
// feature's source across the targets in the model.
|
|
1038
|
+
const byKind: Record<string, string[]> = Object.create(null)
|
|
1039
|
+
|
|
1040
|
+
for (const item of installed) {
|
|
1041
|
+
// The ref that reinstalls it — exactly what `recordedRef` reconstructs
|
|
1042
|
+
// for doctor, so an update and a check agree about where an item is from.
|
|
1043
|
+
const ref = Path.join(item.base, '..', item.origname) +
|
|
1044
|
+
(item.origname === item.name ? '' : '~' + item.name)
|
|
1045
|
+
|
|
1046
|
+
;(byKind[item.kind] = byKind[item.kind] ?? []).push(ref)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
for (const kind of orderedKinds(byKind)) {
|
|
1050
|
+
const add = adderFor(kind)
|
|
1051
|
+
|
|
1052
|
+
if (null == add) {
|
|
1053
|
+
log.warn({
|
|
1054
|
+
point: 'package-kind-unsupported', package: pkgname, kind,
|
|
1055
|
+
note: pkgname + ': nothing can install `' + kind + '` items yet'
|
|
1056
|
+
})
|
|
1057
|
+
continue
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
results.push(await add(byKind[kind], actx))
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return results
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
// `package list` — what this project has installed, and where each item came
|
|
1068
|
+
// from. Read entirely from the MODEL's recorded provenance (§4), which is why
|
|
1069
|
+
// there is no lockfile to consult and nothing that can disagree with it.
|
|
1070
|
+
async function cmd_package_list(
|
|
1071
|
+
_args: string[], actx: ActionContext,
|
|
1072
|
+
): Promise<ActionResult> {
|
|
1073
|
+
const log = actx.log
|
|
1074
|
+
const fs = actx.fs()
|
|
1075
|
+
const kit: any = (actx.model as any)?.main?.[KIT] ?? {}
|
|
1076
|
+
|
|
1077
|
+
// package name -> kind -> [{name, base, origname}]
|
|
1078
|
+
const groups: Record<string, any[]> = Object.create(null)
|
|
1079
|
+
|
|
1080
|
+
for (const kind of Object.keys(KINDS).sort()) {
|
|
1081
|
+
const items = kit[kind] ?? {}
|
|
1082
|
+
|
|
1083
|
+
for (const name of Object.keys(items).sort()) {
|
|
1084
|
+
const item = items[name]
|
|
1085
|
+
|
|
1086
|
+
if (null == item || 'object' !== typeof item) {
|
|
1087
|
+
continue
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// An item with no recorded base predates provenance; it is still
|
|
1091
|
+
// installed, and saying "(unrecorded)" is more use than omitting it.
|
|
1092
|
+
const pkg = ('' === item.package || null == item.package) ?
|
|
1093
|
+
UNRECORDED : item.package
|
|
1094
|
+
|
|
1095
|
+
;(groups[pkg] = groups[pkg] ?? []).push({
|
|
1096
|
+
kind, name,
|
|
1097
|
+
origname: item.origname || name,
|
|
1098
|
+
base: item.base || '',
|
|
1099
|
+
})
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const packages = Object.keys(groups).sort()
|
|
1104
|
+
|
|
1105
|
+
for (const pkg of packages) {
|
|
1106
|
+
// The version ON DISK, not one recorded at add time: what `package
|
|
1107
|
+
// update` would compare against is the source as it is now.
|
|
1108
|
+
const version = installedVersion(fs, actx.folder ?? '.', groups[pkg])
|
|
1109
|
+
|
|
1110
|
+
log.info({
|
|
1111
|
+
point: 'package-list-entry', package: pkg, version,
|
|
1112
|
+
items: groups[pkg],
|
|
1113
|
+
note: pkg + (null == version ? '' : '@' + version) + ': ' +
|
|
1114
|
+
groups[pkg].map((i: any) =>
|
|
1115
|
+
i.kind + ' `' + i.name + '`' +
|
|
1116
|
+
(i.origname === i.name ? '' : ' (' + i.origname + ')')).join(', ')
|
|
1117
|
+
})
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
log.info({
|
|
1121
|
+
point: 'package-list-end', packages: packages.length,
|
|
1122
|
+
note: 0 === packages.length ?
|
|
1123
|
+
'nothing installed' : packages.length + ' package(s)'
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
return { jres: undefined as any, report: { packages, groups } } as any
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
const UNRECORDED = '(unrecorded)'
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
// Read the version from the manifest the items say they came from. All items
|
|
1134
|
+
// of one package share a base in practice; the first that yields a manifest
|
|
1135
|
+
// answers.
|
|
1136
|
+
function installedVersion(
|
|
1137
|
+
fs: any, project: string, items: any[],
|
|
1138
|
+
): string | undefined {
|
|
1139
|
+
for (const item of items) {
|
|
1140
|
+
if ('' === item.base) {
|
|
1141
|
+
continue
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const sdk = Path.isAbsolute(item.base) ?
|
|
1145
|
+
item.base : Path.join(project, item.base)
|
|
1146
|
+
|
|
1147
|
+
const read = readManifest(fs, sdk)
|
|
1148
|
+
|
|
1149
|
+
if (null != read.manifest?.version) {
|
|
1150
|
+
return read.manifest.version
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
return undefined
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
function capitalise(s: string): string {
|
|
1159
|
+
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
export {
|
|
1164
|
+
action_package,
|
|
1165
|
+
package_add,
|
|
1166
|
+
package_update,
|
|
1167
|
+
installedFrom,
|
|
1168
|
+
resolvePackage,
|
|
1169
|
+
selectItems,
|
|
1170
|
+
parseAliases,
|
|
1171
|
+
registerAdder,
|
|
1172
|
+
SDKGEN_VERSION,
|
|
1173
|
+
}
|