@wssto2/vue-core 0.15.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -3
  3. package/bin/architecture/engine.mjs +3 -0
  4. package/bin/architecture/permissions.mjs +56 -0
  5. package/bin/architecture/rules.mjs +17 -0
  6. package/bin/commands/check-architecture.mjs +19 -0
  7. package/bin/config.mjs +2 -0
  8. package/dist/{AccessGate.vue_vue_type_script_setup_true_lang-BH3f0hda.js → AccessGate.vue_vue_type_script_setup_true_lang-Bg8JFAOT.js} +1 -1
  9. package/dist/{ActionRow.vue_vue_type_script_setup_true_lang-DybAIOHS.js → ActionRow.vue_vue_type_script_setup_true_lang-B5IMNnTf.js} +4 -4
  10. package/dist/{Activity-DM9t5d3D.js → Activity-CiYn5E6A.js} +7 -7
  11. package/dist/{AdaptivePageShell.vue_vue_type_script_setup_true_lang-uO4EAmBb.js → AdaptivePageShell.vue_vue_type_script_setup_true_lang-oXWQTfBs.js} +170 -168
  12. package/dist/{Changes-DYpysvfX.js → Changes-Cy8STpLX.js} +3 -3
  13. package/dist/{CollectionPage.vue_vue_type_script_setup_true_lang-qxeSTAKV.js → CollectionPage.vue_vue_type_script_setup_true_lang-Dr6nFNBB.js} +6 -6
  14. package/dist/{DataTable.vue_vue_type_script_setup_true_lang-DbRS37n3.js → DataTable.vue_vue_type_script_setup_true_lang-D372wlxR.js} +1 -1
  15. package/dist/{DateField.vue_vue_type_script_setup_true_lang-DzCvYHAf.js → DateField.vue_vue_type_script_setup_true_lang-D0KcfW8R.js} +17 -16
  16. package/dist/{DeadLetters-vt9vhJD7.js → DeadLetters-DhJkMpd_.js} +4 -4
  17. package/dist/{FormGroup.vue_vue_type_script_setup_true_lang-dsO1m9Mm.js → FormGroup.vue_vue_type_script_setup_true_lang-lvAuBG-0.js} +2 -2
  18. package/dist/FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js +160 -0
  19. package/dist/{FormView.vue_vue_type_script_setup_true_lang-Bq_Qa4py.js → FormView.vue_vue_type_script_setup_true_lang-D01Jr4UU.js} +5 -5
  20. package/dist/{General-oUI2MiwO.js → General-CwTPC5UP.js} +11 -11
  21. package/dist/{IncidentRecord-CQdv_bN5.js → IncidentRecord-hXEGuLGg.js} +14 -14
  22. package/dist/{IncidentsList-DwQ0wuM8.js → IncidentsList-C41VyDwK.js} +6 -6
  23. package/dist/{NewRolePage-C-wbA-uW.js → NewRolePage-BYYZJkK7.js} +3 -3
  24. package/dist/{NumberField.vue_vue_type_script_setup_true_lang-U-_kcnGr.js → NumberField.vue_vue_type_script_setup_true_lang-BhNZgEW6.js} +7 -6
  25. package/dist/{PersonAccess.vue_vue_type_script_setup_true_lang-BPDh9s2m.js → PersonAccess.vue_vue_type_script_setup_true_lang-mYFuG-Y3.js} +56 -56
  26. package/dist/{ProfilePage-Di77M2cX.js → ProfilePage-DGEsIycR.js} +13 -13
  27. package/dist/{RecordHeader.vue_vue_type_script_setup_true_lang-CZtvUH5L.js → RecordHeader.vue_vue_type_script_setup_true_lang-B3BnYg1x.js} +1 -1
  28. package/dist/{ResourcePage.vue_vue_type_script_setup_true_lang-uLrX13ab.js → ResourcePage.vue_vue_type_script_setup_true_lang-1iZtj3rh.js} +1 -1
  29. package/dist/{RoleEditor.vue_vue_type_script_setup_true_lang-V2D7fhR9.js → RoleEditor.vue_vue_type_script_setup_true_lang-BslSDDiM.js} +10 -10
  30. package/dist/{RolePage-Crp71rRb.js → RolePage-C08ZUqw1.js} +9 -9
  31. package/dist/{RolesPage-DKXXnnDV.js → RolesPage-FOuY1VKg.js} +10 -10
  32. package/dist/{SectionNavigator.vue_vue_type_script_setup_true_lang-5ogQCj8M.js → SectionNavigator.vue_vue_type_script_setup_true_lang-DCTFxfPB.js} +2 -2
  33. package/dist/{SegmentedField.vue_vue_type_script_setup_true_lang-zv_AxpBn.js → SegmentedField.vue_vue_type_script_setup_true_lang-CLR6EvzO.js} +13 -12
  34. package/dist/{SelectField.vue_vue_type_script_setup_true_lang-CLkoPbGm.js → SelectField.vue_vue_type_script_setup_true_lang-D5xYofdb.js} +10 -9
  35. package/dist/{ServiceAccountDialog.vue_vue_type_script_setup_true_lang-C7WKxJ9i.js → ServiceAccountDialog.vue_vue_type_script_setup_true_lang-CDr4WNIb.js} +9 -9
  36. package/dist/{ServiceAccountRecord-CTU10RWy.js → ServiceAccountRecord-kWwIQGZb.js} +12 -12
  37. package/dist/{ServiceAccountsList-BVRjoswH.js → ServiceAccountsList-CKy9_YHP.js} +8 -8
  38. package/dist/{Sessions-BHwMrF7c.js → Sessions-BDc8gdYH.js} +4 -4
  39. package/dist/{SettingsPage-lRWvsMYK.js → SettingsPage-t-8kmFVT.js} +13 -13
  40. package/dist/{SignIn-HnujOMRY.js → SignIn-CqtkvtCZ.js} +7 -7
  41. package/dist/{SignInPage-c886Fvd7.js → SignInPage-DcdNKZIb.js} +4 -4
  42. package/dist/{SignIns-BsTlN2Zc.js → SignIns-D4-nGu0B.js} +3 -3
  43. package/dist/{SwitchField.vue_vue_type_script_setup_true_lang-Ds4q0bbl.js → SwitchField.vue_vue_type_script_setup_true_lang-CLtcuofe.js} +15 -14
  44. package/dist/{TextField.vue_vue_type_script_setup_true_lang-CWIeeZRE.js → TextField.vue_vue_type_script_setup_true_lang-CVGh-p3J.js} +7 -6
  45. package/dist/{TextareaField.vue_vue_type_script_setup_true_lang-D2j4s96s.js → TextareaField.vue_vue_type_script_setup_true_lang-YxUlNHSk.js} +6 -5
  46. package/dist/{TimeField.vue_vue_type_script_setup_true_lang-C1mLvOkc.js → TimeField.vue_vue_type_script_setup_true_lang-DHNC_MLt.js} +30 -29
  47. package/dist/{Timestamp.vue_vue_type_script_setup_true_lang-jCXYFLtq.js → Timestamp.vue_vue_type_script_setup_true_lang-d_KuxWjH.js} +1 -1
  48. package/dist/{UserRecord-DrbpVHh9.js → UserRecord-CwZCr9Im.js} +10 -10
  49. package/dist/{UsersList-M5tBxrox.js → UsersList-Cn19sFgv.js} +4 -4
  50. package/dist/access/index.js +4 -4
  51. package/dist/app/application.d.ts +2 -0
  52. package/dist/app/index.js +2 -2
  53. package/dist/app/validate.d.ts +3 -1
  54. package/dist/{application-C900Wz0A.js → application-CDSweJb9.js} +144 -142
  55. package/dist/back-LcLlFodC.js +63 -0
  56. package/dist/{chrome-nUcmVu8G.js → chrome-kMBKIB1N.js} +2 -2
  57. package/dist/collection/authors.d.ts +27 -0
  58. package/dist/collection/index.d.ts +2 -0
  59. package/dist/collection/index.js +81 -76
  60. package/dist/config/index.d.ts +42 -0
  61. package/dist/config/index.js +12 -3
  62. package/dist/content/Panel.vue.d.ts +1 -1
  63. package/dist/content/index.js +2 -2
  64. package/dist/{contributions-9CPT-Z5v.js → contributions-B0x5x5xD.js} +1 -1
  65. package/dist/form/CommandDialog.vue.d.ts +1 -1
  66. package/dist/form/DateField.vue.d.ts +1 -1
  67. package/dist/form/DateTimeField.vue.d.ts +1 -1
  68. package/dist/form/FileField.vue.d.ts +1 -1
  69. package/dist/form/FormRow.vue.d.ts +2 -2
  70. package/dist/form/I18nField.vue.d.ts +2 -2
  71. package/dist/form/TextField.vue.d.ts +1 -1
  72. package/dist/form/TextareaField.vue.d.ts +2 -2
  73. package/dist/form/date/value.d.ts +21 -0
  74. package/dist/form/index.d.ts +1 -0
  75. package/dist/form/index.js +745 -715
  76. package/dist/format/format.d.ts +8 -2
  77. package/dist/format/index.js +1 -1
  78. package/dist/format-C_HXlOYQ.js +136 -0
  79. package/dist/{guards-6fJxfUtk.js → guards-CNJ0ATVC.js} +1 -1
  80. package/dist/i18n/index.d.ts +14 -2
  81. package/dist/i18n/index.js +84 -73
  82. package/dist/i18n/messages.d.ts +10 -1
  83. package/dist/i18n/runtime.d.ts +3 -1
  84. package/dist/identity/index.js +3 -3
  85. package/dist/identity/profile/EmailChangeDialog.vue.d.ts +2 -2
  86. package/dist/identity/users/list.d.ts +1 -1
  87. package/dist/incidents/index.js +1 -1
  88. package/dist/{index-_hDHxrt5.js → index-1HS7O2-e.js} +9 -9
  89. package/dist/{index-DM_nWOYf.js → index-BA2w8uwD.js} +86 -86
  90. package/dist/{index-vES_D5ls.js → index-SLJWm40T.js} +4 -4
  91. package/dist/index.d.ts +1 -1
  92. package/dist/index.js +1 -1
  93. package/dist/modal/DiscardChangesModal.vue.d.ts +2 -2
  94. package/dist/modal/Modal.vue.d.ts +2 -2
  95. package/dist/modal/Sheet.vue.d.ts +2 -2
  96. package/dist/modules/identity/routes.d.ts +1 -1
  97. package/dist/modules/identity/schemas.d.ts +2 -2
  98. package/dist/{navigation-DMJ6akat.js → navigation-d-JAPBGA.js} +1 -1
  99. package/dist/{neighbors-DrEahe7B.js → neighbors-Dg_LU9ry.js} +32 -31
  100. package/dist/notifications/index.js +1 -1
  101. package/dist/onboarding/index.js +1 -1
  102. package/dist/options-mOOX6JSS.js +214 -0
  103. package/dist/overlay/AlertDialog.vue.d.ts +3 -3
  104. package/dist/overlay/Menu.vue.d.ts +2 -2
  105. package/dist/overlay/PhotoViewer.vue.d.ts +2 -2
  106. package/dist/overlay/Popover.vue.d.ts +2 -2
  107. package/dist/page/SectionNavigator.vue.d.ts +1 -1
  108. package/dist/page/SectionPanel.vue.d.ts +1 -1
  109. package/dist/page/back.d.ts +37 -0
  110. package/dist/page/index.d.ts +1 -0
  111. package/dist/page/index.js +46 -42
  112. package/dist/phone/index.js +57 -56
  113. package/dist/platform/index.d.ts +10 -1
  114. package/dist/platform/index.js +16 -15
  115. package/dist/platform/platform.d.ts +4 -2
  116. package/dist/{platform-ORzRR3Tw.js → platform-DU9QFts8.js} +7 -6
  117. package/dist/playwright/config.d.ts +33 -0
  118. package/dist/playwright/index.d.ts +2 -0
  119. package/dist/playwright/index.js +192 -171
  120. package/dist/resource/index.js +1 -1
  121. package/dist/router/index.js +3 -3
  122. package/dist/router/sections.d.ts +1 -1
  123. package/dist/{rowLayout-QKfNHsML.js → rowLayout-PIuD7MGo.js} +2 -2
  124. package/dist/sections-BpiRLntE.js +54 -0
  125. package/dist/{serverMessages-Ce8-MK4e.js → serverMessages-C_KkDvpT.js} +1 -1
  126. package/dist/shell/index.js +5 -5
  127. package/dist/state/NextStepPanel.vue.d.ts +63 -0
  128. package/dist/state/PhaseTiles.vue.d.ts +20 -0
  129. package/dist/state/PhaseTrack.vue.d.ts +48 -0
  130. package/dist/state/RequirementList.vue.d.ts +10 -0
  131. package/dist/state/TodoRow.vue.d.ts +52 -0
  132. package/dist/state/index.d.ts +9 -0
  133. package/dist/state/index.js +731 -126
  134. package/dist/state/phaseStep.d.ts +7 -0
  135. package/dist/state/phaseTile.d.ts +10 -0
  136. package/dist/state/requirementItem.d.ts +16 -0
  137. package/dist/state/todoRow.d.ts +8 -0
  138. package/dist/{state-QDaxntgR.js → state-z1iPF1DT.js} +1 -1
  139. package/dist/styles.css +1 -1
  140. package/dist/{suggestions-q_v3M7G6.js → suggestions-DeMgs_ki.js} +1 -1
  141. package/dist/testing/format.d.ts +1 -1
  142. package/dist/testing/index.js +31 -31
  143. package/dist/{useCommand-DAMgQ5NE.js → useCommand-DQKvro1n.js} +2 -2
  144. package/dist/{useGroupSheet-B-Hs4gNO.js → useGroupSheet-CRw3MUmX.js} +3 -3
  145. package/dist/{usePreferences-BfMXVbSO.js → usePreferences-DB29UTFD.js} +1 -1
  146. package/dist/{useRecordDialog-QPKFH1Ww.js → useRecordDialog-0azHxxvm.js} +1 -1
  147. package/dist/{useResourceForm-Oa6bk3NI.js → useResourceForm-zVuWerSn.js} +1 -1
  148. package/dist/{useSaveChrome-7huA3FzR.js → useSaveChrome-DB9OwIjT.js} +1 -1
  149. package/dist/{useTypedEntry-CITij2C_.js → useTypedEntry-BsMV66Yc.js} +46 -45
  150. package/package.json +1 -1
  151. package/dist/format-Cp1h0vsC.js +0 -125
  152. package/dist/options-B48UiYsG.js +0 -368
  153. package/dist/sections-DI_JhgQG.js +0 -53
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0 — 2026-11-04
4
+
5
+ What every app built on vue-core wrote again: the phase and to-do components, date values in the date fields' own shape, a lazy locale loader, the author of a list row, "today at 14:35", typed access, two architecture rules as data, and the Playwright and vitest setup. Breaking only in that `Platform.access` is now typed by the registered permissions (see Changed).
6
+
7
+ ### New
8
+ - **state:** `PhaseTrack`, `PhaseTiles`, `TodoRow`, `NextStepPanel`, `RequirementList` and their types (`PhaseStep`, `PhaseTile`, `PhaseTone`, `RequirementItem`, `TodoRowAction`, `TodoRowState`): where a record stands, what is next, and what is still missing. New texts `core.stepper`, `core.next_step`, `core.requirements`, `core.todo_state`, `core.phase_tiles` in the five locales. Recipe: `docs/recipes/workflow.md`.
9
+ - **i18n:** `localeMessages(namespace, import.meta.glob("./*.json"))`, still lazy: nothing loads until the namespace is needed. A file's locale is its name; `createApplication({ localeFiles: { bs: "ba" } })` renames the exceptions.
10
+ - **form:** `dateValue(api)` (`YYYY-MM-DD`, sliced as written), `dateTimeValue(api)` (`YYYY-MM-DDTHH:mm` in local time, the shape `DateTimeField` writes), `parseDateValue(model)` (the `T` and the space form, as local time), `now()`; `today()` is exported. Go's zero date is `null`.
11
+ - **collection:** `authorOf(meta, id)`, `ResourceAuthor`, `ResourceMeta`, for go-core's `meta.authors` / `meta.author` / `meta.editor`.
12
+ - **format:** `format.when(value)`: just now, minutes ago, today / yesterday / tomorrow at a time, else the date and time. New texts `core.when.*`.
13
+ - **platform:** `useAccess()`. An application registers its generated permissions once (`interface PermissionRegistry extends Record<Permission, true> {}`); `useAccess()`, `Platform.access`, route meta `access` and `AccessGate` then refuse an unknown permission at compile time, template literals over a namespace type included.
14
+ - **devkit:** `check-architecture` rule `permission-texts` (`architecture.permissions: { generated, keys, locales, dir }`): every generated permission has each key (`policies.{id}`, `policy_descriptions.{id}`) in each locale, and no locale keeps a text for one that is gone.
15
+ - **devkit:** `architecture.forbidden: [{ id, pattern, message, exclude? }]`: text rules as data. Comments are stripped (a `//` in a string or URL is kept), tests, e2e, docs and scripts are skipped by default, and each rule has its allowlist `allow[id]`.
16
+ - **playwright:** `playwrightConfig({ testDir, timezone, ignore, port, report }, overrides)`: `E2E_PORT`, the dev server, CI retries and workers, the reporters plus the JSON report `vue-core e2e-timings` reads, and the time zone for the specs and the browser.
17
+ - **config:** `vitestDefaults({ timezone, setupFiles })` for the vitest `test` block: globals, happy-dom, `e2e/` and `review/` excluded, `TZ` pinned.
18
+
19
+ ### Changed
20
+ - **platform:** `Platform.access` is `AccessClient<Permission>`, the registered permissions. Code that assigned it to a narrower `AccessClient<X>` registers `X` instead.
21
+ - **format:** `Formatting.translate` receives the message parameters.
22
+
23
+ ## 0.16.0 — 2026-10-22
24
+
25
+ Back to the page a record was opened from.
26
+
27
+ ### New
28
+ - **page:** `useBackLinks()` returns `backHere(to)` and `keepBack(to)`. `backHere` adds `?back=<current path>&back_title=<the page's title>` (the title is the page chrome's, so a caller passes nothing and the link updates once the record has loaded); `keepBack` carries the page's own `back` and `back_title`. `BACK_PARAM` and `BACK_TITLE_PARAM` are exported.
29
+ - **page:** `AdaptivePageShell` (so `EditorPage`, `ResourcePage` and list pages) replaces the page's back with the valid `?back=`, on the desktop toolbar and the phone bar alike. The label is `back_title` (text, trimmed, 80 characters), else the target route's `meta.titleKey`. A back that is not a single-slash path of this app, resolves to no route (the catch-all does not count), is the current page, or has no label is ignored and the page keeps its own.
30
+
31
+ ### Changed
32
+ - **router:** `useRouteSections` keeps `back` and `back_title` in a sub-page's section links (the rest of a sub-page's query is still left behind); sections already kept the whole query.
33
+ - **collection:** the previous / next links of `useCollectionNeighbors` drop `back` and `back_title`: another record is not reached from the page the user came from.
34
+
3
35
  ## 0.15.3 — 2026-10-21
