@everysk/apps-devkit 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/eslint/index.d.ts +20 -0
- package/dist/eslint/index.d.ts.map +1 -0
- package/dist/eslint/index.js +85 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/prettier/index.json +8 -0
- package/dist/vite/index.d.ts +30 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +70 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/plugins/envVarsLocation.d.ts +3 -0
- package/dist/vite/plugins/envVarsLocation.d.ts.map +1 -0
- package/dist/vite/plugins/envVarsLocation.js +10 -0
- package/dist/vite/plugins/envVarsLocation.js.map +1 -0
- package/dist/vite/plugins/serveDevAppConfig.d.ts +15 -0
- package/dist/vite/plugins/serveDevAppConfig.d.ts.map +1 -0
- package/dist/vite/plugins/serveDevAppConfig.js +47 -0
- package/dist/vite/plugins/serveDevAppConfig.js.map +1 -0
- package/dist/vite/serverProxy.d.ts +16 -0
- package/dist/vite/serverProxy.d.ts.map +1 -0
- package/dist/vite/serverProxy.js +81 -0
- package/dist/vite/serverProxy.js.map +1 -0
- package/dist/vitest/index.d.ts +33 -0
- package/dist/vitest/index.d.ts.map +1 -0
- package/dist/vitest/index.js +76 -0
- package/dist/vitest/index.js.map +1 -0
- package/package.json +90 -0
- package/src/eslint/index.ts +90 -0
- package/src/prettier/index.json +8 -0
- package/src/vite/index.ts +82 -0
- package/src/vite/plugins/envVarsLocation.ts +11 -0
- package/src/vite/plugins/serveDevAppConfig.ts +51 -0
- package/src/vite/serverProxy.ts +88 -0
- package/src/vitest/index.ts +81 -0
- package/tsconfig/app.json +35 -0
- package/tsconfig/node.json +25 -0
- package/tsconfig/vitest.json +10 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from "node:url";
|
|
2
|
+
import { defineConfig, mergeConfig } from "vitest/config";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
/**
|
|
5
|
+
* defineAppTestConfig
|
|
6
|
+
*
|
|
7
|
+
* The Vitest configuration every Everysk app shares:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { defineAppTestConfig } from "@everysk/apps-devkit/vitest";
|
|
11
|
+
* export default defineAppTestConfig();
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Standalone from the Vite config on purpose — the same split the template kept. The library
|
|
15
|
+
* build, the dev proxy and the React Compiler pass are irrelevant to tests; all they need is
|
|
16
|
+
* the React transform, the `@` alias and jsdom.
|
|
17
|
+
*
|
|
18
|
+
* Notable defaults:
|
|
19
|
+
*
|
|
20
|
+
* - `setupFiles` points at `@everysk/apps-sdk/test/setup`, which installs the jsdom gaps MUI
|
|
21
|
+
* needs (matchMedia, ResizeObserver). It seeds no page globals: an app injects configuration
|
|
22
|
+
* per test with `renderWithProviders(ui, { appId, appEnvironmentVar })`.
|
|
23
|
+
* - `testTimeout` is 30s, not Vitest's 5s. The template's default trips MUI transition-driven
|
|
24
|
+
* tests under `--coverage`, and every app had to raise it by hand.
|
|
25
|
+
* - Coverage thresholds at 80% on all four counters, which the deploy gate enforces. `branches`
|
|
26
|
+
* is listed explicitly: leaving it out does not inherit the others, it leaves that counter
|
|
27
|
+
* unenforced, and an untested `if` is exactly what a coverage floor is there to catch. Barrels
|
|
28
|
+
* are excluded at
|
|
29
|
+
* exactly one level deep. A recursive glob would also swallow every `index.tsx` and quietly
|
|
30
|
+
* drop real components out of the report — which is what happened while building the SDK.
|
|
31
|
+
*
|
|
32
|
+
* @param {ViteUserConfig} [overrides] - Merged over the preset with Vite's `mergeConfig`.
|
|
33
|
+
*/
|
|
34
|
+
export const defineAppTestConfig = (overrides = {}) => {
|
|
35
|
+
const preset = {
|
|
36
|
+
plugins: [react()],
|
|
37
|
+
resolve: {
|
|
38
|
+
alias: {
|
|
39
|
+
"@": fileURLToPath(new URL("./src", `file://${process.cwd()}/`)),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
test: {
|
|
43
|
+
globals: true,
|
|
44
|
+
environment: "jsdom",
|
|
45
|
+
setupFiles: ["@everysk/apps-sdk/test/setup"],
|
|
46
|
+
css: false,
|
|
47
|
+
clearMocks: true,
|
|
48
|
+
restoreMocks: true,
|
|
49
|
+
include: ["src/**/*.{test,spec}.{ts,tsx}"],
|
|
50
|
+
testTimeout: 30_000,
|
|
51
|
+
coverage: {
|
|
52
|
+
provider: "v8",
|
|
53
|
+
reportsDirectory: "./coverage",
|
|
54
|
+
reporter: ["text", "html"],
|
|
55
|
+
include: ["src/**/*.{ts,tsx}"],
|
|
56
|
+
exclude: [
|
|
57
|
+
"src/**/*.{test,spec}.{ts,tsx}",
|
|
58
|
+
"src/test/**",
|
|
59
|
+
"src/main.tsx",
|
|
60
|
+
"src/index.ts",
|
|
61
|
+
"src/*/index.ts",
|
|
62
|
+
"src/**/types.ts",
|
|
63
|
+
"src/types/**",
|
|
64
|
+
],
|
|
65
|
+
thresholds: {
|
|
66
|
+
lines: 80,
|
|
67
|
+
functions: 80,
|
|
68
|
+
statements: 80,
|
|
69
|
+
branches: 80,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
return defineConfig(mergeConfig(preset, overrides));
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vitest/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,WAAW,EAAuB,MAAM,eAAe,CAAC;AAE/E,OAAO,KAAK,MAAM,sBAAsB,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,YAA4B,EAAE,EAAE,EAAE;IAClE,MAAM,MAAM,GAAmB;QAC3B,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC;QAElB,OAAO,EAAE;YACL,KAAK,EAAE;gBACH,GAAG,EAAE,aAAa,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,UAAU,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;aACnE;SACJ;QAED,IAAI,EAAE;YACF,OAAO,EAAE,IAAI;YACb,WAAW,EAAE,OAAO;YACpB,UAAU,EAAE,CAAC,8BAA8B,CAAC;YAC5C,GAAG,EAAE,KAAK;YACV,UAAU,EAAE,IAAI;YAChB,YAAY,EAAE,IAAI;YAClB,OAAO,EAAE,CAAC,+BAA+B,CAAC;YAC1C,WAAW,EAAE,MAAM;YACnB,QAAQ,EAAE;gBACN,QAAQ,EAAE,IAAI;gBACd,gBAAgB,EAAE,YAAY;gBAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;gBAC1B,OAAO,EAAE,CAAC,mBAAmB,CAAC;gBAC9B,OAAO,EAAE;oBACL,+BAA+B;oBAC/B,aAAa;oBACb,cAAc;oBACd,cAAc;oBACd,gBAAgB;oBAChB,iBAAiB;oBACjB,cAAc;iBACjB;gBACD,UAAU,EAAE;oBACR,KAAK,EAAE,EAAE;oBACT,SAAS,EAAE,EAAE;oBACb,UAAU,EAAE,EAAE;oBACd,QAAQ,EAAE,EAAE;iBACf;aACJ;SACJ;KACJ,CAAC;IAEF,OAAO,YAAY,CAAC,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AACxD,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@everysk/apps-devkit",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Build-time presets for Everysk client applications: Vite, ESLint, Vitest, TypeScript and Prettier.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"everysk",
|
|
7
|
+
"vite",
|
|
8
|
+
"eslint",
|
|
9
|
+
"vitest",
|
|
10
|
+
"preset",
|
|
11
|
+
"config"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/everysk/apps-sdk.git",
|
|
16
|
+
"directory": "packages/apps-devkit"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/everysk/apps-sdk/tree/main/packages/apps-devkit#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/everysk/apps-sdk/issues"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"tsconfig",
|
|
28
|
+
"!src/**/*.test.ts"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
"./vite": {
|
|
32
|
+
"types": "./dist/vite/index.d.ts",
|
|
33
|
+
"import": "./dist/vite/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./eslint": {
|
|
36
|
+
"types": "./dist/eslint/index.d.ts",
|
|
37
|
+
"import": "./dist/eslint/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./vitest": {
|
|
40
|
+
"types": "./dist/vitest/index.d.ts",
|
|
41
|
+
"import": "./dist/vitest/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./prettier": "./dist/prettier/index.json",
|
|
44
|
+
"./tsconfig": "./tsconfig/app.json",
|
|
45
|
+
"./tsconfig/node": "./tsconfig/node.json",
|
|
46
|
+
"./tsconfig/vitest": "./tsconfig/vitest.json",
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json && node ./scripts/copyAssets.js",
|
|
51
|
+
"lint": "eslint .",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"test:coverage": "vitest run --coverage"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@babel/core": "^7.29.7",
|
|
58
|
+
"@eslint/js": "^9.39.4",
|
|
59
|
+
"@rolldown/plugin-babel": "^0.2.3",
|
|
60
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
61
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
62
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
63
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
64
|
+
"eslint": "^9.39.4",
|
|
65
|
+
"eslint-config-prettier": "^10.1.8",
|
|
66
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
67
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
68
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
69
|
+
"globals": "^17.6.0",
|
|
70
|
+
"jsdom": "^29.1.1",
|
|
71
|
+
"prettier": "^3.8.3",
|
|
72
|
+
"tailwindcss": "^4.3.0",
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"typescript-eslint": "^8.60.1",
|
|
75
|
+
"vite": "^8.0.16",
|
|
76
|
+
"vitest": "^4.1.8"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@types/babel__core": "^7.20.5",
|
|
80
|
+
"@types/node": "^25.9.1"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"sideEffects": false,
|
|
86
|
+
"engines": {
|
|
87
|
+
"node": ">=22",
|
|
88
|
+
"npm": ">=12"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import globals from "globals";
|
|
3
|
+
import reactHooks from "eslint-plugin-react-hooks";
|
|
4
|
+
import reactRefresh from "eslint-plugin-react-refresh";
|
|
5
|
+
import tseslint from "typescript-eslint";
|
|
6
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
7
|
+
|
|
8
|
+
// Barrel bans applied to every file: faster dev startup and reliable tree-shaking.
|
|
9
|
+
const noBarrelPaths = [
|
|
10
|
+
{
|
|
11
|
+
name: "@mui/material",
|
|
12
|
+
message:
|
|
13
|
+
"Avoid the barrel. Import from the deep path, e.g. `@mui/material/Button` or `@mui/material/styles`.",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "@mui/icons-material",
|
|
17
|
+
message: "Avoid the barrel. Import the specific icon, e.g. `@mui/icons-material/Search`.",
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Data-layer rule: pages and components reach data only through entity hooks.
|
|
22
|
+
const dataLayerMessage =
|
|
23
|
+
"Data access only via entity hooks (@everysk/apps-sdk/<entity> or @/hooks/...). Pages and components must not call axios or an api module directly.";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* everyskConfig
|
|
27
|
+
*
|
|
28
|
+
* The ESLint configuration every Everysk app shares, so an app's `eslint.config.js` is:
|
|
29
|
+
*
|
|
30
|
+
* ```js
|
|
31
|
+
* import { everyskConfig } from "@everysk/apps-devkit/eslint";
|
|
32
|
+
* export default everyskConfig();
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* It encodes the conventions that lint can actually check: no MUI barrel imports, and no
|
|
36
|
+
* direct data access from pages or components. The rules that lint cannot express — which
|
|
37
|
+
* subpath to import, which provider to wire — belong to the `everysk-apps` generator.
|
|
38
|
+
*
|
|
39
|
+
* @param {ReturnType<typeof defineConfig>} [extra] - Appended after the preset, so an app can
|
|
40
|
+
* add or override rules for its own files.
|
|
41
|
+
*/
|
|
42
|
+
export const everyskConfig = (extra: ReturnType<typeof defineConfig> = []) =>
|
|
43
|
+
defineConfig([
|
|
44
|
+
globalIgnores(["dist", "coverage"]),
|
|
45
|
+
{
|
|
46
|
+
files: ["**/*.{ts,tsx}"],
|
|
47
|
+
extends: [
|
|
48
|
+
js.configs.recommended,
|
|
49
|
+
tseslint.configs.recommended,
|
|
50
|
+
reactHooks.configs.flat["recommended-latest"],
|
|
51
|
+
reactRefresh.configs.vite,
|
|
52
|
+
],
|
|
53
|
+
languageOptions: {
|
|
54
|
+
ecmaVersion: 2020,
|
|
55
|
+
globals: globals.browser,
|
|
56
|
+
},
|
|
57
|
+
rules: {
|
|
58
|
+
"@typescript-eslint/no-restricted-imports": ["error", { paths: noBarrelPaths }],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
// A same-named rule REPLACES the base one for these files rather than merging, so
|
|
63
|
+
// the barrel bans are repeated here.
|
|
64
|
+
files: ["src/pages/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
|
|
65
|
+
rules: {
|
|
66
|
+
"@typescript-eslint/no-restricted-imports": [
|
|
67
|
+
"error",
|
|
68
|
+
{
|
|
69
|
+
paths: [
|
|
70
|
+
...noBarrelPaths,
|
|
71
|
+
{ name: "axios", message: dataLayerMessage },
|
|
72
|
+
{ name: "@/api", message: dataLayerMessage },
|
|
73
|
+
],
|
|
74
|
+
patterns: [{ group: ["@/api/*"], message: dataLayerMessage }],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
// Tests may reach the data layer directly, to mock an api module or build axios
|
|
81
|
+
// stubs. Only the barrel bans remain, and the Fast Refresh rule is relaxed for
|
|
82
|
+
// test utilities that export helpers.
|
|
83
|
+
files: ["src/**/*.{test,spec}.{ts,tsx}", "src/test/**/*.{ts,tsx}"],
|
|
84
|
+
rules: {
|
|
85
|
+
"@typescript-eslint/no-restricted-imports": ["error", { paths: noBarrelPaths }],
|
|
86
|
+
"react-refresh/only-export-components": "off",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
...extra,
|
|
90
|
+
]);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { defineConfig, loadEnv, mergeConfig, type UserConfig } from "vite";
|
|
4
|
+
|
|
5
|
+
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
6
|
+
import babel from "@rolldown/plugin-babel";
|
|
7
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
8
|
+
|
|
9
|
+
import { createDevServerProxy } from "./serverProxy.js";
|
|
10
|
+
import { envVarsLocationPlugin } from "./plugins/envVarsLocation.js";
|
|
11
|
+
import { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
|
|
12
|
+
|
|
13
|
+
export { createDevServerProxy } from "./serverProxy.js";
|
|
14
|
+
export { envVarsLocationPlugin } from "./plugins/envVarsLocation.js";
|
|
15
|
+
export { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* defineAppConfig
|
|
19
|
+
*
|
|
20
|
+
* The Vite configuration every Everysk app shares, so an app's own `vite.config.ts` is three
|
|
21
|
+
* lines instead of a copy that drifts:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { defineAppConfig } from "@everysk/apps-devkit/vite";
|
|
25
|
+
* export default defineAppConfig();
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* What it sets up:
|
|
29
|
+
*
|
|
30
|
+
* - `base: "./"` and `envDir` at `PROJECT_ROOT`, because an app is built from
|
|
31
|
+
* `apps/<name>/project/` but its `.env` lives at the repository root.
|
|
32
|
+
* - The `@` alias to the app's `src/`.
|
|
33
|
+
* - The dev server on `PORT` (default 5173, `strictPort`), with the `/api` proxy that
|
|
34
|
+
* authenticates against `EVERYSK_API_URL` using OAuth2 client credentials.
|
|
35
|
+
* - React with the compiler preset, Tailwind, and the two plugins that carry the app's
|
|
36
|
+
* `config.json` `env_vars` into the page — a meta tag in build, `/app-config.json` in dev.
|
|
37
|
+
*
|
|
38
|
+
* @param {UserConfig} [overrides] - Merged over the preset with Vite's own `mergeConfig`, so
|
|
39
|
+
* arrays (plugins, aliases) are concatenated rather than replaced.
|
|
40
|
+
*/
|
|
41
|
+
export const defineAppConfig = (overrides: UserConfig = {}) =>
|
|
42
|
+
defineConfig(({ mode, command }) => {
|
|
43
|
+
// The app is built from apps/<name>/project, but .env lives at the repo root; the
|
|
44
|
+
// devcontainer exports PROJECT_ROOT for exactly this.
|
|
45
|
+
const root = process.env.PROJECT_ROOT ?? process.cwd();
|
|
46
|
+
|
|
47
|
+
const env = loadEnv(mode, root, "");
|
|
48
|
+
Object.assign(process.env, env);
|
|
49
|
+
|
|
50
|
+
const preset: UserConfig = {
|
|
51
|
+
base: "./",
|
|
52
|
+
envDir: root,
|
|
53
|
+
|
|
54
|
+
resolve: {
|
|
55
|
+
alias: {
|
|
56
|
+
"@": fileURLToPath(new URL("./src", `file://${process.cwd()}/`)),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
build: {
|
|
61
|
+
outDir: "./dist",
|
|
62
|
+
emptyOutDir: true,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
server: {
|
|
66
|
+
host: true,
|
|
67
|
+
port: Number(process.env.PORT) || 5173,
|
|
68
|
+
strictPort: true,
|
|
69
|
+
proxy: command === "serve" ? createDevServerProxy() : undefined,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
plugins: [
|
|
73
|
+
react(),
|
|
74
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
75
|
+
tailwindcss(),
|
|
76
|
+
envVarsLocationPlugin(),
|
|
77
|
+
serveDevAppConfigPlugin(),
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return mergeConfig(preset, overrides);
|
|
82
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
export function envVarsLocationPlugin(): Plugin {
|
|
4
|
+
return {
|
|
5
|
+
name: "env_vars_location",
|
|
6
|
+
apply: "build",
|
|
7
|
+
transformIndexHtml() {
|
|
8
|
+
return [{ tag: "meta", attrs: { name: "app-config" }, injectTo: "head" }];
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import type { Plugin } from "vite";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* In production the platform injects `window.APP_CONFIG` from the app's `config.json` `env_vars`
|
|
8
|
+
* (via the `app-config` meta tag added by the build plugin). There is no platform in dev, so this
|
|
9
|
+
* plugin serves that same `env_vars` object at `/app-config.json`, read from the app's `config.json`
|
|
10
|
+
* one level up from the Vite root (apps/<name>/config.json, sibling of project/). `main.tsx` fetches
|
|
11
|
+
* it on bootstrap and merges it into `window.APP_CONFIG`, so dev mirrors production from a single
|
|
12
|
+
* source of truth — there is no separate dev config file to keep in sync.
|
|
13
|
+
*
|
|
14
|
+
* Because `config.json` lives outside the Vite root it is not watched by default, and `main.tsx`
|
|
15
|
+
* only reads it once at bootstrap. So we also add it to the watcher and trigger a full page reload
|
|
16
|
+
* on change — a full reload re-runs the bootstrap fetch, picking up edits to `env_vars` live.
|
|
17
|
+
*/
|
|
18
|
+
export function serveDevAppConfigPlugin(): Plugin {
|
|
19
|
+
return {
|
|
20
|
+
name: "serve-dev-app-config",
|
|
21
|
+
apply: "serve",
|
|
22
|
+
configureServer(server) {
|
|
23
|
+
// apps/<name>/config.json — one level up from the Vite root (the app's project/ dir).
|
|
24
|
+
const configPath = path.resolve(process.cwd(), "..", "config.json");
|
|
25
|
+
|
|
26
|
+
server.middlewares.use(async (req, res, next) => {
|
|
27
|
+
if (req.url !== "/app-config.json") return next();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
31
|
+
const envVars = (JSON.parse(content).env_vars ?? {}) as Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
34
|
+
res.statusCode = 200;
|
|
35
|
+
res.end(JSON.stringify(envVars));
|
|
36
|
+
} catch {
|
|
37
|
+
res.statusCode = 404;
|
|
38
|
+
res.end();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// config.json is outside the Vite root, so watch it explicitly and full-reload on change.
|
|
43
|
+
server.watcher.add(configPath);
|
|
44
|
+
server.watcher.on("change", (file) => {
|
|
45
|
+
if (path.resolve(file) === configPath) {
|
|
46
|
+
server.hot.send({ type: "full-reload" });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ProxyOptions } from "vite";
|
|
2
|
+
|
|
3
|
+
// Refresh the OAuth2 token this many ms before it actually expires, so an in-flight
|
|
4
|
+
// request never races a just-expired token.
|
|
5
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* createDevServerProxy — Vite dev proxy for `/api`, authenticated with OAuth2 client credentials.
|
|
9
|
+
*
|
|
10
|
+
* Forwards `/api/*` to `EVERYSK_API_URL` (stripping the `/api` prefix) and injects an
|
|
11
|
+
* `Authorization: Bearer <access_token>` header. The token is obtained from
|
|
12
|
+
* `${EVERYSK_API_URL}/oauth2/token` with the `client_credentials` grant, cached in memory, and
|
|
13
|
+
* refreshed in the background shortly before it expires. It is fetched eagerly at startup so a
|
|
14
|
+
* token is ready before the first API call.
|
|
15
|
+
*
|
|
16
|
+
* @returns {Record<string, string | ProxyOptions>} The Vite `server.proxy` config for `/api`.
|
|
17
|
+
* @throws {Error} If `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or `EVERYSK_API_CLIENT_SECRET`
|
|
18
|
+
* is missing.
|
|
19
|
+
*/
|
|
20
|
+
export function createDevServerProxy(): Record<string, string | ProxyOptions> {
|
|
21
|
+
const EVERYSK_API_URL = process.env.EVERYSK_API_URL;
|
|
22
|
+
if (!EVERYSK_API_URL) throw new Error("EVERYSK_API_URL must be set.");
|
|
23
|
+
|
|
24
|
+
const clientId = process.env.EVERYSK_API_CLIENT_ID;
|
|
25
|
+
const clientSecret = process.env.EVERYSK_API_CLIENT_SECRET;
|
|
26
|
+
if (!clientId || !clientSecret) {
|
|
27
|
+
throw new Error("EVERYSK_API_CLIENT_ID and EVERYSK_API_CLIENT_SECRET must be set (OAuth2 client credentials).");
|
|
28
|
+
}
|
|
29
|
+
// Captured into an object so the narrowed `string` type survives inside the async closure below.
|
|
30
|
+
const credentials = { clientId, clientSecret };
|
|
31
|
+
|
|
32
|
+
const tokenUrl = `${EVERYSK_API_URL.replace(/\/$/, "")}/oauth2/token`;
|
|
33
|
+
let accessToken: string | null = null;
|
|
34
|
+
let refreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* refreshToken — fetch a fresh access token and schedule the next refresh before it expires.
|
|
38
|
+
*
|
|
39
|
+
* @returns {Promise<void>} Resolves once `accessToken` is updated.
|
|
40
|
+
* @throws {Error} If the token request fails or returns no `access_token`.
|
|
41
|
+
*/
|
|
42
|
+
async function refreshToken(): Promise<void> {
|
|
43
|
+
const res = await fetch(tokenUrl, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
46
|
+
body: new URLSearchParams({
|
|
47
|
+
grant_type: "client_credentials",
|
|
48
|
+
client_id: credentials.clientId,
|
|
49
|
+
client_secret: credentials.clientSecret,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) throw new Error(`OAuth2 token request failed: ${res.status} ${res.statusText}`);
|
|
53
|
+
|
|
54
|
+
const data = (await res.json()) as { access_token?: string; expires_in?: number };
|
|
55
|
+
if (!data.access_token) throw new Error("OAuth2 token response had no access_token.");
|
|
56
|
+
accessToken = data.access_token;
|
|
57
|
+
|
|
58
|
+
const ttlMs = (data.expires_in ?? 3600) * 1000;
|
|
59
|
+
if (refreshTimer) clearTimeout(refreshTimer);
|
|
60
|
+
refreshTimer = setTimeout(() => {
|
|
61
|
+
void refreshToken().catch((error) => console.error("[dev proxy] OAuth2 token refresh failed:", error));
|
|
62
|
+
}, Math.max(ttlMs - REFRESH_SKEW_MS, 30_000));
|
|
63
|
+
refreshTimer.unref?.();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Warm the token before the first request; a failure is logged, not fatal (the request will 401).
|
|
67
|
+
void refreshToken().catch((error) => console.error("[dev proxy] initial OAuth2 token fetch failed:", error));
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"/api": {
|
|
71
|
+
target: EVERYSK_API_URL,
|
|
72
|
+
changeOrigin: true,
|
|
73
|
+
xfwd: true,
|
|
74
|
+
rewrite: (p) => p.replace(/^\/api(?=\/|$)/, ""),
|
|
75
|
+
|
|
76
|
+
configure: (proxy) => {
|
|
77
|
+
proxy.on("proxyReq", (proxyReq) => {
|
|
78
|
+
proxyReq.removeHeader("origin");
|
|
79
|
+
proxyReq.removeHeader("referer");
|
|
80
|
+
|
|
81
|
+
if (accessToken) {
|
|
82
|
+
proxyReq.setHeader("Authorization", `Bearer ${accessToken}`);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { defineConfig, mergeConfig, type ViteUserConfig } from "vitest/config";
|
|
4
|
+
|
|
5
|
+
import react from "@vitejs/plugin-react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* defineAppTestConfig
|
|
9
|
+
*
|
|
10
|
+
* The Vitest configuration every Everysk app shares:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { defineAppTestConfig } from "@everysk/apps-devkit/vitest";
|
|
14
|
+
* export default defineAppTestConfig();
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Standalone from the Vite config on purpose — the same split the template kept. The library
|
|
18
|
+
* build, the dev proxy and the React Compiler pass are irrelevant to tests; all they need is
|
|
19
|
+
* the React transform, the `@` alias and jsdom.
|
|
20
|
+
*
|
|
21
|
+
* Notable defaults:
|
|
22
|
+
*
|
|
23
|
+
* - `setupFiles` points at `@everysk/apps-sdk/test/setup`, which installs the jsdom gaps MUI
|
|
24
|
+
* needs (matchMedia, ResizeObserver). It seeds no page globals: an app injects configuration
|
|
25
|
+
* per test with `renderWithProviders(ui, { appId, appEnvironmentVar })`.
|
|
26
|
+
* - `testTimeout` is 30s, not Vitest's 5s. The template's default trips MUI transition-driven
|
|
27
|
+
* tests under `--coverage`, and every app had to raise it by hand.
|
|
28
|
+
* - Coverage thresholds at 80% on all four counters, which the deploy gate enforces. `branches`
|
|
29
|
+
* is listed explicitly: leaving it out does not inherit the others, it leaves that counter
|
|
30
|
+
* unenforced, and an untested `if` is exactly what a coverage floor is there to catch. Barrels
|
|
31
|
+
* are excluded at
|
|
32
|
+
* exactly one level deep. A recursive glob would also swallow every `index.tsx` and quietly
|
|
33
|
+
* drop real components out of the report — which is what happened while building the SDK.
|
|
34
|
+
*
|
|
35
|
+
* @param {ViteUserConfig} [overrides] - Merged over the preset with Vite's `mergeConfig`.
|
|
36
|
+
*/
|
|
37
|
+
export const defineAppTestConfig = (overrides: ViteUserConfig = {}) => {
|
|
38
|
+
const preset: ViteUserConfig = {
|
|
39
|
+
plugins: [react()],
|
|
40
|
+
|
|
41
|
+
resolve: {
|
|
42
|
+
alias: {
|
|
43
|
+
"@": fileURLToPath(new URL("./src", `file://${process.cwd()}/`)),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
test: {
|
|
48
|
+
globals: true,
|
|
49
|
+
environment: "jsdom",
|
|
50
|
+
setupFiles: ["@everysk/apps-sdk/test/setup"],
|
|
51
|
+
css: false,
|
|
52
|
+
clearMocks: true,
|
|
53
|
+
restoreMocks: true,
|
|
54
|
+
include: ["src/**/*.{test,spec}.{ts,tsx}"],
|
|
55
|
+
testTimeout: 30_000,
|
|
56
|
+
coverage: {
|
|
57
|
+
provider: "v8",
|
|
58
|
+
reportsDirectory: "./coverage",
|
|
59
|
+
reporter: ["text", "html"],
|
|
60
|
+
include: ["src/**/*.{ts,tsx}"],
|
|
61
|
+
exclude: [
|
|
62
|
+
"src/**/*.{test,spec}.{ts,tsx}",
|
|
63
|
+
"src/test/**",
|
|
64
|
+
"src/main.tsx",
|
|
65
|
+
"src/index.ts",
|
|
66
|
+
"src/*/index.ts",
|
|
67
|
+
"src/**/types.ts",
|
|
68
|
+
"src/types/**",
|
|
69
|
+
],
|
|
70
|
+
thresholds: {
|
|
71
|
+
lines: 80,
|
|
72
|
+
functions: 80,
|
|
73
|
+
statements: 80,
|
|
74
|
+
branches: 80,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return defineConfig(mergeConfig(preset, overrides));
|
|
81
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"Base TypeScript options for an Everysk app. Extend it from the app's tsconfig.app.json:",
|
|
4
|
+
" { \"extends\": \"@everysk/apps-devkit/tsconfig\", \"include\": [\"src\"] }",
|
|
5
|
+
"",
|
|
6
|
+
"`paths` is deliberately NOT set here. TypeScript resolves the paths of a config relative",
|
|
7
|
+
"to the file that DECLARES them, so an `@/*` mapping written in this package would point",
|
|
8
|
+
"at this package's own src, not the app's. Each app declares its own:",
|
|
9
|
+
" \"compilerOptions\": { \"paths\": { \"@/*\": [\"./src/*\"] } }"
|
|
10
|
+
],
|
|
11
|
+
"compilerOptions": {
|
|
12
|
+
"target": "ES2022",
|
|
13
|
+
"useDefineForClassFields": true,
|
|
14
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
15
|
+
"module": "ESNext",
|
|
16
|
+
"types": ["vite/client"],
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
|
|
19
|
+
/* Bundler mode */
|
|
20
|
+
"moduleResolution": "bundler",
|
|
21
|
+
"allowImportingTsExtensions": true,
|
|
22
|
+
"verbatimModuleSyntax": true,
|
|
23
|
+
"moduleDetection": "force",
|
|
24
|
+
"noEmit": true,
|
|
25
|
+
"jsx": "react-jsx",
|
|
26
|
+
|
|
27
|
+
/* Linting */
|
|
28
|
+
"strict": true,
|
|
29
|
+
"noUnusedLocals": true,
|
|
30
|
+
"noUnusedParameters": true,
|
|
31
|
+
"erasableSyntaxOnly": true,
|
|
32
|
+
"noFallthroughCasesInSwitch": true,
|
|
33
|
+
"noUncheckedSideEffectImports": true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "For an app's build-time files (vite.config.ts, vitest.config.ts). Extend with \"include\".",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"For an app's test files. Extend it from the app's tsconfig.vitest.json, which must also",
|
|
4
|
+
"re-declare `paths` — see the note in app.json."
|
|
5
|
+
],
|
|
6
|
+
"extends": "./app.json",
|
|
7
|
+
"compilerOptions": {
|
|
8
|
+
"types": ["vitest/globals", "@testing-library/jest-dom", "node", "vite/client"]
|
|
9
|
+
}
|
|
10
|
+
}
|