@venizia/ardor 0.4.8-1 → 0.4.8-11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,8 @@ control comes from `@venizia/ignis-inversion`, and the data layer speaks the que
13
13
  bun add @venizia/ardor @venizia/ignis-inversion @venizia/ignis-filter @venizia/ignis-kernel @venizia/ignis-helpers reflect-metadata
14
14
  ```
15
15
 
16
+ The React-side peers (`react`, `react-dom`, `react-redux`, `@reduxjs/toolkit`, `@tanstack/react-query`, `ra-core`, `ra-i18n-polyglot`, `react-router-dom`) are installed too; the full `bun add` line is in the [setup guide](https://ardor.venizia.ai/guides/get-started/setup).
17
+
16
18
  ## The `Ardor` namespace
17
19
 
18
20
  `Ardor` gathers every React hook of the react and admin packages under one name: `Ardor.useRepository(...)`, `Ardor.useTranslate()`. It is the same function as the bare `useRepository`, the Rules of Hooks check the call, and a bundler keeps only the hooks you use, so it adds no bytes to an app.
@@ -20,8 +22,10 @@ bun add @venizia/ardor @venizia/ignis-inversion @venizia/ignis-filter @venizia/i
20
22
  ```tsx
21
23
  import { Ardor } from '@venizia/ardor';
22
24
 
25
+ import { OrdersRepository } from './orders.repository';
26
+
23
27
  export const Orders = () => {
24
- const repository = Ardor.useRepository({ key: 'repositories.Orders' });
28
+ const repository = Ardor.useRepository({ target: OrdersRepository });
25
29
 
26
30
  return <div>{String(repository)}</div>;
27
31
  };
