@nextpluggable/plugin 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,348 @@
1
+ /**
2
+ * The plugin manifest (`plugin.json`) — the contract between a plugin and any host.
3
+ * Everything a non-technical site owner sees about a plugin (name, what it adds,
4
+ * its settings form, what it is allowed to do) is derived from this file.
5
+ */
6
+ export const MANIFEST_VERSION = 1;
7
+ const ID_RE = /^[a-z][a-z0-9-]{1,48}[a-z0-9]$/;
8
+ const KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
9
+ const COMPONENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
10
+ const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
11
+ const SLOT_RE = /^[a-z][a-z0-9.-]{0,63}$/;
12
+ /** Names that collide with built-in object properties; refusing them keeps every lookup by id/key safe. */
13
+ const RESERVED = new Set(['constructor', 'prototype', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'valueOf']);
14
+ const isKey = (v) => typeof v === 'string' && KEY_RE.test(v) && !RESERVED.has(v);
15
+ const SETTING_TYPES = [
16
+ 'text',
17
+ 'textarea',
18
+ 'number',
19
+ 'toggle',
20
+ 'select',
21
+ 'color',
22
+ 'url',
23
+ 'email',
24
+ 'secret',
25
+ ];
26
+ export class ManifestError extends Error {
27
+ problems;
28
+ constructor(problems) {
29
+ super(`Invalid plugin manifest:\n- ${problems.join('\n- ')}`);
30
+ this.problems = problems;
31
+ this.name = 'ManifestError';
32
+ }
33
+ }
34
+ /** Validate a page path pattern. Returns a problem description, or null when fine. */
35
+ export function checkPagePath(path) {
36
+ if (typeof path !== 'string' || !path.startsWith('/'))
37
+ return 'must start with "/"';
38
+ if (path.length > 200)
39
+ return 'is too long';
40
+ if (path !== '/' && path.endsWith('/'))
41
+ return 'must not end with "/"';
42
+ if (path === '/')
43
+ return 'cannot be the home page';
44
+ const segments = path.slice(1).split('/');
45
+ for (let i = 0; i < segments.length; i++) {
46
+ const seg = segments[i];
47
+ if (seg === '*') {
48
+ if (i !== segments.length - 1)
49
+ return '"*" is only allowed as the last segment';
50
+ continue;
51
+ }
52
+ if (seg.startsWith(':')) {
53
+ if (!KEY_RE.test(seg.slice(1)))
54
+ return `has an invalid parameter name "${seg}"`;
55
+ continue;
56
+ }
57
+ if (!/^[A-Za-z0-9._~-]+$/.test(seg) || seg === '.' || seg === '..')
58
+ return `has an invalid segment "${seg}" (use letters, digits, dashes)`;
59
+ }
60
+ return null;
61
+ }
62
+ export function isStaticPath(path) {
63
+ return !path.split('/').some((s) => s === '*' || s.startsWith(':'));
64
+ }
65
+ function isRecord(v) {
66
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
67
+ }
68
+ /**
69
+ * Validate an untrusted value as a manifest. Throws ManifestError listing every
70
+ * problem in words a plugin author can act on.
71
+ */
72
+ export function validateManifest(input) {
73
+ const p = [];
74
+ if (!isRecord(input))
75
+ throw new ManifestError(['manifest must be a JSON object']);
76
+ const m = input;
77
+ if (m.manifestVersion !== MANIFEST_VERSION)
78
+ p.push(`"manifestVersion" must be ${MANIFEST_VERSION}`);
79
+ if (typeof m.id !== 'string' || !ID_RE.test(m.id) || RESERVED.has(m.id))
80
+ p.push('"id" must be 3-50 characters: lowercase letters, digits and dashes, starting with a letter');
81
+ if (typeof m.name !== 'string' || !m.name.trim() || m.name.length > 80)
82
+ p.push('"name" is required (max 80 characters)');
83
+ if (typeof m.version !== 'string' || !VERSION_RE.test(m.version))
84
+ p.push('"version" must look like 1.2.3');
85
+ for (const k of ['description', 'author', 'homepage', 'icon', 'category']) {
86
+ if (m[k] !== undefined && typeof m[k] !== 'string')
87
+ p.push(`"${k}" must be text`);
88
+ }
89
+ if (typeof m.description === 'string' && m.description.length > 500)
90
+ p.push('"description" is too long (max 500 characters)');
91
+ if (m.entry !== undefined) {
92
+ if (!isRecord(m.entry))
93
+ p.push('"entry" must be an object');
94
+ else
95
+ for (const k of ['client', 'server'])
96
+ if (m.entry[k] !== undefined && typeof m.entry[k] !== 'string')
97
+ p.push(`"entry.${k}" must be a file path`);
98
+ }
99
+ const pageIds = new Set();
100
+ const pagePaths = new Set();
101
+ if (m.pages !== undefined) {
102
+ if (!Array.isArray(m.pages))
103
+ p.push('"pages" must be a list');
104
+ else
105
+ m.pages.forEach((page, i) => {
106
+ const at = `pages[${i}]`;
107
+ if (!isRecord(page))
108
+ return void p.push(`${at} must be an object`);
109
+ if (!isKey(page.id))
110
+ p.push(`${at}.id must be a short identifier`);
111
+ else if (pageIds.has(page.id))
112
+ p.push(`${at}.id "${page.id}" is used twice`);
113
+ else
114
+ pageIds.add(page.id);
115
+ const pathProblem = checkPagePath(page.path);
116
+ if (pathProblem)
117
+ p.push(`${at}.path ${pathProblem}`);
118
+ else if (pagePaths.has(page.path))
119
+ p.push(`${at}.path "${String(page.path)}" is used twice`);
120
+ else
121
+ pagePaths.add(page.path);
122
+ if (typeof page.component !== 'string' || !COMPONENT_RE.test(page.component))
123
+ p.push(`${at}.component must be the name of an exported component`);
124
+ if (typeof page.title !== 'string' || !page.title.trim())
125
+ p.push(`${at}.title is required`);
126
+ if (page.description !== undefined && typeof page.description !== 'string')
127
+ p.push(`${at}.description must be text`);
128
+ if (page.configurablePath !== undefined && typeof page.configurablePath !== 'boolean')
129
+ p.push(`${at}.configurablePath must be true or false`);
130
+ });
131
+ }
132
+ if (m.slots !== undefined) {
133
+ if (!Array.isArray(m.slots))
134
+ p.push('"slots" must be a list');
135
+ else
136
+ m.slots.forEach((slot, i) => {
137
+ const at = `slots[${i}]`;
138
+ if (!isRecord(slot))
139
+ return void p.push(`${at} must be an object`);
140
+ if (typeof slot.slot !== 'string' || !SLOT_RE.test(slot.slot))
141
+ p.push(`${at}.slot must be a slot name like "header.actions"`);
142
+ if (typeof slot.component !== 'string' || !COMPONENT_RE.test(slot.component))
143
+ p.push(`${at}.component must be the name of an exported component`);
144
+ if (slot.order !== undefined && typeof slot.order !== 'number')
145
+ p.push(`${at}.order must be a number`);
146
+ });
147
+ }
148
+ if (m.admin !== undefined) {
149
+ const ids = new Set();
150
+ if (!Array.isArray(m.admin))
151
+ p.push('"admin" must be a list');
152
+ else
153
+ m.admin.forEach((a, i) => {
154
+ const at = `admin[${i}]`;
155
+ if (!isRecord(a))
156
+ return void p.push(`${at} must be an object`);
157
+ if (!isKey(a.id))
158
+ p.push(`${at}.id must be a short identifier`);
159
+ else if (ids.has(a.id))
160
+ p.push(`${at}.id "${a.id}" is used twice`);
161
+ else
162
+ ids.add(a.id);
163
+ if (typeof a.title !== 'string' || !a.title.trim())
164
+ p.push(`${at}.title is required`);
165
+ if (typeof a.component !== 'string' || !COMPONENT_RE.test(a.component))
166
+ p.push(`${at}.component must be the name of an exported component`);
167
+ });
168
+ }
169
+ if (m.settings !== undefined) {
170
+ const keys = new Set();
171
+ if (!Array.isArray(m.settings))
172
+ p.push('"settings" must be a list');
173
+ else
174
+ m.settings.forEach((f, i) => {
175
+ const at = `settings[${i}]`;
176
+ if (!isRecord(f))
177
+ return void p.push(`${at} must be an object`);
178
+ if (!isKey(f.key))
179
+ p.push(`${at}.key must be a short identifier`);
180
+ else if (keys.has(f.key))
181
+ p.push(`${at}.key "${f.key}" is used twice`);
182
+ else
183
+ keys.add(f.key);
184
+ if (typeof f.label !== 'string' || !f.label.trim())
185
+ p.push(`${at}.label is required`);
186
+ if (!SETTING_TYPES.includes(f.type))
187
+ p.push(`${at}.type must be one of: ${SETTING_TYPES.join(', ')}`);
188
+ if (f.type === 'select') {
189
+ if (!Array.isArray(f.options) || f.options.length === 0)
190
+ p.push(`${at}.options is required for a select`);
191
+ else if (!f.options.every((o) => isRecord(o) && typeof o.value === 'string' && typeof o.label === 'string'))
192
+ p.push(`${at}.options must be a list of { value, label }`);
193
+ }
194
+ if (f.type === 'secret' && f.default !== undefined)
195
+ p.push(`${at} is a secret and cannot have a default`);
196
+ if (f.default !== undefined) {
197
+ const want = f.type === 'number' ? 'number' : f.type === 'toggle' ? 'boolean' : 'string';
198
+ if (typeof f.default !== want)
199
+ p.push(`${at}.default must be a ${want}`);
200
+ }
201
+ for (const k of ['min', 'max', 'step'])
202
+ if (f[k] !== undefined && typeof f[k] !== 'number')
203
+ p.push(`${at}.${k} must be a number`);
204
+ for (const k of ['help', 'placeholder', 'group'])
205
+ if (f[k] !== undefined && typeof f[k] !== 'string')
206
+ p.push(`${at}.${k} must be text`);
207
+ for (const k of ['required', 'serverOnly'])
208
+ if (f[k] !== undefined && typeof f[k] !== 'boolean')
209
+ p.push(`${at}.${k} must be true or false`);
210
+ });
211
+ }
212
+ if (m.permissions !== undefined) {
213
+ if (!Array.isArray(m.permissions))
214
+ p.push('"permissions" must be a list');
215
+ else
216
+ m.permissions.forEach((perm, i) => {
217
+ const ok = perm === 'storage' ||
218
+ perm === 'email' ||
219
+ (typeof perm === 'string' && /^fetch:(\*|[a-z0-9.-]+(?::\d+)?)$/i.test(perm));
220
+ if (!ok)
221
+ p.push(`permissions[${i}] must be "storage", "email" or "fetch:<host>"`);
222
+ });
223
+ }
224
+ if (p.length)
225
+ throw new ManifestError(p);
226
+ return m;
227
+ }
228
+ /** Default values declared by the manifest's settings. */
229
+ export function defaultConfig(manifest) {
230
+ const out = {};
231
+ for (const f of manifest.settings ?? [])
232
+ if (f.default !== undefined)
233
+ out[f.key] = f.default;
234
+ return out;
235
+ }
236
+ /** Whether a setting's value may be sent to browsers. */
237
+ export function isPublicSetting(field) {
238
+ return field.type !== 'secret' && !field.serverOnly;
239
+ }
240
+ /**
241
+ * Check and normalise values submitted for a plugin's settings.
242
+ * Returns cleaned values plus per-field messages written for a non-technical reader.
243
+ */
244
+ export function validateSettings(manifest, input) {
245
+ const values = {};
246
+ const errors = {};
247
+ for (const f of manifest.settings ?? []) {
248
+ let v = input[f.key];
249
+ if (typeof v === 'string')
250
+ v = f.type === 'textarea' || f.type === 'secret' ? v : v.trim();
251
+ const empty = v === undefined || v === null || v === '';
252
+ if (empty) {
253
+ if (f.required)
254
+ errors[f.key] = `${f.label} is required.`;
255
+ continue;
256
+ }
257
+ switch (f.type) {
258
+ case 'number': {
259
+ const n = typeof v === 'number' ? v : Number(v);
260
+ if (!Number.isFinite(n))
261
+ errors[f.key] = `${f.label} must be a number.`;
262
+ else if (f.min !== undefined && n < f.min)
263
+ errors[f.key] = `${f.label} must be at least ${f.min}.`;
264
+ else if (f.max !== undefined && n > f.max)
265
+ errors[f.key] = `${f.label} must be at most ${f.max}.`;
266
+ else
267
+ values[f.key] = n;
268
+ break;
269
+ }
270
+ case 'toggle':
271
+ if (typeof v !== 'boolean')
272
+ errors[f.key] = `${f.label} must be on or off.`;
273
+ else
274
+ values[f.key] = v;
275
+ break;
276
+ case 'select':
277
+ if (typeof v !== 'string' || !f.options?.some((o) => o.value === v))
278
+ errors[f.key] = `Choose one of the options for ${f.label}.`;
279
+ else
280
+ values[f.key] = v;
281
+ break;
282
+ case 'color':
283
+ if (typeof v !== 'string' || !/^#[0-9a-fA-F]{6}$/.test(v))
284
+ errors[f.key] = `${f.label} must be a colour like #1d4ed8.`;
285
+ else
286
+ values[f.key] = v.toLowerCase();
287
+ break;
288
+ case 'url':
289
+ if (typeof v !== 'string' || !/^https?:\/\/\S+$/i.test(v))
290
+ errors[f.key] = `${f.label} must be a web address starting with http:// or https://.`;
291
+ else
292
+ values[f.key] = v;
293
+ break;
294
+ case 'email':
295
+ if (typeof v !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))
296
+ errors[f.key] = `${f.label} must be an email address.`;
297
+ else
298
+ values[f.key] = v;
299
+ break;
300
+ default:
301
+ if (typeof v !== 'string')
302
+ errors[f.key] = `${f.label} must be text.`;
303
+ else if (v.length > 10_000)
304
+ errors[f.key] = `${f.label} is too long.`;
305
+ else
306
+ values[f.key] = v;
307
+ }
308
+ }
309
+ return { values, errors };
310
+ }
311
+ export function validateRepositoryIndex(input) {
312
+ if (!isRecord(input) || !Array.isArray(input.plugins) || !input.plugins.every((x) => typeof x === 'string'))
313
+ throw new ManifestError(['nextpluggable.json must contain a "plugins" list of directories']);
314
+ const plugins = input.plugins.map((d) => d.replace(/^\.?\/+/, '').replace(/\/+$/, ''));
315
+ if (plugins.some((d) => d.split('/').includes('..')))
316
+ throw new ManifestError(['plugin directories must stay inside the repository']);
317
+ return {
318
+ name: typeof input.name === 'string' ? input.name : undefined,
319
+ description: typeof input.description === 'string' ? input.description : undefined,
320
+ plugins,
321
+ };
322
+ }
323
+ export function validateBuild(input) {
324
+ if (!isRecord(input) || input.buildVersion !== 1)
325
+ throw new ManifestError(['dist/build.json is missing or was made by an incompatible build tool']);
326
+ const manifest = validateManifest(input.manifest);
327
+ const files = {};
328
+ const rawFiles = isRecord(input.files) ? input.files : {};
329
+ for (const k of ['client', 'server', 'style']) {
330
+ const f = rawFiles[k];
331
+ if (f === undefined)
332
+ continue;
333
+ if (!isRecord(f) ||
334
+ typeof f.path !== 'string' ||
335
+ f.path.split('/').includes('..') ||
336
+ f.path.startsWith('/') ||
337
+ typeof f.sha256 !== 'string' ||
338
+ !/^[0-9a-f]{64}$/.test(f.sha256) ||
339
+ typeof f.bytes !== 'number')
340
+ throw new ManifestError([`dist/build.json has a malformed "${k}" file entry`]);
341
+ files[k] = { path: f.path, sha256: f.sha256, bytes: f.bytes };
342
+ }
343
+ const needsClient = (manifest.pages?.length ?? 0) + (manifest.slots?.length ?? 0) + (manifest.admin?.length ?? 0) > 0;
344
+ if (needsClient && !files.client)
345
+ throw new ManifestError(['the plugin declares pages or components but has no built client bundle']);
346
+ const icon = typeof input.icon === 'string' && input.icon.length <= 64_000 ? input.icon : undefined;
347
+ return { buildVersion: 1, manifest, icon, files };
348
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Server-side API for plugin authors. Everything here is built on web
3
+ * standards (Request/Response) so the same plugin can run on any host adapter.
4
+ */
5
+ /** A small key-value store private to the plugin. Values are JSON. */
6
+ export interface PluginStorage {
7
+ get<T = unknown>(key: string): Promise<T | null>;
8
+ set(key: string, value: unknown): Promise<void>;
9
+ delete(key: string): Promise<void>;
10
+ /** Keys that start with `prefix` (all keys when omitted), sorted. */
11
+ list(prefix?: string): Promise<string[]>;
12
+ }
13
+ export interface AdminUser {
14
+ id: string;
15
+ name?: string;
16
+ }
17
+ export interface PluginContext<C = Record<string, unknown>> {
18
+ pluginId: string;
19
+ /** All settings, including server-only values and decrypted secrets. */
20
+ config: C;
21
+ storage: PluginStorage;
22
+ site: {
23
+ /** Origin of the site, e.g. `https://example.com`. */
24
+ url: string;
25
+ };
26
+ /** Addresses of this plugin's pages keyed by page id. */
27
+ paths: Record<string, string>;
28
+ log(...args: unknown[]): void;
29
+ }
30
+ /**
31
+ * What a page loader receives. There is deliberately no query string here:
32
+ * sites cache plugin pages, and a cached page cannot depend on it. Use address
33
+ * parameters (`/shop/:slug`, `/blog/page/:n`) for content search engines should
34
+ * see, and `useQuery()` in the browser for filters and searches.
35
+ */
36
+ export interface PageContext<C = Record<string, unknown>> extends PluginContext<C> {
37
+ path: string;
38
+ params: Record<string, string>;
39
+ }
40
+ /**
41
+ * How a page presents itself to search engines and when shared. Everything is
42
+ * optional: the site fills in whatever is left out (the page's title and
43
+ * description from the manifest, a canonical link to the page itself, the
44
+ * site's own sharing image), so a page is complete without any of this.
45
+ */
46
+ export interface PageMetadata {
47
+ title?: string;
48
+ /** One or two sentences for search results. About 150 characters. */
49
+ description?: string;
50
+ /** Absolute or site-relative canonical URL. Defaults to the page's own address. */
51
+ canonical?: string;
52
+ /** Absolute or site-relative image shown when the page is shared. Defaults to the site's own. */
53
+ image?: string;
54
+ /** What the image shows, for people who cannot see it. Defaults to the title. */
55
+ imageAlt?: string;
56
+ /** `article` for a dated piece of writing (a post, a news item). Default `website`. */
57
+ type?: 'website' | 'article';
58
+ /** For articles: ISO dates of first publication and last change. */
59
+ publishedTime?: string;
60
+ modifiedTime?: string;
61
+ /** Keep the page out of search engines (a thank-you page, a preview). */
62
+ noindex?: boolean;
63
+ }
64
+ /** One address for the site's sitemap: a path, or a path with the ISO date it last changed. */
65
+ export type SitemapEntry = string | {
66
+ path: string;
67
+ lastModified?: string;
68
+ };
69
+ export interface PageResult<P = Record<string, unknown>> {
70
+ /** Passed to the page component as `data`. Must be JSON-serialisable. */
71
+ data?: P;
72
+ metadata?: PageMetadata;
73
+ /** schema.org structured data, rendered as JSON-LD. */
74
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
75
+ /** Respond with the site's 404 page. */
76
+ notFound?: boolean;
77
+ /** Redirect to another address. */
78
+ redirect?: string;
79
+ }
80
+ export interface ApiContext<C = Record<string, unknown>> extends PluginContext<C> {
81
+ request: Request;
82
+ /** Path below the plugin's API base, e.g. `/items/42`. */
83
+ path: string;
84
+ params: Record<string, string>;
85
+ /** The signed-in dashboard user, or null for public visitors. */
86
+ admin: AdminUser | null;
87
+ }
88
+ export type PageLoader<C = Record<string, unknown>> = (ctx: PageContext<C>) => PageResult | Promise<PageResult>;
89
+ export type ApiHandler<C = Record<string, unknown>> = (ctx: ApiContext<C>) => Response | Promise<Response>;
90
+ export interface PluginServer<C = Record<string, unknown>> {
91
+ /** Server loaders keyed by page id: fetch data and SEO metadata before the page renders. */
92
+ pages?: Record<string, PageLoader<C>>;
93
+ /** HTTP endpoints keyed by `"METHOD /path"`, e.g. `"POST /messages"` or `"GET /items/:id"`. */
94
+ api?: Record<string, ApiHandler<C>>;
95
+ /**
96
+ * The addresses of this plugin's pages that have parameters (`/shop/:slug`), for the
97
+ * site's sitemap: one entry per product, article and so on. Pages with a fixed
98
+ * address are listed by the site already. Addresses that do not belong to one of
99
+ * the plugin's pages are ignored.
100
+ */
101
+ sitemap?(ctx: PluginContext<C>): SitemapEntry[] | Promise<SitemapEntry[]>;
102
+ onInstall?(ctx: PluginContext<C>): void | Promise<void>;
103
+ onUninstall?(ctx: PluginContext<C>): void | Promise<void>;
104
+ }
105
+ /** Identity helper that gives the server entry full type checking. */
106
+ export declare function definePluginServer<C = Record<string, unknown>>(server: PluginServer<C>): PluginServer<C>;
107
+ /** Restrict an endpoint to the signed-in dashboard user. */
108
+ export declare function adminOnly<C = Record<string, unknown>>(handler: ApiHandler<C>): ApiHandler<C>;
109
+ export declare function json(body: unknown, status?: number, headers?: Record<string, string>): Response;
110
+ /** Read a JSON request body, returning null when it is missing, malformed or larger than `maxBytes`. */
111
+ export declare function readJson<T = unknown>(request: Request, maxBytes?: number): Promise<T | null>;
package/dist/server.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Server-side API for plugin authors. Everything here is built on web
3
+ * standards (Request/Response) so the same plugin can run on any host adapter.
4
+ */
5
+ /** Identity helper that gives the server entry full type checking. */
6
+ export function definePluginServer(server) {
7
+ return server;
8
+ }
9
+ /** Restrict an endpoint to the signed-in dashboard user. */
10
+ export function adminOnly(handler) {
11
+ return (ctx) => (ctx.admin ? handler(ctx) : json({ error: 'Sign in to the dashboard to do this.' }, 401));
12
+ }
13
+ export function json(body, status = 200, headers) {
14
+ return new Response(JSON.stringify(body), {
15
+ status,
16
+ headers: { 'content-type': 'application/json; charset=utf-8', ...headers },
17
+ });
18
+ }
19
+ /** Read a JSON request body, returning null when it is missing, malformed or larger than `maxBytes`. */
20
+ export async function readJson(request, maxBytes = 100_000) {
21
+ try {
22
+ const text = await request.text();
23
+ if (text.length > maxBytes)
24
+ return null;
25
+ return JSON.parse(text);
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@nextpluggable/plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "SDK and build tool for writing NextPluggable plugins",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/rohittp0/NextPluggable.git",
8
+ "directory": "packages/plugin"
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-plugin": "./bin/nextpluggable-plugin.js"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./server": {
28
+ "types": "./dist/server.d.ts",
29
+ "default": "./dist/server.js"
30
+ },
31
+ "./manifest": {
32
+ "types": "./dist/manifest.d.ts",
33
+ "default": "./dist/manifest.js"
34
+ },
35
+ "./build": {
36
+ "types": "./dist/build.d.ts",
37
+ "default": "./dist/build.js"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "bin"
43
+ ],
44
+ "peerDependencies": {
45
+ "esbuild": ">=0.21",
46
+ "react": ">=18"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "esbuild": {
50
+ "optional": true
51
+ },
52
+ "react": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@types/react": "^19.2.0",
58
+ "esbuild": "^0.28.2",
59
+ "react": "^19.2.0"
60
+ },
61
+ "scripts": {
62
+ "build": "tsc -p tsconfig.json",
63
+ "typecheck": "tsc -p tsconfig.json --noEmit"
64
+ }
6
65
  }