4
36
 
5
37
  A group sheet focuses its first error.
package/README.md CHANGED
@@ -116,8 +116,8 @@ export function createTicketsFeature(http: HttpClient) {
116
116
  id: "tickets",
117
117
  routes: ticketRoutes.records,
118
118
  context: provideContext(TICKETS, { api, list: createTicketList(api) }),
119
- // Loaded the first time a route of this feature is entered, in the active locale.
120
- messages: localeMessages("tickets", { en: () => import("./i18n/en.json"), hr: () => import("./i18n/hr.json") }),
119
+ // Loaded the first time a route of this feature is entered, in the active locale. The lazy glob takes the file names as locales (en.json, hr.json); `createApplication({ localeFiles: { bs: "ba" } })` renames the exceptions.
120
+ messages: localeMessages("tickets", import.meta.glob("./i18n/*.json")),
121
121
  // The backend's menu names destinations; this says which route opens "tickets" and which records keep it highlighted.
122
122
  navigation: [{ destination: "tickets", to: ticketRoutes.index, within: ["tickets.record"] }],
123
123
  });
@@ -327,7 +327,7 @@ Dark mode is the `dark` class on `<html>`; nothing is decided for you. An app th
327
327
 
328
328
  ## Texts and locales
329
329
 
330
- The library's own texts are under the `core` namespace in English, Croatian, Bosnian and Slovenian (`t("core.actions.cancel")`). `createApplication` merges them into its vue-i18n instance; override any key with `i18n.messages` and restrict the offered locales with `locale.supported`. A feature's texts are a namespace (`localeMessages("tickets", { en: () => import("./i18n/en.json") })`, used as `t("tickets.title")`) loaded per locale on demand. A locale switch (the account menu has one) loads every namespace in use first, so nothing shows a raw key; `locale.onChange` lets you save the choice on the user.
330
+ The library's own texts are under the `core` namespace in English, Croatian, Bosnian and Slovenian (`t("core.actions.cancel")`). `createApplication` merges them into its vue-i18n instance; override any key with `i18n.messages` and restrict the offered locales with `locale.supported`. A feature's texts are a namespace (`localeMessages("tickets", import.meta.glob("./i18n/*.json"))`, or one `() => import("./i18n/en.json")` per locale; used as `t("tickets.title")`) loaded per locale on demand. A locale switch (the account menu has one) loads every namespace in use first, so nothing shows a raw key; `locale.onChange` lets you save the choice on the user.
331
331
 
