create-nextly-app 0.0.2-alpha.6 → 0.0.2-alpha.60
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/dist/{chunk-6NVRVAZA.mjs → chunk-SEU6FVET.mjs} +2701 -1726
- package/dist/chunk-SEU6FVET.mjs.map +1 -0
- package/dist/cli.cjs +2803 -1831
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +31 -35
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +2620 -1643
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +1 -1
- package/package.json +11 -12
- package/templates/base/.env.example +11 -0
- package/templates/base/AGENTS.md.template +158 -0
- package/templates/base/CLAUDE.md.template +1 -0
- package/templates/base/gitignore +42 -0
- package/templates/base/next.config.ts +11 -17
- package/templates/base/src/app/admin/api/media/[[...path]]/route.ts +26 -0
- package/templates/base/src/app/globals.css +15 -2
- package/templates/base/src/app/layout.tsx +30 -15
- package/templates/base/tsconfig.json +8 -1
- package/templates/blank/src/app/globals.css +37 -1
- package/templates/blank/src/app/layout.tsx +26 -15
- package/templates/plugin/AGENTS.md.template +96 -0
- package/templates/plugin/CLAUDE.md.template +1 -0
- package/templates/plugin/README.md +44 -0
- package/templates/plugin/dev/.env.example +8 -0
- package/templates/plugin/dev/instrumentation.ts +10 -0
- package/templates/plugin/dev/next.config.ts +30 -0
- package/templates/plugin/dev/nextly.config.ts +17 -0
- package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +9 -0
- package/templates/plugin/dev/src/app/admin/api/[[...params]]/route.ts +19 -0
- package/templates/plugin/dev/src/app/layout.tsx +13 -0
- package/templates/plugin/dev/tsconfig.json +22 -0
- package/templates/plugin/eslint.config.mjs +8 -0
- package/templates/plugin/gitignore +10 -0
- package/templates/plugin/src/admin/SettingsPage.tsx +57 -0
- package/templates/plugin/src/admin/index.ts +24 -0
- package/templates/plugin/src/collections/example.ts +13 -0
- package/templates/plugin/src/index.ts +6 -0
- package/templates/plugin/src/plugin.test.ts +28 -0
- package/templates/plugin/src/plugin.ts +48 -0
- package/templates/plugin/src/types.ts +17 -0
- package/templates/plugin/template.json +14 -0
- package/templates/plugin/tsconfig.json +18 -0
- package/templates/plugin/tsup.config.ts +24 -0
- package/templates/plugin/vitest.config.ts +8 -0
- package/dist/chunk-6NVRVAZA.mjs.map +0 -1
- package/templates/blank/src/access/README.md +0 -22
- package/templates/blank/src/collections/README.md +0 -22
- package/templates/blank/src/lib/README.md +0 -15
- package/templates/blank/src/singles/README.md +0 -19
- /package/templates/blank/src/{components → app/(frontend)/components}/README.md +0 -0
- /package/templates/blank/src/{components → app/(frontend)/components}/ThemeToggle.tsx +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<!-- nextly:managed:start -->
|
|
2
|
+
<!--
|
|
3
|
+
Everything between these markers is generated by create-nextly-app and may be
|
|
4
|
+
replaced when the project is upgraded. Write your own notes OUTSIDE the block:
|
|
5
|
+
anything above or below it is yours and is preserved.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Agent guide for {{pluginName}}
|
|
9
|
+
|
|
10
|
+
A [Nextly](https://nextlyhq.com) plugin. This is a publishable **library**, not
|
|
11
|
+
an app: `src/` is what ships, and `dev/` is a throwaway Nextly app that exists
|
|
12
|
+
only so you can exercise the plugin in a real admin.
|
|
13
|
+
|
|
14
|
+
## Where things are
|
|
15
|
+
|
|
16
|
+
| path | what it is |
|
|
17
|
+
| ----------------------- | ---------------------------------------------------------------- |
|
|
18
|
+
| `src/index.ts` | the public entry — React-free, so it stays importable on the server |
|
|
19
|
+
| `src/admin/index.ts` | the `./admin` subpath, where React components are registered |
|
|
20
|
+
| `src/plugin.ts` | the plugin factory: what it contributes and what it does on init |
|
|
21
|
+
| `src/collections/` | collections the plugin contributes to its host |
|
|
22
|
+
| `dev/` | the embedded playground: a minimal Nextly app on SQLite |
|
|
23
|
+
| `dist/` | the build output, and the only thing `npm publish` ships |
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
{{runCommand}} dev # the embedded playground → http://localhost:3000/admin
|
|
29
|
+
{{runCommand}} test # vitest, against a real Nextly on in-memory SQLite
|
|
30
|
+
{{runCommand}} build # tsup → dist/
|
|
31
|
+
{{runCommand}} check-types # tsc --noEmit
|
|
32
|
+
{{runCommand}} lint # eslint
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`{{runCommand}} dev` runs `next dev dev` — Next.js with `dev/` as the project root.
|
|
36
|
+
Editing anything under `src/` reloads the playground, so the loop is the same as
|
|
37
|
+
working on an app.
|
|
38
|
+
|
|
39
|
+
## Things that surprise people
|
|
40
|
+
|
|
41
|
+
- **Plugin APIs come from `@nextlyhq/plugin-sdk`; schema APIs come from
|
|
42
|
+
`nextly/config`.** The SDK is the stability boundary for everything a plugin
|
|
43
|
+
does AS a plugin: `definePlugin`, `registerComponents` and
|
|
44
|
+
`registerKnownPlugin` from `@nextlyhq/plugin-sdk/admin`, `createTestNextly`
|
|
45
|
+
from `@nextlyhq/plugin-sdk/testing`, field factories, `pluginField`,
|
|
46
|
+
`NextlyError`. Collections and singles are defined the same way an app defines
|
|
47
|
+
them, with `defineCollection` from `nextly/config` — which is what
|
|
48
|
+
`src/collections/example.ts` does.
|
|
49
|
+
|
|
50
|
+
**What is never supported is importing `@nextlyhq/admin` directly.** It carries
|
|
51
|
+
no compatibility promise to plugin authors, so an import that compiles today
|
|
52
|
+
breaks on a patch release. If you need something from it that the SDK does not
|
|
53
|
+
re-export, that is worth raising rather than reaching past the boundary.
|
|
54
|
+
|
|
55
|
+
- **The root export must stay React-free.** It is imported by the host's server
|
|
56
|
+
config, so a React import at `src/index.ts` breaks apps that never render your
|
|
57
|
+
admin UI. Components belong on the `./admin` subpath, which is where the host
|
|
58
|
+
looks for them.
|
|
59
|
+
- **Resolve your own slugs through `ctx.self`.** A host can rename a collection
|
|
60
|
+
your plugin contributes; `ctx.self.collections.examples` follows the rename,
|
|
61
|
+
a hardcoded `"examples"` does not.
|
|
62
|
+
- **`dev/` is never published.** `files` is `["dist"]`, so the playground can
|
|
63
|
+
depend on anything it likes without adding weight for consumers.
|
|
64
|
+
- **Peers, not dependencies.** `nextly`, `@nextlyhq/admin`,
|
|
65
|
+
`@nextlyhq/plugin-sdk` and React are peer dependencies — the host supplies
|
|
66
|
+
them, and bundling a second copy is what causes the "two Reacts" class of
|
|
67
|
+
failure.
|
|
68
|
+
|
|
69
|
+
## Testing
|
|
70
|
+
|
|
71
|
+
`createTestNextly` from `@nextlyhq/plugin-sdk/testing` boots a real Nextly on
|
|
72
|
+
in-memory SQLite with your plugin registered, so a test exercises the same
|
|
73
|
+
lifecycle a host would run:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const t = await createTestNextly({ plugins: [myPlugin()] });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Pass `plugins` alone.** Registration auto-syncs the tables for contributed
|
|
80
|
+
collections, so passing the same collections again as `collections` registers
|
|
81
|
+
those slugs a second time as code-owned entities and boot fails with
|
|
82
|
+
`NEXTLY_SCHEMA_SLUG_COLLISION`.
|
|
83
|
+
|
|
84
|
+
Assert on behaviour through `t.nextly` rather than on the plugin object: what a
|
|
85
|
+
host observes is the contract, and the object shape is not.
|
|
86
|
+
|
|
87
|
+
## Conventions worth keeping
|
|
88
|
+
|
|
89
|
+
- Version `nextly` compatibility in the factory's `nextly` range, and widen it
|
|
90
|
+
deliberately rather than by default.
|
|
91
|
+
- Clean up in `destroy()`. Event subscriptions are dropped for you on registry
|
|
92
|
+
reset; timers, watchers and connections are not.
|
|
93
|
+
- Keep `contributes` declarative. Work belongs in `init()`, where the context is
|
|
94
|
+
available and failures are attributable to your plugin.
|
|
95
|
+
|
|
96
|
+
<!-- nextly:managed:end -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# {{pluginName}}
|
|
2
|
+
|
|
3
|
+
A [Nextly](https://nextlyhq.com) plugin.
|
|
4
|
+
|
|
5
|
+
## Develop
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev # runs the embedded /dev playground → open http://localhost:3000/admin
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Your plugin lives in `src/`. The `dev/` folder is a minimal Nextly app on SQLite
|
|
13
|
+
that registers this plugin so you can exercise it in a real admin with hot-reload.
|
|
14
|
+
Editing files under `src/` reloads the playground. **`dev/` is never published.**
|
|
15
|
+
|
|
16
|
+
## Test
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm test # boots a real Nextly on in-memory SQLite via createTestNextly
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Build & publish
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm build # tsup → dist/
|
|
26
|
+
npm publish
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Only `dist/` ships (see `files` in `package.json`). The `nextly-plugin` keyword
|
|
30
|
+
makes this package discoverable.
|
|
31
|
+
|
|
32
|
+
## Use in an app
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// nextly.config.ts
|
|
36
|
+
import { defineConfig } from "nextly";
|
|
37
|
+
import { myPlugin } from "{{pluginName}}";
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
plugins: [myPlugin()],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
See the [Nextly plugin author guide](https://nextlyhq.com/docs/plugins/author-guide).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Dev playground env. Copy to dev/.env and run `pnpm dev`.
|
|
2
|
+
DB_DIALECT=sqlite
|
|
3
|
+
DATABASE_URL=file:./nextly-dev.db
|
|
4
|
+
|
|
5
|
+
# Any non-empty secret works for local dev (sign/verify session cookies).
|
|
6
|
+
NEXTLY_SECRET=dev-secret-change-me
|
|
7
|
+
|
|
8
|
+
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Warm up Nextly once per worker at startup (Node.js runtime only) so admin/API
|
|
2
|
+
// handlers never run before init and schema boot-apply runs once, not per request.
|
|
3
|
+
// Imported dynamically + guarded so nextly stays out of the Edge bundle.
|
|
4
|
+
export async function register(): Promise<void> {
|
|
5
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
6
|
+
|
|
7
|
+
const { createRegister } = await import("nextly");
|
|
8
|
+
const { default: config } = await import("./nextly.config");
|
|
9
|
+
await createRegister(config)();
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
// Dev playground for the plugin. The plugin's own source (../src) is aliased and
|
|
4
|
+
// transpiled so editing it hot-reloads. `nextly` + the admin come from
|
|
5
|
+
// node_modules (real deps). Node-only / dynamically-imported packages stay
|
|
6
|
+
// external (Turbopack can't bundle them).
|
|
7
|
+
const nextConfig: NextConfig = {
|
|
8
|
+
typescript: { ignoreBuildErrors: true },
|
|
9
|
+
// Transpile the aliased plugin source so HMR fires on src/ edits.
|
|
10
|
+
transpilePackages: ["{{pluginName}}"],
|
|
11
|
+
serverExternalPackages: [
|
|
12
|
+
"better-sqlite3",
|
|
13
|
+
"drizzle-orm",
|
|
14
|
+
"drizzle-kit",
|
|
15
|
+
"esbuild",
|
|
16
|
+
"bundle-require",
|
|
17
|
+
"sharp",
|
|
18
|
+
"@nextlyhq/adapter-drizzle",
|
|
19
|
+
"@nextlyhq/adapter-sqlite",
|
|
20
|
+
],
|
|
21
|
+
turbopack: {
|
|
22
|
+
resolveAlias: {
|
|
23
|
+
// Map the package name to its source so HMR works without a build step.
|
|
24
|
+
"{{pluginName}}": ["./../src/index.ts"],
|
|
25
|
+
"{{pluginName}}/admin": ["./../src/admin/index.ts"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default nextConfig;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev playground config — a minimal Nextly app that registers THIS plugin so you
|
|
3
|
+
* can exercise it in a real admin. SQLite, dev auto-login. Never published.
|
|
4
|
+
*/
|
|
5
|
+
import { defineConfig } from "nextly/config";
|
|
6
|
+
import { myPlugin } from "{{pluginName}}";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
admin: {
|
|
10
|
+
// Land on /admin already logged in. Hard-blocked in production by Nextly.
|
|
11
|
+
devAutoLogin: {
|
|
12
|
+
email: "dev@nextly.local",
|
|
13
|
+
password: "DevPassword123!",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
plugins: [myPlugin()],
|
|
17
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- framework adapter passes through Next.js App Router context */
|
|
2
|
+
/**
|
|
3
|
+
* Admin API catch-all. Routes every /admin/api/* request (incl. your plugin's
|
|
4
|
+
* /api/plugins/<name>/… routes) through the handler built from nextly.config.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { createDynamicHandlers } from "nextly/runtime";
|
|
7
|
+
|
|
8
|
+
import nextlyConfig from "../../../../../nextly.config";
|
|
9
|
+
|
|
10
|
+
const handlers = createDynamicHandlers({ config: nextlyConfig });
|
|
11
|
+
|
|
12
|
+
export const GET = (req: Request, context: any) => handlers.GET(req, context);
|
|
13
|
+
export const POST = (req: Request, context: any) => handlers.POST(req, context);
|
|
14
|
+
export const PUT = (req: Request, context: any) => handlers.PUT(req, context);
|
|
15
|
+
export const PATCH = (req: Request, context: any) =>
|
|
16
|
+
handlers.PATCH(req, context);
|
|
17
|
+
export const DELETE = (req: Request, context: any) =>
|
|
18
|
+
handlers.DELETE(req, context);
|
|
19
|
+
export const OPTIONS = (req: Request) => handlers.OPTIONS(req);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "{{pluginName}} — dev playground",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<body>{children}</body>
|
|
11
|
+
</html>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"plugins": [{ "name": "next" }],
|
|
15
|
+
"paths": {
|
|
16
|
+
"{{pluginName}}": ["../src/index.ts"],
|
|
17
|
+
"{{pluginName}}/admin": ["../src/admin/index.ts"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
21
|
+
"exclude": ["node_modules"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example plugin settings page, rendered at `/admin/plugins/<slug>` and wrapped in an
|
|
3
|
+
* error boundary, so a mistake here cannot take the admin down with it.
|
|
4
|
+
*
|
|
5
|
+
* Every class here is a design token utility, so this page follows the admin's
|
|
6
|
+
* theme and its light/dark modes with no styling work of its own. Build your
|
|
7
|
+
* real settings UI the same way.
|
|
8
|
+
*
|
|
9
|
+
* Third-party plugins are not part of the admin's Tailwind `@source` scan, so
|
|
10
|
+
* only the curated utilities in `plugin-safelist.css` are guaranteed to be
|
|
11
|
+
* compiled — layout, spacing, type, and the token-mapped colours used below.
|
|
12
|
+
* Anything outside that set belongs in the stylesheet your plugin declares as
|
|
13
|
+
* `adminStyles`, written against the `--nx-*` custom properties.
|
|
14
|
+
*/
|
|
15
|
+
export function SettingsPage() {
|
|
16
|
+
return (
|
|
17
|
+
<div className="w-full max-w-2xl space-y-6 p-6">
|
|
18
|
+
<header className="space-y-1">
|
|
19
|
+
<h1 className="text-xl font-semibold tracking-tight text-foreground">
|
|
20
|
+
Plugin Settings
|
|
21
|
+
</h1>
|
|
22
|
+
<p className="text-sm text-muted-foreground">
|
|
23
|
+
Settings UI goes here. Edit{" "}
|
|
24
|
+
<span className="font-mono text-xs">src/admin/SettingsPage.tsx</span>.
|
|
25
|
+
</p>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<section className="space-y-4 rounded-lg border border-border bg-card p-6">
|
|
29
|
+
<div className="space-y-1">
|
|
30
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
31
|
+
Where to put your fields
|
|
32
|
+
</h2>
|
|
33
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
34
|
+
Replace this card with your own form. Group related settings under a
|
|
35
|
+
heading, and keep one save action for the page rather than one per
|
|
36
|
+
group.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<ul className="space-y-2 text-sm text-muted-foreground">
|
|
41
|
+
<li>
|
|
42
|
+
Colours come from tokens —{" "}
|
|
43
|
+
<span className="font-mono text-xs">bg-card</span>,{" "}
|
|
44
|
+
<span className="font-mono text-xs">text-muted-foreground</span>,{" "}
|
|
45
|
+
<span className="font-mono text-xs">border-border</span> — never
|
|
46
|
+
from a fixed palette shade, which would keep its light-mode
|
|
47
|
+
appearance in dark mode.
|
|
48
|
+
</li>
|
|
49
|
+
<li>
|
|
50
|
+
Spacing and radius come from the same scales the rest of the admin
|
|
51
|
+
uses, so your page stays aligned with it when those move.
|
|
52
|
+
</li>
|
|
53
|
+
</ul>
|
|
54
|
+
</section>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin component registration (D19/D60). Importing this module registers the
|
|
3
|
+
* React components referenced by `contributes.admin` string paths so the host
|
|
4
|
+
* admin can resolve them. The host's generated import map imports this for you;
|
|
5
|
+
* the eager + lazy registration below is the fallback.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
registerComponents,
|
|
9
|
+
registerKnownPlugin,
|
|
10
|
+
} from "@nextlyhq/plugin-sdk/admin";
|
|
11
|
+
|
|
12
|
+
import { SettingsPage } from "./SettingsPage";
|
|
13
|
+
|
|
14
|
+
const SETTINGS_PATH = "{{pluginName}}/admin#SettingsPage";
|
|
15
|
+
|
|
16
|
+
// Eager: register on module load.
|
|
17
|
+
registerComponents({ [SETTINGS_PATH]: SettingsPage });
|
|
18
|
+
|
|
19
|
+
// Lazy fallback: the host can trigger registration on demand by package prefix.
|
|
20
|
+
registerKnownPlugin("{{pluginName}}", async () => {
|
|
21
|
+
registerComponents({ [SETTINGS_PATH]: SettingsPage });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export { SettingsPage };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineCollection, text, textarea } from "nextly/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An example plugin-owned collection. Replace or remove it with your own.
|
|
5
|
+
* Plugin collections are merged into the host schema automatically (D3/D12)
|
|
6
|
+
* and get a real table on `nextly migrate` / boot.
|
|
7
|
+
*/
|
|
8
|
+
export const Examples = defineCollection({
|
|
9
|
+
slug: "examples",
|
|
10
|
+
labels: { singular: "Example", plural: "Examples" },
|
|
11
|
+
fields: [text({ name: "title", required: true }), textarea({ name: "body" })],
|
|
12
|
+
admin: { useAsTitle: "title" },
|
|
13
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createTestNextly } from "@nextlyhq/plugin-sdk/testing";
|
|
2
|
+
import { afterEach, beforeEach, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { myPlugin } from "./index";
|
|
5
|
+
|
|
6
|
+
let t: Awaited<ReturnType<typeof createTestNextly>>;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
// `plugins` alone. Runtime auto-sync creates the tables for contributed
|
|
10
|
+
// collections during registration, so passing them again as `collections`
|
|
11
|
+
// registers the same slugs as code-owned entities and boot fails with
|
|
12
|
+
// NEXTLY_SCHEMA_SLUG_COLLISION.
|
|
13
|
+
t = await createTestNextly({ plugins: [myPlugin()] });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await t.destroy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("boots and exposes the plugin's example collection (real table)", async () => {
|
|
21
|
+
// Creating an entry proves the contributed collection got a real table and
|
|
22
|
+
// the plugin's lifecycle ran. Replace with assertions for your own behavior.
|
|
23
|
+
const created = await t.nextly.create("examples", { title: "First example" });
|
|
24
|
+
expect(created.item.id).toBeDefined();
|
|
25
|
+
|
|
26
|
+
const fetched = await t.nextly.findById("examples", created.item.id);
|
|
27
|
+
expect(fetched?.title).toBe("First example");
|
|
28
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { definePlugin } from "@nextlyhq/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
import { Examples } from "./collections/example";
|
|
4
|
+
import type { MyPluginOptions } from "./types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Your plugin factory. Call it in a host app's `defineConfig({ plugins: [...] })`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { myPlugin } from "{{pluginName}}";
|
|
12
|
+
* export default defineConfig({ plugins: [myPlugin({ greeting: "Hi" })] });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const myPlugin = (opts: MyPluginOptions = {}) =>
|
|
16
|
+
definePlugin({
|
|
17
|
+
name: "{{pluginName}}",
|
|
18
|
+
version: "0.1.0",
|
|
19
|
+
nextly: "{{nextlyRange}}",
|
|
20
|
+
enabled: opts.enabled,
|
|
21
|
+
contributes: {
|
|
22
|
+
collections: [Examples],
|
|
23
|
+
permissions: [
|
|
24
|
+
{ action: "manage", resource: "examples", label: "Manage Examples" },
|
|
25
|
+
],
|
|
26
|
+
admin: {
|
|
27
|
+
menu: [
|
|
28
|
+
{
|
|
29
|
+
label: "Examples",
|
|
30
|
+
to: "/admin/collections/examples",
|
|
31
|
+
icon: "Sparkles",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
settings: { component: "{{pluginName}}/admin#SettingsPage" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
init(ctx) {
|
|
38
|
+
const greeting = opts.greeting ?? "Hello";
|
|
39
|
+
// Resolve your own slug via ctx.self so this keeps working if the host
|
|
40
|
+
// renames the collection (D54). React to the post-commit create event.
|
|
41
|
+
ctx.events.on(`collection.${ctx.self.collections.examples}.created`, () =>
|
|
42
|
+
ctx.logger.info(`${greeting} from {{pluginName}} — example created`)
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
destroy() {
|
|
46
|
+
// Clean up here (subscriptions are auto-dropped on registry reset).
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options accepted by your plugin factory. These are JSDoc-documented so editors
|
|
3
|
+
* surface them at the call site (D44).
|
|
4
|
+
*/
|
|
5
|
+
export interface MyPluginOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Greeting prefix used by the example collection's `afterCreate` hook.
|
|
8
|
+
* @default "Hello"
|
|
9
|
+
*/
|
|
10
|
+
greeting?: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Disable the plugin's behavior (init/hooks/events/routes/admin) while still
|
|
14
|
+
* applying its schema contributions. Default `true`.
|
|
15
|
+
*/
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plugin",
|
|
3
|
+
"label": "Plugin",
|
|
4
|
+
"description": "Build a reusable Nextly plugin (publishable npm package)",
|
|
5
|
+
"hint": "Plugin package + embedded /dev playground; no schema approach",
|
|
6
|
+
"approaches": [],
|
|
7
|
+
"defaultApproach": null,
|
|
8
|
+
"collections": [],
|
|
9
|
+
"singles": [],
|
|
10
|
+
"hasDemoData": false,
|
|
11
|
+
"hasFrontendPages": false,
|
|
12
|
+
"recommendedDatabase": "sqlite",
|
|
13
|
+
"release": "alpha"
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"verbatimModuleSyntax": false
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "dev"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
// Builds the publishable plugin. `dev/` is NOT an entry — it never ships.
|
|
4
|
+
// nextly / admin / sdk / ui / react are peers, kept external: bundling one
|
|
5
|
+
// would ship a second copy of code the host already provides.
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
entry: ["src/index.ts", "src/admin/index.ts"],
|
|
8
|
+
format: ["esm"],
|
|
9
|
+
dts: true,
|
|
10
|
+
clean: true,
|
|
11
|
+
sourcemap: true,
|
|
12
|
+
treeshake: true,
|
|
13
|
+
external: [
|
|
14
|
+
"nextly",
|
|
15
|
+
"@nextlyhq/admin",
|
|
16
|
+
"@nextlyhq/plugin-sdk",
|
|
17
|
+
"@nextlyhq/ui",
|
|
18
|
+
"react",
|
|
19
|
+
"react-dom",
|
|
20
|
+
],
|
|
21
|
+
outExtension() {
|
|
22
|
+
return { js: ".mjs" };
|
|
23
|
+
},
|
|
24
|
+
});
|