@trokky/studio 2.0.6 → 3.0.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/dist/index.html
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
</style>
|
|
46
46
|
|
|
47
47
|
<title>Trokky Studio</title>
|
|
48
|
-
<script type="module" crossorigin src="/assets/index-
|
|
48
|
+
<script type="module" crossorigin src="/assets/index-D0f8OCEI.js"></script>
|
|
49
49
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CbXtAM7H.js">
|
|
50
50
|
<link rel="modulepreload" crossorigin href="/assets/router-Dpg4KYTB.js">
|
|
51
51
|
<link rel="modulepreload" crossorigin href="/assets/vendor-igJJvL5X.js">
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio as an Express router.
|
|
3
|
+
*
|
|
4
|
+
* Studio owns its own mounting. A site does:
|
|
5
|
+
*
|
|
6
|
+
* import { studioRouter } from '@trokky/studio/express'
|
|
7
|
+
* app.use('/studio', studioRouter({ apiPath: '/api' }))
|
|
8
|
+
*
|
|
9
|
+
* The router serves the built SPA (HTML, hashed assets, and the HTML again
|
|
10
|
+
* for every client-side route) and injects the small bootstrap config the
|
|
11
|
+
* SPA needs before it can talk to the API: where the API is, what path
|
|
12
|
+
* Studio is mounted on, and optional branding and i18n defaults for the
|
|
13
|
+
* login screen. Everything else (structure, schemas, media settings, the
|
|
14
|
+
* branding stored in settings) Studio fetches from the API once it runs.
|
|
15
|
+
*
|
|
16
|
+
* The mount path is read from `req.baseUrl` on every request, so the same
|
|
17
|
+
* router works wherever the site mounts it and behind any prefix a proxy
|
|
18
|
+
* adds. It is a plain middleware function underneath, so it mounts on
|
|
19
|
+
* Express 4 and Express 5 alike; no path-pattern syntax is used.
|
|
20
|
+
*/
|
|
21
|
+
import { Router } from 'express';
|
|
22
|
+
export interface StudioRouterOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Path the Trokky API is mounted on, as seen by the browser.
|
|
25
|
+
* Default: '/api'.
|
|
26
|
+
*/
|
|
27
|
+
apiPath?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Full origin + API path the browser should call, e.g.
|
|
30
|
+
* 'https://cms.example.com/api'. When omitted it is derived from the
|
|
31
|
+
* incoming request (protocol + Host header + apiPath), which is right for
|
|
32
|
+
* the usual same-origin deployment. Set it when the server sits behind a
|
|
33
|
+
* proxy that rewrites the Host header.
|
|
34
|
+
*/
|
|
35
|
+
backendUrl?: string;
|
|
36
|
+
/** Branding shown before the API has answered (login screen title, theme). */
|
|
37
|
+
branding?: {
|
|
38
|
+
title?: string;
|
|
39
|
+
logo?: string;
|
|
40
|
+
theme?: 'light' | 'dark' | 'system';
|
|
41
|
+
};
|
|
42
|
+
/** Locale defaults for the UI. */
|
|
43
|
+
i18n?: {
|
|
44
|
+
defaultLocale?: string;
|
|
45
|
+
supportedLocales?: string[];
|
|
46
|
+
fallbackLocale?: string;
|
|
47
|
+
detectBrowserLanguage?: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** Custom field plugins registered at build time. */
|
|
50
|
+
customFields?: unknown[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build an Express router that serves Trokky Studio.
|
|
54
|
+
*/
|
|
55
|
+
export declare function studioRouter(options?: StudioRouterOptions): Router;
|
|
56
|
+
export { getStudioHTML, getStudioAsset } from './assets.js';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio as an Express router.
|
|
3
|
+
*
|
|
4
|
+
* Studio owns its own mounting. A site does:
|
|
5
|
+
*
|
|
6
|
+
* import { studioRouter } from '@trokky/studio/express'
|
|
7
|
+
* app.use('/studio', studioRouter({ apiPath: '/api' }))
|
|
8
|
+
*
|
|
9
|
+
* The router serves the built SPA (HTML, hashed assets, and the HTML again
|
|
10
|
+
* for every client-side route) and injects the small bootstrap config the
|
|
11
|
+
* SPA needs before it can talk to the API: where the API is, what path
|
|
12
|
+
* Studio is mounted on, and optional branding and i18n defaults for the
|
|
13
|
+
* login screen. Everything else (structure, schemas, media settings, the
|
|
14
|
+
* branding stored in settings) Studio fetches from the API once it runs.
|
|
15
|
+
*
|
|
16
|
+
* The mount path is read from `req.baseUrl` on every request, so the same
|
|
17
|
+
* router works wherever the site mounts it and behind any prefix a proxy
|
|
18
|
+
* adds. It is a plain middleware function underneath, so it mounts on
|
|
19
|
+
* Express 4 and Express 5 alike; no path-pattern syntax is used.
|
|
20
|
+
*/
|
|
21
|
+
import { Router } from 'express';
|
|
22
|
+
import { getStudioHTML, getStudioAsset } from './assets.js';
|
|
23
|
+
/** How long browsers may cache Studio's hashed assets. */
|
|
24
|
+
const ASSET_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
|
25
|
+
function trimTrailingSlash(path) {
|
|
26
|
+
return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build an Express router that serves Trokky Studio.
|
|
30
|
+
*/
|
|
31
|
+
export function studioRouter(options = {}) {
|
|
32
|
+
const apiPath = trimTrailingSlash(options.apiPath ?? '/api');
|
|
33
|
+
const router = Router();
|
|
34
|
+
const sendHtml = (req, res) => {
|
|
35
|
+
// req.baseUrl is the path this router was mounted on, including any
|
|
36
|
+
// prefix an outer app.use() added. That is exactly what the SPA needs
|
|
37
|
+
// for its own routing and for asset URLs.
|
|
38
|
+
const basePath = trimTrailingSlash(req.baseUrl || '');
|
|
39
|
+
const backendUrl = options.backendUrl ?? `${req.protocol}://${req.get('host')}${apiPath}`;
|
|
40
|
+
const html = getStudioHTML({
|
|
41
|
+
mode: 'production',
|
|
42
|
+
apiBasePath: apiPath,
|
|
43
|
+
basePath,
|
|
44
|
+
backendUrl,
|
|
45
|
+
schemas: [],
|
|
46
|
+
branding: options.branding,
|
|
47
|
+
customFields: options.customFields,
|
|
48
|
+
...(options.i18n ? { i18n: options.i18n } : {}),
|
|
49
|
+
}, basePath);
|
|
50
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
51
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
52
|
+
res.send(html);
|
|
53
|
+
};
|
|
54
|
+
router.use((req, res, next) => {
|
|
55
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
56
|
+
next();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// req.path is relative to the mount point.
|
|
60
|
+
if (req.path.startsWith('/assets/')) {
|
|
61
|
+
const asset = getStudioAsset(req.path.slice('/assets/'.length));
|
|
62
|
+
if (!asset) {
|
|
63
|
+
res.status(404).send('Not found');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
res.setHeader('Content-Type', asset.contentType);
|
|
67
|
+
// Vite emits content-hashed filenames, so they can be cached forever.
|
|
68
|
+
res.setHeader('Cache-Control', `public, max-age=${ASSET_MAX_AGE_SECONDS}, immutable`);
|
|
69
|
+
res.send(asset.content);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// The root and every client-side route get the same document.
|
|
73
|
+
sendHtml(req, res);
|
|
74
|
+
});
|
|
75
|
+
return router;
|
|
76
|
+
}
|
|
77
|
+
export { getStudioHTML, getStudioAsset } from './assets.js';
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trokky/studio",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Trokky Studio
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Trokky Studio: the admin UI, mounted by the site as an Express router",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite --port 5173",
|
|
8
|
-
"dev:server": "tsc src/index.ts src/server/assets.ts src/utils/logger.ts --outDir dist --module esnext --target es2020 --skipLibCheck --moduleResolution node --allowSyntheticDefaultImports --esModuleInterop --declaration --watch",
|
|
8
|
+
"dev:server": "tsc src/index.ts src/server/assets.ts src/server/express.ts src/utils/logger.ts --outDir dist --module esnext --target es2020 --skipLibCheck --moduleResolution node --allowSyntheticDefaultImports --esModuleInterop --declaration --watch",
|
|
9
9
|
"build": "vite build && npm run build:server",
|
|
10
10
|
"build:single": "vite build --config vite.single.config.ts",
|
|
11
|
-
"build:server": "tsc src/index.ts src/server/assets.ts src/utils/logger.ts --outDir dist --module esnext --target es2020 --skipLibCheck --moduleResolution node --allowSyntheticDefaultImports --esModuleInterop --declaration",
|
|
11
|
+
"build:server": "tsc src/index.ts src/server/assets.ts src/server/express.ts src/utils/logger.ts --outDir dist --module esnext --target es2020 --skipLibCheck --moduleResolution node --allowSyntheticDefaultImports --esModuleInterop --declaration",
|
|
12
12
|
"preview": "vite preview",
|
|
13
13
|
"type-check": "tsc --noEmit",
|
|
14
14
|
"lint": "eslint src --ext ts,tsx",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@tiptap/pm": "^3.31.3",
|
|
32
32
|
"@tiptap/react": "^3.31.3",
|
|
33
33
|
"@tiptap/starter-kit": "^3.31.3",
|
|
34
|
-
"@trokky/trokky": "^
|
|
34
|
+
"@trokky/trokky": "^3.0.0",
|
|
35
35
|
"clsx": "^2.0.0",
|
|
36
36
|
"date-fns": "^2.29.0",
|
|
37
37
|
"express": "^4.18.0",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"jsdom": "^29.0.1",
|
|
62
62
|
"postcss": "^8.4.24",
|
|
63
63
|
"rollup": "^4.63.1",
|
|
64
|
+
"supertest": "^7.2.2",
|
|
64
65
|
"tailwindcss": "^3.4.0",
|
|
65
66
|
"typescript": "^5.0.0",
|
|
66
67
|
"vite": "^8.2.2",
|
|
@@ -72,13 +73,13 @@
|
|
|
72
73
|
"import": "./dist/index.js",
|
|
73
74
|
"types": "./dist/index.d.ts"
|
|
74
75
|
},
|
|
76
|
+
"./express": {
|
|
77
|
+
"import": "./dist/server/express.js",
|
|
78
|
+
"types": "./dist/server/express.d.ts"
|
|
79
|
+
},
|
|
75
80
|
"./standalone": {
|
|
76
81
|
"import": "./dist/index.js",
|
|
77
82
|
"types": "./dist/index.d.ts"
|
|
78
|
-
},
|
|
79
|
-
"./dist/server/assets.js": {
|
|
80
|
-
"import": "./dist/server/assets.js",
|
|
81
|
-
"types": "./dist/server/assets.d.ts"
|
|
82
83
|
}
|
|
83
84
|
},
|
|
84
85
|
"files": [
|