@imjp/writenex-astro 0.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/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- package/src/types/version.ts +117 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Astro integration for Writenex visual editor
|
|
3
|
+
*
|
|
4
|
+
* This module provides the main Astro integration that injects the Writenex
|
|
5
|
+
* editor UI and API routes into an Astro project.
|
|
6
|
+
*
|
|
7
|
+
* ## Features:
|
|
8
|
+
* - Injects editor UI at /_writenex
|
|
9
|
+
* - Provides API routes for content CRUD operations
|
|
10
|
+
* - Auto-discovers content collections
|
|
11
|
+
* - Production guard to prevent accidental exposure
|
|
12
|
+
*
|
|
13
|
+
* ## Usage:
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // astro.config.mjs
|
|
16
|
+
* import { defineConfig } from 'astro/config';
|
|
17
|
+
* import writenex from '@writenex/astro';
|
|
18
|
+
*
|
|
19
|
+
* export default defineConfig({
|
|
20
|
+
* integrations: [writenex()],
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module @writenex/astro/integration
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { AstroIntegration } from "astro";
|
|
28
|
+
import type { WritenexOptions, WritenexConfig } from "@/types";
|
|
29
|
+
import { loadConfig } from "@/config/loader";
|
|
30
|
+
import { createMiddleware } from "@/server/middleware";
|
|
31
|
+
import { ContentWatcher } from "@/filesystem/watcher";
|
|
32
|
+
import { getCache } from "@/server/cache";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default base path for the Writenex editor UI
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_BASE_PATH = "/_writenex";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Package name for logging
|
|
41
|
+
*/
|
|
42
|
+
const PACKAGE_NAME = "@writenex/astro";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates the Writenex Astro integration.
|
|
46
|
+
*
|
|
47
|
+
* This integration injects the Writenex visual editor into your Astro project,
|
|
48
|
+
* providing a WYSIWYG interface for editing content collections.
|
|
49
|
+
*
|
|
50
|
+
* @param options - Integration options
|
|
51
|
+
* @param options.allowProduction - Allow running in production (default: false)
|
|
52
|
+
* @returns Astro integration object
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Basic usage
|
|
57
|
+
* export default defineConfig({
|
|
58
|
+
* integrations: [writenex()],
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // With options
|
|
62
|
+
* export default defineConfig({
|
|
63
|
+
* integrations: [
|
|
64
|
+
* writenex({
|
|
65
|
+
* allowProduction: true, // Enable in production (use with caution)
|
|
66
|
+
* }),
|
|
67
|
+
* ],
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export default function writenex(options?: WritenexOptions): AstroIntegration {
|
|
72
|
+
const { allowProduction = false } = options ?? {};
|
|
73
|
+
|
|
74
|
+
// Use fixed base path for consistency and branding
|
|
75
|
+
const basePath = DEFAULT_BASE_PATH;
|
|
76
|
+
|
|
77
|
+
// Track if we should be active
|
|
78
|
+
let isActive = true;
|
|
79
|
+
|
|
80
|
+
// Store loaded configuration
|
|
81
|
+
let resolvedConfig: Required<WritenexConfig> | null = null;
|
|
82
|
+
|
|
83
|
+
// Store project root
|
|
84
|
+
let projectRoot = "";
|
|
85
|
+
|
|
86
|
+
// Store Astro's trailingSlash setting
|
|
87
|
+
let astroTrailingSlash: "always" | "never" | "ignore" = "ignore";
|
|
88
|
+
|
|
89
|
+
// File watcher instance
|
|
90
|
+
let watcher: ContentWatcher | null = null;
|
|
91
|
+
|
|
92
|
+
// Track if editor URL has been logged (to avoid duplicate logs)
|
|
93
|
+
let hasLoggedEditorUrl = false;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
name: PACKAGE_NAME,
|
|
97
|
+
hooks: {
|
|
98
|
+
/**
|
|
99
|
+
* Configuration setup hook
|
|
100
|
+
*
|
|
101
|
+
* This hook runs during Astro's config resolution phase.
|
|
102
|
+
* We use it to:
|
|
103
|
+
* 1. Check if we should run (production guard)
|
|
104
|
+
* 2. Load Writenex configuration
|
|
105
|
+
* 3. Register any necessary Vite plugins
|
|
106
|
+
*/
|
|
107
|
+
"astro:config:setup": async ({ command, logger, config }) => {
|
|
108
|
+
// Production guard: disable in production unless explicitly allowed
|
|
109
|
+
if (command === "build" && !allowProduction) {
|
|
110
|
+
logger.warn(
|
|
111
|
+
"Disabled in production build. Use allowProduction: true to override."
|
|
112
|
+
);
|
|
113
|
+
isActive = false;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Store project root
|
|
118
|
+
projectRoot = config.root.pathname;
|
|
119
|
+
|
|
120
|
+
// Capture Astro's trailingSlash setting for preview URLs
|
|
121
|
+
astroTrailingSlash = config.trailingSlash ?? "ignore";
|
|
122
|
+
|
|
123
|
+
// Load Writenex configuration
|
|
124
|
+
const { config: loadedConfig, warnings } =
|
|
125
|
+
await loadConfig(projectRoot);
|
|
126
|
+
resolvedConfig = loadedConfig;
|
|
127
|
+
|
|
128
|
+
// Log any configuration warnings
|
|
129
|
+
for (const warning of warnings) {
|
|
130
|
+
logger.warn(warning);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Server setup hook
|
|
136
|
+
*
|
|
137
|
+
* This hook runs when the Astro dev server starts.
|
|
138
|
+
* We use it to:
|
|
139
|
+
* 1. Inject middleware for API routes
|
|
140
|
+
* 2. Serve the editor UI
|
|
141
|
+
* 3. Start file watcher for cache invalidation
|
|
142
|
+
*/
|
|
143
|
+
"astro:server:setup": ({ server }) => {
|
|
144
|
+
// Skip if disabled (production guard triggered)
|
|
145
|
+
if (!isActive || !resolvedConfig) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create and register the middleware
|
|
150
|
+
const middleware = createMiddleware({
|
|
151
|
+
basePath,
|
|
152
|
+
projectRoot,
|
|
153
|
+
config: resolvedConfig,
|
|
154
|
+
trailingSlash: astroTrailingSlash,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
server.middlewares.use(middleware);
|
|
158
|
+
|
|
159
|
+
// Setup cache with file watcher integration
|
|
160
|
+
const cache = getCache({ hasWatcher: true });
|
|
161
|
+
|
|
162
|
+
// Start file watcher for cache invalidation
|
|
163
|
+
watcher = new ContentWatcher(projectRoot, "src/content", {
|
|
164
|
+
onChange: (event) => {
|
|
165
|
+
cache.handleFileChange(event.type, event.collection);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
watcher.start();
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Server start hook
|
|
174
|
+
*
|
|
175
|
+
* This hook runs after the dev server has started and is listening.
|
|
176
|
+
* We use it to log the full editor URL with the actual server address.
|
|
177
|
+
*/
|
|
178
|
+
"astro:server:start": ({ address, logger }) => {
|
|
179
|
+
if (!isActive || hasLoggedEditorUrl) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Build the full URL from the server address
|
|
184
|
+
// Normalize loopback addresses to "localhost" for better readability
|
|
185
|
+
const protocol = "http";
|
|
186
|
+
const rawHost = address.address;
|
|
187
|
+
const isLoopback =
|
|
188
|
+
rawHost === "" ||
|
|
189
|
+
rawHost === "::" ||
|
|
190
|
+
rawHost === "127.0.0.1" ||
|
|
191
|
+
rawHost === "::1";
|
|
192
|
+
const host = isLoopback ? "localhost" : rawHost;
|
|
193
|
+
const port = address.port;
|
|
194
|
+
const editorUrl = `${protocol}://${host}:${port}${basePath}`;
|
|
195
|
+
|
|
196
|
+
logger.info(`Writenex editor running at: ${editorUrl}`);
|
|
197
|
+
hasLoggedEditorUrl = true;
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Server done hook
|
|
202
|
+
*
|
|
203
|
+
* This hook runs when the server is shutting down.
|
|
204
|
+
* We use it to clean up the file watcher.
|
|
205
|
+
*/
|
|
206
|
+
"astro:server:done": async () => {
|
|
207
|
+
if (watcher) {
|
|
208
|
+
await watcher.stop();
|
|
209
|
+
watcher = null;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build done hook
|
|
215
|
+
*
|
|
216
|
+
* This hook runs after the build completes.
|
|
217
|
+
* Currently just logs a warning if production mode is enabled.
|
|
218
|
+
*/
|
|
219
|
+
"astro:build:done": ({ logger }) => {
|
|
220
|
+
if (allowProduction) {
|
|
221
|
+
logger.warn(
|
|
222
|
+
"Production mode enabled. Ensure your deployment is secured."
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Static asset serving for Writenex editor
|
|
3
|
+
*
|
|
4
|
+
* This module handles serving the editor UI HTML and static assets
|
|
5
|
+
* (JavaScript, CSS) for the Writenex editor interface.
|
|
6
|
+
*
|
|
7
|
+
* ## Asset Strategy:
|
|
8
|
+
* - In development: Serve from source with Vite transform
|
|
9
|
+
* - In production: Serve pre-bundled assets from dist/client
|
|
10
|
+
*
|
|
11
|
+
* @module @writenex/astro/server/assets
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { join, extname } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import type { MiddlewareContext } from "./middleware";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the package root directory
|
|
23
|
+
*
|
|
24
|
+
* This function determines the package root based on where the code is running from.
|
|
25
|
+
* When installed from npm, the structure is:
|
|
26
|
+
* node_modules/@writenex/astro/dist/index.js
|
|
27
|
+
*
|
|
28
|
+
* We need to find the package root to locate dist/client/ assets.
|
|
29
|
+
*/
|
|
30
|
+
function getPackageRoot(): string {
|
|
31
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
32
|
+
const currentDir = fileURLToPath(new URL(".", import.meta.url));
|
|
33
|
+
|
|
34
|
+
// When bundled, import.meta.url points to the dist/index.js file
|
|
35
|
+
// We need to go up one level to get to package root
|
|
36
|
+
if (currentFile.endsWith("dist/index.js") || currentDir.endsWith("dist/")) {
|
|
37
|
+
return join(currentDir, "..");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// When running from source (development), we're in src/server/
|
|
41
|
+
// Go up 2 levels to get to package root
|
|
42
|
+
if (currentDir.includes("/src/")) {
|
|
43
|
+
return join(currentDir, "..", "..");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fallback: assume we're in dist
|
|
47
|
+
return join(currentDir, "..");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PACKAGE_ROOT = getPackageRoot();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* MIME types for static assets
|
|
54
|
+
*/
|
|
55
|
+
const MIME_TYPES: Record<string, string> = {
|
|
56
|
+
".js": "application/javascript",
|
|
57
|
+
".mjs": "application/javascript",
|
|
58
|
+
".css": "text/css",
|
|
59
|
+
".json": "application/json",
|
|
60
|
+
".svg": "image/svg+xml",
|
|
61
|
+
".png": "image/png",
|
|
62
|
+
".jpg": "image/jpeg",
|
|
63
|
+
".jpeg": "image/jpeg",
|
|
64
|
+
".gif": "image/gif",
|
|
65
|
+
".woff": "font/woff",
|
|
66
|
+
".woff2": "font/woff2",
|
|
67
|
+
".ttf": "font/ttf",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Serve the editor HTML page
|
|
72
|
+
*
|
|
73
|
+
* This generates the HTML shell that loads the React editor application.
|
|
74
|
+
* The actual React components will be loaded via the bundled client assets.
|
|
75
|
+
*
|
|
76
|
+
* @param _req - The incoming request
|
|
77
|
+
* @param res - The server response
|
|
78
|
+
* @param context - Middleware context
|
|
79
|
+
*/
|
|
80
|
+
export async function serveEditorHtml(
|
|
81
|
+
_req: IncomingMessage,
|
|
82
|
+
res: ServerResponse,
|
|
83
|
+
context: MiddlewareContext
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const { basePath } = context;
|
|
86
|
+
|
|
87
|
+
const html = generateEditorHtml(basePath);
|
|
88
|
+
|
|
89
|
+
res.statusCode = 200;
|
|
90
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
91
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
92
|
+
res.end(html);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Serve static assets (JS, CSS, etc.)
|
|
97
|
+
*
|
|
98
|
+
* @param _req - The incoming request
|
|
99
|
+
* @param res - The server response
|
|
100
|
+
* @param assetPath - Path to the asset (relative to assets directory)
|
|
101
|
+
* @param _context - Middleware context
|
|
102
|
+
*/
|
|
103
|
+
export async function serveAsset(
|
|
104
|
+
_req: IncomingMessage,
|
|
105
|
+
res: ServerResponse,
|
|
106
|
+
assetPath: string,
|
|
107
|
+
_context: MiddlewareContext
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
// Determine asset location
|
|
110
|
+
// Assets are always in dist/client (pre-bundled by tsup)
|
|
111
|
+
const distPath = join(PACKAGE_ROOT, "dist", "client", assetPath);
|
|
112
|
+
|
|
113
|
+
if (!existsSync(distPath)) {
|
|
114
|
+
console.error("[writenex] Asset not found:", distPath);
|
|
115
|
+
res.statusCode = 404;
|
|
116
|
+
res.setHeader("Content-Type", "text/plain");
|
|
117
|
+
res.end(`Asset not found: ${assetPath}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const filePath = distPath;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const content = await readFile(filePath);
|
|
125
|
+
const ext = extname(assetPath).toLowerCase();
|
|
126
|
+
const mimeType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
127
|
+
|
|
128
|
+
res.statusCode = 200;
|
|
129
|
+
res.setHeader("Content-Type", mimeType);
|
|
130
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
131
|
+
res.end(content);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(`[writenex] Failed to serve asset: ${assetPath}`, error);
|
|
134
|
+
res.statusCode = 500;
|
|
135
|
+
res.setHeader("Content-Type", "text/plain");
|
|
136
|
+
res.end("Failed to read asset");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate the editor HTML shell
|
|
142
|
+
*
|
|
143
|
+
* This creates the HTML page that bootstraps the React editor application.
|
|
144
|
+
* It includes:
|
|
145
|
+
* - Meta tags for viewport and charset
|
|
146
|
+
* - CSS for the editor
|
|
147
|
+
* - React mount point
|
|
148
|
+
* - JavaScript bundle
|
|
149
|
+
*
|
|
150
|
+
* @param basePath - Base path for the editor
|
|
151
|
+
* @returns HTML string
|
|
152
|
+
*/
|
|
153
|
+
function generateEditorHtml(basePath: string): string {
|
|
154
|
+
return `<!DOCTYPE html>
|
|
155
|
+
<html lang="en">
|
|
156
|
+
<head>
|
|
157
|
+
<meta charset="UTF-8">
|
|
158
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
159
|
+
<meta name="robots" content="noindex, nofollow">
|
|
160
|
+
<title>Writenex - Content Editor</title>
|
|
161
|
+
|
|
162
|
+
<!-- Editor styles -->
|
|
163
|
+
<link rel="stylesheet" href="${basePath}/assets/index.css">
|
|
164
|
+
<link rel="stylesheet" href="${basePath}/assets/styles.css">
|
|
165
|
+
|
|
166
|
+
<style>
|
|
167
|
+
/* Critical CSS for initial load */
|
|
168
|
+
* {
|
|
169
|
+
margin: 0;
|
|
170
|
+
padding: 0;
|
|
171
|
+
box-sizing: border-box;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
html, body, #root {
|
|
175
|
+
height: 100%;
|
|
176
|
+
width: 100%;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
body {
|
|
180
|
+
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
181
|
+
background-color: #0a0a0a;
|
|
182
|
+
color: #fafafa;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Loading state */
|
|
186
|
+
.writenex-loading {
|
|
187
|
+
display: flex;
|
|
188
|
+
flex-direction: column;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: center;
|
|
191
|
+
height: 100%;
|
|
192
|
+
gap: 1rem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.writenex-loading-spinner {
|
|
196
|
+
width: 40px;
|
|
197
|
+
height: 40px;
|
|
198
|
+
border: 3px solid rgba(59, 130, 246, 0.2);
|
|
199
|
+
border-top-color: #3b82f6;
|
|
200
|
+
border-radius: 50%;
|
|
201
|
+
animation: spin 1s linear infinite;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes spin {
|
|
205
|
+
to { transform: rotate(360deg); }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.writenex-loading-text {
|
|
209
|
+
color: #71717a;
|
|
210
|
+
font-size: 0.875rem;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
213
|
+
</head>
|
|
214
|
+
<body>
|
|
215
|
+
<div id="root">
|
|
216
|
+
<!-- Loading state shown while React loads -->
|
|
217
|
+
<div class="writenex-loading">
|
|
218
|
+
<div class="writenex-loading-spinner"></div>
|
|
219
|
+
<div class="writenex-loading-text">Loading Writenex Editor...</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Configuration for the client app -->
|
|
224
|
+
<script>
|
|
225
|
+
window.__WRITENEX_CONFIG__ = {
|
|
226
|
+
basePath: "${basePath}",
|
|
227
|
+
apiBase: "${basePath}/api",
|
|
228
|
+
};
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<!-- Editor application -->
|
|
232
|
+
<script type="module" src="${basePath}/assets/index.js"></script>
|
|
233
|
+
</body>
|
|
234
|
+
</html>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the path to bundled client assets
|
|
239
|
+
*
|
|
240
|
+
* @returns Path to the client dist directory
|
|
241
|
+
*/
|
|
242
|
+
export function getClientDistPath(): string {
|
|
243
|
+
return join(PACKAGE_ROOT, "dist", "client");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if client assets are bundled
|
|
248
|
+
*
|
|
249
|
+
* @returns True if bundled assets exist
|
|
250
|
+
*/
|
|
251
|
+
export function hasClientBundle(): boolean {
|
|
252
|
+
const indexPath = join(getClientDistPath(), "index.js");
|
|
253
|
+
return existsSync(indexPath);
|
|
254
|
+
}
|