332
332
  ## Recipes
333
333
 
@@ -345,6 +345,7 @@ The library's own texts are under the `core` namespace in English, Croatian, Bos
345
345
  - [Notifications](docs/recipes/notifications.md): the bell, the inbox and the live stream, categories, the failed-events page, trying it against go-core's dev server
346
346
  - [Incidents](docs/recipes/incidents.md): go-core's incident module: the list and an incident, "Report a bug", the browser's errors reported (what is and is not sent), source maps
347
347
  - [Preferences and tips](docs/recipes/preferences.md): `usePreferences`, the person's stored settings, `darkMode()` on them, the `Tip` beside something new
348
+ - [Where a record stands](docs/recipes/workflow.md): `PhaseTrack`, `PhaseTiles`, `TodoRow`, `NextStepPanel`, `RequirementList` in `/state`
348
349
  - [Theming](docs/recipes/theming.md): brand accent, dark mode, tokens, status tones and category hues
349
350
  - [Testing](docs/recipes/testing.md): the application's environment and a fake backend for your tests
350
351
  - [End-to-end specs](docs/recipes/e2e.md): `@wssto2/vue-core/playwright`, the Playwright mocks of go-core's envelope, session, access, notification, profile and users routes
@@ -4,6 +4,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
4
  import path from "node:path";
