@everysk/apps-devkit 0.2.0 → 0.3.0-beta-20261016180126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/dist/eslint/index.d.ts +13 -4
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +53 -48
- package/dist/eslint/index.js.map +1 -1
- package/dist/vite/apiToken.d.ts +27 -0
- package/dist/vite/apiToken.d.ts.map +1 -0
- package/dist/vite/apiToken.js +86 -0
- package/dist/vite/apiToken.js.map +1 -0
- package/dist/vite/index.d.ts +4 -0
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +9 -1
- package/dist/vite/index.js.map +1 -1
- package/dist/vite/plugins/envVarsLocation.js.map +1 -1
- package/dist/vite/plugins/serveDevAppConfig.js.map +1 -1
- package/dist/vite/plugins/serveDevFiles.d.ts +41 -0
- package/dist/vite/plugins/serveDevFiles.d.ts.map +1 -0
- package/dist/vite/plugins/serveDevFiles.js +153 -0
- package/dist/vite/plugins/serveDevFiles.js.map +1 -0
- package/dist/vite/serverProxy.d.ts +7 -7
- package/dist/vite/serverProxy.d.ts.map +1 -1
- package/dist/vite/serverProxy.js +9 -56
- package/dist/vite/serverProxy.js.map +1 -1
- package/package.json +1 -3
- package/src/eslint/index.ts +23 -5
- package/src/vite/apiToken.ts +107 -0
- package/src/vite/index.ts +10 -1
- package/src/vite/plugins/envVarsLocation.ts +7 -7
- package/src/vite/plugins/serveDevAppConfig.ts +28 -28
- package/src/vite/plugins/serveDevFiles.ts +216 -0
- package/src/vite/serverProxy.ts +29 -77
- package/tsconfig/vitest.json +1 -1
package/README.md
CHANGED
|
@@ -56,9 +56,14 @@ export default defineAppTestConfig();
|
|
|
56
56
|
// eslint.config.js
|
|
57
57
|
import { everyskConfig } from "@everysk/apps-devkit/eslint";
|
|
58
58
|
|
|
59
|
-
export default everyskConfig();
|
|
59
|
+
export default everyskConfig({ tsconfigRootDir: import.meta.dirname });
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
`tsconfigRootDir` is the app's own directory, which only its config can name. In a repository of
|
|
63
|
+
several apps, linting them in one run (an editor does, and so does `eslint` from the root) fails
|
|
64
|
+
with "No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present" without it.
|
|
65
|
+
`everyskConfig(extra)` still works, and so does `everyskConfig({ tsconfigRootDir, extra })`.
|
|
66
|
+
|
|
62
67
|
```jsonc
|
|
63
68
|
// tsconfig.app.json
|
|
64
69
|
{
|
|
@@ -75,7 +80,7 @@ export default everyskConfig();
|
|
|
75
80
|
|
|
76
81
|
| Subpath | |
|
|
77
82
|
|---|---|
|
|
78
|
-
| `/vite` | `defineAppConfig`, plus the dev proxy and the two config plugins
|
|
83
|
+
| `/vite` | `defineAppConfig`, plus on their own: the dev proxy, the `/file/` route plugin, the token source they share, and the two config plugins |
|
|
79
84
|
| `/vitest` | `defineAppTestConfig` |
|
|
80
85
|
| `/eslint` | `everyskConfig` |
|
|
81
86
|
| `/prettier` | the Prettier config as JSON |
|
|
@@ -88,6 +93,11 @@ but `.env` lives at the repository root), the `@` alias, React with the compiler
|
|
|
88
93
|
the `/api` dev proxy that authenticates with OAuth2 client credentials and refreshes the token
|
|
89
94
|
before it expires.
|
|
90
95
|
|
|
96
|
+
The dev server and `vite preview` also answer the platform's `/file/<hash>` route, so a file's
|
|
97
|
+
own `url` shows it there as on the platform. That route reads only the user's session, which the
|
|
98
|
+
dev server does not have, so the file is found through the API with the proxy's OAuth2 token,
|
|
99
|
+
by its `url`, and its content decoded and sent inline with its type.
|
|
100
|
+
|
|
91
101
|
It also serves the app's `config.json` `env_vars` at `/app-config.json` in dev, read **one level
|
|
92
102
|
above** the Vite root. That is why an app is `<name>/{config.json, project/}`: flatten it and the
|
|
93
103
|
app boots with no configuration, silently.
|
package/dist/eslint/index.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { defineConfig } from "eslint/config";
|
|
2
|
+
export type EveryskConfigOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* The app's directory, `import.meta.dirname` in its `eslint.config.js`. An editor lints a
|
|
5
|
+
* repository of several apps in one ESLint process, where typescript-eslint refuses to pick
|
|
6
|
+
* among their tsconfigs; only the app's own config knows which directory is its root.
|
|
7
|
+
*/
|
|
8
|
+
tsconfigRootDir?: string;
|
|
9
|
+
/** Appended after the preset, so an app can add or override rules for its own files. */
|
|
10
|
+
extra?: ReturnType<typeof defineConfig>;
|
|
11
|
+
};
|
|
2
12
|
/**
|
|
3
13
|
* everyskConfig
|
|
4
14
|
*
|
|
@@ -6,15 +16,14 @@ import { defineConfig } from "eslint/config";
|
|
|
6
16
|
*
|
|
7
17
|
* ```js
|
|
8
18
|
* import { everyskConfig } from "@everysk/apps-devkit/eslint";
|
|
9
|
-
* export default everyskConfig();
|
|
19
|
+
* export default everyskConfig({ tsconfigRootDir: import.meta.dirname });
|
|
10
20
|
* ```
|
|
11
21
|
*
|
|
12
22
|
* It encodes the conventions that lint can actually check: no MUI barrel imports, and no
|
|
13
23
|
* direct data access from pages or components. The rules that lint cannot express — which
|
|
14
24
|
* subpath to import, which provider to wire — belong to the `everysk-apps` generator.
|
|
15
25
|
*
|
|
16
|
-
*
|
|
17
|
-
* add or override rules for its own files.
|
|
26
|
+
* Takes the options, or the app's own blocks alone, as it did before there were options.
|
|
18
27
|
*/
|
|
19
|
-
export declare const everyskConfig: (
|
|
28
|
+
export declare const everyskConfig: (optionsOrExtra?: EveryskConfigOptions | ReturnType<typeof defineConfig>) => import("eslint/config").ConfigObject[];
|
|
20
29
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAiB,MAAM,eAAe,CAAC;AAmB5D
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAiB,MAAM,eAAe,CAAC;AAmB5D,MAAM,MAAM,oBAAoB,GAAG;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wFAAwF;IACxF,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;CAC3C,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,GACtB,iBAAgB,oBAAoB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAM,2CAuD9E,CAAC"}
|
package/dist/eslint/index.js
CHANGED
|
@@ -24,62 +24,67 @@ const dataLayerMessage = "Data access only via entity hooks (@everysk/apps-sdk/<
|
|
|
24
24
|
*
|
|
25
25
|
* ```js
|
|
26
26
|
* import { everyskConfig } from "@everysk/apps-devkit/eslint";
|
|
27
|
-
* export default everyskConfig();
|
|
27
|
+
* export default everyskConfig({ tsconfigRootDir: import.meta.dirname });
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
30
|
* It encodes the conventions that lint can actually check: no MUI barrel imports, and no
|
|
31
31
|
* direct data access from pages or components. The rules that lint cannot express — which
|
|
32
32
|
* subpath to import, which provider to wire — belong to the `everysk-apps` generator.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
* add or override rules for its own files.
|
|
34
|
+
* Takes the options, or the app's own blocks alone, as it did before there were options.
|
|
36
35
|
*/
|
|
37
|
-
export const everyskConfig = (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
rules: {
|
|
52
|
-
"@typescript-eslint/no-restricted-imports": ["error", { paths: noBarrelPaths }],
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
// A same-named rule REPLACES the base one for these files rather than merging, so
|
|
57
|
-
// the barrel bans are repeated here.
|
|
58
|
-
files: ["src/pages/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
|
|
59
|
-
rules: {
|
|
60
|
-
"@typescript-eslint/no-restricted-imports": [
|
|
61
|
-
"error",
|
|
62
|
-
{
|
|
63
|
-
paths: [
|
|
64
|
-
...noBarrelPaths,
|
|
65
|
-
{ name: "axios", message: dataLayerMessage },
|
|
66
|
-
{ name: "@/api", message: dataLayerMessage },
|
|
67
|
-
],
|
|
68
|
-
patterns: [{ group: ["@/api/*"], message: dataLayerMessage }],
|
|
69
|
-
},
|
|
36
|
+
export const everyskConfig = (optionsOrExtra = {}) => {
|
|
37
|
+
const { tsconfigRootDir, extra = [] } = Array.isArray(optionsOrExtra)
|
|
38
|
+
? { extra: optionsOrExtra }
|
|
39
|
+
: optionsOrExtra;
|
|
40
|
+
return defineConfig([
|
|
41
|
+
globalIgnores(["dist", "coverage"]),
|
|
42
|
+
{
|
|
43
|
+
files: ["**/*.{ts,tsx}"],
|
|
44
|
+
extends: [
|
|
45
|
+
js.configs.recommended,
|
|
46
|
+
tseslint.configs.recommended,
|
|
47
|
+
reactHooks.configs.flat["recommended-latest"],
|
|
48
|
+
reactRefresh.configs.vite,
|
|
70
49
|
],
|
|
50
|
+
languageOptions: {
|
|
51
|
+
ecmaVersion: 2020,
|
|
52
|
+
globals: globals.browser,
|
|
53
|
+
...(tsconfigRootDir ? { parserOptions: { tsconfigRootDir } } : {}),
|
|
54
|
+
},
|
|
55
|
+
rules: {
|
|
56
|
+
"@typescript-eslint/no-restricted-imports": ["error", { paths: noBarrelPaths }],
|
|
57
|
+
},
|
|
71
58
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
{
|
|
60
|
+
// A same-named rule REPLACES the base one for these files rather than merging, so
|
|
61
|
+
// the barrel bans are repeated here.
|
|
62
|
+
files: ["src/pages/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
|
|
63
|
+
rules: {
|
|
64
|
+
"@typescript-eslint/no-restricted-imports": [
|
|
65
|
+
"error",
|
|
66
|
+
{
|
|
67
|
+
paths: [
|
|
68
|
+
...noBarrelPaths,
|
|
69
|
+
{ name: "axios", message: dataLayerMessage },
|
|
70
|
+
{ name: "@/api", message: dataLayerMessage },
|
|
71
|
+
],
|
|
72
|
+
patterns: [{ group: ["@/api/*"], message: dataLayerMessage }],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
81
76
|
},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
{
|
|
78
|
+
// Tests may reach the data layer directly, to mock an api module or build axios
|
|
79
|
+
// stubs. Only the barrel bans remain, and the Fast Refresh rule is relaxed for
|
|
80
|
+
// test utilities that export helpers.
|
|
81
|
+
files: ["src/**/*.{test,spec}.{ts,tsx}", "src/test/**/*.{ts,tsx}"],
|
|
82
|
+
rules: {
|
|
83
|
+
"@typescript-eslint/no-restricted-imports": ["error", { paths: noBarrelPaths }],
|
|
84
|
+
"react-refresh/only-export-components": "off",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
...extra,
|
|
88
|
+
]);
|
|
89
|
+
};
|
|
85
90
|
//# sourceMappingURL=index.js.map
|
package/dist/eslint/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,UAAU,MAAM,2BAA2B,CAAC;AACnD,OAAO,YAAY,MAAM,6BAA6B,CAAC;AACvD,OAAO,QAAQ,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE5D,mFAAmF;AACnF,MAAM,aAAa,GAAG;IAClB;QACI,IAAI,EAAE,eAAe;QACrB,OAAO,EACH,qGAAqG;KAC5G;IACD;QACI,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,gFAAgF;KAC5F;CACJ,CAAC;AAEF,8EAA8E;AAC9E,MAAM,gBAAgB,GAClB,oJAAoJ,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,UAAU,MAAM,2BAA2B,CAAC;AACnD,OAAO,YAAY,MAAM,6BAA6B,CAAC;AACvD,OAAO,QAAQ,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE5D,mFAAmF;AACnF,MAAM,aAAa,GAAG;IAClB;QACI,IAAI,EAAE,eAAe;QACrB,OAAO,EACH,qGAAqG;KAC5G;IACD;QACI,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,gFAAgF;KAC5F;CACJ,CAAC;AAEF,8EAA8E;AAC9E,MAAM,gBAAgB,GAClB,oJAAoJ,CAAC;AAazJ;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CACzB,iBAAyE,EAAE,EAC7E,EAAE;IACA,MAAM,EAAE,eAAe,EAAE,KAAK,GAAG,EAAE,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC;QACjE,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE;QAC3B,CAAC,CAAC,cAAc,CAAC;IAErB,OAAO,YAAY,CAAC;QAChB,aAAa,CAAC,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACnC;YACI,KAAK,EAAE,CAAC,eAAe,CAAC;YACxB,OAAO,EAAE;gBACL,EAAE,CAAC,OAAO,CAAC,WAAW;gBACtB,QAAQ,CAAC,OAAO,CAAC,WAAW;gBAC5B,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC;gBAC7C,YAAY,CAAC,OAAO,CAAC,IAAI;aAC5B;YACD,eAAe,EAAE;gBACb,WAAW,EAAE,IAAI;gBACjB,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,EAAE,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACrE;YACD,KAAK,EAAE;gBACH,0CAA0C,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;aAClF;SACJ;QACD;YACI,kFAAkF;YAClF,qCAAqC;YACrC,KAAK,EAAE,CAAC,yBAAyB,EAAE,8BAA8B,CAAC;YAClE,KAAK,EAAE;gBACH,0CAA0C,EAAE;oBACxC,OAAO;oBACP;wBACI,KAAK,EAAE;4BACH,GAAG,aAAa;4BAChB,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE;4BAC5C,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE;yBAC/C;wBACD,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;qBAChE;iBACJ;aACJ;SACJ;QACD;YACI,gFAAgF;YAChF,+EAA+E;YAC/E,sCAAsC;YACtC,KAAK,EAAE,CAAC,+BAA+B,EAAE,wBAAwB,CAAC;YAClE,KAAK,EAAE;gBACH,0CAA0C,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC;gBAC/E,sCAAsC,EAAE,KAAK;aAChD;SACJ;QACD,GAAG,KAAK;KACX,CAAC,CAAC;AACP,CAAC,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The API the dev server talks to, and the token it talks to it with. */
|
|
2
|
+
export type ApiTokenSource = {
|
|
3
|
+
/** `EVERYSK_API_URL`, without a trailing slash, so a path can be appended as is. */
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
/** The current access token; `null` until one arrives, and once it has expired. */
|
|
6
|
+
token: () => string | null;
|
|
7
|
+
/** Why there is no token: the last failed request, until one succeeds. */
|
|
8
|
+
lastError: () => Error | null;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* createApiTokenSource
|
|
12
|
+
*
|
|
13
|
+
* An OAuth2 access token for `EVERYSK_API_URL`, from the `client_credentials` grant at
|
|
14
|
+
* `${EVERYSK_API_URL}/oauth2/token`, cached in memory and renewed in the background a minute
|
|
15
|
+
* before it expires. It is fetched at once, so a token is ready before the first request. A
|
|
16
|
+
* failed request is logged and tried again, waiting 5 seconds and doubling up to a minute, so a
|
|
17
|
+
* VPN or API that comes back later does not need a restart of the dev server; until then the
|
|
18
|
+
* `/api` proxy sends no token (the API answers 401) and the `/file/` route says why, from
|
|
19
|
+
* `lastError`. `defineAppConfig` creates one source and hands it to both, so the dev server asks
|
|
20
|
+
* for one token, not one per use.
|
|
21
|
+
*
|
|
22
|
+
* @returns {ApiTokenSource} The API URL, the current token, and the last failure.
|
|
23
|
+
* @throws {Error} If `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or `EVERYSK_API_CLIENT_SECRET`
|
|
24
|
+
* is missing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createApiTokenSource(): ApiTokenSource;
|
|
27
|
+
//# sourceMappingURL=apiToken.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiToken.d.ts","sourceRoot":"","sources":["../../src/vite/apiToken.ts"],"names":[],"mappings":"AAOA,0EAA0E;AAC1E,MAAM,MAAM,cAAc,GAAG;IACzB,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,KAAK,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC3B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,KAAK,GAAG,IAAI,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,cAAc,CAyErD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** How long before a token expires it is renewed, so a request in flight never meets an expired one. */
|
|
2
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
3
|
+
/** The first wait after a failed token request; it doubles on each failure, up to the maximum. */
|
|
4
|
+
const FIRST_RETRY_MS = 5_000;
|
|
5
|
+
const MAX_RETRY_MS = 60_000;
|
|
6
|
+
/**
|
|
7
|
+
* createApiTokenSource
|
|
8
|
+
*
|
|
9
|
+
* An OAuth2 access token for `EVERYSK_API_URL`, from the `client_credentials` grant at
|
|
10
|
+
* `${EVERYSK_API_URL}/oauth2/token`, cached in memory and renewed in the background a minute
|
|
11
|
+
* before it expires. It is fetched at once, so a token is ready before the first request. A
|
|
12
|
+
* failed request is logged and tried again, waiting 5 seconds and doubling up to a minute, so a
|
|
13
|
+
* VPN or API that comes back later does not need a restart of the dev server; until then the
|
|
14
|
+
* `/api` proxy sends no token (the API answers 401) and the `/file/` route says why, from
|
|
15
|
+
* `lastError`. `defineAppConfig` creates one source and hands it to both, so the dev server asks
|
|
16
|
+
* for one token, not one per use.
|
|
17
|
+
*
|
|
18
|
+
* @returns {ApiTokenSource} The API URL, the current token, and the last failure.
|
|
19
|
+
* @throws {Error} If `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or `EVERYSK_API_CLIENT_SECRET`
|
|
20
|
+
* is missing.
|
|
21
|
+
*/
|
|
22
|
+
export function createApiTokenSource() {
|
|
23
|
+
const apiUrl = process.env.EVERYSK_API_URL?.replace(/\/$/, "");
|
|
24
|
+
if (!apiUrl)
|
|
25
|
+
throw new Error("EVERYSK_API_URL must be set.");
|
|
26
|
+
const clientId = process.env.EVERYSK_API_CLIENT_ID;
|
|
27
|
+
const clientSecret = process.env.EVERYSK_API_CLIENT_SECRET;
|
|
28
|
+
if (!clientId || !clientSecret) {
|
|
29
|
+
throw new Error("EVERYSK_API_CLIENT_ID and EVERYSK_API_CLIENT_SECRET must be set (OAuth2 client credentials).");
|
|
30
|
+
}
|
|
31
|
+
const credentials = { clientId, clientSecret };
|
|
32
|
+
const tokenUrl = `${apiUrl}/oauth2/token`;
|
|
33
|
+
let accessToken = null;
|
|
34
|
+
let expiresAt = 0;
|
|
35
|
+
let lastError = null;
|
|
36
|
+
let retryMs = FIRST_RETRY_MS;
|
|
37
|
+
let timer = null;
|
|
38
|
+
const schedule = (delayMs) => {
|
|
39
|
+
if (timer)
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
timer = setTimeout(() => void refreshToken(), delayMs);
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
};
|
|
44
|
+
/** Asks for a token, then schedules the next request: a renewal on success, a retry on failure. */
|
|
45
|
+
async function requestToken() {
|
|
46
|
+
const res = await fetch(tokenUrl, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
49
|
+
body: new URLSearchParams({
|
|
50
|
+
grant_type: "client_credentials",
|
|
51
|
+
client_id: credentials.clientId,
|
|
52
|
+
client_secret: credentials.clientSecret,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
throw new Error(`OAuth2 token request failed: ${res.status} ${res.statusText}`);
|
|
57
|
+
const data = (await res.json());
|
|
58
|
+
if (!data.access_token)
|
|
59
|
+
throw new Error("OAuth2 token response had no access_token.");
|
|
60
|
+
const ttlMs = (data.expires_in ?? 3600) * 1000;
|
|
61
|
+
accessToken = data.access_token;
|
|
62
|
+
expiresAt = Date.now() + ttlMs;
|
|
63
|
+
return ttlMs;
|
|
64
|
+
}
|
|
65
|
+
async function refreshToken() {
|
|
66
|
+
try {
|
|
67
|
+
const ttlMs = await requestToken();
|
|
68
|
+
lastError = null;
|
|
69
|
+
retryMs = FIRST_RETRY_MS;
|
|
70
|
+
schedule(Math.max(ttlMs - REFRESH_SKEW_MS, 30_000));
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
74
|
+
console.error(`[dev server] OAuth2 token request failed; trying again in ${retryMs / 1000}s:`, lastError);
|
|
75
|
+
schedule(retryMs);
|
|
76
|
+
retryMs = Math.min(retryMs * 2, MAX_RETRY_MS);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
void refreshToken();
|
|
80
|
+
return {
|
|
81
|
+
apiUrl,
|
|
82
|
+
token: () => (accessToken && Date.now() < expiresAt ? accessToken : null),
|
|
83
|
+
lastError: () => lastError,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=apiToken.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apiToken.js","sourceRoot":"","sources":["../../src/vite/apiToken.ts"],"names":[],"mappings":"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"}
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type UserConfig } from "vite";
|
|
2
|
+
export { createApiTokenSource, type ApiTokenSource } from "./apiToken.js";
|
|
2
3
|
export { createDevServerProxy } from "./serverProxy.js";
|
|
3
4
|
export { envVarsLocationPlugin } from "./plugins/envVarsLocation.js";
|
|
4
5
|
export { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
|
|
6
|
+
export { serveDevFilesPlugin } from "./plugins/serveDevFiles.js";
|
|
5
7
|
/**
|
|
6
8
|
* defineAppConfig
|
|
7
9
|
*
|
|
@@ -22,6 +24,8 @@ export { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
|
|
|
22
24
|
* authenticates against `EVERYSK_API_URL` using OAuth2 client credentials.
|
|
23
25
|
* - React with the compiler preset, Tailwind, and the two plugins that carry the app's
|
|
24
26
|
* `config.json` `env_vars` into the page — a meta tag in build, `/app-config.json` in dev.
|
|
27
|
+
* - The platform's `/file/<hash>` route, on the dev server and `vite preview`, so a file's own
|
|
28
|
+
* `url` shows it there too. It shares the `/api` proxy's OAuth2 token.
|
|
25
29
|
*
|
|
26
30
|
* @param {UserConfig} [overrides] - Merged over the preset with Vite's own `mergeConfig`, so
|
|
27
31
|
* arrays (plugins, aliases) are concatenated rather than replaced.
|
package/dist/vite/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAY3E,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,eAAe,GAAI,YAAW,UAAe,sCA4CpD,CAAC"}
|
package/dist/vite/index.js
CHANGED
|
@@ -3,12 +3,16 @@ import { defineConfig, loadEnv, mergeConfig } from "vite";
|
|
|
3
3
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
4
4
|
import babel from "@rolldown/plugin-babel";
|
|
5
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
6
|
+
import { createApiTokenSource } from "./apiToken.js";
|
|
6
7
|
import { createDevServerProxy } from "./serverProxy.js";
|
|
7
8
|
import { envVarsLocationPlugin } from "./plugins/envVarsLocation.js";
|
|
8
9
|
import { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
|
|
10
|
+
import { serveDevFilesPlugin } from "./plugins/serveDevFiles.js";
|
|
11
|
+
export { createApiTokenSource } from "./apiToken.js";
|
|
9
12
|
export { createDevServerProxy } from "./serverProxy.js";
|
|
10
13
|
export { envVarsLocationPlugin } from "./plugins/envVarsLocation.js";
|
|
11
14
|
export { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
|
|
15
|
+
export { serveDevFilesPlugin } from "./plugins/serveDevFiles.js";
|
|
12
16
|
/**
|
|
13
17
|
* defineAppConfig
|
|
14
18
|
*
|
|
@@ -29,6 +33,8 @@ export { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
|
|
|
29
33
|
* authenticates against `EVERYSK_API_URL` using OAuth2 client credentials.
|
|
30
34
|
* - React with the compiler preset, Tailwind, and the two plugins that carry the app's
|
|
31
35
|
* `config.json` `env_vars` into the page — a meta tag in build, `/app-config.json` in dev.
|
|
36
|
+
* - The platform's `/file/<hash>` route, on the dev server and `vite preview`, so a file's own
|
|
37
|
+
* `url` shows it there too. It shares the `/api` proxy's OAuth2 token.
|
|
32
38
|
*
|
|
33
39
|
* @param {UserConfig} [overrides] - Merged over the preset with Vite's own `mergeConfig`, so
|
|
34
40
|
* arrays (plugins, aliases) are concatenated rather than replaced.
|
|
@@ -39,6 +45,7 @@ export const defineAppConfig = (overrides = {}) => defineConfig(({ mode, command
|
|
|
39
45
|
const root = process.env.PROJECT_ROOT ?? process.cwd();
|
|
40
46
|
const env = loadEnv(mode, root, "");
|
|
41
47
|
Object.assign(process.env, env);
|
|
48
|
+
const tokens = command === "serve" ? createApiTokenSource() : undefined;
|
|
42
49
|
const preset = {
|
|
43
50
|
base: "./",
|
|
44
51
|
envDir: root,
|
|
@@ -55,7 +62,7 @@ export const defineAppConfig = (overrides = {}) => defineConfig(({ mode, command
|
|
|
55
62
|
host: true,
|
|
56
63
|
port: Number(process.env.PORT) || 5173,
|
|
57
64
|
strictPort: true,
|
|
58
|
-
proxy:
|
|
65
|
+
proxy: tokens ? createDevServerProxy(tokens) : undefined,
|
|
59
66
|
},
|
|
60
67
|
plugins: [
|
|
61
68
|
react(),
|
|
@@ -63,6 +70,7 @@ export const defineAppConfig = (overrides = {}) => defineConfig(({ mode, command
|
|
|
63
70
|
tailwindcss(),
|
|
64
71
|
envVarsLocationPlugin(),
|
|
65
72
|
serveDevAppConfigPlugin(),
|
|
73
|
+
serveDevFilesPlugin(tokens),
|
|
66
74
|
],
|
|
67
75
|
};
|
|
68
76
|
return mergeConfig(preset, overrides);
|
package/dist/vite/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAmB,MAAM,MAAM,CAAC;AAE3E,OAAO,KAAK,EAAE,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,MAAM,wBAAwB,CAAC;AAC3C,OAAO,WAAW,MAAM,mBAAmB,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAmB,MAAM,MAAM,CAAC;AAE3E,OAAO,KAAK,EAAE,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,KAAK,MAAM,wBAAwB,CAAC;AAC3C,OAAO,WAAW,MAAM,mBAAmB,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE,OAAO,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,YAAwB,EAAE,EAAE,EAAE,CAC1D,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;IAC/B,kFAAkF;IAClF,sDAAsD;IACtD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAEvD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAEhC,MAAM,MAAM,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAExE,MAAM,MAAM,GAAe;QACvB,IAAI,EAAE,IAAI;QACV,MAAM,EAAE,IAAI;QAEZ,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,KAAK,EAAE;YACH,MAAM,EAAE,QAAQ;YAChB,WAAW,EAAE,IAAI;SACpB;QAED,MAAM,EAAE;YACJ,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI;YACtC,UAAU,EAAE,IAAI;YAChB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;SAC3D;QAED,OAAO,EAAE;YACL,KAAK,EAAE;YACP,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,mBAAmB,EAAE,CAAC,EAAE,CAAC;YAC3C,WAAW,EAAE;YACb,qBAAqB,EAAE;YACvB,uBAAuB,EAAE;YACzB,mBAAmB,CAAC,MAAM,CAAC;SAC9B;KACJ,CAAC;IAEF,OAAO,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAC1C,CAAC,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"envVarsLocation.js","sourceRoot":"","sources":["../../../src/vite/plugins/envVarsLocation.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB;
|
|
1
|
+
{"version":3,"file":"envVarsLocation.js","sourceRoot":"","sources":["../../../src/vite/plugins/envVarsLocation.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB;IACjC,OAAO;QACH,IAAI,EAAE,mBAAmB;QACzB,KAAK,EAAE,OAAO;QACd,kBAAkB;YACd,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC9E,CAAC;KACJ,CAAC;AACN,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serveDevAppConfig.js","sourceRoot":"","sources":["../../../src/vite/plugins/serveDevAppConfig.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIlC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB;
|
|
1
|
+
{"version":3,"file":"serveDevAppConfig.js","sourceRoot":"","sources":["../../../src/vite/plugins/serveDevAppConfig.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIlC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,uBAAuB;IACnC,OAAO;QACH,IAAI,EAAE,sBAAsB;QAC5B,KAAK,EAAE,OAAO;QACd,eAAe,CAAC,MAAM;YAClB,sFAAsF;YACtF,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC;YAEpE,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;gBAC5C,IAAI,GAAG,CAAC,GAAG,KAAK,kBAAkB;oBAAE,OAAO,IAAI,EAAE,CAAC;gBAElD,IAAI,CAAC;oBACD,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;oBACvD,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,QAAQ,IAAI,EAAE,CAA4B,CAAC;oBAEhF,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,iCAAiC,CAAC,CAAC;oBACjE,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;oBACrB,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;gBACrC,CAAC;gBAAC,MAAM,CAAC;oBACL,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;oBACrB,GAAG,CAAC,GAAG,EAAE,CAAC;gBACd,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,0FAA0F;YAC1F,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;gBACjC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,UAAU,EAAE,CAAC;oBACpC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;gBAC7C,CAAC;YACL,CAAC,CAAC,CAAC;QACP,CAAC;KACJ,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Plugin } from "vite";
|
|
3
|
+
import { type ApiTokenSource } from "../apiToken.js";
|
|
4
|
+
/** What `createDevFileRoute` needs: the API and its token, a `fetch` and a log to replace in tests. */
|
|
5
|
+
type DevFileRouteParams = ApiTokenSource & {
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
log?: (message: string) => void;
|
|
8
|
+
};
|
|
9
|
+
type RouteRequest = Pick<IncomingMessage, "url" | "method">;
|
|
10
|
+
type RouteResponse = Pick<ServerResponse, "statusCode" | "setHeader" | "end">;
|
|
11
|
+
/**
|
|
12
|
+
* createDevFileRoute
|
|
13
|
+
*
|
|
14
|
+
* The dev server's stand-in for the platform's `/file/<hash>` route. On the platform a file's
|
|
15
|
+
* `url` is that route, served with the user's session and shown inline; the dev server has no
|
|
16
|
+
* session, so the file is found through the API with the dev proxy's OAuth2 token instead, by
|
|
17
|
+
* its stored `url`, and its base64 content is decoded here and sent with its own type. So an app
|
|
18
|
+
* shows a file through its `url` wherever it runs.
|
|
19
|
+
*
|
|
20
|
+
* Read with the OAuth2 client's permissions, not a user's, as everything through the dev proxy is.
|
|
21
|
+
* Every path under `/file/` is answered here, never handed on: Vite would answer it with the
|
|
22
|
+
* app's `index.html` and a 200. A file that cannot be served gets a status that says why, and a
|
|
23
|
+
* line in the terminal, since an `<img>` or `<iframe>` never shows the body. Nothing escapes the
|
|
24
|
+
* middleware: Connect ignores its promise, and a rejection would end the dev server.
|
|
25
|
+
*
|
|
26
|
+
* @param params - The API, its token and last failure, and a `fetch` and a log to replace in tests.
|
|
27
|
+
* @returns A Connect middleware that answers `/file/` and passes everything else on.
|
|
28
|
+
*/
|
|
29
|
+
export declare const createDevFileRoute: ({ apiUrl, token, lastError, fetch: fetchApi, log, }: DevFileRouteParams) => (req: RouteRequest, res: RouteResponse, next: () => void) => Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* serveDevFilesPlugin
|
|
32
|
+
*
|
|
33
|
+
* Mounts `createDevFileRoute` on the dev server and on `vite preview`, neither of which has the
|
|
34
|
+
* platform's `/file/` route, logging through Vite's own logger.
|
|
35
|
+
*
|
|
36
|
+
* @param {ApiTokenSource} [tokens] - The token to send; created when the server starts if
|
|
37
|
+
* omitted. `defineAppConfig` passes the one it shares with the `/api` proxy.
|
|
38
|
+
*/
|
|
39
|
+
export declare function serveDevFilesPlugin(tokens?: ApiTokenSource): Plugin;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=serveDevFiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serveDevFiles.d.ts","sourceRoot":"","sources":["../../../src/vite/plugins/serveDevFiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAW3E,uGAAuG;AACvG,KAAK,kBAAkB,GAAG,cAAc,GAAG;IACvC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAIF,KAAK,YAAY,GAAG,IAAI,CAAC,eAAe,EAAE,KAAK,GAAG,QAAQ,CAAC,CAAC;AAC5D,KAAK,aAAa,GAAG,IAAI,CAAC,cAAc,EAAE,YAAY,GAAG,WAAW,GAAG,KAAK,CAAC,CAAC;AAgG9E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAC1B,qDAME,kBAAkB,MACd,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,MAAM,IAAI,kBA6C7D,CAAC;AAEN;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,MAAM,CAanE"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createApiTokenSource } from "../apiToken.js";
|
|
2
|
+
/** Every path under the platform's file route, claimed whole so none falls through to `index.html`. */
|
|
3
|
+
const FILE_ROUTE = /^\/file\//;
|
|
4
|
+
/**
|
|
5
|
+
* A file's url as the platform serves it: the hex of a version-4 UUID, as everysk/client's
|
|
6
|
+
* `FileRequestHandler` matches it (`RE_FILE_URL`).
|
|
7
|
+
*/
|
|
8
|
+
const FILE_URL = /^\/file\/([0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15})\/?(?:\?.*)?$/;
|
|
9
|
+
/** Why a file could not be served, with the status to answer it with. */
|
|
10
|
+
class FileRouteError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(status, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The error and, when Node gives one, its cause, which is where fetch keeps ECONNREFUSED or DNS. */
|
|
18
|
+
const describeError = (error) => {
|
|
19
|
+
if (!(error instanceof Error))
|
|
20
|
+
return String(error);
|
|
21
|
+
return error.cause instanceof Error
|
|
22
|
+
? `${error.message}: ${error.cause.message}`
|
|
23
|
+
: error.message;
|
|
24
|
+
};
|
|
25
|
+
/** The API's own `message` from a refusal, or the start of whatever came back instead. */
|
|
26
|
+
const refusalOf = async (response) => {
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(text).message ?? text.slice(0, 200);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return text.slice(0, 200);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The file whose stored url is `/<hash>`, read with its content. A file stores its url as
|
|
37
|
+
* `/<hash>` and the API adds `/file` only when it returns it, and the file list accepts a `url`
|
|
38
|
+
* filter without a workspace (everysk-lib 1.13.4 `sdk/entities/file`, everysk/client
|
|
39
|
+
* `File.validate_query`).
|
|
40
|
+
*/
|
|
41
|
+
const findFile = async (fetchApi, apiUrl, accessToken, hash) => {
|
|
42
|
+
const query = JSON.stringify({ filters: [["url", `/${hash}`]], page_size: 1 });
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await fetchApi(`${apiUrl}/files?query=${encodeURIComponent(query)}`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new FileRouteError(502, `The API could not be reached: ${describeError(error)}`);
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const refusal = await refusalOf(response);
|
|
54
|
+
throw new FileRouteError(502, `The API refused the file: ${response.status} ${response.statusText}: ${refusal}`);
|
|
55
|
+
}
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = (await response.json());
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new FileRouteError(502, `The API answered with ${response.headers.get("content-type")}, not JSON.`);
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(body.files)) {
|
|
64
|
+
throw new FileRouteError(502, `The file list came back without files: ${JSON.stringify(body).slice(0, 200)}`);
|
|
65
|
+
}
|
|
66
|
+
return body.files[0];
|
|
67
|
+
};
|
|
68
|
+
/** The content of a found file, refusing what would be served as an empty or corrupt file. */
|
|
69
|
+
const contentOf = (file, hash) => {
|
|
70
|
+
if (typeof file.data !== "string") {
|
|
71
|
+
throw new FileRouteError(502, `The API returned ${file.name} (/file/${hash}) without its content.`);
|
|
72
|
+
}
|
|
73
|
+
if (file.data.startsWith("data:")) {
|
|
74
|
+
throw new FileRouteError(422, `${file.name} was stored as a data URL, so its bytes are not the file: upload it again, read with fileToBase64.`);
|
|
75
|
+
}
|
|
76
|
+
return Buffer.from(file.data, "base64");
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* createDevFileRoute
|
|
80
|
+
*
|
|
81
|
+
* The dev server's stand-in for the platform's `/file/<hash>` route. On the platform a file's
|
|
82
|
+
* `url` is that route, served with the user's session and shown inline; the dev server has no
|
|
83
|
+
* session, so the file is found through the API with the dev proxy's OAuth2 token instead, by
|
|
84
|
+
* its stored `url`, and its base64 content is decoded here and sent with its own type. So an app
|
|
85
|
+
* shows a file through its `url` wherever it runs.
|
|
86
|
+
*
|
|
87
|
+
* Read with the OAuth2 client's permissions, not a user's, as everything through the dev proxy is.
|
|
88
|
+
* Every path under `/file/` is answered here, never handed on: Vite would answer it with the
|
|
89
|
+
* app's `index.html` and a 200. A file that cannot be served gets a status that says why, and a
|
|
90
|
+
* line in the terminal, since an `<img>` or `<iframe>` never shows the body. Nothing escapes the
|
|
91
|
+
* middleware: Connect ignores its promise, and a rejection would end the dev server.
|
|
92
|
+
*
|
|
93
|
+
* @param params - The API, its token and last failure, and a `fetch` and a log to replace in tests.
|
|
94
|
+
* @returns A Connect middleware that answers `/file/` and passes everything else on.
|
|
95
|
+
*/
|
|
96
|
+
export const createDevFileRoute = ({ apiUrl, token, lastError, fetch: fetchApi = fetch, log = console.warn, }) => async (req, res, next) => {
|
|
97
|
+
const url = req.url ?? "";
|
|
98
|
+
if (!FILE_ROUTE.test(url))
|
|
99
|
+
return next();
|
|
100
|
+
const fail = (status, message) => {
|
|
101
|
+
log(`[dev server] ${url}: ${message}`);
|
|
102
|
+
res.statusCode = status;
|
|
103
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
104
|
+
res.end(message);
|
|
105
|
+
};
|
|
106
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
107
|
+
return fail(405, "A file is only read here.");
|
|
108
|
+
const hash = FILE_URL.exec(url)?.[1];
|
|
109
|
+
if (!hash)
|
|
110
|
+
return fail(400, "Not a file url: /file/ and the 32-character hex hash of a file.");
|
|
111
|
+
const accessToken = token();
|
|
112
|
+
if (!accessToken) {
|
|
113
|
+
return fail(503, `No OAuth2 token for EVERYSK_API_URL: ${lastError()?.message ?? "not there yet"}`);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const file = await findFile(fetchApi, apiUrl, accessToken, hash);
|
|
117
|
+
if (!file) {
|
|
118
|
+
return fail(404, `No file has the url /file/${hash}, as the OAuth2 client EVERYSK_API_CLIENT_ID sees.`);
|
|
119
|
+
}
|
|
120
|
+
const content = contentOf(file, hash);
|
|
121
|
+
const disposition = `inline; filename*=UTF-8''${encodeURIComponent(file.name)}`;
|
|
122
|
+
res.setHeader("Content-Type", file.content_type || "application/octet-stream");
|
|
123
|
+
res.setHeader("Content-Length", content.length);
|
|
124
|
+
res.setHeader("Content-Disposition", disposition);
|
|
125
|
+
res.statusCode = 200;
|
|
126
|
+
res.end(req.method === "HEAD" ? undefined : content);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
fail(error instanceof FileRouteError ? error.status : 502, describeError(error));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* serveDevFilesPlugin
|
|
134
|
+
*
|
|
135
|
+
* Mounts `createDevFileRoute` on the dev server and on `vite preview`, neither of which has the
|
|
136
|
+
* platform's `/file/` route, logging through Vite's own logger.
|
|
137
|
+
*
|
|
138
|
+
* @param {ApiTokenSource} [tokens] - The token to send; created when the server starts if
|
|
139
|
+
* omitted. `defineAppConfig` passes the one it shares with the `/api` proxy.
|
|
140
|
+
*/
|
|
141
|
+
export function serveDevFilesPlugin(tokens) {
|
|
142
|
+
const route = (log) => createDevFileRoute({ ...(tokens ?? createApiTokenSource()), log });
|
|
143
|
+
return {
|
|
144
|
+
name: "serve-dev-files",
|
|
145
|
+
configureServer(server) {
|
|
146
|
+
server.middlewares.use(route((message) => server.config.logger.warn(message)));
|
|
147
|
+
},
|
|
148
|
+
configurePreviewServer(server) {
|
|
149
|
+
server.middlewares.use(route((message) => server.config.logger.warn(message)));
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=serveDevFiles.js.map
|