@@ -29,7 +33,13 @@ export const Orders = () => {
29
33
 
30
34
  ## Moving to the new names
31
35
 
32
- `bun run ardor codemod namespace src` lists the removed bare names that have a replacement (`getRuntimeConfig` to `RuntimeConfig.get`, `isDefined` to `ValueUtility.isDefined`, `NullableType` to `undefined | null | void`, `Logger.getInstance({ scope })` to `LoggerFactory.getLogger([scope])`, ...) and the removed names with none, and writes nothing; `--write` applies it, `--check` fails while any is left, and `--hooks` also moves hooks imported from `@venizia/ardor` to `Ardor.useX`. It needs the `typescript` package.
36
+ `bun run ardor codemod namespace src` lists the removed bare names that have a replacement (`getRuntimeConfig` to `RuntimeConfig.get`, `isDefined` to `ValueUtility.isDefined`, `NullableType` to `undefined | null | void`, `Logger.getInstance({ scope })` to `LoggerFactory.getLogger([scope])`, ...) and the removed names with none, and writes nothing; `--write` applies it (it refuses a file with local changes unless you add `--allow-dirty`), `--check` fails while any is left, and `--hooks` also moves hooks imported from `@venizia/ardor` to `Ardor.useX`. It needs the `typescript` package.
37
+
38
+ ## Subpaths
39
+
40
+ - `@venizia/ardor/repository` - `HttpRepository` and the datasource settings; needs the optional peer `@venizia/ignis-connectors`.
41
+ - `@venizia/ardor/testing` - `assertArdorApplicationBoots` and fakes of `ResizeObserver`, `IntersectionObserver`, `matchMedia`, storage and the clipboard, for your test suite.
42
+ - `@venizia/ardor/vite` - `buildInfoFile`, a Vite plugin that writes the build info file; needs the optional peers `@venizia/ignis-boot` and `vite`.
33
43
 
34
44
  ## Quick start
35
45
 
@@ -108,7 +118,8 @@ export class Application extends BaseArdorApplication {
108
118
  ```
109
119
 
110
120
  The full guide, including the root component and the typed hooks, is in the
111
- [repository README](https://github.com/VENIZIA-AI/ardor#readme).
121
+ [Quickstart](https://ardor.venizia.ai/guides/get-started/5-minute-quickstart) and the
122
+ [references](https://ardor.venizia.ai/references/hooks).
112
123
 
113
124
  ## Migrating from `@minimaltech/ra-core-infra`
114
125
 
@@ -3,7 +3,7 @@ import { NamespaceCommand } from './cli.js';
3
3
  const USAGE = `Usage: ardor <command>
4
4
 
5
5
  Commands:
6
- codemod namespace move the deprecated bare names to the class members that replace them
6
+ codemod namespace move the removed bare names to the class members that replace them
7
7
 
8
8
  Run "ardor codemod namespace --help" for the options.`;
9
9
  /** The `ardor` bin of `@venizia/ardor`: `ardor codemod namespace [paths...] [--write | --check]`. */
@@ -1,6 +1,6 @@
1
1
  /** `ardor codemod namespace`: a dry run unless `--write`; `--check` is the dry run that fails on work left. */
2
2
  export declare class NamespaceCommand {
3
- static readonly USAGE = "Usage: ardor codemod namespace [paths...] [--hooks] [--write [--allow-dirty] | --check]\n\nMoves the names removed from @venizia/ardor to what replaces them:\n\n isDefined, isString, isNumber, isObject, isValidDate ValueUtility.<name>\n isBrowser BrowserUtility.isBrowser\n toDataUrl FileUtility.toDataUrl\n getRuntimeConfig RuntimeConfig.get\n createAppDispatch, createAppSelectors DispatchHooks.create, SelectorHooks.create\n resolveApplicationProviders ApplicationProviders.resolve\n hasPendingFeatureMessages, loadFeatureMessages Features.hasPendingMessages, Features.loadMessages\n NumberIdType, StringIdType, NullableType number, string, undefined | null | void\n BaseHelper, LogLevels, ILogger, TLogLevel the same names from @venizia/ignis-helpers/core\n Logger.getInstance({ scope }), new Logger({ scope }) LoggerFactory.getLogger([scope]) (enableDebug is dropped, with a note)\n Logger (a type) ILogger\n\nRemoved with nothing to write in their place, so only listed with their line: isEditableTarget,\nTDataCount, ISyncFC, App.DEFAULT_LOCALE, HeaderConstants.REQUEST_DEVICE_INFO, HeaderConstants.RESPONSE_FORMAT,\nthe enableDebug prop of ArdorApplication (pass logger instead), and a Logger use with no one-line form.\n\nIt rewrites a name imported from @venizia/ardor, @venizia/ardor-kernel, @venizia/ardor-react or\n@venizia/ardor-admin, its uses, and the import. It reads syntax only: a name the file also declares\nitself, a re-export by name and a class name already taken are listed with their line and left for you.\n\n --hooks also move a hook imported from @venizia/ardor to the Ardor namespace (useRepository()\n becomes Ardor.useRepository()). A hook imported from @venizia/ardor-react or\n @venizia/ardor-admin is never touched: those packages cannot import Ardor.\n --write write the changed files (without it, nothing is written)\n --allow-dirty with --write, also write a file that has local changes, such as one another codemod\n just changed (devz codemod namespace-functions --write leaves the tree dirty)\n --check dry run that exits 1 when anything would change or is left (for CI)\n --help print this text and exit\n\nWith no flag it lists what would change and writes nothing. Inside a git repository it reads the\nfiles git knows (tracked, or untracked and not ignored), and --write refuses a file with local\nchanges (changed, staged or untracked), so git can undo the codemod: the file is listed as \"refused\"\nwith its path, the summary counts it and the run exits 1. Commit or stash it and run again, or run\nagain with --allow-dirty to write it anyway (it keeps the other codemod's edits). To run two codemods\none after the other in a dirty tree, give this one --write --allow-dirty.\nRun your formatter afterwards: the new names are appended to the import they join.";
3
+ static readonly USAGE = "Usage: ardor codemod namespace [paths...] [--hooks] [--write [--allow-dirty] | --check]\n\nMoves the names removed from @venizia/ardor to what replaces them:\n\n isDefined, isString, isNumber, isObject, isValidDate ValueUtility.<name>\n isBrowser BrowserUtility.isBrowser\n toDataUrl FileUtility.toDataUrl\n getRuntimeConfig RuntimeConfig.get\n createAppDispatch, createAppSelectors DispatchHooks.create, SelectorHooks.create\n resolveApplicationProviders ApplicationProviders.resolve\n hasPendingFeatureMessages, loadFeatureMessages Features.hasPendingMessages, Features.loadMessages\n NumberIdType, StringIdType, NullableType number, string, undefined | null | void\n BaseHelper, LogLevels, ILogger, TLogLevel the same names from @venizia/ignis-helpers/core\n Logger.getInstance({ scope }), new Logger({ scope }) LoggerFactory.getLogger([scope]) (enableDebug is dropped, with a note)\n Logger (a type) ILogger\n ISendResponse INetworkResponse\n\nRemoved with nothing to write in their place, so only listed with their line: isEditableTarget,\nTDataCount, ISyncFC, IListFilter.offset (use skip; listed wherever a file that imports IListFilter or ListFilterBuilder reads or sets .offset), useSizer (use useElementSize), useInjectableContainer (renamed useApplicationContainer), App.DEFAULT_LOCALE, HeaderConstants.REQUEST_DEVICE_INFO, HeaderConstants.RESPONSE_FORMAT,\nthe enableDebug prop of ArdorApplication (pass logger instead), and a Logger use with no one-line form.\n\nIt rewrites a name imported from @venizia/ardor, @venizia/ardor-kernel, @venizia/ardor-react or\n@venizia/ardor-admin, its uses, and the import. It reads syntax only: a name the file also declares\nitself, a re-export by name and a class name already taken are listed with their line and left for you.\n\n --hooks also move a hook imported from @venizia/ardor to the Ardor namespace (useRepository()\n becomes Ardor.useRepository()). A hook imported from @venizia/ardor-react or\n @venizia/ardor-admin is never touched: those packages cannot import Ardor.\n --write write the changed files (without it, nothing is written)\n --allow-dirty with --write, also write a file that has local changes, such as one another codemod\n just changed (devz codemod namespace-functions --write leaves the tree dirty)\n --check dry run that exits 1 when anything would change or is left (for CI)\n --help print this text and exit\n\n--write is all or nothing: it reads and rewrites every file in memory first, and writes only if every\nfile went through. A file that cannot be parsed or rewritten is listed as \"failed\" with its path, the\nrun exits 1 and nothing is written. A file that is not valid UTF-8 and would change is a failure too:\nit is left alone rather than damaged. Outside a git repository nothing can undo a write, so --write\nrefuses unless you give --allow-dirty.\n\nWith no flag it lists what would change and writes nothing. Inside a git repository it reads the\nfiles git knows (tracked, or untracked and not ignored), and --write refuses a file with local\nchanges (changed, staged or untracked), so git can undo the codemod: the file is listed as \"refused\"\nwith its path, the summary counts it and the run exits 1. Commit or stash it and run again, or run\nagain with --allow-dirty to write it anyway (it keeps the other codemod's edits). To run two codemods\none after the other in a dirty tree, give this one --write --allow-dirty.\nRun your formatter afterwards: the new names are appended to the import they join.";
4
4
  /** Runs the command and returns the exit code; prints to the console. */
5
5
  static run(opts: {
6
6
  root: string;
@@ -8,6 +8,15 @@ export declare class NamespaceCommand {
8
8
  }): Promise<number>;
9
9
  private static loadTypeScript;
10
10
  private static sweep;
11
+ /**
12
+ * Writes every file next to itself first, and renames them into place only when all were written,
13
+ * so a failure leaves no file changed. A file that is not the file that was read (a link swapped in,
14
+ * an edit made meanwhile) is refused: the rename replaces a link instead of following it, and a
15
+ * hard link is not written through.
16
+ */
17
+ private static writeAll;
18
+ /** Text for the terminal: control, format and line-separator characters, such as an escape sequence in a file name, become `?`. */
19
+ private static clean;
11
20
  private static report;
12
21
  }
13
22
  //# sourceMappingURL=cli.d.ts.map
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
1
+ import { chmodSync, existsSync, lstatSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
2
2
  import { relative, resolve } from 'node:path';
3
3
  import { CodemodFiles } from './files.js';
4
4
  import { Migration } from './migrate.js';
@@ -17,9 +17,10 @@ Moves the names removed from @venizia/ardor to what replaces them:
17
17
  BaseHelper, LogLevels, ILogger, TLogLevel the same names from @venizia/ignis-helpers/core
18
18
  Logger.getInstance({ scope }), new Logger({ scope }) LoggerFactory.getLogger([scope]) (enableDebug is dropped, with a note)
19
19
  Logger (a type) ILogger
20
+ ISendResponse INetworkResponse
20
21
 
21
22
  Removed with nothing to write in their place, so only listed with their line: isEditableTarget,
22
- TDataCount, ISyncFC, App.DEFAULT_LOCALE, HeaderConstants.REQUEST_DEVICE_INFO, HeaderConstants.RESPONSE_FORMAT,
23
+ TDataCount, ISyncFC, IListFilter.offset (use skip; listed wherever a file that imports IListFilter or ListFilterBuilder reads or sets .offset), useSizer (use useElementSize), useInjectableContainer (renamed useApplicationContainer), App.DEFAULT_LOCALE, HeaderConstants.REQUEST_DEVICE_INFO, HeaderConstants.RESPONSE_FORMAT,
23
24
  the enableDebug prop of ArdorApplication (pass logger instead), and a Logger use with no one-line form.
24
25
 
25
26
  It rewrites a name imported from @venizia/ardor, @venizia/ardor-kernel, @venizia/ardor-react or
@@ -35,6 +36,12 @@ itself, a re-export by name and a class name already taken are listed with their
35
36
  --check dry run that exits 1 when anything would change or is left (for CI)
36
37
  --help print this text and exit
37
38
 
39
+ --write is all or nothing: it reads and rewrites every file in memory first, and writes only if every
40
+ file went through. A file that cannot be parsed or rewritten is listed as "failed" with its path, the
41
+ run exits 1 and nothing is written. A file that is not valid UTF-8 and would change is a failure too:
42
+ it is left alone rather than damaged. Outside a git repository nothing can undo a write, so --write
43
+ refuses unless you give --allow-dirty.
44
+
38
45
  With no flag it lists what would change and writes nothing. Inside a git repository it reads the
39
46
  files git knows (tracked, or untracked and not ignored), and --write refuses a file with local
40
47
  changes (changed, staged or untracked), so git can undo the codemod: the file is listed as "refused"
@@ -97,6 +104,11 @@ export class NamespaceCommand {
97
104
  const paths = args.filter((argument) => !argument.startsWith('-'));
98
105
  const targets = (paths.length > 0 ? paths : ['.']).map((path) => resolve(root, path));
99
106
  const hooks = args.includes(CodemodFlags.HOOKS);
107
+ const missing = targets.filter((target) => !existsSync(target));
108
+ if (missing.length > 0) {
109
+ console.error(`ardor codemod: no such file or folder: ${missing.map(NamespaceCommand.clean).join(', ')}`);
110
+ return USAGE_ERROR_CODE;
111
+ }
100
112
  return NamespaceCommand.sweep({ root, ts, targets, hooks, isWrite, isCheck, isAllowDirty });
101
113
  }
102
114
  static async loadTypeScript() {
@@ -113,43 +125,118 @@ export class NamespaceCommand {
113
125
  static sweep(opts) {
114
126
  const { root, ts, targets, hooks, isWrite, isCheck, isAllowDirty } = opts;
115
127
  const files = [...new Set(targets.flatMap((target) => CodemodFiles.collect({ target })))];
116
- const dirty = isWrite && !isAllowDirty
117
- ? new Set(targets.flatMap((target) => [...(CodemodFiles.dirty({ target }) ?? [])]))
118
- : new Set();
128
+ const dirtyOf = targets.map((target) => CodemodFiles.dirty({ target }));
129
+ if (isWrite && !isAllowDirty && dirtyOf.some((dirty) => dirty === undefined)) {
130
+ console.error(`ardor codemod: this is not a git repository, so nothing can undo ${CodemodFlags.WRITE}; run again with ${CodemodFlags.ALLOW_DIRTY} to write anyway`);
131
+ return 1;
132
+ }
133
+ const dirty = new Set(isAllowDirty ? [] : dirtyOf.flatMap((paths) => [...(paths ?? [])]));
119
134
  const changes = [];
135
+ const pending = [];
120
136
  const refused = [];
137
+ const failed = [];
121
138
  const left = [];
122
139
  const notes = [];
123
140
  for (const path of files) {
124
- const text = readFileSync(path, 'utf8');
125
- const result = Migration.run({ ts, fileName: path, text, hooks });
126
- for (const finding of result.findings) {
127
- (finding.kind === 'note' ? notes : left).push({ path, finding });
141
+ try {
142
+ const stat = lstatSync(path);
143
+ const bytes = readFileSync(path);
144
+ const text = bytes.toString('utf8');
145
+ const result = Migration.run({ ts, fileName: path, text, hooks });
146
+ for (const finding of result.findings) {
147
+ (finding.kind === 'note' ? notes : left).push({ path, finding });
148
+ }
149
+ if (result.text === text) {
150
+ continue;
151
+ }
152
+ if (!Buffer.from(text, 'utf8').equals(bytes)) {
153
+ failed.push({ path, reason: 'it is not valid UTF-8, so a rewrite would damage it' });
154
+ continue;
155
+ }
156
+ if (isWrite && dirty.has(path)) {
157
+ refused.push(path);
158
+ continue;
159
+ }
160
+ changes.push({ path, renamed: result.renamed });
161
+ pending.push({ path, text: result.text, stat });
128
162
  }
129
- if (result.text === text) {
130
- continue;
163
+ catch (error) {
164
+ failed.push({ path, reason: error instanceof Error ? error.message : String(error) });
131
165
  }
132
- if (isWrite && dirty.has(path)) {
133
- refused.push(path);
134
- continue;
166
+ }
167
+ if (isWrite && failed.length === 0) {
168
+ failed.push(...NamespaceCommand.writeAll({ pending }));
169
+ }
170
+ return NamespaceCommand.report({
171
+ root,
172
+ isWrite,
173
+ isCheck,
174
+ changes,
175
+ refused,
176
+ failed,
177
+ left,
178
+ notes,
179
+ });
180
+ }
181
+ /**
182
+ * Writes every file next to itself first, and renames them into place only when all were written,
183
+ * so a failure leaves no file changed. A file that is not the file that was read (a link swapped in,
184
+ * an edit made meanwhile) is refused: the rename replaces a link instead of following it, and a
185
+ * hard link is not written through.
186
+ */
187
+ static writeAll(opts) {
188
+ const staged = [];
189
+ const failures = [];
190
+ for (const { path, text, stat } of opts.pending) {
191
+ const temp = `${path}.${process.pid}.codemod`;
192
+ try {
193
+ const now = lstatSync(path);
194
+ if (!now.isFile() || now.mtimeMs !== stat.mtimeMs || now.size !== stat.size) {
195
+ failures.push({
196
+ path,
197
+ reason: 'it changed while the codemod ran, so it was not written',
198
+ });
199
+ continue;
200
+ }
201
+ writeFileSync(temp, text);
202
+ chmodSync(temp, now.mode & 0o7777);
203
+ staged.push({ path, temp });
204
+ }
205
+ catch (error) {
206
+ rmSync(temp, { force: true });
207
+ failures.push({ path, reason: error instanceof Error ? error.message : String(error) });
135
208
  }
136
- changes.push({ path, renamed: result.renamed });
137
- if (isWrite) {
138
- writeFileSync(path, result.text);
209
+ }
210
+ if (failures.length > 0) {
211
+ for (const { temp } of staged) {
212
+ rmSync(temp, { force: true });
139
213
  }
214
+ return failures;
140
215
  }
141
- return NamespaceCommand.report({ root, isWrite, isCheck, changes, refused, left, notes });
216
+ for (const { path, temp } of staged) {
217
+ renameSync(temp, path);
218
+ }
219
+ return [];
220
+ }
221
+ /** Text for the terminal: control, format and line-separator characters, such as an escape sequence in a file name, become `?`. */
222
+ static clean(text) {
223
+ return text.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '?');
142
224
  }
143
225
  static report(opts) {
144
- const { root, isWrite, isCheck, changes, refused, left, notes } = opts;
145
- const verb = isWrite ? 'changed' : 'would change';
146
- const show = (path) => relative(root, path) || path;
226
+ const { root, isWrite, isCheck, changes, refused, failed, left, notes } = opts;
227
+ // Nothing is written when a file failed, so the files are not "changed".
228
+ const verb = isWrite && failed.length === 0 ? 'changed' : 'would change';
229
+ // A path comes from the file system: a control character in it is shown as `?`, never sent to the terminal.
230
+ const show = (path) => NamespaceCommand.clean(relative(root, path) || path);
147
231
  for (const change of changes) {
148
232
  console.log(`${verb} ${show(change.path)} (${change.renamed} use${change.renamed === 1 ? '' : 's'})`);
149
233
  }
150
234
  for (const path of refused) {
151
235
  console.error(`refused ${show(path)}: it has local changes, so git cannot undo the codemod there; commit or stash it and run again, or run again with ${CodemodFlags.ALLOW_DIRTY}`);
152
236
  }
237
+ for (const { path, reason } of failed) {
238
+ console.error(NamespaceCommand.clean(`failed ${show(path)}: ${reason}`));
239
+ }
153
240
  // A note is a change made on purpose that is worth knowing; it is not work left, so --check ignores it.
154
241
  for (const { path, finding } of notes) {
155
242
  console.log(`note ${show(path)}:${finding.line} ${finding.name}: ${finding.reason}`);
@@ -157,9 +244,14 @@ export class NamespaceCommand {
157
244
  for (const { path, finding } of left) {
158
245
  console.error(`left ${show(path)}:${finding.line} ${finding.name}: ${finding.reason}`);
159
246
  }
160
- const refusedNote = refused.length > 0 ? `, ${refused.length} refused (listed above)` : '';
247
+ const refusedNote = [
248
+ refused.length > 0 ? `, ${refused.length} refused (listed above)` : '',
249
+ failed.length > 0
250
+ ? `, ${failed.length} failed (listed above)${isWrite ? ', nothing was written' : ''}`
251
+ : '',
252
+ ].join('');
161
253
  console.log(`${changes.length} file${changes.length === 1 ? '' : 's'} ${verb}${refusedNote}, ${left.length} left for you${isWrite || changes.length === 0 ? '' : '. Run again with --write to apply.'}`);
162
- if (refused.length > 0) {
254
+ if (refused.length > 0 || failed.length > 0) {
163
255
  return 1;
164
256
  }
165
257
  return isCheck && (changes.length > 0 || left.length > 0) ? 1 : 0;
@@ -7,7 +7,7 @@ export declare class CodemodFiles {
7
7
  static isSource(opts: {
8
8
  name: string;
9
9
  }): boolean;
10
- /** Every source file under `target`; a file target is returned as it is. */
10
+ /** Every source file under `target`; a file target is returned as it is. A symlink or a file that is not a source file is never returned. */
11
11
  static collect(opts: {
12
12
  target: string;
13
13
  }): string[];
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { lstatSync, readdirSync, statSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', '.git']);
5
5
  const SOURCE_PATTERN = /\.(?:ts|tsx|mts|cts)$/;
@@ -13,11 +13,15 @@ export class CodemodFiles {
13
13
  static isSource(opts) {
14
14
  return SOURCE_PATTERN.test(opts.name) && !DECLARATION_PATTERN.test(opts.name);
15
15
  }
16
- /** Every source file under `target`; a file target is returned as it is. */
16
+ /** Every source file under `target`; a file target is returned as it is. A symlink or a file that is not a source file is never returned. */
17
17
  static collect(opts) {
18
18
  const { target } = opts;
19
- if (statSync(target).isFile()) {
20
- return [target];
19
+ // Read without following a link: a symlink is never a file to write, whatever it points to.
20
+ if (lstatSync(target).isFile()) {
21
+ return CodemodFiles.isSource({ name: target }) ? [target] : [];
22
+ }
23
+ if (!statSync(target).isDirectory()) {
24
+ return [];
21
25
  }
22
26
  return CodemodFiles.fromGit({ target }) ?? CodemodFiles.fromWalk({ target });
23
27
  }
@@ -42,7 +46,7 @@ export class CodemodFiles {
42
46
  .filter((path) => path !== '' && CodemodFiles.isSource({ name: path }))
43
47
  .filter((path) => !path.split('/').some((segment) => SKIPPED_DIRECTORIES.has(segment)))
44
48
  .map((path) => join(target, path))
45
- .filter((path) => existsSync(path) && statSync(path).isFile());
49
+ .filter((path) => lstatSync(path, { throwIfNoEntry: false })?.isFile() === true);
46
50
  }
47
51
  static fromWalk(opts) {
48
52
  const files = [];
@@ -179,7 +179,7 @@ export class LoggerTransform {
179
179
  findings.push({
180
180
  line: lineOf(call),
181
181
  name: 'enableDebug',
182
- reason: 'note: dropped. debug lines are off by default now (a browser, or `DEBUG` unset); turn them on with `LoggerFactory.use({ level: LogLevels.DEBUG })` at startup',
182
+ reason: 'dropped. debug lines are off by default now (a browser, or `DEBUG` unset); turn them on with `LoggerFactory.use({ level: LogLevels.DEBUG })` at startup',
183
183
  kind: 'note',
184
184
  });
185
185
  }
@@ -34,11 +34,23 @@ export declare class TypeRenames {
34
34
  name: string;
35
35
  }): string | undefined;
36
36
  }
37
+ /** Types removed in favour of another export of the same packages: the import and each use are renamed. */
38
+ export declare class TypeExportRenames {
39
+ static readonly TARGETS: Readonly<Record<string, string>>;
40
+ static find(opts: {
41
+ name: string;
42
+ }): string | undefined;
43
+ }
37
44
  /** Exports and members removed with no code the codemod can write in their place. */
38
45
  export declare class RemovedNames {
39
46
  static readonly EXPORTS: Readonly<Record<string, string>>;
40
47
  /** Class name to removed static member to the reason. */
41
48
  static readonly MEMBERS: Readonly<Record<string, Readonly<Record<string, string>>>>;
49
+ /** A property removed from the list filter: listed in a file that imports one of `owners`, as the file may use it. */
50
+ static readonly PROPERTIES: Readonly<Record<string, {
51
+ owners: ReadonlySet<string>;
52
+ reason: string;
53
+ }>>;
42
54
  static findExport(opts: {
43
55
  name: string;
44
56
  }): string | undefined;
@@ -85,12 +85,23 @@ export class TypeRenames {
85
85
  return Object.hasOwn(this.TARGETS, opts.name) ? this.TARGETS[opts.name] : undefined;
86
86
  }
87
87
  }
88
+ /** Types removed in favour of another export of the same packages: the import and each use are renamed. */
89
+ export class TypeExportRenames {
90
+ static { this.TARGETS = {
91
+ ISendResponse: 'INetworkResponse',
92
+ }; }
93
+ static find(opts) {
94
+ return Object.hasOwn(this.TARGETS, opts.name) ? this.TARGETS[opts.name] : undefined;
95
+ }
96
+ }
88
97
  /** Exports and members removed with no code the codemod can write in their place. */
89
98
  export class RemovedNames {
90
99
  static { this.EXPORTS = {
91
100
  isEditableTarget: 'nothing in ARDOR called it; check the event target in your own code',
92
101
  TDataCount: 'nothing in ARDOR used it; declare the shape you need',
93
102
  ISyncFC: 'write the function type, (props: TProps) => ReactNode',
103
+ useInjectableContainer: 'renamed: call useApplicationContainer',
104
+ useSizer: 'use useElementSize({ ref, box: "border-box", round: true }) with a ref in place of the id',
94
105
  }; }
95
106
  /** Class name to removed static member to the reason. */
96
107
  static { this.MEMBERS = {
@@ -100,6 +111,13 @@ export class RemovedNames {
100
111
  RESPONSE_FORMAT: 'nothing in ARDOR sent or read this header',
101
112
  },
102
113
  }; }
114
+ /** A property removed from the list filter: listed in a file that imports one of `owners`, as the file may use it. */
115
+ static { this.PROPERTIES = {
116
+ offset: {
117
+ owners: new Set(['IListFilter', 'ListFilterBuilder']),
118
+ reason: 'the list filter has no offset, use skip',
119
+ },
120
+ }; }
103
121
  static findExport(opts) {
104
122
  return Object.hasOwn(this.EXPORTS, opts.name) ? this.EXPORTS[opts.name] : undefined;
105
123
  }
@@ -15,7 +15,7 @@ export interface INamespaceResult {
15
15
  findings: IFinding[];
16
16
  }
17
17
  /**
18
- * Rewrites the deprecated bare names of `@venizia/ardor` to their class members, in source text:
18
+ * Rewrites the removed bare names of `@venizia/ardor` to their class members, in source text:
19
19
  * `isDefined(x)` becomes `ValueUtility.isDefined(x)` and the import follows. With `hooks`, a hook
20
20
  * imported from `@venizia/ardor` itself becomes `Ardor.useX` (never one imported from the react or
21
21
  * admin package, which cannot import `Ardor`).
@@ -1,6 +1,6 @@
1
- import { FunctionRenames, HookRenames, NamespaceModules, RemovedNames, TypeRenames, } from './renames.js';
1
+ import { FunctionRenames, HookRenames, NamespaceModules, RemovedNames, TypeExportRenames, TypeRenames, } from './renames.js';
2
2
  /**
3
- * Rewrites the deprecated bare names of `@venizia/ardor` to their class members, in source text:
3
+ * Rewrites the removed bare names of `@venizia/ardor` to their class members, in source text:
4
4
  * `isDefined(x)` becomes `ValueUtility.isDefined(x)` and the import follows. With `hooks`, a hook
5
5
  * imported from `@venizia/ardor` itself becomes `Ardor.useX` (never one imported from the react or
6
6
  * admin package, which cannot import `Ardor`).
@@ -66,13 +66,15 @@ export class NamespaceTransform {
66
66
  });
67
67
  continue;
68
68
  }
69
- const typeText = TypeRenames.find({ name: imported });
69
+ const exportName = TypeExportRenames.find({ name: imported });
70
+ const typeText = exportName ?? TypeRenames.find({ name: imported });
70
71
  if (typeText) {
71
72
  renames.push({
72
73
  specifier,
73
74
  declaration: statement,
74
75
  local: specifier.name.text,
75
76
  typeText,
77
+ importName: exportName,
76
78
  references: [],
77
79
  });
78
80
  continue;
@@ -139,7 +141,9 @@ export class NamespaceTransform {
139
141
  else if ((ts.isFunctionDeclaration(node) ||
140
142
  ts.isClassDeclaration(node) ||
141
143
  ts.isFunctionExpression(node) ||
142
- ts.isClassExpression(node)) &&
144
+ ts.isClassExpression(node) ||
145
+ ts.isTypeAliasDeclaration(node) ||
146
+ ts.isInterfaceDeclaration(node)) &&
143
147
  node.name) {
144
148
  declared.add(node.name.text);
145
149
  }
@@ -162,6 +166,18 @@ export class NamespaceTransform {
162
166
  });
163
167
  return false;
164
168
  }
169
+ const { importName } = rename;
170
+ const takenBy = importName ? bindings.imported.get(importName) : undefined;
171
+ if (importName &&
172
+ (bindings.declared.has(importName) ||
173
+ (takenBy && !NamespaceModules.isValid(takenBy.module)))) {
174
+ findings.push({
175
+ line,
176
+ name: rename.local,
177
+ reason: `"${importName}" is already taken in this file; import it under another name and use it in place of ${rename.local}`,
178
+ });
179
+ return false;
180
+ }
165
181
  if (!owner || !rename.target) {
166
182
  return true;
167
183
  }
@@ -240,7 +256,8 @@ export class NamespaceTransform {
240
256
  }
241
257
  if (rename.typeText !== undefined) {
242
258
  // A type is a name in a type position only: `NullableType[]` needs parentheses once replaced.
243
- if (ts.isTypeReferenceNode(parent) && parent.typeName === node) {
259
+ if ((ts.isTypeReferenceNode(parent) && parent.typeName === node) ||
260
+ (ts.isExpressionWithTypeArguments(parent) && parent.expression === node)) {
244
261
  const grand = parent.parent;
245
262
  const wrap = rename.typeText.includes('|') &&
246
263
  (ts.isArrayTypeNode(grand) ||
@@ -278,7 +295,32 @@ export class NamespaceTransform {
278
295
  if (owners.size === 0) {
279
296
  return;
280
297
  }
298
+ const imported = new Set(owners.values());
281
299
  const visit = (node) => {
300
+ let name;
301
+ if (ts.isPropertyAccessExpression(node) ||
302
+ ts.isPropertyAssignment(node) ||
303
+ ts.isShorthandPropertyAssignment(node)) {
304
+ name = node.name;
305
+ }
306
+ else if (ts.isBindingElement(node) && ts.isObjectBindingPattern(node.parent)) {
307
+ name = node.propertyName ?? node.name;
308
+ }
309
+ else if (ts.isElementAccessExpression(node) &&
310
+ ts.isStringLiteral(node.argumentExpression)) {
311
+ name = node.argumentExpression;
312
+ }
313
+ const key = name && (ts.isIdentifier(name) || ts.isStringLiteral(name)) ? name : undefined;
314
+ const property = key && Object.hasOwn(RemovedNames.PROPERTIES, key.text)
315
+ ? RemovedNames.PROPERTIES[key.text]
316
+ : undefined;
317
+ if (key && property && [...property.owners].some((owner) => imported.has(owner))) {
318
+ findings.push({
319
+ line: sourceFile.getLineAndCharacterOfPosition(key.getStart(sourceFile)).line + 1,
320
+ name: `.${key.text}`,
321
+ reason: `removed: ${property.reason}`,
322
+ });
323
+ }
282
324
  if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) {
283
325
  const owner = owners.get(node.expression.text);
284
326
  const reason = owner
@@ -323,7 +365,17 @@ export class NamespaceTransform {
323
365
  for (const owner of owners) {
324
366
  added.add(owner);
325
367
  }
326
- const names = [...kept, ...owners];
368
+ const types = [];
369
+ for (const { declaration: from, importName, specifier } of used) {
370
+ if (from === declaration &&
371
+ importName &&
372
+ !bindings.imported.has(importName) &&
373
+ !added.has(importName)) {
374
+ added.add(importName);
375
+ types.push(`${specifier.isTypeOnly ? 'type ' : ''}${importName}`);
376
+ }
377
+ }
378
+ const names = [...kept, ...owners, ...types];
327
379
  if (names.length === 0) {
328
380
  edits.push(NamespaceTransform.removeImport({ sourceFile, declaration, clause, named, ts }));
329
381
  continue;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,24 @@
1
+ /**
2
+ * Every ARDOR hook under one name: `Ardor.useRepository(...)` is the same function as
3
+ * `useRepository`. Call a member by its name; do not index with a computed key (`Ardor[name]`), which
4
+ * keeps every hook in a bundle.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { Ardor, repository } from '@venizia/ardor';
9
+ *
10
+ * @repository()
11
+ * class OrdersRepository {
12
+ * count() { return 3; }
13
+ * }
14
+ *
15
+ * export const Orders = () => {
16
+ * const orders = Ardor.useRepository({ target: OrdersRepository });
17
+ * const container = Ardor.useApplicationContainer();
18
+ * return <span>{orders.count()} {container.isBound({ key: 'repositories.OrdersRepository' }) ? 'bound' : 'missing'}</span>;
19
+ * };
20
+ * ```
21
+ */
1
22
  export * as Ardor from './ardor.js';
2
23
  export * from '@venizia/ardor-kernel';
3
24
  export * from '@venizia/ardor-react';
package/dist/index.js CHANGED
@@ -1,6 +1,27 @@
1
1
  // ARDOR - one entry point over the framework packages.
2
2
  // An application imports from `@venizia/ardor`; a consumer that needs only one layer installs that
3
3
  // package directly.
4
+ /**
5
+ * Every ARDOR hook under one name: `Ardor.useRepository(...)` is the same function as
6
+ * `useRepository`. Call a member by its name; do not index with a computed key (`Ardor[name]`), which
7
+ * keeps every hook in a bundle.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { Ardor, repository } from '@venizia/ardor';
12
+ *
13
+ * @repository()
14
+ * class OrdersRepository {
15
+ * count() { return 3; }
16
+ * }
17
+ *
18
+ * export const Orders = () => {
19
+ * const orders = Ardor.useRepository({ target: OrdersRepository });
20
+ * const container = Ardor.useApplicationContainer();
21
+ * return <span>{orders.count()} {container.isBound({ key: 'repositories.OrdersRepository' }) ? 'bound' : 'missing'}</span>;
22
+ * };
23
+ * ```
24
+ */
4
25
  export * as Ardor from './ardor.js';
5
26
  export * from '@venizia/ardor-kernel';
6
27
  export * from '@venizia/ardor-react';
@@ -17,6 +17,11 @@
17
17
  FROM docker.io/oven/bun:1.4.3-alpine AS deps
18
18
 
19
19
  # A private registry, when the build runs where the public one is not reachable. Empty by default.
20
+ # The URL stays in the metadata of this stage and the next two (not in the final image): never put a user or a
21
+ # password in it. For a registry that needs credentials, mount a bunfig.toml for the install only, with BuildKit:
22
+ # RUN --mount=type=secret,id=bunfig,target=/home/bun/.bunfig.toml,uid=1000 bun install ...
23
+ # docker build --secret id=bunfig,src=./bunfig.toml ...
24
+ # A project that wants fixed base images pins them by digest (FROM image@sha256:...).
20
25
  ARG BUN_CONFIG_REGISTRY=
21
26
  ENV BUN_CONFIG_REGISTRY=${BUN_CONFIG_REGISTRY}
22
27
 
@@ -10,8 +10,9 @@
10
10
  # What is published: the variables named VITE_ followed by capitals, digits and underscores, and nothing
11
11
  # else. That is Vite's own rule for what reaches the browser, so the same names work in dev and here.
12
12
  # Every published value is readable by every visitor: never put a secret in a VITE_ variable. A name
13
- # with SECRET, PASSWORD or TOKEN in it stops the container, to catch that mistake early (a name with KEY
14
- # is fine: public keys are normal). Values are escaped as JavaScript string content, so no value can
13
+ # naming a secret (SECRET, PASSWORD, PASSWD, PASSPHRASE, PASSCODE, PASS as a word, PRIVATE, CREDENTIAL or TOKEN),
14
+ # or a value that is a URL carrying a user and a password, stops the container, to catch that mistake early
15
+ # (a name with KEY is fine, public keys are normal; this is a best-effort tripwire, not a secret scanner). Values are escaped as JavaScript string content, so no value can
15
16
  # end its string or the file.
16
17
  #
17
18
  # Required variables: APP_REQUIRED_ENV lists, comma separated, the variables the app cannot run without,
@@ -89,7 +90,8 @@ BEGIN {
89
90
  if (name !~ /^VITE_[A-Z0-9_]+$/) {
90
91
  continue
91
92
  }
92
- if (name ~ /SECRET|PASSWORD|TOKEN/) {
93
+ # Names that say secret, and a URL that carries a user and a password; a value is never printed.
94
+ if (name ~ /SECRET|PASSWORD|PASSWD|PASSPHRASE|PASSCODE|(^|_)PASS(_|$)|PRIVATE|CREDENTIAL|TOKEN/ || ENVIRON[name] ~ /(^|:)\/\/[^\/@?# ]*:[^\/@?# ]+@/) {
93
95
  print "[runtime-config] " name " looks like a secret, and every VITE_ variable is published to every visitor: rename it or keep it out of VITE_" > "/dev/stderr"
94
96
  failed = 1
95
97
  continue
@@ -10,6 +10,8 @@
10
10
  # The upstream is written into the nginx configuration, so it must be an address and nothing else: a value with a
11
11
  # character outside letters, digits, brackets (an IPv6 address) and `._:/@%+-` stops the container, naming the variable and never the value.
12
12
  #
13
+ # NGINX_LOCAL_RESOLVERS is checked the same way: addresses and spaces.
14
+ #
13
15
  # API_PROXY_SOURCE and API_PROXY_TARGET name other files, for the tests; they are not VITE_ names, so never published.
14
16
  set -eu
15
17
 
@@ -31,6 +33,23 @@ case "$API_UPSTREAM" in
31
33
  ;;
32
34
  esac
33
35
 
36
+ # The resolvers are written into the nginx configuration too: addresses (IPv4, IPv6 in brackets) and
37
+ # spaces between them, nothing else.
38
+ case "${NGINX_LOCAL_RESOLVERS:-}" in
39
+ *[!\ ]*) ;;
40
+ *)
41
+ echo "[api-proxy] FATAL: NGINX_LOCAL_RESOLVERS is empty, and nginx needs at least one resolver for /api" >&2
42
+ exit 1
43
+ ;;
44
+ esac
45
+
46
+ case "${NGINX_LOCAL_RESOLVERS:-}" in
47
+ *[!][0-9A-Fa-f.:\ ]*)
48
+ echo "[api-proxy] FATAL: NGINX_LOCAL_RESOLVERS has a character that is not part of an address" >&2
49
+ exit 1
50
+ ;;
51
+ esac
52
+
34
53
  # Only these two: nginx's own variables ($api_upstream, $host) stay as they are.
35
54
  envsubst '${API_UPSTREAM} ${NGINX_LOCAL_RESOLVERS}' < "$SOURCE" > "$TARGET"
36
55
  echo "[api-proxy] /api goes to the upstream of API_UPSTREAM"
@@ -9,6 +9,10 @@ location /api/ {
9
9
  proxy_pass $api_upstream;
10
10
  proxy_http_version 1.1;
11
11
  proxy_set_header Host $host;
12
+ # Behind a trusted front proxy (an ingress, a load balancer) this keeps the client address it appends. A
13
+ # browser can put its own value first in X-Forwarded-For, and $scheme is http here whatever the front proxy
14
+ # served: a backend must trust only the entries its own proxy added, and learn the scheme from that proxy. With
15
+ # no front proxy, replace the value with $remote_addr.
12
16
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
13
17
  proxy_set_header X-Forwarded-Proto $scheme;
14
18
  }
@@ -4,8 +4,10 @@ add_header X-Content-Type-Options "nosniff" always;
4
4
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
5
5
  add_header X-Frame-Options "SAMEORIGIN" always;
6
6
 
7
- # A Content-Security-Policy depends on what the app loads, so none is sent. An example to adapt: start with
8
- # Content-Security-Policy-Report-Only, watch the browser console, then switch to the enforcing header. Admin
9
- # libraries often need 'unsafe-inline' for styles. `frame-ancestors` replaces X-Frame-Options where both exist.
10
- #
11
- # add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
7
+ # A Content-Security-Policy, in report-only mode: the browser reports what it would block in its console and
8
+ # blocks nothing. It fits the app as generated (the config.js script and the module script are of the same
9
+ # origin, the favicon is a data: URL, the styles are linked, /api is of the same origin). Adapt it to what the app
10
+ # loads (an API of another origin goes into connect-src, a CDN into script-src and style-src), watch the console,
11
+ # then change the header name to Content-Security-Policy to enforce it. Admin libraries often need 'unsafe-inline'
12
+ # for styles. `frame-ancestors` replaces X-Frame-Options where both exist.
13
+ add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
@@ -8,6 +8,9 @@ server {
8
8
  listen 8080;
9
9
  server_name _;
10
10
 
11
+ # The Server header and the error pages name no nginx version.
12
+ server_tokens off;
13
+
11
14
  root /usr/share/nginx/html;
12
15
  index index.html;
13
16
 
@@ -28,16 +28,15 @@ export default defineConfig(({ mode }) => {
28
28
  rolldownOptions: {
29
29
  output: {
30
30
  // Runs each module after the modules it imports: a split chunk can otherwise run a DI package
31
- // before reflect-metadata (vitejs/vite#21866). It costs about 3% of the gzip size; keep it on.
31
+ // before reflect-metadata (vitejs/vite#21866). It costs about 3% of the gzip size of a small app
32
+ // and 3 to 20% of the first screen of an app with more than a thousand chunks: keep it on, or
33
+ // measure (the Vite setup guide).
32
34
  strictExecutionOrder: true,
33
- // React, react-admin and the router weigh about 500 KB before any application code. Two
34
- // vendor chunks keep the application's own chunk small, cacheable across a deploy, and
35
- // under Vite's 500 KB warning.
35
+ // React gets a chunk of its own. There is no catch-all `vendor` group: it put the libraries of
36
+ // lazy routes into a chunk the first screen loads (+56 to +117% gzip on the two largest of six
37
+ // measured apps, a 6.5 MB chunk); the react group alone costs about 4%. The guide has the opt-in.
36
38
  codeSplitting: {
37
- groups: [
38
- { name: 'react', test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/ },
39
- { name: 'vendor', test: /node_modules/ },
40
- ],
39
+ groups: [{ name: 'react', test: /node_modules[\\/](react|react-dom|scheduler)[\\/]/ }],
41
40
  },
42
41
  },
43
42
  },
@@ -13,12 +13,12 @@
13
13
  "dependencies": {
14
14
  "@reduxjs/toolkit": "^2.13.0",
15
15
  "@tanstack/react-query": "^5.104.5",
16
- "@venizia/ardor": "0.4.8-1",
17
- "@venizia/ignis-connectors": "0.4.8-0",
18
- "@venizia/ignis-filter": "0.4.7-0",
19
- "@venizia/ignis-helpers": "0.4.8-0",
20
- "@venizia/ignis-inversion": "0.4.7-0",
21
- "@venizia/ignis-kernel": "0.4.8-0",
16
+ "@venizia/ardor": "0.4.8-11",
17
+ "@venizia/ignis-connectors": "0.4.8-11",
18
+ "@venizia/ignis-filter": "0.4.7-4",
19
+ "@venizia/ignis-helpers": "0.4.8-9",
20
+ "@venizia/ignis-inversion": "0.4.7-3",
21
+ "@venizia/ignis-kernel": "0.4.8-11",
22
22
  "class-variance-authority": "^0.7.1",
23
23
  "clsx": "^2.1.1",
24
24
  "ra-core": "^5.15.5",
@@ -35,8 +35,8 @@
35
35
  "@types/bun": "^1.4.3",
36
36
  "@types/react": "^19",
37
37
  "@types/react-dom": "^19",
38
- "@venizia/dev-configs": "0.4.8-0",
39
- "@venizia/ignis-boot": "0.4.8-0",
38
+ "@venizia/dev-configs": "0.4.8-7",
39
+ "@venizia/ignis-boot": "0.4.8-9",
40
40
  "@vitejs/plugin-react": "^6.1.2",
41
41
  "eslint": "^10.13.0",
42
42
  "oxlint": "1.89.0",
@@ -47,11 +47,11 @@
47
47
  },
48
48
  "peers": {
49
49
  "@venizia/dev-configs": "^0.4.0 || ^0.4.8-0",
50
- "@venizia/ignis-connectors": "^0.4.8-0",
51
- "@venizia/ignis-filter": "^0.4.7-0",
52
- "@venizia/ignis-helpers": "^0.4.8-0",
53
- "@venizia/ignis-inversion": "^0.4.7-0",
54
- "@venizia/ignis-kernel": "^0.4.8-0"
50
+ "@venizia/ignis-connectors": "^0.4.8-11",
51
+ "@venizia/ignis-filter": "^0.4.7-4",
52
+ "@venizia/ignis-helpers": "^0.4.8-9",
53
+ "@venizia/ignis-inversion": "^0.4.7-3",
54
+ "@venizia/ignis-kernel": "^0.4.8-11"
55
55
  },
56
56
  "scripts": {
57
57
  "server:dev": "vite --config .configs/builders/vite.config.mts",
@@ -5,14 +5,14 @@
5
5
  "devDependencies": {
6
6
  "@types/bun": "^1.4.3",
7
7
  "@types/react": "^19",
8
- "@venizia/ardor": "^0.4.8-1",
9
- "@venizia/dev-configs": "0.4.8-0",
10
- "@venizia/ignis-boot": "0.4.8-0",
11
- "@venizia/ignis-connectors": "0.4.8-0",
12
- "@venizia/ignis-filter": "0.4.7-0",
13
- "@venizia/ignis-helpers": "0.4.8-0",
14
- "@venizia/ignis-inversion": "0.4.7-0",
15
- "@venizia/ignis-kernel": "0.4.8-0",
8
+ "@venizia/ardor": "^0.4.8-11",
9
+ "@venizia/dev-configs": "0.4.8-7",
10
+ "@venizia/ignis-boot": "0.4.8-9",
11
+ "@venizia/ignis-connectors": "0.4.8-11",
12
+ "@venizia/ignis-filter": "0.4.7-4",
13
+ "@venizia/ignis-helpers": "0.4.8-9",
14
+ "@venizia/ignis-inversion": "0.4.7-3",
15
+ "@venizia/ignis-kernel": "0.4.8-11",
16
16
  "eslint": "^10.13.0",
17
17
  "oxlint": "1.89.0",
18
18
  "oxlint-tsgolint": "7.0.2004",
@@ -23,7 +23,7 @@
23
23
  "typescript": "^6.0.3"
24
24
  },
25
25
  "peerDependencies": {
26
- "@venizia/ardor": "^0.4.8-1",
26
+ "@venizia/ardor": "^0.4.8-11",
27
27
  "react": "^19.3.1",
28
28
  "react-dom": "^19.3.1",
29
29
  "react-router-dom": "^7.18.4",
package/dist/testing.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import * as inversion from '@venizia/ignis-inversion';
2
2
  import { type BaseArdorApplication } from '@venizia/ardor-kernel';
3
+ export * from '@venizia/ardor-kernel/testing';
3
4
  /** A class (matched exactly), a class name, or a binding key; the shape the shared core takes. */
4
5
  export type TBootSkip = inversion.TBindingSkip;
6
+ /** What `assertArdorApplicationBoots` leaves out, which locale it loads, and when an empty application passes. */
5
7
  export interface IAssertArdorApplicationBootsOptions {
6
8
  /**
7
9
  * Bindings the check leaves unresolved, for the few that open a socket or read the page when
@@ -16,8 +18,9 @@ export interface IAssertArdorApplicationBootsOptions {
16
18
  /** The locale whose messages must load. Default: the i18n provider's initial one. */
17
19
  locale?: string;
18
20
  /**
19
- * Default `false`: an application with no service, repository, datasource, component or
20
- * configuration and no feature proves nothing (usually the artifact index was empty).
21
+ * An application with no service, repository, datasource, component or configuration and no
22
+ * feature proves nothing (usually the artifact index was empty), so it fails unless this is `true`.
23
+ * @default false
21
24
  */
22
25
  allowEmpty?: boolean;
23
26
  }
@@ -34,27 +37,4 @@ export interface IAssertArdorApplicationBootsOptions {
34
37
  * build a new one (pass a factory) for each call.
35
38
  */
36
39
  export declare const assertArdorApplicationBoots: <Application extends BaseArdorApplication>(application: Application | (() => Application | Promise<Application>), options?: IAssertArdorApplicationBootsOptions) => Promise<Application>;
37
- /**
38
- * A `ResizeObserver` for a test host whose own `observe()` does nothing, such as happy-dom. Call
39
- * `FakeResizeObserver.install()` before the code under test creates its observer, and `flush` to
40
- * deliver a size. A callback runs at once; code that waits for an animation frame still needs the
41
- * test's own frame control.
42
- */
43
- export declare class FakeResizeObserver implements ResizeObserver {
44
- private readonly callback;
45
- private static readonly all;
46
- private readonly targets;
47
- constructor(callback: ResizeObserverCallback);
48
- /** Replaces `globalThis.ResizeObserver` with the fake. The returned function puts the previous one back. */
49
- static install(): () => void;
50
- /** Calls every observer of `element` with one entry of that size, for the content and the border box alike. */
51
- static flush(opts: {
52
- element: Element;
53
- width: number;
54
- height: number;
55
- }): void;
56
- observe(target: Element): void;
57
- unobserve(target: Element): void;
58
- disconnect(): void;
59
- }
60
40
  //# sourceMappingURL=testing.d.ts.map
package/dist/testing.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as inversion from '@venizia/ignis-inversion';
2
2
  import { ApplicationProviders, Features } from '@venizia/ardor-admin';
3
3
  import { CoreBindings } from '@venizia/ardor-kernel';
4
+ // The browser fakes live in the kernel, so the React and admin tests can use them without a cycle.
5
+ export * from '@venizia/ardor-kernel/testing';
4
6
  /** The namespaces ARDOR binds its stereotyped classes under; the check resolves each binding of them. */
5
7
  const VERIFIED_NAMESPACES = [
6
8
  'services',
@@ -73,54 +75,4 @@ export const assertArdorApplicationBoots = async (application, options = {}) =>
73
75
  }
74
76
  return target;
75
77
  };
76
- /**
77
- * A `ResizeObserver` for a test host whose own `observe()` does nothing, such as happy-dom. Call
78
- * `FakeResizeObserver.install()` before the code under test creates its observer, and `flush` to
79
- * deliver a size. A callback runs at once; code that waits for an animation frame still needs the
80
- * test's own frame control.
81
- */
82
- export class FakeResizeObserver {
83
- static { this.all = new Set(); }
84
- constructor(callback) {
85
- this.callback = callback;
86
- this.targets = new Set();
87
- FakeResizeObserver.all.add(this);
88
- }
89
- /** Replaces `globalThis.ResizeObserver` with the fake. The returned function puts the previous one back. */
90
- static install() {
91
- const previous = globalThis.ResizeObserver;
92
- FakeResizeObserver.all.clear();
93
- globalThis.ResizeObserver = FakeResizeObserver;
94
- return () => {
95
- globalThis.ResizeObserver = previous;
96
- };
97
- }
98
- /** Calls every observer of `element` with one entry of that size, for the content and the border box alike. */
99
- static flush(opts) {
100
- const { element, width, height } = opts;
101
- const box = { x: 0, y: 0, width, height, top: 0, left: 0, right: width, bottom: height };
102
- const size = [{ inlineSize: width, blockSize: height }];
103
- const entry = {
104
- target: element,
105
- contentRect: { ...box, toJSON: () => box },
106
- contentBoxSize: size,
107
- borderBoxSize: size,
108
- devicePixelContentBoxSize: [],
109
- };
110
- for (const observer of FakeResizeObserver.all) {
111
- if (observer.targets.has(element)) {
112
- observer.callback([entry], observer);
113
- }
114
- }
115
- }
116
- observe(target) {
117
- this.targets.add(target);
118
- }
119
- unobserve(target) {
120
- this.targets.delete(target);
121
- }
122
- disconnect() {
123
- this.targets.clear();
124
- }
125
- }
126
78
  //# sourceMappingURL=testing.js.map
@@ -1,10 +1,16 @@
1
+ /** The build info file's path when `fileName` is not set. */
1
2
  export declare const DEFAULT_BUILD_INFO_FILE_NAME = "build-info.json";
3
+ /** The package-name prefix listed under `frameworks` when `frameworkScope` is not set. */
2
4
  export declare const DEFAULT_FRAMEWORK_SCOPE = "@venizia/";
3
5
  /** What to do when the commit is neither in the pipeline environment nor in git. */
4
6
  export declare class UnresolvedPolicies {
7
+ /** The build fails. */
5
8
  static readonly ERROR = "error";
9
+ /** The build goes on with the commit recorded as `unspecified`. */
6
10
  static readonly UNSPECIFIED = "unspecified";
11
+ /** Every accepted policy value. */
7
12
  static readonly SCHEME_SET: Set<string>;
13
+ /** Whether `value` is one of the policies. */
8
14
  static isValid(value: string): boolean;
9
15
  }
10
16
  //# sourceMappingURL=constants.d.ts.map
@@ -1,10 +1,16 @@
1
+ /** The build info file's path when `fileName` is not set. */
1
2
  export const DEFAULT_BUILD_INFO_FILE_NAME = 'build-info.json';
3
+ /** The package-name prefix listed under `frameworks` when `frameworkScope` is not set. */
2
4
  export const DEFAULT_FRAMEWORK_SCOPE = '@venizia/';
3
5
  /** What to do when the commit is neither in the pipeline environment nor in git. */
4
6
  export class UnresolvedPolicies {
7
+ /** The build fails. */
5
8
  static { this.ERROR = 'error'; }
9
+ /** The build goes on with the commit recorded as `unspecified`. */
6
10
  static { this.UNSPECIFIED = 'unspecified'; }
11
+ /** Every accepted policy value. */
7
12
  static { this.SCHEME_SET = new Set([this.ERROR, this.UNSPECIFIED]); }
13
+ /** Whether `value` is one of the policies. */
8
14
  static isValid(value) {
9
15
  return this.SCHEME_SET.has(value);
10
16
  }
@@ -1,15 +1,31 @@
1
1
  import type { IBuildInfoFile, TConstValue } from '@venizia/ardor-kernel';
2
2
  import type { TBuildInfoRecord } from '@venizia/ignis-helpers/core';
3
3
  import type { UnresolvedPolicies } from './constants.js';
4
+ /** `'error'` or `'unspecified'`; see `UnresolvedPolicies`. */
4
5
  export type TUnresolvedPolicy = TConstValue<typeof UnresolvedPolicies>;
6
+ /** The options of the `buildInfoFile` plugin. */
5
7
  export interface IBuildInfoFileOptions {
6
- /** The app folder, where its package.json is. Default: the folder Vite runs in. */
8
+ /**
9
+ * The app folder, where its package.json is.
10
+ * @default process.cwd()
11
+ */
7
12
  root?: string;
8
- /** The file's path under the output folder and under the dev server's base. Default: `build-info.json`. */
13
+ /**
14
+ * The file's path under the output folder and under the dev server's base.
15
+ * A relative path of letters, digits, `.`, `_` and `-`, with `/` between folders, ending `.json`, and
16
+ * no `..` segment; anything else throws.
17
+ * @default 'build-info.json'
18
+ */
9
19
  fileName?: string;
10
- /** Dependencies whose name starts with this are listed under `frameworks`. Default: `@venizia/`. */
20
+ /**
21
+ * Dependencies whose name starts with this are listed under `frameworks`.
22
+ * @default '@venizia/'
23
+ */
11
24
  frameworkScope?: string;
12
- /** What a build does when no commit can be found: `error` (default) or `unspecified`. The dev server always serves what it has and warns. */
25
+ /**
26
+ * What a build does when no commit can be found: `error` or `unspecified`. The dev server always serves what it has and warns.
27
+ * @default UnresolvedPolicies.ERROR
28
+ */
13
29
  onUnresolved?: TUnresolvedPolicy;
14
30
  }
15
31
  export type TGitReadResult = {
@@ -15,6 +15,16 @@ export declare const createBuildInfoFilePlugin: (opts: {
15
15
  * Emits `build-info.json` into the build and serves it on the dev server: the commit, the branch and
16
16
  * the build time from the pipeline or git, the app's name and version, and the installed versions of
17
17
  * the framework packages it depends on. The shape is `IBuildInfoFile` of `@venizia/ardor`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { buildInfoFile } from '@venizia/ardor/vite';
22
+ * import { defineConfig } from 'vite';
23
+ *
24
+ * export default defineConfig({
25
+ * plugins: [buildInfoFile({ fileName: 'build-info.json', onUnresolved: 'unspecified' })],
26
+ * });
27
+ * ```
18
28
  */
19
29
  export declare const buildInfoFile: (opts?: IBuildInfoFileOptions) => Plugin;
20
30
  export {};
@@ -104,6 +104,16 @@ export const createBuildInfoFilePlugin = (opts) => {
104
104
  * Emits `build-info.json` into the build and serves it on the dev server: the commit, the branch and
105
105
  * the build time from the pipeline or git, the app's name and version, and the installed versions of
106
106
  * the framework packages it depends on. The shape is `IBuildInfoFile` of `@venizia/ardor`.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * import { buildInfoFile } from '@venizia/ardor/vite';
111
+ * import { defineConfig } from 'vite';
112
+ *
113
+ * export default defineConfig({
114
+ * plugins: [buildInfoFile({ fileName: 'build-info.json', onUnresolved: 'unspecified' })],
115
+ * });
116
+ * ```
107
117
  */
108
118
  export const buildInfoFile = (opts = {}) => createBuildInfoFilePlugin({ options: opts, sources: defaultSources });
109
119
  //# sourceMappingURL=plugin.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@venizia/ardor",
3
- "version": "0.4.8-1",
3
+ "version": "0.4.8-11",
4
4
  "description": "ARDOR - frontend application framework for the VENIZIA family. One entry point over ardor-kernel, ardor-react and ardor-admin: IGNIS inversion of control, a REST data layer, auth and i18n providers, and the React bindings for a react-admin application.",
5
5
  "keywords": [
6
6
  "ardor",
@@ -75,25 +75,24 @@
75
75
  "access": "public"
76
76
  },
77
77
  "scripts": {
78
- "force-update": "sh ../../scripts/build/force-update.sh",
79
78
  "prepublishOnly": "make rebuild"
80
79
  },
81
80
  "dependencies": {
82
- "@venizia/ardor-admin": "0.4.8-1",
83
- "@venizia/ardor-kernel": "0.4.8-1",
84
- "@venizia/ardor-react": "0.4.8-1"
81
+ "@venizia/ardor-admin": "0.4.8-11",
82
+ "@venizia/ardor-kernel": "0.4.8-11",
83
+ "@venizia/ardor-react": "0.4.8-11"
85
84
  },
86
85
  "devDependencies": {
87
86
  "@size-limit/preset-small-lib": "^14.2.0",
88
87
  "@types/bun": "^1.4.3",
89
88
  "@types/node": "^25.0.10",
90
- "@venizia/dev-configs": "^0.4.8-0",
91
- "@venizia/ignis-boot": "^0.4.8-0",
92
- "@venizia/ignis-connectors": "^0.4.8-0",
93
- "@venizia/ignis-filter": "^0.4.7-0",
94
- "@venizia/ignis-helpers": "^0.4.8-0",
95
- "@venizia/ignis-inversion": "^0.4.7-0",
96
- "@venizia/ignis-kernel": "^0.4.8-0",
89
+ "@venizia/dev-configs": "^0.4.8-7",
90
+ "@venizia/ignis-boot": "^0.4.8-9",
91
+ "@venizia/ignis-connectors": "^0.4.8-11",
92
+ "@venizia/ignis-filter": "^0.4.7-4",
93
+ "@venizia/ignis-helpers": "^0.4.8-9",
94
+ "@venizia/ignis-inversion": "^0.4.7-3",
95
+ "@venizia/ignis-kernel": "^0.4.8-11",
97
96
  "eslint": "^10.13.0",
98
97
  "oxlint": "1.89.0",
99
98
  "oxlint-tsgolint": "7.0.2004",
@@ -105,12 +104,12 @@
105
104
  "peerDependencies": {
106
105
  "@reduxjs/toolkit": ">=2.13.0",
107
106
  "@tanstack/react-query": ">=5.104.5",
108
- "@venizia/ignis-boot": "^0.4.8-0",
109
- "@venizia/ignis-connectors": "^0.4.8-0",
110
- "@venizia/ignis-filter": "^0.4.7-0",
111
- "@venizia/ignis-helpers": "^0.4.8-0",
112
- "@venizia/ignis-inversion": "^0.4.7-0",
113
- "@venizia/ignis-kernel": "^0.4.8-0",
107
+ "@venizia/ignis-boot": "^0.4.8-9",
108
+ "@venizia/ignis-connectors": "^0.4.8-11",
109
+ "@venizia/ignis-filter": "^0.4.7-4",
110
+ "@venizia/ignis-helpers": "^0.4.8-9",
111
+ "@venizia/ignis-inversion": "^0.4.7-3",
112
+ "@venizia/ignis-kernel": "^0.4.8-11",
114
113
  "ra-core": ">=5.15.5",
115
114
  "ra-i18n-polyglot": ">=5.15.5",
116
115
  "react": ">=19.3.1",
@@ -174,7 +173,7 @@
174
173
  "react-router-dom",
175
174
  "reflect-metadata"
176
175
  ],
177
- "limit": "1120 B"
176
+ "limit": "1050 B"
178
177
  }
179
178
  ],
180
179
  "engines": {