@easy-cms/next 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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +62 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +87 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kanawoot K.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @easy-cms/next
|
|
2
|
+
|
|
3
|
+
Next.js (App Router, 15+) adapter for [Easy CMS](https://github.com/maritonx/easy-crm).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @easy-cms/next @easy-cms/core @easy-cms/db-postgres
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**1. Wrap `next.config.ts`** so the server build keeps the admin UI and database drivers:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { withEasyCMS } from '@easy-cms/next/config'
|
|
13
|
+
export default withEasyCMS({ /* your Next config */ })
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**2. Add two route handlers** (paths match `routes.api` and `admin.path` in your Easy CMS config):
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// app/api/cms/[[...path]]/route.ts
|
|
20
|
+
import { createRouteHandlers } from '@easy-cms/next'
|
|
21
|
+
import config from '@/easy-cms.config'
|
|
22
|
+
export const { GET, HEAD, POST, PATCH, PUT, DELETE, OPTIONS } = createRouteHandlers(config)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// app/admin/[[...path]]/route.ts
|
|
27
|
+
import { createAdminRouteHandlers } from '@easy-cms/next'
|
|
28
|
+
import config from '@/easy-cms.config'
|
|
29
|
+
export const { GET, HEAD } = createAdminRouteHandlers(config)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**3. Read content in Server Components** with the typed Local API:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { getEasyCMS, getEasyCMSUser } from '@easy-cms/next'
|
|
36
|
+
import config from '@/easy-cms.config'
|
|
37
|
+
|
|
38
|
+
export const dynamic = 'force-dynamic'
|
|
39
|
+
|
|
40
|
+
export default async function Page() {
|
|
41
|
+
const cms = await getEasyCMS(config)
|
|
42
|
+
const { docs } = await cms.find('posts', { limit: 10 })
|
|
43
|
+
const user = await getEasyCMSUser(config) // logged-in Easy CMS user or null
|
|
44
|
+
// …
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Pages that read from the CMS should be dynamic (or use `revalidate`), otherwise `next build`
|
|
49
|
+
tries to query the database while prerendering.
|
|
50
|
+
|
|
51
|
+
`createRouteHandlers(config, { trustProxy: true })` uses `X-Forwarded-For` for login rate limiting;
|
|
52
|
+
enable it behind a proxy you trust, such as Vercel.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The subset of Next's config this helper reads and sets. */
|
|
2
|
+
interface NextConfigLike {
|
|
3
|
+
serverExternalPackages?: string[];
|
|
4
|
+
outputFileTracingIncludes?: Record<string, string[]>;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
type NextConfigInput = NextConfigLike | ((phase: string, ctx: {
|
|
8
|
+
defaultConfig: NextConfigLike;
|
|
9
|
+
}) => NextConfigLike | Promise<NextConfigLike>);
|
|
10
|
+
interface WithEasyCMSOptions {
|
|
11
|
+
/** Path to the Easy CMS config, relative to the project root. Default: `easy-cms.config.ts` (or .mts/.js/.mjs). */
|
|
12
|
+
readonly configPath?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Packages Next must load from node_modules instead of bundling: they read files next to
|
|
16
|
+
* themselves (the admin app, PGlite's WebAssembly) or load native binaries.
|
|
17
|
+
*/
|
|
18
|
+
declare const SERVER_EXTERNAL_PACKAGES: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Wraps `next.config.ts` so the server build includes what Easy CMS needs at runtime:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { withEasyCMS } from '@easy-cms/next/config'
|
|
24
|
+
* export default withEasyCMS({ ... })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare function withEasyCMS(nextConfig?: NextConfigInput, options?: WithEasyCMSOptions): (phase: string, ctx: {
|
|
28
|
+
defaultConfig: NextConfigLike;
|
|
29
|
+
}) => Promise<NextConfigLike>;
|
|
30
|
+
|
|
31
|
+
export { SERVER_EXTERNAL_PACKAGES, type WithEasyCMSOptions, withEasyCMS };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
5
|
+
import { CONFIG_FILE_NAMES, importConfig } from "@easy-cms/core";
|
|
6
|
+
var SERVER_EXTERNAL_PACKAGES = [
|
|
7
|
+
"@easy-cms/core",
|
|
8
|
+
"@easy-cms/admin",
|
|
9
|
+
"@easy-cms/drizzle",
|
|
10
|
+
"@easy-cms/db-sqlite",
|
|
11
|
+
"@easy-cms/db-postgres",
|
|
12
|
+
"@libsql/client",
|
|
13
|
+
"libsql",
|
|
14
|
+
"@electric-sql/pglite",
|
|
15
|
+
"drizzle-kit",
|
|
16
|
+
"drizzle-orm",
|
|
17
|
+
"postgres",
|
|
18
|
+
"sharp",
|
|
19
|
+
"jiti"
|
|
20
|
+
];
|
|
21
|
+
function withEasyCMS(nextConfig = {}, options = {}) {
|
|
22
|
+
return async (phase, ctx) => {
|
|
23
|
+
const base = typeof nextConfig === "function" ? await nextConfig(phase, ctx) : nextConfig;
|
|
24
|
+
const root = process.cwd();
|
|
25
|
+
const include = [...await tracedFiles(root, options.configPath)];
|
|
26
|
+
return {
|
|
27
|
+
...base,
|
|
28
|
+
serverExternalPackages: [
|
|
29
|
+
.../* @__PURE__ */ new Set([...base.serverExternalPackages ?? [], ...SERVER_EXTERNAL_PACKAGES])
|
|
30
|
+
],
|
|
31
|
+
outputFileTracingIncludes: {
|
|
32
|
+
...base.outputFileTracingIncludes,
|
|
33
|
+
"/**": [...base.outputFileTracingIncludes?.["/**"] ?? [], ...include]
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function tracedFiles(root, configPath) {
|
|
39
|
+
const toGlob = (path) => relative(root, path).replace(/\\/g, "/");
|
|
40
|
+
const files = ["easy-cms/migrations/**"];
|
|
41
|
+
try {
|
|
42
|
+
const adminPackage = createRequire(join(root, "package.json")).resolve(
|
|
43
|
+
"@easy-cms/admin/package.json"
|
|
44
|
+
);
|
|
45
|
+
files.push(`${toGlob(join(dirname(adminPackage), "dist/app"))}/**`);
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
const file = configPath ? isAbsolute(configPath) ? configPath : resolve(root, configPath) : CONFIG_FILE_NAMES.map((name) => join(root, name)).find((path) => existsSync(path));
|
|
49
|
+
if (file && existsSync(file)) {
|
|
50
|
+
try {
|
|
51
|
+
const config = await importConfig(file);
|
|
52
|
+
for (const path of config?.db?.bundle?.traceInclude ?? []) files.push(toGlob(path));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.warn(`[easy-cms] Could not read ${file} at build time: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
SERVER_EXTERNAL_PACKAGES,
|
|
61
|
+
withEasyCMS
|
|
62
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Config, EasyCMS, AuthUser } from '@easy-cms/core';
|
|
2
|
+
|
|
3
|
+
type Handler = (request: Request) => Promise<Response>;
|
|
4
|
+
/**
|
|
5
|
+
* The Easy CMS Local API for this server, typed from your config. Use it in Server
|
|
6
|
+
* Components, Route Handlers and Server Actions. The instance survives hot reloads.
|
|
7
|
+
*/
|
|
8
|
+
declare function getEasyCMS<const C extends Config>(config: C): Promise<EasyCMS<C>>;
|
|
9
|
+
interface RouteHandlerOptions {
|
|
10
|
+
/** Use `X-Forwarded-For` for the client IP (login rate limiting). Enable behind a proxy you trust, e.g. Vercel. */
|
|
11
|
+
readonly trustProxy?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* REST API route handlers. In `app/api/cms/[...path]/route.ts` (matching `routes.api`):
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import config from '@/easy-cms.config'
|
|
18
|
+
* import { createRouteHandlers } from '@easy-cms/next'
|
|
19
|
+
* export const { GET, HEAD, POST, PATCH, PUT, DELETE, OPTIONS } = createRouteHandlers(config)
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare function createRouteHandlers(config: Config, options?: RouteHandlerOptions): {
|
|
23
|
+
GET: Handler;
|
|
24
|
+
HEAD: Handler;
|
|
25
|
+
POST: Handler;
|
|
26
|
+
PATCH: Handler;
|
|
27
|
+
PUT: Handler;
|
|
28
|
+
DELETE: Handler;
|
|
29
|
+
OPTIONS: Handler;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Admin UI route handlers. In `app/admin/[[...path]]/route.ts` (matching `admin.path`):
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* import config from '@/easy-cms.config'
|
|
36
|
+
* import { createAdminRouteHandlers } from '@easy-cms/next'
|
|
37
|
+
* export const { GET, HEAD } = createAdminRouteHandlers(config)
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function createAdminRouteHandlers(config: Config, options?: {
|
|
41
|
+
appDir?: string;
|
|
42
|
+
}): {
|
|
43
|
+
GET: Handler;
|
|
44
|
+
HEAD: Handler;
|
|
45
|
+
};
|
|
46
|
+
/** The logged-in Easy CMS user for the current request (session cookie or Bearer token), or `null`. */
|
|
47
|
+
declare function getEasyCMSUser(config: Config): Promise<AuthUser | null>;
|
|
48
|
+
|
|
49
|
+
export { type RouteHandlerOptions, createAdminRouteHandlers, createRouteHandlers, getEasyCMS, getEasyCMSUser };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { createAdminHandler } from "@easy-cms/admin";
|
|
5
|
+
import {
|
|
6
|
+
createEasyCMS,
|
|
7
|
+
createRestHandler,
|
|
8
|
+
readCookie,
|
|
9
|
+
resolveConfig,
|
|
10
|
+
SESSION_COOKIE
|
|
11
|
+
} from "@easy-cms/core";
|
|
12
|
+
var KEY = /* @__PURE__ */ Symbol.for("easy-cms.next");
|
|
13
|
+
var store = globalThis;
|
|
14
|
+
function getEasyCMS(config) {
|
|
15
|
+
const cached = store[KEY];
|
|
16
|
+
if (cached?.config === config) return cached.promise;
|
|
17
|
+
if (cached) void cached.promise.then((cms) => cms.destroy()).catch(() => {
|
|
18
|
+
});
|
|
19
|
+
const promise = createEasyCMS(config);
|
|
20
|
+
store[KEY] = { config, promise };
|
|
21
|
+
promise.catch(() => {
|
|
22
|
+
if (store[KEY]?.promise === promise) delete store[KEY];
|
|
23
|
+
});
|
|
24
|
+
return promise;
|
|
25
|
+
}
|
|
26
|
+
function createRouteHandlers(config, options = {}) {
|
|
27
|
+
const handlers = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
const handle = async (request) => {
|
|
29
|
+
const cms = await getEasyCMS(config);
|
|
30
|
+
let handler = handlers.get(cms);
|
|
31
|
+
if (!handler) {
|
|
32
|
+
handler = createRestHandler(cms, {
|
|
33
|
+
getClientIp: (r) => options.trustProxy ? r.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || void 0 : void 0
|
|
34
|
+
});
|
|
35
|
+
handlers.set(cms, handler);
|
|
36
|
+
}
|
|
37
|
+
return handler(request);
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
GET: handle,
|
|
41
|
+
HEAD: handle,
|
|
42
|
+
POST: handle,
|
|
43
|
+
PATCH: handle,
|
|
44
|
+
PUT: handle,
|
|
45
|
+
DELETE: handle,
|
|
46
|
+
OPTIONS: handle
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createAdminRouteHandlers(config, options = {}) {
|
|
50
|
+
let handler;
|
|
51
|
+
const handle = async (request) => {
|
|
52
|
+
handler ??= resolveConfig(config).then(
|
|
53
|
+
(resolved) => createAdminHandler({
|
|
54
|
+
basePath: resolved.admin.path,
|
|
55
|
+
apiPath: resolved.routes.api,
|
|
56
|
+
locale: resolved.admin.locale,
|
|
57
|
+
appDir: options.appDir ?? adminAppDir(),
|
|
58
|
+
// Next.js strips trailing slashes; redirecting back would loop.
|
|
59
|
+
trailingSlashRedirect: false
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
return (await handler)(request);
|
|
63
|
+
};
|
|
64
|
+
return { GET: handle, HEAD: handle };
|
|
65
|
+
}
|
|
66
|
+
async function getEasyCMSUser(config) {
|
|
67
|
+
const { headers } = await import("next/headers.js");
|
|
68
|
+
const all = await headers();
|
|
69
|
+
const cms = await getEasyCMS(config);
|
|
70
|
+
const authorization = all.get("authorization");
|
|
71
|
+
if (authorization?.startsWith("Bearer ")) return cms.auth.verify(authorization.slice(7).trim());
|
|
72
|
+
const cookie = all.get("cookie");
|
|
73
|
+
const token = cookie ? readCookie(new Request("http://x", { headers: { cookie } }), SESSION_COOKIE) : void 0;
|
|
74
|
+
return cms.auth.verify(token);
|
|
75
|
+
}
|
|
76
|
+
function adminAppDir() {
|
|
77
|
+
const fromProject = createRequire(join(process.cwd(), "package.json"));
|
|
78
|
+
const nextPackage = fromProject.resolve("@easy-cms/next/package.json");
|
|
79
|
+
const adminPackage = createRequire(nextPackage).resolve("@easy-cms/admin/package.json");
|
|
80
|
+
return join(dirname(adminPackage), "dist/app");
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
createAdminRouteHandlers,
|
|
84
|
+
createRouteHandlers,
|
|
85
|
+
getEasyCMS,
|
|
86
|
+
getEasyCMSUser
|
|
87
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@easy-cms/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Next.js adapter for Easy CMS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./config": {
|
|
13
|
+
"types": "./dist/config.d.ts",
|
|
14
|
+
"default": "./dist/config.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.12"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@easy-cms/admin": "^0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"next": ">=15.0.0",
|
|
29
|
+
"@easy-cms/core": "^0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"next": "^16.3.6",
|
|
33
|
+
"@easy-cms/db-sqlite": "^0.1.0",
|
|
34
|
+
"@easy-cms/core": "^0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/maritonx/easy-crm.git",
|
|
42
|
+
"directory": "packages/next"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"easy-cms",
|
|
46
|
+
"nextjs",
|
|
47
|
+
"next",
|
|
48
|
+
"cms",
|
|
49
|
+
"headless-cms"
|
|
50
|
+
],
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run"
|
|
55
|
+
}
|
|
56
|
+
}
|