@olenbetong/appframe-vite 6.15.0 → 6.17.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 +340 -340
- package/lib/appEntry.d.ts +15 -0
- package/lib/appEntry.js +19 -0
- package/lib/appframeDataGlobal.js +2 -0
- package/lib/build.js +2 -9
- package/lib/cli-resources-migrate.js +4 -0
- package/lib/devServer.js +2 -10
- package/lib/index.js +14 -0
- package/lib/resourceGenerate.d.ts +2 -0
- package/lib/resourceGenerate.js +23 -2
- package/lib/uniqueIgnoredFields.test.d.ts +1 -0
- package/lib/uniqueIgnoredFields.test.js +33 -0
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,340 +1,340 @@
|
|
|
1
|
-
# @olenbetong/appframe-vite
|
|
2
|
-
|
|
3
|
-
Tools to develop and build Vite applications that run inside Appframe articles.
|
|
4
|
-
|
|
5
|
-
- Provides a Vite plugin you add in `vite.config.*`.
|
|
6
|
-
- Proxies API and static routes to an Appframe server during `pnpm start`.
|
|
7
|
-
- Logs in automatically and refreshes the session on an interval.
|
|
8
|
-
- Serves real article HTML from Appframe in dev, but swaps production assets for your local entry.
|
|
9
|
-
- Caches i18n strings locally to speed up development loads.
|
|
10
|
-
- Shapes production builds to Appframe paths and filenames, with optional React externals.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
```sh
|
|
15
|
-
pnpm add -D @olenbetong/appframe-vite
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Usage (Vite)
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
// vite.config.mjs or vite.config.ts
|
|
22
|
-
import appframe from "@olenbetong/appframe-vite";
|
|
23
|
-
import { defineConfig } from "vite";
|
|
24
|
-
|
|
25
|
-
export default defineConfig({
|
|
26
|
-
plugins: [appframe()],
|
|
27
|
-
});
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Then run your app:
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
pnpm --filter <app> start
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The dev server logs in with the credentials `@olenbetong/appframe-auth` finds for the site:
|
|
37
|
-
`APPFRAME_LOGIN_<ENV>` / `APPFRAME_PWD_<ENV>`, then `APPFRAME_LOGIN` / `APPFRAME_PWD`, then the
|
|
38
|
-
password `af auth <env>` saved in the OS keychain. It never asks for one; without credentials it
|
|
39
|
-
stops and says which `af auth` to run.
|
|
40
|
-
|
|
41
|
-
On first run, the plugin logs in, configures Vite’s proxy for Appframe routes, and serves the article HTML with your local entry injected.
|
|
42
|
-
|
|
43
|
-
### MUI X license key
|
|
44
|
-
|
|
45
|
-
The plugin injects the MUI X Premium license key into the bundle as the global `__MUI_X_LICENSE_KEY__`, for both `serve` and `build`. `@olenbetong/appframe-ds/grid` reads it and registers the license itself, so an app using the Designsystemet `AfGrid` does not need its own `LicenseInfo.setLicenseKey()` call.
|
|
46
|
-
|
|
47
|
-
The key is resolved from, in order:
|
|
48
|
-
|
|
49
|
-
1. the `MUI_X_LICENSE_KEY` environment variable (CI, a local `.env`, or the shell),
|
|
50
|
-
2. the `MUI_X_LICENSE_KEY` constant declared in the app's `src/shared/licenses.ts`.
|
|
51
|
-
|
|
52
|
-
The fallback keeps existing apps working with no changes; setting the environment variable is the forward path for getting the key out of the repository.
|
|
53
|
-
|
|
54
|
-
## Project configuration (package.json)
|
|
55
|
-
|
|
56
|
-
This package reads `package.json.appframe` to know what to proxy and how to build.
|
|
57
|
-
|
|
58
|
-
```jsonc
|
|
59
|
-
{
|
|
60
|
-
"appframe": {
|
|
61
|
-
"article": {
|
|
62
|
-
"id": "my-article", // Required
|
|
63
|
-
"altNames": ["my-article-dev"], // Optional: alternate root paths
|
|
64
|
-
},
|
|
65
|
-
"deploy": { "hostname": "dev.obet.no" }, // Default server if proxy not set
|
|
66
|
-
"proxy": {
|
|
67
|
-
"hostname": "dev.obet.no", // Optional override for dev proxy
|
|
68
|
-
"routes": ["^/custom/.*"], // Optional: extra proxied routes
|
|
69
|
-
},
|
|
70
|
-
"build": {
|
|
71
|
-
"platform": true, // Import React and the af packages from the site's platform bundle
|
|
72
|
-
"externals": true, // Legacy: map React and ReactDOM to globals
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### `build.platform`
|
|
79
|
-
|
|
80
|
-
Apps on the 2026 site templates (`ob.2026.application`) set `"platform": true`. The production
|
|
81
|
-
build then leaves `react`, `react/jsx-runtime`, `react/jsx-dev-runtime`, `react-dom`,
|
|
82
|
-
`react-dom/client`, `@olenbetong/appframe-core` and `@olenbetong/appframe-data` as bare imports,
|
|
83
|
-
which the template's import map resolves to `@olenbetong/synergi-platform` — one cached React
|
|
84
|
-
per page, shared with the site chrome. The `af.data` global redirect (`dataGlobal`) then only
|
|
85
|
-
applies to the dev server, which has no import map. The build also writes `dist/platform.json`
|
|
86
|
-
with the platform version the app was built alongside (the workspace's
|
|
87
|
-
`packages/synergi-platform`) and the versions it compiled against; `scripts/app-deploy.ts` pins
|
|
88
|
-
the article to that platform version (its `PlatformHead` and `PlatformStyles` blocks) and
|
|
89
|
-
refuses to deploy when the article is not on a 2026 template, that version is not served on the
|
|
90
|
-
target site, or its manifest carries other versions than the app (override with
|
|
91
|
-
`--allow-platform-mismatch`).
|
|
92
|
-
|
|
93
|
-
## What it does (at a glance)
|
|
94
|
-
|
|
95
|
-
- Dev server
|
|
96
|
-
- Proxies Appframe routes (e.g. `/api`, `/file`, `/lib`, etc.) to the configured hostname.
|
|
97
|
-
- Performs an initial login and refreshes the session every 5 minutes to prevent 403 errors.
|
|
98
|
-
- Serves the real article HTML from Appframe, but removes production assets and injects your local entry (`/src/index.*`).
|
|
99
|
-
- Caches localized strings under `node_modules/.appframe/localizeCache.json` and injects them into the page to avoid many small HTTP 1.1 requests.
|
|
100
|
-
- Build
|
|
101
|
-
- Enables `manifest` and `sourcemap`.
|
|
102
|
-
- Writes filenames to Appframe paths, using the article ID, e.g.:
|
|
103
|
-
- `file/article/script/<ARTICLE_ID>/main.[hash].min.js`
|
|
104
|
-
- `file/article/style/<ARTICLE_ID>/style.[hash].min.css`
|
|
105
|
-
- With `build.platform`, leaves React and the af packages as bare imports for the template's import map and writes `dist/platform.json`; the legacy `build.externals` maps `react` and `react-dom` to `React` and `ReactDOM` globals instead.
|
|
106
|
-
- Adds a Rollup visualizer report at `dist/stats.html`.
|
|
107
|
-
- Resolve
|
|
108
|
-
- Adds the alias `~/` to `/src/` so imports like `~/components/Button` resolve to `src/components/Button`.
|
|
109
|
-
- DevTools API (see below)
|
|
110
|
-
|
|
111
|
-
## DevTools
|
|
112
|
-
|
|
113
|
-
In dev mode the plugin exposes a small JSON API at `/__appframe_devtools__/api` for the **Appframe DevTools
|
|
114
|
-
browser extension** (`packages/appframe-devtools` in the SynergiWeb repo). The extension adds an "Appframe"
|
|
115
|
-
panel to Chrome/Edge DevTools with a live view of the app's data objects, an editor for `resources.yaml`
|
|
116
|
-
and a browser for the data API catalog. Nothing is injected into the page — the toolbar that older
|
|
117
|
-
versions added to every dev-served page is gone.
|
|
118
|
-
|
|
119
|
-
| Endpoint | Purpose |
|
|
120
|
-
| --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
121
|
-
| `GET /api/config` | Discovery: `{ appframe: true, apiVersion, hostname, articleId, articleHostname, appName, appVersion, pluginVersion }` |
|
|
122
|
-
| `GET /api/resources` | Parsed `resources.yaml` |
|
|
123
|
-
| `POST /api/resources` | Add an entry (`type: "dataObject" \| "procedure"` plus the entry fields) |
|
|
124
|
-
| `PUT /api/resources/:id` | Replace an entry |
|
|
125
|
-
| `DELETE /api/resources/:id` | Remove an entry and its generated output file |
|
|
126
|
-
| `POST /api/catalog` | Register a `dbObjectId` as a data API resource on dev, stage and prod |
|
|
127
|
-
| `GET /api/release` | Current session (`session: null` when none) and the capabilities derived from the app's scripts |
|
|
128
|
-
| `POST /api/release` | Start a job: `{ kind: "release", dry?, force? }` or `{ kind: "deploy", target, copyDatasources? }`; 409 while running |
|
|
129
|
-
| `DELETE /api/release` | Cancel the running job |
|
|
130
|
-
| `POST /api/release/input` | Answer one of the script's questions (`{ name, value }`: the release type and notes, or the apply confirmation) |
|
|
131
|
-
| `GET /api/release/events` | Server-sent events: the session snapshot, then every step, log line and question as it happens |
|
|
132
|
-
|
|
133
|
-
The release job runs whatever `scripts.release` is in the app's package.json, from the app directory, with
|
|
134
|
-
`--ui` appended. The deploy job runs `pnpm run build`, optionally `pnpm run cd` (or `copy-datasources`) and
|
|
135
|
-
`pnpm run deploy` with `APPFRAME_DEPLOY_HOSTNAME` set to the target, which `app-deploy.ts` and
|
|
136
|
-
`app-copy-datasources.ts` honour over the hostnames in package.json. The script is expected to follow the protocol in `scripts/lib/releaseUi.ts` (JSON event lines
|
|
137
|
-
prefixed with `@@appframe-devtools ` on stdout, answers as JSON lines on stdin).
|
|
138
|
-
|
|
139
|
-
Disable it with `appframe({ devtools: false })` when there is no article context (Storybook does this).
|
|
140
|
-
The extension page has host permissions for `localhost`, so the API needs no CORS headers.
|
|
141
|
-
|
|
142
|
-
## CLI — `appframe-vite`
|
|
143
|
-
|
|
144
|
-
The package ships a CLI (`appframe-vite`) alongside the Vite plugin.
|
|
145
|
-
|
|
146
|
-
### `generate-types`
|
|
147
|
-
|
|
148
|
-
Generates TypeScript type definitions from the article's data objects and procedures:
|
|
149
|
-
|
|
150
|
-
```sh
|
|
151
|
-
appframe-vite generate-types
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### `resources generate`
|
|
155
|
-
|
|
156
|
-
Reads `resources.yaml` and regenerates all data object / procedure files:
|
|
157
|
-
|
|
158
|
-
```sh
|
|
159
|
-
appframe-vite resources generate
|
|
160
|
-
# custom config file path:
|
|
161
|
-
appframe-vite resources generate --config path/to/resources.yaml
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### `resources add`
|
|
165
|
-
|
|
166
|
-
Interactive wizard — prompts for resource, ID, output path, permissions, fields, and more, then appends an entry to `resources.yaml` and generates the output file:
|
|
167
|
-
|
|
168
|
-
```sh
|
|
169
|
-
appframe-vite resources add
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### `resources edit [id]`
|
|
173
|
-
|
|
174
|
-
Interactive editor — pre-fills the wizard with the current values from `resources.yaml` and regenerates the file on save:
|
|
175
|
-
|
|
176
|
-
```sh
|
|
177
|
-
appframe-vite resources edit dsAccountGroups
|
|
178
|
-
# or without an id (shows a searchable list):
|
|
179
|
-
appframe-vite resources edit
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### `resources migrate [id]`
|
|
183
|
-
|
|
184
|
-
Migrates data objects and procedures that are configured on the article in the appdesigner over to `resources.yaml`. The article's data sources and their fields are read from the CMS, converted to `resources.yaml` entries, and the output files are generated.
|
|
185
|
-
|
|
186
|
-
Each migrated view/procedure is also registered as a Data API resource if needed (the same thing `af resources add` does), because `af.data.generateApiDataObject` and `af.ProcedureAPI` go through `/api/data` instead of the article's data sources.
|
|
187
|
-
|
|
188
|
-
```sh
|
|
189
|
-
# migrate everything on the article
|
|
190
|
-
appframe-vite resources migrate
|
|
191
|
-
# migrate a single data object or procedure
|
|
192
|
-
appframe-vite resources migrate dsVouchers
|
|
193
|
-
# preview without changing anything
|
|
194
|
-
appframe-vite resources migrate --dry-run
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Options:
|
|
198
|
-
|
|
199
|
-
| Option | Description |
|
|
200
|
-
| ------------------------ | -------------------------------------------------------------- |
|
|
201
|
-
| `-c, --config <path>` | Path to the resources config file (default: `resources.yaml`) |
|
|
202
|
-
| `-o, --output-dir <dir>` | Directory for the generated files (default: `src/data`) |
|
|
203
|
-
| `--dry-run` | Show what would be migrated without writing anything |
|
|
204
|
-
| `--no-generate` | Only update `resources.yaml`, do not generate the output files |
|
|
205
|
-
|
|
206
|
-
Existing `resources.yaml` entries with the same id are updated in place. Field aliases have no `resources.yaml` equivalent and are reported as warnings.
|
|
207
|
-
|
|
208
|
-
After migrating, import the generated files from your app, remove the data objects and procedures from the article in the appdesigner, and run `pnpm generate-types`.
|
|
209
|
-
|
|
210
|
-
## `resources.yaml` config file
|
|
211
|
-
|
|
212
|
-
Place `resources.yaml` at the project root to declare all data objects and procedures for an app. The Vite dev server watches this file and auto-regenerates all output files when it changes.
|
|
213
|
-
|
|
214
|
-
**Example:**
|
|
215
|
-
|
|
216
|
-
```yaml
|
|
217
|
-
dataObjects:
|
|
218
|
-
- id: dsAccountGroups
|
|
219
|
-
resource: atbv_Accounting_SubsidiaryLedgerGroups
|
|
220
|
-
global: true
|
|
221
|
-
types: true
|
|
222
|
-
maxRecords: -1
|
|
223
|
-
expose: true
|
|
224
|
-
fields:
|
|
225
|
-
- Domain
|
|
226
|
-
- SubsidiaryLedgerGroup
|
|
227
|
-
- PrimKey
|
|
228
|
-
- Description
|
|
229
|
-
output: src/data/dsAccountGroups.ts
|
|
230
|
-
|
|
231
|
-
- id: dsSubsidiaryLedger
|
|
232
|
-
resource: atbv_Accounting_SubsidiaryLedger
|
|
233
|
-
global: true
|
|
234
|
-
types: true
|
|
235
|
-
permissions: IUD
|
|
236
|
-
output: src/data/dsSubsidiaryLedger.ts
|
|
237
|
-
|
|
238
|
-
- id: dsProjectLocations
|
|
239
|
-
resource: aviw_Accounting_ProjectsLocations
|
|
240
|
-
global: true
|
|
241
|
-
types: true
|
|
242
|
-
master: dsProjects
|
|
243
|
-
linkFields:
|
|
244
|
-
- Domain
|
|
245
|
-
- ProjectID
|
|
246
|
-
output: src/data/dsProjectLocations.ts
|
|
247
|
-
|
|
248
|
-
procedures:
|
|
249
|
-
- id: procCreateCustomer
|
|
250
|
-
resource: astp_Accounting_SubsidiaryLedger_Create
|
|
251
|
-
global: true
|
|
252
|
-
types: true
|
|
253
|
-
expose: true
|
|
254
|
-
output: src/data/procCreateCustomer.ts
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
**Supported fields per entry:**
|
|
258
|
-
|
|
259
|
-
| Field | Type | Description |
|
|
260
|
-
| ------------- | -------------------- | ------------------------------------------------------------------------------- |
|
|
261
|
-
| `id` | `string` | Variable name used in generated code (e.g. `dsAccountGroups`) |
|
|
262
|
-
| `resource` | `string` | Database object ID (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) |
|
|
263
|
-
| `output` | `string` | Output file path relative to project root |
|
|
264
|
-
| `global` | `boolean` | Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals |
|
|
265
|
-
| `types` | `boolean` | Emit TypeScript type definitions |
|
|
266
|
-
| `permissions` | `string` | Permissions: I = insert, U = update, D = delete (e.g. `IUD`) |
|
|
267
|
-
| `maxRecords` | `number` | Max records to fetch (default `50`; `-1` for all) |
|
|
268
|
-
| `sortOrder` | `string \| string[]` | Sort order, e.g. `Created:Desc` or `[Created:Desc, Name]` |
|
|
269
|
-
| `master` | `string` | Master data object name (or `name:importPath`) |
|
|
270
|
-
| `linkFields` | `string \| string[]` | Fields linking child to master |
|
|
271
|
-
| `expose` | `boolean \| string` | Expose on `af.article.dataObjects` / `af.article.procedures` |
|
|
272
|
-
| `dynamic` | `boolean` | Enable dynamic loading |
|
|
273
|
-
| `unique` | `string` | Unique table name for update/delete |
|
|
274
|
-
| `overrides` | `string \| string[]` | Type overrides, e.g. `MyField:string[]` |
|
|
275
|
-
| `distinct` | `boolean` | Fetch distinct rows |
|
|
276
|
-
| `aggregates` | `string \| string[]` | Aggregate bindings, e.g. `Qty:SUM` |
|
|
277
|
-
| `groupBy` | `string \| string[]` | Group-by fields |
|
|
278
|
-
| `where` | `string` | Initial where clause |
|
|
279
|
-
| `fields` | `string \| string[]` | Fields to include (all if omitted) |
|
|
280
|
-
| `transaction` | `boolean` | Procedures only. `false` stops the framework wrapping the call in a transaction |
|
|
281
|
-
| `timeout` | `number` | Procedures only. Milliseconds before the call is abandoned (default `30000`) |
|
|
282
|
-
| `returnType` | `object[] \| string` | Procedures only. The result type; see below |
|
|
283
|
-
|
|
284
|
-
A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
|
|
285
|
-
|
|
286
|
-
**Master/child relationships:** set both `master` and `linkFields` on the child object. The generated data object will include `masterDataObject` and `linkFields` in the `generateApiDataObject(...)` options.
|
|
287
|
-
|
|
288
|
-
**Procedure result types:** the server only describes a procedure's parameters, so its result is
|
|
289
|
-
typed `{ Table?: unknown[] }` unless `returnType` says otherwise. Never type the result by editing
|
|
290
|
-
the generated file — every regeneration (a `resources.yaml` change seen by the dev server, a
|
|
291
|
-
`git pull --rebase` that touches it, `resources generate`) writes the file from scratch. With
|
|
292
|
-
`returnType` (and `types: true`) the generated file exports the type under the procedure's name,
|
|
293
|
-
next to the parameters type — `procIndexDNA` gets `IndexDNAParams` and `IndexDNAResult` — so
|
|
294
|
-
components import it from the generated file.
|
|
295
|
-
|
|
296
|
-
`returnType` is either a list of result sets, in the order the procedure returns them (`Table`,
|
|
297
|
-
`Table1`, …; `table` overrides the key), with each field's TypeScript type:
|
|
298
|
-
|
|
299
|
-
```yaml
|
|
300
|
-
procedures:
|
|
301
|
-
- id: procIndexDNA
|
|
302
|
-
resource: astp_Operations_IndexDNA
|
|
303
|
-
global: true
|
|
304
|
-
types: true
|
|
305
|
-
returnType:
|
|
306
|
-
- fields:
|
|
307
|
-
- name: IndexName
|
|
308
|
-
type: string
|
|
309
|
-
- name: CurrentFF
|
|
310
|
-
type: number
|
|
311
|
-
- fields:
|
|
312
|
-
- name: PageSort
|
|
313
|
-
type: number
|
|
314
|
-
output: src/features/IndexDetails/procIndexDNA.ts
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
or a TypeScript type written out as a string, for anything the list cannot express. It may refer
|
|
318
|
-
to types in the app's `custom.d.ts` through the `Custom.` prefix, which adds the import:
|
|
319
|
-
|
|
320
|
-
```yaml
|
|
321
|
-
returnType: "Custom.IndexDNAResult"
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
`procedureReturnTypes` in `types.json`, keyed by the procedure's database name, does the same
|
|
325
|
-
and is used when the entry has no `returnType`.
|
|
326
|
-
|
|
327
|
-
## `@olenbetong/appframe-vite/resources` export
|
|
328
|
-
|
|
329
|
-
Shared code generation utilities for Node.js consumers:
|
|
330
|
-
|
|
331
|
-
```ts
|
|
332
|
-
import {
|
|
333
|
-
fetchAndGenerate,
|
|
334
|
-
buildYamlConfig,
|
|
335
|
-
parseYamlConfig,
|
|
336
|
-
writeGeneratedFile,
|
|
337
|
-
getCustomImportPath,
|
|
338
|
-
type CLIOptions,
|
|
339
|
-
} from "@olenbetong/appframe-vite/resources";
|
|
340
|
-
```
|
|
1
|
+
# @olenbetong/appframe-vite
|
|
2
|
+
|
|
3
|
+
Tools to develop and build Vite applications that run inside Appframe articles.
|
|
4
|
+
|
|
5
|
+
- Provides a Vite plugin you add in `vite.config.*`.
|
|
6
|
+
- Proxies API and static routes to an Appframe server during `pnpm start`.
|
|
7
|
+
- Logs in automatically and refreshes the session on an interval.
|
|
8
|
+
- Serves real article HTML from Appframe in dev, but swaps production assets for your local entry.
|
|
9
|
+
- Caches i18n strings locally to speed up development loads.
|
|
10
|
+
- Shapes production builds to Appframe paths and filenames, with optional React externals.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add -D @olenbetong/appframe-vite
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage (Vite)
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// vite.config.mjs or vite.config.ts
|
|
22
|
+
import appframe from "@olenbetong/appframe-vite";
|
|
23
|
+
import { defineConfig } from "vite";
|
|
24
|
+
|
|
25
|
+
export default defineConfig({
|
|
26
|
+
plugins: [appframe()],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then run your app:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pnpm --filter <app> start
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The dev server logs in with the credentials `@olenbetong/appframe-auth` finds for the site:
|
|
37
|
+
`APPFRAME_LOGIN_<ENV>` / `APPFRAME_PWD_<ENV>`, then `APPFRAME_LOGIN` / `APPFRAME_PWD`, then the
|
|
38
|
+
password `af auth <env>` saved in the OS keychain. It never asks for one; without credentials it
|
|
39
|
+
stops and says which `af auth` to run.
|
|
40
|
+
|
|
41
|
+
On first run, the plugin logs in, configures Vite’s proxy for Appframe routes, and serves the article HTML with your local entry injected.
|
|
42
|
+
|
|
43
|
+
### MUI X license key
|
|
44
|
+
|
|
45
|
+
The plugin injects the MUI X Premium license key into the bundle as the global `__MUI_X_LICENSE_KEY__`, for both `serve` and `build`. `@olenbetong/appframe-ds/grid` reads it and registers the license itself, so an app using the Designsystemet `AfGrid` does not need its own `LicenseInfo.setLicenseKey()` call.
|
|
46
|
+
|
|
47
|
+
The key is resolved from, in order:
|
|
48
|
+
|
|
49
|
+
1. the `MUI_X_LICENSE_KEY` environment variable (CI, a local `.env`, or the shell),
|
|
50
|
+
2. the `MUI_X_LICENSE_KEY` constant declared in the app's `src/shared/licenses.ts`.
|
|
51
|
+
|
|
52
|
+
The fallback keeps existing apps working with no changes; setting the environment variable is the forward path for getting the key out of the repository.
|
|
53
|
+
|
|
54
|
+
## Project configuration (package.json)
|
|
55
|
+
|
|
56
|
+
This package reads `package.json.appframe` to know what to proxy and how to build.
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
{
|
|
60
|
+
"appframe": {
|
|
61
|
+
"article": {
|
|
62
|
+
"id": "my-article", // Required
|
|
63
|
+
"altNames": ["my-article-dev"], // Optional: alternate root paths
|
|
64
|
+
},
|
|
65
|
+
"deploy": { "hostname": "dev.obet.no" }, // Default server if proxy not set
|
|
66
|
+
"proxy": {
|
|
67
|
+
"hostname": "dev.obet.no", // Optional override for dev proxy
|
|
68
|
+
"routes": ["^/custom/.*"], // Optional: extra proxied routes
|
|
69
|
+
},
|
|
70
|
+
"build": {
|
|
71
|
+
"platform": true, // Import React and the af packages from the site's platform bundle
|
|
72
|
+
"externals": true, // Legacy: map React and ReactDOM to globals
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `build.platform`
|
|
79
|
+
|
|
80
|
+
Apps on the 2026 site templates (`ob.2026.application`) set `"platform": true`. The production
|
|
81
|
+
build then leaves `react`, `react/jsx-runtime`, `react/jsx-dev-runtime`, `react-dom`,
|
|
82
|
+
`react-dom/client`, `@olenbetong/appframe-core` and `@olenbetong/appframe-data` as bare imports,
|
|
83
|
+
which the template's import map resolves to `@olenbetong/synergi-platform` — one cached React
|
|
84
|
+
per page, shared with the site chrome. The `af.data` global redirect (`dataGlobal`) then only
|
|
85
|
+
applies to the dev server, which has no import map. The build also writes `dist/platform.json`
|
|
86
|
+
with the platform version the app was built alongside (the workspace's
|
|
87
|
+
`packages/synergi-platform`) and the versions it compiled against; `scripts/app-deploy.ts` pins
|
|
88
|
+
the article to that platform version (its `PlatformHead` and `PlatformStyles` blocks) and
|
|
89
|
+
refuses to deploy when the article is not on a 2026 template, that version is not served on the
|
|
90
|
+
target site, or its manifest carries other versions than the app (override with
|
|
91
|
+
`--allow-platform-mismatch`).
|
|
92
|
+
|
|
93
|
+
## What it does (at a glance)
|
|
94
|
+
|
|
95
|
+
- Dev server
|
|
96
|
+
- Proxies Appframe routes (e.g. `/api`, `/file`, `/lib`, etc.) to the configured hostname.
|
|
97
|
+
- Performs an initial login and refreshes the session every 5 minutes to prevent 403 errors.
|
|
98
|
+
- Serves the real article HTML from Appframe, but removes production assets and injects your local entry (`/src/index.*`).
|
|
99
|
+
- Caches localized strings under `node_modules/.appframe/localizeCache.json` and injects them into the page to avoid many small HTTP 1.1 requests.
|
|
100
|
+
- Build
|
|
101
|
+
- Enables `manifest` and `sourcemap`.
|
|
102
|
+
- Writes filenames to Appframe paths, using the article ID, e.g.:
|
|
103
|
+
- `file/article/script/<ARTICLE_ID>/main.[hash].min.js`
|
|
104
|
+
- `file/article/style/<ARTICLE_ID>/style.[hash].min.css`
|
|
105
|
+
- With `build.platform`, leaves React and the af packages as bare imports for the template's import map and writes `dist/platform.json`; the legacy `build.externals` maps `react` and `react-dom` to `React` and `ReactDOM` globals instead.
|
|
106
|
+
- Adds a Rollup visualizer report at `dist/stats.html`.
|
|
107
|
+
- Resolve
|
|
108
|
+
- Adds the alias `~/` to `/src/` so imports like `~/components/Button` resolve to `src/components/Button`.
|
|
109
|
+
- DevTools API (see below)
|
|
110
|
+
|
|
111
|
+
## DevTools
|
|
112
|
+
|
|
113
|
+
In dev mode the plugin exposes a small JSON API at `/__appframe_devtools__/api` for the **Appframe DevTools
|
|
114
|
+
browser extension** (`packages/appframe-devtools` in the SynergiWeb repo). The extension adds an "Appframe"
|
|
115
|
+
panel to Chrome/Edge DevTools with a live view of the app's data objects, an editor for `resources.yaml`
|
|
116
|
+
and a browser for the data API catalog. Nothing is injected into the page — the toolbar that older
|
|
117
|
+
versions added to every dev-served page is gone.
|
|
118
|
+
|
|
119
|
+
| Endpoint | Purpose |
|
|
120
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
121
|
+
| `GET /api/config` | Discovery: `{ appframe: true, apiVersion, hostname, articleId, articleHostname, appName, appVersion, pluginVersion }` |
|
|
122
|
+
| `GET /api/resources` | Parsed `resources.yaml` |
|
|
123
|
+
| `POST /api/resources` | Add an entry (`type: "dataObject" \| "procedure"` plus the entry fields) |
|
|
124
|
+
| `PUT /api/resources/:id` | Replace an entry |
|
|
125
|
+
| `DELETE /api/resources/:id` | Remove an entry and its generated output file |
|
|
126
|
+
| `POST /api/catalog` | Register a `dbObjectId` as a data API resource on dev, stage and prod |
|
|
127
|
+
| `GET /api/release` | Current session (`session: null` when none) and the capabilities derived from the app's scripts |
|
|
128
|
+
| `POST /api/release` | Start a job: `{ kind: "release", dry?, force? }` or `{ kind: "deploy", target, copyDatasources? }`; 409 while running |
|
|
129
|
+
| `DELETE /api/release` | Cancel the running job |
|
|
130
|
+
| `POST /api/release/input` | Answer one of the script's questions (`{ name, value }`: the release type and notes, or the apply confirmation) |
|
|
131
|
+
| `GET /api/release/events` | Server-sent events: the session snapshot, then every step, log line and question as it happens |
|
|
132
|
+
|
|
133
|
+
The release job runs whatever `scripts.release` is in the app's package.json, from the app directory, with
|
|
134
|
+
`--ui` appended. The deploy job runs `pnpm run build`, optionally `pnpm run cd` (or `copy-datasources`) and
|
|
135
|
+
`pnpm run deploy` with `APPFRAME_DEPLOY_HOSTNAME` set to the target, which `app-deploy.ts` and
|
|
136
|
+
`app-copy-datasources.ts` honour over the hostnames in package.json. The script is expected to follow the protocol in `scripts/lib/releaseUi.ts` (JSON event lines
|
|
137
|
+
prefixed with `@@appframe-devtools ` on stdout, answers as JSON lines on stdin).
|
|
138
|
+
|
|
139
|
+
Disable it with `appframe({ devtools: false })` when there is no article context (Storybook does this).
|
|
140
|
+
The extension page has host permissions for `localhost`, so the API needs no CORS headers.
|
|
141
|
+
|
|
142
|
+
## CLI — `appframe-vite`
|
|
143
|
+
|
|
144
|
+
The package ships a CLI (`appframe-vite`) alongside the Vite plugin.
|
|
145
|
+
|
|
146
|
+
### `generate-types`
|
|
147
|
+
|
|
148
|
+
Generates TypeScript type definitions from the article's data objects and procedures:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
appframe-vite generate-types
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `resources generate`
|
|
155
|
+
|
|
156
|
+
Reads `resources.yaml` and regenerates all data object / procedure files:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
appframe-vite resources generate
|
|
160
|
+
# custom config file path:
|
|
161
|
+
appframe-vite resources generate --config path/to/resources.yaml
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `resources add`
|
|
165
|
+
|
|
166
|
+
Interactive wizard — prompts for resource, ID, output path, permissions, fields, and more, then appends an entry to `resources.yaml` and generates the output file:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
appframe-vite resources add
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `resources edit [id]`
|
|
173
|
+
|
|
174
|
+
Interactive editor — pre-fills the wizard with the current values from `resources.yaml` and regenerates the file on save:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
appframe-vite resources edit dsAccountGroups
|
|
178
|
+
# or without an id (shows a searchable list):
|
|
179
|
+
appframe-vite resources edit
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### `resources migrate [id]`
|
|
183
|
+
|
|
184
|
+
Migrates data objects and procedures that are configured on the article in the appdesigner over to `resources.yaml`. The article's data sources and their fields are read from the CMS, converted to `resources.yaml` entries, and the output files are generated.
|
|
185
|
+
|
|
186
|
+
Each migrated view/procedure is also registered as a Data API resource if needed (the same thing `af resources add` does), because `af.data.generateApiDataObject` and `af.ProcedureAPI` go through `/api/data` instead of the article's data sources.
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
# migrate everything on the article
|
|
190
|
+
appframe-vite resources migrate
|
|
191
|
+
# migrate a single data object or procedure
|
|
192
|
+
appframe-vite resources migrate dsVouchers
|
|
193
|
+
# preview without changing anything
|
|
194
|
+
appframe-vite resources migrate --dry-run
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Options:
|
|
198
|
+
|
|
199
|
+
| Option | Description |
|
|
200
|
+
| ------------------------ | -------------------------------------------------------------- |
|
|
201
|
+
| `-c, --config <path>` | Path to the resources config file (default: `resources.yaml`) |
|
|
202
|
+
| `-o, --output-dir <dir>` | Directory for the generated files (default: `src/data`) |
|
|
203
|
+
| `--dry-run` | Show what would be migrated without writing anything |
|
|
204
|
+
| `--no-generate` | Only update `resources.yaml`, do not generate the output files |
|
|
205
|
+
|
|
206
|
+
Existing `resources.yaml` entries with the same id are updated in place. Field aliases have no `resources.yaml` equivalent and are reported as warnings.
|
|
207
|
+
|
|
208
|
+
After migrating, import the generated files from your app, remove the data objects and procedures from the article in the appdesigner, and run `pnpm generate-types`.
|
|
209
|
+
|
|
210
|
+
## `resources.yaml` config file
|
|
211
|
+
|
|
212
|
+
Place `resources.yaml` at the project root to declare all data objects and procedures for an app. The Vite dev server watches this file and auto-regenerates all output files when it changes.
|
|
213
|
+
|
|
214
|
+
**Example:**
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
dataObjects:
|
|
218
|
+
- id: dsAccountGroups
|
|
219
|
+
resource: atbv_Accounting_SubsidiaryLedgerGroups
|
|
220
|
+
global: true
|
|
221
|
+
types: true
|
|
222
|
+
maxRecords: -1
|
|
223
|
+
expose: true
|
|
224
|
+
fields:
|
|
225
|
+
- Domain
|
|
226
|
+
- SubsidiaryLedgerGroup
|
|
227
|
+
- PrimKey
|
|
228
|
+
- Description
|
|
229
|
+
output: src/data/dsAccountGroups.ts
|
|
230
|
+
|
|
231
|
+
- id: dsSubsidiaryLedger
|
|
232
|
+
resource: atbv_Accounting_SubsidiaryLedger
|
|
233
|
+
global: true
|
|
234
|
+
types: true
|
|
235
|
+
permissions: IUD
|
|
236
|
+
output: src/data/dsSubsidiaryLedger.ts
|
|
237
|
+
|
|
238
|
+
- id: dsProjectLocations
|
|
239
|
+
resource: aviw_Accounting_ProjectsLocations
|
|
240
|
+
global: true
|
|
241
|
+
types: true
|
|
242
|
+
master: dsProjects
|
|
243
|
+
linkFields:
|
|
244
|
+
- Domain
|
|
245
|
+
- ProjectID
|
|
246
|
+
output: src/data/dsProjectLocations.ts
|
|
247
|
+
|
|
248
|
+
procedures:
|
|
249
|
+
- id: procCreateCustomer
|
|
250
|
+
resource: astp_Accounting_SubsidiaryLedger_Create
|
|
251
|
+
global: true
|
|
252
|
+
types: true
|
|
253
|
+
expose: true
|
|
254
|
+
output: src/data/procCreateCustomer.ts
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Supported fields per entry:**
|
|
258
|
+
|
|
259
|
+
| Field | Type | Description |
|
|
260
|
+
| ------------- | -------------------- | ------------------------------------------------------------------------------- |
|
|
261
|
+
| `id` | `string` | Variable name used in generated code (e.g. `dsAccountGroups`) |
|
|
262
|
+
| `resource` | `string` | Database object ID (e.g. `atbv_Accounting_SubsidiaryLedgerGroups`) |
|
|
263
|
+
| `output` | `string` | Output file path relative to project root |
|
|
264
|
+
| `global` | `boolean` | Use `af.data.generateApiDataObject` / `new af.ProcedureAPI` globals |
|
|
265
|
+
| `types` | `boolean` | Emit TypeScript type definitions |
|
|
266
|
+
| `permissions` | `string` | Permissions: I = insert, U = update, D = delete (e.g. `IUD`) |
|
|
267
|
+
| `maxRecords` | `number` | Max records to fetch (default `50`; `-1` for all) |
|
|
268
|
+
| `sortOrder` | `string \| string[]` | Sort order, e.g. `Created:Desc` or `[Created:Desc, Name]` |
|
|
269
|
+
| `master` | `string` | Master data object name (or `name:importPath`) |
|
|
270
|
+
| `linkFields` | `string \| string[]` | Fields linking child to master |
|
|
271
|
+
| `expose` | `boolean \| string` | Expose on `af.article.dataObjects` / `af.article.procedures` |
|
|
272
|
+
| `dynamic` | `boolean` | Enable dynamic loading |
|
|
273
|
+
| `unique` | `string` | Unique table name for update/delete |
|
|
274
|
+
| `overrides` | `string \| string[]` | Type overrides, e.g. `MyField:string[]` |
|
|
275
|
+
| `distinct` | `boolean` | Fetch distinct rows |
|
|
276
|
+
| `aggregates` | `string \| string[]` | Aggregate bindings, e.g. `Qty:SUM` |
|
|
277
|
+
| `groupBy` | `string \| string[]` | Group-by fields |
|
|
278
|
+
| `where` | `string` | Initial where clause |
|
|
279
|
+
| `fields` | `string \| string[]` | Fields to include (all if omitted) |
|
|
280
|
+
| `transaction` | `boolean` | Procedures only. `false` stops the framework wrapping the call in a transaction |
|
|
281
|
+
| `timeout` | `number` | Procedures only. Milliseconds before the call is abandoned (default `30000`) |
|
|
282
|
+
| `returnType` | `object[] \| string` | Procedures only. The result type; see below |
|
|
283
|
+
|
|
284
|
+
A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
|
|
285
|
+
|
|
286
|
+
**Master/child relationships:** set both `master` and `linkFields` on the child object. The generated data object will include `masterDataObject` and `linkFields` in the `generateApiDataObject(...)` options.
|
|
287
|
+
|
|
288
|
+
**Procedure result types:** the server only describes a procedure's parameters, so its result is
|
|
289
|
+
typed `{ Table?: unknown[] }` unless `returnType` says otherwise. Never type the result by editing
|
|
290
|
+
the generated file — every regeneration (a `resources.yaml` change seen by the dev server, a
|
|
291
|
+
`git pull --rebase` that touches it, `resources generate`) writes the file from scratch. With
|
|
292
|
+
`returnType` (and `types: true`) the generated file exports the type under the procedure's name,
|
|
293
|
+
next to the parameters type — `procIndexDNA` gets `IndexDNAParams` and `IndexDNAResult` — so
|
|
294
|
+
components import it from the generated file.
|
|
295
|
+
|
|
296
|
+
`returnType` is either a list of result sets, in the order the procedure returns them (`Table`,
|
|
297
|
+
`Table1`, …; `table` overrides the key), with each field's TypeScript type:
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
procedures:
|
|
301
|
+
- id: procIndexDNA
|
|
302
|
+
resource: astp_Operations_IndexDNA
|
|
303
|
+
global: true
|
|
304
|
+
types: true
|
|
305
|
+
returnType:
|
|
306
|
+
- fields:
|
|
307
|
+
- name: IndexName
|
|
308
|
+
type: string
|
|
309
|
+
- name: CurrentFF
|
|
310
|
+
type: number
|
|
311
|
+
- fields:
|
|
312
|
+
- name: PageSort
|
|
313
|
+
type: number
|
|
314
|
+
output: src/features/IndexDetails/procIndexDNA.ts
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
or a TypeScript type written out as a string, for anything the list cannot express. It may refer
|
|
318
|
+
to types in the app's `custom.d.ts` through the `Custom.` prefix, which adds the import:
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
returnType: "Custom.IndexDNAResult"
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`procedureReturnTypes` in `types.json`, keyed by the procedure's database name, does the same
|
|
325
|
+
and is used when the entry has no `returnType`.
|
|
326
|
+
|
|
327
|
+
## `@olenbetong/appframe-vite/resources` export
|
|
328
|
+
|
|
329
|
+
Shared code generation utilities for Node.js consumers:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import {
|
|
333
|
+
fetchAndGenerate,
|
|
334
|
+
buildYamlConfig,
|
|
335
|
+
parseYamlConfig,
|
|
336
|
+
writeGeneratedFile,
|
|
337
|
+
getCustomImportPath,
|
|
338
|
+
type CLIOptions,
|
|
339
|
+
} from "@olenbetong/appframe-vite/resources";
|
|
340
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the app's entry module. Apps have no `index.html`: the dev server fetches the
|
|
3
|
+
* article's HTML from Synergi and injects a `<script type="module">` pointing at this file,
|
|
4
|
+
* and the production build uses it as the single rolldown input.
|
|
5
|
+
*
|
|
6
|
+
* `relative` is a POSIX path from the root, usable as a glob pattern (`optimizeDeps.entries`
|
|
7
|
+
* takes globs, and an absolute Windows path would be read as one with escapes in it).
|
|
8
|
+
* The last candidate is returned even when it does not exist, so a missing entry fails
|
|
9
|
+
* with a resolve error that names the file instead of a vaguer one later on.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getAppEntry(root?: string): {
|
|
12
|
+
absolute: string;
|
|
13
|
+
relative: string;
|
|
14
|
+
url: string;
|
|
15
|
+
};
|
package/lib/appEntry.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/** The entry file candidates, in the order they are tried, relative to the app root. */
|
|
4
|
+
const ENTRY_CANDIDATES = ["src/index.tsx", "src/index.ts", "src/index.js"];
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the app's entry module. Apps have no `index.html`: the dev server fetches the
|
|
7
|
+
* article's HTML from Synergi and injects a `<script type="module">` pointing at this file,
|
|
8
|
+
* and the production build uses it as the single rolldown input.
|
|
9
|
+
*
|
|
10
|
+
* `relative` is a POSIX path from the root, usable as a glob pattern (`optimizeDeps.entries`
|
|
11
|
+
* takes globs, and an absolute Windows path would be read as one with escapes in it).
|
|
12
|
+
* The last candidate is returned even when it does not exist, so a missing entry fails
|
|
13
|
+
* with a resolve error that names the file instead of a vaguer one later on.
|
|
14
|
+
*/
|
|
15
|
+
export function getAppEntry(root = process.cwd()) {
|
|
16
|
+
let found = ENTRY_CANDIDATES.find((candidate) => existsSync(resolve(root, candidate)));
|
|
17
|
+
let relative = found ?? ENTRY_CANDIDATES[ENTRY_CANDIDATES.length - 1];
|
|
18
|
+
return { absolute: resolve(root, relative), relative, url: `/${relative}` };
|
|
19
|
+
}
|
package/lib/build.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import { dirname, resolve } from "node:path";
|
|
4
3
|
import { visualizer } from "rollup-plugin-visualizer";
|
|
5
4
|
import { esmExternalRequirePlugin } from "vite";
|
|
5
|
+
import { getAppEntry } from "./appEntry.js";
|
|
6
6
|
import { importJson } from "./importJson.js";
|
|
7
7
|
/**
|
|
8
8
|
* The bare specifiers the 2026 site templates resolve through their import map to the
|
|
@@ -202,16 +202,9 @@ export async function addAppframeBuildConfig(config) {
|
|
|
202
202
|
sourcemap: true,
|
|
203
203
|
};
|
|
204
204
|
config.plugins = config.plugins ?? [];
|
|
205
|
-
let entry = resolve(process.cwd(), "./src/index.tsx");
|
|
206
|
-
if (!existsSync(resolve(entry))) {
|
|
207
|
-
entry = resolve(process.cwd(), "./src/index.ts");
|
|
208
|
-
}
|
|
209
|
-
if (!existsSync(resolve(entry))) {
|
|
210
|
-
entry = resolve(process.cwd(), "./src/index.js");
|
|
211
|
-
}
|
|
212
205
|
config.appType = "custom";
|
|
213
206
|
config.build.rolldownOptions = {
|
|
214
|
-
input: { main:
|
|
207
|
+
input: { main: getAppEntry().absolute },
|
|
215
208
|
plugins: [
|
|
216
209
|
// Cast visualizer plugin to any to avoid Rollup type version clashes
|
|
217
210
|
visualizer({ filename: "./dist/stats.html", gzipSize: true }),
|
|
@@ -182,6 +182,10 @@ function dataObjectToEntry(datasource, fields, outputDir, warnings) {
|
|
|
182
182
|
if (sortFields.length > 0) {
|
|
183
183
|
entry.sortOrder = sortFields.map((f) => {
|
|
184
184
|
let direction = (f.AscDesc ?? "asc").toLowerCase();
|
|
185
|
+
// Keep the null placement of AscNullsLast / DescNullsFirst
|
|
186
|
+
if (direction === "ascnullslast" || direction === "descnullsfirst") {
|
|
187
|
+
return { field: f.FieldName, direction };
|
|
188
|
+
}
|
|
185
189
|
return direction.startsWith("desc") ? { field: f.FieldName, direction: "desc" } : { field: f.FieldName };
|
|
186
190
|
});
|
|
187
191
|
}
|
package/lib/devServer.js
CHANGED
|
@@ -5,6 +5,7 @@ import { AuthenticationError, authenticate, loginHint, resolveCredentials } from
|
|
|
5
5
|
import { Client } from "@olenbetong/appframe-data";
|
|
6
6
|
import dotenv from "dotenv";
|
|
7
7
|
import { JSDOM } from "jsdom";
|
|
8
|
+
import { getAppEntry } from "./appEntry.js";
|
|
8
9
|
import { importJson } from "./importJson.js";
|
|
9
10
|
import { getStringCache } from "./localization.js";
|
|
10
11
|
import { diagnoseServerResponse } from "./utils.js";
|
|
@@ -68,16 +69,7 @@ export function getProxyRoutes() {
|
|
|
68
69
|
* @returns
|
|
69
70
|
*/
|
|
70
71
|
async function transformArticleHtml(html) {
|
|
71
|
-
let entry;
|
|
72
|
-
if (existsSync(resolve(process.cwd(), "./src/index.tsx"))) {
|
|
73
|
-
entry = "/src/index.tsx";
|
|
74
|
-
}
|
|
75
|
-
else if (existsSync(resolve(process.cwd(), "./src/index.ts"))) {
|
|
76
|
-
entry = "/src/index.ts";
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
entry = "/src/index.js";
|
|
80
|
-
}
|
|
72
|
+
let entry = getAppEntry().url;
|
|
81
73
|
let article = appframe.article?.id;
|
|
82
74
|
let { hostname } = await getAppHostname();
|
|
83
75
|
let dom = new JSDOM(html);
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import bodyParser from "body-parser";
|
|
4
4
|
import { watch } from "chokidar";
|
|
5
|
+
import { getAppEntry } from "./appEntry.js";
|
|
5
6
|
import { appframeDataGlobal } from "./appframeDataGlobal.js";
|
|
6
7
|
import { addAppframeBuildConfig, PLATFORM_EXTERNALS, PLATFORM_GLOBALS, PLATFORM_UPSTREAM, platformManifest, usesPlatform, } from "./build.js";
|
|
7
8
|
import { generateFromConfig } from "./cli-resources-generate.js";
|
|
@@ -184,6 +185,19 @@ export default function appframe(options = {}) {
|
|
|
184
185
|
...config.server,
|
|
185
186
|
proxy,
|
|
186
187
|
};
|
|
188
|
+
// Apps have no index.html, so the dependency scanner Vite runs at startup has
|
|
189
|
+
// nothing to crawl (it globs "**/*.html") and skips pre-bundling altogether. Every
|
|
190
|
+
// dependency is then discovered when a module first imports it: the ones behind
|
|
191
|
+
// the initial page load are batched, but a lazy route that pulls in a new one
|
|
192
|
+
// re-runs the optimizer and forces a full reload, which lands back on the page
|
|
193
|
+
// you navigated from. Crawling from the entry module finds the lazy imports up
|
|
194
|
+
// front, so the deps are ready before the browser asks for them.
|
|
195
|
+
if (!config.optimizeDeps?.entries) {
|
|
196
|
+
config.optimizeDeps = {
|
|
197
|
+
...config.optimizeDeps,
|
|
198
|
+
entries: [getAppEntry().relative],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
187
201
|
}
|
|
188
202
|
return config;
|
|
189
203
|
},
|
|
@@ -32,6 +32,8 @@ export type CLIOptions = {
|
|
|
32
32
|
output?: string;
|
|
33
33
|
typesJsonParamOverrides?: Record<string, string>;
|
|
34
34
|
typesJsonReturnType?: string | null;
|
|
35
|
+
/** Columns of the `unique` table; fields outside it are marked ignored so saves skip them. */
|
|
36
|
+
uniqueFields?: string[];
|
|
35
37
|
};
|
|
36
38
|
/**
|
|
37
39
|
* Serialize the generation options to a `/* af:config ... *\/` YAML comment block.
|
package/lib/resourceGenerate.js
CHANGED
|
@@ -169,6 +169,13 @@ export function parseYamlConfig(fileContent) {
|
|
|
169
169
|
// ---------------------------------------------------------------------------
|
|
170
170
|
// Code generation helpers
|
|
171
171
|
// ---------------------------------------------------------------------------
|
|
172
|
+
/**
|
|
173
|
+
* A field name as an object key in generated code, quoted when it is not a valid identifier,
|
|
174
|
+
* such as a view column named `EU-Neste`.
|
|
175
|
+
*/
|
|
176
|
+
function propertyKey(name) {
|
|
177
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
178
|
+
}
|
|
172
179
|
/**
|
|
173
180
|
* Convert Appframe (SQL) data types to typescript/field definition types
|
|
174
181
|
*/
|
|
@@ -361,6 +368,9 @@ export function getDataObjectDefinition(name, viewDefinition, options) {
|
|
|
361
368
|
if (field.Identity) {
|
|
362
369
|
fieldDefinition.identity = true;
|
|
363
370
|
}
|
|
371
|
+
if (options.uniqueFields && !options.uniqueFields.includes(field.Name)) {
|
|
372
|
+
fieldDefinition.ignored = true;
|
|
373
|
+
}
|
|
364
374
|
if (aggregates[field.Name]) {
|
|
365
375
|
fieldDefinition.aggregate = aggregates[field.Name];
|
|
366
376
|
}
|
|
@@ -394,7 +404,7 @@ export function getDataObjectDefinition(name, viewDefinition, options) {
|
|
|
394
404
|
type += " | null";
|
|
395
405
|
}
|
|
396
406
|
}
|
|
397
|
-
fieldTypes += ` ${field.name}: ${type};\n`;
|
|
407
|
+
fieldTypes += ` ${propertyKey(field.name)}: ${type};\n`;
|
|
398
408
|
}
|
|
399
409
|
types = `export type ${typeName} = {
|
|
400
410
|
${fieldTypes}}`;
|
|
@@ -465,7 +475,7 @@ ${fieldTypes}}`;
|
|
|
465
475
|
order = "DescNullsFirst";
|
|
466
476
|
break;
|
|
467
477
|
}
|
|
468
|
-
return `{ ${field}: ${sortPrefix}${order} }`;
|
|
478
|
+
return `{ ${propertyKey(field)}: ${sortPrefix}${order} }`;
|
|
469
479
|
})
|
|
470
480
|
.join(", ");
|
|
471
481
|
parametersOption.push(`sortOrder: [${sortOrder}]`);
|
|
@@ -553,6 +563,17 @@ export async function fetchAndGenerate(resourceName, options, client) {
|
|
|
553
563
|
options.typesJsonParamOverrides = typesJson.parameterTypes?.[resourceName] ?? {};
|
|
554
564
|
// Structured returnType from resources.yaml takes precedence over the raw types.json string
|
|
555
565
|
options.typesJsonReturnType = options.typesJsonReturnType ?? typesJson.procedureReturnTypes?.[resourceName] ?? null;
|
|
566
|
+
// A view saved through another table (`unique`) usually carries joined columns that table
|
|
567
|
+
// lacks. CMS data objects know to leave those out of a save; mark them ignored so these do too.
|
|
568
|
+
if (definition.ObjectType === "V" && options.unique) {
|
|
569
|
+
try {
|
|
570
|
+
let uniqueDefinition = await fetchResourceDefinition(client, options.unique);
|
|
571
|
+
options.uniqueFields = (uniqueDefinition.Parameters ?? []).map((p) => p.Name);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.warn(`Could not read the unique table '${options.unique}' for '${resourceName}', so no fields are marked ignored: ${error instanceof Error ? error.message.split(" — ")[0] : error}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
556
577
|
return definition.ObjectType === "V"
|
|
557
578
|
? getDataObjectDefinition(resourceName, definition, options)
|
|
558
579
|
: getProcedureDefinition(resourceName, definition, options);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getDataObjectDefinition } from "./resourceGenerate.js";
|
|
3
|
+
import { entryToCLIOptions } from "./resourcesConfig.js";
|
|
4
|
+
const definition = {
|
|
5
|
+
ObjectType: "V",
|
|
6
|
+
Parameters: [
|
|
7
|
+
{ Name: "PrimKey", DataType: "uniqueidentifier", Nullable: false },
|
|
8
|
+
{ Name: "ProjectID", DataType: "nvarchar", Nullable: false },
|
|
9
|
+
{ Name: "ProjectName", DataType: "nvarchar", Nullable: true },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
12
|
+
function generate(uniqueFields) {
|
|
13
|
+
let options = entryToCLIOptions({
|
|
14
|
+
id: "dsPlans",
|
|
15
|
+
resource: "aviw_Production_Plans",
|
|
16
|
+
unique: "atbv_Production_Plans",
|
|
17
|
+
output: "src/dsPlans.ts",
|
|
18
|
+
global: true,
|
|
19
|
+
permissions: "IUD",
|
|
20
|
+
}, "example.com");
|
|
21
|
+
options.uniqueFields = uniqueFields;
|
|
22
|
+
return getDataObjectDefinition("aviw_Production_Plans", definition, options);
|
|
23
|
+
}
|
|
24
|
+
describe("fields outside the unique table", () => {
|
|
25
|
+
it("marks joined fields ignored so saves leave them out", () => {
|
|
26
|
+
let output = generate(["PrimKey", "ProjectID"]);
|
|
27
|
+
expect(output).toMatch(/"name": "ProjectName",[^}]*"ignored": true/);
|
|
28
|
+
expect(output).not.toMatch(/"name": "ProjectID",[^}]*"ignored": true/);
|
|
29
|
+
});
|
|
30
|
+
it("marks nothing when the unique table could not be read", () => {
|
|
31
|
+
expect(generate(undefined)).not.toContain('"ignored"');
|
|
32
|
+
});
|
|
33
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-vite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.17.0",
|
|
4
4
|
"description": "Tools to use and deploy Vite applications to Appframe",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,24 +30,24 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@olenbetong/appframe-auth": "^0.1.0",
|
|
33
|
-
"@olenbetong/appframe-data": "^1.8.
|
|
33
|
+
"@olenbetong/appframe-data": "^1.8.4",
|
|
34
34
|
"body-parser": "^2.3.0",
|
|
35
35
|
"chalk": "6.0.1",
|
|
36
36
|
"chokidar": "^5.0.0",
|
|
37
37
|
"commander": "15.0.0",
|
|
38
|
-
"dotenv": "^18.0.
|
|
38
|
+
"dotenv": "^18.0.8",
|
|
39
39
|
"fuzzy": "^0.1.3",
|
|
40
|
-
"inquirer": "^14.2.
|
|
40
|
+
"inquirer": "^14.2.4",
|
|
41
41
|
"inquirer-autocomplete-standalone": "^0.8.1",
|
|
42
|
-
"jsdom": "30.1.
|
|
42
|
+
"jsdom": "30.1.3",
|
|
43
43
|
"rollup-plugin-visualizer": "^7.1.1",
|
|
44
44
|
"yaml": "^2.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/jsdom": "^30.0.0",
|
|
48
|
-
"@types/node": "26.6.
|
|
48
|
+
"@types/node": "26.6.5",
|
|
49
49
|
"typescript": "7.0.2",
|
|
50
|
-
"vite": "8.3.
|
|
50
|
+
"vite": "8.3.5"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"vite": ">=8.1.5"
|