5
5
 
6
6
  import { PACKS } from "./packs.mjs";
7
+ import { checkPermissionTexts } from "./permissions.mjs";
7
8
  import { builtInRules } from "./rules.mjs";
8
9
 
9
10
  const SOURCE_EXTENSIONS = new Set([".ts", ".vue"]);
@@ -142,6 +143,8 @@ export function check({ root, config, configName }) {
142
143
  }
143
144
  }
144
145
 
146
+ if (config.permissions) violations.push(...checkPermissionTexts({ root, config: config.permissions }));
147
+
145
148
  // every directory name under the features folder is a single lowercase word
146
149
  for (const dirPath of collectFeatureDirs(featuresRoot)) {
147
150
  const rel = toPosix(path.relative(root, dirPath));
@@ -0,0 +1,56 @@
1
+ // permission-texts: the permissions the generated catalogue lists and the texts the locale files keep for them agree. Every id has each key
2
+ // in each locale, and no locale keeps a key for an id that is gone. A key template is `<namespace>.<prefix>{id}`: the namespace is the folder
3
+ // `<dir>/<namespace>/<locale>.json`, and the file is nested by dots (`"crm": { "lead:view": "…" }` holds `crm.lead:view`).
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import path from "node:path";
6
+
7
+ /** The ids of a generated permissions file: its quoted strings (`export type Permission = 'a:view' | 'b:view'`), comments cut out. */
8
+ export function generatedIds(source) {
9
+ const code = source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
10
+ // Only the `Permission` union names ids; go-core's generator writes a metadata map after it, full of other strings.
11
+ const union = /export\s+type\s+Permission\s*=([^;]*);/.exec(code);
12
+ if (!union) throw new Error("permission-texts: the generated file has no `export type Permission = …;` union");
13
+ return [...new Set([...union[1].matchAll(/(["'`])([^"'`\n]+)\1/g)].map((match) => match[2]))];
14
+ }
15
+
16
+ // A nested messages file as the identifiers it holds: a leaf string is named by the dotted path to it.
17
+ function flatten(node, prefix, out) {
18
+ for (const [key, value] of Object.entries(node)) {
19
+ if (typeof value === "string") out.add(prefix + key);
20
+ else if (value && typeof value === "object") flatten(value, `${prefix}${key}.`, out);
21
+ }
22
+ }
23
+
24
+ /** The violations of `permission-texts`, as messages that start with the file they concern. */
25
+ export function checkPermissionTexts({ root, config: { generated, keys, locales, dir = "i18n" } }) {
26
+ const generatedFile = path.join(root, generated);
27
+ if (!existsSync(generatedFile)) return [`${generated}: permission-texts — the generated permissions file does not exist.`];
28
+ const ids = generatedIds(readFileSync(generatedFile, "utf8"));
29
+ const known = new Set(ids);
30
+ const violations = [];
31
+ for (const locale of locales) {
32
+ for (const key of keys) {
33
+ const [namespace, ...rest] = key.split(".");
34
+ const prefix = rest.join(".").replace("{id}", "");
35
+ const file = `${dir}/${namespace}/${locale}.json`;
36
+ if (!existsSync(path.join(root, file))) {
37
+ violations.push(`${file}: permission-texts — the file does not exist.`);
38
+ continue;
39
+ }
40
+ let messages;
41
+ try {
42
+ messages = JSON.parse(readFileSync(path.join(root, file), "utf8"));
43
+ } catch (error) {
44
+ violations.push(`${file}: permission-texts — not valid JSON (${error.message}).`);
45
+ continue;
46
+ }
47
+ const have = new Set();
48
+ flatten(messages, "", have);
49
+ for (const id of ids) if (!have.has(prefix + id)) violations.push(`${file}: permission-texts — ${id} has no text (${key.replace("{id}", id)}).`);
50
+ for (const name of [...have].sort()) {
51
+ if (name.startsWith(prefix) && !known.has(name.slice(prefix.length))) violations.push(`${file}: permission-texts — ${name.slice(prefix.length)} is not in ${generated}; remove its text.`);
52
+ }
53
+ }
54
+ }
55
+ return violations;
56
+ }
@@ -81,6 +81,22 @@ function noRoutePolicy({ sessionField, exclude }) {
81
81
  };
82
82
  }
83
83
 
84
+ /**
85
+ * forbidden: the application's own text rules as data. The first line of the file (comments cut out, tests never scanned, and by default
86
+ * not the e2e / review / scripts / tests / docs / templates / static folders) that `pattern` matches. `exclude` replaces that folder default.
87
+ */
88
+ function forbiddenRule({ pattern, message, exclude = PLAIN_SOURCE }) {
89
+ return ({ file, content, isTest }) => {
90
+ if (isTest || matches(exclude, file)) return null;
91
+ const hit = withoutComments(content)
92
+ .split("\n")
93
+ .find((line) => matches(pattern, line));
94
+ return hit ? `“${hit.trim().slice(0, 80)}” — ${message}` : null;
95
+ };
96
+ }
97
+
98
+ const PLAIN_SOURCE = /^(e2e|review|scripts|tests|docs|templates|static)\//;
99
+
84
100
  export const RULE_IDS = ["no-hardcoded-copy", "no-route-policy"];
85
101
 
86
102
  /** The rules the config switches on, each `{ id, check, seen }` (`seen`: the allowlisted files that tripped it). */
@@ -91,5 +107,6 @@ export function builtInRules(config) {
91
107
  rules.push({ id: "no-hardcoded-copy", check: noHardcodedCopy({ ...config.copy, scope }) });
92
108
  }
93
109
  if (config.routeAccess) rules.push({ id: "no-route-policy", check: noRoutePolicy(config.routeAccess) });
110
+ for (const rule of config.forbidden) rules.push({ id: rule.id, check: forbiddenRule(rule) });
94
111
  return rules.map((rule) => ({ ...rule, seen: new Set() }));
95
112
  }
@@ -20,10 +20,29 @@ export default function run({ config, root, configPath, err }) {
20
20
  const packRules = [
21
21
  ...settings.packs.filter((name) => Object.hasOwn(PACKS, name)).flatMap((name) => PACKS[name].rules.map((rule) => rule.id)),
22
22
  ...RULE_IDS.filter((id) => (id === "no-hardcoded-copy" ? settings.copy : settings.routeAccess)),
23
+ ...settings.forbidden.map((rule) => rule.id),
23
24
  ];
24
25
  if (settings.copy && !(settings.copy.pattern instanceof RegExp)) problems.push("copy.pattern: a regular expression for the language's letters and words, e.g. /[čćžšđ]|\\b(Spremi|Odustani)\\b/");
25
26
  for (const key of ["scope", "properNames"]) if (settings.copy?.[key] !== undefined && !(settings.copy[key] instanceof RegExp)) problems.push(`copy.${key}: must be a regular expression`);
26
27
  if (settings.routeAccess?.exclude !== undefined && !(settings.routeAccess.exclude instanceof RegExp)) problems.push("routeAccess.exclude: must be a regular expression");
28
+ const ids = new Set([...RULE_IDS, ...ALLOW_RULES, "permission-texts"]);
29
+ for (const [index, rule] of settings.forbidden.entries()) {
30
+ const at = `forbidden[${index}]`;
31
+ if (typeof rule?.id !== "string" || rule.id === "") problems.push(`${at}.id: a rule name, used in the message and the allowlist`);
32
+ else if (ids.has(rule.id)) problems.push(`${at}.id: "${rule.id}" is a built-in rule's name`);
33
+ else ids.add(rule.id);
34
+ if (!(rule?.pattern instanceof RegExp)) problems.push(`${at}.pattern: must be a regular expression`);
35
+ if (typeof rule?.message !== "string" || rule.message === "") problems.push(`${at}.message: what to do instead`);
36
+ if (rule?.exclude !== undefined && !(rule.exclude instanceof RegExp)) problems.push(`${at}.exclude: must be a regular expression`);
37
+ }
38
+ const texts = settings.permissions;
39
+ if (texts) {
40
+ if (typeof texts.generated !== "string") problems.push("permissions.generated: the generated permissions file, relative to the config's folder");
41
+ if (!Array.isArray(texts.keys) || texts.keys.length === 0 || !texts.keys.every((key) => typeof key === "string" && key.includes("{id}") && key.includes("."))) {
42
+ problems.push('permissions.keys: message keys with {id}, "<namespace>.{id}", e.g. ["policies.{id}", "policy_descriptions.{id}"]');
43
+ }
44
+ if (!Array.isArray(texts.locales) || texts.locales.length === 0) problems.push('permissions.locales: the locale file names, e.g. ["hr", "en"]');
45
+ }
27
46
  for (const rule of Object.keys(settings.allow)) {
28
47
  if (!ALLOW_RULES.includes(rule) && !packRules.includes(rule)) problems.push(`allow.${rule}: no such rule; the allowlists are ${[...ALLOW_RULES, ...packRules].join(", ")}`);
29
48
  }
package/bin/config.mjs CHANGED
@@ -32,6 +32,8 @@ export const DEFAULTS = {
32
32
  doc: {},
33
33
  copy: null,
34
34
  routeAccess: null,
35
+ forbidden: [],
36
+ permissions: null,
35
37
  },
36
38
  generator: { template: join(dirname(fileURLToPath(import.meta.url)), "templates", "feature-crud"), out: "features" },
37
39
  e2eTimings: { report: "e2e-timings/results.json" },
@@ -1,5 +1,5 @@
1
1
  import { defineComponent as t, computed as i, renderSlot as n } from "vue";
2
- import { u as l } from "./platform-ORzRR3Tw.js";
2
+ import { u as l } from "./platform-DU9QFts8.js";
3
3
  const m = /* @__PURE__ */ t({
4
4
  __name: "AccessGate",
5
5
  props: {
@@ -1,7 +1,7 @@
1
1
  import { defineComponent as r, openBlock as o, createElementBlock as l, createElementVNode as s, toDisplayString as n, createCommentVNode as d, renderSlot as f, createBlock as u, unref as c, withCtx as p, normalizeClass as _, createVNode as x } from "vue";
2
- import { g as b } from "./options-B48UiYsG.js";
2
+ import { _ as b } from "./FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js";
3
3
  import { _ as h } from "./Icon.vue_vue_type_script_lang-BhhsFYwp.js";
4
- const k = { class: "flex flex-wrap items-end gap-3 px-row-inset" }, g = { class: "flex min-w-0 grow basis-56 flex-col gap-1" }, w = { class: "text-title font-semibold tracking-tight text-content-strong" }, y = {
4
+ const k = { class: "flex flex-wrap items-end gap-3 px-row-inset" }, w = { class: "flex min-w-0 grow basis-56 flex-col gap-1" }, g = { class: "text-title font-semibold tracking-tight text-content-strong" }, y = {
5
5
  key: 0,
6
6
  class: "max-w-readable text-subheadline text-content-muted"
7
7
  }, E = /* @__PURE__ */ r({
@@ -12,8 +12,8 @@ const k = { class: "flex flex-wrap items-end gap-3 px-row-inset" }, g = { class:
12
12
  },
13
13
  setup(e) {
14
14
  return (i, t) => (o(), l("div", k, [
15
- s("div", g, [
16
- s("h2", w, n(e.title), 1),
15
+ s("div", w, [
16
+ s("h2", g, n(e.title), 1),
17
17
  e.description ? (o(), l("p", y, n(e.description), 1)) : d("", !0)
18
18
  ]),
19
19
  f(i.$slots, "actions")
@@ -3,18 +3,18 @@ import { useI18n as D } from "vue-i18n";
3
3
  import { RouterLink as I } from "vue-router";
4
4
  import { _ as T } from "./IconTile.vue_vue_type_script_setup_true_lang-BmxCuUZ9.js";
5
5
  import { _ as z } from "./Tabs.vue_vue_type_script_setup_true_lang-tpkGp98F.js";
6
- import { u as G } from "./format-Cp1h0vsC.js";
7
- import { _ as B } from "./DateField.vue_vue_type_script_setup_true_lang-DzCvYHAf.js";
8
- import { a as H } from "./FormGroup.vue_vue_type_script_setup_true_lang-dsO1m9Mm.js";
9
- import { g as J } from "./options-B48UiYsG.js";
6
+ import { u as G } from "./format-C_HXlOYQ.js";
7
+ import { _ as B } from "./DateField.vue_vue_type_script_setup_true_lang-D0KcfW8R.js";
8
+ import { a as H } from "./FormGroup.vue_vue_type_script_setup_true_lang-lvAuBG-0.js";
9
+ import { _ as J } from "./FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js";
10
10
  import { i as M } from "./preferences-hYhrw8Uy.js";
11
- import { u as Q } from "./platform-ORzRR3Tw.js";
11
+ import { u as Q } from "./platform-DU9QFts8.js";
12
12
  import { b as W } from "./routeResource-DlTsGTg_.js";
13
13
  import { _ as X } from "./AsyncSection.vue_vue_type_script_setup_true_lang-CX8mpXRE.js";
14
14
  import { _ as Y } from "./Badge.vue_vue_type_script_setup_true_lang-DJyZc0C7.js";
15
15
  import { u as Z } from "./useLoad-M7V-AVwR.js";
16
- import { P as tt, f as et, j as at } from "./index-DM_nWOYf.js";
17
- import { _ as st } from "./ActionRow.vue_vue_type_script_setup_true_lang-DybAIOHS.js";
16
+ import { P as tt, f as et, j as at } from "./index-BA2w8uwD.js";
17
+ import { _ as st } from "./ActionRow.vue_vue_type_script_setup_true_lang-B5IMNnTf.js";
18
18
  const ot = {
19
19
  class: "flex min-w-0 flex-col gap-group-gap",
20
20
  "data-person-activity": ""