create-wp-reactor 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/bin/create-app.js +61 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/templates/client/.env.example +7 -0
- package/templates/client/.github/workflows/ci.yml +24 -0
- package/templates/client/.github/workflows/deploy.yml +43 -0
- package/templates/client/README.md +40 -0
- package/templates/client/apps/webapp/.env.example +10 -0
- package/templates/client/apps/webapp/README.md +14 -0
- package/templates/client/apps/webapp/blocks/cta-banner/block.json +39 -0
- package/templates/client/apps/webapp/blocks/cta-banner/edit.tsx +65 -0
- package/templates/client/apps/webapp/blocks/cta-banner/editor.scss +3 -0
- package/templates/client/apps/webapp/blocks/cta-banner/index.tsx +9 -0
- package/templates/client/apps/webapp/blocks/cta-banner/render.php +11 -0
- package/templates/client/apps/webapp/blocks/cta-banner/save.tsx +3 -0
- package/templates/client/apps/webapp/package.json +44 -0
- package/templates/client/apps/webapp/schemas/cta-banner.json +41 -0
- package/templates/client/apps/webapp/src/authOps.ts +17 -0
- package/templates/client/apps/webapp/src/blocks.tsx +29 -0
- package/templates/client/apps/webapp/src/cart.ts +28 -0
- package/templates/client/apps/webapp/src/env.ts +28 -0
- package/templates/client/apps/webapp/src/renderers/CtaBannerBlock.tsx +20 -0
- package/templates/client/apps/webapp/src/router.tsx +42 -0
- package/templates/client/apps/webapp/src/routes/__root.tsx +45 -0
- package/templates/client/apps/webapp/src/routes/index.tsx +51 -0
- package/templates/client/apps/webapp/src/routes/preview.tsx +32 -0
- package/templates/client/apps/webapp/src/server/auth.ts +66 -0
- package/templates/client/apps/webapp/src/server/authMiddleware.ts +79 -0
- package/templates/client/apps/webapp/src/server/cart.ts +68 -0
- package/templates/client/apps/webapp/src/server/cartMiddleware.ts +60 -0
- package/templates/client/apps/webapp/src/server/session.ts +34 -0
- package/templates/client/apps/webapp/src/start.ts +19 -0
- package/templates/client/apps/webapp/src/styles.css +1 -0
- package/templates/client/apps/webapp/tsconfig.json +12 -0
- package/templates/client/apps/webapp/vite.config.ts +49 -0
- package/templates/client/apps/webapp/wp-reactor.config.json +13 -0
- package/templates/client/apps/wordpress/Dockerfile +11 -0
- package/templates/client/apps/wordpress/docker/child-entrypoint.sh +13 -0
- package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/functions.php +12 -0
- package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/index.php +2 -0
- package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/src/blocks/.gitkeep +0 -0
- package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/style.css +8 -0
- package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/theme.json +5 -0
- package/templates/client/gitignore +14 -0
- package/templates/client/npmrc +5 -0
- package/templates/client/package.json +20 -0
- package/templates/client/pnpm-workspace.yaml +3 -0
- package/templates/client/turbo.json +9 -0
- package/templates/client/wp-reactor.config.json +9 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
// Charge la source TS via tsx (dev), sinon le build dist.
|
|
5
|
+
async function loadApi() {
|
|
6
|
+
try {
|
|
7
|
+
return await import("../src/index.ts");
|
|
8
|
+
} catch {
|
|
9
|
+
return await import("../dist/index.js");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const out = { projectName: null, dir: null, namespace: null, range: null };
|
|
15
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (arg === "--dir") out.dir = argv[++i];
|
|
18
|
+
else if (arg === "--namespace") out.namespace = argv[++i];
|
|
19
|
+
else if (arg === "--range") out.range = argv[++i];
|
|
20
|
+
else if (!arg.startsWith("--") && !out.projectName) out.projectName = arg;
|
|
21
|
+
else {
|
|
22
|
+
console.error(`Unknown option: ${arg}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const args = parseArgs(process.argv.slice(2));
|
|
31
|
+
if (!args.projectName) {
|
|
32
|
+
console.error(
|
|
33
|
+
"Usage:\n create-wp-reactor <project-name> [--dir <path>] [--namespace <ns>] [--range <semver>]",
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { scaffold } = await loadApi();
|
|
39
|
+
const targetDir = args.dir
|
|
40
|
+
? resolve(args.dir)
|
|
41
|
+
: resolve(process.cwd(), args.projectName);
|
|
42
|
+
|
|
43
|
+
const result = scaffold({
|
|
44
|
+
projectName: args.projectName,
|
|
45
|
+
targetDir,
|
|
46
|
+
namespace: args.namespace ?? undefined,
|
|
47
|
+
pkgRange: args.range ?? undefined,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log(`\n✓ Repo client généré : ${result.targetDir}`);
|
|
51
|
+
console.log(` ${result.written.length} entrées écrites`);
|
|
52
|
+
console.log("\nProchaines étapes :");
|
|
53
|
+
console.log(` cd ${args.projectName}`);
|
|
54
|
+
console.log(" pnpm install # nécessite l'auth GitHub Packages (read:packages)");
|
|
55
|
+
console.log(" pnpm dev");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
main().catch((error) => {
|
|
59
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare const TEMPLATES_DIR: string;
|
|
2
|
+
interface ScaffoldOptions {
|
|
3
|
+
/** Nom kebab du projet (ex. "client-2") → noms de packages, dossiers, thème. */
|
|
4
|
+
projectName: string;
|
|
5
|
+
/** Dossier cible (créé). */
|
|
6
|
+
targetDir: string;
|
|
7
|
+
/** Namespace des blocs gen-block. Défaut : projectName sans tirets. */
|
|
8
|
+
namespace?: string;
|
|
9
|
+
/** Override du dossier template (tests). */
|
|
10
|
+
templatesDir?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ScaffoldResult {
|
|
13
|
+
targetDir: string;
|
|
14
|
+
written: string[];
|
|
15
|
+
}
|
|
16
|
+
declare function scaffold(opts: ScaffoldOptions): ScaffoldResult;
|
|
17
|
+
|
|
18
|
+
export { type ScaffoldOptions, type ScaffoldResult, TEMPLATES_DIR, scaffold };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync
|
|
8
|
+
} from "fs";
|
|
9
|
+
import { dirname, join, resolve } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
var TEMPLATES_DIR = resolve(HERE, "../templates/client");
|
|
13
|
+
function titleCase(kebab) {
|
|
14
|
+
return kebab.split(/[-_]/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
15
|
+
}
|
|
16
|
+
function applyTokens(input, tokens) {
|
|
17
|
+
let out = input;
|
|
18
|
+
for (const [key, value] of Object.entries(tokens)) {
|
|
19
|
+
out = out.split(key).join(value);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
var DOTFILES = {
|
|
24
|
+
gitignore: ".gitignore",
|
|
25
|
+
npmrc: ".npmrc"
|
|
26
|
+
};
|
|
27
|
+
function copyTemplateTree(srcDir, destDir, tokens, written) {
|
|
28
|
+
for (const entry of readdirSync(srcDir)) {
|
|
29
|
+
const srcPath = join(srcDir, entry);
|
|
30
|
+
const destName = DOTFILES[entry] ?? applyTokens(entry, tokens);
|
|
31
|
+
const destPath = join(destDir, destName);
|
|
32
|
+
if (statSync(srcPath).isDirectory()) {
|
|
33
|
+
mkdirSync(destPath, { recursive: true });
|
|
34
|
+
copyTemplateTree(srcPath, destPath, tokens, written);
|
|
35
|
+
} else {
|
|
36
|
+
const content = applyTokens(readFileSync(srcPath, "utf8"), tokens);
|
|
37
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
38
|
+
writeFileSync(destPath, content);
|
|
39
|
+
written.push(destPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function scaffold(opts) {
|
|
44
|
+
const projectName = opts.projectName;
|
|
45
|
+
const namespace = opts.namespace ?? projectName.replace(/[-_]/g, "");
|
|
46
|
+
const templatesDir = opts.templatesDir ?? TEMPLATES_DIR;
|
|
47
|
+
const targetDir = resolve(opts.targetDir);
|
|
48
|
+
const tokens = {
|
|
49
|
+
__PROJECT_NAME__: projectName,
|
|
50
|
+
__PROJECT_TITLE__: titleCase(projectName),
|
|
51
|
+
__BLOCK_NAMESPACE__: namespace
|
|
52
|
+
};
|
|
53
|
+
const written = [];
|
|
54
|
+
mkdirSync(targetDir, { recursive: true });
|
|
55
|
+
copyTemplateTree(templatesDir, targetDir, tokens, written);
|
|
56
|
+
return { targetDir, written };
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
TEMPLATES_DIR,
|
|
60
|
+
scaffold
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// create-wp-reactor — scaffolder de repo client mince.\n//\n// Émet un monorepo client « 2 mondes » depuis un template BUNDLÉ (npx-friendly :\n// aucun accès au monorepo framework au runtime) :\n// • apps/webapp ← coque (starter synchronisé dans templates/, voir\n// scripts/sync-template.mjs)\n// • apps/wordpress ← thème ENFANT\n// • .github, docker, wp-reactor.config.json, workspace root\n//\n// Le runtime ne fait que copier templates/client/ en substituant les tokens.\n\nimport {\n\tmkdirSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst HERE = dirname(fileURLToPath(import.meta.url));\n// src/ (dev via tsx) comme dist/ (build) sont à 1 niveau du package.\nconst TEMPLATES_DIR = resolve(HERE, \"../templates/client\");\n\nexport interface ScaffoldOptions {\n\t/** Nom kebab du projet (ex. \"client-2\") → noms de packages, dossiers, thème. */\n\tprojectName: string;\n\t/** Dossier cible (créé). */\n\ttargetDir: string;\n\t/** Namespace des blocs gen-block. Défaut : projectName sans tirets. */\n\tnamespace?: string;\n\t/** Override du dossier template (tests). */\n\ttemplatesDir?: string;\n}\n\nexport interface ScaffoldResult {\n\ttargetDir: string;\n\twritten: string[];\n}\n\nfunction titleCase(kebab: string): string {\n\treturn kebab\n\t\t.split(/[-_]/)\n\t\t.filter(Boolean)\n\t\t.map((w) => w[0].toUpperCase() + w.slice(1))\n\t\t.join(\" \");\n}\n\nfunction applyTokens(input: string, tokens: Record<string, string>): string {\n\tlet out = input;\n\tfor (const [key, value] of Object.entries(tokens)) {\n\t\tout = out.split(key).join(value);\n\t}\n\treturn out;\n}\n\n// npm strippe `.gitignore` et `.npmrc` des tarballs publiés : on les stocke\n// sans point dans le template et on les re-dotte à l'écriture.\nconst DOTFILES: Record<string, string> = {\n\tgitignore: \".gitignore\",\n\tnpmrc: \".npmrc\",\n};\n\n/** Copie récursive du template en substituant les tokens dans chemins + contenu. */\nfunction copyTemplateTree(\n\tsrcDir: string,\n\tdestDir: string,\n\ttokens: Record<string, string>,\n\twritten: string[],\n): void {\n\tfor (const entry of readdirSync(srcDir)) {\n\t\tconst srcPath = join(srcDir, entry);\n\t\tconst destName = DOTFILES[entry] ?? applyTokens(entry, tokens);\n\t\tconst destPath = join(destDir, destName);\n\t\tif (statSync(srcPath).isDirectory()) {\n\t\t\tmkdirSync(destPath, { recursive: true });\n\t\t\tcopyTemplateTree(srcPath, destPath, tokens, written);\n\t\t} else {\n\t\t\tconst content = applyTokens(readFileSync(srcPath, \"utf8\"), tokens);\n\t\t\tmkdirSync(dirname(destPath), { recursive: true });\n\t\t\twriteFileSync(destPath, content);\n\t\t\twritten.push(destPath);\n\t\t}\n\t}\n}\n\nexport function scaffold(opts: ScaffoldOptions): ScaffoldResult {\n\tconst projectName = opts.projectName;\n\tconst namespace = opts.namespace ?? projectName.replace(/[-_]/g, \"\");\n\tconst templatesDir = opts.templatesDir ?? TEMPLATES_DIR;\n\tconst targetDir = resolve(opts.targetDir);\n\tconst tokens: Record<string, string> = {\n\t\t__PROJECT_NAME__: projectName,\n\t\t__PROJECT_TITLE__: titleCase(projectName),\n\t\t__BLOCK_NAMESPACE__: namespace,\n\t};\n\tconst written: string[] = [];\n\n\tmkdirSync(targetDir, { recursive: true });\n\tcopyTemplateTree(templatesDir, targetDir, tokens, written);\n\n\treturn { targetDir, written };\n}\n\nexport { TEMPLATES_DIR };\n"],"mappings":";AAWA;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,SAAS,MAAM,eAAe;AACvC,SAAS,qBAAqB;AAE9B,IAAM,OAAO,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEnD,IAAM,gBAAgB,QAAQ,MAAM,qBAAqB;AAkBzD,SAAS,UAAU,OAAuB;AACzC,SAAO,MACL,MAAM,MAAM,EACZ,OAAO,OAAO,EACd,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,CAAC,EAC1C,KAAK,GAAG;AACX;AAEA,SAAS,YAAY,OAAe,QAAwC;AAC3E,MAAI,MAAM;AACV,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AAClD,UAAM,IAAI,MAAM,GAAG,EAAE,KAAK,KAAK;AAAA,EAChC;AACA,SAAO;AACR;AAIA,IAAM,WAAmC;AAAA,EACxC,WAAW;AAAA,EACX,OAAO;AACR;AAGA,SAAS,iBACR,QACA,SACA,QACA,SACO;AACP,aAAW,SAAS,YAAY,MAAM,GAAG;AACxC,UAAM,UAAU,KAAK,QAAQ,KAAK;AAClC,UAAM,WAAW,SAAS,KAAK,KAAK,YAAY,OAAO,MAAM;AAC7D,UAAM,WAAW,KAAK,SAAS,QAAQ;AACvC,QAAI,SAAS,OAAO,EAAE,YAAY,GAAG;AACpC,gBAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AACvC,uBAAiB,SAAS,UAAU,QAAQ,OAAO;AAAA,IACpD,OAAO;AACN,YAAM,UAAU,YAAY,aAAa,SAAS,MAAM,GAAG,MAAM;AACjE,gBAAU,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAChD,oBAAc,UAAU,OAAO;AAC/B,cAAQ,KAAK,QAAQ;AAAA,IACtB;AAAA,EACD;AACD;AAEO,SAAS,SAAS,MAAuC;AAC/D,QAAM,cAAc,KAAK;AACzB,QAAM,YAAY,KAAK,aAAa,YAAY,QAAQ,SAAS,EAAE;AACnE,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,YAAY,QAAQ,KAAK,SAAS;AACxC,QAAM,SAAiC;AAAA,IACtC,kBAAkB;AAAA,IAClB,mBAAmB,UAAU,WAAW;AAAA,IACxC,qBAAqB;AAAA,EACtB;AACA,QAAM,UAAoB,CAAC;AAE3B,YAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AACxC,mBAAiB,cAAc,WAAW,QAAQ,OAAO;AAEzD,SAAO,EAAE,WAAW,QAAQ;AAC7B;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-wp-reactor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Scaffolder de repo client mince WP Reactor : émet apps/webapp (depuis le starter) + thème enfant + CI réutilisable + docker. Usage : npm create wp-reactor <nom>.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-wp-reactor": "./bin/create-app.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"bin",
|
|
15
|
+
"templates"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"sync": "node scripts/sync-template.mjs",
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"release:npm": "node scripts/publish-npm.mjs",
|
|
21
|
+
"prepack": "node scripts/sync-template.mjs",
|
|
22
|
+
"check-types": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"lint": "biome lint .",
|
|
25
|
+
"format": "biome format --write ."
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.10.2",
|
|
29
|
+
"@wp-reactor/config-biome": "workspace:*",
|
|
30
|
+
"@wp-reactor/config-tsconfig": "workspace:*",
|
|
31
|
+
"tsup": "^8.5.1",
|
|
32
|
+
"typescript": "5.9.2",
|
|
33
|
+
"vitest": "^4.1.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"registry": "https://registry.npmjs.org",
|
|
38
|
+
"main": "./dist/index.js",
|
|
39
|
+
"module": "./dist/index.js",
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"import": "./dist/index.js"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
jobs:
|
|
7
|
+
build:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: pnpm/action-setup@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
|
+
with:
|
|
14
|
+
node-version: 22
|
|
15
|
+
cache: pnpm
|
|
16
|
+
registry-url: https://npm.pkg.github.com
|
|
17
|
+
scope: "@wp-reactor"
|
|
18
|
+
- run: pnpm install --frozen-lockfile
|
|
19
|
+
env:
|
|
20
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
21
|
+
- run: pnpm build
|
|
22
|
+
env:
|
|
23
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
24
|
+
- run: pnpm check-types
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Build & deploy
|
|
2
|
+
|
|
3
|
+
# Appelle les workflows RÉUTILISABLES du framework WP Reactor — aucune logique
|
|
4
|
+
# CI dupliquée ici. Voir wp-reactor/wp-reactor/.github/workflows/build-and-deploy-image.yml
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
webapp:
|
|
12
|
+
uses: wp-reactor/wp-reactor/.github/workflows/build-and-deploy-image.yml@main
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
packages: write
|
|
16
|
+
with:
|
|
17
|
+
image: ghcr.io/${{ github.repository }}/webapp
|
|
18
|
+
dockerfile: apps/webapp/Dockerfile
|
|
19
|
+
build-args: |
|
|
20
|
+
NODE_AUTH_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
|
21
|
+
VITE_WORDPRESS_URL=${{ vars.VITE_WORDPRESS_URL }}
|
|
22
|
+
VITE_GRAPHQL_PATH=${{ vars.VITE_GRAPHQL_PATH }}
|
|
23
|
+
VITE_FEATURE_EDITORIAL=${{ vars.VITE_FEATURE_EDITORIAL }}
|
|
24
|
+
VITE_FEATURE_ECOMMERCE=${{ vars.VITE_FEATURE_ECOMMERCE }}
|
|
25
|
+
VITE_FEATURE_AUTH=${{ vars.VITE_FEATURE_AUTH }}
|
|
26
|
+
SESSION_SECRET=${{ secrets.SESSION_SECRET }}
|
|
27
|
+
secrets:
|
|
28
|
+
COOLIFY_URL: ${{ secrets.COOLIFY_URL }}
|
|
29
|
+
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
|
|
30
|
+
COOLIFY_UUID: ${{ secrets.COOLIFY_WEBAPP_UUID }}
|
|
31
|
+
|
|
32
|
+
wordpress:
|
|
33
|
+
uses: wp-reactor/wp-reactor/.github/workflows/build-and-deploy-image.yml@main
|
|
34
|
+
permissions:
|
|
35
|
+
contents: read
|
|
36
|
+
packages: write
|
|
37
|
+
with:
|
|
38
|
+
image: ghcr.io/${{ github.repository }}/wordpress
|
|
39
|
+
dockerfile: apps/wordpress/Dockerfile
|
|
40
|
+
secrets:
|
|
41
|
+
COOLIFY_URL: ${{ secrets.COOLIFY_URL }}
|
|
42
|
+
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
|
|
43
|
+
COOLIFY_UUID: ${{ secrets.COOLIFY_WORDPRESS_UUID }}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# __PROJECT_TITLE__
|
|
2
|
+
|
|
3
|
+
Repo client **WP Reactor** (framework headless WordPress) — monorepo léger « 2 mondes ».
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
apps/webapp/ coque TanStack Start (deps @wp-reactor/* publiées)
|
|
7
|
+
apps/wordpress/theme-__PROJECT_NAME__/ thème ENFANT (branding + blocs propres)
|
|
8
|
+
apps/wordpress/Dockerfile image WP = base framework + thème enfant overlayé
|
|
9
|
+
.github/workflows/deploy.yml appelle les workflows réutilisables du framework
|
|
10
|
+
wp-reactor.config.json config gen-block (namespace, chemins)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Prérequis
|
|
14
|
+
|
|
15
|
+
Les packages `@wp-reactor/*` sont sur **GitHub Packages** (privé, org `wp-reactor`).
|
|
16
|
+
Authentifie-toi avant `pnpm install` :
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
echo "//npm.pkg.github.com/:_authToken=<PAT read:packages>" >> ~/.npmrc
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Dev
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm install
|
|
26
|
+
cp .env.example .env # renseigne VITE_WORDPRESS_URL, SESSION_SECRET…
|
|
27
|
+
pnpm dev # webapp sur :3000
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Générer un bloc
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm gen:block schemas/<slug>.json # écrit thème enfant + renderer webapp + registre
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Déploiement
|
|
37
|
+
|
|
38
|
+
Push sur `main` → `.github/workflows/deploy.yml` build+push les images webapp & WordPress
|
|
39
|
+
sur GHCR via les workflows réutilisables du framework, puis déclenche Coolify (secrets
|
|
40
|
+
`COOLIFY_*` requis).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# WordPress headless endpoint consommé par @wp-reactor/headless-core
|
|
2
|
+
VITE_WORDPRESS_URL=https://wp.example.test
|
|
3
|
+
|
|
4
|
+
# Modules métier activés côté webapp (cf. docs/new-client-project du plan)
|
|
5
|
+
VITE_FEATURE_EDITORIAL=true
|
|
6
|
+
VITE_FEATURE_ECOMMERCE=false
|
|
7
|
+
VITE_FEATURE_AUTH=false
|
|
8
|
+
|
|
9
|
+
# Hôte autorisé supplémentaire (dev/staging)
|
|
10
|
+
# VITE_ALLOWED_HOST=
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @wp-reactor/webapp-template
|
|
2
|
+
|
|
3
|
+
Starter TanStack Start du framework WP Reactor — l'app de référence minimale, CI-testée, dont
|
|
4
|
+
chaque repo client dérive sa coque (`apps/webapp`).
|
|
5
|
+
|
|
6
|
+
🚧 Squelette : route d'accueil unique. Le câblage WordPress (clients GraphQL, cache, providers)
|
|
7
|
+
arrive avec la distillation de `@wp-reactor/headless-core` puis des modules métier.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install # à la racine du monorepo
|
|
11
|
+
pnpm --filter @wp-reactor/webapp-template dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Source de référence distillée : `.reference/namaki/apps/webapp`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schemas.wp.org/trunk/block.json",
|
|
3
|
+
"apiVersion": 3,
|
|
4
|
+
"name": "wp-reactor/cta-banner",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"title": "CTA Banner",
|
|
7
|
+
"category": "design",
|
|
8
|
+
"icon": "megaphone",
|
|
9
|
+
"description": "Bannière d'appel à l'action",
|
|
10
|
+
"textdomain": "wp-reactor",
|
|
11
|
+
"editorScript": "file:./index.js",
|
|
12
|
+
"editorStyle": "file:./editor.css",
|
|
13
|
+
"render": "file:./render.php",
|
|
14
|
+
"attributes": {
|
|
15
|
+
"heading": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"default": "Titre"
|
|
18
|
+
},
|
|
19
|
+
"body": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"default": ""
|
|
22
|
+
},
|
|
23
|
+
"inverted": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"default": false
|
|
26
|
+
},
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "array",
|
|
29
|
+
"default": []
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"supports": {
|
|
33
|
+
"html": false,
|
|
34
|
+
"align": [
|
|
35
|
+
"wide",
|
|
36
|
+
"full"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useBlockProps } from "@wordpress/block-editor";
|
|
2
|
+
import type { BlockEditProps } from "@wordpress/blocks";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
BlockEditModal,
|
|
6
|
+
BlockEditOverlay,
|
|
7
|
+
FieldControl,
|
|
8
|
+
FrontPreviewFrame,
|
|
9
|
+
} from "@wp-reactor/block-editor";
|
|
10
|
+
|
|
11
|
+
export interface CtaBannerBlockAttributes {
|
|
12
|
+
heading: string;
|
|
13
|
+
body: string;
|
|
14
|
+
inverted: boolean;
|
|
15
|
+
items: unknown[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Edit({
|
|
19
|
+
attributes,
|
|
20
|
+
setAttributes,
|
|
21
|
+
}: BlockEditProps<CtaBannerBlockAttributes>) {
|
|
22
|
+
const blockProps = useBlockProps({ className: "wp-reactor-cta-banner-editor" });
|
|
23
|
+
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
24
|
+
|
|
25
|
+
const fields = (
|
|
26
|
+
<>
|
|
27
|
+
<FieldControl
|
|
28
|
+
field={{ key: "heading", label: "Titre", ui: "TextInput" }}
|
|
29
|
+
value={attributes.heading}
|
|
30
|
+
onChange={(value) => setAttributes({ heading: value as string })}
|
|
31
|
+
/>
|
|
32
|
+
<FieldControl
|
|
33
|
+
field={{ key: "body", label: "Texte", ui: "RichText" }}
|
|
34
|
+
value={attributes.body}
|
|
35
|
+
onChange={(value) => setAttributes({ body: value as string })}
|
|
36
|
+
/>
|
|
37
|
+
<FieldControl
|
|
38
|
+
field={{ key: "inverted", label: "Thème inversé", ui: "Toggle" }}
|
|
39
|
+
value={attributes.inverted}
|
|
40
|
+
onChange={(value) => setAttributes({ inverted: value as boolean })}
|
|
41
|
+
/>
|
|
42
|
+
<FieldControl
|
|
43
|
+
field={{ key: "items", label: "Liens", ui: "ArrayRepeater" }}
|
|
44
|
+
value={attributes.items}
|
|
45
|
+
onChange={(value) => setAttributes({ items: value as unknown[] })}
|
|
46
|
+
/>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
{isModalOpen && (
|
|
53
|
+
<BlockEditModal title="CTA Banner" onClose={() => setIsModalOpen(false)}>
|
|
54
|
+
{fields}
|
|
55
|
+
</BlockEditModal>
|
|
56
|
+
)}
|
|
57
|
+
<BlockEditOverlay blockProps={blockProps} onEditClick={() => setIsModalOpen(true)}>
|
|
58
|
+
<FrontPreviewFrame
|
|
59
|
+
block="wp-reactor/cta-banner"
|
|
60
|
+
attributes={attributes as Record<string, unknown>}
|
|
61
|
+
/>
|
|
62
|
+
</BlockEditOverlay>
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { registerBlockType } from "@wordpress/blocks";
|
|
2
|
+
import metadata from "./block.json";
|
|
3
|
+
import { Edit } from "./edit";
|
|
4
|
+
import { save } from "./save";
|
|
5
|
+
|
|
6
|
+
import "./editor.scss";
|
|
7
|
+
|
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: WP block registration typing
|
|
9
|
+
registerBlockType(metadata.name, { ...metadata, edit: Edit, save } as any);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Server render for wp-reactor/cta-banner.
|
|
4
|
+
* Headless : le rendu réel est fait par la webapp (@wp-reactor/editorial).
|
|
5
|
+
* Ce wrapper expose juste les attributs au front via le bloc éditeur.
|
|
6
|
+
*/
|
|
7
|
+
if (!defined('ABSPATH')) {
|
|
8
|
+
exit;
|
|
9
|
+
}
|
|
10
|
+
?>
|
|
11
|
+
<div <?php echo get_block_wrapper_attributes(); ?>></div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__PROJECT_NAME__/webapp",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Starter TanStack Start du framework WP Reactor — app de référence minimale, CI-testée.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev --port 3000",
|
|
8
|
+
"build": "vite build && tsc --noEmit",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"check-types": "tsc --noEmit",
|
|
11
|
+
"format": "biome format --write .",
|
|
12
|
+
"lint": "biome lint ."
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@tanstack/react-query": "^5.66.5",
|
|
16
|
+
"@tanstack/react-router": "^1.170.8",
|
|
17
|
+
"@tanstack/react-router-ssr-query": "^1.167.0",
|
|
18
|
+
"@tanstack/react-start": "^1.168.14",
|
|
19
|
+
"@tanstack/router-plugin": "^1.168.11",
|
|
20
|
+
"@tailwindcss/vite": "^4.2.4",
|
|
21
|
+
"@wp-reactor/auth": "^0.1.0",
|
|
22
|
+
"@wp-reactor/commerce": "^0.1.0",
|
|
23
|
+
"@wp-reactor/editorial": "^0.1.0",
|
|
24
|
+
"@wp-reactor/headless-core": "^0.1.0",
|
|
25
|
+
"@wp-reactor/ui": "^0.1.0",
|
|
26
|
+
"graphql": "^16.10.0",
|
|
27
|
+
"graphql-request": "^7.4.0",
|
|
28
|
+
"nitro": "npm:nitro-nightly@3.0.1-20260601-173724-a2597363",
|
|
29
|
+
"react": "^19.2.0",
|
|
30
|
+
"react-dom": "^19.2.0",
|
|
31
|
+
"tailwindcss": "^4.2.4"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "2.2.4",
|
|
35
|
+
"@types/node": "^22.10.2",
|
|
36
|
+
"@types/react": "^19.2.0",
|
|
37
|
+
"@types/react-dom": "^19.2.0",
|
|
38
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
39
|
+
"@wp-reactor/config-biome": "^0.1.0",
|
|
40
|
+
"@wp-reactor/config-tsconfig": "^0.1.0",
|
|
41
|
+
"typescript": "5.9.2",
|
|
42
|
+
"vite": "^8.0.2"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://wp-reactor.dev/block-contract.schema.json",
|
|
3
|
+
"schemaVersion": "1.0.0",
|
|
4
|
+
"slug": "cta-banner",
|
|
5
|
+
"title": "CTA Banner",
|
|
6
|
+
"category": "design",
|
|
7
|
+
"icon": "megaphone",
|
|
8
|
+
"description": "Bannière d'appel à l'action",
|
|
9
|
+
"attributes": {
|
|
10
|
+
"heading": {
|
|
11
|
+
"ui": "TextInput",
|
|
12
|
+
"default": "Titre",
|
|
13
|
+
"label": "Titre"
|
|
14
|
+
},
|
|
15
|
+
"body": {
|
|
16
|
+
"ui": "RichText",
|
|
17
|
+
"label": "Texte"
|
|
18
|
+
},
|
|
19
|
+
"inverted": {
|
|
20
|
+
"ui": "Toggle",
|
|
21
|
+
"default": false,
|
|
22
|
+
"label": "Thème inversé"
|
|
23
|
+
},
|
|
24
|
+
"items": {
|
|
25
|
+
"ui": "ArrayRepeater",
|
|
26
|
+
"label": "Liens",
|
|
27
|
+
"fields": [
|
|
28
|
+
{
|
|
29
|
+
"key": "label",
|
|
30
|
+
"ui": "TextInput",
|
|
31
|
+
"label": "Libellé"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"key": "url",
|
|
35
|
+
"ui": "TextInput",
|
|
36
|
+
"label": "URL"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AuthOperations } from "@wp-reactor/auth";
|
|
2
|
+
import {
|
|
3
|
+
getCurrentUserServerFn,
|
|
4
|
+
loginServerFn,
|
|
5
|
+
logoutServerFn,
|
|
6
|
+
} from "./server/auth";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Opérations injectées dans `AuthProvider` : la coque branche le provider du
|
|
10
|
+
* package sur SES serverFns (Option B). Le transform Start retire le handler
|
|
11
|
+
* serveur du bundle client — importer la référence serverFn ici est sûr.
|
|
12
|
+
*/
|
|
13
|
+
export const authOperations: AuthOperations = {
|
|
14
|
+
getCurrentUser: () => getCurrentUserServerFn(),
|
|
15
|
+
login: (input) => loginServerFn({ data: input }),
|
|
16
|
+
logout: () => logoutServerFn(),
|
|
17
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createBlockRegistry } from "@wp-reactor/editorial";
|
|
2
|
+
import { isDev } from "./env";
|
|
3
|
+
import { CtaBannerBlock } from "./renderers/CtaBannerBlock";
|
|
4
|
+
|
|
5
|
+
// Bloc démo du starter. Un vrai client a un bloc par `block.json` ; gen-block
|
|
6
|
+
// écrit les `register*` ci-dessous depuis ces block.json (source unique).
|
|
7
|
+
function HeroBlock({ attributes }: { attributes: { title?: unknown } }) {
|
|
8
|
+
return (
|
|
9
|
+
<section className="bg-neutral-900 px-6 py-20 text-center text-white">
|
|
10
|
+
<h1 className="text-4xl font-bold">
|
|
11
|
+
{attributes.title ? String(attributes.title) : "Hero"}
|
|
12
|
+
</h1>
|
|
13
|
+
</section>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registre de blocs DU CLIENT. Le framework fournit le mécanisme
|
|
19
|
+
* (`createBlockRegistry`) ; ce fichier déclare le set propre au site. gen-block
|
|
20
|
+
* insère ses renderers au marqueur, dans la chaîne, avant le `;`.
|
|
21
|
+
*/
|
|
22
|
+
export const blockRegistry = createBlockRegistry({
|
|
23
|
+
namespace: "wp-reactor",
|
|
24
|
+
dev: isDev,
|
|
25
|
+
})
|
|
26
|
+
.registerAttributeBlock("hero", HeroBlock)
|
|
27
|
+
.registerAttributeBlock("cta-banner", CtaBannerBlock, { "heading": "string", "body": "string", "inverted": "boolean", "items": "array" })
|
|
28
|
+
// @gen:block:block-registry
|
|
29
|
+
;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useWooCart,
|
|
3
|
+
type UseWooCartResult,
|
|
4
|
+
type WooCartActions,
|
|
5
|
+
} from "@wp-reactor/commerce";
|
|
6
|
+
import { isSsr } from "./env";
|
|
7
|
+
import {
|
|
8
|
+
addCartItem,
|
|
9
|
+
applyCoupon,
|
|
10
|
+
getCart,
|
|
11
|
+
removeCartItems,
|
|
12
|
+
removeCoupons,
|
|
13
|
+
updateCartItemQuantities,
|
|
14
|
+
} from "./server/cart";
|
|
15
|
+
|
|
16
|
+
// La coque branche le hook panier du package sur SES serverFns (Option B).
|
|
17
|
+
const actions: WooCartActions = {
|
|
18
|
+
addCartItem: (input) => addCartItem({ data: input }),
|
|
19
|
+
updateCartItemQuantities: (items) =>
|
|
20
|
+
updateCartItemQuantities({ data: { items } }),
|
|
21
|
+
removeCartItems: (keys) => removeCartItems({ data: { keys } }),
|
|
22
|
+
applyCoupon: (code) => applyCoupon({ data: { code } }),
|
|
23
|
+
removeCoupons: (codes) => removeCoupons({ data: { codes } }),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function useCart(): UseWooCartResult {
|
|
27
|
+
return useWooCart({ getCart: () => getCart(), ssr: isSsr, actions });
|
|
28
|
+
}
|