@oss-ma/tpl 1.0.0 → 1.0.2

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 (40) hide show
  1. package/dist/engine/copy.js +35 -15
  2. package/dist/engine/loadTemplate.js +53 -10
  3. package/dist/engine/packs/loadPack.js +3 -8
  4. package/dist/engine/validators/standard.js +4 -19
  5. package/dist/utils/path.js +18 -1
  6. package/package.json +2 -2
  7. package/resources/packs/company-pack-a/files/.github/badges/company-a.svg +6 -0
  8. package/resources/packs/company-pack-a/files/README.company.md +15 -0
  9. package/resources/packs/company-pack-a/files/SECURITY.md +20 -0
  10. package/resources/packs/company-pack-a/pack.yaml +29 -0
  11. package/resources/packs/company-pack-a/rules/rules.json +12 -0
  12. package/resources/standard.schema.json +29 -0
  13. package/resources/templates/react-ts/files/.editorconfig +9 -0
  14. package/resources/templates/react-ts/files/.gitattributes +1 -0
  15. package/resources/templates/react-ts/files/.github/dependabot.yml +12 -0
  16. package/resources/templates/react-ts/files/.github/workflows/ci.yml +94 -0
  17. package/resources/templates/react-ts/files/.github/workflows/codeql.yml +37 -0
  18. package/resources/templates/react-ts/files/.husky/commit-msg +4 -0
  19. package/resources/templates/react-ts/files/.husky/pre-commit +6 -0
  20. package/resources/templates/react-ts/files/.prettierrc.json +5 -0
  21. package/resources/templates/react-ts/files/README.md +51 -0
  22. package/resources/templates/react-ts/files/commitlint.config.cjs +3 -0
  23. package/resources/templates/react-ts/files/docs/adr/0001-context.md +0 -0
  24. package/resources/templates/react-ts/files/eslint.config.js +67 -0
  25. package/resources/templates/react-ts/files/index.html +12 -0
  26. package/resources/templates/react-ts/files/package.json +56 -0
  27. package/resources/templates/react-ts/files/src/app/App.js +7 -0
  28. package/resources/templates/react-ts/files/src/app/App.tsx +10 -0
  29. package/resources/templates/react-ts/files/src/app/main.js +6 -0
  30. package/resources/templates/react-ts/files/src/app/main.tsx +9 -0
  31. package/resources/templates/react-ts/files/src/features/example/Example.js +10 -0
  32. package/resources/templates/react-ts/files/src/features/example/Example.tsx +14 -0
  33. package/resources/templates/react-ts/files/src/shared/ui/Button.js +10 -0
  34. package/resources/templates/react-ts/files/src/shared/ui/Button.tsx +20 -0
  35. package/resources/templates/react-ts/files/template.lock +9 -0
  36. package/resources/templates/react-ts/files/tsconfig.json +21 -0
  37. package/resources/templates/react-ts/files/tsconfig.node.json +10 -0
  38. package/resources/templates/react-ts/files/vite.config.ts +6 -0
  39. package/resources/templates/react-ts/files/vitest.config.js +9 -0
  40. package/resources/templates/react-ts/template.yaml +20 -0
@@ -21,35 +21,55 @@ const TEXT_EXT = new Set([
21
21
  ".cjs",
22
22
  ".mjs"
23
23
  ]);
24
+ /**
25
+ * Dotfiles that MUST be copied if present in template root.
26
+ * Some Windows + tooling edge cases can hide them from readdir.
27
+ */
28
+ const MUST_COPY_DOTFILES = [".gitignore", ".editorconfig", ".gitattributes"];
24
29
  function isTextFile(filePath) {
25
30
  const ext = path.extname(filePath).toLowerCase();
26
31
  if (TEXT_EXT.has(ext))
27
32
  return true;
28
- // Special dotfiles with no extension
29
33
  const base = path.basename(filePath);
30
34
  if (base === "Dockerfile")
31
35
  return true;
32
36
  return false;
33
37
  }
