kempo 4.2.30 → 4.2.32
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/middleware/kempo.js +9 -2
- package/package.json +1 -1
- package/server/utils/extensions/dependencies.js +26 -0
- package/server/utils/extensions/disableExtension.js +6 -0
- package/server/utils/extensions/enableExtension.js +13 -6
- package/server/utils/extensions/installExtension.js +6 -0
- package/server/utils/extensions/uninstallExtension.js +6 -0
package/middleware/kempo.js
CHANGED
|
@@ -212,16 +212,23 @@ export default config => {
|
|
|
212
212
|
Revalidated rather than immutable. The URL carries lexical's version but not kempo's, so
|
|
213
213
|
rebuilding kempo changes what lives at the same URL, and caching that for a year would
|
|
214
214
|
strand every client on a stale bundle. A 304 costs almost nothing.
|
|
215
|
+
|
|
216
|
+
`no-cache`, not `must-revalidate` alone: without an explicit max-age, must-revalidate
|
|
217
|
+
leaves freshness to the browser's heuristics, and a heuristically-fresh entry is served
|
|
218
|
+
straight from disk cache with no conditional request at all — so a stale entry chunk can
|
|
219
|
+
keep pointing at a chunk hash a rebuild already deleted, breaking the editor with no
|
|
220
|
+
network request to observe. no-cache forces a conditional GET on every load regardless of
|
|
221
|
+
heuristic freshness, so a rebuild is always picked up via the ETag check above.
|
|
215
222
|
*/
|
|
216
223
|
const etag = `"${info.size}-${Math.floor(info.mtimeMs)}"`;
|
|
217
224
|
if(request.headers['if-none-match'] === etag){
|
|
218
|
-
response.writeHead(304, { ETag: etag, 'Cache-Control': 'public,
|
|
225
|
+
response.writeHead(304, { ETag: etag, 'Cache-Control': 'public, no-cache' });
|
|
219
226
|
response.end();
|
|
220
227
|
return;
|
|
221
228
|
}
|
|
222
229
|
response.writeHead(200, {
|
|
223
230
|
'Content-Type': 'application/javascript; charset=utf-8',
|
|
224
|
-
'Cache-Control': 'public,
|
|
231
|
+
'Cache-Control': 'public, no-cache',
|
|
225
232
|
ETag: etag,
|
|
226
233
|
});
|
|
227
234
|
response.end(await readFile(file));
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import db from '../../db/index.js';
|
|
2
|
+
import { extension } from '../../db/schema.js';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
An extension declares the other extensions it requires via kempo-config.json's `dependencies`
|
|
7
|
+
array (a list of extension names). Enforced at install/enable time (a dependency must already be
|
|
8
|
+
enabled) and in reverse at disable/uninstall time (nothing enabled may be left depending on an
|
|
9
|
+
extension that is going away).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const getUnmetDependencies = async (dependencies = []) => {
|
|
13
|
+
if(!dependencies.length) return [];
|
|
14
|
+
const rows = await db.select({ name: extension.name, enabled: extension.enabled }).from(extension);
|
|
15
|
+
const enabled = new Set(rows.filter(row => row.enabled).map(row => row.name));
|
|
16
|
+
return dependencies.filter(dep => !enabled.has(dep));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getEnabledDependents = async (name) => {
|
|
20
|
+
const rows = await db.select({ name: extension.name, kempo: extension.kempo })
|
|
21
|
+
.from(extension)
|
|
22
|
+
.where(eq(extension.enabled, true));
|
|
23
|
+
return rows
|
|
24
|
+
.filter(row => row.name !== name && (row.kempo?.dependencies || []).includes(name))
|
|
25
|
+
.map(row => row.name);
|
|
26
|
+
};
|
|
@@ -2,12 +2,18 @@ import db from '../../db/index.js';
|
|
|
2
2
|
import { extension } from '../../db/schema.js';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
4
|
import { invalidateScopeCache } from './scopeCache.js';
|
|
5
|
+
import { getEnabledDependents } from './dependencies.js';
|
|
5
6
|
|
|
6
7
|
export default async ({ name }) => {
|
|
7
8
|
if(!name){
|
|
8
9
|
return [{ code: 400, msg: 'Extension name is required' }, null];
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
const dependents = await getEnabledDependents(name);
|
|
13
|
+
if(dependents.length){
|
|
14
|
+
return [{ code: 409, msg: `${dependents.join(', ')} depends on this extension and is still enabled` }, null];
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
try {
|
|
12
18
|
const updated = await db.update(extension)
|
|
13
19
|
.set({ enabled: false, updatedAt: new Date() })
|
|
@@ -2,6 +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 { invalidateScopeCache } from './scopeCache.js';
|
|
5
|
+
import { getUnmetDependencies } from './dependencies.js';
|
|
5
6
|
|
|
6
7
|
export default async ({ name }) => {
|
|
7
8
|
if(!name){
|
|
@@ -9,17 +10,23 @@ export default async ({ name }) => {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
try {
|
|
12
|
-
const
|
|
13
|
+
const [existing] = await db.select().from(extension).where(eq(extension.name, name)).limit(1);
|
|
14
|
+
if(!existing){
|
|
15
|
+
return [{ code: 404, msg: 'Extension not found' }, null];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const unmet = await getUnmetDependencies(existing.kempo?.dependencies || []);
|
|
19
|
+
if(unmet.length){
|
|
20
|
+
return [{ code: 409, msg: `Requires ${unmet.join(', ')} to be enabled first` }, null];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const [updated] = await db.update(extension)
|
|
13
24
|
.set({ enabled: true, updatedAt: new Date() })
|
|
14
25
|
.where(eq(extension.name, name))
|
|
15
26
|
.returning();
|
|
16
27
|
|
|
17
|
-
if(!updated.length){
|
|
18
|
-
return [{ code: 404, msg: 'Extension not found' }, null];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
28
|
invalidateScopeCache();
|
|
22
|
-
return [null, updated
|
|
29
|
+
return [null, updated];
|
|
23
30
|
} catch(error) {
|
|
24
31
|
return [{ code: 500, msg: 'Failed to enable extension' }, null];
|
|
25
32
|
}
|
|
@@ -11,6 +11,7 @@ import createHook from '../hooks/createHook.js';
|
|
|
11
11
|
import setSetting from '../settings/setSetting.js';
|
|
12
12
|
import createTablesFromSchema from './createTablesFromSchema.js';
|
|
13
13
|
import { invalidateScopeCache } from './scopeCache.js';
|
|
14
|
+
import { getUnmetDependencies } from './dependencies.js';
|
|
14
15
|
|
|
15
16
|
export default async ({ name }) => {
|
|
16
17
|
if(!name){
|
|
@@ -44,6 +45,11 @@ export default async ({ name }) => {
|
|
|
44
45
|
return [{ code: 500, msg: `Failed to read extension package files` }, null];
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
const unmet = await getUnmetDependencies(kempoConfig.dependencies || []);
|
|
49
|
+
if(unmet.length){
|
|
50
|
+
return [{ code: 409, msg: `Requires ${unmet.join(', ')} to be installed and enabled first` }, null];
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
/*
|
|
48
54
|
Create tables from declarative schema
|
|
49
55
|
*/
|
|
@@ -6,6 +6,7 @@ import { pathToFileURL } from 'url';
|
|
|
6
6
|
import { sql } from 'drizzle-orm';
|
|
7
7
|
import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
|
|
8
8
|
import { invalidateScopeCache } from './scopeCache.js';
|
|
9
|
+
import { getEnabledDependents } from './dependencies.js';
|
|
9
10
|
|
|
10
11
|
/*
|
|
11
12
|
Uninstall an extension.
|
|
@@ -38,6 +39,11 @@ export default async ({ name, purgeData = false }) => {
|
|
|
38
39
|
return [{ code: 500, msg: 'Failed to find extension' }, null];
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
const dependents = await getEnabledDependents(name);
|
|
43
|
+
if(dependents.length){
|
|
44
|
+
return [{ code: 409, msg: `${dependents.join(', ')} depends on this extension and is still enabled` }, null];
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
try {
|
|
42
48
|
const extRoot = join(process.cwd(), 'node_modules', name);
|
|
43
49
|
const uninstallModule = await import(pathToFileURL(join(extRoot, 'uninstall.js')).href + `?t=${Date.now()}`).catch(() => null);
|