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.
Files changed (51) hide show
  1. package/bin/create-app.js +61 -0
  2. package/dist/index.d.ts +18 -0
  3. package/dist/index.js +62 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +48 -0
  6. package/templates/client/.env.example +7 -0
  7. package/templates/client/.github/workflows/ci.yml +24 -0
  8. package/templates/client/.github/workflows/deploy.yml +43 -0
  9. package/templates/client/README.md +40 -0
  10. package/templates/client/apps/webapp/.env.example +10 -0
  11. package/templates/client/apps/webapp/README.md +14 -0
  12. package/templates/client/apps/webapp/blocks/cta-banner/block.json +39 -0
  13. package/templates/client/apps/webapp/blocks/cta-banner/edit.tsx +65 -0
  14. package/templates/client/apps/webapp/blocks/cta-banner/editor.scss +3 -0
  15. package/templates/client/apps/webapp/blocks/cta-banner/index.tsx +9 -0
  16. package/templates/client/apps/webapp/blocks/cta-banner/render.php +11 -0
  17. package/templates/client/apps/webapp/blocks/cta-banner/save.tsx +3 -0
  18. package/templates/client/apps/webapp/package.json +44 -0
  19. package/templates/client/apps/webapp/schemas/cta-banner.json +41 -0
  20. package/templates/client/apps/webapp/src/authOps.ts +17 -0
  21. package/templates/client/apps/webapp/src/blocks.tsx +29 -0
  22. package/templates/client/apps/webapp/src/cart.ts +28 -0
  23. package/templates/client/apps/webapp/src/env.ts +28 -0
  24. package/templates/client/apps/webapp/src/renderers/CtaBannerBlock.tsx +20 -0
  25. package/templates/client/apps/webapp/src/router.tsx +42 -0
  26. package/templates/client/apps/webapp/src/routes/__root.tsx +45 -0
  27. package/templates/client/apps/webapp/src/routes/index.tsx +51 -0
  28. package/templates/client/apps/webapp/src/routes/preview.tsx +32 -0
  29. package/templates/client/apps/webapp/src/server/auth.ts +66 -0
  30. package/templates/client/apps/webapp/src/server/authMiddleware.ts +79 -0
  31. package/templates/client/apps/webapp/src/server/cart.ts +68 -0
  32. package/templates/client/apps/webapp/src/server/cartMiddleware.ts +60 -0
  33. package/templates/client/apps/webapp/src/server/session.ts +34 -0
  34. package/templates/client/apps/webapp/src/start.ts +19 -0
  35. package/templates/client/apps/webapp/src/styles.css +1 -0
  36. package/templates/client/apps/webapp/tsconfig.json +12 -0
  37. package/templates/client/apps/webapp/vite.config.ts +49 -0
  38. package/templates/client/apps/webapp/wp-reactor.config.json +13 -0
  39. package/templates/client/apps/wordpress/Dockerfile +11 -0
  40. package/templates/client/apps/wordpress/docker/child-entrypoint.sh +13 -0
  41. package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/functions.php +12 -0
  42. package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/index.php +2 -0
  43. package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/src/blocks/.gitkeep +0 -0
  44. package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/style.css +8 -0
  45. package/templates/client/apps/wordpress/theme-__PROJECT_NAME__/theme.json +5 -0
  46. package/templates/client/gitignore +14 -0
  47. package/templates/client/npmrc +5 -0
  48. package/templates/client/package.json +20 -0
  49. package/templates/client/pnpm-workspace.yaml +3 -0
  50. package/templates/client/turbo.json +9 -0
  51. 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
+ });
@@ -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,7 @@
1
+ # Webapp (bakées au build Vite)
2
+ VITE_WORDPRESS_URL=https://__PROJECT_NAME__-wp.example.com
3
+ VITE_GRAPHQL_PATH=/graphql
4
+ VITE_FEATURE_EDITORIAL=true
5
+ VITE_FEATURE_ECOMMERCE=true
6
+ VITE_FEATURE_AUTH=true
7
+ SESSION_SECRET=change-me
@@ -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,3 @@
1
+ .wp-reactor-cta-banner-editor {
2
+ display: block;
3
+ }
@@ -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,3 @@
1
+ export function save() {
2
+ return null;
3
+ }
@@ -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
+ }