create-procode 0.0.0-stage → 1.0.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.
Files changed (54) hide show
  1. package/README.md +240 -2
  2. package/bin/create-procode.mjs +9 -0
  3. package/package.json +30 -4
  4. package/src/args.mjs +139 -0
  5. package/src/cli.mjs +392 -0
  6. package/src/deps.mjs +195 -0
  7. package/src/log.mjs +23 -0
  8. package/src/prompt.mjs +60 -0
  9. package/src/scaffold.mjs +104 -0
  10. package/templates/base/README.md.tmpl +183 -0
  11. package/templates/base/_dotenv.local.example.tmpl +8 -0
  12. package/templates/base/_dotenv.tmpl +41 -0
  13. package/templates/base/_doteslintrc.cjs.tmpl +125 -0
  14. package/templates/base/_dotgitignore.tmpl +32 -0
  15. package/templates/base/_dotnvmrc.tmpl +1 -0
  16. package/templates/base/_dotvscode/extensions.json +3 -0
  17. package/templates/base/index.html.tmpl +18 -0
  18. package/templates/base/package.json.tmpl +15 -0
  19. package/templates/base/public/favicon.svg +4 -0
  20. package/templates/base/src/AdapterProvider/AdapterFactory.ts.tmpl +21 -0
  21. package/templates/base/src/AdapterProvider/SampleContacts.ts.tmpl +136 -0
  22. package/templates/base/src/CustomAction.ts.tmpl +47 -0
  23. package/templates/base/src/CustomAuthorizationProvider.ts.tmpl +77 -0
  24. package/templates/base/src/Factory/Actions/myAction.ts.tmpl +51 -0
  25. package/templates/base/src/Factory/Actions/sampleContacts.ts.tmpl +244 -0
  26. package/templates/base/src/Factory/Actions/setField.ts.tmpl +20 -0
  27. package/templates/base/src/Factory/CustomRuleEffectFactory.ts.tmpl +30 -0
  28. package/templates/base/src/Factory/CustomValidationFactory.tsx.tmpl +24 -0
  29. package/templates/base/src/Factory/CustomWidgetFactory.tsx.tmpl +43 -0
  30. package/templates/base/src/Factory/RuleEffects/readOnlyEffect.ts.tmpl +27 -0
  31. package/templates/base/src/Factory/Validations/myValidation.ts.tmpl +52 -0
  32. package/templates/base/src/Factory/Widgets/MyWidget/index.tsx.tmpl +101 -0
  33. package/templates/base/src/Factory/Widgets/Notification/index.tsx.tmpl +53 -0
  34. package/templates/base/src/Factory/Widgets/Notification/notifiFunc.ts.tmpl +9 -0
  35. package/templates/base/src/Factory/Widgets/SectionNav/index.tsx +193 -0
  36. package/templates/base/src/Factory/Widgets/SectionNav/style.scss +139 -0
  37. package/templates/base/src/Module/Sample/Contacts.json.tmpl +965 -0
  38. package/templates/base/src/Module/index.ts.tmpl +26 -0
  39. package/templates/base/src/Screens/index.tsx.tmpl +45 -0
  40. package/templates/base/src/Utils/errorMessages.ts.tmpl +35 -0
  41. package/templates/base/src/Utils/fieldErrorMapper.ts.tmpl +46 -0
  42. package/templates/base/src/Utils/logger.ts.tmpl +21 -0
  43. package/templates/base/src/Utils/responseFormatter.ts.tmpl +39 -0
  44. package/templates/base/src/Utils/storages.ts.tmpl +35 -0
  45. package/templates/base/src/assets/styles/index.scss.tmpl +146 -0
  46. package/templates/base/src/constant.ts.tmpl +53 -0
  47. package/templates/base/src/icons.ts.tmpl +17 -0
  48. package/templates/base/src/localizationResolver.ts.tmpl +48 -0
  49. package/templates/base/src/main.tsx.tmpl +136 -0
  50. package/templates/base/src/routes.ts.tmpl +20 -0
  51. package/templates/base/src/vite-env.d.ts.tmpl +20 -0
  52. package/templates/base/tsconfig.json.tmpl +30 -0
  53. package/templates/base/tsconfig.node.json.tmpl +11 -0
  54. package/templates/base/vite.config.ts.tmpl +61 -0
