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 +28 -2
- package/package.json +1 -1
- package/src/args.mjs +6 -0
- package/src/cli.mjs +123 -1
- package/src/deps.mjs +208 -195
- package/src/multiLanguage.mjs +409 -0
- package/templates/base/README.md.tmpl +5 -3
- package/templates/base/src/assets/styles/index.scss.tmpl +149 -146
- package/templates/base/vite.config.ts.tmpl +83 -54
- package/templates/multi-language/locales/fr/core.json +76 -0
- package/templates/multi-language/locales/fr/kendo.json +181 -0
- package/templates/multi-language/locales/fr/template.json +169 -0
- package/templates/multi-language/locales/fr/widget.json +65 -0
- package/templates/multi-language/locales/hi/core.json +76 -0
- package/templates/multi-language/locales/hi/kendo.json +181 -0
- package/templates/multi-language/locales/hi/template.json +169 -0
- package/templates/multi-language/locales/hi/widget.json +65 -0
- package/templates/multi-language/sample/fr.json +55 -0
- package/templates/multi-language/sample/hi.json +55 -0
- package/templates/tests/src/Module/schemas.test.ts +50 -0
- package/templates/tests-multi-language/src/multiLanguage/enText.ts +68 -0
- package/templates/tests-multi-language/src/multiLanguage/translations.test.ts +92 -0
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.
|
|
211
|
-
vs-template `^2.
|
|
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.
|
|
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(
|