@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.
- package/CHANGELOG.md +32 -0
- package/README.md +4 -3
- package/bin/architecture/engine.mjs +3 -0
- package/bin/architecture/permissions.mjs +56 -0
- package/bin/architecture/rules.mjs +17 -0
- package/bin/commands/check-architecture.mjs +19 -0
- package/bin/config.mjs +2 -0
- package/dist/{AccessGate.vue_vue_type_script_setup_true_lang-BH3f0hda.js → AccessGate.vue_vue_type_script_setup_true_lang-Bg8JFAOT.js} +1 -1
- package/dist/{ActionRow.vue_vue_type_script_setup_true_lang-DybAIOHS.js → ActionRow.vue_vue_type_script_setup_true_lang-B5IMNnTf.js} +4 -4
- package/dist/{Activity-DM9t5d3D.js → Activity-CiYn5E6A.js} +7 -7
- package/dist/{AdaptivePageShell.vue_vue_type_script_setup_true_lang-uO4EAmBb.js → AdaptivePageShell.vue_vue_type_script_setup_true_lang-oXWQTfBs.js} +170 -168
- package/dist/{Changes-DYpysvfX.js → Changes-Cy8STpLX.js} +3 -3
- package/dist/{CollectionPage.vue_vue_type_script_setup_true_lang-qxeSTAKV.js → CollectionPage.vue_vue_type_script_setup_true_lang-Dr6nFNBB.js} +6 -6
- package/dist/{DataTable.vue_vue_type_script_setup_true_lang-DbRS37n3.js → DataTable.vue_vue_type_script_setup_true_lang-D372wlxR.js} +1 -1
- package/dist/{DateField.vue_vue_type_script_setup_true_lang-DzCvYHAf.js → DateField.vue_vue_type_script_setup_true_lang-D0KcfW8R.js} +17 -16
- package/dist/{DeadLetters-vt9vhJD7.js → DeadLetters-DhJkMpd_.js} +4 -4
- 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
- package/dist/FormRow.vue_vue_type_script_setup_true_lang-CpgNVQF4.js +160 -0
- 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
- package/dist/{General-oUI2MiwO.js → General-CwTPC5UP.js} +11 -11
- package/dist/{IncidentRecord-CQdv_bN5.js → IncidentRecord-hXEGuLGg.js} +14 -14
- package/dist/{IncidentsList-DwQ0wuM8.js → IncidentsList-C41VyDwK.js} +6 -6
- package/dist/{NewRolePage-C-wbA-uW.js → NewRolePage-BYYZJkK7.js} +3 -3
- 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
- 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
- package/dist/{ProfilePage-Di77M2cX.js → ProfilePage-DGEsIycR.js} +13 -13
- package/dist/{RecordHeader.vue_vue_type_script_setup_true_lang-CZtvUH5L.js → RecordHeader.vue_vue_type_script_setup_true_lang-B3BnYg1x.js} +1 -1
- package/dist/{ResourcePage.vue_vue_type_script_setup_true_lang-uLrX13ab.js → ResourcePage.vue_vue_type_script_setup_true_lang-1iZtj3rh.js} +1 -1
- package/dist/{RoleEditor.vue_vue_type_script_setup_true_lang-V2D7fhR9.js → RoleEditor.vue_vue_type_script_setup_true_lang-BslSDDiM.js} +10 -10
- package/dist/{RolePage-Crp71rRb.js → RolePage-C08ZUqw1.js} +9 -9
- package/dist/{RolesPage-DKXXnnDV.js → RolesPage-FOuY1VKg.js} +10 -10
- package/dist/{SectionNavigator.vue_vue_type_script_setup_true_lang-5ogQCj8M.js → SectionNavigator.vue_vue_type_script_setup_true_lang-DCTFxfPB.js} +2 -2
- 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
- package/dist/{SelectField.vue_vue_type_script_setup_true_lang-CLkoPbGm.js → SelectField.vue_vue_type_script_setup_true_lang-D5xYofdb.js} +10 -9
- package/dist/{ServiceAccountDialog.vue_vue_type_script_setup_true_lang-C7WKxJ9i.js → ServiceAccountDialog.vue_vue_type_script_setup_true_lang-CDr4WNIb.js} +9 -9
- package/dist/{ServiceAccountRecord-CTU10RWy.js → ServiceAccountRecord-kWwIQGZb.js} +12 -12
- package/dist/{ServiceAccountsList-BVRjoswH.js → ServiceAccountsList-CKy9_YHP.js} +8 -8
- package/dist/{Sessions-BHwMrF7c.js → Sessions-BDc8gdYH.js} +4 -4
- package/dist/{SettingsPage-lRWvsMYK.js → SettingsPage-t-8kmFVT.js} +13 -13
- package/dist/{SignIn-HnujOMRY.js → SignIn-CqtkvtCZ.js} +7 -7
- package/dist/{SignInPage-c886Fvd7.js → SignInPage-DcdNKZIb.js} +4 -4
- package/dist/{SignIns-BsTlN2Zc.js → SignIns-D4-nGu0B.js} +3 -3
- package/dist/{SwitchField.vue_vue_type_script_setup_true_lang-Ds4q0bbl.js → SwitchField.vue_vue_type_script_setup_true_lang-CLtcuofe.js} +15 -14
- 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
- package/dist/{TextareaField.vue_vue_type_script_setup_true_lang-D2j4s96s.js → TextareaField.vue_vue_type_script_setup_true_lang-YxUlNHSk.js} +6 -5
- 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
- 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
- package/dist/{UserRecord-DrbpVHh9.js → UserRecord-CwZCr9Im.js} +10 -10
- package/dist/{UsersList-M5tBxrox.js → UsersList-Cn19sFgv.js} +4 -4
- package/dist/access/index.js +4 -4
- package/dist/app/application.d.ts +2 -0
- package/dist/app/index.js +2 -2
- package/dist/app/validate.d.ts +3 -1
- package/dist/{application-C900Wz0A.js → application-CDSweJb9.js} +144 -142
- package/dist/back-LcLlFodC.js +63 -0
- package/dist/{chrome-nUcmVu8G.js → chrome-kMBKIB1N.js} +2 -2
- package/dist/collection/authors.d.ts +27 -0
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +81 -76
- package/dist/config/index.d.ts +42 -0
- package/dist/config/index.js +12 -3
- package/dist/content/Panel.vue.d.ts +1 -1
- package/dist/content/index.js +2 -2
- package/dist/{contributions-9CPT-Z5v.js → contributions-B0x5x5xD.js} +1 -1
- package/dist/form/CommandDialog.vue.d.ts +1 -1
- package/dist/form/DateField.vue.d.ts +1 -1
- package/dist/form/DateTimeField.vue.d.ts +1 -1
- package/dist/form/FileField.vue.d.ts +1 -1
- package/dist/form/FormRow.vue.d.ts +2 -2
- package/dist/form/I18nField.vue.d.ts +2 -2
- package/dist/form/TextField.vue.d.ts +1 -1
- package/dist/form/TextareaField.vue.d.ts +2 -2
- package/dist/form/date/value.d.ts +21 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/form/index.js +745 -715
- package/dist/format/format.d.ts +8 -2
- package/dist/format/index.js +1 -1
- package/dist/format-C_HXlOYQ.js +136 -0
- package/dist/{guards-6fJxfUtk.js → guards-CNJ0ATVC.js} +1 -1
- package/dist/i18n/index.d.ts +14 -2
- package/dist/i18n/index.js +84 -73
- package/dist/i18n/messages.d.ts +10 -1
- package/dist/i18n/runtime.d.ts +3 -1
- package/dist/identity/index.js +3 -3
- package/dist/identity/profile/EmailChangeDialog.vue.d.ts +2 -2
- package/dist/identity/users/list.d.ts +1 -1
- package/dist/incidents/index.js +1 -1
- package/dist/{index-_hDHxrt5.js → index-1HS7O2-e.js} +9 -9
- package/dist/{index-DM_nWOYf.js → index-BA2w8uwD.js} +86 -86
- package/dist/{index-vES_D5ls.js → index-SLJWm40T.js} +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modal/DiscardChangesModal.vue.d.ts +2 -2
- package/dist/modal/Modal.vue.d.ts +2 -2
- package/dist/modal/Sheet.vue.d.ts +2 -2
- package/dist/modules/identity/routes.d.ts +1 -1
- package/dist/modules/identity/schemas.d.ts +2 -2
- package/dist/{navigation-DMJ6akat.js → navigation-d-JAPBGA.js} +1 -1
- package/dist/{neighbors-DrEahe7B.js → neighbors-Dg_LU9ry.js} +32 -31
- package/dist/notifications/index.js +1 -1
- package/dist/onboarding/index.js +1 -1
- package/dist/options-mOOX6JSS.js +214 -0
- package/dist/overlay/AlertDialog.vue.d.ts +3 -3
- package/dist/overlay/Menu.vue.d.ts +2 -2
- package/dist/overlay/PhotoViewer.vue.d.ts +2 -2
- package/dist/overlay/Popover.vue.d.ts +2 -2
- package/dist/page/SectionNavigator.vue.d.ts +1 -1
- package/dist/page/SectionPanel.vue.d.ts +1 -1
- package/dist/page/back.d.ts +37 -0
- package/dist/page/index.d.ts +1 -0
- package/dist/page/index.js +46 -42
- package/dist/phone/index.js +57 -56
- package/dist/platform/index.d.ts +10 -1
- package/dist/platform/index.js +16 -15
- package/dist/platform/platform.d.ts +4 -2
- package/dist/{platform-ORzRR3Tw.js → platform-DU9QFts8.js} +7 -6
- package/dist/playwright/config.d.ts +33 -0
- package/dist/playwright/index.d.ts +2 -0
- package/dist/playwright/index.js +192 -171
- package/dist/resource/index.js +1 -1
- package/dist/router/index.js +3 -3
- package/dist/router/sections.d.ts +1 -1
- package/dist/{rowLayout-QKfNHsML.js → rowLayout-PIuD7MGo.js} +2 -2
- package/dist/sections-BpiRLntE.js +54 -0
- package/dist/{serverMessages-Ce8-MK4e.js → serverMessages-C_KkDvpT.js} +1 -1
- package/dist/shell/index.js +5 -5
- package/dist/state/NextStepPanel.vue.d.ts +63 -0
- package/dist/state/PhaseTiles.vue.d.ts +20 -0
- package/dist/state/PhaseTrack.vue.d.ts +48 -0
- package/dist/state/RequirementList.vue.d.ts +10 -0
- package/dist/state/TodoRow.vue.d.ts +52 -0
- package/dist/state/index.d.ts +9 -0
- package/dist/state/index.js +731 -126
- package/dist/state/phaseStep.d.ts +7 -0
- package/dist/state/phaseTile.d.ts +10 -0
- package/dist/state/requirementItem.d.ts +16 -0
- package/dist/state/todoRow.d.ts +8 -0
- package/dist/{state-QDaxntgR.js → state-z1iPF1DT.js} +1 -1
- package/dist/styles.css +1 -1
- package/dist/{suggestions-q_v3M7G6.js → suggestions-DeMgs_ki.js} +1 -1
- package/dist/testing/format.d.ts +1 -1
- package/dist/testing/index.js +31 -31
- package/dist/{useCommand-DAMgQ5NE.js → useCommand-DQKvro1n.js} +2 -2
- package/dist/{useGroupSheet-B-Hs4gNO.js → useGroupSheet-CRw3MUmX.js} +3 -3
- package/dist/{usePreferences-BfMXVbSO.js → usePreferences-DB29UTFD.js} +1 -1
- package/dist/{useRecordDialog-QPKFH1Ww.js → useRecordDialog-0azHxxvm.js} +1 -1
- package/dist/{useResourceForm-Oa6bk3NI.js → useResourceForm-zVuWerSn.js} +1 -1
- package/dist/{useSaveChrome-7huA3FzR.js → useSaveChrome-DB9OwIjT.js} +1 -1
- package/dist/{useTypedEntry-CITij2C_.js → useTypedEntry-BsMV66Yc.js} +46 -45
- package/package.json +1 -1
- package/dist/format-Cp1h0vsC.js +0 -125
- package/dist/options-B48UiYsG.js +0 -368
- 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",
|
|
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",
|
|
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,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 {
|
|
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" },
|
|
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",
|
|
16
|
-
s("h2",
|
|
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-
|
|
7
|
-
import { _ as B } from "./DateField.vue_vue_type_script_setup_true_lang-
|
|
8
|
-
import { a as H } from "./FormGroup.vue_vue_type_script_setup_true_lang-
|
|
9
|
-
import {
|
|
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-
|
|
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-
|
|
17
|
-
import { _ as st } from "./ActionRow.vue_vue_type_script_setup_true_lang-
|
|
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": ""
|