kempo 4.5.7 → 4.5.8

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 CHANGED
@@ -359,7 +359,7 @@ Extensions can push live updates to browsers: declare a channel, publish to it f
359
359
 
360
360
  ## Admin Dashboard
361
361
 
362
- The admin home page is a grid of widgets one to four blocks wide and one to three tall. Kempo provides widgets for pages, content, users, notifications and extensions; extensions add their own with `kempo.widgets` or `registerDashboardWidget`. Each person chooses which are shown, their order and size, kept in their browser's localStorage, and a widget only loads its script and data once it is shown. Existing sites need `npx drizzle-kit push` to add the `dashboardWidget` table (until then only Kempo's own widgets appear). See [docs/dashboard.md](docs/dashboard.md).
362
+ The admin home page is a grid of widgets one to four blocks wide and one to three tall. Kempo provides widgets for pages, content, users, notifications and extensions; extensions add their own with `widgets` in `kempo-config.json` or `registerDashboardWidget`. Each person chooses which are shown, their order and size, kept in their browser's localStorage, and a widget only loads its script and data once it is shown. Existing sites need `npx drizzle-kit push` to add the `dashboardWidget` table (until then only Kempo's own widgets appear). See [docs/dashboard.md](docs/dashboard.md).
363
363
 
364
364
  ## Notifications
365
365
 
@@ -6,6 +6,7 @@ import getSession from '../server/utils/auth/getSession.js';
6
6
  import currentUserHasPermission from '../server/utils/permissions/currentUserHasPermission.js';
7
7
  import { LEXICAL_BASE, bundleFileName as lexicalBundleFileName } from '../server/utils/lexical/packages.js';
8
8
  import { getEnabledExtensions } from '../server/utils/extensions/scopeCache.js';
9
+ import readKempoConfig from '../server/utils/extensions/readKempoConfig.js';
9
10
  import { extensionAdminDirs, extensionPublicDirs } from '../server/utils/extensions/contentDirs.js';
10
11
  import triggerHook from '../server/utils/hooks/triggerHook.js';
11
12
  import { ADMIN_GLOBALS_DIR } from '../server/utils/admin-global-content/helpers.js';
