@trokky/studio 3.2.0 → 3.4.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/assets/{index-Da-xtsMB.js → index-CMHu7Of4.js} +4 -4
- package/dist/assets/{index-Da-xtsMB.js.map → index-CMHu7Of4.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist/server/document.d.ts +9 -0
- package/dist/server/document.js +37 -0
- package/dist/server/workers.d.ts +32 -0
- package/dist/server/workers.js +60 -0
- package/package.json +7 -3
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-CMHu7Of4.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,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Studio document, prepared for a mount point.
|
|
3
|
+
*
|
|
4
|
+
* Pure: takes the built `index.html` as a string and returns it with the bootstrap config
|
|
5
|
+
* injected and asset paths rewritten. Nothing here touches the filesystem, so it runs
|
|
6
|
+
* anywhere — the Workers handler feeds it HTML from the ASSETS binding.
|
|
7
|
+
*/
|
|
8
|
+
import type { StudioConfig } from './assets.js';
|
|
9
|
+
export declare function transformStudioDocument(html: string, config: StudioConfig, basePath: string): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Studio document, prepared for a mount point.
|
|
3
|
+
*
|
|
4
|
+
* Pure: takes the built `index.html` as a string and returns it with the bootstrap config
|
|
5
|
+
* injected and asset paths rewritten. Nothing here touches the filesystem, so it runs
|
|
6
|
+
* anywhere — the Workers handler feeds it HTML from the ASSETS binding.
|
|
7
|
+
*/
|
|
8
|
+
function escapeHtml(str) {
|
|
9
|
+
return str
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
15
|
+
}
|
|
16
|
+
/** Serialize for a <script> body without letting `</script>` or friends break out. */
|
|
17
|
+
function escapeJsonForScript(obj) {
|
|
18
|
+
return JSON.stringify(obj)
|
|
19
|
+
.replace(/</g, '\\u003c')
|
|
20
|
+
.replace(/>/g, '\\u003e')
|
|
21
|
+
.replace(/&/g, '\\u0026')
|
|
22
|
+
.replace(/'/g, '\\u0027');
|
|
23
|
+
}
|
|
24
|
+
export function transformStudioDocument(html, config, basePath) {
|
|
25
|
+
const prefix = basePath.replace(/\/+$/, '');
|
|
26
|
+
// The built document carries a development bootstrap; replace it wholesale.
|
|
27
|
+
html = html.replace(/window\.TROKKY_CONFIG\s*=\s*{[\s\S]*?};/, `window.TROKKY_CONFIG = ${escapeJsonForScript(config)};`);
|
|
28
|
+
// Vite emits absolute /assets/ URLs; Studio is rarely mounted at the origin root.
|
|
29
|
+
html = html.replace(/src="\/assets\//g, `src="${prefix}/assets/`);
|
|
30
|
+
html = html.replace(/href="\/assets\//g, `href="${prefix}/assets/`);
|
|
31
|
+
// The favicon path assumes the root too, and nothing serves it there.
|
|
32
|
+
html = html.replace(/<link rel="icon"[^>]*>/g, '');
|
|
33
|
+
if (config.branding?.title) {
|
|
34
|
+
html = html.replace(/<title>.*?<\/title>/, `<title>${escapeHtml(config.branding.title)}</title>`);
|
|
35
|
+
}
|
|
36
|
+
return html;
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trokky Studio on a web-standard runtime (Cloudflare Workers, or anything with `fetch`).
|
|
3
|
+
*
|
|
4
|
+
* `studioRouter` is Express and reads the built files from disk. On Workers there is no disk:
|
|
5
|
+
* the built Studio is shipped as static assets and served through the ASSETS binding. So this
|
|
6
|
+
* handler does two things and nothing else — hands hashed assets to the binding with immutable
|
|
7
|
+
* caching, and serves the document for the root and every client-side route, with the bootstrap
|
|
8
|
+
* config injected for this mount point.
|
|
9
|
+
*
|
|
10
|
+
* // wrangler.jsonc: "assets": { "directory": "./public" }, and @trokky/studio/dist copied
|
|
11
|
+
* // into public/studio at build time.
|
|
12
|
+
* const studio = createStudioFetchHandler({ basePath: '/studio', apiPath: '/api', assets: env.ASSETS })
|
|
13
|
+
* if (url.pathname.startsWith('/studio')) return studio(request)
|
|
14
|
+
*/
|
|
15
|
+
import type { StudioConfig } from './assets.js';
|
|
16
|
+
/** The shape of the ASSETS binding; declared structurally so this package needs no Workers types. */
|
|
17
|
+
export interface AssetsFetcher {
|
|
18
|
+
fetch(request: Request): Promise<Response>;
|
|
19
|
+
}
|
|
20
|
+
export interface StudioFetchHandlerOptions {
|
|
21
|
+
/** Where the Studio is mounted, e.g. '/studio'. Also where its files live under the assets root. */
|
|
22
|
+
basePath: string;
|
|
23
|
+
/** Where the Trokky API is mounted. Default '/api'. */
|
|
24
|
+
apiPath?: string;
|
|
25
|
+
/** The Workers static assets binding (`env.ASSETS`). */
|
|
26
|
+
assets: AssetsFetcher;
|
|
27
|
+
/** An absolute API URL, for when the Studio is served from a different origin than the API. */
|
|
28
|
+
backendUrl?: string;
|
|
29
|
+
branding?: StudioConfig['branding'];
|
|
30
|
+
i18n?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
export declare function createStudioFetchHandler(options: StudioFetchHandlerOptions): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trokky Studio on a web-standard runtime (Cloudflare Workers, or anything with `fetch`).
|
|
3
|
+
*
|
|
4
|
+
* `studioRouter` is Express and reads the built files from disk. On Workers there is no disk:
|
|
5
|
+
* the built Studio is shipped as static assets and served through the ASSETS binding. So this
|
|
6
|
+
* handler does two things and nothing else — hands hashed assets to the binding with immutable
|
|
7
|
+
* caching, and serves the document for the root and every client-side route, with the bootstrap
|
|
8
|
+
* config injected for this mount point.
|
|
9
|
+
*
|
|
10
|
+
* // wrangler.jsonc: "assets": { "directory": "./public" }, and @trokky/studio/dist copied
|
|
11
|
+
* // into public/studio at build time.
|
|
12
|
+
* const studio = createStudioFetchHandler({ basePath: '/studio', apiPath: '/api', assets: env.ASSETS })
|
|
13
|
+
* if (url.pathname.startsWith('/studio')) return studio(request)
|
|
14
|
+
*/
|
|
15
|
+
import { transformStudioDocument } from './document.js';
|
|
16
|
+
const ASSET_MAX_AGE_SECONDS = 60 * 60 * 24 * 365;
|
|
17
|
+
const trimTrailingSlash = (value) => value.replace(/\/+$/, '');
|
|
18
|
+
export function createStudioFetchHandler(options) {
|
|
19
|
+
const basePath = trimTrailingSlash(options.basePath);
|
|
20
|
+
const apiPath = trimTrailingSlash(options.apiPath ?? '/api');
|
|
21
|
+
return async function handleStudio(request) {
|
|
22
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
23
|
+
return new Response('Method Not Allowed', { status: 405, headers: { allow: 'GET, HEAD' } });
|
|
24
|
+
}
|
|
25
|
+
const url = new URL(request.url);
|
|
26
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
|
|
27
|
+
return new Response('Not Found', { status: 404 });
|
|
28
|
+
}
|
|
29
|
+
// Hashed assets: the binding serves them from `${basePath}/assets/…` as copied at build time.
|
|
30
|
+
if (url.pathname.startsWith(`${basePath}/assets/`)) {
|
|
31
|
+
const asset = await options.assets.fetch(new Request(url.toString(), { method: request.method }));
|
|
32
|
+
if (!asset.ok)
|
|
33
|
+
return new Response('Not Found', { status: 404 });
|
|
34
|
+
const headers = new Headers(asset.headers);
|
|
35
|
+
// Vite emits content-hashed filenames, so they can be cached forever.
|
|
36
|
+
headers.set('cache-control', `public, max-age=${ASSET_MAX_AGE_SECONDS}, immutable`);
|
|
37
|
+
return new Response(asset.body, { status: asset.status, headers });
|
|
38
|
+
}
|
|
39
|
+
// The root and every client-side route get the same document.
|
|
40
|
+
const documentUrl = new URL(`${basePath}/index.html`, url.origin);
|
|
41
|
+
const built = await options.assets.fetch(new Request(documentUrl.toString()));
|
|
42
|
+
if (!built.ok) {
|
|
43
|
+
return new Response(`Studio is not in the assets bundle at ${basePath}/index.html. Copy @trokky/studio/dist there at build time.`, { status: 500, headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
44
|
+
}
|
|
45
|
+
const config = {
|
|
46
|
+
mode: 'production',
|
|
47
|
+
apiBasePath: apiPath,
|
|
48
|
+
basePath,
|
|
49
|
+
backendUrl: options.backendUrl ?? `${url.origin}${apiPath}`,
|
|
50
|
+
schemas: [],
|
|
51
|
+
branding: options.branding,
|
|
52
|
+
...(options.i18n ? { i18n: options.i18n } : {}),
|
|
53
|
+
};
|
|
54
|
+
const html = transformStudioDocument(await built.text(), config, basePath);
|
|
55
|
+
return new Response(request.method === 'HEAD' ? null : html, {
|
|
56
|
+
status: 200,
|
|
57
|
+
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trokky/studio",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Trokky Studio: the admin UI, mounted by the site as an Express router",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
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/server/express.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/server/document.ts src/server/workers.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": "^3.
|
|
34
|
+
"@trokky/trokky": "^3.4.0",
|
|
35
35
|
"clsx": "^2.0.0",
|
|
36
36
|
"date-fns": "^2.29.0",
|
|
37
37
|
"express": "^4.18.0",
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
"./standalone": {
|
|
81
81
|
"import": "./dist/index.js",
|
|
82
82
|
"types": "./dist/index.d.ts"
|
|
83
|
+
},
|
|
84
|
+
"./workers": {
|
|
85
|
+
"import": "./dist/server/workers.js",
|
|
86
|
+
"types": "./dist/server/workers.d.ts"
|
|
83
87
|
}
|
|
84
88
|
},
|
|
85
89
|
"files": [
|