@trokky/studio 2.0.7 → 3.1.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-D0f8OCEI.js"></script>
48
+ <script type="module" crossorigin src="/assets/index-Da-xtsMB.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,54 @@
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
+ }
50
+ /**
51
+ * Build an Express router that serves Trokky Studio.
52
+ */
53
+ export declare function studioRouter(options?: StudioRouterOptions): Router;
54
+ export { getStudioHTML, getStudioAsset } from './assets.js';
@@ -0,0 +1,76 @@
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
+ ...(options.i18n ? { i18n: options.i18n } : {}),
48
+ }, basePath);
49
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
50
+ res.setHeader('Cache-Control', 'no-store');
51
+ res.send(html);
52
+ };
53
+ router.use((req, res, next) => {
54
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
55
+ next();
56
+ return;
57
+ }
58
+ // req.path is relative to the mount point.
59
+ if (req.path.startsWith('/assets/')) {
60
+ const asset = getStudioAsset(req.path.slice('/assets/'.length));
61
+ if (!asset) {
62
+ res.status(404).send('Not found');
63
+ return;
64
+ }
65
+ res.setHeader('Content-Type', asset.contentType);
66
+ // Vite emits content-hashed filenames, so they can be cached forever.
67
+ res.setHeader('Cache-Control', `public, max-age=${ASSET_MAX_AGE_SECONDS}, immutable`);
68
+ res.send(asset.content);
69
+ return;
70
+ }
71
+ // The root and every client-side route get the same document.
72
+ sendHtml(req, res);
73
+ });
74
+ return router;
75
+ }
76
+ export { getStudioHTML, getStudioAsset } from './assets.js';
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@trokky/studio",
3
- "version": "2.0.7",
4
- "description": "Trokky Studio v2 - Zero-config content management interface",
3
+ "version": "3.1.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": "^2.0.7",
34
+ "@trokky/trokky": "^3.1.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": [