@@ -394,19 +395,21 @@ export default config => {
394
395
  const publicContentDirs = await extensionPublicDirs();
395
396
 
396
397
  for(const ext of extensions){
397
- let pkgPath, pkg;
398
+ const extRoot = join(NODE_MODULES, ext.name);
399
+ let kempo;
398
400
  try {
399
- pkgPath = join(NODE_MODULES, ext.name, 'package.json');
400
- pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
401
- } catch { continue; }
401
+ ({ kempo } = await readKempoConfig(extRoot));
402
+ } catch {
403
+ continue;
404
+ }
402
405
 
403
- const scope = pkg.kempo?.['public-scope'];
406
+ const scope = kempo['public-scope'];
404
407
  if(!scope) continue;
405
408
 
406
409
  const scopePrefix = `/${scope}`;
407
410
  if(url !== scopePrefix && !url.startsWith(`${scopePrefix}/`)) continue;
408
411
 
409
- const publicDir = join(dirname(pkgPath), 'public');
412
+ const publicDir = join(extRoot, 'public');
410
413
  const subPath = url.slice(scopePrefix.length) || '/';
411
414
  const method = request.method?.toUpperCase() || 'GET';
412
415
  const resolveDir = buildResolveDir(PROJECT_PUBLIC, url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo",
3
- "version": "4.5.7",
3
+ "version": "4.5.8",
4
4
  "type": "module",
5
5
  "description": "A fullstack CMS framework for creating websites with user authentication",
6
6
  "scripts": {
@@ -2,7 +2,7 @@ import db from '../../db/index.js';
2
2
  import { extension } from '../../db/schema.js';
3
3
  import { eq } from 'drizzle-orm';
4
4
  import { join } from 'path';
5
- import { readFile } from 'fs/promises';
5
+ import readKempoConfig from './readKempoConfig.js';
6
6
 
7
7
  const NODE_MODULES = join(process.cwd(), 'node_modules');
8
8
 
@@ -18,15 +18,16 @@ export default async ({ name }) => {
18
18
  }
19
19
  const ext = results[0];
20
20
  let pkg = {};
21
+ let kempo = {};
21
22
  try {
22
- pkg = JSON.parse(await readFile(join(NODE_MODULES, name, 'package.json'), 'utf-8'));
23
+ ({ pkg, kempo } = await readKempoConfig(join(NODE_MODULES, name)));
23
24
  } catch { /* package may not be installed */ }
24
25
  return [null, {
25
26
  ...ext,
26
27
  description: pkg.description || null,
27
28
  author: pkg.author || null,
28
29
  license: pkg.license || null,
29
- docs: pkg.kempo?.docs || null,
30
+ docs: kempo.docs || null,
30
31
  }];
31
32
  } catch(error) {
32
33
  return [{ code: 500, msg: 'Failed to get extension' }, null];
@@ -4,7 +4,6 @@ import { eq } from 'drizzle-orm';
4
4
  import { join } from 'path';
5
5
  import { pathToFileURL } from 'url';
6
6
  import { existsSync } from 'fs';
7
- import { readFile } from 'fs/promises';
8
7
  import { randomUUID } from 'crypto';
9
8
  import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
10
9
  import createHook from '../hooks/createHook.js';
@@ -12,6 +11,7 @@ import registerDashboardWidget from '../dashboard/registerDashboardWidget.js';
12
11
  import setSetting from '../settings/setSetting.js';
13
12
  import declaredValue from '../settings/declaredValue.js';
14
13
  import createTablesFromSchema from './createTablesFromSchema.js';
14
+ import readKempoConfig from './readKempoConfig.js';
15
15
  import { invalidateScopeCache } from './scopeCache.js';
16
16
  import { getUnmetDependencies } from './dependencies.js';
17
17
 
@@ -31,7 +31,6 @@ export default async ({ name }) => {
31
31
 
32
32
  const extRoot = join(process.cwd(), 'node_modules', name);
33
33
  const extPkgPath = join(extRoot, 'package.json');
34
- const kempoConfigPath = join(extRoot, 'kempo-config.json');
35
34
 
36
35
  if(!existsSync(extPkgPath)){
37
36
  return [{ code: 404, msg: `Extension package "${name}" not found` }, null];
@@ -39,10 +38,7 @@ export default async ({ name }) => {
39
38
 
40
39
  let pkg, kempoConfig;
41
40
  try {
42
- pkg = JSON.parse(await readFile(extPkgPath, 'utf-8'));
43
- kempoConfig = existsSync(kempoConfigPath)
44
- ? JSON.parse(await readFile(kempoConfigPath, 'utf-8'))
45
- : {};
41
+ ({ pkg, kempo: kempoConfig } = await readKempoConfig(extRoot));
46
42
  } catch {
47
43
  return [{ code: 500, msg: `Failed to read extension package files` }, null];
48
44
  }
@@ -2,15 +2,16 @@ import db from '../../db/index.js';
2
2
  import { extension } from '../../db/schema.js';
3
3
  import { sql } from 'drizzle-orm';
4
4
  import { join } from 'path';
5
- import { readFile } from 'fs/promises';
5
+ import readKempoConfig from './readKempoConfig.js';
6
6
 
7
7
  const NODE_MODULES = join(process.cwd(), 'node_modules');
8
8
 
9
- const readPkg = async (name) => {
9
+ const readConfig = async name => {
10
10
  try {
11
- const raw = await readFile(join(NODE_MODULES, name, 'package.json'), 'utf-8');
12
- return JSON.parse(raw);
13
- } catch { return {}; }
11
+ return await readKempoConfig(join(NODE_MODULES, name));
12
+ } catch {
13
+ return { pkg: {}, kempo: {} };
14
+ }
14
15
  };
15
16
 
16
17
  export default async ({ limit = 50, offset = 0 } = {}) => {
@@ -20,13 +21,13 @@ export default async ({ limit = 50, offset = 0 } = {}) => {
20
21
  db.select({ count: sql`count(*)` }).from(extension),
21
22
  ]);
22
23
  const enriched = await Promise.all(items.map(async (ext) => {
23
- const pkg = await readPkg(ext.name);
24
+ const { pkg, kempo } = await readConfig(ext.name);
24
25
  return {
25
26
  ...ext,
26
27
  description: pkg.description || null,
27
28
  author: pkg.author || null,
28
29
  license: pkg.license || null,
29
- docs: pkg.kempo?.docs || null,
30
+ docs: kempo.docs || null,
30
31
  };
31
32
  }));
32
33
  return [null, { items: enriched, total: Number(count), limit, offset }];
@@ -0,0 +1,20 @@
1
+ import { join } from 'path';
2
+ import { readFile } from 'fs/promises';
3
+
4
+ /*
5
+ What an installed extension declares to kempo: its `public-scope`, `docs`, `git`, `schema`, `hooks`,
6
+ `permissions`, `settings`, `groups`, `dependencies` and `widgets`.
7
+
8
+ It lives in the extension's kempo-config.json, which is the one place it is written. package.json's own
9
+ `kempo` field is still read, as the fallback for an older extension that put everything there, and
10
+ kempo-config.json wins field by field. Nothing should add new keys to package.json.
11
+
12
+ Reads both files fresh each time, so an extension replaced on disk is seen without a restart.
13
+ Throws when package.json cannot be read, which is how a caller knows the package is not installed.
14
+ */
15
+ export default async extRoot => {
16
+ const pkg = JSON.parse(await readFile(join(extRoot, 'package.json'), 'utf-8'));
17
+ const config = await readFile(join(extRoot, 'kempo-config.json'), 'utf-8').then(JSON.parse, () => ({}));
18
+
19
+ return { pkg, kempo: { ...(pkg.kempo || {}), ...config } };
20
+ };
@@ -3,7 +3,6 @@ import { extension, hook, permission, group, groupPermission } from '../../db/sc
3
3
  import { eq, and } from 'drizzle-orm';
4
4
  import { createRequire } from 'module';
5
5
  import { dirname, join } from 'path';
6
- import { readFile } from 'fs/promises';
7
6
  import { randomUUID } from 'crypto';
8
7
  import { execSync } from 'child_process';
9
8
  import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
@@ -13,6 +12,7 @@ import unregisterDashboardWidget from '../dashboard/unregisterDashboardWidget.js
13
12
  import setSetting from '../settings/setSetting.js';
14
13
  import declaredValue from '../settings/declaredValue.js';
15
14
  import createTablesFromSchema from './createTablesFromSchema.js';
15
+ import readKempoConfig from './readKempoConfig.js';
16
16
 
17
17
  const requireFromProject = createRequire(join(process.cwd(), 'package.json'));
18
18
 
@@ -43,17 +43,19 @@ export default async ({ name, version = 'latest' }) => {
43
43
  return [{ code: 500, msg: `Failed to update package: ${error.message}` }, null];
44
44
  }
45
45
 
46
- let pkgPath, extRoot, pkg;
46
+ /*
47
+ What the new version declares comes from its kempo-config.json (package.json's own `kempo` field is
48
+ only the fallback for an older extension). Reading package.json alone meant that no extension's new
49
+ permissions, settings, groups, hooks or widgets ever reached the database through this update.
50
+ */
51
+ let extRoot, pkg, newKempo;
47
52
  try {
48
- pkgPath = requireFromProject.resolve(`${name}/package.json`);
49
- extRoot = dirname(pkgPath);
50
- pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
53
+ extRoot = dirname(requireFromProject.resolve(`${name}/package.json`));
54
+ ({ pkg, kempo: newKempo } = await readKempoConfig(extRoot));
51
55
  } catch {
52
56
  return [{ code: 404, msg: `Updated package "${name}" not found` }, null];
53
57
  }
54
58
 
55
- const newKempo = pkg.kempo || {};
56
-
57
59
  /*
58
60
  Create any new tables from schema
59
61
  */