frontend-harness 0.7.6 → 0.7.7
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 +8 -0
- package/README.md +1 -1
- package/dist/runtime/scaffold/vue-template.js +7 -4
- package/dist/runtime/scaffold/vue-template.js.map +1 -1
- package/package.json +1 -1
- package/templates/scaffold/vue/README.md.tpl +14 -7
- package/templates/scaffold/vue/env.example.tpl +1 -0
- package/templates/scaffold/vue/eslint.config.mjs +5 -0
- package/templates/scaffold/vue/src/env.d.ts +9 -1
- package/templates/scaffold/vue/src/features/home/HomeView.vue +4 -4
- package/templates/scaffold/vue/src/service/http.ts +75 -11
- package/templates/scaffold/vue/src/service/index.ts +1 -1
- package/templates/scaffold/vue/src/storage/index.ts +59 -4
- package/templates/scaffold/vue/src/styles/main.scss +2 -9
- package/templates/scaffold/vue/src/styles/reset.scss +23 -0
- package/templates/scaffold/vue/src/styles/tokens.scss +0 -1
- package/templates/scaffold/vue/src/styles/utilities.scss +38 -0
- package/templates/scaffold/vue/src/utils/index.ts +69 -0
- package/templates/scaffold/vue/tests/e2e/app-desktop.spec.ts +1 -1
- package/templates/scaffold/vue/tests/e2e/app-mobile.spec.ts +1 -1
- package/templates/scaffold/vue/tests/integration/app.test.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to `frontend-harness` are documented here.
|
|
|
4
4
|
|
|
5
5
|
This project follows semver while it is pre-1.0: patch releases may include harness policy improvements that tighten validation, and minor releases may change generated protocol expectations.
|
|
6
6
|
|
|
7
|
+
## 0.7.7 - 2026-05-26
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Reframed the Vue scaffold as a general Vue application baseline instead of an admin-oriented template, with admin shells, menus, permissions, and table-page workflows documented as optional recipes.
|
|
12
|
+
- Strengthened the default Vue scaffold with product-neutral utility, storage, HTTP client, reset, and utility-style layers inspired by mature Vue projects while avoiding Element Plus style variants.
|
|
13
|
+
- Added `.env.example` and typed `VITE_API_BASE_URL` support to the Vue scaffold, tightened generated dependency ranges, and verified generated projects through lint, typecheck, test, and build.
|
|
14
|
+
|
|
7
15
|
## 0.7.6 - 2026-05-25
|
|
8
16
|
|
|
9
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ npm run harness:clean
|
|
|
136
136
|
|
|
137
137
|
Use `init` after installation or package upgrades. Use `protocol check` when you want to verify that `AGENTS.md` and `CLAUDE.md` are current. Use `clean` to remove ignored runtime artifacts while preserving durable project skills, knowledge, and config.
|
|
138
138
|
|
|
139
|
-
`scaffold --preset vue` creates a Vue 3 + Vite + TypeScript engineering baseline
|
|
139
|
+
`scaffold --preset vue` creates a general Vue 3 + Vite + TypeScript engineering baseline. It includes Vite config, ESLint/Prettier, Element Plus auto imports, product-neutral style layers, and small `utils`, `storage`, and `service` layers. Mature internal Vue applications can inform the baseline, but the default scaffold is not an admin template and does not create dashboards, sidebars, permission systems, business pages, or domain components. Admin-style shells, table pages, route permissions, and menu trees belong in optional recipes rather than the default preset.
|
|
140
140
|
|
|
141
141
|
UI restoration work treats design exports, screenshots, and generated code as source evidence for implementation and loop repair, not as paperwork to backfill after coding. For design-backed restoration, visual equivalence is the highest priority for the first project implementation: source material does not mean the agent may redesign the screen. When design_to_code or generated design markup is available, preserve it and classify whether it is usable_as_visual_baseline, usable_for_patch_reference, usable_as_source, or unusable before deciding whether it can seed implementation or only guide repair; verify enforces that classification through visual-baseline metadata for design_to_code sources, and temporary asset localization is required only when the generated artifact is used as project source. Project-native rewrites may change file placement, framework integration, semantic markup, asset localization, naming, state wiring, and component boundaries only when the rendered result preserves the design's primary layout, spacing, typography, color, density, hierarchy, and visible states. When a design contains images, classify each one as required content, functional UI element, brand/product asset, decorative sample, or replaceable placeholder before implementation. Required images should become durable assets; decorative samples and placeholders should be replaced or documented in design evidence. Final handoff evidence should include a restoration verdict with score or pass/fail judgement, key visual differences, accepted gaps, and links to evidence artifacts. Component splits should follow semantic responsibility, reuse, and interaction boundaries rather than mirroring every design layer as a component.
|
|
142
142
|
|
|
@@ -4,6 +4,7 @@ const VUE_TEMPLATE_ROOT = new URL("../../../templates/scaffold/vue/", import.met
|
|
|
4
4
|
const VUE_TEMPLATE_FILES = [
|
|
5
5
|
{ source: "gitignore.tpl", target: ".gitignore" },
|
|
6
6
|
{ source: ".husky/pre-commit", target: ".husky/pre-commit", mode: 0o755 },
|
|
7
|
+
{ source: "env.example.tpl", target: ".env.example" },
|
|
7
8
|
{ source: "eslint.config.mjs", target: "eslint.config.mjs" },
|
|
8
9
|
{ source: "index.html.tpl", target: "index.html" },
|
|
9
10
|
{ source: "postcss.config.mjs", target: "postcss.config.mjs" },
|
|
@@ -16,7 +17,9 @@ const VUE_TEMPLATE_FILES = [
|
|
|
16
17
|
{ source: "src/env.d.ts", target: "src/env.d.ts" },
|
|
17
18
|
{ source: "src/features/home/HomeView.vue", target: "src/features/home/HomeView.vue" },
|
|
18
19
|
{ source: "src/styles/main.scss", target: "src/styles/main.scss" },
|
|
20
|
+
{ source: "src/styles/reset.scss", target: "src/styles/reset.scss" },
|
|
19
21
|
{ source: "src/styles/tokens.scss", target: "src/styles/tokens.scss" },
|
|
22
|
+
{ source: "src/styles/utilities.scss", target: "src/styles/utilities.scss" },
|
|
20
23
|
{ source: "src/utils/index.ts", target: "src/utils/index.ts" },
|
|
21
24
|
{ source: "src/storage/index.ts", target: "src/storage/index.ts" },
|
|
22
25
|
{ source: "src/service/http.ts", target: "src/service/http.ts" },
|
|
@@ -90,18 +93,18 @@ function packageJson(projectName) {
|
|
|
90
93
|
"eslint-plugin-vue": "^10.6.2",
|
|
91
94
|
"frontend-harness": `^${currentHarnessVersion()}`,
|
|
92
95
|
husky: "^9.1.7",
|
|
93
|
-
jsdom: "
|
|
96
|
+
jsdom: "~25.0.1",
|
|
94
97
|
"lint-staged": "^15.5.2",
|
|
95
98
|
postcss: "^8.4.47",
|
|
96
99
|
prettier: "^3.3.3",
|
|
97
|
-
sass: "
|
|
100
|
+
sass: "~1.93.2",
|
|
98
101
|
typescript: "^5.7.0",
|
|
99
102
|
"unplugin-auto-import": "^19.0.0",
|
|
100
103
|
"unplugin-vue-components": "^28.0.0",
|
|
101
104
|
"vue-eslint-parser": "^10.2.0",
|
|
102
105
|
"vue-tsc": "^3.0.7",
|
|
103
|
-
vite: "
|
|
104
|
-
vitest: "
|
|
106
|
+
vite: "~5.4.21",
|
|
107
|
+
vitest: "~2.1.9"
|
|
105
108
|
},
|
|
106
109
|
"lint-staged": {
|
|
107
110
|
"src/**/*.{ts,vue}": [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vue-template.js","sourceRoot":"","sources":["../../../src/runtime/scaffold/vue-template.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vue-template.js","sourceRoot":"","sources":["../../../src/runtime/scaffold/vue-template.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EACL,UAAU,EACV,IAAI,EACJ,2BAA2B,EAG5B,MAAM,wBAAwB,CAAC;AAEhC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,kCAAkC,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEvF,MAAM,kBAAkB,GAAmC;IACzD,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE;IACjD,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,KAAK,EAAE;IACzE,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,cAAc,EAAE;IACrD,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,mBAAmB,EAAE;IAC5D,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,YAAY,EAAE;IAClD,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,oBAAoB,EAAE;IAC9D,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE;IAChD,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,EAAE;IACxD,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,mBAAmB,EAAE;IAC5D,EAAE,MAAM,EAAE,uBAAuB,EAAE,MAAM,EAAE,uBAAuB,EAAE;IACpE,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE;IAChD,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE;IAChD,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE;IAClD,EAAE,MAAM,EAAE,gCAAgC,EAAE,MAAM,EAAE,gCAAgC,EAAE;IACtF,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,EAAE,sBAAsB,EAAE;IAClE,EAAE,MAAM,EAAE,uBAAuB,EAAE,MAAM,EAAE,uBAAuB,EAAE;IACpE,EAAE,MAAM,EAAE,wBAAwB,EAAE,MAAM,EAAE,wBAAwB,EAAE;IACtE,EAAE,MAAM,EAAE,2BAA2B,EAAE,MAAM,EAAE,2BAA2B,EAAE;IAC5E,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,EAAE,oBAAoB,EAAE;IAC9D,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,EAAE,sBAAsB,EAAE;IAClE,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,qBAAqB,EAAE;IAChE,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,EAAE,sBAAsB,EAAE;IAClE,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE;IACtD,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,EAAE;IACxD,EAAE,MAAM,EAAE,+BAA+B,EAAE,MAAM,EAAE,+BAA+B,EAAE;IACpF,EAAE,MAAM,EAAE,+BAA+B,EAAE,MAAM,EAAE,+BAA+B,EAAE;IACpF,EAAE,MAAM,EAAE,8BAA8B,EAAE,MAAM,EAAE,8BAA8B,EAAE;CACnF,CAAC;AAEF,MAAM,UAAU,qBAAqB,CAAC,WAAmB;IACvD,OAAO,2BAA2B,CAAC;QACjC,YAAY,EAAE,iBAAiB;QAC/B,OAAO,EAAE;YACP,WAAW;YACX,eAAe,EAAE,UAAU,CAAC,WAAW,CAAC;SACzC;QACD,WAAW,EAAE;YACX,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,CAAC,WAAW,CAAC,EAAE;YAC3D,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE;YACtG,EAAE,IAAI,EAAE,+BAA+B,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE;YACnE,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;YAC9C,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE;SACxD;QACD,aAAa,EAAE,kBAAkB;KAClC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC3F,OAAO,UAAU,IAAI,cAAc,CAAC;AACtC,CAAC;AAED,SAAS,WAAW,CAAC,WAAmB;IACtC,OAAO,IAAI,CAAC;QACV,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE;YACP,GAAG,EAAE,aAAa;YAClB,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE,qBAAqB;YAC9B,IAAI,EAAE,UAAU;YAChB,SAAS,EAAE,kBAAkB;YAC7B,IAAI,EAAE,YAAY;YAClB,UAAU,EAAE,iBAAiB;YAC7B,MAAM,EAAE,oBAAoB;YAC5B,MAAM,EAAE,gEAAgE;YACxE,OAAO,EAAE,kBAAkB;YAC3B,cAAc,EAAE,uBAAuB;YACvC,eAAe,EAAE,iCAAiC;YAClD,eAAe,EAAE,+BAA+B;YAChD,OAAO,EAAE,OAAO;SACjB;QACD,YAAY,EAAE;YACZ,KAAK,EAAE,SAAS;YAChB,cAAc,EAAE,SAAS;YACzB,GAAG,EAAE,SAAS;SACf;QACD,eAAe,EAAE;YACf,kBAAkB,EAAE,SAAS;YAC7B,YAAY,EAAE,SAAS;YACvB,2BAA2B,EAAE,QAAQ;YACrC,6BAA6B,EAAE,SAAS;YACxC,sBAAsB,EAAE,QAAQ;YAChC,2BAA2B,EAAE,SAAS;YACtC,aAAa,EAAE,SAAS;YACxB,oBAAoB,EAAE,QAAQ;YAC9B,YAAY,EAAE,UAAU;YACxB,MAAM,EAAE,SAAS;YACjB,wBAAwB,EAAE,SAAS;YACnC,mBAAmB,EAAE,SAAS;YAC9B,kBAAkB,EAAE,IAAI,qBAAqB,EAAE,EAAE;YACjD,KAAK,EAAE,QAAQ;YACf,KAAK,EAAE,SAAS;YAChB,aAAa,EAAE,SAAS;YACxB,OAAO,EAAE,SAAS;YAClB,QAAQ,EAAE,QAAQ;YAClB,IAAI,EAAE,SAAS;YACf,UAAU,EAAE,QAAQ;YACpB,sBAAsB,EAAE,SAAS;YACjC,yBAAyB,EAAE,SAAS;YACpC,mBAAmB,EAAE,SAAS;YAC9B,SAAS,EAAE,QAAQ;YACnB,IAAI,EAAE,SAAS;YACf,MAAM,EAAE,QAAQ;SACjB;QACD,aAAa,EAAE;YACb,mBAAmB,EAAE;gBACnB,kBAAkB;gBAClB,cAAc;aACf;SACF;KACF,CAAC,CAAC;AACL,CAAC;AAED,SAAS,qBAAqB;IAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAErG,CAAC;IACF,OAAO,OAAO,WAAW,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AACjF,CAAC;AAED,SAAS,aAAa;IACpB,OAAO,IAAI,CAAC;QACV,YAAY,EAAE;YACZ,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE;gBACzC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB,EAAE;gBACnD,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE;gBACrC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE;aAC5C;SACF;KACF,CAAC,CAAC;AACL,CAAC;AAED,SAAS,QAAQ;IACf,OAAO,IAAI,CAAC;QACV,eAAe,EAAE;YACf,MAAM,EAAE,QAAQ;YAChB,uBAAuB,EAAE,IAAI;YAC7B,GAAG,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,cAAc,CAAC;YACtC,MAAM,EAAE,QAAQ;YAChB,YAAY,EAAE,IAAI;YAClB,gBAAgB,EAAE,SAAS;YAC3B,iBAAiB,EAAE,IAAI;YACvB,eAAe,EAAE,IAAI;YACrB,MAAM,EAAE,IAAI;YACZ,GAAG,EAAE,UAAU;YACf,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,GAAG;YACZ,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE;YAC3B,KAAK,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,gBAAgB,EAAE,2BAA2B,CAAC;SAC9E;QACD,OAAO,EAAE,CAAC,aAAa,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,CAAC;QAC1F,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC;KAC/C,CAAC,CAAC;AACL,CAAC;AAED,SAAS,YAAY;IACnB,OAAO,IAAI,CAAC;QACV,eAAe,EAAE;YACf,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;YACb,mBAAmB,EAAE,IAAI;YACzB,WAAW,EAAE,IAAI;YACjB,cAAc,EAAE,IAAI;YACpB,MAAM,EAAE,iCAAiC;YACzC,MAAM,EAAE,QAAQ;YAChB,gBAAgB,EAAE,SAAS;YAC3B,4BAA4B,EAAE,IAAI;SACnC;QACD,OAAO,EAAE,CAAC,iBAAiB,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,oBAAoB,CAAC;KACtH,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# {{projectName}}
|
|
2
2
|
|
|
3
|
-
Vue 3 + Vite
|
|
3
|
+
Vue 3 + Vite application scaffold.
|
|
4
4
|
|
|
5
|
-
This preset
|
|
6
|
-
|
|
5
|
+
This preset provides a general Vue engineering baseline. It includes project infrastructure and small reusable layers, but it is not an admin-management template.
|
|
6
|
+
Use mature internal Vue applications as references for proven patterns, then pull admin shells, table pages, menus, route permissions, and business workflows in as project-specific recipes only when the application needs them.
|
|
7
7
|
|
|
8
8
|
## Scripts
|
|
9
9
|
|
|
@@ -20,17 +20,24 @@ It intentionally avoids business pages, admin routes, dashboards, permissions, a
|
|
|
20
20
|
- `npm run harness:check`
|
|
21
21
|
- `npm run harness:clean`
|
|
22
22
|
|
|
23
|
-
## Base
|
|
23
|
+
## Base Layers
|
|
24
24
|
|
|
25
25
|
- `src/utils` for shared helpers
|
|
26
26
|
- `src/storage` for browser storage wrappers
|
|
27
|
-
- `src/service` for API client setup
|
|
28
|
-
- `src/styles` for project-wide
|
|
27
|
+
- `src/service` for configurable API client setup, token injection hooks, blob passthrough, and normalized errors
|
|
28
|
+
- `src/styles` for project-wide style layers
|
|
29
29
|
- `src/styles/tokens.scss` for reusable UI restoration tokens
|
|
30
|
-
- `src/
|
|
30
|
+
- `src/styles/reset.scss` for browser reset rules
|
|
31
|
+
- `src/styles/utilities.scss` for small, product-neutral utility classes
|
|
32
|
+
- `src/features/<feature>` for feature-local views, types, fixtures, calculations, and components
|
|
31
33
|
- `tests/integration` for Vitest + Testing Library behavior tests
|
|
32
34
|
- `tests/e2e` for Playwright browser smoke tests
|
|
33
35
|
|
|
36
|
+
## Optional Recipes
|
|
37
|
+
|
|
38
|
+
Admin layouts, side navigation, menu trees, route permissions, searchable tables, and pagination shells are intentionally outside this default preset.
|
|
39
|
+
Add those as project recipes or feature templates when the product is actually an admin-style application.
|
|
40
|
+
|
|
34
41
|
## UI restoration workflow
|
|
35
42
|
|
|
36
43
|
Use the injected `ui-implementation` project skill and `.frontend-harness/guidance/latest.md` for the current harness plan.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VITE_API_BASE_URL=/api
|
|
@@ -15,9 +15,11 @@ export default [
|
|
|
15
15
|
languageOptions: {
|
|
16
16
|
globals: {
|
|
17
17
|
document: "readonly",
|
|
18
|
+
clearTimeout: "readonly",
|
|
18
19
|
localStorage: "readonly",
|
|
19
20
|
sessionStorage: "readonly",
|
|
20
21
|
Storage: "readonly",
|
|
22
|
+
setTimeout: "readonly",
|
|
21
23
|
window: "readonly"
|
|
22
24
|
}
|
|
23
25
|
}
|
|
@@ -30,6 +32,9 @@ export default [
|
|
|
30
32
|
ecmaVersion: "latest",
|
|
31
33
|
sourceType: "module"
|
|
32
34
|
}
|
|
35
|
+
},
|
|
36
|
+
rules: {
|
|
37
|
+
"no-unused-vars": "off"
|
|
33
38
|
}
|
|
34
39
|
},
|
|
35
40
|
{
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly VITE_API_BASE_URL?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface ImportMeta {
|
|
8
|
+
readonly env: ImportMetaEnv;
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
declare module "*.vue" {
|
|
4
12
|
import type { DefineComponent } from "vue";
|
|
5
|
-
const component: DefineComponent<
|
|
13
|
+
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>;
|
|
6
14
|
export default component;
|
|
7
15
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<main class="app-shell">
|
|
3
3
|
<section class="hero">
|
|
4
|
-
<p class="eyebrow">Vue
|
|
5
|
-
<h1>
|
|
4
|
+
<p class="eyebrow">Vue application</p>
|
|
5
|
+
<h1>Project foundation is ready</h1>
|
|
6
6
|
<p class="lead">
|
|
7
|
-
This scaffold
|
|
7
|
+
This scaffold gives the project a neutral Vue foundation: Vite, TypeScript, Element Plus, auto imports, storage, utility, and service layers.
|
|
8
8
|
</p>
|
|
9
9
|
</section>
|
|
10
10
|
<section class="grid">
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
</article>
|
|
19
19
|
<article>
|
|
20
20
|
<h2>service</h2>
|
|
21
|
-
<p>API client setup and error normalization.</p>
|
|
21
|
+
<p>API client setup and error normalization without product-specific assumptions.</p>
|
|
22
22
|
</article>
|
|
23
23
|
</section>
|
|
24
24
|
</main>
|
|
@@ -1,18 +1,82 @@
|
|
|
1
|
-
import axios from "axios";
|
|
1
|
+
import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from "axios";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
baseURL
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
export interface HttpClientOptions {
|
|
4
|
+
baseURL?: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
getToken?: () => string | null | undefined;
|
|
7
|
+
onUnauthorized?: (error: Error) => void | Promise<void>;
|
|
8
|
+
isSuccess?: (payload: unknown, response: AxiosResponse) => boolean;
|
|
9
|
+
getMessage?: (payload: unknown, response?: AxiosResponse) => string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HttpError extends Error {
|
|
13
|
+
status?: number;
|
|
14
|
+
code?: string | number;
|
|
15
|
+
payload?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createHttpClient(options: HttpClientOptions = {}) {
|
|
19
|
+
const client = axios.create({
|
|
20
|
+
baseURL: options.baseURL ?? import.meta.env.VITE_API_BASE_URL ?? "/api",
|
|
21
|
+
timeout: options.timeoutMs ?? 15_000,
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
paramsSerializer: {
|
|
24
|
+
indexes: null
|
|
25
|
+
}
|
|
26
|
+
});
|
|
7
27
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
client.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
|
29
|
+
const token = options.getToken?.();
|
|
30
|
+
if (token) {
|
|
31
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
client.interceptors.response.use(
|
|
37
|
+
async (response) => {
|
|
38
|
+
if (response.config.responseType === "blob") return response;
|
|
39
|
+
if (options.isSuccess && !options.isSuccess(response.data, response)) {
|
|
40
|
+
const error = createHttpError(options.getMessage?.(response.data, response) ?? "Request failed", response, response.data);
|
|
41
|
+
if (error.status === 401) await options.onUnauthorized?.(error);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
return response;
|
|
45
|
+
},
|
|
46
|
+
async (error) => {
|
|
47
|
+
const normalized = normalizeHttpError(error, options);
|
|
48
|
+
if (normalized.status === 401) await options.onUnauthorized?.(normalized);
|
|
49
|
+
return Promise.reject(normalized);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return client;
|
|
54
|
+
}
|
|
12
55
|
|
|
13
|
-
export
|
|
56
|
+
export const http = createHttpClient();
|
|
57
|
+
|
|
58
|
+
export function normalizeHttpError(error: unknown, options: Pick<HttpClientOptions, "getMessage"> = {}): HttpError {
|
|
14
59
|
if (axios.isAxiosError(error)) {
|
|
15
|
-
return
|
|
60
|
+
return createHttpError(
|
|
61
|
+
options.getMessage?.(error.response?.data, error.response) ?? error.response?.data?.message ?? error.message ?? "Request failed",
|
|
62
|
+
error.response,
|
|
63
|
+
error.response?.data
|
|
64
|
+
);
|
|
16
65
|
}
|
|
17
66
|
return error instanceof Error ? error : new Error(String(error));
|
|
18
67
|
}
|
|
68
|
+
|
|
69
|
+
function createHttpError(message: string, response?: AxiosResponse, payload?: unknown): HttpError {
|
|
70
|
+
const error = new Error(message) as HttpError;
|
|
71
|
+
error.status = response?.status;
|
|
72
|
+
error.payload = payload;
|
|
73
|
+
if (isRecord(payload)) {
|
|
74
|
+
const code = payload.code;
|
|
75
|
+
if (typeof code === "string" || typeof code === "number") error.code = code;
|
|
76
|
+
}
|
|
77
|
+
return error;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
81
|
+
return typeof value === "object" && value !== null;
|
|
82
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { http, normalizeHttpError } from "./http";
|
|
1
|
+
export { createHttpClient, http, normalizeHttpError, type HttpClientOptions, type HttpError } from "./http";
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
+
import { safeJsonParse } from "@/utils";
|
|
2
|
+
|
|
1
3
|
type StorageKind = "local" | "session";
|
|
2
4
|
|
|
5
|
+
interface StorageOptions {
|
|
6
|
+
namespace?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
function resolveStorage(kind: StorageKind): Storage {
|
|
4
10
|
return kind === "session" ? sessionStorage : localStorage;
|
|
5
11
|
}
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
function createKey(key: string, namespace?: string): string {
|
|
14
|
+
return namespace ? `${namespace}:${key}` : key;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createStorage(kind: StorageKind = "local", options: StorageOptions = {}) {
|
|
8
18
|
const storage = resolveStorage(kind);
|
|
19
|
+
const namespace = options.namespace;
|
|
9
20
|
|
|
10
21
|
return {
|
|
22
|
+
key(key: string): string {
|
|
23
|
+
return createKey(key, namespace);
|
|
24
|
+
},
|
|
11
25
|
get<T>(key: string): T | null {
|
|
12
|
-
const raw = storage.getItem(key);
|
|
26
|
+
const raw = storage.getItem(createKey(key, namespace));
|
|
13
27
|
if (raw === null) return null;
|
|
14
28
|
try {
|
|
15
29
|
return JSON.parse(raw) as T;
|
|
@@ -18,14 +32,55 @@ export function createStorage(kind: StorageKind = "local") {
|
|
|
18
32
|
throw new Error(`Storage value for ${key} is not valid JSON: ${message}`);
|
|
19
33
|
}
|
|
20
34
|
},
|
|
35
|
+
getOr<T>(key: string, fallback: T): T {
|
|
36
|
+
return safeJsonParse(storage.getItem(createKey(key, namespace)), fallback);
|
|
37
|
+
},
|
|
21
38
|
set<T>(key: string, value: T): void {
|
|
22
|
-
storage.setItem(key, JSON.stringify(value));
|
|
39
|
+
storage.setItem(createKey(key, namespace), JSON.stringify(value));
|
|
23
40
|
},
|
|
24
41
|
remove(key: string): void {
|
|
25
|
-
storage.removeItem(key);
|
|
42
|
+
storage.removeItem(createKey(key, namespace));
|
|
26
43
|
},
|
|
27
44
|
clear(): void {
|
|
28
45
|
storage.clear();
|
|
29
46
|
}
|
|
30
47
|
};
|
|
31
48
|
}
|
|
49
|
+
|
|
50
|
+
export function createCookieStorage(options: StorageOptions & { maxAgeSeconds?: number } = {}) {
|
|
51
|
+
const namespace = options.namespace;
|
|
52
|
+
const maxAgeSeconds = options.maxAgeSeconds;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
key(key: string): string {
|
|
56
|
+
return createKey(key, namespace);
|
|
57
|
+
},
|
|
58
|
+
get<T>(key: string): T | string | null {
|
|
59
|
+
const value = readCookie(createKey(key, namespace));
|
|
60
|
+
if (value === null) return null;
|
|
61
|
+
return safeJsonParse<T | string>(value, value);
|
|
62
|
+
},
|
|
63
|
+
set<T>(key: string, value: T): void {
|
|
64
|
+
const encodedKey = encodeURIComponent(createKey(key, namespace));
|
|
65
|
+
const encodedValue = encodeURIComponent(JSON.stringify(value));
|
|
66
|
+
const maxAge = typeof maxAgeSeconds === "number" ? `; max-age=${maxAgeSeconds}` : "";
|
|
67
|
+
document.cookie = `${encodedKey}=${encodedValue}${maxAge}; path=/`;
|
|
68
|
+
},
|
|
69
|
+
remove(key: string): void {
|
|
70
|
+
document.cookie = `${encodeURIComponent(createKey(key, namespace))}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
|
71
|
+
},
|
|
72
|
+
has(key: string): boolean {
|
|
73
|
+
return readCookie(createKey(key, namespace)) !== null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readCookie(key: string): string | null {
|
|
79
|
+
const encodedKey = encodeURIComponent(key);
|
|
80
|
+
const item = document.cookie
|
|
81
|
+
.split(";")
|
|
82
|
+
.map((part) => part.trim())
|
|
83
|
+
.find((part) => part.startsWith(`${encodedKey}=`));
|
|
84
|
+
if (!item) return null;
|
|
85
|
+
return decodeURIComponent(item.slice(encodedKey.length + 1));
|
|
86
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
@use "./tokens" as *;
|
|
2
|
+
@use "./reset";
|
|
3
|
+
@use "./utilities";
|
|
2
4
|
|
|
3
5
|
:root {
|
|
4
6
|
color-scheme: light;
|
|
@@ -10,12 +12,3 @@
|
|
|
10
12
|
-webkit-font-smoothing: antialiased;
|
|
11
13
|
-moz-osx-font-smoothing: grayscale;
|
|
12
14
|
}
|
|
13
|
-
|
|
14
|
-
* { box-sizing: border-box; }
|
|
15
|
-
|
|
16
|
-
html, body, #app {
|
|
17
|
-
min-height: 100%;
|
|
18
|
-
margin: 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
body { background: $color-surface-muted; }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@use "./tokens" as *;
|
|
2
|
+
|
|
3
|
+
* {
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
html,
|
|
8
|
+
body,
|
|
9
|
+
#app {
|
|
10
|
+
min-height: 100%;
|
|
11
|
+
margin: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
background: $color-surface-muted;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
button,
|
|
19
|
+
input,
|
|
20
|
+
textarea,
|
|
21
|
+
select {
|
|
22
|
+
font: inherit;
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@use "./tokens" as *;
|
|
2
|
+
|
|
3
|
+
.u-flex {
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.u-flex-column {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.u-flex-center {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.u-flex-between {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.u-text-truncate {
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
text-overflow: ellipsis;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.u-full-width {
|
|
31
|
+
width: 100%;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.u-surface {
|
|
35
|
+
background: $color-surface;
|
|
36
|
+
border: 1px solid $color-border;
|
|
37
|
+
border-radius: $radius-card;
|
|
38
|
+
}
|
|
@@ -9,3 +9,72 @@ export function isBlank(value: unknown): value is string {
|
|
|
9
9
|
export function pickDefined<T extends Record<string, unknown>>(value: T): Partial<T> {
|
|
10
10
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as Partial<T>;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export function safeJsonParse<T>(value: string | null, fallback: T): T {
|
|
14
|
+
if (value === null || value.trim() === "") return fallback;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(value) as T;
|
|
17
|
+
} catch {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function awaitTo<T>(promise: Promise<T>): Promise<[Error, null] | [null, T]> {
|
|
23
|
+
try {
|
|
24
|
+
return [null, await promise];
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return [normalizeError(error), null];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeError(error: unknown): Error {
|
|
31
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function debounce<T extends (...args: unknown[]) => void>(callback: T, delayMs: number): (...args: Parameters<T>) => void {
|
|
35
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
return (...args) => {
|
|
37
|
+
if (timer) clearTimeout(timer);
|
|
38
|
+
timer = setTimeout(() => callback(...args), delayMs);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function debounceAsync<T extends (...args: unknown[]) => Promise<unknown>>(
|
|
43
|
+
callback: T,
|
|
44
|
+
delayMs: number
|
|
45
|
+
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
|
|
46
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
return (...args) =>
|
|
48
|
+
new Promise((resolve, reject) => {
|
|
49
|
+
if (timer) clearTimeout(timer);
|
|
50
|
+
timer = setTimeout(() => {
|
|
51
|
+
callback(...args).then(
|
|
52
|
+
(value) => resolve(value as Awaited<ReturnType<T>>),
|
|
53
|
+
(error) => reject(error)
|
|
54
|
+
);
|
|
55
|
+
}, delayMs);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createLoadingTask<TArgs extends unknown[], TResult>(
|
|
60
|
+
task: (...args: TArgs) => Promise<TResult>
|
|
61
|
+
): {
|
|
62
|
+
readonly isLoading: boolean;
|
|
63
|
+
run: (...args: TArgs) => Promise<TResult | null>;
|
|
64
|
+
} {
|
|
65
|
+
let loading = false;
|
|
66
|
+
return {
|
|
67
|
+
get isLoading() {
|
|
68
|
+
return loading;
|
|
69
|
+
},
|
|
70
|
+
async run(...args) {
|
|
71
|
+
if (loading) return null;
|
|
72
|
+
loading = true;
|
|
73
|
+
try {
|
|
74
|
+
return await task(...args);
|
|
75
|
+
} finally {
|
|
76
|
+
loading = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
|
|
3
3
|
test("desktop smoke renders the scaffold shell", async ({ page }) => {
|
|
4
4
|
await page.goto("/");
|
|
5
5
|
|
|
6
|
-
await expect(page.getByRole("heading", { level: 1, name: "
|
|
6
|
+
await expect(page.getByRole("heading", { level: 1, name: "Project foundation is ready" })).toBeVisible();
|
|
7
7
|
await expect(page.getByRole("heading", { level: 2, name: "utils" })).toBeVisible();
|
|
8
8
|
await expect(page.getByRole("heading", { level: 2, name: "storage" })).toBeVisible();
|
|
9
9
|
await expect(page.getByRole("heading", { level: 2, name: "service" })).toBeVisible();
|
|
@@ -3,7 +3,7 @@ import { expect, test } from "@playwright/test";
|
|
|
3
3
|
test("mobile smoke keeps scaffold content in the viewport", async ({ page }) => {
|
|
4
4
|
await page.goto("/");
|
|
5
5
|
|
|
6
|
-
await expect(page.getByRole("heading", { level: 1, name: "
|
|
6
|
+
await expect(page.getByRole("heading", { level: 1, name: "Project foundation is ready" })).toBeVisible();
|
|
7
7
|
const documentOverflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth);
|
|
8
8
|
expect(documentOverflow).toBe(false);
|
|
9
9
|
});
|
|
@@ -4,10 +4,10 @@ import App from "../../src/App.vue";
|
|
|
4
4
|
import { renderPage } from "../render";
|
|
5
5
|
|
|
6
6
|
describe("scaffold app", () => {
|
|
7
|
-
it("renders the
|
|
7
|
+
it("renders the application baseline sections", () => {
|
|
8
8
|
renderPage(App);
|
|
9
9
|
|
|
10
|
-
expect(screen.getByRole("heading", { level: 1, name: "
|
|
10
|
+
expect(screen.getByRole("heading", { level: 1, name: "Project foundation is ready" })).toBeInTheDocument();
|
|
11
11
|
expect(screen.getByRole("heading", { level: 2, name: "utils" })).toBeInTheDocument();
|
|
12
12
|
expect(screen.getByRole("heading", { level: 2, name: "storage" })).toBeInTheDocument();
|
|
13
13
|
expect(screen.getByRole("heading", { level: 2, name: "service" })).toBeInTheDocument();
|