@oss-ma/tpl 1.0.0 → 1.0.1
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/dist/engine/loadTemplate.js +53 -10
- package/dist/engine/packs/loadPack.js +3 -8
- package/dist/engine/validators/standard.js +4 -19
- package/dist/utils/path.js +18 -1
- package/package.json +2 -2
- package/resources/packs/company-pack-a/files/.github/badges/company-a.svg +6 -0
- package/resources/packs/company-pack-a/files/README.company.md +15 -0
- package/resources/packs/company-pack-a/files/SECURITY.md +20 -0
- package/resources/packs/company-pack-a/pack.yaml +29 -0
- package/resources/packs/company-pack-a/rules/rules.json +12 -0
- package/resources/standard.schema.json +29 -0
- package/resources/templates/react-ts/files/.editorconfig +9 -0
- package/resources/templates/react-ts/files/.gitattributes +1 -0
- package/resources/templates/react-ts/files/.github/dependabot.yml +12 -0
- package/resources/templates/react-ts/files/.github/workflows/ci.yml +94 -0
- package/resources/templates/react-ts/files/.github/workflows/codeql.yml +37 -0
- package/resources/templates/react-ts/files/.husky/commit-msg +4 -0
- package/resources/templates/react-ts/files/.husky/pre-commit +6 -0
- package/resources/templates/react-ts/files/.prettierrc.json +5 -0
- package/resources/templates/react-ts/files/README.md +51 -0
- package/resources/templates/react-ts/files/commitlint.config.cjs +3 -0
- package/resources/templates/react-ts/files/docs/adr/0001-context.md +0 -0
- package/resources/templates/react-ts/files/eslint.config.js +67 -0
- package/resources/templates/react-ts/files/index.html +12 -0
- package/resources/templates/react-ts/files/package.json +56 -0
- package/resources/templates/react-ts/files/src/app/App.js +7 -0
- package/resources/templates/react-ts/files/src/app/App.tsx +10 -0
- package/resources/templates/react-ts/files/src/app/main.js +6 -0
- package/resources/templates/react-ts/files/src/app/main.tsx +9 -0
- package/resources/templates/react-ts/files/src/features/example/Example.js +10 -0
- package/resources/templates/react-ts/files/src/features/example/Example.tsx +14 -0
- package/resources/templates/react-ts/files/src/shared/ui/Button.js +10 -0
- package/resources/templates/react-ts/files/src/shared/ui/Button.tsx +20 -0
- package/resources/templates/react-ts/files/template.lock +9 -0
- package/resources/templates/react-ts/files/tsconfig.json +21 -0
- package/resources/templates/react-ts/files/tsconfig.node.json +10 -0
- package/resources/templates/react-ts/files/vite.config.js +5 -0
- package/resources/templates/react-ts/files/vite.config.ts +6 -0
- package/resources/templates/react-ts/files/vitest.config.js +9 -0
- package/resources/templates/react-ts/files/vitest.config.ts +10 -0
- package/resources/templates/react-ts/template.yaml +20 -0
|
@@ -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 {
|
|
4
|
+
import { getResourcesRoot } from "../utils/path.js";
|
|
5
5
|
export async function loadTemplate(templateName) {
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
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 {
|
|
4
|
+
import { getResourcesRoot } from "../../utils/path.js";
|
|
5
5
|
export async function loadPack(packName) {
|
|
6
|
-
|
|
7
|
-
const
|
|
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 {
|
|
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 =
|
|
50
|
-
if (!schemaPath) {
|
|
51
|
-
throw new Error(`standard.schema.json not found
|
|
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);
|
package/dist/utils/path.js
CHANGED
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.1",
|
|
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,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,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 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -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,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
|
|
File without changes
|
|
@@ -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,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,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,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,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"
|