package/src/cli.mjs ADDED
@@ -0,0 +1,392 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { parseArgs, helpText } from "./args.mjs";
7
+ import { ask, confirm, closePrompt, isInteractive } from "./prompt.mjs";
8
+ import { c, log, step, ok, warn, info } from "./log.mjs";
9
+ import { buildDependencies, buildDevDependencies, PINNED_SET, resolveDependencySet } from "./deps.mjs";
10
+ import {
11
+ BASE_DIR,
12
+ copyTree,
13
+ inspectTarget,
14
+ toPackageName,
15
+ toBasePath,
16
+ writeJson,
17
+ } from "./scaffold.mjs";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const PKG = JSON.parse(fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8"));
21
+
22
+ const DEFAULTS = {
23
+ name: "procode-app",
24
+ appCode: "MYAPP",
25
+ apiUrl: "https://qa.example.com/lowcodev2/api/{APP_CODE}/",
26
+ authUrl: "https://localhost:7016/token",
27
+ toolUrl: "https://lowcodeapi-399313.uc.r.appspot.com/",
28
+ env: "InDraft",
29
+ };
30
+
31
+ function detectPackageManager() {
32
+ const ua = process.env.npm_config_user_agent ?? "";
33
+ for (const pm of ["pnpm", "yarn", "bun"]) if (ua.startsWith(pm)) return pm;
34
+ return "npm";
35
+ }
36
+
37
+ function runCommand(cmd, args, cwd) {
38
+ const res = spawnSync(cmd, args, {
39
+ cwd,
40
+ stdio: "inherit",
41
+ // On Windows npm/pnpm/yarn/git are .cmd shims, which spawn cannot exec directly.
42
+ shell: process.platform === "win32",
43
+ });
44
+ if (res.error) throw res.error;
45
+ return res.status === 0;
46
+ }
47
+
48
+ const VALID_APP_CODE = /^[A-Za-z][A-Za-z0-9_]*$/;
49
+
50
+ async function collectOptions(opts, positionals) {
51
+ const nonInteractive = opts.yes || !isInteractive();
52
+ const given = (key) => Object.prototype.hasOwnProperty.call(opts, key);
53
+
54
+ // Every question below is skippable: press Enter to take the shown default,
55
+ // or pass the matching flag and it is never asked at all.
56
+
57
+ // ---- project name -------------------------------------------------------
58
+ let name = opts.name ?? positionals[0];
59
+ if (!name) {
60
+ if (nonInteractive) {
61
+ name = DEFAULTS.name;
62
+ } else {
63
+ name = await ask("Project name?", DEFAULTS.name, (v) =>
64
+ v.includes("/") || v.includes("\\") ? "Use a plain directory name, not a path." : null,
65
+ );
66
+ }
67
+ }
68
+ const packageName = toPackageName(name);
69
+
70
+ // ---- application code ---------------------------------------------------
71
+ // The core resolves the adapter global by NAME as "getAdapter" +
72
+ // VITE_APPLICATION_CODE. If the two diverge every adapter silently falls back
73
+ // to a dummy and no screen ever has data - so this is validated, not free text.
74
+ const codeFromName = packageName.replace(/[^a-zA-Z0-9]/g, "").toUpperCase() || DEFAULTS.appCode;
75
+ let appCode = opts["app-code"];
76
+ if (!appCode) {
77
+ appCode = nonInteractive
78
+ ? codeFromName
79
+ : await ask("Application code (VITE_APPLICATION_CODE)?", codeFromName, (v) =>
80
+ VALID_APP_CODE.test(v)
81
+ ? null
82
+ : "Must start with a letter and hold only letters, digits and underscores - it becomes part of a JS identifier.",
83
+ );
84
+ }
85
+ if (!VALID_APP_CODE.test(appCode)) {
86
+ throw new Error(
87
+ "--app-code \"" +
88
+ appCode +
89
+ "\" is not usable: it becomes the global name getAdapter" +
90
+ appCode +
91
+ ", so it must start with a letter and hold only letters, digits and underscores.",
92
+ );
93
+ }
94
+
95
+ // ---- base path ----------------------------------------------------------
96
+ let basePath = opts.base;
97
+ if (!basePath) {
98
+ basePath = nonInteractive
99
+ ? "/" + packageName
100
+ : await ask("Base path (Vite base + VITE_HOMEPAGE)?", "/" + packageName);
101
+ }
102
+ basePath = toBasePath(basePath);
103
+
104
+ // ---- service URLs -------------------------------------------------------
105
+ const apiUrl = (opts["api-url"] ?? DEFAULTS.apiUrl).replace(/\{APP_CODE\}/g, appCode);
106
+ const authUrl = opts["auth-url"] ?? DEFAULTS.authUrl;
107
+ const toolUrl = opts["tool-url"] ?? DEFAULTS.toolUrl;
108
+ const projectEnv = opts.env ?? DEFAULTS.env;
109
+
110
+ // ---- booleans -----------------------------------------------------------
111
+ const pm = opts.pm ?? detectPackageManager();
112
+ const withSample = given("sample") ? opts.sample : true;
113
+ const withLint = given("lint") ? opts.lint : true;
114
+ const latest = given("latest") ? opts.latest : true;
115
+
116
+ let install = given("install") ? opts.install : null;
117
+ if (install === null) {
118
+ install = nonInteractive ? true : await confirm("Run " + pm + " install now?", true);
119
+ }
120
+
121
+ let git = given("git") ? opts.git : null;
122
+ if (git === null) git = nonInteractive ? true : await confirm("Initialise a git repository?", true);
123
+
124
+ return {
125
+ name,
126
+ packageName,
127
+ appCode,
128
+ basePath,
129
+ apiUrl,
130
+ authUrl,
131
+ toolUrl,
132
+ projectEnv,
133
+ pm,
134
+ install,
135
+ git,
136
+ withSample,
137
+ withLint,
138
+ latest,
139
+ force: Boolean(opts.force),
140
+ };
141
+ }
142
+
143
+ function buildTokens(o, set) {
144
+ return {
145
+ APP_NAME: o.name,
146
+ PKG_NAME: o.packageName,
147
+ APP_CODE: o.appCode,
148
+ BASE_PATH: o.basePath, // "/myapp"
149
+ BASE_PATH_SLASH: o.basePath.endsWith("/") ? o.basePath : o.basePath + "/", // "/myapp/"
150
+ API_BASE_URL: o.apiUrl,
151
+ AUTH_URL: o.authUrl,
152
+ TOOL_BASE_URL: o.toolUrl,
153
+ PROJECT_ENV: o.projectEnv,
154
+ GENERATOR_VERSION: PKG.version,
155
+ // Rendered from the same set package.json is written from, so the
156
+ // generated README cannot drift from the versions it pins.
157
+ REACT_RANGE: set.peers.react ?? "",
158
+ PROCODE_VERSIONS: Object.entries(set.procode)
159
+ .map(([name, range]) => "| `" + name + "` | " + range + " |")
160
+ .join("\n"),
161
+ };
162
+ }
163
+
164
+ /** Remove the sample screen and unwire it, when --no-sample was passed. */
165
+ function stripSample(root) {
166
+ fs.rmSync(path.join(root, "src", "Module", "Sample"), { recursive: true, force: true });
167
+ fs.rmSync(path.join(root, "src", "AdapterProvider", "SampleContacts.ts"), { force: true });
168
+ fs.rmSync(path.join(root, "src", "Factory", "Actions", "sampleContacts.ts"), { force: true });
169
+
170
+ // The registry keeps the two generic actions and the custom-widget pair;
171
+ // only the sample-screen stand-ins go.
172
+ fs.writeFileSync(
173
+ path.join(root, "src", "CustomAction.ts"),
174
+ [
175
+ 'import { setField } from "./Factory/Actions/setField";',
176
+ 'import { myAction, suggestReferenceCode } from "./Factory/Actions/myAction";',
177
+ "",
178
+ "/**",
179
+ " * Extension point 4 of 5: custom actions, resolved by a CUS_-prefixed key.",
180
+ " *",
181
+ " * Resolution is custom -> template -> standard. A key no tier knows is logged",
182
+ ' * and toasted as "This action is not available". A Map, not an object literal:',
183
+ ' * on a plain object, actionMap["constructor"] returns a function.',
184
+ " *",
185
+ " * Write a custom action only for logic none of the standard keys expresses,",
186
+ " * and NEVER to make an HTTP call.",
187
+ " */",
188
+ "class CustomActionFactory {",
189
+ " private actionMap = new Map<string, (props: any, ...args: any[]) => any>([",
190
+ ' ["CUS_SET_FIELD", setField],',
191
+ ' ["CUS_MY_ACTION", myAction],',
192
+ ' ["CUS_SUGGEST_CODE", suggestReferenceCode],',
193
+ " ]);",
194
+ "",
195
+ " getAction(key: string) {",
196
+ " return this.actionMap.get(key) ?? null; // never throw",
197
+ " }",
198
+ "}",
199
+ "",
200
+ "export default CustomActionFactory;",
201
+ "",
202
+ ].join("\n"),
203
+ "utf8",
204
+ );
205
+
206
+ fs.writeFileSync(
207
+ path.join(root, "src", "Module", "index.ts"),
208
+ [
209
+ "/**",
210
+ " * slug -> schema. The slug is the URL segment after the Vite base; the schema",
211
+ " * is the JSON the core renders. A slug with no branch here is page-not-found.",
212
+ " */",
213
+ "export const getSchemaBySlugService = (_slug: string, _appCode: string): any => {",
214
+ " return null;",
215
+ "};",
216
+ "",
217
+ ].join("\n"),
218
+ "utf8",
219
+ );
220
+
221
+ fs.writeFileSync(
222
+ path.join(root, "src", "AdapterProvider", "AdapterFactory.ts"),
223
+ [
224
+ "/**",
225
+ " * screenId -> adapter. Keyed by the `screenId` INSIDE the schema JSON, never by",
226
+ " * the slug. Read the id out of the file; never guess it.",
227
+ " *",
228
+ " * Registered onto window as `getAdapter<APPLICATION_CODE>` by main.tsx.",
229
+ " */",
230
+ "export const getAdapter = (_screenId: string): any => {",
231
+ " return null;",
232
+ "};",
233
+ "",
234
+ ].join("\n"),
235
+ "utf8",
236
+ );
237
+ }
238
+
239
+ export async function run(argv) {
240
+ const { opts, positionals } = parseArgs(argv);
241
+
242
+ if (opts.help) {
243
+ log(helpText(PKG.version));
244
+ return;
245
+ }
246
+ if (opts.version) {
247
+ log(PKG.version);
248
+ return;
249
+ }
250
+
251
+ log("");
252
+ log(" " + c.bold(c.magenta("ProCode")) + " " + c.dim("project generator") + " " + c.dim("v" + PKG.version));
253
+ log(" " + c.dim("React + TypeScript + Vite, wired to the four ProCode libraries"));
254
+ log("");
255
+
256
+ let o;
257
+ try {
258
+ o = await collectOptions(opts, positionals);
259
+ } finally {
260
+ closePrompt();
261
+ }
262
+
263
+ const targetRoot = path.resolve(process.cwd(), o.name);
264
+
265
+ // ---- target directory ---------------------------------------------------
266
+ const target = inspectTarget(targetRoot);
267
+ if (target.notADirectory) throw new Error(targetRoot + " exists and is not a directory.");
268
+ if (!target.empty && !o.force) {
269
+ const shown = target.conflicts.slice(0, 5).join(", ") + (target.conflicts.length > 5 ? ", ..." : "");
270
+ throw new Error(
271
+ targetRoot +
272
+ " is not empty (" +
273
+ shown +
274
+ ").\n Pick another name, empty the directory, or pass --force to scaffold into it anyway.",
275
+ );
276
+ }
277
+
278
+ log("");
279
+ step("Scaffolding into " + c.bold(targetRoot));
280
+ info("package " + o.packageName);
281
+ info("app code " + o.appCode + " " + c.dim("(global: getAdapter" + o.appCode + ")"));
282
+ info("base path " + o.basePath);
283
+ info("api " + o.apiUrl);
284
+ log("");
285
+
286
+ // ---- library versions ---------------------------------------------------
287
+ // The newest published ProCode packages, with the peers and bundled packages
288
+ // those versions declare. Any failure (offline, a registry without the
289
+ // packages, a timeout) falls back to the set this generator was tested with.
290
+ let set = PINNED_SET;
291
+ if (o.latest) {
292
+ step("Resolving the latest ProCode library versions from the npm registry");
293
+ try {
294
+ set = await resolveDependencySet();
295
+ ok(
296
+ Object.entries(set.procode)
297
+ .map(([name, range]) => name.replace(/^procode-(lowcode-)?/, "") + " " + range.replace(/^\^/, ""))
298
+ .join(", "),
299
+ );
300
+ } catch (err) {
301
+ warn("Could not resolve the latest versions (" + (err?.message ?? err) + ").");
302
+ info("Using the versions this generator was tested with. Pass --no-latest to skip the lookup.");
303
+ }
304
+ } else {
305
+ info("using the tested library versions (--no-latest)");
306
+ }
307
+ log("");
308
+
309
+ // ---- files --------------------------------------------------------------
310
+ const tokens = buildTokens(o, set);
311
+ const created = copyTree(BASE_DIR, targetRoot, tokens);
312
+ ok(created.length + " project files written");
313
+
314
+ if (!o.withSample) {
315
+ stripSample(targetRoot);
316
+ info("sample module screen omitted (--no-sample)");
317
+ }
318
+
319
+ if (!o.withLint) {
320
+ fs.rmSync(path.join(targetRoot, ".eslintrc.cjs"), { force: true });
321
+ // Its only recommendation is the ESLint extension.
322
+ fs.rmSync(path.join(targetRoot, ".vscode"), { recursive: true, force: true });
323
+ }
324
+
325
+ // ---- package.json -------------------------------------------------------
326
+ const pkgFile = path.join(targetRoot, "package.json");
327
+ const generated = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
328
+ generated.dependencies = buildDependencies(set);
329
+ generated.devDependencies = buildDevDependencies({ lint: o.withLint, set });
330
+ if (!o.withLint) delete generated.scripts.lint;
331
+ generated._procodeBundled = {
332
+ note:
333
+ "Bundled INSIDE the four ProCode packages. Do not add them here and do not bump them independently - react-redux in particular must stay on 8.x, or the core's store becomes invisible and every screen renders blank.",
334
+ ...set.bundled,
335
+ };
336
+ writeJson(pkgFile, generated);
337
+ ok(
338
+ "package.json written (" + (set.source === "latest" ? "latest" : "tested") + " versions): " +
339
+ Object.keys(generated.dependencies).length +
340
+ " dependencies, " +
341
+ Object.keys(generated.devDependencies).length +
342
+ " dev",
343
+ );
344
+
345
+ // ---- git ----------------------------------------------------------------
346
+ if (o.git) {
347
+ if (fs.existsSync(path.join(targetRoot, ".git"))) {
348
+ info("git repository already present, skipping init");
349
+ } else if (runCommand("git", ["init", "-q"], targetRoot)) {
350
+ ok("git repository initialised");
351
+ } else {
352
+ warn("git init failed - carrying on without it");
353
+ }
354
+ }
355
+
356
+ // ---- install ------------------------------------------------------------
357
+ let installed = false;
358
+ if (o.install) {
359
+ log("");
360
+ step("Installing dependencies with " + o.pm + " - this takes a few minutes");
361
+ log("");
362
+ installed = runCommand(o.pm, ["install"], targetRoot);
363
+ log("");
364
+ if (installed) ok("dependencies installed");
365
+ else warn(o.pm + " install failed - run it yourself in " + o.name + " and read the error");
366
+ }
367
+
368
+ // ---- what to do next ----------------------------------------------------
369
+ const runCmd = o.pm === "npm" ? "npm run" : o.pm;
370
+ log("");
371
+ log(" " + c.green(c.bold("Done.")) + " Next:");
372
+ log("");
373
+ log(" " + c.cyan("cd") + " " + o.name);
374
+ if (!installed) log(" " + c.cyan(o.pm) + " install");
375
+ log(" " + c.cyan(runCmd) + " dev");
376
+ log("");
377
+ if (o.withSample) {
378
+ log(" The sample screen is at " + c.bold("http://localhost:5173" + o.basePath + "/sample"));
379
+ log(" " + c.dim(o.basePath + " is the Vite base, /sample is the slug in src/Module/index.ts"));
380
+ log("");
381
+ }
382
+ log(" " + c.yellow("Before it will load real data:"));
383
+ log(" 1. Point " + c.bold("VITE_CENTRAL_SERVICE_BASE_URL") + " in .env at your environment.");
384
+ log(
385
+ " 2. Put a dev token in " +
386
+ c.bold(".env.local") +
387
+ " as VITE_HARDCODED_TOKEN, or sign in for the AuthToken cookie.",
388
+ );
389
+ log(" " + c.dim(".env.local is gitignored. Never commit a token to .env."));
390
+ log(" 3. " + c.bold("README.md") + " holds the rules - schema first, no axios under src/ outside main.tsx.");
391
+ log("");
392
+ }
package/src/deps.mjs ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Dependency set for a generated ProCode app.
3
+ *
4
+ * By default the CLI resolves it at scaffold time with resolveDependencySet():
5
+ * the newest published version of each ProCode package, plus the peers and
6
+ * bundled packages THOSE versions declare — so an app scaffolded after a
7
+ * library release gets that release and the peers it was built against, with
8
+ * no generator update. The constants below are the fallback (--no-latest, or
9
+ * the registry is unreachable): the set verified against a running ProCode
10
+ * host app (vsbackoffice / WorksLogicERP, 2026-10-08) at core 2.5.0 /
11
+ * kendo-widget 2.11.0 / vs-template 2.13.1 / vs-theme 2.6.0.
12
+ *
13
+ * Three rules encoded here, each of which is a hard failure if broken:
14
+ *
15
+ * 1. React stays on 18. All three runtime libraries declare `react ^18.2.0`
16
+ * as a peer; React 19 installs cleanly and then fails at runtime.
17
+ * 2. `react-redux` stays on 8.x. On 9.x you get two Provider contexts, the
18
+ * core's store becomes invisible to your components, and every screen
19
+ * renders blank with nothing in the console.
20
+ * 3. `@progress/kendo-theme-default` is a MISSING peer declaration, not an
21
+ * optional extra — the widget library's SCSS imports it by name, so
22
+ * leaving it out is a hard build failure on the first `npm run dev`.
23
+ */
24
+
25
+ /** The four ProCode packages. */
26
+ export const PROCODE = {
27
+ "procode-lowcode-core": "^2.5.0",
28
+ "procode-lowcode-kendo-widget": "^2.11.0",
29
+ "procode-vs-template": "^2.13.1",
30
+ "procode-vs-theme": "^2.6.0",
31
+ };
32
+
33
+ /** Every peer the four packages declare. peerDependenciesMeta is empty on all
34
+ * four — nothing here is optional. */
35
+ export const DECLARED_PEERS = {
36
+ react: "^18.2.0",
37
+ "react-dom": "^18.2.0",
38
+ "react-router-dom": "^6.22.3",
39
+ axios: "^1.6.7",
40
+ "@progress/kendo-data-query": "^1.7.1",
41
+ "@progress/kendo-svg-icons": "^4.5.0",
42
+ "@progress/kendo-react-buttons": "^12.1.0",
43
+ "@progress/kendo-react-charts": "^12.1.0",
44
+ "@progress/kendo-react-common": "^12.1.0",
45
+ "@progress/kendo-react-conversational-ui": "^12.1.0",
46
+ "@progress/kendo-react-data-tools": "^12.1.0",
47
+ "@progress/kendo-react-dateinputs": "^12.1.0",
48
+ "@progress/kendo-react-dialogs": "^12.1.0",
49
+ "@progress/kendo-react-dropdowns": "^12.1.0",
50
+ "@progress/kendo-react-editor": "^12.1.0",
51
+ "@progress/kendo-react-gauges": "^12.1.0",
52
+ "@progress/kendo-react-grid": "^12.1.0",
53
+ "@progress/kendo-react-inputs": "^12.1.0",
54
+ "@progress/kendo-react-layout": "^12.1.0",
55
+ "@progress/kendo-react-listview": "^12.1.0",
56
+ };
57
+
58
+ /** Required at runtime or build time, but NOT declared as a peer by anything. */
59
+ export const UNDECLARED_BUT_REQUIRED = {
60
+ "@progress/kendo-theme-default": "^12.1.0",
61
+ "@progress/kendo-licensing": "^1.11.3",
62
+ "@progress/kendo-react-labels": "^12.1.0",
63
+ "@progress/kendo-intl": "^3.1.1",
64
+ "react-toastify": "^11.0.5",
65
+ sass: "^1.38.0",
66
+ };
67
+
68
+ export const DEV_DEPS = {
69
+ // vite.config.ts imports node:url.
70
+ "@types/node": "^24.9.2",
71
+ "@types/react": "^18.2.43",
72
+ "@types/react-dom": "^18.2.17",
73
+ "@vitejs/plugin-react": "^4.2.1",
74
+ typescript: "^5.2.2",
75
+ vite: "^5.2.0",
76
+ };
77
+
78
+ export const LINT_DEV_DEPS = {
79
+ eslint: "^8.57.0",
80
+ "@typescript-eslint/eslint-plugin": "^7.2.0",
81
+ "@typescript-eslint/parser": "^7.2.0",
82
+ "eslint-plugin-react-hooks": "^4.6.0",
83
+ "eslint-plugin-react-refresh": "^0.4.6",
84
+ };
85
+
86
+ /** Bundled INSIDE the libraries. Listed for documentation only — do not add
87
+ * them to a generated package.json, and never bump them independently. */
88
+ export const BUNDLED_DO_NOT_ADD = {
89
+ "procode-lowcode-core": {
90
+ "@reduxjs/toolkit": "^1.9.1",
91
+ "react-redux": "^8.0.5",
92
+ "redux-saga": "^1.2.3",
93
+ uuid: "^11.1.1",
94
+ },
95
+ "procode-lowcode-kendo-widget": { lodash: "^4.17.21", uuid: "^11.1.1" },
96
+ "procode-vs-template": {
97
+ lodash: "^4.17.21",
98
+ "react-beautiful-dnd": "^13.1.1",
99
+ uuid: "^11.1.1",
100
+ },
101
+ "procode-vs-theme": {},
102
+ };
103
+
104
+ const sortKeys = (obj) =>
105
+ Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
106
+
107
+ /** The tested set, used with --no-latest or when the registry is unreachable. */
108
+ export const PINNED_SET = {
109
+ procode: PROCODE,
110
+ peers: DECLARED_PEERS,
111
+ extras: UNDECLARED_BUT_REQUIRED,
112
+ bundled: BUNDLED_DO_NOT_ADD,
113
+ source: "pinned",
114
+ };
115
+
116
+ /** Lowest version a range admits, as [major, minor, patch]; [0,0,0] if none. */
117
+ const floorOf = (range) => (String(range).match(/(\d+)\.(\d+)\.(\d+)/) ?? [0, 0, 0, 0]).slice(1).map(Number);
118
+ const newer = (a, b) => {
119
+ const [x, y] = [floorOf(a), floorOf(b)];
120
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i] ? a : b;
121
+ return a;
122
+ };
123
+
124
+ /**
125
+ * Ask the registry for the newest published version of each ProCode package,
126
+ * and derive everything else from those manifests:
127
+ *
128
+ * peers every peerDependency the four declare; where two declare the same
129
+ * peer, the range with the higher floor wins (vs-template wants
130
+ * react-router-dom ^6.22.3 where core wants ^6.6.1).
131
+ * extras required but undeclared. kendo-theme-default and kendo-react-labels
132
+ * follow the kendo-react range the widget library declares — the
133
+ * theme's major must match the components' major. The rest stay on
134
+ * their tested pins.
135
+ * bundled each package's own dependencies, for the _procodeBundled note.
136
+ *
137
+ * Uses the registry npm/npx was invoked with (npm_config_registry), so a
138
+ * mirror or proxy registry is honoured. Throws on any failure; the caller
139
+ * falls back to PINNED_SET.
140
+ */
141
+ export async function resolveDependencySet({ registry, timeoutMs = 10000 } = {}) {
142
+ const base = String(registry || process.env.npm_config_registry || "https://registry.npmjs.org/").replace(/\/?$/, "/");
143
+
144
+ const manifests = await Promise.all(
145
+ Object.keys(PROCODE).map(async (name) => {
146
+ const res = await fetch(base + name.replace("/", "%2f") + "/latest", {
147
+ headers: { accept: "application/json" },
148
+ signal: AbortSignal.timeout(timeoutMs),
149
+ });
150
+ if (!res.ok) throw new Error(name + ": HTTP " + res.status + " from " + base);
151
+ const manifest = await res.json();
152
+ if (typeof manifest?.version !== "string") throw new Error(name + ": no version in the registry response");
153
+ return manifest;
154
+ }),
155
+ );
156
+
157
+ const procode = {};
158
+ const peers = {};
159
+ const bundled = {};
160
+ for (const m of manifests) {
161
+ procode[m.name] = "^" + m.version;
162
+ bundled[m.name] = m.dependencies ?? {};
163
+ for (const [peer, range] of Object.entries(m.peerDependencies ?? {})) {
164
+ peers[peer] = peers[peer] ? newer(peers[peer], range) : range;
165
+ }
166
+ }
167
+
168
+ const extras = { ...UNDECLARED_BUT_REQUIRED };
169
+ const kendoReact = Object.entries(peers).find(([n]) => n.startsWith("@progress/kendo-react-"))?.[1];
170
+ if (kendoReact) {
171
+ extras["@progress/kendo-theme-default"] = kendoReact;
172
+ extras["@progress/kendo-react-labels"] = kendoReact;
173
+ }
174
+
175
+ return { procode, peers, extras, bundled, source: "latest" };
176
+ }
177
+
178
+ /** `set` is PINNED_SET or what resolveDependencySet() returned. */
179
+ export function buildDependencies(set = PINNED_SET) {
180
+ return sortKeys({ ...set.procode, ...set.peers, ...set.extras });
181
+ }
182
+
183
+ /**
184
+ * The React type packages follow the React major the libraries declare, so a
185
+ * future move off React 18 does not leave @types/react behind.
186
+ */
187
+ export function buildDevDependencies({ lint = true, set = PINNED_SET } = {}) {
188
+ const devDeps = { ...DEV_DEPS, ...(lint ? LINT_DEV_DEPS : {}) };
189
+ const reactMajor = floorOf(set.peers.react ?? "")[0];
190
+ if (reactMajor && reactMajor !== floorOf(DEV_DEPS["@types/react"])[0]) {
191
+ devDeps["@types/react"] = "^" + reactMajor + ".0.0";
192
+ devDeps["@types/react-dom"] = "^" + reactMajor + ".0.0";
193
+ }
194
+ return sortKeys(devDeps);
195
+ }
package/src/log.mjs ADDED
@@ -0,0 +1,23 @@
1
+ const useColor =
2
+ process.env.NO_COLOR === undefined &&
3
+ process.env.TERM !== "dumb" &&
4
+ process.stdout.isTTY !== false;
5
+
6
+ const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
7
+
8
+ export const c = {
9
+ bold: wrap("1"),
10
+ dim: wrap("2"),
11
+ red: wrap("31"),
12
+ green: wrap("32"),
13
+ yellow: wrap("33"),
14
+ blue: wrap("34"),
15
+ magenta: wrap("35"),
16
+ cyan: wrap("36"),
17
+ };
18
+
19
+ export const log = (msg = "") => console.log(msg);
20
+ export const step = (msg) => console.log(`${c.cyan("›")} ${msg}`);
21
+ export const ok = (msg) => console.log(`${c.green("✔")} ${msg}`);
22
+ export const warn = (msg) => console.log(`${c.yellow("!")} ${msg}`);
23
+ export const info = (msg) => console.log(` ${c.dim(msg)}`);
package/src/prompt.mjs ADDED
@@ -0,0 +1,60 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { c } from "./log.mjs";
4
+
5
+ let rl = null;
6
+ const getRl = () => (rl ??= readline.createInterface({ input, output }));
7
+
8
+ export const closePrompt = () => {
9
+ rl?.close();
10
+ rl = null;
11
+ };
12
+
13
+ export const isInteractive = () => Boolean(input.isTTY && output.isTTY);
14
+
15
+ /** Free-text question with a default. */
16
+ export async function ask(question, def, validate) {
17
+ for (;;) {
18
+ const hint = def ? c.dim(` (${def})`) : "";
19
+ const raw = (await getRl().question(`${c.cyan("?")} ${question}${hint} `)).trim();
20
+ const value = raw || def || "";
21
+ const problem = validate?.(value);
22
+ if (!problem) return value;
23
+ console.log(` ${c.red(problem)}`);
24
+ }
25
+ }
26
+
27
+ /** Yes/no question. */
28
+ export async function confirm(question, def = true) {
29
+ const hint = def ? "Y/n" : "y/N";
30
+ for (;;) {
31
+ const raw = (await getRl().question(`${c.cyan("?")} ${question} ${c.dim(`(${hint})`)} `))
32
+ .trim()
33
+ .toLowerCase();
34
+ if (!raw) return def;
35
+ if (["y", "yes"].includes(raw)) return true;
36
+ if (["n", "no"].includes(raw)) return false;
37
+ console.log(` ${c.red("Please answer y or n.")}`);
38
+ }
39
+ }
40
+
41
+ /** Single choice from a list of { value, label, hint }. */
42
+ export async function select(question, choices, defIndex = 0) {
43
+ console.log(`${c.cyan("?")} ${question}`);
44
+ choices.forEach((ch, i) => {
45
+ const mark = i === defIndex ? c.green("›") : " ";
46
+ const hint = ch.hint ? c.dim(` — ${ch.hint}`) : "";
47
+ console.log(` ${mark} ${c.bold(String(i + 1))}. ${ch.label}${hint}`);
48
+ });
49
+ for (;;) {
50
+ const raw = (
51
+ await getRl().question(` ${c.dim(`Choose 1-${choices.length}`)} ${c.dim(`(${defIndex + 1})`)} `)
52
+ ).trim();
53
+ if (!raw) return choices[defIndex].value;
54
+ const n = Number(raw);
55
+ if (Number.isInteger(n) && n >= 1 && n <= choices.length) return choices[n - 1].value;
56
+ const byValue = choices.find((ch) => ch.value === raw.toLowerCase());
57
+ if (byValue) return byValue.value;
58
+ console.log(` ${c.red(`Enter a number between 1 and ${choices.length}.`)}`);
59
+ }
60
+ }