create-daniworks-app 0.2.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 +20 -0
- package/bin/cms.js +101 -0
- package/bin/index.js +147 -0
- package/package.json +43 -0
- package/template/.claude/skills/builder-extender/SKILL.md +129 -0
- package/template/.claude/skills/builder-extender/references/strapi-server.template.ts +71 -0
- package/template/.claude/skills/builder-extender/references/verify-boot.cjs +70 -0
- package/template/README.md +46 -0
- package/template/_gitignore +10 -0
- package/template/apps/web/_env.example +6 -0
- package/template/apps/web/app/globals.css +14 -0
- package/template/apps/web/app/layout.tsx +18 -0
- package/template/apps/web/app/page.tsx +11 -0
- package/template/apps/web/components/blocks-provider.tsx +19 -0
- package/template/apps/web/lib/strapi.ts +17 -0
- package/template/apps/web/next.config.ts +19 -0
- package/template/apps/web/package.json +25 -0
- package/template/apps/web/postcss.config.mjs +1 -0
- package/template/apps/web/tsconfig.json +11 -0
- package/template/package.json +15 -0
- package/template/pnpm-workspace.yaml +8 -0
- package/template/turbo.json +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HJ-company
|
|
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,20 @@
|
|
|
1
|
+
# create-daniworks-app
|
|
2
|
+
|
|
3
|
+
Scaffold a new [Daniworks Builder](https://github.com/HJ-company/daniworks-builder-v2)
|
|
4
|
+
project — a Strapi v5 CMS + a Next.js front end that renders CMS block content.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npx create-daniworks-app my-site
|
|
8
|
+
cd my-site
|
|
9
|
+
pnpm dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The scaffold wires the front end (`apps/web`) — `@dani-builder/blocks` provider,
|
|
13
|
+
Tailwind `@theme` + `@source`, and a `home` page renderer — and generates all
|
|
14
|
+
secrets. It also creates the Strapi CMS (`apps/cms`), installs the Builder plugin
|
|
15
|
+
and its custom-field peer plugins, enables Builder, and configures sqlite's
|
|
16
|
+
default database filename.
|
|
17
|
+
|
|
18
|
+
## License
|
|
19
|
+
|
|
20
|
+
MIT © HJ-company
|
package/bin/cms.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const CMS_PACKAGES = [
|
|
6
|
+
["@dani-builder/strapi-plugin-builder", "latest"],
|
|
7
|
+
["@strapi/plugin-color-picker", "^5.0.0"],
|
|
8
|
+
["@d3levvv/strapi-react-icons-plugin", "^1.0.0"],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function runCommand(command, args, options = {}) {
|
|
12
|
+
const result = spawnSync(command, args, { stdio: "inherit", ...options });
|
|
13
|
+
|
|
14
|
+
if (result.error) {
|
|
15
|
+
throw result.error;
|
|
16
|
+
}
|
|
17
|
+
if (result.status !== 0) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Command failed (${result.status}): ${command} ${args.join(" ")}`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function enableBuilder(cmsDir) {
|
|
27
|
+
const pluginsFile = join(cmsDir, "config", "plugins.ts");
|
|
28
|
+
const config = readFileSync(pluginsFile, "utf-8");
|
|
29
|
+
const objectStart = /=> \(\{\r?\n/;
|
|
30
|
+
|
|
31
|
+
if (!objectStart.test(config)) {
|
|
32
|
+
throw new Error(`Cannot enable Builder in ${pluginsFile}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
writeFileSync(
|
|
36
|
+
pluginsFile,
|
|
37
|
+
config.replace(
|
|
38
|
+
objectStart,
|
|
39
|
+
(match) => `${match} builder: { enabled: true },\n`,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function addCmsDependencies(cmsDir) {
|
|
45
|
+
const packageFile = join(cmsDir, "package.json");
|
|
46
|
+
const packageJson = JSON.parse(readFileSync(packageFile, "utf-8"));
|
|
47
|
+
|
|
48
|
+
packageJson.dependencies ??= {};
|
|
49
|
+
for (const [name, version] of CMS_PACKAGES) {
|
|
50
|
+
packageJson.dependencies[name] = version;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
writeFileSync(packageFile, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function fixSqliteDatabaseFilename(cmsDir) {
|
|
57
|
+
const envFile = join(cmsDir, ".env");
|
|
58
|
+
|
|
59
|
+
if (!existsSync(envFile)) return;
|
|
60
|
+
|
|
61
|
+
const env = readFileSync(envFile, "utf-8");
|
|
62
|
+
const fixed = /^DATABASE_FILENAME=[ \t]*$/m.test(env)
|
|
63
|
+
? env.replace(
|
|
64
|
+
/^DATABASE_FILENAME=[ \t]*$/m,
|
|
65
|
+
"DATABASE_FILENAME=.tmp/data.db",
|
|
66
|
+
)
|
|
67
|
+
: env;
|
|
68
|
+
|
|
69
|
+
if (fixed !== env) writeFileSync(envFile, fixed);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createCms(projectDir, execute = runCommand) {
|
|
73
|
+
const cmsDir = join(projectDir, "apps", "cms");
|
|
74
|
+
|
|
75
|
+
execute(
|
|
76
|
+
"pnpm",
|
|
77
|
+
[
|
|
78
|
+
"dlx",
|
|
79
|
+
"create-strapi-app@^5",
|
|
80
|
+
"apps/cms",
|
|
81
|
+
"--ts",
|
|
82
|
+
"--use-pnpm",
|
|
83
|
+
"--skip-cloud",
|
|
84
|
+
"--no-example",
|
|
85
|
+
"--no-git-init",
|
|
86
|
+
"--no-run",
|
|
87
|
+
"--no-install",
|
|
88
|
+
"--non-interactive",
|
|
89
|
+
"--dbclient",
|
|
90
|
+
"sqlite",
|
|
91
|
+
"--dbfile",
|
|
92
|
+
".tmp/data.db",
|
|
93
|
+
],
|
|
94
|
+
{ cwd: projectDir },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
addCmsDependencies(cmsDir);
|
|
98
|
+
enableBuilder(cmsDir);
|
|
99
|
+
fixSqliteDatabaseFilename(cmsDir);
|
|
100
|
+
execute("pnpm", ["install"], { cwd: projectDir });
|
|
101
|
+
}
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { basename, dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { randomBytes } from "node:crypto";
|
|
17
|
+
|
|
18
|
+
import { createCms, runCommand } from "./cms.js";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const TEMPLATE_DIR = join(__dirname, "..", "template");
|
|
22
|
+
|
|
23
|
+
const c = {
|
|
24
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
25
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
26
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
27
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
28
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function secret(bytes = 16) {
|
|
32
|
+
return randomBytes(bytes).toString("base64");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function die(msg) {
|
|
36
|
+
console.error(c.red(`\n✗ ${msg}\n`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Args ──
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const input = args.find((a) => !a.startsWith("-"));
|
|
43
|
+
if (!input) {
|
|
44
|
+
die("Usage: create-daniworks-app <project-name>");
|
|
45
|
+
}
|
|
46
|
+
const target = join(process.cwd(), input);
|
|
47
|
+
// The workspace package name must be a valid npm name — derive it from the
|
|
48
|
+
// target directory, not the raw arg (which may be a path like "./my-site").
|
|
49
|
+
const pkgName = basename(target)
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
52
|
+
.replace(/^[._-]+|[._-]+$/g, "");
|
|
53
|
+
if (!pkgName) {
|
|
54
|
+
die(`Cannot derive a valid package name from "${input}".`);
|
|
55
|
+
}
|
|
56
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
57
|
+
die(`Directory "${input}" already exists and is not empty.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = runCommand("pnpm", ["--version"], {
|
|
62
|
+
stdio: "pipe",
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
});
|
|
65
|
+
const [major, minor] = result.stdout.trim().split(".").map(Number);
|
|
66
|
+
if (
|
|
67
|
+
!Number.isInteger(major) ||
|
|
68
|
+
!Number.isInteger(minor) ||
|
|
69
|
+
major < 9 ||
|
|
70
|
+
(major === 9 && minor < 4)
|
|
71
|
+
) {
|
|
72
|
+
throw new Error("create-daniworks-app requires pnpm >=9.4 on PATH.");
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
die(error instanceof Error ? error.message : String(error));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(
|
|
79
|
+
`\n${c.bold("create-daniworks-app")} ${c.dim("· scaffolding a Strapi + Next.js block site")}\n`,
|
|
80
|
+
);
|
|
81
|
+
console.log(` ${c.cyan("→")} ${target}`);
|
|
82
|
+
console.log(` ${c.dim("package name:")} ${pkgName}`);
|
|
83
|
+
|
|
84
|
+
const targetExisted = existsSync(target);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// ── Copy template ──
|
|
88
|
+
mkdirSync(target, { recursive: true });
|
|
89
|
+
cpSync(TEMPLATE_DIR, target, { recursive: true });
|
|
90
|
+
|
|
91
|
+
// Rename dotfile stand-ins that npm would otherwise strip on publish.
|
|
92
|
+
for (const [from, to] of [
|
|
93
|
+
["_gitignore", ".gitignore"],
|
|
94
|
+
["_env.example", ".env.example"],
|
|
95
|
+
]) {
|
|
96
|
+
walk(target, (file) => {
|
|
97
|
+
if (basename(file) === from) renameSync(file, join(dirname(file), to));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Interpolate {{PROJECT_NAME}} ──
|
|
102
|
+
walk(target, (file) => {
|
|
103
|
+
if (/\.(json|ts|tsx|js|md|css|yaml)$/.test(file)) {
|
|
104
|
+
const content = readFileSync(file, "utf-8");
|
|
105
|
+
if (content.includes("{{PROJECT_NAME}}")) {
|
|
106
|
+
writeFileSync(file, content.split("{{PROJECT_NAME}}").join(pkgName));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── Generate real secrets into a gitignored .env.local ──
|
|
112
|
+
// .env.example stays committed with blank values; the runtime secrets live in
|
|
113
|
+
// .env.local (already covered by the template's .gitignore).
|
|
114
|
+
const webEnvExample = join(target, "apps", "web", ".env.example");
|
|
115
|
+
if (existsSync(webEnvExample)) {
|
|
116
|
+
const local = readFileSync(webEnvExample, "utf-8")
|
|
117
|
+
.replace(/^REVALIDATE_SECRET=.*$/m, `REVALIDATE_SECRET=${secret()}`)
|
|
118
|
+
.replace(/^PREVIEW_SECRET=.*$/m, `PREVIEW_SECRET=${secret()}`);
|
|
119
|
+
writeFileSync(join(target, "apps", "web", ".env.local"), local);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Create and configure the Strapi CMS ──
|
|
123
|
+
console.log(` ${c.cyan("→")} Creating Strapi CMS`);
|
|
124
|
+
createCms(target);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
rmSync(target, { recursive: true, force: true });
|
|
127
|
+
if (targetExisted) mkdirSync(target, { recursive: true });
|
|
128
|
+
die(error instanceof Error ? error.message : String(error));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Done ──
|
|
132
|
+
console.log(c.green("\n✓ Project created.\n"));
|
|
133
|
+
console.log(c.bold("Next steps:"));
|
|
134
|
+
console.log(` ${c.cyan("cd")} ${input}`);
|
|
135
|
+
console.log(
|
|
136
|
+
` ${c.cyan("pnpm --filter cms strapi admin:create-user")} ${c.dim("# create an admin + Full-access API token")}`,
|
|
137
|
+
);
|
|
138
|
+
console.log(` ${c.dim("# set STRAPI_TOKEN in apps/web/.env.local, then:")}`);
|
|
139
|
+
console.log(` ${c.cyan("pnpm dev")}\n`);
|
|
140
|
+
|
|
141
|
+
function walk(dir, fn) {
|
|
142
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
143
|
+
const full = join(dir, entry.name);
|
|
144
|
+
if (entry.isDirectory()) walk(full, fn);
|
|
145
|
+
else fn(full);
|
|
146
|
+
}
|
|
147
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-daniworks-app",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Scaffold a new Daniworks Builder project (Strapi v5 CMS + Next.js block-rendering front end)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-daniworks-app": "bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.13",
|
|
12
|
+
"pnpm": ">=9.4"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"template",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/HJ-company/daniworks-builder-v2.git",
|
|
26
|
+
"directory": "packages/create-daniworks-app"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"strapi",
|
|
30
|
+
"strapi-v5",
|
|
31
|
+
"nextjs",
|
|
32
|
+
"daniworks-builder",
|
|
33
|
+
"scaffold",
|
|
34
|
+
"create-app",
|
|
35
|
+
"cli"
|
|
36
|
+
],
|
|
37
|
+
"homepage": "https://github.com/HJ-company/daniworks-builder-v2/tree/main/packages/create-daniworks-app#readme",
|
|
38
|
+
"author": "HJ-company",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "node --test",
|
|
41
|
+
"sync:skills": "rm -rf template/.claude/skills/builder-extender && mkdir -p template/.claude/skills && cp -R ../../.claude/skills/builder-extender template/.claude/skills/"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: builder-extender
|
|
3
|
+
description: "Extend @dani-builder/strapi-plugin-builder from a consumer Strapi
|
|
4
|
+
project without touching the plugin. Actions: add a custom block to
|
|
5
|
+
Page.blocks, add fields to a plugin component (hero, features, cta, ...),
|
|
6
|
+
register populate for a custom block, install the plugin with its peer
|
|
7
|
+
plugins, verify an extension boots. Triggers: 'add a block', 'custom
|
|
8
|
+
component', 'new hero variant', 'add field to hero', 'extend the builder',
|
|
9
|
+
'dynamic zone', 'plugin::builder', 'strapi-server.ts', 'registerBlock'."
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Builder Extender
|
|
13
|
+
|
|
14
|
+
Adds project-specific blocks and fields to a Strapi project that consumes
|
|
15
|
+
`@dani-builder/strapi-plugin-builder` from npm. Every change lands in the
|
|
16
|
+
consumer's `src/`; the plugin package is never edited.
|
|
17
|
+
|
|
18
|
+
Long-form rationale and reproduced failure cases:
|
|
19
|
+
`docs/guides/extending-builder.md` (in the builder monorepo). This skill is the
|
|
20
|
+
executable version.
|
|
21
|
+
|
|
22
|
+
## 1. When to apply
|
|
23
|
+
|
|
24
|
+
- The user wants a block that the plugin does not ship (a project-specific
|
|
25
|
+
hero, a product grid, an embed) selectable in `Page.blocks`.
|
|
26
|
+
- The user wants extra fields on a plugin block or shared component
|
|
27
|
+
(`blocks.hero`, `shared.link`, …).
|
|
28
|
+
- The user is installing the plugin into a fresh Strapi app.
|
|
29
|
+
- `cms-content-populator` was asked for a block that does not exist.
|
|
30
|
+
|
|
31
|
+
Do **not** apply when the working directory is the builder monorepo itself and
|
|
32
|
+
the request is to change the plugin for everyone — edit
|
|
33
|
+
`packages/strapi-plugin-builder/server/src/components/**` there instead and
|
|
34
|
+
run `pnpm docs:gen`.
|
|
35
|
+
|
|
36
|
+
## 2. Locate the consumer project
|
|
37
|
+
|
|
38
|
+
The consumer is the directory whose `package.json` lists
|
|
39
|
+
`@dani-builder/strapi-plugin-builder` in `dependencies` and has
|
|
40
|
+
`config/plugins.ts`. In a scaffolded customer repo that is the CMS app; in the
|
|
41
|
+
builder monorepo it is `apps/demo-cms`. All paths below are relative to it.
|
|
42
|
+
|
|
43
|
+
## 3. Procedure
|
|
44
|
+
|
|
45
|
+
### 3.1 Install (only if the plugin is not yet a dependency)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm add @dani-builder/strapi-plugin-builder @strapi/plugin-color-picker @d3levvv/strapi-react-icons-plugin
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
All three are required: the plugin's components use the two peers' custom
|
|
52
|
+
fields, and Strapi only loads plugins listed in the consumer's own
|
|
53
|
+
`package.json`. Then ensure `config/plugins.ts` contains
|
|
54
|
+
`builder: { enabled: true }`.
|
|
55
|
+
|
|
56
|
+
### 3.2 Add a custom block
|
|
57
|
+
|
|
58
|
+
1. Create `src/components/<category>/<name>.json`. Use a project category
|
|
59
|
+
(`custom`, or the project name) — never `blocks`, `elements`, or `shared`.
|
|
60
|
+
Reuse plugin sub-components (`elements.section`, `shared.link`,
|
|
61
|
+
`shared.image`, `shared.slider-item`, …) for anything the front end already
|
|
62
|
+
knows how to render. Follow Strapi component JSON: `collectionName`,
|
|
63
|
+
`info.displayName`, `attributes`.
|
|
64
|
+
2. Create or update `src/extensions/builder/strapi-server.ts` from
|
|
65
|
+
`references/strapi-server.template.ts`:
|
|
66
|
+
- push the uid (`<category>.<name>`) onto
|
|
67
|
+
`plugin.contentTypes.page.schema.attributes.blocks.components`;
|
|
68
|
+
- inside the wrapped `register`, call
|
|
69
|
+
`strapi.plugin('builder').service('populate').registerBlock(uid, populate)`
|
|
70
|
+
with a populate object that covers every nested component, media, and
|
|
71
|
+
relation of the new block. Scalars need nothing.
|
|
72
|
+
3. Verify (§3.4).
|
|
73
|
+
4. Remind the user that `BlockRenderer` from `@dani-builder/blocks` will not
|
|
74
|
+
render the new block: the front end needs a project renderer keyed on the
|
|
75
|
+
uid (see guide §4).
|
|
76
|
+
|
|
77
|
+
### 3.3 Add fields to a plugin component
|
|
78
|
+
|
|
79
|
+
1. In `src/extensions/builder/strapi-server.ts`, inside the wrapped
|
|
80
|
+
`register` **after** `await baseRegister(ctx)`:
|
|
81
|
+
```ts
|
|
82
|
+
const target = ctx.strapi.get('components').get('blocks.hero');
|
|
83
|
+
Object.assign(target.attributes, EXTRA);
|
|
84
|
+
Object.assign(target.__schema__.attributes, EXTRA);
|
|
85
|
+
```
|
|
86
|
+
Update both objects. Additive changes only — never delete or rename a
|
|
87
|
+
plugin attribute (the DB sync drops the column and its data).
|
|
88
|
+
2. Verify (§3.4). The new columns appear on the component table; the Content
|
|
89
|
+
Manager layout updates itself; the smart API returns scalars without a
|
|
90
|
+
populate change. A nested component or media added this way also needs
|
|
91
|
+
the block's populate extended via `registerBlock` with the full populate
|
|
92
|
+
for that block (it replaces the plugin's entry for that uid).
|
|
93
|
+
3. Remind the user the front-end block component must be wrapped or replaced
|
|
94
|
+
to show the new fields.
|
|
95
|
+
|
|
96
|
+
### 3.4 Verify
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
rm -rf dist # TypeScript consumers: stale compiled JSON is loaded as current
|
|
100
|
+
node <path-to>/references/verify-boot.cjs
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Copy `references/verify-boot.cjs` into the consumer (e.g. `scripts/`) so it
|
|
104
|
+
resolves the consumer's `@strapi/strapi` and `.env`. It boots Strapi headlessly
|
|
105
|
+
and prints the Page dynamic-zone list, the attributes and DB columns of any
|
|
106
|
+
component named in `VERIFY_COMPONENTS`, and the registered populate keys.
|
|
107
|
+
Confirm all three show the change, then start the server normally.
|
|
108
|
+
|
|
109
|
+
If the boot fails with `unable to open database file` on sqlite, set
|
|
110
|
+
`DATABASE_FILENAME=.tmp/data.db` in `.env` and create `.tmp/`.
|
|
111
|
+
|
|
112
|
+
## 4. Hard rules
|
|
113
|
+
|
|
114
|
+
| Never | Because |
|
|
115
|
+
|---|---|
|
|
116
|
+
| Write a partial `src/extensions/builder/content-types/<ct>/schema.json` | Strapi shallow-merges it: `attributes` is replaced wholesale, the other fields vanish, and the DB sync drops their columns without an error. |
|
|
117
|
+
| Create `src/components/blocks/*.json`, `elements/*.json`, or `shared/*.json` with a plugin uid | Boot fails: `Component <uid> has already been registered.` |
|
|
118
|
+
| Edit a plugin component in the Content-Type Builder UI | Save returns 400 (`Error writing schema files … "path" argument must be of type string`) and the edit is lost. Plugin content types are read-only there by design. |
|
|
119
|
+
| Copy plugin content-type schemas into `src/api/` | Duplicate `collectionName`s prevent boot (see plugin README). |
|
|
120
|
+
| Register populate at the top level of `strapi-server.ts` | Plugin services do not exist until the plugin loads; do it inside the wrapped `register`. |
|
|
121
|
+
|
|
122
|
+
## 5. Reference
|
|
123
|
+
|
|
124
|
+
- `references/strapi-server.template.ts` — complete extension file to copy.
|
|
125
|
+
- `references/verify-boot.cjs` — headless boot verifier.
|
|
126
|
+
- Guide with reproduced failure cases: `docs/guides/extending-builder.md`
|
|
127
|
+
(builder monorepo).
|
|
128
|
+
- Plugin populate hook: `packages/strapi-plugin-builder/server/src/services/populate.ts`
|
|
129
|
+
(`registerBlock`, `getPageBlocksPopulate`, `getGlobalPopulate`).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/extensions/builder/strapi-server.ts
|
|
3
|
+
*
|
|
4
|
+
* Project-level extension of @dani-builder/strapi-plugin-builder. Strapi loads
|
|
5
|
+
* this file after the plugin package and before the plugin registers, passes
|
|
6
|
+
* the plugin object in, and uses whatever is returned.
|
|
7
|
+
*
|
|
8
|
+
* Copy into the consumer project and edit the three marked sections. Delete
|
|
9
|
+
* any section you do not need. Everything is additive: never remove or rename
|
|
10
|
+
* a plugin attribute here (the DB sync would drop the column and its data).
|
|
11
|
+
*/
|
|
12
|
+
import type { Core } from '@strapi/strapi';
|
|
13
|
+
|
|
14
|
+
type AnyRecord = Record<string, any>;
|
|
15
|
+
|
|
16
|
+
interface BuilderPlugin extends AnyRecord {
|
|
17
|
+
register?: (ctx: { strapi: Core.Strapi }) => void | Promise<void>;
|
|
18
|
+
contentTypes: Record<string, { schema: AnyRecord }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── 1. Project blocks to make selectable in Page.blocks ──────────────────────
|
|
22
|
+
// Each entry is a component defined in src/components/<category>/<name>.json
|
|
23
|
+
// with its smart-API populate. Cover every nested component / media / relation;
|
|
24
|
+
// scalars need nothing. `true` populates a flat component or media field.
|
|
25
|
+
const PROJECT_BLOCKS: Record<string, AnyRecord> = {
|
|
26
|
+
'custom.dynamic-hero': {
|
|
27
|
+
populate: {
|
|
28
|
+
section: { populate: { backgroundImage: true } },
|
|
29
|
+
slides: { populate: { link: true, image: { populate: { media: true } } } },
|
|
30
|
+
ctaLink: true,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ── 2. Extra attributes on plugin components ─────────────────────────────────
|
|
36
|
+
// Keyed by component uid. Plain Strapi attribute definitions.
|
|
37
|
+
const COMPONENT_EXTENSIONS: Record<string, AnyRecord> = {
|
|
38
|
+
'blocks.hero': {
|
|
39
|
+
badgeText: { type: 'string' },
|
|
40
|
+
videoUrl: { type: 'string' },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default (plugin: BuilderPlugin): BuilderPlugin => {
|
|
45
|
+
const pageBlocks: string[] = plugin.contentTypes.page.schema.attributes.blocks.components;
|
|
46
|
+
for (const uid of Object.keys(PROJECT_BLOCKS)) {
|
|
47
|
+
if (!pageBlocks.includes(uid)) pageBlocks.push(uid);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const baseRegister = plugin.register;
|
|
51
|
+
plugin.register = async (ctx) => {
|
|
52
|
+
// Plugin components are registered here; extend them only afterwards.
|
|
53
|
+
await baseRegister?.(ctx);
|
|
54
|
+
|
|
55
|
+
const components: AnyRecord = ctx.strapi.get('components');
|
|
56
|
+
for (const [uid, attributes] of Object.entries(COMPONENT_EXTENSIONS)) {
|
|
57
|
+
const component = components.get(uid);
|
|
58
|
+
if (!component) throw new Error(`[builder extension] unknown plugin component ${uid}`);
|
|
59
|
+
Object.assign(component.attributes, attributes);
|
|
60
|
+
Object.assign(component.__schema__.attributes, attributes);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 3. Populate registration (must run after the plugin loaded) ──────────
|
|
64
|
+
const populate = ctx.strapi.plugin('builder').service('populate');
|
|
65
|
+
for (const [uid, config] of Object.entries(PROJECT_BLOCKS)) {
|
|
66
|
+
populate.registerBlock(uid, config);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return plugin;
|
|
71
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Headless boot verifier for builder extensions.
|
|
4
|
+
*
|
|
5
|
+
* Copy into the consumer Strapi project (e.g. scripts/verify-boot.cjs) and run
|
|
6
|
+
* from the project root so it resolves the project's @strapi/strapi and .env:
|
|
7
|
+
*
|
|
8
|
+
* rm -rf dist && node scripts/verify-boot.cjs
|
|
9
|
+
*
|
|
10
|
+
* Boots Strapi (register + bootstrap + DB sync, no HTTP listener) and prints:
|
|
11
|
+
* - the Page.blocks dynamic-zone component list
|
|
12
|
+
* - attributes and DB columns for each component in VERIFY_COMPONENTS
|
|
13
|
+
* - the populate keys the smart API will use for Page.blocks
|
|
14
|
+
*
|
|
15
|
+
* Environment:
|
|
16
|
+
* VERIFY_COMPONENTS comma-separated uids to inspect (default: blocks.hero)
|
|
17
|
+
*/
|
|
18
|
+
const { createStrapi, compileStrapi } = require('@strapi/strapi');
|
|
19
|
+
|
|
20
|
+
const componentsToInspect = (process.env.VERIFY_COMPONENTS || 'blocks.hero')
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((s) => s.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
|
|
25
|
+
async function columnsOf(app, table) {
|
|
26
|
+
const client = app.db.connection.client.config.client;
|
|
27
|
+
if (client === 'sqlite' || client === 'better-sqlite3') {
|
|
28
|
+
const rows = await app.db.connection.raw(`pragma table_info('${table}')`);
|
|
29
|
+
return rows.map((r) => r.name);
|
|
30
|
+
}
|
|
31
|
+
const rows = await app.db.connection('information_schema.columns')
|
|
32
|
+
.select('column_name')
|
|
33
|
+
.where({ table_name: table });
|
|
34
|
+
return rows.map((r) => r.column_name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
(async () => {
|
|
38
|
+
const app = await createStrapi(await compileStrapi()).load();
|
|
39
|
+
try {
|
|
40
|
+
const page = app.contentTypes['plugin::builder.page'];
|
|
41
|
+
if (!page) throw new Error('plugin::builder.page is not registered — is the builder plugin enabled?');
|
|
42
|
+
|
|
43
|
+
const report = {
|
|
44
|
+
pageBlocks: page.attributes.blocks.components,
|
|
45
|
+
components: {},
|
|
46
|
+
populateKeys: Object.keys(app.plugin('builder').service('populate').getPageBlocksPopulate().on),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const uid of componentsToInspect) {
|
|
50
|
+
const component = app.components[uid];
|
|
51
|
+
if (!component) {
|
|
52
|
+
report.components[uid] = 'NOT REGISTERED';
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
report.components[uid] = {
|
|
56
|
+
attributes: Object.keys(component.attributes),
|
|
57
|
+
ctbAttributes: Object.keys(component.__schema__?.attributes || {}),
|
|
58
|
+
columns: await columnsOf(app, component.collectionName),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(JSON.stringify(report, null, 2));
|
|
63
|
+
} finally {
|
|
64
|
+
await app.destroy();
|
|
65
|
+
}
|
|
66
|
+
process.exit(0);
|
|
67
|
+
})().catch((error) => {
|
|
68
|
+
console.error('BOOT FAILED:', error.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A [Daniworks Builder](https://github.com/HJ-company/daniworks-builder-v2) site:
|
|
4
|
+
a Strapi v5 CMS + a Next.js front end that renders the CMS's block content.
|
|
5
|
+
|
|
6
|
+
This scaffold ships the front end (`apps/web`) and Strapi CMS (`apps/cms`) fully
|
|
7
|
+
wired.
|
|
8
|
+
|
|
9
|
+
The generator installed the workspace and created `apps/cms` with Builder and
|
|
10
|
+
its two custom-field peer plugins as direct dependencies. It also enabled
|
|
11
|
+
Builder in `apps/cms/config/plugins.ts` and configured sqlite's database
|
|
12
|
+
filename.
|
|
13
|
+
|
|
14
|
+
The plugin registers page/article/etc. and the write-only inquiry router when
|
|
15
|
+
Strapi starts. Do not copy these schemas into `apps/cms/src/api/`.
|
|
16
|
+
|
|
17
|
+
To add your own blocks or extra fields on the builder's components, use
|
|
18
|
+
`apps/cms/src/extensions/builder/strapi-server.ts` — the `builder-extender`
|
|
19
|
+
skill in `.claude/skills/` walks an agent through it, and the full guide is
|
|
20
|
+
[docs/guides/extending-builder.md](https://github.com/HJ-company/daniworks-builder-v2/blob/main/docs/guides/extending-builder.md).
|
|
21
|
+
|
|
22
|
+
Create an admin and a **Full-access API token** (Strapi Admin → Settings →
|
|
23
|
+
API Tokens), then set it as `STRAPI_TOKEN` in `apps/web/.env.local` (this file
|
|
24
|
+
was generated for you from `.env.example`, with `REVALIDATE_SECRET` /
|
|
25
|
+
`PREVIEW_SECRET` already filled in).
|
|
26
|
+
|
|
27
|
+
## Run
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- CMS admin: http://localhost:1337/admin — create a `page` with slug `home`.
|
|
34
|
+
- Front end: http://localhost:3000
|
|
35
|
+
|
|
36
|
+
## What's wired for you
|
|
37
|
+
|
|
38
|
+
- `apps/web/components/blocks-provider.tsx` — injects `next/image` / `next/link`
|
|
39
|
+
and a token-free `resolveAssetUrl` into `@dani-builder/blocks`.
|
|
40
|
+
- `apps/web/app/globals.css` — Tailwind v4 `@theme` tokens + `@source` for the
|
|
41
|
+
block package + `@dani-builder/blocks/styles.css` import in the layout.
|
|
42
|
+
- `apps/web/app/page.tsx` — fetches the `home` page and renders it with
|
|
43
|
+
`<BlockRenderer>`.
|
|
44
|
+
|
|
45
|
+
Runtime secrets were generated into `apps/web/.env.local` (gitignored). The
|
|
46
|
+
committed `.env.example` keeps blank placeholders.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@source "../node_modules/@dani-builder/blocks/dist";
|
|
3
|
+
|
|
4
|
+
@theme {
|
|
5
|
+
--color-primary: #6366F1;
|
|
6
|
+
--color-primary-light: #818CF8;
|
|
7
|
+
--color-cta: #047857;
|
|
8
|
+
--color-cta-hover: #065F46;
|
|
9
|
+
--color-bg: #F5F3FF;
|
|
10
|
+
--color-text: #1E1B4B;
|
|
11
|
+
--color-text-muted: #4B5563;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body { background-color: #fff; color: var(--color-text); }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { BlocksProvider } from "@/components/blocks-provider";
|
|
3
|
+
import { mediaHost } from "@/lib/strapi";
|
|
4
|
+
import "./globals.css";
|
|
5
|
+
import "@dani-builder/blocks/styles.css";
|
|
6
|
+
|
|
7
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<script dangerouslySetInnerHTML={{ __html: "document.documentElement.classList.add('js')" }} />
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<BlocksProvider mediaHost={mediaHost}>{children}</BlocksProvider>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import { BlockRenderer } from "@dani-builder/blocks";
|
|
3
|
+
import { fetchPageBySlug } from "@/lib/strapi";
|
|
4
|
+
|
|
5
|
+
export const revalidate = 60;
|
|
6
|
+
|
|
7
|
+
export default async function HomePage() {
|
|
8
|
+
const page = await fetchPageBySlug("home");
|
|
9
|
+
if (!page) notFound();
|
|
10
|
+
return <BlockRenderer blocks={page.blocks} />;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import NextImage from "next/image";
|
|
4
|
+
import NextLink from "next/link";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { getAssetUrl } from "@dani-builder/strapi-client/utils";
|
|
7
|
+
import type { StrapiMedia } from "@dani-builder/strapi-client/types";
|
|
8
|
+
import { BlocksProvider as Provider, type BlocksImageProps, type BlocksLinkProps } from "@dani-builder/blocks";
|
|
9
|
+
|
|
10
|
+
const Image = (p: BlocksImageProps) => <NextImage {...p} />;
|
|
11
|
+
const Link = ({ href, children, ...r }: BlocksLinkProps) => (
|
|
12
|
+
<NextLink href={href} {...r}>{children}</NextLink>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export function BlocksProvider({ mediaHost, children }: { mediaHost: string; children: ReactNode }) {
|
|
16
|
+
const resolveAssetUrl = (a: StrapiMedia | null | undefined, f?: "thumbnail" | "small" | "medium" | "large") =>
|
|
17
|
+
getAssetUrl(a, f, mediaHost);
|
|
18
|
+
return <Provider value={{ Image, Link, resolveAssetUrl }}>{children}</Provider>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
2
|
+
import { createStrapiClient } from "@dani-builder/strapi-client/client";
|
|
3
|
+
import { getPageBySlug } from "@dani-builder/strapi-client/api";
|
|
4
|
+
|
|
5
|
+
const strapiUrl = process.env.STRAPI_URL ?? "http://localhost:1337";
|
|
6
|
+
export const mediaHost = process.env.STRAPI_MEDIA_HOST ?? strapiUrl;
|
|
7
|
+
|
|
8
|
+
const strapi = createStrapiClient({
|
|
9
|
+
baseUrl: strapiUrl,
|
|
10
|
+
apiToken: process.env.STRAPI_TOKEN,
|
|
11
|
+
mediaHost,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// getPageBySlug returns null for a genuinely missing slug (200 + empty result),
|
|
15
|
+
// which the page turns into a 404. Real failures (network/auth/5xx) are left to
|
|
16
|
+
// throw so they surface in the error boundary instead of silently 404-ing.
|
|
17
|
+
export const fetchPageBySlug = cache((slug: string) => getPageBySlug(strapi, slug));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
const withScheme = (v: string) => (/^https?:\/\//.test(v) ? v : `https://${v}`);
|
|
4
|
+
const mediaHost = withScheme(process.env.STRAPI_MEDIA_HOST ?? process.env.STRAPI_URL ?? "http://localhost:1337");
|
|
5
|
+
|
|
6
|
+
// Next 16 refuses to optimize images from local/loopback IPs (SSRF guard) even
|
|
7
|
+
// when remotePatterns matches — /_next/image 400s. Local dev serves Strapi media
|
|
8
|
+
// from localhost, so opt in ONLY when the media host is loopback.
|
|
9
|
+
const mediaHostname = new URL(mediaHost).hostname;
|
|
10
|
+
const allowLocalIP = /^(localhost|127\.|0\.0\.0\.0|::1|\[::1\])/.test(mediaHostname);
|
|
11
|
+
|
|
12
|
+
const nextConfig: NextConfig = {
|
|
13
|
+
images: {
|
|
14
|
+
remotePatterns: [new URL(`${mediaHost}/**`)],
|
|
15
|
+
...(allowLocalIP ? { dangerouslyAllowLocalIP: true } : {}),
|
|
16
|
+
},
|
|
17
|
+
transpilePackages: ["@dani-builder/strapi-client", "@dani-builder/blocks"],
|
|
18
|
+
};
|
|
19
|
+
export default nextConfig;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@dani-builder/blocks": "latest",
|
|
12
|
+
"@dani-builder/strapi-client": "latest",
|
|
13
|
+
"next": "^16",
|
|
14
|
+
"react": "^19",
|
|
15
|
+
"react-dom": "^19"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@tailwindcss/postcss": "^4",
|
|
19
|
+
"@types/node": "^26",
|
|
20
|
+
"@types/react": "^19",
|
|
21
|
+
"@types/react-dom": "^19",
|
|
22
|
+
"tailwindcss": "^4",
|
|
23
|
+
"typescript": "^6.0.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { plugins: { "@tailwindcss/postcss": {} } };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022", "lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"module": "esnext", "moduleResolution": "bundler", "jsx": "preserve",
|
|
5
|
+
"strict": true, "noEmit": true, "esModuleInterop": true, "skipLibCheck": true,
|
|
6
|
+
"incremental": true, "plugins": [{ "name": "next" }],
|
|
7
|
+
"paths": { "@/*": ["./*"] }
|
|
8
|
+
},
|
|
9
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
10
|
+
"exclude": ["node_modules"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"packageManager": "pnpm@11.21.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.13"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "turbo run dev",
|
|
10
|
+
"build": "turbo run build"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"turbo": "^2"
|
|
14
|
+
}
|
|
15
|
+
}
|