create-nextly-app 0.0.2-alpha.7 → 0.0.2-alpha.71
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 +36 -9
- package/dist/cli.cjs +3047 -32460
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.mjs +3048 -3602
- package/dist/cli.mjs.map +1 -1
- package/dist/create-nextly-5wiGMVI6.mjs +52550 -0
- package/dist/create-nextly-5wiGMVI6.mjs.map +1 -0
- package/dist/create-nextly-BbKGDrEC.cjs +52628 -0
- package/dist/create-nextly-BbKGDrEC.cjs.map +1 -0
- package/dist/index.cjs +3 -24861
- package/dist/index.d.cts +40 -34
- package/dist/index.d.ts +40 -34
- package/dist/index.mjs +1 -6
- package/package.json +28 -19
- package/skills/nextly-authentication/SKILL.md +187 -0
- package/skills/nextly-collections/SKILL.md +215 -0
- package/skills/nextly-deploy/SKILL.md +248 -0
- package/skills/nextly-install/SKILL.md +121 -0
- package/templates/base/.env.example +21 -6
- package/templates/base/AGENTS.md.template +226 -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/[[...params]]/page.tsx +3 -0
- 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 +32 -18
- package/templates/base/src/lib/site-url.ts +55 -0
- package/templates/base/src/types/generated/nextly-types.ts +6 -4
- package/templates/base/src/types/generated/plugin-admin-components.generated.ts +6 -0
- package/templates/base/src/types/generated/plugin-admin-imports.generated.ts +32 -0
- package/templates/base/tsconfig.json +8 -1
- package/templates/blank/src/{components → app/(frontend)/components}/ThemeToggle.tsx +55 -24
- package/templates/blank/src/app/(frontend)/page.tsx +2 -2
- package/templates/blank/src/app/globals.css +37 -1
- package/templates/blank/src/app/layout.tsx +28 -18
- package/templates/blank/src/instrumentation.ts +32 -0
- package/templates/plugin/AGENTS.md.template +125 -0
- package/templates/plugin/CLAUDE.md.template +1 -0
- package/templates/plugin/README.md +48 -0
- package/templates/plugin/dev/.env.example +11 -0
- package/templates/plugin/dev/next.config.ts +43 -0
- package/templates/plugin/dev/nextly.config.ts +17 -0
- package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +24 -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/src/instrumentation.ts +25 -0
- package/templates/plugin/dev/src/seed-dev-user.ts +51 -0
- package/templates/plugin/dev/src/types/generated/plugin-admin-components.generated.ts +8 -0
- package/templates/plugin/dev/src/types/generated/plugin-admin-imports.generated.ts +37 -0
- package/templates/plugin/dev/tsconfig.json +22 -0
- package/templates/plugin/eslint.config.mjs +19 -0
- package/templates/plugin/gitignore +13 -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 +35 -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/tsdown.config.ts +30 -0
- package/templates/plugin/vitest.config.ts +27 -0
- package/dist/chunk-RPGFJSDP.mjs +0 -28835
- package/dist/chunk-RPGFJSDP.mjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.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
|
@@ -0,0 +1,125 @@
|
|
|
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 # tsdown → 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`, field factories, `pluginField` and
|
|
44
|
+
`NextlyError` from `@nextlyhq/plugin-sdk`; `registerComponents` and
|
|
45
|
+
`registerKnownPlugin` from `@nextlyhq/plugin-sdk/admin`; `createTestNextly`
|
|
46
|
+
from `@nextlyhq/plugin-sdk/testing`. 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
|
+
import { createTestNextly } from "@nextlyhq/plugin-sdk/testing";
|
|
77
|
+
|
|
78
|
+
const t = await createTestNextly({ plugins: [myPlugin()] });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Pass `plugins` alone.** Registration auto-syncs the tables for contributed
|
|
82
|
+
collections, so passing the same collections again as `collections` registers
|
|
83
|
+
those slugs a second time as code-owned entities and boot fails with
|
|
84
|
+
`NEXTLY_SCHEMA_SLUG_COLLISION`.
|
|
85
|
+
|
|
86
|
+
Assert on behaviour through `t.nextly` rather than on the plugin object: what a
|
|
87
|
+
host observes is the contract, and the object shape is not.
|
|
88
|
+
|
|
89
|
+
## Which Nextly this is, and where its docs are
|
|
90
|
+
|
|
91
|
+
An agent working here will otherwise answer from whatever it remembers of some
|
|
92
|
+
other version. Ask the project instead:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
{{execCommand}} nextly --version # the running core version
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
That is the same version the plugin resolver validates a plugin's `nextly`
|
|
99
|
+
range against, so a compatibility answer and a reported version cannot
|
|
100
|
+
disagree. The dependency range this project pins is in `package.json` under
|
|
101
|
+
`nextly`; the resolved version is what the command above prints.
|
|
102
|
+
|
|
103
|
+
**The authoritative documentation is <https://nextlyhq.com/docs>.** Two files
|
|
104
|
+
are published for machine readers and are the ones to fetch:
|
|
105
|
+
|
|
106
|
+
| file | what it is |
|
|
107
|
+
| ---- | ---------- |
|
|
108
|
+
| <https://nextlyhq.com/llms.txt> | the index — every documentation page, with a one-line summary |
|
|
109
|
+
| <https://nextlyhq.com/llms-full.txt> | the same documentation as one file |
|
|
110
|
+
|
|
111
|
+
Both are generated from the pages the site publishes, so they cannot describe a
|
|
112
|
+
page that does not exist. Neither is version-stamped: they describe the CURRENT
|
|
113
|
+
release, so check the version above before trusting a detail against an older
|
|
114
|
+
pin.
|
|
115
|
+
|
|
116
|
+
## Conventions worth keeping
|
|
117
|
+
|
|
118
|
+
- Version `nextly` compatibility in the factory's `nextly` range, and widen it
|
|
119
|
+
deliberately rather than by default.
|
|
120
|
+
- Clean up in `destroy()`. Event subscriptions are dropped for you on registry
|
|
121
|
+
reset; timers, watchers and connections are not.
|
|
122
|
+
- Keep `contributes` declarative. Work belongs in `init()`, where the context is
|
|
123
|
+
available and failures are attributable to your plugin.
|
|
124
|
+
|
|
125
|
+
<!-- nextly:managed:end -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
The playground signs you in automatically as `dev@nextly.local` (password
|
|
17
|
+
`DevPassword123!`). That user is created on the first `pnpm dev`, and only
|
|
18
|
+
under `next dev`; production builds ignore the auto-login setting.
|
|
19
|
+
|
|
20
|
+
## Test
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm test # boots a real Nextly on in-memory SQLite via createTestNextly
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Build & publish
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm build # tsdown → dist/
|
|
30
|
+
npm publish
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Only `dist/` ships (see `files` in `package.json`). The `nextly-plugin` keyword
|
|
34
|
+
makes this package discoverable.
|
|
35
|
+
|
|
36
|
+
## Use in an app
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// nextly.config.ts
|
|
40
|
+
import { defineConfig } from "nextly";
|
|
41
|
+
import { myPlugin } from "{{pluginName}}";
|
|
42
|
+
|
|
43
|
+
export default defineConfig({
|
|
44
|
+
plugins: [myPlugin()],
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See the [Nextly plugin author guide](https://nextlyhq.com/docs/plugins/author-guide).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Dev playground env. create-nextly-app writes dev/.env from this file;
|
|
2
|
+
# copy it again to start over.
|
|
3
|
+
DB_DIALECT=sqlite
|
|
4
|
+
# Relative to the plugin root, where `pnpm dev` runs (`next dev dev` keeps
|
|
5
|
+
# that working directory), so the database lands in dev/.
|
|
6
|
+
DATABASE_URL=file:./dev/nextly-dev.db
|
|
7
|
+
|
|
8
|
+
# Any non-empty secret works for local dev (sign/verify session cookies).
|
|
9
|
+
NEXTLY_SECRET=dev-secret-change-me
|
|
10
|
+
|
|
11
|
+
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
// Nextly does not support Cache Components yet; see the installation guide.
|
|
10
|
+
cacheComponents: false,
|
|
11
|
+
// Transpile the aliased plugin source so HMR fires on src/ edits.
|
|
12
|
+
transpilePackages: ["{{pluginName}}"],
|
|
13
|
+
serverExternalPackages: [
|
|
14
|
+
"better-sqlite3",
|
|
15
|
+
"drizzle-orm",
|
|
16
|
+
"drizzle-kit",
|
|
17
|
+
"esbuild",
|
|
18
|
+
"bundle-require",
|
|
19
|
+
"sharp",
|
|
20
|
+
"@nextlyhq/adapter-drizzle",
|
|
21
|
+
"@nextlyhq/adapter-sqlite",
|
|
22
|
+
],
|
|
23
|
+
experimental: {
|
|
24
|
+
// Next.js type-checks and loads its config through the project's own
|
|
25
|
+
// TypeScript CLI rather than TypeScript's library API. That is the 16.4
|
|
26
|
+
// default, stated here so it cannot change underneath: a TypeScript with no
|
|
27
|
+
// library API (7.0) works only this way.
|
|
28
|
+
useTypeScriptCli: true,
|
|
29
|
+
// Turbopack's garbage collector removes cache entries nothing reaches any
|
|
30
|
+
// more, so `.next` stops growing across dev sessions and builds. Off by
|
|
31
|
+
// default in 16.4.
|
|
32
|
+
turbopackGc: true,
|
|
33
|
+
},
|
|
34
|
+
turbopack: {
|
|
35
|
+
resolveAlias: {
|
|
36
|
+
// Map the package name to its source so HMR works without a build step.
|
|
37
|
+
"{{pluginName}}": ["./../src/index.ts"],
|
|
38
|
+
"{{pluginName}}/admin": ["./../src/admin/index.ts"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
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,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import "@nextlyhq/admin/style.css";
|
|
4
|
+
// Registers your plugin's admin components. Generated by
|
|
5
|
+
// `nextly generate:types`; run it again after adding or changing a plugin.
|
|
6
|
+
import "../../../types/generated/plugin-admin-imports.generated";
|
|
7
|
+
import { ErrorBoundary, QueryProvider, RootLayout } from "@nextlyhq/admin";
|
|
8
|
+
|
|
9
|
+
// Mounts the Nextly admin shell. Your plugin's menu/pages/settings appear here.
|
|
10
|
+
// The admin's data hooks read their client from QueryProvider, so RootLayout
|
|
11
|
+
// renders only inside it, as in an app scaffold.
|
|
12
|
+
export default function AdminPage() {
|
|
13
|
+
return (
|
|
14
|
+
<ErrorBoundary
|
|
15
|
+
onError={(error, errorInfo) => {
|
|
16
|
+
console.error("Admin error:", error, errorInfo);
|
|
17
|
+
}}
|
|
18
|
+
>
|
|
19
|
+
<QueryProvider>
|
|
20
|
+
<RootLayout />
|
|
21
|
+
</QueryProvider>
|
|
22
|
+
</ErrorBoundary>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -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,25 @@
|
|
|
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. It sits
|
|
4
|
+
// in src/ beside app/, where Next.js looks for it when an app lives under src/.
|
|
5
|
+
export async function register(): Promise<void> {
|
|
6
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
7
|
+
|
|
8
|
+
const { createRegister, getService } = await import("nextly");
|
|
9
|
+
const { default: config } = await import("../nextly.config");
|
|
10
|
+
// Next.js writes its Cache Components setting into this file as it builds
|
|
11
|
+
// it. Nextly, loaded from node_modules, cannot read that for itself, and
|
|
12
|
+
// warns when Cache Components is on, which it does not support yet.
|
|
13
|
+
await createRegister(config, {
|
|
14
|
+
cacheComponents: process.env.__NEXT_CACHE_COMPONENTS,
|
|
15
|
+
})();
|
|
16
|
+
|
|
17
|
+
// Create the user admin.devAutoLogin signs in as (see seed-dev-user.ts).
|
|
18
|
+
// After boot, because boot seeds every permission and the Super Admin role
|
|
19
|
+
// created here takes all that exist.
|
|
20
|
+
const { seedSuperAdmin } = await import("nextly/database/seeders");
|
|
21
|
+
const { seedDevUser } = await import("./seed-dev-user");
|
|
22
|
+
await seedDevUser(config.admin?.devAutoLogin, options =>
|
|
23
|
+
seedSuperAdmin(getService("adapter"), options)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the account `admin.devAutoLogin` signs in as.
|
|
3
|
+
*
|
|
4
|
+
* Auto-login signs in a user that already exists and never creates one, so
|
|
5
|
+
* without this the first /admin visit lands on the setup wizard. The seeder is
|
|
6
|
+
* passed in by instrumentation.ts, which keeps this file free of imports and
|
|
7
|
+
* lets it run on its own.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The account `admin.devAutoLogin` names, as nextly.config.ts declares it. */
|
|
11
|
+
export type DevAutoLogin = false | { email: string; password?: string };
|
|
12
|
+
|
|
13
|
+
/** Creates a super-admin, skipping one that already exists. */
|
|
14
|
+
export type SeedSuperAdmin = (options: {
|
|
15
|
+
email: string;
|
|
16
|
+
password: string;
|
|
17
|
+
name: string;
|
|
18
|
+
silent: boolean;
|
|
19
|
+
}) => Promise<{ success: boolean; errorMessages?: string[] }>;
|
|
20
|
+
|
|
21
|
+
export async function seedDevUser(
|
|
22
|
+
login: DevAutoLogin | undefined,
|
|
23
|
+
seed: SeedSuperAdmin
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
// `next dev` only: no credentials in a production build, and tests bring
|
|
26
|
+
// their own fixtures.
|
|
27
|
+
if (process.env.NODE_ENV !== "development" || !login) return;
|
|
28
|
+
|
|
29
|
+
// Auto-login needs no password, but creating the account does.
|
|
30
|
+
if (!login.password) {
|
|
31
|
+
console.warn(
|
|
32
|
+
`[dev] admin.devAutoLogin names ${login.email} without a password, so that user is not created.`
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await seed({
|
|
38
|
+
email: login.email,
|
|
39
|
+
password: login.password,
|
|
40
|
+
name: "Dev User",
|
|
41
|
+
silent: true,
|
|
42
|
+
});
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
// Otherwise the only sign is the setup wizard, with nothing saying why.
|
|
45
|
+
console.warn(
|
|
46
|
+
`[dev] Could not create the auto-login user ${login.email}: ${
|
|
47
|
+
result.errorMessages?.join("; ") ?? "unknown error"
|
|
48
|
+
}`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* AUTO-GENERATED by `nextly generate:types` — DO NOT EDIT.
|
|
2
|
+
* The component paths `plugin-admin-imports.generated.ts` registers, as
|
|
3
|
+
* plain strings, so server code can read what the admin's map holds
|
|
4
|
+
* without importing the components. Regenerated with it. */
|
|
5
|
+
|
|
6
|
+
export const registeredComponentPaths = [
|
|
7
|
+
"{{pluginName}}/admin#SettingsPage",
|
|
8
|
+
] as const;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* AUTO-GENERATED by `nextly generate:types` — DO NOT EDIT.
|
|
2
|
+
* Statically registers the plugin admin components the admin resolves by
|
|
3
|
+
* string (pages, settings, views, slots, field-type editors) so they load
|
|
4
|
+
* without manual host imports. */
|
|
5
|
+
|
|
6
|
+
import { registerComponents } from "@nextlyhq/admin";
|
|
7
|
+
import * as _p0 from "{{pluginName}}/admin";
|
|
8
|
+
import { registeredComponentPaths as listed } from "./plugin-admin-components.generated";
|
|
9
|
+
|
|
10
|
+
const registered = {
|
|
11
|
+
"{{pluginName}}/admin#SettingsPage": _p0.SettingsPage,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type Registered = keyof typeof registered;
|
|
15
|
+
type Listed = (typeof listed)[number];
|
|
16
|
+
/** `true` only while this map registers exactly the paths its list names. */
|
|
17
|
+
type Agrees = [Registered] extends [Listed]
|
|
18
|
+
? [Listed] extends [Registered]
|
|
19
|
+
? true
|
|
20
|
+
: "component map and its list disagree: run npx nextly generate:types"
|
|
21
|
+
: "component map and its list disagree: run npx nextly generate:types";
|
|
22
|
+
export const componentMapAgreesWithList: Agrees = true;
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
JSON.stringify(Object.keys(registered).sort()) !==
|
|
26
|
+
JSON.stringify([...listed].sort())
|
|
27
|
+
) {
|
|
28
|
+
throw Object.assign(
|
|
29
|
+
new Error(
|
|
30
|
+
"The admin's component map and its list disagree: run npx nextly generate:types."
|
|
31
|
+
),
|
|
32
|
+
{ code: "NEXTLY_ADMIN_COMPONENT_MAP_MISMATCH" }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
registerComponents(registered);
|
|
37
|
+
export const registeredComponentPaths = listed;
|
|
@@ -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,19 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
import nextly from "@nextlyhq/eslint-plugin";
|
|
3
|
+
import tseslint from "typescript-eslint";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `nextly.configs.recommended` carries the design-token rules the Nextly admin
|
|
7
|
+
* holds itself to: no fixed palette colours, no hardcoded colour literals, and
|
|
8
|
+
* no inline `style` object that a class could have expressed. Admin UI that
|
|
9
|
+
* follows them inherits light and dark mode and survives a retheme.
|
|
10
|
+
*
|
|
11
|
+
* Mark a genuine exception in place, with a reason, rather than switching a rule
|
|
12
|
+
* off: `// design-lint-ok: <why>` on the line or the line above.
|
|
13
|
+
*/
|
|
14
|
+
export default tseslint.config(
|
|
15
|
+
{ ignores: ["dist", "dev/.next", "node_modules"] },
|
|
16
|
+
js.configs.recommended,
|
|
17
|
+
...tseslint.configs.recommended,
|
|
18
|
+
...nextly.configs.recommended
|
|
19
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
dist
|
|
3
|
+
*.log
|
|
4
|
+
|
|
5
|
+
# Embedded dev playground — never published, never committed build output
|
|
6
|
+
dev/.next
|
|
7
|
+
# SQLite files, and the journal and WAL files beside them, wherever they land.
|
|
8
|
+
*.db
|
|
9
|
+
*.db-journal
|
|
10
|
+
*.db-wal
|
|
11
|
+
*.db-shm
|
|
12
|
+
.env
|
|
13
|
+
.env.local
|
|
@@ -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,35 @@
|
|
|
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
|
+
// The Direct API takes one argument object per call.
|
|
24
|
+
const created = await t.nextly.create({
|
|
25
|
+
collection: "examples",
|
|
26
|
+
data: { title: "First example" },
|
|
27
|
+
});
|
|
28
|
+
expect(created.item.id).toBeDefined();
|
|
29
|
+
|
|
30
|
+
const fetched = await t.nextly.findByID({
|
|
31
|
+
collection: "examples",
|
|
32
|
+
id: String(created.item.id),
|
|
33
|
+
});
|
|
34
|
+
expect(fetched?.title).toBe("First example");
|
|
35
|
+
});
|