38
+ async function copyOneFile(srcPath, destPath, vars) {
39
+ await fs.ensureDir(path.dirname(destPath));
40
+ if (isTextFile(srcPath)) {
41
+ const raw = await fs.readFile(srcPath, "utf8");
42
+ const rendered = renderString(raw, vars);
43
+ await fs.writeFile(destPath, rendered, "utf8");
44
+ }
45
+ else {
46
+ await fs.copyFile(srcPath, destPath);
47
+ }
48
+ }
34
49
  export async function copyAndRenderDir(srcDir, destDir, vars) {
35
50
  await fs.ensureDir(destDir);
51
+ // 0) Fail-safe: explicitly copy must-have dotfiles if they exist in this directory
52
+ // (especially important for template root where .gitignore lives)
53
+ for (const dot of MUST_COPY_DOTFILES) {
54
+ const srcDot = path.join(srcDir, dot);
55
+ const destDot = path.join(destDir, dot);
56
+ if (await fs.pathExists(srcDot)) {
57
+ await copyOneFile(srcDot, destDot, vars);
58
+ }
59
+ }
36
60
  const entries = await fs.readdir(srcDir, { withFileTypes: true });
37
- for (const e of entries) {
38
- const srcPath = path.join(srcDir, e.name);
39
- const destPath = path.join(destDir, e.name);
40
- if (e.isDirectory()) {
61
+ for (const entry of entries) {
62
+ // Skip the dotfiles already handled explicitly above (avoid duplicate work)
63
+ if (MUST_COPY_DOTFILES.includes(entry.name))
64
+ continue;
65
+ const srcPath = path.join(srcDir, entry.name);
66
+ const destPath = path.join(destDir, entry.name);
67
+ if (entry.isDirectory()) {
41
68
  await copyAndRenderDir(srcPath, destPath, vars);
42
69
  continue;
43
70
  }
44
- if (e.isFile()) {
45
- if (isTextFile(srcPath)) {
46
- const raw = await fs.readFile(srcPath, "utf8");
47
- const out = renderString(raw, vars);
48
- await fs.outputFile(destPath, out, "utf8");
49
- }
50
- else {
51
- await fs.copyFile(srcPath, destPath);
52
- }
53
- }
71
+ if (!entry.isFile())
72
+ continue;
73
+ await copyOneFile(srcPath, destPath, vars);
54
74
  }
55
75
  }
@@ -1,14 +1,10 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import YAML from "yaml";
4
- import { findUp } from "../utils/findUp.js";
4
+ import { getResourcesRoot } from "../utils/path.js";
5
5
  export async function loadTemplate(templateName) {
6
- const schemaPath = await findUp(process.cwd(), "standard.schema.json");
7
- const repoRoot = schemaPath ? path.dirname(schemaPath) : null;
8
- if (!repoRoot) {
9
- throw new Error(`Cannot locate repo root (standard.schema.json not found upward from: ${process.cwd()})`);
10
- }
11
- const templateDir = path.join(repoRoot, "templates", templateName);
6
+ const resourcesRoot = getResourcesRoot();
7
+ const templateDir = path.join(resourcesRoot, "templates", templateName);
12
8
  const templateYamlPath = path.join(templateDir, "template.yaml");
13
9
  const filesDir = path.join(templateDir, "files");
14
10
  if (!(await fs.pathExists(templateYamlPath))) {
@@ -24,10 +20,37 @@ export async function loadTemplate(templateName) {
24
20
  }
25
21
  return { spec, templateDir, filesDir };
26
22
  }
23
+ // import path from "node:path";
24
+ // import fs from "fs-extra";
25
+ // import YAML from "yaml";
26
+ // import { findUp } from "../utils/findUp.js";
27
+ // export type TemplateQuestion = {
28
+ // name: string;
29
+ // message: string;
30
+ // default?: string;
31
+ // choices?: string[];
32
+ // };
33
+ // export type TemplateHook = { run: string };
34
+ // export type TemplateSpec = {
35
+ // name: string;
36
+ // version: string;
37
+ // description?: string;
38
+ // questions?: TemplateQuestion[];
39
+ // hooks?: {
40
+ // postGenerate?: TemplateHook[];
41
+ // };
42
+ // };
43
+ // export type LoadedTemplate = {
44
+ // spec: TemplateSpec;
45
+ // templateDir: string;
46
+ // filesDir: string;
47
+ // };
27
48
  // export async function loadTemplate(templateName: string): Promise<LoadedTemplate> {
28
- // // CLI is at: template-platform/cli
29
- // // templates are at: template-platform/templates/<name>
30
- // const repoRoot = path.resolve(process.cwd(), ".."); // assumes we run from cli/
49
+ // const schemaPath = await findUp(process.cwd(), "standard.schema.json");
50
+ // const repoRoot = schemaPath ? path.dirname(schemaPath) : null;
51
+ // if (!repoRoot) {
52
+ // throw new Error(`Cannot locate repo root (standard.schema.json not found upward from: ${process.cwd()})`);
53
+ // }
31
54
  // const templateDir = path.join(repoRoot, "templates", templateName);
32
55
  // const templateYamlPath = path.join(templateDir, "template.yaml");
33
56
  // const filesDir = path.join(templateDir, "files");
@@ -44,3 +67,23 @@ export async function loadTemplate(templateName) {
44
67
  // }
45
68
  // return { spec, templateDir, filesDir };
46
69
  // }
70
+ // // export async function loadTemplate(templateName: string): Promise<LoadedTemplate> {
71
+ // // // CLI is at: template-platform/cli
72
+ // // // templates are at: template-platform/templates/<name>
73
+ // // const repoRoot = path.resolve(process.cwd(), ".."); // assumes we run from cli/
74
+ // // const templateDir = path.join(repoRoot, "templates", templateName);
75
+ // // const templateYamlPath = path.join(templateDir, "template.yaml");
76
+ // // const filesDir = path.join(templateDir, "files");
77
+ // // if (!(await fs.pathExists(templateYamlPath))) {
78
+ // // throw new Error(`Template not found: ${templateYamlPath}`);
79
+ // // }
80
+ // // if (!(await fs.pathExists(filesDir))) {
81
+ // // throw new Error(`Template files folder not found: ${filesDir}`);
82
+ // // }
83
+ // // const raw = await fs.readFile(templateYamlPath, "utf8");
84
+ // // const spec = YAML.parse(raw) as TemplateSpec;
85
+ // // if (!spec?.name || !spec?.version) {
86
+ // // throw new Error(`Invalid template.yaml: missing name/version (${templateYamlPath})`);
87
+ // // }
88
+ // // return { spec, templateDir, filesDir };
89
+ // // }
@@ -1,15 +1,10 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import YAML from "yaml";
4
- import { findUp } from "../../utils/findUp.js";
4
+ import { getResourcesRoot } from "../../utils/path.js";
5
5
  export async function loadPack(packName) {
6
- // findUp returns the FULL path to the "packs" directory
7
- const packsDir = await findUp(process.cwd(), "packs");
8
- if (!packsDir) {
9
- throw new Error(`Cannot locate "packs" directory (searched upward from: ${process.cwd()})`);
10
- }
11
- // packsDir already ends with ".../packs"
12
- const packDir = path.join(packsDir, packName);
6
+ const packsRoot = path.join(getResourcesRoot(), "packs");
7
+ const packDir = path.join(packsRoot, packName);
13
8
  const packYaml = path.join(packDir, "pack.yaml");
14
9
  if (!(await fs.pathExists(packYaml))) {
15
10
  throw new Error(`Pack not found: ${packName} (expected: ${packYaml})`);
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
- import { findUp } from "../../utils/findUp.js";
3
+ import { getResourcesRoot } from "../../utils/path.js";
4
4
  async function fileExists(p) {
5
5
  try {
6
6
  const stat = await fs.stat(p);
@@ -30,25 +30,10 @@ function extractMarkdownHeadings(md) {
30
30
  }
31
31
  return headings;
32
32
  }
33
- // export async function loadStandardSchema(): Promise<{ schema: StandardSchema; schemaPath: string }> {
34
- // // CLI lives in template-platform/cli
35
- // // standard.schema.json lives in template-platform/standard.schema.json
36
- // const repoRoot = path.resolve(process.cwd(), "..");
37
- // const schemaPath = path.join(repoRoot, "standard.schema.json");
38
- // if (!(await fileExists(schemaPath))) {
39
- // throw new Error(`standard.schema.json not found at: ${schemaPath}`);
40
- // }
41
- // const raw = await fs.readFile(schemaPath, "utf8");
42
- // const schema = JSON.parse(raw) as StandardSchema;
43
- // if (!schema?.version || !Array.isArray(schema.requiredFiles) || !Array.isArray(schema.requiredDirs)) {
44
- // throw new Error(`Invalid standard.schema.json: ${schemaPath}`);
45
- // }
46
- // return { schema, schemaPath };
47
- // }
48
33
  export async function loadStandardSchema() {
49
- const schemaPath = await findUp(process.cwd(), "standard.schema.json");
50
- if (!schemaPath) {
51
- throw new Error(`standard.schema.json not found (searched upward from: ${process.cwd()})`);
34
+ const schemaPath = path.join(getResourcesRoot(), "standard.schema.json");
35
+ if (!(await fs.pathExists(schemaPath))) {
36
+ throw new Error(`standard.schema.json not found in resources: ${schemaPath}`);
52
37
  }
53
38
  const raw = await fs.readFile(schemaPath, "utf8");
54
39
  const schema = JSON.parse(raw);
@@ -1 +1,18 @@
1
- export {};
1
+ import { fileURLToPath } from "node:url";
2
+ import path from "node:path";
3
+ /**
4
+ * Absolute path to the installed CLI package root (folder containing dist/ and resources/).
5
+ * Works with: local dev, npm install, and npx.
6
+ */
7
+ export function getCliPackageRoot() {
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ // dist/utils/path.js -> dist/utils -> dist -> (package root)
11
+ return path.resolve(__dirname, "../../");
12
+ }
13
+ /**
14
+ * Absolute path to published runtime assets.
15
+ */
16
+ export function getResourcesRoot() {
17
+ return path.join(getCliPackageRoot(), "resources");
18
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@oss-ma/tpl",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Generate, enforce and maintain clean project architectures",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tpl": "./dist/index.js"
8
8
  },
9
- "files": ["dist"],
9
+ "files": ["dist", "resources"],
10
10
  "engines": {
11
11
  "node": ">=18"
12
12
  },
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="120" height="20">
2
+ <rect width="120" height="20" fill="#0052cc"/>
3
+ <text x="60" y="14" fill="#fff" text-anchor="middle" font-size="12">
4
+ Company A
5
+ </text>
6
+ </svg>
@@ -0,0 +1,15 @@
1
+ # Company A Engineering Standards
2
+
3
+ This project follows **Company A mandatory engineering standards**.
4
+
5
+ ## Enforced by tooling
6
+ These rules are automatically enforced by:
7
+ - tpl check
8
+ - CI pipelines
9
+ - security scans
10
+
11
+ ## Non-compliance
12
+ Any violation of these standards will block:
13
+ - merges
14
+ - releases
15
+ - deployments
@@ -0,0 +1,20 @@
1
+ # Security Policy — Company A
2
+
3
+ ## Reporting a vulnerability
4
+ If you discover a security vulnerability, please report it immediately to:
5
+
6
+ - security@company-a.com
7
+
8
+ Do NOT open public issues for security problems.
9
+
10
+ ## Scope
11
+ This policy applies to:
12
+ - application code
13
+ - dependencies
14
+ - CI/CD pipelines
15
+ - infrastructure-as-code
16
+
17
+ ## Mandatory rules
18
+ - All dependencies must be kept up to date.
19
+ - Security alerts must be treated as high priority.
20
+ - CodeQL must remain enabled.
@@ -0,0 +1,29 @@
1
+ name: company-pack-a
2
+ version: 1.0.0
3
+ description: "Company A mandatory engineering standards"
4
+
5
+ appliesTo:
6
+ templates:
7
+ - react-ts
8
+
9
+ enforcement:
10
+ level: strict
11
+
12
+ adds:
13
+ files: true
14
+ rules: true
15
+
16
+ # name: company-pack-a
17
+ # version: 1.0.0
18
+ # description: "Company A engineering standards"
19
+
20
+ # appliesTo:
21
+ # templates:
22
+ # - react-ts
23
+
24
+ # enforces:
25
+ # level: strict # strict | advisory
26
+
27
+ # adds:
28
+ # files: true
29
+ # rules: true
@@ -0,0 +1,12 @@
1
+ {
2
+ "requiredFiles": [
3
+ "SECURITY.md"
4
+ ],
5
+ "requiredScripts": [
6
+ "audit",
7
+ "typecheck"
8
+ ],
9
+ "forbiddenPatterns": [
10
+ "alert("
11
+ ]
12
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "requiredFiles": [
4
+ "README.md",
5
+ ".editorconfig",
6
+ ".gitignore",
7
+ "template.lock",
8
+ ".github/workflows/ci.yml"
9
+ ],
10
+ "requiredDirs": [
11
+ "docs/adr"
12
+ ],
13
+ "readmeRequiredHeadings": [
14
+ "Overview",
15
+ "Quickstart",
16
+ "Scripts",
17
+ "Architecture",
18
+ "Contributing"
19
+ ],
20
+ "requiredAdrFiles": [
21
+ "docs/adr/0001-context.md"
22
+ ],
23
+ "requiredActions": [
24
+ "lint",
25
+ "format",
26
+ "test",
27
+ "build"
28
+ ]
29
+ }
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ indent_style = space
8
+ indent_size = 2
9
+ trim_trailing_whitespace = true
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "npm"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ open-pull-requests-limit: 10
8
+ groups:
9
+ dev-dependencies:
10
+ dependency-type: "development"
11
+ patterns:
12
+ - "*"
@@ -0,0 +1,94 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ lint:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: "20"
21
+ cache: "npm"
22
+
23
+ - name: Install
24
+ run: npm ci
25
+
26
+ - name: Lint
27
+ run: npm run lint
28
+
29
+ typecheck:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - uses: actions/setup-node@v4
35
+ with:
36
+ node-version: "20"
37
+ cache: "npm"
38
+
39
+ - name: Install
40
+ run: npm ci
41
+
42
+ - name: Typecheck
43
+ run: npm run typecheck
44
+
45
+ test:
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - uses: actions/setup-node@v4
51
+ with:
52
+ node-version: "20"
53
+ cache: "npm"
54
+
55
+ - name: Install
56
+ run: npm ci
57
+
58
+ - name: Test
59
+ run: npm run test
60
+
61
+ build:
62
+ runs-on: ubuntu-latest
63
+ needs: [lint, typecheck, test]
64
+ steps:
65
+ - uses: actions/checkout@v4
66
+
67
+ - uses: actions/setup-node@v4
68
+ with:
69
+ node-version: "20"
70
+ cache: "npm"
71
+
72
+ - name: Install
73
+ run: npm ci
74
+
75
+ - name: Build
76
+ run: npm run build
77
+
78
+ audit:
79
+ runs-on: ubuntu-latest
80
+ needs: [lint, typecheck, test]
81
+ continue-on-error: true
82
+ steps:
83
+ - uses: actions/checkout@v4
84
+
85
+ - uses: actions/setup-node@v4
86
+ with:
87
+ node-version: "20"
88
+ cache: "npm"
89
+
90
+ - name: Install
91
+ run: npm ci
92
+
93
+ - name: Audit (high+)
94
+ run: npm run audit
@@ -0,0 +1,37 @@
1
+ name: codeql
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ schedule:
8
+ - cron: "0 3 * * 1" # every Monday 03:00 UTC
9
+
10
+ permissions:
11
+ contents: read
12
+ security-events: write
13
+
14
+ jobs:
15
+ analyze:
16
+ name: Analyze (CodeQL)
17
+ runs-on: ubuntu-latest
18
+
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ language: ["javascript-typescript"]
23
+
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Initialize CodeQL
29
+ uses: github/codeql-action/init@v3
30
+ with:
31
+ languages: ${{ matrix.language }}
32
+
33
+ - name: Autobuild
34
+ uses: github/codeql-action/autobuild@v3
35
+
36
+ - name: Perform CodeQL Analysis
37
+ uses: github/codeql-action/analyze@v3
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx --no -- commitlint --edit "$1"
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npm run lint
5
+ npm run typecheck
6
+ npm run test
@@ -0,0 +1,5 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "printWidth": 100
5
+ }
@@ -0,0 +1,51 @@
1
+ # {{appName}}
2
+
3
+ ## Overview
4
+ Starter React + TypeScript (Vite) with a strict structure and default quality gates (lint/format/test/build).
5
+
6
+ ## Quickstart
7
+ ```bash
8
+ {{packageManager}} install
9
+ {{packageManager}} run dev
10
+ ```
11
+
12
+ ## Scripts
13
+ {{packageManager}} run dev : start dev server
14
+ {{packageManager}} run build : production build
15
+ {{packageManager}} run test : run tests
16
+ {{packageManager}} run lint : lint code
17
+ {{packageManager}} run format : format code
18
+
19
+ ## Architecture
20
+ src/app : app bootstrap (entry, root component)
21
+ src/features : feature modules (business/UI by feature)
22
+ src/shared : reusable primitives (ui, utils, types)
23
+
24
+ ## Contributing
25
+ Keep code inside features/ when it belongs to a domain feature.
26
+ Reuse from shared/ only when it’s truly cross-feature.
27
+ CI must stay green: lint + test + build.
28
+
29
+
30
+ ### ADR obligatoire
31
+ **`templates/react-ts/files/docs/adr/0001-context.md`**
32
+ ```md
33
+ # ADR 0001 — Project context
34
+
35
+ - Date: {{date}}
36
+ - Status: Accepted
37
+
38
+ ## Context
39
+ We need a React + TypeScript starter that is easy to hand over between teams and stays consistent over time.
40
+
41
+ ## Decision
42
+ Use Vite + React + TypeScript with:
43
+ - Feature-based structure
44
+ - ESLint + Prettier
45
+ - Vitest for unit tests
46
+ - GitHub Actions CI running lint, test and build
47
+
48
+ ## Consequences
49
+ - Fast local setup and consistent code style
50
+ - Enforced quality gates via CI
51
+ - Clear separation between app bootstrap, features and shared code
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ extends: ["@commitlint/config-conventional"]
3
+ };
@@ -0,0 +1,67 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tsParser from "@typescript-eslint/parser";
4
+ import tsPlugin from "@typescript-eslint/eslint-plugin";
5
+ import reactHooks from "eslint-plugin-react-hooks";
6
+ import reactRefresh from "eslint-plugin-react-refresh";
7
+
8
+ export default [
9
+ {
10
+ ignores: ["dist/**", "coverage/**", "node_modules/**"]
11
+ },
12
+
13
+ js.configs.recommended,
14
+
15
+ {
16
+ files: ["**/*.{ts,tsx}"],
17
+ languageOptions: {
18
+ ecmaVersion: 2022,
19
+ sourceType: "module",
20
+ parser: tsParser,
21
+ parserOptions: {
22
+ ecmaFeatures: { jsx: true }
23
+ },
24
+ globals: globals.browser
25
+ },
26
+ plugins: {
27
+ "@typescript-eslint": tsPlugin,
28
+ "react-hooks": reactHooks,
29
+ "react-refresh": reactRefresh
30
+ },
31
+ rules: {
32
+ ...tsPlugin.configs.recommended.rules,
33
+ ...reactHooks.configs.recommended.rules,
34
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }]
35
+ }
36
+ }
37
+ ];
38
+
39
+ // import js from "@eslint/js";
40
+ // import globals from "globals";
41
+ // import reactHooks from "eslint-plugin-react-hooks";
42
+ // import reactRefresh from "eslint-plugin-react-refresh";
43
+
44
+ // export default [
45
+ // js.configs.recommended,
46
+
47
+ // // ignore build artifacts
48
+ // {
49
+ // ignores: ["dist/**", "coverage/**", "node_modules/**"]
50
+ // },
51
+
52
+ // {
53
+ // files: ["**/*.{ts,tsx}"],
54
+ // languageOptions: {
55
+ // ecmaVersion: 2022,
56
+ // globals: globals.browser
57
+ // },
58
+ // plugins: {
59
+ // "react-hooks": reactHooks,
60
+ // "react-refresh": reactRefresh
61
+ // },
62
+ // rules: {
63
+ // ...reactHooks.configs.recommended.rules,
64
+ // "react-refresh/only-export-components": ["warn", { allowConstantExport: true }]
65
+ // }
66
+ // }
67
+ // ];
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{appName}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/app/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "{{appName}}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview",
10
+
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+
14
+ "lint": "eslint .",
15
+ "format": "prettier -w .",
16
+ "typecheck": "tsc -p tsconfig.json --noEmit",
17
+
18
+ "gen:feature": "node scripts/gen-feature.mjs",
19
+
20
+ "prepare": "husky",
21
+ "audit": "npm audit --audit-level=high"
22
+ },
23
+ "dependencies": {
24
+ "react": "^18.3.0",
25
+ "react-dom": "^18.3.0"
26
+ },
27
+ "devDependencies": {
28
+ "@commitlint/cli": "^19.3.0",
29
+ "@commitlint/config-conventional": "^19.2.0",
30
+ "@eslint/js": "^9.0.0",
31
+ "@types/react": "^18.3.0",
32
+ "@types/react-dom": "^18.3.0",
33
+ "@vitejs/plugin-react": "^4.3.0",
34
+ "eslint": "^9.0.0",
35
+ "eslint-plugin-react-hooks": "^5.0.0",
36
+ "eslint-plugin-react-refresh": "^0.4.0",
37
+ "globals": "^15.0.0",
38
+ "husky": "^9.0.0",
39
+ "lint-staged": "^15.2.0",
40
+ "prettier": "^3.3.0",
41
+ "typescript": "^5.5.0",
42
+ "vite": "^5.4.0",
43
+ "vitest": "^2.0.0",
44
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
45
+ "@typescript-eslint/parser": "^8.0.0",
46
+ "jsdom": "^26.0.0"
47
+ },
48
+ "lint-staged": {
49
+ "*.{ts,tsx,js,jsx,json,md,yml,yaml}": [
50
+ "prettier -w"
51
+ ],
52
+ "*.{ts,tsx,js,jsx}": [
53
+ "eslint --fix"
54
+ ]
55
+ }
56
+ }
@@ -0,0 +1,7 @@
1
+ import { Example } from "../features/example/Example.js";
2
+ export function App() {
3
+ return (<main style={{ padding: 16 }}>
4
+ <h1>{{ appName }}</h1>
5
+ <Example />
6
+ </main>);
7
+ }
@@ -0,0 +1,10 @@
1
+ import { Example } from "../features/example/Example.js";
2
+
3
+ export function App() {
4
+ return (
5
+ <main style={{ padding: 16 }}>
6
+ <h1>{{appName}}</h1>
7
+ <Example />
8
+ </main>
9
+ );
10
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { App } from "./App.js";
4
+ ReactDOM.createRoot(document.getElementById("root")).render(<React.StrictMode>
5
+ <App />
6
+ </React.StrictMode>);
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { App } from "./App.js";
4
+
5
+ ReactDOM.createRoot(document.getElementById("root")!).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>
9
+ );
@@ -0,0 +1,10 @@
1
+ import { useState } from "react";
2
+ import { Button } from "../../shared/ui/Button.js";
3
+ export function Example() {
4
+ const [msg, setMsg] = useState(null);
5
+ return (<section style={{ marginTop: 16 }}>
6
+ <p>Feature module example.</p>
7
+ <Button onClick={() => setMsg("Hello!")}>Click</Button>
8
+ {msg && <p style={{ marginTop: 8 }}>{msg}</p>}
9
+ </section>);
10
+ }
@@ -0,0 +1,14 @@
1
+ import { useState } from "react";
2
+ import { Button } from "../../shared/ui/Button.js";
3
+
4
+ export function Example() {
5
+ const [msg, setMsg] = useState<string | null>(null);
6
+
7
+ return (
8
+ <section style={{ marginTop: 16 }}>
9
+ <p>Feature module example.</p>
10
+ <Button onClick={() => setMsg("Hello!")}>Click</Button>
11
+ {msg && <p style={{ marginTop: 8 }}>{msg}</p>}
12
+ </section>
13
+ );
14
+ }
@@ -0,0 +1,10 @@
1
+ export function Button({ children, onClick }) {
2
+ return (<button onClick={onClick} style={{
3
+ padding: "8px 12px",
4
+ borderRadius: 10,
5
+ border: "1px solid #ddd",
6
+ cursor: "pointer"
7
+ }}>
8
+ {children}
9
+ </button>);
10
+ }
@@ -0,0 +1,20 @@
1
+ type Props = {
2
+ children: React.ReactNode;
3
+ onClick?: () => void;
4
+ };
5
+
6
+ export function Button({ children, onClick }: Props) {
7
+ return (
8
+ <button
9
+ onClick={onClick}
10
+ style={{
11
+ padding: "8px 12px",
12
+ borderRadius: 10,
13
+ border: "1px solid #ddd",
14
+ cursor: "pointer"
15
+ }}
16
+ >
17
+ {children}
18
+ </button>
19
+ );
20
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "template": "react-ts",
3
+ "version": "1.0.0",
4
+ "options": {
5
+ "appName": "{{appName}}",
6
+ "packageManager": "{{packageManager}}"
7
+ },
8
+ "generatedAt": "{{isoDate}}"
9
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ "moduleResolution": "Bundler",
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+
15
+ "strict": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true
19
+ },
20
+ "include": ["src", "vitest.setup.ts"]
21
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "skipLibCheck": true,
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts", "vitest.config.ts"]
10
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()]
6
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import react from "@vitejs/plugin-react";
3
+ export default defineConfig({
4
+ plugins: [react()],
5
+ test: {
6
+ environment: "jsdom",
7
+ setupFiles: ["./vitest.setup.ts"]
8
+ }
9
+ });
@@ -0,0 +1,20 @@
1
+ name: react-ts
2
+ version: 1.0.0
3
+ description: "Template React + TypeScript (Vite) with lint/format/test/build + CI + ADR"
4
+ engine: "v1"
5
+
6
+ questions:
7
+ - name: appName
8
+ message: "Nom du projet ?"
9
+ default: "my-react-app"
10
+
11
+ - name: packageManager
12
+ message: "Package manager ?"
13
+ choices: ["npm", "pnpm", "yarn"]
14
+ default: "npm"
15
+
16
+ hooks:
17
+ postGenerate:
18
+ - run: "git init"
19
+ - run: "{{packageManager}} install"
20
+ - run: "{{packageManager}} run test"