@olenbetong/appframe-vite 6.9.0 → 6.11.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 +300 -297
- package/lib/build.d.ts +10 -4
- package/lib/build.js +45 -6
- package/lib/index.js +21 -13
- package/lib/resourceGenerate.js +8 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,297 +1,300 @@
|
|
|
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
|
-
Make sure your Appframe credentials are available as environment variables:
|
|
37
|
-
|
|
38
|
-
- `APPFRAME_LOGIN`: username
|
|
39
|
-
- `APPFRAME_PWD`: password
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `POST /api/
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
|
260
|
-
|
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
271
|
-
| `
|
|
272
|
-
| `
|
|
273
|
-
| `
|
|
274
|
-
| `
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
277
|
-
| `
|
|
278
|
-
| `
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
Make sure your Appframe credentials are available as environment variables:
|
|
37
|
+
|
|
38
|
+
- `APPFRAME_LOGIN`: username
|
|
39
|
+
- `APPFRAME_PWD`: password
|
|
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
|
+
|
|
283
|
+
A top-level `server` key can override the hostname (defaults to `appframe.proxy.hostname` from `package.json`).
|
|
284
|
+
|
|
285
|
+
**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.
|
|
286
|
+
|
|
287
|
+
## `@olenbetong/appframe-vite/resources` export
|
|
288
|
+
|
|
289
|
+
Shared code generation utilities for Node.js consumers:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import {
|
|
293
|
+
fetchAndGenerate,
|
|
294
|
+
buildYamlConfig,
|
|
295
|
+
parseYamlConfig,
|
|
296
|
+
writeGeneratedFile,
|
|
297
|
+
getCustomImportPath,
|
|
298
|
+
type CLIOptions,
|
|
299
|
+
} from "@olenbetong/appframe-vite/resources";
|
|
300
|
+
```
|
package/lib/build.d.ts
CHANGED
|
@@ -22,14 +22,20 @@ export declare function usesPlatform(appframe: {
|
|
|
22
22
|
*/
|
|
23
23
|
export declare const PLATFORM_UPSTREAM: string[];
|
|
24
24
|
export type PlatformManifest = {
|
|
25
|
+
/**
|
|
26
|
+
* The `@olenbetong/synergi-platform` version the app was built alongside - the one its
|
|
27
|
+
* article is pinned to on deploy. Missing when the build found no platform package.
|
|
28
|
+
*/
|
|
29
|
+
platform?: string;
|
|
25
30
|
/** The package versions the app resolved at build time. */
|
|
26
31
|
upstream: Record<string, string>;
|
|
27
32
|
};
|
|
28
33
|
/**
|
|
29
|
-
* Writes `dist/platform.json` after a platform build: the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* Writes `dist/platform.json` after a platform build: the platform version the app was built
|
|
35
|
+
* alongside and the versions of the platform packages it compiled against. The deploy pins the
|
|
36
|
+
* article to that platform version (its `PlatformHead` block) and compares the versions with the
|
|
37
|
+
* manifest that version serves on the target site, since an app compiled against one React and
|
|
38
|
+
* run on another is a runtime error, not a build error.
|
|
33
39
|
*/
|
|
34
40
|
export declare function platformManifest(): Plugin;
|
|
35
41
|
export declare function addAppframeBuildConfig(config: UserConfig): Promise<UserConfig>;
|
package/lib/build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { visualizer } from "rollup-plugin-visualizer";
|
|
5
5
|
import { esmExternalRequirePlugin } from "vite";
|
|
6
6
|
import { importJson } from "./importJson.js";
|
|
@@ -27,6 +27,9 @@ export const PLATFORM_EXTERNALS = [
|
|
|
27
27
|
"@olenbetong/appframe-ds/mdi",
|
|
28
28
|
"@olenbetong/appframe-ds/progress",
|
|
29
29
|
"@olenbetong/appframe-ds/report-downloader",
|
|
30
|
+
"pdfjs-dist",
|
|
31
|
+
"react-pdf",
|
|
32
|
+
"pdf-lib",
|
|
30
33
|
];
|
|
31
34
|
/**
|
|
32
35
|
* The IIFE global each platform specifier is published under (`buildConfig.entries` in the
|
|
@@ -49,6 +52,9 @@ export const PLATFORM_GLOBALS = {
|
|
|
49
52
|
"@olenbetong/appframe-ds/mdi": "SynergiPlatform.appframeDsMdi",
|
|
50
53
|
"@olenbetong/appframe-ds/progress": "SynergiPlatform.appframeDsProgress",
|
|
51
54
|
"@olenbetong/appframe-ds/report-downloader": "SynergiPlatform.appframeDsReportDownloader",
|
|
55
|
+
"pdfjs-dist": "SynergiPlatform.pdfjs",
|
|
56
|
+
"react-pdf": "SynergiPlatform.reactPdf",
|
|
57
|
+
"pdf-lib": "SynergiPlatform.pdfLib",
|
|
52
58
|
};
|
|
53
59
|
export function usesPlatform(appframe) {
|
|
54
60
|
return appframe?.build?.platform === true;
|
|
@@ -68,19 +74,52 @@ export const PLATFORM_UPSTREAM = [
|
|
|
68
74
|
"@olenbetong/appframe-data",
|
|
69
75
|
"@olenbetong/appframe-react",
|
|
70
76
|
"@olenbetong/appframe-ds",
|
|
77
|
+
"pdfjs-dist",
|
|
78
|
+
"react-pdf",
|
|
79
|
+
"pdf-lib",
|
|
71
80
|
];
|
|
72
81
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
82
|
+
* The platform package's version as the app sees it: its own copy under node_modules when it
|
|
83
|
+
* depends on `@olenbetong/synergi-platform`, otherwise the workspace's `packages/synergi-platform`
|
|
84
|
+
* found by walking up from the app - every app lives in the monorepo next to it.
|
|
85
|
+
*/
|
|
86
|
+
async function platformVersion(cwd) {
|
|
87
|
+
let candidates = [resolve(cwd, "node_modules", "@olenbetong", "synergi-platform", "package.json")];
|
|
88
|
+
let dir = cwd;
|
|
89
|
+
while (true) {
|
|
90
|
+
candidates.push(resolve(dir, "packages", "synergi-platform", "package.json"));
|
|
91
|
+
let parent = dirname(dir);
|
|
92
|
+
if (parent === dir)
|
|
93
|
+
break;
|
|
94
|
+
dir = parent;
|
|
95
|
+
}
|
|
96
|
+
for (let file of candidates) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(await readFile(file, "utf8")).version;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Not here; try the next candidate.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Writes `dist/platform.json` after a platform build: the platform version the app was built
|
|
108
|
+
* alongside and the versions of the platform packages it compiled against. The deploy pins the
|
|
109
|
+
* article to that platform version (its `PlatformHead` block) and compares the versions with the
|
|
110
|
+
* manifest that version serves on the target site, since an app compiled against one React and
|
|
111
|
+
* run on another is a runtime error, not a build error.
|
|
77
112
|
*/
|
|
78
113
|
export function platformManifest() {
|
|
79
114
|
return {
|
|
80
115
|
name: "appframe:platform-manifest",
|
|
81
116
|
apply: "build",
|
|
82
117
|
async writeBundle() {
|
|
83
|
-
let manifest = { upstream: {} };
|
|
118
|
+
let manifest = { platform: await platformVersion(process.cwd()), upstream: {} };
|
|
119
|
+
if (manifest.platform === undefined) {
|
|
120
|
+
delete manifest.platform;
|
|
121
|
+
this.warn("No @olenbetong/synergi-platform package found; dist/platform.json records no platform version and the app cannot be deployed to a platform article.");
|
|
122
|
+
}
|
|
84
123
|
for (let name of PLATFORM_UPSTREAM) {
|
|
85
124
|
let file = resolve(process.cwd(), "node_modules", name, "package.json");
|
|
86
125
|
try {
|
package/lib/index.js
CHANGED
|
@@ -22,19 +22,27 @@ let lastHostname;
|
|
|
22
22
|
let watcher;
|
|
23
23
|
let resourcesWatcher = null;
|
|
24
24
|
let resourcesDebounce = null;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Restarts the dev server when the app's package.json changes its hostname. Started with the
|
|
27
|
+
* dev server, never at import: a watcher keeps node alive, and the deploy and release scripts
|
|
28
|
+
* import this package for its exports without ever running a server.
|
|
29
|
+
*/
|
|
30
|
+
function watchAppPackage() {
|
|
31
|
+
watcher?.close();
|
|
32
|
+
try {
|
|
33
|
+
let appPkgUrl = `file://${process.cwd()}/package.json`;
|
|
34
|
+
watcher = watch(appPkgUrl).on("all", async () => {
|
|
35
|
+
if (server) {
|
|
36
|
+
let { hostname } = await getLoginInfo();
|
|
37
|
+
if (lastHostname !== hostname) {
|
|
38
|
+
server.restart(false);
|
|
39
|
+
}
|
|
32
40
|
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.log(createLogMessage(`failed to watch package.json: ${error.message}`, { type: "warn" }));
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
const jsonParser = bodyParser.json();
|
|
40
48
|
export default function appframe(options = {}) {
|
|
@@ -121,10 +129,10 @@ export default function appframe(options = {}) {
|
|
|
121
129
|
},
|
|
122
130
|
};
|
|
123
131
|
if (command === "build") {
|
|
124
|
-
watcher.close();
|
|
125
132
|
await addAppframeBuildConfig(config);
|
|
126
133
|
}
|
|
127
134
|
else {
|
|
135
|
+
watchAppPackage();
|
|
128
136
|
let proxy = config.server?.proxy ?? {};
|
|
129
137
|
let routes = getProxyRoutes();
|
|
130
138
|
let { hostname, username, password } = await getLoginInfo();
|
package/lib/resourceGenerate.js
CHANGED
|
@@ -302,10 +302,10 @@ export function getProcedureDefinition(name, procDefinition, options) {
|
|
|
302
302
|
output.push(`export type ${paramTypeName} = null | undefined | Record<string, unknown>;\n`);
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${options.typesJsonReturnType ?? "{ Table?: unknown[] }"}>` : ""}({
|
|
306
|
-
procedureId: "${name}",
|
|
307
|
-
parameters: ${JSON.stringify(parameters, null, 2)},
|
|
308
|
-
timeout: ${options.timeout ?? 30000}${options.transaction === false ? ",\n transaction: false" : ""}
|
|
305
|
+
output.push(`export const ${procName} = new ${options.global ? "af." : ""}ProcedureAPI${options.types ? `<${paramTypeName}, ${options.typesJsonReturnType ?? "{ Table?: unknown[] }"}>` : ""}({
|
|
306
|
+
procedureId: "${name}",
|
|
307
|
+
parameters: ${JSON.stringify(parameters, null, 2)},
|
|
308
|
+
timeout: ${options.timeout ?? 30000}${options.transaction === false ? ",\n transaction: false" : ""}
|
|
309
309
|
});`);
|
|
310
310
|
if (options.expose) {
|
|
311
311
|
output.push("");
|
|
@@ -389,7 +389,7 @@ export function getDataObjectDefinition(name, viewDefinition, options) {
|
|
|
389
389
|
}
|
|
390
390
|
fieldTypes += ` ${field.name}: ${type};\n`;
|
|
391
391
|
}
|
|
392
|
-
types = `export type ${typeName} = {
|
|
392
|
+
types = `export type ${typeName} = {
|
|
393
393
|
${fieldTypes}}`;
|
|
394
394
|
}
|
|
395
395
|
if (options.global) {
|
|
@@ -473,9 +473,9 @@ ${fieldTypes}}`;
|
|
|
473
473
|
parametersOption.push(`distinctRows: true`);
|
|
474
474
|
}
|
|
475
475
|
dsOptions.push(`parameters: {\n\t\t${parametersOption.join(",\n\t\t")}\n\t}`);
|
|
476
|
-
output += `export const ${options.id} = ${api}({
|
|
477
|
-
${dsOptions.join(",\n\t")}
|
|
478
|
-
});
|
|
476
|
+
output += `export const ${options.id} = ${api}({
|
|
477
|
+
${dsOptions.join(",\n\t")}
|
|
478
|
+
});
|
|
479
479
|
`;
|
|
480
480
|
if (options.expose) {
|
|
481
481
|
let id = typeof options.expose === "string" ? options.expose : options.id;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-vite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0",
|
|
4
4
|
"description": "Tools to use and deploy Vite applications to Appframe",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"fuzzy": "^0.1.3",
|
|
38
38
|
"inquirer": "^14.2.2",
|
|
39
39
|
"inquirer-autocomplete-standalone": "^0.8.1",
|
|
40
|
-
"jsdom": "30.0
|
|
40
|
+
"jsdom": "30.1.0",
|
|
41
41
|
"rollup-plugin-visualizer": "^7.1.1",
|
|
42
42
|
"yaml": "^2.9.1",
|
|
43
43
|
"@olenbetong/appframe-data": "1.7.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/jsdom": "^30.0.0",
|
|
47
|
-
"@types/node": "26.
|
|
47
|
+
"@types/node": "26.6.1",
|
|
48
48
|
"typescript": "7.0.2",
|
|
49
49
|
"vite": "8.3.0"
|
|
50
50
|
},
|