create-procode 0.0.0-stage → 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.
Files changed (68) hide show
  1. package/README.md +266 -2
  2. package/bin/create-procode.mjs +9 -0
  3. package/package.json +30 -4
  4. package/src/args.mjs +145 -0
  5. package/src/cli.mjs +514 -0
  6. package/src/deps.mjs +208 -0
  7. package/src/log.mjs +23 -0
  8. package/src/multiLanguage.mjs +409 -0
  9. package/src/prompt.mjs +60 -0
  10. package/src/scaffold.mjs +104 -0
  11. package/templates/base/README.md.tmpl +185 -0
  12. package/templates/base/_dotenv.local.example.tmpl +8 -0
  13. package/templates/base/_dotenv.tmpl +41 -0
  14. package/templates/base/_doteslintrc.cjs.tmpl +125 -0
  15. package/templates/base/_dotgitignore.tmpl +32 -0
  16. package/templates/base/_dotnvmrc.tmpl +1 -0
  17. package/templates/base/_dotvscode/extensions.json +3 -0
  18. package/templates/base/index.html.tmpl +18 -0
  19. package/templates/base/package.json.tmpl +15 -0
  20. package/templates/base/public/favicon.svg +4 -0
  21. package/templates/base/src/AdapterProvider/AdapterFactory.ts.tmpl +21 -0
  22. package/templates/base/src/AdapterProvider/SampleContacts.ts.tmpl +136 -0
  23. package/templates/base/src/CustomAction.ts.tmpl +47 -0
  24. package/templates/base/src/CustomAuthorizationProvider.ts.tmpl +77 -0
  25. package/templates/base/src/Factory/Actions/myAction.ts.tmpl +51 -0
  26. package/templates/base/src/Factory/Actions/sampleContacts.ts.tmpl +244 -0
  27. package/templates/base/src/Factory/Actions/setField.ts.tmpl +20 -0
  28. package/templates/base/src/Factory/CustomRuleEffectFactory.ts.tmpl +30 -0
  29. package/templates/base/src/Factory/CustomValidationFactory.tsx.tmpl +24 -0
  30. package/templates/base/src/Factory/CustomWidgetFactory.tsx.tmpl +43 -0
  31. package/templates/base/src/Factory/RuleEffects/readOnlyEffect.ts.tmpl +27 -0
  32. package/templates/base/src/Factory/Validations/myValidation.ts.tmpl +52 -0
  33. package/templates/base/src/Factory/Widgets/MyWidget/index.tsx.tmpl +101 -0
  34. package/templates/base/src/Factory/Widgets/Notification/index.tsx.tmpl +53 -0
  35. package/templates/base/src/Factory/Widgets/Notification/notifiFunc.ts.tmpl +9 -0
  36. package/templates/base/src/Factory/Widgets/SectionNav/index.tsx +193 -0
  37. package/templates/base/src/Factory/Widgets/SectionNav/style.scss +139 -0
  38. package/templates/base/src/Module/Sample/Contacts.json.tmpl +965 -0
  39. package/templates/base/src/Module/index.ts.tmpl +26 -0
  40. package/templates/base/src/Screens/index.tsx.tmpl +45 -0
  41. package/templates/base/src/Utils/errorMessages.ts.tmpl +35 -0
  42. package/templates/base/src/Utils/fieldErrorMapper.ts.tmpl +46 -0
  43. package/templates/base/src/Utils/logger.ts.tmpl +21 -0
  44. package/templates/base/src/Utils/responseFormatter.ts.tmpl +39 -0
  45. package/templates/base/src/Utils/storages.ts.tmpl +35 -0
  46. package/templates/base/src/assets/styles/index.scss.tmpl +149 -0
  47. package/templates/base/src/constant.ts.tmpl +53 -0
  48. package/templates/base/src/icons.ts.tmpl +17 -0
  49. package/templates/base/src/localizationResolver.ts.tmpl +48 -0
  50. package/templates/base/src/main.tsx.tmpl +136 -0
  51. package/templates/base/src/routes.ts.tmpl +20 -0
  52. package/templates/base/src/vite-env.d.ts.tmpl +20 -0
  53. package/templates/base/tsconfig.json.tmpl +30 -0
  54. package/templates/base/tsconfig.node.json.tmpl +11 -0
  55. package/templates/base/vite.config.ts.tmpl +90 -0
  56. package/templates/multi-language/locales/fr/core.json +76 -0
  57. package/templates/multi-language/locales/fr/kendo.json +181 -0
  58. package/templates/multi-language/locales/fr/template.json +169 -0
  59. package/templates/multi-language/locales/fr/widget.json +65 -0
  60. package/templates/multi-language/locales/hi/core.json +76 -0
  61. package/templates/multi-language/locales/hi/kendo.json +181 -0
  62. package/templates/multi-language/locales/hi/template.json +169 -0
  63. package/templates/multi-language/locales/hi/widget.json +65 -0
  64. package/templates/multi-language/sample/fr.json +55 -0
  65. package/templates/multi-language/sample/hi.json +55 -0
  66. package/templates/tests/src/Module/schemas.test.ts +50 -0
  67. package/templates/tests-multi-language/src/multiLanguage/enText.ts +68 -0
  68. package/templates/tests-multi-language/src/multiLanguage/translations.test.ts +92 -0
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
+ }
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ export const TEMPLATES_DIR = path.resolve(__dirname, "..", "templates");
7
+ export const BASE_DIR = path.join(TEMPLATES_DIR, "base");
8
+
9
+ /**
10
+ * Template naming conventions, applied when a template file lands on disk:
11
+ * foo.ts.tmpl -> foo.ts (token substitution runs)
12
+ * _dotgitignore -> .gitignore (leading `_dot` becomes `.`)
13
+ * anything else -> copied byte-for-byte, no substitution
14
+ */
15
+ export function targetName(name) {
16
+ let out = name;
17
+ if (out.startsWith("_dot")) out = "." + out.slice(4);
18
+ if (out.endsWith(".tmpl")) out = out.slice(0, -5);
19
+ return out;
20
+ }
21
+
22
+ const TOKEN_RE = /__([A-Z0-9_]+)__/g;
23
+
24
+ export function substitute(text, tokens) {
25
+ return text.replace(TOKEN_RE, (match, key) =>
26
+ Object.prototype.hasOwnProperty.call(tokens, key) ? String(tokens[key]) : match,
27
+ );
28
+ }
29
+
30
+ /** Files that must never be substituted even though they are text. */
31
+ const isTemplated = (name) => name.endsWith(".tmpl");
32
+
33
+ /**
34
+ * Recursively copy `from` into `to`, renaming and substituting as it goes.
35
+ * Returns the list of created project-relative paths.
36
+ */
37
+ export function copyTree(from, to, tokens, created = [], rootTo = to) {
38
+ fs.mkdirSync(to, { recursive: true });
39
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
40
+ const src = path.join(from, entry.name);
41
+ const dest = path.join(to, targetName(entry.name));
42
+ if (entry.isDirectory()) {
43
+ copyTree(src, dest, tokens, created, rootTo);
44
+ } else {
45
+ if (isTemplated(entry.name)) {
46
+ const text = fs.readFileSync(src, "utf8");
47
+ fs.writeFileSync(dest, substitute(text, tokens), "utf8");
48
+ } else {
49
+ fs.copyFileSync(src, dest);
50
+ }
51
+ created.push(path.relative(rootTo, dest).split(path.sep).join("/"));
52
+ }
53
+ }
54
+ return created;
55
+ }
56
+
57
+ /** A directory is usable as a target if it does not exist, or is empty, or
58
+ * holds only benign files a user tends to create first. */
59
+ const BENIGN = new Set([".git", ".gitignore", ".idea", ".vscode", "README.md", "LICENSE", ".DS_Store"]);
60
+
61
+ export function inspectTarget(dir) {
62
+ if (!fs.existsSync(dir)) return { exists: false, empty: true, conflicts: [] };
63
+ const stat = fs.statSync(dir);
64
+ if (!stat.isDirectory()) return { exists: true, empty: false, notADirectory: true, conflicts: [dir] };
65
+ const entries = fs.readdirSync(dir);
66
+ const conflicts = entries.filter((e) => !BENIGN.has(e));
67
+ return { exists: true, empty: conflicts.length === 0, conflicts };
68
+ }
69
+
70
+ export function writeJson(file, value) {
71
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", "utf8");
72
+ }
73
+
74
+ /** npm forbids uppercase and a few characters in a package name. */
75
+ export function toPackageName(input) {
76
+ return (
77
+ input
78
+ .trim()
79
+ .toLowerCase()
80
+ .replace(/[^a-z0-9._~-]+/g, "-")
81
+ .replace(/^[-_.]+|[-_.]+$/g, "")
82
+ .slice(0, 214) || "procode-app"
83
+ );
84
+ }
85
+
86
+ /** `myapp` / `my-app/` / `/my-app` all normalise to `/my-app` (no trailing slash). */
87
+ export function toBasePath(input) {
88
+ const raw = String(input).trim();
89
+
90
+ // Git Bash / MSYS rewrites a leading-slash argument into a Windows path before
91
+ // node ever sees it, so `--base /myapp` arrives as
92
+ // `C:/Program Files/Git/myapp`. Silently accepting that produces a Vite base
93
+ // nobody can serve, and the mistake only shows up as a 404 at dev time.
94
+ if (/^\/?[A-Za-z]:[\\/]/.test(raw)) {
95
+ throw new Error(
96
+ `--base "${raw}" looks like a Windows path, not a URL path.\n` +
97
+ " Git Bash rewrites a leading-slash argument into a filesystem path. Either drop the\n" +
98
+ " leading slash (--base myapp), or prefix the command with MSYS_NO_PATHCONV=1.",
99
+ );
100
+ }
101
+
102
+ const trimmed = raw.replace(/^\/+|\/+$/g, "");
103
+ return "/" + trimmed;
104
+ }
@@ -0,0 +1,185 @@
1
+ # __APP_NAME__
2
+
3
+ A ProCode low-code application — React 18 + TypeScript + Vite on `procode-lowcode-core`,
4
+ `procode-lowcode-kendo-widget`, `procode-vs-template` and `procode-vs-theme`.
5
+
6
+ Scaffolded with `create-procode` v__GENERATOR_VERSION__.
7
+
8
+ ## Quick start
9
+
10
+ ```bash
11
+ npm install
12
+ npm run dev
13
+ ```
14
+
15
+ Then open **http://localhost:5173__BASE_PATH__/sample**
16
+
17
+ ```
18
+ http://localhost:5173__BASE_PATH__/sample
19
+ ^^^^^^^^^^^^^^ ^^^^^^
20
+ base slug
21
+ ```
22
+
23
+ The base comes from `vite.config.ts` (and must equal `VITE_HOMEPAGE` in `.env`); the
24
+ slug comes from `src/Module/index.ts`.
25
+
26
+ ## Kendo licence
27
+
28
+ Without a licence key, Kendo shows a banner and a console warning on every page.
29
+ Put the key in `kendo-ui-license.txt` at the project root (it is gitignored), or set
30
+ the `KENDO_UI_LICENSE` environment variable in CI. Then run `npm install`, or
31
+ `npx kendo-ui-license activate`.
32
+
33
+ ## Before it will load real data
34
+
35
+ 1. Point `VITE_CENTRAL_SERVICE_BASE_URL` in `.env` at your environment.
36
+ 2. Copy `.env.local.example` to `.env.local` and put a dev token in
37
+ `VITE_HARDCODED_TOKEN` — or sign in so the `AuthToken` cookie is set.
38
+ **`.env.local` is gitignored. Never commit a token to `.env`.** `npm run build`
39
+ refuses to run while `VITE_HARDCODED_TOKEN` is set, so a personal token cannot
40
+ be compiled into `dist/` (a built app authenticates from the cookie).
41
+ 3. Follow the `_readme` at the top of `src/Module/Sample/Contacts.json`: add an
42
+ `apiConfig`, bind `LOAD_SCREEN_DATA`, and swap the `CUS_SAMPLE_*` stand-ins for
43
+ `DELETE_ASYNC` / `UPDATE_ASYNC` / `FORMSUBMIT_CREATE_ASYNC`.
44
+
45
+ ## Scripts
46
+
47
+ | Script | What it does |
48
+ |---|---|
49
+ | `npm run dev` | Vite dev server on port 5173 |
50
+ | `npm run build` | `tsc` then `vite build` — refused while `VITE_HARDCODED_TOKEN` is set |
51
+ | `npm run preview` | Serve the built bundle |
52
+ | `npm run lint` | ESLint over `src/` |__TEST_SCRIPT_ROW__
53
+
54
+ ## Layout
55
+
56
+ ```
57
+ src/
58
+ ├── main.tsx the SINGLE integration point — every <Core> prop, typed
59
+ ├── constant.ts APPLICATION_CODE, AuthToken, WidgetTypes, ValidationTypes, RuleEffectTypes
60
+ ├── CustomAction.ts (4) custom actions, keyed CUS_*
61
+ ├── CustomAuthorizationProvider.ts screen / widget / action gates
62
+ ├── localizationResolver.ts date pattern, currency symbol, … for vs-template widgets
63
+ ├── icons.ts app SVG icons, registered with registerIcons
64
+ ├── routes.ts extra routes (customRoute)
65
+ ├── Screens/index.tsx loader, error and 404 screens
66
+ ├── Utils/
67
+ │ ├── responseFormatter.ts reshapes every response body before adapters see it
68
+ │ ├── fieldErrorMapper.ts 422 body -> per-field messages
69
+ │ ├── errorMessages.ts fallback toast text per verb and status
70
+ │ ├── storages.ts extra STORAGE:: stores (MEMORY)
71
+ │ └── logger.ts one log sink for the core and the widget library
72
+ ├── Module/
73
+ │ ├── index.ts (1) slug -> schema
74
+ │ └── Sample/Contacts.json the sample screen — grid, bulk actions, editor, dialogs, rules
75
+ ├── AdapterProvider/
76
+ │ ├── AdapterFactory.ts (2) screenId -> adapter
77
+ │ └── SampleContacts.ts a pure translator, no HTTP; seeds the sample rows
78
+ ├── Factory/
79
+ │ ├── CustomWidgetFactory.tsx (3) custom widgets, keyed by WidgetTypes
80
+ │ ├── CustomValidationFactory.tsx (5) custom validations, keyed by ValidationTypes
81
+ │ ├── CustomRuleEffectFactory.ts custom rule effects, keyed by RuleEffectTypes
82
+ │ ├── RuleEffects/readOnlyEffect.ts CUS_READONLY — read-only while a condition holds
83
+ │ ├── Widgets/MyWidget/ a presentation-only widget, composed from library widgets
84
+ │ ├── Widgets/Notification/ the app's one toast host
85
+ │ ├── Actions/setField.ts CUS_SET_FIELD — lets a button reach MODIFY_MODEL
86
+ │ ├── Actions/myAction.ts the custom widget's two actions
87
+ │ ├── Actions/sampleContacts.ts the sample's stand-ins for the API-backed standard actions
88
+ │ └── Validations/myValidation.ts
89
+ └── assets/styles/index.scss the SCSS cascade — import order matters
90
+ public/ copied verbatim to the build, served from the base path
91
+ .env / .env.local.example every variable the core reads, documented
92
+ .eslintrc.cjs the framework's import boundary, enforced at lint time
93
+ .vscode/extensions.json recommends the ESLint extension
94
+ ```
95
+
96
+ Reference files in `public/` root-relative — `/favicon.svg`, not
97
+ `__BASE_PATH_SLASH__favicon.svg`. Vite prepends `base` itself, so writing it
98
+ yourself doubles it.
99
+
100
+ ## The sample screen
101
+
102
+ It demonstrates all five extension points at once, and its four coordinates must agree:
103
+
104
+ | Coordinate | Value | File |
105
+ |---|---|---|
106
+ | slug | `sample` | `src/Module/index.ts` |
107
+ | schema | — | `src/Module/Sample/Contacts.json` |
108
+ | screenId | `sample-contacts` | inside that JSON |
109
+ | adapter | `SampleContacts` | `src/AdapterProvider/AdapterFactory.ts` |
110
+
111
+ One JSON document, no backend, and everything on the page is declared in it:
112
+
113
+ - a heading, a subtitle and an **Add contact** button gated by a permission key;
114
+ - three `BLOCK` summary tiles that stay live;
115
+ - a `GRID` with sorting, column search, checkbox selection and three bulk actions
116
+ (Deactivate / Activate / Delete) that open the confirmation dialog with the
117
+ selection count in the text — sorting and searching go through the grid's
118
+ `onScreenDataLoad` event exactly as they would against a server;
119
+ - a click on a name that opens the inline editor for that row;
120
+ - an editor with `TEXTBOX`, `DROPDOWN`, `DATE`, `SWITCH`, `TEXTAREA` and the custom
121
+ `MY_WIDGET`, standard validators, and a *Sales region* field that a rule shows only
122
+ for the Sales department;
123
+ - a custom rule effect, `CUS_READONLY`, that makes *Notes* read-only while the
124
+ contact is inactive;
125
+ - a **Save** that validates with the framework's validators, adds or updates the row
126
+ in the grid and opens the information dialog.
127
+
128
+ The rows are seeded by the adapter. The `CUS_SAMPLE_*` actions stand in for the
129
+ API-backed standard actions and say so in their headers; the schema's `_readme`
130
+ lists the three swaps that take the screen live.
131
+
132
+ ## The one rule worth knowing up front
133
+
134
+ **All application data access is `apiConfig` plus a standard action bound to a schema
135
+ event.** An `import axios` or a bare `fetch(` anywhere under `src/` outside `main.tsx`
136
+ silently forfeits the loading counter and skeletons, status-code toasts, 422 →
137
+ field-error routing, action chainability, a schema-visible endpoint, and
138
+ per-environment retargeting without a code change.
139
+
140
+ A widget renders; it does not fetch.
141
+
142
+ ## The second rule: custom code is still made of library parts
143
+
144
+ A custom widget renders the library's `TextBox` / `Dropdown` / `Button` from
145
+ `procode-lowcode-kendo-widget/widgets` — never `import { … } from "@progress/kendo-react-*"`
146
+ and never a raw `<input>`. Icons go through `loadIcon` / `registerIcons`; navigation is
147
+ `NAVIGATE` / `MENUNAVIGATE`; a toast is `responseConfig` or the Notification host; the
148
+ store is `props.viewModel`. `react-redux`, `react-router-dom`, `react-toastify` and
149
+ `procode-*/src/…` deep paths are library internals. `npm run lint` refuses code that
150
+ breaks this; the one exception — a control the library genuinely does not wrap — is
151
+ marked with a `// procode-escalation: …` comment so it can be found with `grep`.
152
+
153
+ ## Localization
154
+
155
+ vs-template widgets ask the app for per-user values — date pattern, date
156
+ placeholder, currency symbol and decimals — through `src/localizationResolver.ts`,
157
+ which `main.tsx` registers with `registerLocalization()` before render. It returns
158
+ `undefined` for every key out of the box, so each widget uses its own default. Answer
159
+ a key there (from a token claim, a cached settings payload, or a constant) and every
160
+ widget that asks for it picks the value up on its next render.
161
+
162
+ ## Versions
163
+
164
+ The newest ProCode releases when this app was scaffolded, with the React, Kendo,
165
+ router and axios ranges those releases declare as peers:
166
+
167
+ | Package | Version |
168
+ |---|---|
169
+ __PROCODE_VERSIONS__
170
+
171
+ Node 24, React __REACT_RANGE__. **Keep React on the major the ProCode libraries declare as
172
+ a peer** — a newer major installs cleanly, then fails at runtime.
173
+
174
+ To move to newer ProCode releases later, upgrade the four together, then check for
175
+ peer warnings:
176
+
177
+ ```bash
178
+ npm install procode-lowcode-core@latest procode-lowcode-kendo-widget@latest procode-vs-template@latest procode-vs-theme@latest
179
+ npm ls --depth=0
180
+ ```
181
+
182
+ **Do not add `react-redux`** to `package.json`. It is bundled inside
183
+ `procode-lowcode-core` (on 8.x); a second copy gives two Provider contexts and every
184
+ screen renders blank with nothing in the console. The `_procodeBundled` block in
185
+ `package.json` lists every package bundled this way.
@@ -0,0 +1,8 @@
1
+ # Copy to .env.local (gitignored) and fill in.
2
+ #
3
+ # AuthToken = VITE_HARDCODED_TOKEN ?? the AuthToken cookie. Without one of the
4
+ # two, every request 401s and every screen is empty.
5
+ VITE_HARDCODED_TOKEN=""
6
+
7
+ # Point at a local API without touching the committed .env:
8
+ # VITE_CENTRAL_SERVICE_BASE_URL="https://localhost:7016/lowcodev2/api/__APP_CODE__/"
@@ -0,0 +1,41 @@
1
+ # ---------------------------------------------------------------------------
2
+ # __APP_NAME__ — committed defaults.
3
+ #
4
+ # procode-lowcode-core reads every one of these directly. A missing value does
5
+ # NOT throw: it produces a wrong URL or an empty screen, so treat a blank here
6
+ # as a bug rather than a default.
7
+ #
8
+ # Machine-specific values and ANY token belong in .env.local (gitignored).
9
+ # ---------------------------------------------------------------------------
10
+
11
+ # Where application data comes from. The <APP_CODE> segment is part of the path.
12
+ VITE_CENTRAL_SERVICE_BASE_URL="__API_BASE_URL__"
13
+
14
+ # Must equal the code main.tsx builds the adapter global from. The core resolves
15
+ # that global by name as "getAdapter" + VITE_APPLICATION_CODE — if the two
16
+ # diverge, EVERY adapter silently falls back to a dummy and no screen has data.
17
+ VITE_APPLICATION_CODE="__APP_CODE__"
18
+
19
+ # Must match `base` in vite.config.ts.
20
+ VITE_HOMEPAGE="__BASE_PATH__"
21
+
22
+ # InDraft | Published
23
+ VITE_PROJECT_ENVIROMENT="__PROJECT_ENV__"
24
+
25
+ # The low-code tool that stores and serves schemas.
26
+ VITE_TOOL_BASE_URL="__TOOL_BASE_URL__"
27
+ VITE_TOOL_SCHEMA_FETCH="https://lowcodeapi-399313.uc.r.appspot.com/api/screenRetriever/getScreenBySlug"
28
+
29
+ # Auth
30
+ VITE_AUTH_URL="__AUTH_URL__"
31
+ # VITE_BASE_LOGOUT_URL=""
32
+
33
+ # Slugs for the shared header and footer schemas.
34
+ VITE_HEADER_SLUG="header"
35
+ VITE_FOOTER_SLUG="footer"
36
+
37
+ # How long a notification stays up, in ms.
38
+ # VITE_NOTIFICATION_VISIBLE_DURATION="3000"
39
+
40
+ # A personal dev token belongs in .env.local, NOT here:
41
+ # VITE_HARDCODED_TOKEN="eyJhbGciOi..."
@@ -0,0 +1,125 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The `no-restricted-imports` block below enforces the framework's import
3
+ // boundary: custom code is built from library parts, never from the packages
4
+ // those parts wrap.
5
+ //
6
+ // Every entry has one legitimate exception: a control the library genuinely
7
+ // does not wrap. Disable the line with the escalation marker in the
8
+ // description, naming the module and the limitation:
9
+ //
10
+ // // eslint-disable-next-line no-restricted-imports -- procode-escalation: @progress/kendo-react-layout — L4 cannot compose this because the catalog has no stepper widget.
11
+ // import { Stepper } from "@progress/kendo-react-layout";
12
+ //
13
+ // `grep -rn "procode-escalation:" src` then lists every exception in the app.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const KENDO = {
17
+ group: ["@progress/kendo-react-*", "@progress/kendo-svg-icons"],
18
+ message:
19
+ "Library boundary: render the library's widget from procode-lowcode-kendo-widget/widgets, or loadIcon()/registerIcons for a glyph. A control the catalog does not wrap: `-- procode-escalation: <module> — L4 cannot compose this because <limitation>`.",
20
+ };
21
+
22
+ const ICON_SETS = {
23
+ group: ["@fortawesome/*", "react-icons", "react-icons/*", "lucide-react", "@heroicons/*", "@tabler/icons*"],
24
+ message:
25
+ "A second icon set is parallel infrastructure. Use the ICON widget, loadIcon(\"name\"), or registerIcons({ name: svg }) once for a glyph Kendo lacks.",
26
+ };
27
+
28
+ const LIBRARY_INTERNALS = {
29
+ group: ["react-redux", "react-router-dom"],
30
+ message:
31
+ "Library internals, not app imports. A widget reads props.viewModel; an action uses props.navigate / props.eventService; navigation is NAVIGATE / NAVIGATE_BACK / MENUNAVIGATE.",
32
+ };
33
+
34
+ const TOAST = {
35
+ group: ["react-toastify"],
36
+ message:
37
+ "One toast host: use responseConfig on the verb block, or the helper in src/Factory/Widgets/Notification/notifiFunc.ts.",
38
+ };
39
+
40
+ const HTTP = {
41
+ group: ["axios"],
42
+ message:
43
+ "All data access is apiConfig + a standard action bound to a schema event. Only src/main.tsx imports axios.",
44
+ };
45
+
46
+ const DEEP_PATHS = {
47
+ group: [
48
+ "procode-lowcode-core/src/*",
49
+ "procode-lowcode-core/dist/*",
50
+ "procode-lowcode-kendo-widget/src/*",
51
+ "procode-lowcode-kendo-widget/dist/*",
52
+ "procode-vs-template/src/*",
53
+ "procode-vs-template/dist/*",
54
+ "procode-vs-theme/src/*",
55
+ "procode-vs-theme/dist/*",
56
+ ],
57
+ message:
58
+ "Deep import into a package internal. Import only the declared exports subpaths.",
59
+ };
60
+
61
+ const OTHER_TOOLKITS = {
62
+ group: ["@mui/*", "antd", "antd/*", "react-bootstrap", "@chakra-ui/*", "primereact/*", "styled-components", "@emotion/*", "@radix-ui/*", "@headlessui/*"],
63
+ message:
64
+ "A second UI toolkit or styling system is parallel infrastructure. Widgets come from procode-lowcode-kendo-widget; styling from procode-vs-theme utilities and widgetStyle.",
65
+ };
66
+
67
+ const ALL = [KENDO, ICON_SETS, LIBRARY_INTERNALS, TOAST, HTTP, DEEP_PATHS, OTHER_TOOLKITS];
68
+ const without = (...skip) => ALL.filter((rule) => !skip.includes(rule));
69
+
70
+ const WINDOW_LOCATION = [
71
+ "error",
72
+ {
73
+ object: "window",
74
+ property: "location",
75
+ message:
76
+ "Navigation is NAVIGATE / NAVIGATE_BACK, MENUNAVIGATE EXTERNAL to leave the SPA, or props.navigate in an action; a query/route param is a QUERYPARAMS / ROUTEPARAMS policy.",
77
+ },
78
+ ];
79
+
80
+ module.exports = {
81
+ root: true,
82
+ env: { browser: true, es2020: true },
83
+ extends: [
84
+ "eslint:recommended",
85
+ "plugin:@typescript-eslint/recommended",
86
+ "plugin:react-hooks/recommended",
87
+ ],
88
+ ignorePatterns: ["dist", ".eslintrc.cjs"],
89
+ parser: "@typescript-eslint/parser",
90
+ plugins: ["react-refresh"],
91
+ rules: {
92
+ "react-refresh/only-export-components": [
93
+ "warn",
94
+ { allowConstantExport: true },
95
+ ],
96
+ // Widget, action and adapter props cross a schema boundary that TypeScript
97
+ // cannot see, so `any` is the honest type at those seams.
98
+ "@typescript-eslint/no-explicit-any": "off",
99
+ // The framework fixes several signatures — getSchemaBySlugService(slug, appCode),
100
+ // a validator's (value, props, viewModel), an adapter hook's (props) — so an
101
+ // implementation that needs only the first argument still has to declare the
102
+ // rest. A leading underscore marks them as deliberately unused.
103
+ "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
104
+ "no-restricted-imports": ["error", { patterns: ALL }],
105
+ "no-restricted-properties": WINDOW_LOCATION,
106
+ },
107
+ overrides: [
108
+ {
109
+ // The single integration point: the one `import axios` (validateStatus)
110
+ // and the one place the Kendo/router/redux packages may be touched.
111
+ files: ["src/main.tsx"],
112
+ rules: {
113
+ "no-restricted-imports": ["error", { patterns: without(HTTP, LIBRARY_INTERNALS) }],
114
+ "no-restricted-properties": "off",
115
+ },
116
+ },
117
+ {
118
+ // The app's ONE toast host.
119
+ files: ["src/Factory/Widgets/Notification/**"],
120
+ rules: {
121
+ "no-restricted-imports": ["error", { patterns: without(TOAST) }],
122
+ },
123
+ },
124
+ ],
125
+ };
@@ -0,0 +1,32 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+
9
+ node_modules
10
+ dist
11
+ dist-ssr
12
+ *.local
13
+
14
+ # Local env — this is where a personal dev token belongs. Never .env.
15
+ .env.local
16
+ .env.*.local
17
+
18
+ # Kendo UI licence key. @progress/kendo-licensing activates it on npm install
19
+ # from this file or the KENDO_UI_LICENSE environment variable.
20
+ kendo-ui-license.txt
21
+ kendo-ui-license.js
22
+
23
+ # Editor
24
+ .vscode/*
25
+ !.vscode/extensions.json
26
+ .idea
27
+ .DS_Store
28
+ *.suo
29
+ *.ntvs*
30
+ *.njsproj
31
+ *.sln
32
+ *.sw?
@@ -0,0 +1 @@
1
+ 24
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["dbaeumer.vscode-eslint"]
3
+ }
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <!-- Root-relative: Vite prepends `base` at build time, so public/favicon.svg
7
+ is served at __BASE_PATH_SLASH__favicon.svg. Writing the base here
8
+ yourself would double it. -->
9
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
10
+ <!-- Overwritten at runtime: procode-lowcode-core's Screen sets
11
+ document.title from the rendered schema's screenName. -->
12
+ <title>__APP_NAME__</title>
13
+ </head>
14
+ <body>
15
+ <div id="root" style="height: 100dvh"></div>
16
+ <script type="module" src="/src/main.tsx"></script>
17
+ </body>
18
+ </html>
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "__PKG_NAME__",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "homepage": "__BASE_PATH_SLASH__",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "tsc && vite build",
10
+ "preview": "vite preview",
11
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
12
+ },
13
+ "dependencies": {},
14
+ "devDependencies": {}
15
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="7" fill="#1261e8"/>
3
+ <path d="M11 24V8h6.2c3.6 0 5.8 2 5.8 5.1s-2.2 5.1-5.8 5.1H14.4V24z M14.4 15.3h2.6c1.7 0 2.6-.8 2.6-2.2s-.9-2.2-2.6-2.2h-2.6z" fill="#fff"/>
4
+ </svg>
@@ -0,0 +1,21 @@
1
+ import { SampleContacts } from "./SampleContacts";
2
+
3
+ /**
4
+ * Extension point 2 of 5: screenId -> adapter.
5
+ *
6
+ * Keyed by the `screenId` INSIDE the schema JSON — NOT by the slug. Read the id
7
+ * out of the file; never guess it.
8
+ *
9
+ * main.tsx registers this function onto window as `getAdapter__APP_CODE__`, and
10
+ * the core resolves that global by name from VITE_APPLICATION_CODE. If the two
11
+ * ever diverge, EVERY adapter here silently falls back to a dummy and no screen
12
+ * has data — on every screen at once, with nothing in the console.
13
+ */
14
+ export const getAdapter = (screenId: string): any => {
15
+ switch (screenId) {
16
+ case "sample-contacts":
17
+ return new SampleContacts();
18
+ default:
19
+ return null;
20
+ }
21
+ };