kempo 4.2.24 → 4.2.26

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.
@@ -4,6 +4,7 @@ import { pathToFileURL, fileURLToPath } from 'url';
4
4
  import { renderExternalPage } from 'kempo-server/templating';
5
5
  import getSession from '../server/utils/auth/getSession.js';
6
6
  import currentUserHasPermission from '../server/utils/permissions/currentUserHasPermission.js';
7
+ import { LEXICAL_BASE, bundleFileName as lexicalBundleFileName } from '../server/utils/lexical/packages.js';
7
8
  import { getEnabledExtensions } from '../server/utils/extensions/scopeCache.js';
8
9
  import triggerHook from '../server/utils/hooks/triggerHook.js';
9
10
  import { ADMIN_GLOBALS_DIR } from '../server/utils/admin-global-content/helpers.js';
@@ -34,6 +35,24 @@ const NODE_MODULES = join(process.cwd(), 'node_modules');
34
35
  */
35
36
  const DEFAULT_ADMIN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'admin');
36
37
 
38
+ /*
39
+ Where the lexical bundles live and the URL they are served at. Must stay in step with
40
+ window.kempo.lexicalUrl in src/admin/init.js, which is what kempo-ui reads.
41
+ */
42
+ const LEXICAL_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'kempo', 'vendor', 'lexical');
43
+
44
+ let lexicalVersionCache = null;
45
+ const lexicalVersion = async () => {
46
+ if(lexicalVersionCache) return lexicalVersionCache;
47
+ try {
48
+ const manifest = JSON.parse(await readFile(join(LEXICAL_DIR, 'manifest.json'), 'utf8'));
49
+ lexicalVersionCache = manifest.version;
50
+ } catch {
51
+ lexicalVersionCache = 'unknown';
52
+ }
53
+ return lexicalVersionCache;
54
+ };
55
+
37
56
  const resolveAdminRoot = adminRoot => {
38
57
  if(!adminRoot) return DEFAULT_ADMIN_ROOT;
39
58
  return isAbsolute(adminRoot) ? adminRoot : resolve(process.cwd(), adminRoot);
@@ -157,6 +176,64 @@ export default config => {
157
176
  const { path } = request;
158
177
  const url = request.url.split('?')[0];
159
178
 
179
+ /*
180
+ Locally hosted lexical for the admin's WYSIWYG.
181
+
182
+ kempo-ui's HtmlEditor requests `${window.kempo.lexicalUrl}/${pkg}@${version}`, an esm.sh-shaped
183
+ URL with no file extension, so it cannot be served by a static route: the package name
184
+ contains a slash and the response needs a JavaScript content type to be accepted as a module.
185
+ This maps that shape onto the bundles the build produced.
186
+
187
+ Served before the auth gate on purpose. These are public third-party assets, and requiring a
188
+ session would make the editor fail for exactly the reader who is allowed to see it.
189
+ */
190
+ if(url.startsWith(`${LEXICAL_BASE}/`)){
191
+ const requested = decodeURIComponent(url.slice(LEXICAL_BASE.length + 1));
192
+ const match = requested.match(/^(.+)@([^@]+)$/);
193
+
194
+ /*
195
+ Shared chunks. Splitting puts lexical's core in a chunk that the package entries import by
196
+ relative path, so those requests arrive here as ordinary filenames with no version suffix.
197
+ */
198
+ let file = null;
199
+ if(match){
200
+ file = join(LEXICAL_DIR, lexicalBundleFileName(match[1]));
201
+ if(match[2] !== await lexicalVersion()){
202
+ console.warn(`[kempo] lexical ${match[2]} requested but ${await lexicalVersion()} is bundled — rebuild kempo, or kempo-ui's LEXICAL_VERSION has moved`);
203
+ }
204
+ } else if(/^[\w./-]+\.js$/.test(requested) && !requested.includes('..')){
205
+ file = join(LEXICAL_DIR, requested);
206
+ }
207
+
208
+ if(file){
209
+ try {
210
+ const info = await stat(file);
211
+ /*
212
+ Revalidated rather than immutable. The URL carries lexical's version but not kempo's, so
213
+ rebuilding kempo changes what lives at the same URL, and caching that for a year would
214
+ strand every client on a stale bundle. A 304 costs almost nothing.
215
+ */
216
+ const etag = `"${info.size}-${Math.floor(info.mtimeMs)}"`;
217
+ if(request.headers['if-none-match'] === etag){
218
+ response.writeHead(304, { ETag: etag, 'Cache-Control': 'public, must-revalidate' });
219
+ response.end();
220
+ return;
221
+ }
222
+ response.writeHead(200, {
223
+ 'Content-Type': 'application/javascript; charset=utf-8',
224
+ 'Cache-Control': 'public, must-revalidate',
225
+ ETag: etag,
226
+ });
227
+ response.end(await readFile(file));
228
+ return;
229
+ } catch {
230
+ response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
231
+ response.end(`No bundled lexical asset "${requested}". Run \`npm run build\` in kempo.`);
232
+ return;
233
+ }
234
+ }
235
+ }
236
+
160
237
  /*
161
238
  Auth: protect /account and /admin routes
162
239
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo",
3
- "version": "4.2.24",
3
+ "version": "4.2.26",
4
4
  "type": "module",
5
5
  "description": "A fullstack CMS framework for creating websites with user authentication",
6
6
  "scripts": {
@@ -37,11 +37,20 @@
37
37
  "scripts/init-db.js"
38
38
  ],
39
39
  "dependencies": {
40
+ "@lexical/code": "0.43.0",
41
+ "@lexical/history": "0.43.0",
42
+ "@lexical/html": "0.43.0",
43
+ "@lexical/link": "0.43.0",
44
+ "@lexical/list": "0.43.0",
45
+ "@lexical/rich-text": "0.43.0",
46
+ "@lexical/selection": "0.43.0",
47
+ "@lexical/table": "0.43.0",
40
48
  "bcrypt": "^6.0.0",
41
49
  "dotenv": "^17.2.3",
42
50
  "drizzle-kit": "^0.31.7",
43
51
  "drizzle-orm": "^0.45.2",
44
- "inquirer": "^12.6.1",
52
+ "inquirer": "^14.0.2",
53
+ "lexical": "0.43.0",
45
54
  "monaco-editor": "^0.56.0",
46
55
  "postgres": "^3.4.7",
47
56
  "resend": "^6.5.2"
@@ -52,6 +61,7 @@
52
61
  "kempo-ui": ">=0.4.39"
53
62
  },
54
63
  "devDependencies": {
64
+ "esbuild": "^0.28.2",
55
65
  "html-minifier-terser": "^7.2.0",
56
66
  "kempo-testing-framework": "^1.4.6",
57
67
  "terser": "^5.46.2"
@@ -0,0 +1,28 @@
1
+ /*
2
+ The lexical packages kempo-ui's HtmlEditor imports, and how each one's browser bundle is named on
3
+ disk. Shared by the build (which produces the bundles) and the middleware (which serves them), so
4
+ the two cannot drift.
5
+
6
+ This lives under server/utils rather than scripts/ because scripts/ is not published — a
7
+ middleware importing from there would resolve here but be missing for every consumer.
8
+ */
9
+ /*
10
+ Where the bundles are served. The build stamps this into chunk references and the middleware
11
+ answers on it, so the two must agree.
12
+ */
13
+ export const LEXICAL_BASE = '/kempo/vendor/lexical';
14
+
15
+ export const LEXICAL_PACKAGES = [
16
+ 'lexical',
17
+ '@lexical/rich-text',
18
+ '@lexical/html',
19
+ '@lexical/history',
20
+ '@lexical/list',
21
+ '@lexical/link',
22
+ '@lexical/selection',
23
+ '@lexical/table',
24
+ '@lexical/code',
25
+ ];
26
+
27
+ // "@lexical/rich-text" is not a usable filename, so the scope marker and slash are flattened
28
+ export const bundleFileName = pkg => `${pkg.replace('@', '').replace('/', '__')}.js`;