@jskit-ai/jskit-cli 0.2.29 → 0.2.31
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/package.json +3 -3
- package/src/server/cliRuntime/mutationWhen.js +25 -7
- package/src/server/commandHandlers/health.js +140 -0
- package/src/server/commandHandlers/list.js +268 -1
- package/src/server/commandHandlers/packageCommands/add.js +113 -0
- package/src/server/commandHandlers/packageCommands/discoverabilityHelp.js +649 -0
- package/src/server/commandHandlers/packageCommands/generate.js +151 -10
- package/src/server/commandHandlers/packageCommands/tabLinkItemProvisioning.js +326 -0
- package/src/server/core/commandCatalog.js +4 -1
- package/src/server/core/createCommandHandlers.js +2 -1
- package/src/server/core/dispatchCli.js +3 -0
- package/src/server/core/usageHelp.js +32 -5
- package/src/server/shared/optionInterpolation.js +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/jskit-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "Bundle and package orchestration CLI for JSKIT apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"test": "node --test"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@jskit-ai/jskit-catalog": "0.1.
|
|
24
|
-
"@jskit-ai/kernel": "0.1.
|
|
23
|
+
"@jskit-ai/jskit-catalog": "0.1.31",
|
|
24
|
+
"@jskit-ai/kernel": "0.1.23"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": "20.x"
|
|
@@ -55,11 +55,9 @@ function normalizeMutationWhen(value) {
|
|
|
55
55
|
const allConditions = ensureArray(source.all)
|
|
56
56
|
.map((entry) => normalizeMutationWhen(entry))
|
|
57
57
|
.filter(Boolean);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
}
|
|
58
|
+
const anyConditions = ensureArray(source.any)
|
|
59
|
+
.map((entry) => normalizeMutationWhen(entry))
|
|
60
|
+
.filter(Boolean);
|
|
63
61
|
|
|
64
62
|
const option = String(source.option || "").trim();
|
|
65
63
|
const config = String(source.config || "").trim();
|
|
@@ -70,11 +68,13 @@ function normalizeMutationWhen(value) {
|
|
|
70
68
|
const includes = ensureArray(source.in).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
71
69
|
const excludes = ensureArray(source.notIn).map((entry) => String(entry || "").trim()).filter(Boolean);
|
|
72
70
|
|
|
73
|
-
if (!option && !config) {
|
|
71
|
+
if (!option && !config && allConditions.length < 1 && anyConditions.length < 1) {
|
|
74
72
|
return null;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
return {
|
|
76
|
+
all: allConditions,
|
|
77
|
+
any: anyConditions,
|
|
78
78
|
option,
|
|
79
79
|
config,
|
|
80
80
|
equals,
|
|
@@ -217,7 +217,7 @@ function shouldApplyMutationWhen(
|
|
|
217
217
|
|
|
218
218
|
const allConditions = ensureArray(when.all).filter((entry) => entry && typeof entry === "object");
|
|
219
219
|
if (allConditions.length > 0) {
|
|
220
|
-
|
|
220
|
+
const allMatch = allConditions.every((entry) =>
|
|
221
221
|
shouldApplyMutationWhen(entry, {
|
|
222
222
|
options,
|
|
223
223
|
configContext,
|
|
@@ -225,6 +225,24 @@ function shouldApplyMutationWhen(
|
|
|
225
225
|
mutationContext
|
|
226
226
|
})
|
|
227
227
|
);
|
|
228
|
+
if (!allMatch) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const anyConditions = ensureArray(when.any).filter((entry) => entry && typeof entry === "object");
|
|
234
|
+
if (anyConditions.length > 0) {
|
|
235
|
+
const anyMatch = anyConditions.some((entry) =>
|
|
236
|
+
shouldApplyMutationWhen(entry, {
|
|
237
|
+
options,
|
|
238
|
+
configContext,
|
|
239
|
+
packageId,
|
|
240
|
+
mutationContext
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
if (!anyMatch) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
228
246
|
}
|
|
229
247
|
|
|
230
248
|
const optionName = String(when.option || "").trim();
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readdir,
|
|
3
|
+
readFile
|
|
4
|
+
} from "node:fs/promises";
|
|
1
5
|
import {
|
|
2
6
|
ensureArray,
|
|
3
7
|
ensureObject,
|
|
@@ -15,9 +19,42 @@ function createHealthCommands(ctx = {}) {
|
|
|
15
19
|
hydratePackageRegistryFromInstalledNodeModules,
|
|
16
20
|
inspectPackageOfferings,
|
|
17
21
|
fileExists,
|
|
22
|
+
normalizeRelativePath,
|
|
18
23
|
path
|
|
19
24
|
} = ctx;
|
|
20
25
|
|
|
26
|
+
const MDI_SVG_MAIN_ENTRY_CANDIDATES = Object.freeze([
|
|
27
|
+
"src/main.js",
|
|
28
|
+
"src/main.mjs",
|
|
29
|
+
"src/main.ts"
|
|
30
|
+
]);
|
|
31
|
+
const MDI_SVG_SCAN_ROOTS = Object.freeze([
|
|
32
|
+
"src",
|
|
33
|
+
"packages"
|
|
34
|
+
]);
|
|
35
|
+
const MDI_SVG_IGNORED_DIRECTORY_NAMES = new Set([
|
|
36
|
+
".git",
|
|
37
|
+
".jskit",
|
|
38
|
+
".build",
|
|
39
|
+
"coverage",
|
|
40
|
+
"dist",
|
|
41
|
+
"docs",
|
|
42
|
+
"LEGACY",
|
|
43
|
+
"node_modules",
|
|
44
|
+
"test",
|
|
45
|
+
"tests",
|
|
46
|
+
"__tests__"
|
|
47
|
+
]);
|
|
48
|
+
const MDI_SVG_IGNORED_FILE_PATTERNS = Object.freeze([
|
|
49
|
+
/\.spec\./i,
|
|
50
|
+
/\.test\./i,
|
|
51
|
+
/\.vitest\./i
|
|
52
|
+
]);
|
|
53
|
+
const DIRECT_MDI_LITERAL_ICON_PATTERN =
|
|
54
|
+
/<(v-[a-z0-9-]+)[^>]*?\b(icon|prepend-icon|append-icon)\s*=\s*(['"])(mdi-[^'"]+)\3/gi;
|
|
55
|
+
const DIRECT_MDI_BOUND_LITERAL_ICON_PATTERN =
|
|
56
|
+
/<(v-[a-z0-9-]+)[^>]*?(?::|v-bind:)(icon|prepend-icon|append-icon)\s*=\s*(['"])(['"])(mdi-[^'"]+)\4\3/gi;
|
|
57
|
+
|
|
21
58
|
function collectDescriptorContainerTokens({ packageId, side, values, issues }) {
|
|
22
59
|
const declaredTokens = new Set();
|
|
23
60
|
const duplicateTokens = new Set();
|
|
@@ -115,6 +152,104 @@ function createHealthCommands(ctx = {}) {
|
|
|
115
152
|
}
|
|
116
153
|
}
|
|
117
154
|
|
|
155
|
+
async function appUsesVuetifyMdiSvg(appRoot) {
|
|
156
|
+
for (const relativePath of MDI_SVG_MAIN_ENTRY_CANDIDATES) {
|
|
157
|
+
const absolutePath = path.join(appRoot, relativePath);
|
|
158
|
+
if (!(await fileExists(absolutePath))) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const fileContent = await readFile(absolutePath, "utf8");
|
|
162
|
+
if (fileContent.includes("vuetify/iconsets/mdi-svg")) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function shouldSkipMdiSvgDoctorDirectory(directoryName = "") {
|
|
171
|
+
return MDI_SVG_IGNORED_DIRECTORY_NAMES.has(String(directoryName || "").trim());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function shouldSkipMdiSvgDoctorFile(fileName = "") {
|
|
175
|
+
const normalizedFileName = String(fileName || "").trim();
|
|
176
|
+
if (!normalizedFileName.endsWith(".vue")) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return MDI_SVG_IGNORED_FILE_PATTERNS.some((pattern) => pattern.test(normalizedFileName));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function collectVueSourceFiles(rootDirectory, collected = []) {
|
|
183
|
+
if (!(await fileExists(rootDirectory))) {
|
|
184
|
+
return collected;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const entries = await readdir(rootDirectory, { withFileTypes: true });
|
|
188
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
189
|
+
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const entryPath = path.join(rootDirectory, entry.name);
|
|
192
|
+
if (entry.isDirectory()) {
|
|
193
|
+
if (shouldSkipMdiSvgDoctorDirectory(entry.name)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
await collectVueSourceFiles(entryPath, collected);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (entry.isFile() && !shouldSkipMdiSvgDoctorFile(entry.name)) {
|
|
200
|
+
collected.push(entryPath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return collected;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveLineNumberFromIndex(sourceText = "", index = 0) {
|
|
208
|
+
return String(sourceText || "").slice(0, Math.max(0, index)).split("\n").length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function collectDirectMdiSvgTemplateIconIssues({ sourceText, relativePath, issues }) {
|
|
212
|
+
DIRECT_MDI_LITERAL_ICON_PATTERN.lastIndex = 0;
|
|
213
|
+
for (const match of sourceText.matchAll(DIRECT_MDI_LITERAL_ICON_PATTERN)) {
|
|
214
|
+
const [, tagName = "v-component", propName = "icon", , rawIcon = ""] = match;
|
|
215
|
+
const lineNumber = resolveLineNumberFromIndex(sourceText, match.index || 0);
|
|
216
|
+
issues.push(
|
|
217
|
+
`${relativePath}:${lineNumber}: raw "${rawIcon}" passed to <${tagName}> ${propName} while the app uses vuetify/iconsets/mdi-svg. Use an @mdi/js path or a Vuetify alias.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
DIRECT_MDI_BOUND_LITERAL_ICON_PATTERN.lastIndex = 0;
|
|
222
|
+
for (const match of sourceText.matchAll(DIRECT_MDI_BOUND_LITERAL_ICON_PATTERN)) {
|
|
223
|
+
const [, tagName = "v-component", propName = "icon", , , rawIcon = ""] = match;
|
|
224
|
+
const lineNumber = resolveLineNumberFromIndex(sourceText, match.index || 0);
|
|
225
|
+
issues.push(
|
|
226
|
+
`${relativePath}:${lineNumber}: raw "${rawIcon}" passed to <${tagName}> ${propName} while the app uses vuetify/iconsets/mdi-svg. Use an @mdi/js path or a Vuetify alias.`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function collectMdiSvgDoctorIssues({ appRoot, issues }) {
|
|
232
|
+
if (!(await appUsesVuetifyMdiSvg(appRoot))) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const vueFilePaths = [];
|
|
237
|
+
for (const relativeRoot of MDI_SVG_SCAN_ROOTS) {
|
|
238
|
+
await collectVueSourceFiles(path.join(appRoot, relativeRoot), vueFilePaths);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
vueFilePaths.sort((left, right) => left.localeCompare(right));
|
|
242
|
+
|
|
243
|
+
for (const absolutePath of vueFilePaths) {
|
|
244
|
+
const sourceText = await readFile(absolutePath, "utf8");
|
|
245
|
+
collectDirectMdiSvgTemplateIconIssues({
|
|
246
|
+
sourceText,
|
|
247
|
+
relativePath: normalizeRelativePath(appRoot, absolutePath),
|
|
248
|
+
issues
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
118
253
|
function collectDiLabelParityIssuesForPackage({ packageEntry, packageInsights }) {
|
|
119
254
|
const packageId = String(packageEntry?.packageId || "").trim();
|
|
120
255
|
const descriptor = ensureObject(packageEntry?.descriptor);
|
|
@@ -199,6 +334,11 @@ function createHealthCommands(ctx = {}) {
|
|
|
199
334
|
}
|
|
200
335
|
}
|
|
201
336
|
|
|
337
|
+
await collectMdiSvgDoctorIssues({
|
|
338
|
+
appRoot,
|
|
339
|
+
issues
|
|
340
|
+
});
|
|
341
|
+
|
|
202
342
|
const payload = {
|
|
203
343
|
appRoot,
|
|
204
344
|
lockVersion: lock.lockVersion,
|
|
@@ -1,9 +1,134 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
1
4
|
import {
|
|
2
5
|
ensureArray,
|
|
3
6
|
ensureObject,
|
|
4
7
|
sortStrings
|
|
5
8
|
} from "../shared/collectionUtils.js";
|
|
6
9
|
|
|
10
|
+
const PLACEMENT_FILE_RELATIVE_PATH = "src/placement.js";
|
|
11
|
+
const MAIN_CLIENT_PROVIDERS_RELATIVE_PATH = "packages/main/src/client/providers";
|
|
12
|
+
const COMPONENT_TOKEN_PATTERN = /\bcomponentToken\s*:\s*["']([^"']+)["']/g;
|
|
13
|
+
const REGISTER_MAIN_CLIENT_COMPONENT_PATTERN = /registerMainClientComponent\(\s*["']([^"']+)["']\s*,/g;
|
|
14
|
+
const LINK_ITEM_TOKEN_SUFFIX = "link-item";
|
|
15
|
+
const PROVIDER_SOURCE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
16
|
+
const READ_FILE_IGNORE_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EISDIR", "EACCES", "EPERM"]);
|
|
17
|
+
const READ_DIRECTORY_IGNORE_ERROR_CODES = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"]);
|
|
18
|
+
|
|
19
|
+
function collectTokenMatches(source = "", pattern = COMPONENT_TOKEN_PATTERN) {
|
|
20
|
+
const sourceText = String(source || "");
|
|
21
|
+
const tokens = [];
|
|
22
|
+
for (const match of sourceText.matchAll(pattern)) {
|
|
23
|
+
const token = String(match[1] || "").trim();
|
|
24
|
+
if (token) {
|
|
25
|
+
tokens.push(token);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return tokens;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function appendTokenSource(map, token = "", source = "") {
|
|
32
|
+
const normalizedToken = String(token || "").trim();
|
|
33
|
+
if (!normalizedToken) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const normalizedSource = String(source || "").trim();
|
|
37
|
+
const existingSources = map.get(normalizedToken) || new Set();
|
|
38
|
+
if (normalizedSource) {
|
|
39
|
+
existingSources.add(normalizedSource);
|
|
40
|
+
}
|
|
41
|
+
map.set(normalizedToken, existingSources);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isLinkItemToken(token = "") {
|
|
45
|
+
return String(token || "").trim().toLowerCase().endsWith(LINK_ITEM_TOKEN_SUFFIX);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readFileIfExists(filePath = "") {
|
|
49
|
+
try {
|
|
50
|
+
return await readFile(filePath, "utf8");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const errorCode = String(error?.code || "").trim().toUpperCase();
|
|
53
|
+
if (READ_FILE_IGNORE_ERROR_CODES.has(errorCode)) {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function resolveDescriptorFromLockEntry({ appRoot = "", packageId = "", installedPackageEntry = {} } = {}) {
|
|
61
|
+
const source = ensureObject(installedPackageEntry.source);
|
|
62
|
+
const descriptorRelativePath = String(source.descriptorPath || "").trim();
|
|
63
|
+
if (!descriptorRelativePath) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const descriptorAbsolutePath = path.resolve(appRoot, descriptorRelativePath);
|
|
68
|
+
const descriptorSource = await readFileIfExists(descriptorAbsolutePath);
|
|
69
|
+
if (!descriptorSource) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let descriptorModule = null;
|
|
74
|
+
try {
|
|
75
|
+
descriptorModule = await import(`${pathToFileURL(descriptorAbsolutePath).href}?t=${Date.now()}_${Math.random()}`);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const descriptor = ensureObject(descriptorModule?.default);
|
|
81
|
+
if (Object.keys(descriptor).length < 1) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const resolvedPackageId = String(descriptor.packageId || packageId || "").trim();
|
|
86
|
+
if (!resolvedPackageId) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
packageId: resolvedPackageId,
|
|
92
|
+
descriptor
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function collectProviderSourceFiles(rootPath = "") {
|
|
97
|
+
const files = [];
|
|
98
|
+
const stack = [path.resolve(String(rootPath || ""))];
|
|
99
|
+
|
|
100
|
+
while (stack.length > 0) {
|
|
101
|
+
const currentPath = stack.pop();
|
|
102
|
+
let entries = [];
|
|
103
|
+
try {
|
|
104
|
+
entries = await readdir(currentPath, { withFileTypes: true });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const errorCode = String(error?.code || "").trim().toUpperCase();
|
|
107
|
+
if (READ_DIRECTORY_IGNORE_ERROR_CODES.has(errorCode)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
stack.push(entryPath);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!entry.isFile()) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
123
|
+
if (PROVIDER_SOURCE_EXTENSIONS.has(extension)) {
|
|
124
|
+
files.push(entryPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
130
|
+
}
|
|
131
|
+
|
|
7
132
|
function createListCommands(ctx = {}) {
|
|
8
133
|
const {
|
|
9
134
|
createCliError,
|
|
@@ -14,7 +139,9 @@ function createListCommands(ctx = {}) {
|
|
|
14
139
|
loadPackageRegistry,
|
|
15
140
|
loadBundleRegistry,
|
|
16
141
|
loadAppLocalPackageRegistry,
|
|
142
|
+
resolveInstalledNodeModulePackageEntry,
|
|
17
143
|
discoverShellOutletTargetsFromApp,
|
|
144
|
+
normalizePlacementContributions,
|
|
18
145
|
resolvePackageKind
|
|
19
146
|
} = ctx;
|
|
20
147
|
|
|
@@ -51,6 +178,11 @@ function createListCommands(ctx = {}) {
|
|
|
51
178
|
if (mode === "placements") {
|
|
52
179
|
throw createCliError('list mode "placements" moved to a dedicated command: jskit list-placements.');
|
|
53
180
|
}
|
|
181
|
+
if (mode === "placement-component-tokens") {
|
|
182
|
+
throw createCliError(
|
|
183
|
+
'list mode "placement-component-tokens" moved to a dedicated command: jskit list-link-items.'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
54
186
|
|
|
55
187
|
if (!shouldListBundles && !shouldListPackages && !shouldListGenerators) {
|
|
56
188
|
throw createCliError(`Unknown list mode: ${mode}`, { showUsage: true });
|
|
@@ -283,9 +415,144 @@ function createListCommands(ctx = {}) {
|
|
|
283
415
|
return 0;
|
|
284
416
|
}
|
|
285
417
|
|
|
418
|
+
async function commandListLinkItems({ options, cwd, stdout }) {
|
|
419
|
+
const appRoot = await resolveAppRootFromCwd(cwd);
|
|
420
|
+
const tokenPrefixFilter = String(options?.inlineOptions?.prefix || "").trim();
|
|
421
|
+
const includeAllClientContainerTokens = options?.all === true;
|
|
422
|
+
const onlyLinkItemTokens = !includeAllClientContainerTokens;
|
|
423
|
+
const { lock } = await loadLockFile(appRoot);
|
|
424
|
+
const packageRegistry = await loadPackageRegistry();
|
|
425
|
+
const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
|
|
426
|
+
const installedPackageEntries = ensureObject(lock.installedPackages);
|
|
427
|
+
const installedPackageIds = sortStrings(Object.keys(installedPackageEntries));
|
|
428
|
+
|
|
429
|
+
const packageEntryById = new Map();
|
|
430
|
+
for (const [packageId, packageEntry] of packageRegistry.entries()) {
|
|
431
|
+
packageEntryById.set(packageId, packageEntry);
|
|
432
|
+
}
|
|
433
|
+
for (const [packageId, packageEntry] of appLocalRegistry.entries()) {
|
|
434
|
+
packageEntryById.set(packageId, packageEntry);
|
|
435
|
+
}
|
|
436
|
+
for (const packageId of installedPackageIds) {
|
|
437
|
+
if (packageEntryById.has(packageId)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const installedPackageEntry = ensureObject(installedPackageEntries[packageId]);
|
|
441
|
+
const descriptorFromLockEntry = await resolveDescriptorFromLockEntry({
|
|
442
|
+
appRoot,
|
|
443
|
+
packageId,
|
|
444
|
+
installedPackageEntry
|
|
445
|
+
});
|
|
446
|
+
if (descriptorFromLockEntry) {
|
|
447
|
+
packageEntryById.set(packageId, descriptorFromLockEntry);
|
|
448
|
+
packageEntryById.set(descriptorFromLockEntry.packageId, descriptorFromLockEntry);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (typeof resolveInstalledNodeModulePackageEntry !== "function") {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const resolvedNodeModuleEntry = await resolveInstalledNodeModulePackageEntry({
|
|
455
|
+
appRoot,
|
|
456
|
+
packageId
|
|
457
|
+
});
|
|
458
|
+
if (resolvedNodeModuleEntry) {
|
|
459
|
+
packageEntryById.set(resolvedNodeModuleEntry.packageId, resolvedNodeModuleEntry);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const tokenSourceByToken = new Map();
|
|
464
|
+
for (const packageId of installedPackageIds) {
|
|
465
|
+
const packageEntry = packageEntryById.get(packageId) || null;
|
|
466
|
+
if (!packageEntry) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const descriptor = ensureObject(packageEntry.descriptor);
|
|
470
|
+
const metadata = ensureObject(descriptor.metadata);
|
|
471
|
+
const ui = ensureObject(metadata.ui);
|
|
472
|
+
const placements = ensureObject(ui.placements);
|
|
473
|
+
const contributions = normalizePlacementContributions(placements.contributions);
|
|
474
|
+
for (const contribution of contributions) {
|
|
475
|
+
const componentToken = String(contribution.componentToken || "").trim();
|
|
476
|
+
if (!componentToken) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const contributionSource = String(contribution.source || "").trim();
|
|
480
|
+
const sourceLabel = contributionSource
|
|
481
|
+
? `package:${packageId}:${contributionSource}`
|
|
482
|
+
: `package:${packageId}:metadata.ui.placements.contributions`;
|
|
483
|
+
appendTokenSource(tokenSourceByToken, componentToken, sourceLabel);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (includeAllClientContainerTokens) {
|
|
487
|
+
const apiSummary = ensureObject(metadata.apiSummary);
|
|
488
|
+
const containerTokens = ensureObject(apiSummary.containerTokens);
|
|
489
|
+
const clientTokens = ensureArray(containerTokens.client).map((value) => String(value || "").trim()).filter(Boolean);
|
|
490
|
+
for (const clientToken of clientTokens) {
|
|
491
|
+
appendTokenSource(tokenSourceByToken, clientToken, `package:${packageId}:metadata.apiSummary.containerTokens.client`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const placementSourcePath = path.join(appRoot, PLACEMENT_FILE_RELATIVE_PATH);
|
|
497
|
+
const placementSource = await readFileIfExists(placementSourcePath);
|
|
498
|
+
for (const token of collectTokenMatches(placementSource, COMPONENT_TOKEN_PATTERN)) {
|
|
499
|
+
appendTokenSource(tokenSourceByToken, token, `app:${normalizeRelativePosixPath(PLACEMENT_FILE_RELATIVE_PATH)}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const providersRootPath = path.join(appRoot, MAIN_CLIENT_PROVIDERS_RELATIVE_PATH);
|
|
503
|
+
const providerSourceFiles = await collectProviderSourceFiles(providersRootPath);
|
|
504
|
+
for (const providerSourceFile of providerSourceFiles) {
|
|
505
|
+
const providerSource = await readFileIfExists(providerSourceFile);
|
|
506
|
+
if (!providerSource) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const providerRelativePath = normalizeRelativePosixPath(path.relative(appRoot, providerSourceFile));
|
|
510
|
+
for (const token of collectTokenMatches(providerSource, REGISTER_MAIN_CLIENT_COMPONENT_PATTERN)) {
|
|
511
|
+
appendTokenSource(tokenSourceByToken, token, `app:${providerRelativePath}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const tokens = sortStrings([...tokenSourceByToken.keys()])
|
|
516
|
+
.filter((token) => !tokenPrefixFilter || token.startsWith(tokenPrefixFilter))
|
|
517
|
+
.filter((token) => !onlyLinkItemTokens || isLinkItemToken(token))
|
|
518
|
+
.map((token) => ({
|
|
519
|
+
token,
|
|
520
|
+
sources: sortStrings([...(tokenSourceByToken.get(token) || new Set())])
|
|
521
|
+
}));
|
|
522
|
+
|
|
523
|
+
if (options.json) {
|
|
524
|
+
stdout.write(`${JSON.stringify({ placementComponentTokens: tokens }, null, 2)}\n`);
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const color = createColorFormatter(stdout);
|
|
529
|
+
const lines = [color.heading("Available placement component tokens:")];
|
|
530
|
+
lines.push(
|
|
531
|
+
color.dim(
|
|
532
|
+
includeAllClientContainerTokens
|
|
533
|
+
? "Showing all discovered tokens (--all), including non-link-item/container/runtime tokens."
|
|
534
|
+
: 'Showing link-item tokens only (token must end with "link-item"). Tip: use --all for full token list.'
|
|
535
|
+
)
|
|
536
|
+
);
|
|
537
|
+
if (tokens.length < 1) {
|
|
538
|
+
lines.push("- none");
|
|
539
|
+
} else {
|
|
540
|
+
for (const entry of tokens) {
|
|
541
|
+
const token = String(entry.token || "").trim();
|
|
542
|
+
const sources = ensureArray(entry.sources).map((value) => String(value || "").trim()).filter(Boolean);
|
|
543
|
+
const sourceLabel = sources.length > 0 ? ` ${color.dim(`[${sources.join(", ")}]`)}` : "";
|
|
544
|
+
lines.push(`- ${color.item(token)}${sourceLabel}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
stdout.write(`${lines.join("\n")}\n`);
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
|
|
286
552
|
return {
|
|
287
553
|
commandList,
|
|
288
|
-
commandListPlacements
|
|
554
|
+
commandListPlacements,
|
|
555
|
+
commandListLinkItems
|
|
289
556
|
};
|
|
290
557
|
}
|
|
291
558
|
|
|
@@ -3,6 +3,16 @@ import {
|
|
|
3
3
|
ensureObject,
|
|
4
4
|
sortStrings
|
|
5
5
|
} from "../../shared/collectionUtils.js";
|
|
6
|
+
import {
|
|
7
|
+
isHelpToken,
|
|
8
|
+
renderAddCatalogHelp,
|
|
9
|
+
renderAddPackageHelp,
|
|
10
|
+
renderAddBundleHelp
|
|
11
|
+
} from "./discoverabilityHelp.js";
|
|
12
|
+
import {
|
|
13
|
+
TAB_LINK_COMPONENT_TOKEN,
|
|
14
|
+
ensureLocalMainTabLinkItemProvisioning
|
|
15
|
+
} from "./tabLinkItemProvisioning.js";
|
|
6
16
|
|
|
7
17
|
async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io }) {
|
|
8
18
|
const {
|
|
@@ -34,6 +44,94 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
|
|
|
34
44
|
const invocationMode = options?.commandMode === "generate" ? "generate" : "add";
|
|
35
45
|
const targetType = String(positional[0] || "").trim();
|
|
36
46
|
const targetId = String(positional[1] || "").trim();
|
|
47
|
+
const thirdToken = String(positional[2] || "").trim();
|
|
48
|
+
|
|
49
|
+
if (invocationMode === "add" && !targetType) {
|
|
50
|
+
const packageRegistry = await loadPackageRegistry();
|
|
51
|
+
const bundleRegistry = await loadBundleRegistry();
|
|
52
|
+
renderAddCatalogHelp({
|
|
53
|
+
io,
|
|
54
|
+
packageRegistry,
|
|
55
|
+
bundleRegistry,
|
|
56
|
+
resolvePackageKind,
|
|
57
|
+
json: options.json
|
|
58
|
+
});
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const addShorthandHelpTargetId =
|
|
63
|
+
invocationMode === "add" &&
|
|
64
|
+
targetType &&
|
|
65
|
+
targetType !== "bundle" &&
|
|
66
|
+
targetType !== "package" &&
|
|
67
|
+
isHelpToken(targetId) &&
|
|
68
|
+
!thirdToken
|
|
69
|
+
? targetType
|
|
70
|
+
: "";
|
|
71
|
+
|
|
72
|
+
const addPackageHelpTargetId =
|
|
73
|
+
invocationMode === "add" && targetType === "package" && targetId && isHelpToken(thirdToken)
|
|
74
|
+
? targetId
|
|
75
|
+
: addShorthandHelpTargetId;
|
|
76
|
+
const addBundleHelpTargetId =
|
|
77
|
+
invocationMode === "add" && targetType === "bundle" && targetId && isHelpToken(thirdToken)
|
|
78
|
+
? targetId
|
|
79
|
+
: "";
|
|
80
|
+
|
|
81
|
+
if (addPackageHelpTargetId) {
|
|
82
|
+
const appRoot = await resolveAppRootFromCwd(cwd);
|
|
83
|
+
const packageRegistry = await loadPackageRegistry();
|
|
84
|
+
const appLocalRegistry = await loadAppLocalPackageRegistry(appRoot);
|
|
85
|
+
const combinedPackageRegistry = mergePackageRegistries(packageRegistry, appLocalRegistry);
|
|
86
|
+
const resolvedPackageId = await resolvePackageIdFromRegistryOrNodeModules({
|
|
87
|
+
appRoot,
|
|
88
|
+
packageRegistry: combinedPackageRegistry,
|
|
89
|
+
packageIdInput: addPackageHelpTargetId
|
|
90
|
+
});
|
|
91
|
+
if (!resolvedPackageId) {
|
|
92
|
+
throw createCliError(
|
|
93
|
+
`Unknown package: ${addPackageHelpTargetId}. Install an external module first (npm install ${addPackageHelpTargetId}) if you want to adopt it into lock.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await hydratePackageRegistryFromInstalledNodeModules({
|
|
98
|
+
appRoot,
|
|
99
|
+
packageRegistry: combinedPackageRegistry,
|
|
100
|
+
seedPackageIds: [resolvedPackageId]
|
|
101
|
+
});
|
|
102
|
+
const packageEntry = combinedPackageRegistry.get(resolvedPackageId);
|
|
103
|
+
if (!packageEntry) {
|
|
104
|
+
throw createCliError(`Unknown package: ${addPackageHelpTargetId}`);
|
|
105
|
+
}
|
|
106
|
+
const packageKind = resolvePackageKind(packageEntry);
|
|
107
|
+
if (packageKind === "generator") {
|
|
108
|
+
throw createCliError(
|
|
109
|
+
`Package ${resolvedPackageId} is a generator. Use: jskit generate ${resolvedPackageId}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
renderAddPackageHelp({
|
|
113
|
+
io,
|
|
114
|
+
packageEntry,
|
|
115
|
+
packageIdInput: addPackageHelpTargetId,
|
|
116
|
+
json: options.json
|
|
117
|
+
});
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (addBundleHelpTargetId) {
|
|
122
|
+
const bundleRegistry = await loadBundleRegistry();
|
|
123
|
+
const bundle = bundleRegistry.get(addBundleHelpTargetId);
|
|
124
|
+
if (!bundle) {
|
|
125
|
+
throw createCliError(`Unknown bundle: ${addBundleHelpTargetId}`);
|
|
126
|
+
}
|
|
127
|
+
renderAddBundleHelp({
|
|
128
|
+
io,
|
|
129
|
+
bundleId: addBundleHelpTargetId,
|
|
130
|
+
bundle,
|
|
131
|
+
json: options.json
|
|
132
|
+
});
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
37
135
|
|
|
38
136
|
if (!targetType || !targetId) {
|
|
39
137
|
if (invocationMode === "generate") {
|
|
@@ -222,6 +320,21 @@ async function runPackageAddCommand(ctx = {}, { positional, options, cwd, io })
|
|
|
222
320
|
|
|
223
321
|
const finalResolvedPackageIds = sortStrings([...resolvedPackageIds, ...adoptedPackageIds]);
|
|
224
322
|
|
|
323
|
+
const requestedPlacementComponentToken = String(options?.inlineOptions?.["placement-component-token"] || "").trim();
|
|
324
|
+
if (
|
|
325
|
+
invocationMode === "generate" &&
|
|
326
|
+
targetType === "package" &&
|
|
327
|
+
resolvedTargetPackageId === "@jskit-ai/crud-ui-generator" &&
|
|
328
|
+
requestedPlacementComponentToken === TAB_LINK_COMPONENT_TOKEN
|
|
329
|
+
) {
|
|
330
|
+
await ensureLocalMainTabLinkItemProvisioning({
|
|
331
|
+
appRoot,
|
|
332
|
+
createCliError,
|
|
333
|
+
dryRun: options.dryRun === true,
|
|
334
|
+
touchedFiles
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
225
338
|
const touchedFileList = sortStrings([...touchedFiles]);
|
|
226
339
|
const successLabel = invocationMode === "generate"
|
|
227
340
|
? "Generated with"
|