create-procode 1.0.0 → 1.1.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/README.md CHANGED
@@ -42,6 +42,9 @@ npm create procode@latest myapp -- --app-code MYAPP --base /myapp --yes
42
42
  | `--no-git` | inits | Skip `git init` |
43
43
  | `--no-sample` | includes it | Skip the sample module screen (leaves the registries empty but wired) |
44
44
  | `--no-lint` | includes it | Skip ESLint config and its dev dependencies |
45
+ | `--no-tests` | includes them | Skip vitest and the wiring tests (`npm test`): every screen's slug registered and screenId unique; with multi-language, every translation key present in every language (`enText.ts` included) |
46
+ | `--multi-language` | asks; off with `--yes` | Switch on multi-language — a root screen with a language switch, translation files, the start-language hook ([below](#multi-language)) |
47
+ | `--languages <codes>` | `en,fr` | With `--multi-language` (and implies it): the languages offered, e.g. `en,fr,hi`. English is always included — it is the libraries' default text |
45
48
  | `--no-latest` | resolves latest | Use the tested library versions in `src/deps.mjs` instead of the newest published ones |
46
49
  | `-y, --yes` | — | Take defaults for everything not passed as a flag |
47
50
  | `-f, --force` | — | Scaffold into a non-empty directory |
@@ -130,6 +133,28 @@ against the registries.
130
133
 
131
134
  Pass `--no-sample` for empty-but-wired registries instead.
132
135
 
136
+ ### Tests and the build guard
137
+
138
+ By default the project gets **vitest** (`npm test`) with the one setting ProCode needs — `test.server.deps.inline: [/procode-/]` in `vite.config.ts`, without which a test importing a package fails to load — and `src/Module/schemas.test.ts`: every screen JSON under `src/Module` has a slug registered in `getSchemaBySlugService` (an unregistered one is a silent page-not-found) and a unique `screenId`. With multi-language it also gets `src/multiLanguage/enText.ts` (reads a reference's English back for assertions) and `translations.test.ts`: every `@t:` reference in a screen has English, every language has every key, no translation uses a `{placeholder}` its English lacks. `--no-tests` leaves all of it out.
139
+
140
+ `vite build` **refuses to run while `VITE_HARDCODED_TOKEN` is set** — it reads `.env.local` like `vite dev`, so a personal dev token would otherwise be compiled into `dist/`. `ALLOW_TOKEN_IN_BUILD=1` overrides it for a throwaway local build.
141
+
142
+ ### Multi-language
143
+
144
+ Asked as *Enable multi-language?* (default **no**), then *Languages?* (default `en,fr`); or `--multi-language --languages en,fr,hi`. ProCode multi-language (core 2.6.0+) is switched on by metadata, so "yes" writes:
145
+
146
+ | File | What it is |
147
+ |---|---|
148
+ | `src/Module/Header.json` | The root screen (`VITE_HEADER_SLUG`, loaded before any page): its `multiLanguage` block — `enabled`, `defaultLocale: "en"`, the `locales`, the `resources` (`{base}locales/{locale}/{namespace}.json`) — and a `LANGUAGESELECTOR` (segmented up to 4 languages, a menu above). Registered as the `header` slug in `src/Module/index.ts` |
149
+ | `public/locales/<code>/` | One folder per language: `core`, `widget`, `kendo`, `template` (the libraries' own text — empty for English, which is built in; **French and Hindi are included**, other languages show English until translated) and `header` (the language names) |
150
+ | `src/multiLanguage/index.ts` | `initialLocale` — the start language when the user has not chosen one (a `?lang=fr` link; extend it with a company or profile setting) |
151
+ | `src/main.tsx` | `multiLanguage: { initialLocale }` on the typed Core props |
152
+ | `src/Factory/CustomWidgetFactory.tsx` | every custom widget wrapped with `withWidgetMultiLanguage`, so a Dropdown it draws from `/widgets` and Kendo's own text follow the language |
153
+ | the sample screen | shows the header (`enableHeader: true`); after the install, `procode-extract-text` moves its text into `public/locales/en/sample.json`, and French and Hindi translations of it are written. With `--no-install` the command is printed instead |
154
+ | `README.md` | a *Multi-language* section: where everything lives, how to convert a screen, add a language, translate in code |
155
+
156
+ The generated README carries the rest. The smoke test scaffolds a multi-language app too and checks every file, key and wiring point above.
157
+
133
158
  ### What the generated project encodes
134
159
 
135
160
  These failure modes are prevented by construction rather than left to memory:
@@ -207,8 +232,9 @@ still need care:
207
232
 
208
233
  - **The tested fallback set**: used with `--no-latest` or when the registry is
209
234
  unreachable. Refresh it after a library release has been verified, then run the
210
- smoke test with `--install`. Current pins: core `^2.5.0`, kendo-widget `^2.11.0`,
211
- vs-template `^2.13.1`, vs-theme `^2.6.0` (the latest releases as of 2026-10-12).
235
+ smoke test with `--install`. Current pins: core `^2.7.0`, kendo-widget `^3.2.0`,
236
+ vs-template `^2.15.1`, vs-theme `^2.7.1` (the latest releases as of 2026-11-06), on KendoReact `^16.2.0` with
237
+ kendo-theme-default `^14.7.0`.
212
238
  - **`UNDECLARED_BUT_REQUIRED`**: packages the libraries need but do not declare
213
239
  as peers, so the lookup cannot find them. Add one here if a release starts
214
240
  needing a new one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-procode",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Scaffold a React + TypeScript + Vite app wired to the four ProCode low-code libraries (core, kendo-widget, vs-template, vs-theme).",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/src/args.mjs CHANGED
@@ -15,6 +15,9 @@ export const FLAGS = {
15
15
  git: { type: "boolean", value: true, desc: "Run git init (--no-git to skip)" },
16
16
  sample: { type: "boolean", value: true, desc: "Include the Sample module screen (--no-sample to skip)" },
17
17
  lint: { type: "boolean", value: true, desc: "Include ESLint config and dev dependencies" },
18
+ tests: { type: "boolean", value: true, desc: "Include vitest and the wiring tests — screen registration, and with multi-language, translation completeness (--no-tests to skip)" },
19
+ "multi-language": { type: "boolean", value: true, desc: "Switch on multi-language: root screen, language selector, translation files (--no-multi-language to skip)" },
20
+ languages: { type: "string", arg: "<codes>", desc: "With --multi-language: the languages offered, e.g. en,fr,hi (English is always included; default en,fr)" },
18
21
  latest: { type: "boolean", value: true, desc: "Use the newest published ProCode libraries and their peers (--no-latest: the tested versions)" },
19
22
  yes: { type: "boolean", value: true, short: "y", desc: "Accept defaults for everything not passed as a flag" },
20
23
  force: { type: "boolean", value: true, short: "f", desc: "Scaffold into a non-empty directory" },
@@ -125,6 +128,9 @@ EXAMPLES
125
128
  # everything on the command line, no prompts
126
129
  npx create-procode myapp --app-code MYAPP --base /myapp --yes
127
130
 
131
+ # with multi-language: English, French and Hindi, a language switch in the header
132
+ npx create-procode myapp --multi-language --languages en,fr,hi --yes
133
+
128
134
  # match an existing deployment, skip install
129
135
  npx create-procode vsbackoffice-next \
130
136
  --app-code LCBOV2 --base /lcbov2 \
package/src/cli.mjs CHANGED
@@ -14,7 +14,9 @@ import {
14
14
  toPackageName,
15
15
  toBasePath,
16
16
  writeJson,
17
+ TEMPLATES_DIR,
17
18
  } from "./scaffold.mjs";
19
+ import { applyMultiLanguage, applySampleTranslations, languagesProblem, parseLanguages } from "./multiLanguage.mjs";
18
20
 
19
21
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
22
  const PKG = JSON.parse(fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf8"));
@@ -26,6 +28,7 @@ const DEFAULTS = {
26
28
  authUrl: "https://localhost:7016/token",
27
29
  toolUrl: "https://lowcodeapi-399313.uc.r.appspot.com/",
28
30
  env: "InDraft",
31
+ languages: "en,fr",
29
32
  };
30
33
 
31
34
  function detectPackageManager() {
@@ -111,8 +114,28 @@ async function collectOptions(opts, positionals) {
111
114
  const pm = opts.pm ?? detectPackageManager();
112
115
  const withSample = given("sample") ? opts.sample : true;
113
116
  const withLint = given("lint") ? opts.lint : true;
117
+ const withTests = given("tests") ? opts.tests : true;
114
118
  const latest = given("latest") ? opts.latest : true;
115
119
 
120
+ // ---- multi-language -----------------------------------------------------
121
+ // Off unless asked for: a root screen, a language switch and translation
122
+ // files are only worth carrying when the app is going to be translated.
123
+ // `--languages` alone implies it.
124
+ let multiLanguage = given("multi-language") ? opts["multi-language"] : given("languages") ? true : null;
125
+ if (multiLanguage === null) {
126
+ multiLanguage = nonInteractive ? false : await confirm("Enable multi-language (a language switch and translation files)?", false);
127
+ }
128
+ let languages = ["en"];
129
+ if (multiLanguage) {
130
+ let raw = opts.languages;
131
+ if (!raw) {
132
+ raw = nonInteractive
133
+ ? DEFAULTS.languages
134
+ : await ask("Languages (comma-separated codes, English is always included)?", DEFAULTS.languages, languagesProblem);
135
+ }
136
+ languages = parseLanguages(raw);
137
+ }
138
+
116
139
  let install = given("install") ? opts.install : null;
117
140
  if (install === null) {
118
141
  install = nonInteractive ? true : await confirm("Run " + pm + " install now?", true);
@@ -135,7 +158,10 @@ async function collectOptions(opts, positionals) {
135
158
  git,
136
159
  withSample,
137
160
  withLint,
161
+ withTests,
138
162
  latest,
163
+ multiLanguage,
164
+ languages,
139
165
  force: Boolean(opts.force),
140
166
  };
141
167
  }
@@ -152,6 +178,12 @@ function buildTokens(o, set) {
152
178
  TOOL_BASE_URL: o.toolUrl,
153
179
  PROJECT_ENV: o.projectEnv,
154
180
  GENERATOR_VERSION: PKG.version,
181
+ // README's Scripts table gains the test row only when tests are generated.
182
+ TEST_SCRIPT_ROW: o.withTests
183
+ ? "\n| `npm test` | vitest: every screen's slug is registered and its screenId unique" +
184
+ (o.multiLanguage ? "; every translation key exists in every language" : "") +
185
+ " (`src/**/*.test.ts`) |"
186
+ : "",
155
187
  // Rendered from the same set package.json is written from, so the
156
188
  // generated README cannot drift from the versions it pins.
157
189
  REACT_RANGE: set.peers.react ?? "",
@@ -161,6 +193,40 @@ function buildTokens(o, set) {
161
193
  };
162
194
  }
163
195
 
196
+ /** Edit a generated file line by line, keeping its line endings. */
197
+ function stripLines(file, drop, map = (line) => line) {
198
+ const raw = fs.readFileSync(file, "utf8");
199
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
200
+ const out = raw.split(/\r?\n/).filter((line) => !drop(line)).map(map);
201
+ fs.writeFileSync(file, out.join(eol), "utf8");
202
+ }
203
+
204
+ const TESTS_START = /\/\/ @tests-start\s*$/;
205
+ const TESTS_END = /\/\/ @tests-end\s*$/;
206
+ const TESTS_LINE = /\s*\/\/ @tests\s*$/;
207
+
208
+ /** --no-tests: take vitest's block and its type reference out of vite.config.ts. */
209
+ function stripTestBlock(file) {
210
+ let inBlock = false;
211
+ stripLines(file, (line) => {
212
+ if (TESTS_START.test(line)) return (inBlock = true);
213
+ if (TESTS_END.test(line)) {
214
+ inBlock = false;
215
+ return true;
216
+ }
217
+ return inBlock || TESTS_LINE.test(line);
218
+ });
219
+ }
220
+
221
+ /** With tests: keep the block, drop only the markers. */
222
+ function keepTestBlock(file) {
223
+ stripLines(
224
+ file,
225
+ (line) => TESTS_START.test(line) || TESTS_END.test(line),
226
+ (line) => line.replace(TESTS_LINE, ""),
227
+ );
228
+ }
229
+
164
230
  /** Remove the sample screen and unwire it, when --no-sample was passed. */
165
231
  function stripSample(root) {
166
232
  fs.rmSync(path.join(root, "src", "Module", "Sample"), { recursive: true, force: true });
@@ -281,6 +347,7 @@ export async function run(argv) {
281
347
  info("app code " + o.appCode + " " + c.dim("(global: getAdapter" + o.appCode + ")"));
282
348
  info("base path " + o.basePath);
283
349
  info("api " + o.apiUrl);
350
+ info("languages " + (o.multiLanguage ? o.languages.join(", ") : c.dim("English only (multi-language off)")));
284
351
  log("");
285
352
 
286
353
  // ---- library versions ---------------------------------------------------
@@ -316,6 +383,30 @@ export async function run(argv) {
316
383
  info("sample module screen omitted (--no-sample)");
317
384
  }
318
385
 
386
+ if (o.multiLanguage) {
387
+ const { translatedLibraries } = applyMultiLanguage(targetRoot, { languages: o.languages, withSample: o.withSample });
388
+ ok("multi-language: " + o.languages.join(", ") + " — root screen src/Module/Header.json, files in public/locales/");
389
+ const untranslated = o.languages.filter((l) => l !== "en" && !translatedLibraries.includes(l));
390
+ if (untranslated.length) {
391
+ info(
392
+ "the libraries' own text is included for " +
393
+ (translatedLibraries.join(", ") || "no language but English") +
394
+ "; for " + untranslated.join(", ") + " it shows in English until public/locales/<code>/{core,widget,kendo,template}.json are translated",
395
+ );
396
+ }
397
+ }
398
+
399
+ // ---- tests --------------------------------------------------------------
400
+ const viteConfig = path.join(targetRoot, "vite.config.ts");
401
+ if (o.withTests) {
402
+ copyTree(path.join(TEMPLATES_DIR, "tests"), targetRoot, tokens);
403
+ if (o.multiLanguage) copyTree(path.join(TEMPLATES_DIR, "tests-multi-language"), targetRoot, tokens);
404
+ // The markers only delimit the block for --no-tests; drop them.
405
+ keepTestBlock(viteConfig);
406
+ } else {
407
+ stripTestBlock(viteConfig);
408
+ }
409
+
319
410
  if (!o.withLint) {
320
411
  fs.rmSync(path.join(targetRoot, ".eslintrc.cjs"), { force: true });
321
412
  // Its only recommendation is the ESLint extension.
@@ -326,8 +417,9 @@ export async function run(argv) {
326
417
  const pkgFile = path.join(targetRoot, "package.json");
327
418
  const generated = JSON.parse(fs.readFileSync(pkgFile, "utf8"));
328
419
  generated.dependencies = buildDependencies(set);
329
- generated.devDependencies = buildDevDependencies({ lint: o.withLint, set });
420
+ generated.devDependencies = buildDevDependencies({ lint: o.withLint, tests: o.withTests, set });
330
421
  if (!o.withLint) delete generated.scripts.lint;
422
+ if (o.withTests) generated.scripts.test = "vitest run";
331
423
  generated._procodeBundled = {
332
424
  note:
333
425
  "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.",
@@ -365,6 +457,28 @@ export async function run(argv) {
365
457
  else warn(o.pm + " install failed - run it yourself in " + o.name + " and read the error");
366
458
  }
367
459
 
460
+ // ---- multi-language: move the sample screen's text into the files ------
461
+ // The same versioned tool an app uses later (it ships with
462
+ // procode-lowcode-core), so it needs the install.
463
+ let sampleExtracted = false;
464
+ if (o.multiLanguage && o.withSample) {
465
+ if (installed) {
466
+ step("Moving the sample screen's text into public/locales/en/sample.json");
467
+ sampleExtracted = runCommand("npx", ["procode-extract-text", "sample", "src/Module/Sample/Contacts.json"], targetRoot);
468
+ if (sampleExtracted) {
469
+ const filled = applySampleTranslations(targetRoot, o.languages);
470
+ const rest = o.languages.filter((l) => l !== "en" && !filled.includes(l));
471
+ ok(
472
+ "sample screen text extracted" +
473
+ (filled.length ? " — translated into " + filled.join(", ") : "") +
474
+ (rest.length ? "; " + rest.join(", ") + " start in English — translate public/locales/{" + rest.join(",") + "}/sample.json" : ""),
475
+ );
476
+ } else {
477
+ warn("procode-extract-text failed - the sample screen stays in English; run it yourself (below)");
478
+ }
479
+ }
480
+ }
481
+
368
482
  // ---- what to do next ----------------------------------------------------
369
483
  const runCmd = o.pm === "npm" ? "npm run" : o.pm;
370
484
  log("");
@@ -379,6 +493,14 @@ export async function run(argv) {
379
493
  log(" " + c.dim(o.basePath + " is the Vite base, /sample is the slug in src/Module/index.ts"));
380
494
  log("");
381
495
  }
496
+ if (o.multiLanguage) {
497
+ log(" Multi-language: " + c.bold(o.languages.join(", ")) + " — switch in the header; " + c.dim("?lang=<code> picks the start language"));
498
+ if (o.withSample && !sampleExtracted) {
499
+ log(" Move the sample screen's text into the translation files once installed:");
500
+ log(" " + c.cyan("npx procode-extract-text sample src/Module/Sample/Contacts.json"));
501
+ }
502
+ log("");
503
+ }
382
504
  log(" " + c.yellow("Before it will load real data:"));
383
505
  log(" 1. Point " + c.bold("VITE_CENTRAL_SERVICE_BASE_URL") + " in .env at your environment.");
384
506
  log(