nextpluggable 0.0.0-stage → 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/bin/nextpluggable.js +10 -0
- package/dist/api/handler.d.ts +10 -0
- package/dist/api/handler.js +188 -0
- package/dist/auth/custom.d.ts +13 -0
- package/dist/auth/custom.js +12 -0
- package/dist/auth/password.d.ts +10 -0
- package/dist/auth/password.js +66 -0
- package/dist/auth/types.d.ts +23 -0
- package/dist/auth/types.js +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +119 -0
- package/dist/client/host-runtime.d.ts +9 -0
- package/dist/client/host-runtime.js +71 -0
- package/dist/client/island.d.ts +28 -0
- package/dist/client/island.js +177 -0
- package/dist/client/ssr-guard.d.ts +11 -0
- package/dist/client/ssr-guard.js +100 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.js +55 -0
- package/dist/core/installer.d.ts +30 -0
- package/dist/core/installer.js +340 -0
- package/dist/core/routes.d.ts +20 -0
- package/dist/core/routes.js +95 -0
- package/dist/core/runtime.d.ts +32 -0
- package/dist/core/runtime.js +190 -0
- package/dist/core/state.d.ts +13 -0
- package/dist/core/state.js +23 -0
- package/dist/core/store.d.ts +17 -0
- package/dist/core/store.js +58 -0
- package/dist/core/types.d.ts +69 -0
- package/dist/core/types.js +1 -0
- package/dist/crypto.d.ts +14 -0
- package/dist/crypto.js +61 -0
- package/dist/dashboard/Dashboard.d.ts +11 -0
- package/dist/dashboard/Dashboard.js +324 -0
- package/dist/dashboard/styles.d.ts +2 -0
- package/dist/dashboard/styles.js +129 -0
- package/dist/dashboard/ui.d.ts +61 -0
- package/dist/dashboard/ui.js +135 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +29 -0
- package/dist/globals.d.ts +24 -0
- package/dist/globals.js +24 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +48 -0
- package/dist/next/dashboard-page.d.ts +5 -0
- package/dist/next/dashboard-page.js +23 -0
- package/dist/next/plugin-page.d.ts +16 -0
- package/dist/next/plugin-page.js +102 -0
- package/dist/next/slot.d.ts +7 -0
- package/dist/next/slot.js +42 -0
- package/dist/sources/github.d.ts +19 -0
- package/dist/sources/github.js +109 -0
- package/dist/sources/local.d.ts +3 -0
- package/dist/sources/local.js +22 -0
- package/dist/sources/reader.d.ts +17 -0
- package/dist/sources/reader.js +78 -0
- package/dist/storage/file.d.ts +12 -0
- package/dist/storage/file.js +61 -0
- package/dist/storage/memory.d.ts +3 -0
- package/dist/storage/memory.js +22 -0
- package/dist/storage/postgres.d.ts +15 -0
- package/dist/storage/postgres.js +39 -0
- package/dist/storage/types.d.ts +18 -0
- package/dist/storage/types.js +15 -0
- package/dist/storage/upstash.d.ts +15 -0
- package/dist/storage/upstash.js +61 -0
- package/dist/views.d.ts +112 -0
- package/dist/views.js +124 -0
- package/package.json +48 -3
- package/README.md +0 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { assertKey } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Stores everything in Upstash Redis over its REST API (also what Vercel's
|
|
4
|
+
* "Upstash for Redis" marketplace integration provisions). Works on serverless
|
|
5
|
+
* hosts and needs no extra dependency.
|
|
6
|
+
*/
|
|
7
|
+
export function upstashStorage(options = {}) {
|
|
8
|
+
const prefix = options.prefix ?? 'np:';
|
|
9
|
+
const creds = () => {
|
|
10
|
+
const url = options.url ?? process.env.UPSTASH_REDIS_REST_URL ?? process.env.KV_REST_API_URL;
|
|
11
|
+
const token = options.token ?? process.env.UPSTASH_REDIS_REST_TOKEN ?? process.env.KV_REST_API_TOKEN;
|
|
12
|
+
if (!url || !token)
|
|
13
|
+
throw new Error('upstashStorage needs a REST URL and token (set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN).');
|
|
14
|
+
return { url: url.replace(/\/+$/, ''), token };
|
|
15
|
+
};
|
|
16
|
+
async function command(...args) {
|
|
17
|
+
const { url, token } = creds();
|
|
18
|
+
// Next.js wraps `fetch`, and an uncached request through that wrapper makes it refuse to
|
|
19
|
+
// prerender the page being rendered: every static page with a plugin slot in its layout
|
|
20
|
+
// would fail to build or turn dynamic. Storage reads are neither cached by Next.js nor a
|
|
21
|
+
// reason to render per request, so they go through the fetch that Next.js wrapped.
|
|
22
|
+
const send = globalThis.fetch._nextOriginalFetch ?? globalThis.fetch;
|
|
23
|
+
const res = await send(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
|
|
26
|
+
body: JSON.stringify(args),
|
|
27
|
+
cache: 'no-store',
|
|
28
|
+
});
|
|
29
|
+
const body = (await res.json().catch(() => null));
|
|
30
|
+
if (!res.ok || !body || body.error)
|
|
31
|
+
throw new Error(`Upstash ${String(args[0])} failed: ${body?.error ?? `HTTP ${res.status}`}`);
|
|
32
|
+
return body.result;
|
|
33
|
+
}
|
|
34
|
+
// Redis glob characters in a prefix must be escaped for SCAN MATCH.
|
|
35
|
+
const escapeGlob = (s) => s.replace(/[\\*?[\]]/g, '\\$&');
|
|
36
|
+
return {
|
|
37
|
+
async get(key) {
|
|
38
|
+
assertKey(key);
|
|
39
|
+
return command('GET', prefix + key);
|
|
40
|
+
},
|
|
41
|
+
async set(key, value) {
|
|
42
|
+
assertKey(key);
|
|
43
|
+
await command('SET', prefix + key, value);
|
|
44
|
+
},
|
|
45
|
+
async delete(key) {
|
|
46
|
+
assertKey(key);
|
|
47
|
+
await command('DEL', prefix + key);
|
|
48
|
+
},
|
|
49
|
+
async list(keyPrefix) {
|
|
50
|
+
const out = new Set();
|
|
51
|
+
let cursor = '0';
|
|
52
|
+
do {
|
|
53
|
+
const [next, keys] = await command('SCAN', cursor, 'MATCH', `${escapeGlob(prefix + keyPrefix)}*`, 'COUNT', 1000);
|
|
54
|
+
for (const k of keys)
|
|
55
|
+
out.add(k.slice(prefix.length));
|
|
56
|
+
cursor = next;
|
|
57
|
+
} while (cursor !== '0');
|
|
58
|
+
return [...out].sort();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/views.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { SettingField } from '@nextpluggable/plugin/manifest';
|
|
2
|
+
import type { AdminUser } from './auth/types.js';
|
|
3
|
+
import type { ResolvedConfig } from './config.js';
|
|
4
|
+
export interface SessionView {
|
|
5
|
+
user: AdminUser | null;
|
|
6
|
+
mode: 'password' | 'custom';
|
|
7
|
+
/** False when no password/secret has been set up yet. */
|
|
8
|
+
configured: boolean;
|
|
9
|
+
loginUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PageView {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
path: string;
|
|
16
|
+
defaultPath: string;
|
|
17
|
+
/** Whether the owner may change the address. */
|
|
18
|
+
configurable: boolean;
|
|
19
|
+
/** False for pattern pages such as /shop/:slug, which cannot be opened directly. */
|
|
20
|
+
linkable: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface PluginView {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
author?: string;
|
|
28
|
+
icon?: string;
|
|
29
|
+
category?: string;
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
needsSetup: boolean;
|
|
32
|
+
pages: PageView[];
|
|
33
|
+
slots: string[];
|
|
34
|
+
adminPages: {
|
|
35
|
+
id: string;
|
|
36
|
+
title: string;
|
|
37
|
+
component: string;
|
|
38
|
+
}[];
|
|
39
|
+
settings: SettingField[];
|
|
40
|
+
/** Current values of non-secret settings. */
|
|
41
|
+
values: Record<string, string | number | boolean>;
|
|
42
|
+
/** Which secret settings have a stored value. */
|
|
43
|
+
secretsSet: Record<string, boolean>;
|
|
44
|
+
permissions: string[];
|
|
45
|
+
hasServer: boolean;
|
|
46
|
+
source: {
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
connected: boolean;
|
|
50
|
+
};
|
|
51
|
+
update: {
|
|
52
|
+
version: string;
|
|
53
|
+
} | null;
|
|
54
|
+
installedAt: string;
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
/** What the dashboard needs to render the plugin's own admin screens. */
|
|
57
|
+
runtime: {
|
|
58
|
+
moduleKey: string;
|
|
59
|
+
scriptSrc: string;
|
|
60
|
+
styleHref?: string;
|
|
61
|
+
apiBase: string;
|
|
62
|
+
publicConfig: Record<string, unknown>;
|
|
63
|
+
paths: Record<string, string>;
|
|
64
|
+
} | null;
|
|
65
|
+
}
|
|
66
|
+
export interface CatalogPluginView {
|
|
67
|
+
sourceId: string;
|
|
68
|
+
sourceLabel: string;
|
|
69
|
+
path: string;
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
version: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
author?: string;
|
|
75
|
+
icon?: string;
|
|
76
|
+
category?: string;
|
|
77
|
+
pages: {
|
|
78
|
+
title: string;
|
|
79
|
+
path: string;
|
|
80
|
+
}[];
|
|
81
|
+
slots: string[];
|
|
82
|
+
adminPages: string[];
|
|
83
|
+
permissions: string[];
|
|
84
|
+
hasServer: boolean;
|
|
85
|
+
installed: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface SourceView {
|
|
88
|
+
id: string;
|
|
89
|
+
type: 'github' | 'local';
|
|
90
|
+
label: string;
|
|
91
|
+
url?: string;
|
|
92
|
+
hasToken: boolean;
|
|
93
|
+
addedAt: string;
|
|
94
|
+
checkedAt?: string;
|
|
95
|
+
name?: string;
|
|
96
|
+
pluginCount: number;
|
|
97
|
+
problems: {
|
|
98
|
+
path: string;
|
|
99
|
+
message: string;
|
|
100
|
+
}[];
|
|
101
|
+
}
|
|
102
|
+
export interface DashboardState {
|
|
103
|
+
plugins: PluginView[];
|
|
104
|
+
catalog: CatalogPluginView[];
|
|
105
|
+
sources: SourceView[];
|
|
106
|
+
site: {
|
|
107
|
+
name: string;
|
|
108
|
+
url: string;
|
|
109
|
+
allowLocalSources: boolean;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export declare function buildDashboardState(cfg: ResolvedConfig): Promise<DashboardState>;
|
package/dist/views.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes the dashboard UI receives. Built on the server so that nothing
|
|
3
|
+
* sensitive (tokens, secrets, server-only settings' internals) reaches the browser by accident.
|
|
4
|
+
*/
|
|
5
|
+
import { isStaticPath } from '@nextpluggable/plugin/manifest';
|
|
6
|
+
import { isEncrypted } from './crypto.js';
|
|
7
|
+
import { needsSetup } from './core/installer.js';
|
|
8
|
+
import { effectivePaths } from './core/routes.js';
|
|
9
|
+
import { assetUrl, clientModuleKey, publicConfig } from './core/runtime.js';
|
|
10
|
+
import { getCatalog, getInstalls, getSources } from './core/store.js';
|
|
11
|
+
function pluginView(cfg, install, catalogs, connected) {
|
|
12
|
+
const m = install.manifest;
|
|
13
|
+
const paths = effectivePaths(install);
|
|
14
|
+
const values = {};
|
|
15
|
+
const secretsSet = {};
|
|
16
|
+
for (const f of m.settings ?? []) {
|
|
17
|
+
const v = install.config[f.key];
|
|
18
|
+
if (f.type === 'secret')
|
|
19
|
+
secretsSet[f.key] = isEncrypted(v);
|
|
20
|
+
else if (v !== undefined)
|
|
21
|
+
values[f.key] = v;
|
|
22
|
+
}
|
|
23
|
+
const offered = catalogs.get(install.source.id)?.plugins.find((p) => p.build.manifest.id === install.id);
|
|
24
|
+
const moduleKey = clientModuleKey(install);
|
|
25
|
+
return {
|
|
26
|
+
id: install.id,
|
|
27
|
+
name: m.name,
|
|
28
|
+
version: m.version,
|
|
29
|
+
description: m.description,
|
|
30
|
+
author: m.author,
|
|
31
|
+
icon: install.icon,
|
|
32
|
+
category: m.category,
|
|
33
|
+
enabled: install.enabled,
|
|
34
|
+
needsSetup: needsSetup(m, install.config),
|
|
35
|
+
pages: (m.pages ?? []).map((p) => ({
|
|
36
|
+
id: p.id,
|
|
37
|
+
title: p.title,
|
|
38
|
+
description: p.description,
|
|
39
|
+
path: paths[p.id],
|
|
40
|
+
defaultPath: p.path,
|
|
41
|
+
configurable: p.configurablePath !== false && isStaticPath(p.path),
|
|
42
|
+
linkable: isStaticPath(paths[p.id]),
|
|
43
|
+
})),
|
|
44
|
+
slots: [...new Set((m.slots ?? []).map((s) => s.slot))],
|
|
45
|
+
adminPages: (m.admin ?? []).map((a) => ({ id: a.id, title: a.title, component: a.component })),
|
|
46
|
+
settings: m.settings ?? [],
|
|
47
|
+
values,
|
|
48
|
+
secretsSet,
|
|
49
|
+
permissions: m.permissions ?? [],
|
|
50
|
+
hasServer: Boolean(install.files.server),
|
|
51
|
+
source: { id: install.source.id, label: install.source.label, connected: connected.has(install.source.id) },
|
|
52
|
+
update: offered && offered.buildHash !== install.buildHash ? { version: offered.build.manifest.version } : null,
|
|
53
|
+
installedAt: install.installedAt,
|
|
54
|
+
updatedAt: install.updatedAt,
|
|
55
|
+
runtime: moduleKey && install.files.client
|
|
56
|
+
? {
|
|
57
|
+
moduleKey,
|
|
58
|
+
scriptSrc: assetUrl(cfg, install.id, install.files.client.sha256, 'client.js'),
|
|
59
|
+
styleHref: install.files.style ? assetUrl(cfg, install.id, install.files.style.sha256, 'style.css') : undefined,
|
|
60
|
+
apiBase: `${cfg.apiPath}/p/${install.id}`,
|
|
61
|
+
publicConfig: publicConfig(install),
|
|
62
|
+
paths,
|
|
63
|
+
}
|
|
64
|
+
: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export async function buildDashboardState(cfg) {
|
|
68
|
+
const [sources, installs] = await Promise.all([getSources(cfg.storage), getInstalls(cfg.storage)]);
|
|
69
|
+
const catalogs = new Map();
|
|
70
|
+
await Promise.all(sources.map(async (s) => {
|
|
71
|
+
const c = await getCatalog(cfg.storage, s.id);
|
|
72
|
+
if (c)
|
|
73
|
+
catalogs.set(s.id, c);
|
|
74
|
+
}));
|
|
75
|
+
const connected = new Set(sources.map((s) => s.id));
|
|
76
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
77
|
+
const catalog = [];
|
|
78
|
+
for (const s of sources)
|
|
79
|
+
for (const entry of catalogs.get(s.id)?.plugins ?? []) {
|
|
80
|
+
const m = entry.build.manifest;
|
|
81
|
+
catalog.push({
|
|
82
|
+
sourceId: s.id,
|
|
83
|
+
sourceLabel: s.label,
|
|
84
|
+
path: entry.path,
|
|
85
|
+
id: m.id,
|
|
86
|
+
name: m.name,
|
|
87
|
+
version: m.version,
|
|
88
|
+
description: m.description,
|
|
89
|
+
author: m.author,
|
|
90
|
+
icon: entry.build.icon,
|
|
91
|
+
category: m.category,
|
|
92
|
+
pages: (m.pages ?? []).map((p) => ({ title: p.title, path: p.path })),
|
|
93
|
+
slots: [...new Set((m.slots ?? []).map((x) => x.slot))],
|
|
94
|
+
adminPages: (m.admin ?? []).map((a) => a.title),
|
|
95
|
+
permissions: m.permissions ?? [],
|
|
96
|
+
hasServer: Boolean(entry.build.files.server),
|
|
97
|
+
installed: Boolean(installs[m.id]),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
plugins: Object.values(installs)
|
|
102
|
+
.map((i) => pluginView(cfg, i, catalogs, connected))
|
|
103
|
+
.sort(byName),
|
|
104
|
+
catalog: catalog.sort(byName),
|
|
105
|
+
sources: sources.map((s) => {
|
|
106
|
+
const c = catalogs.get(s.id);
|
|
107
|
+
return {
|
|
108
|
+
id: s.id,
|
|
109
|
+
type: s.type,
|
|
110
|
+
label: s.label,
|
|
111
|
+
url: s.github
|
|
112
|
+
? `https://github.com/${s.github.owner}/${s.github.repo}${s.github.dir || s.github.ref ? `/tree/${s.github.ref ?? 'HEAD'}${s.github.dir ? `/${s.github.dir}` : ''}` : ''}`
|
|
113
|
+
: undefined,
|
|
114
|
+
hasToken: Boolean(s.github?.token),
|
|
115
|
+
addedAt: s.addedAt,
|
|
116
|
+
checkedAt: c?.fetchedAt,
|
|
117
|
+
name: c?.name,
|
|
118
|
+
pluginCount: c?.plugins.length ?? 0,
|
|
119
|
+
problems: c?.problems ?? [],
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
122
|
+
site: { name: cfg.siteName, url: cfg.siteUrl, allowLocalSources: cfg.allowLocalSources },
|
|
123
|
+
};
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextpluggable",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WordPress-style plugins for Next.js: a dashboard where site owners install full-featured plugins from GitHub repositories, with no rebuild.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/rohittp0/NextPluggable.git",
|
|
8
|
+
"directory": "packages/nextpluggable"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/rohittp0/NextPluggable#readme",
|
|
11
|
+
"bugs": "https://github.com/rohittp0/NextPluggable/issues",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20.16"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"nextpluggable": "./bin/nextpluggable.js"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"bin"
|
|
31
|
+
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@nextpluggable/plugin": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"next": ">=15.0.0",
|
|
37
|
+
"react": ">=19.0.0",
|
|
38
|
+
"react-dom": ">=19.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^19.2.0",
|
|
42
|
+
"@types/react-dom": "^19.2.0",
|
|
43
|
+
"next": "16.4.6",
|
|
44
|
+
"react": "19.3.1",
|
|
45
|
+
"react-dom": "19.3.1"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
50
|
+
}
|
|
6
51
|
}
|
package/README.md
DELETED