@oss-ma/tpl 1.0.33 → 1.0.35

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.
@@ -7,23 +7,38 @@ import pc from "picocolors";
7
7
  import { loadStandardSchema, checkAgainstStandard } from "../engine/validators/standard.js";
8
8
  import { readTemplateLock } from "../engine/readLock.js";
9
9
  import { validateReactTs } from "../engine/validators/reactTs.js";
10
+ import { validateReactNext } from "../engine/validators/reactNext.js";
10
11
  import { loadPack } from "../engine/packs/loadPack.js";
11
12
  import { validatePackRules } from "../engine/packs/validatePackRules.js";
12
- ;
13
- // ── Integrity helpers
13
+ // ── Integrity helpers ────────────────────────────────────────────────────────
14
14
  async function hashFile(filePath) {
15
15
  const content = await fs.readFile(filePath);
16
16
  return crypto.createHash("sha256").update(content).digest("hex");
17
17
  }
18
- async function collectFiles(dir) {
18
+ const EXCLUDED_DIRS = new Set([
19
+ "node_modules",
20
+ ".git",
21
+ ".next",
22
+ "dist",
23
+ "build",
24
+ "coverage",
25
+ ".turbo",
26
+ ".cache",
27
+ ]);
28
+ async function collectFiles(dir, root) {
29
+ const baseDir = root ?? dir;
19
30
  const entries = await fs.readdir(dir, { withFileTypes: true });
20
31
  const results = [];
21
32
  for (const entry of entries) {
33
+ if (EXCLUDED_DIRS.has(entry.name))
34
+ continue;
22
35
  const full = path.join(dir, entry.name);
23
- if (entry.isDirectory())
24
- results.push(...(await collectFiles(full)));
25
- else if (entry.isFile())
36
+ if (entry.isDirectory()) {
37
+ results.push(...(await collectFiles(full, baseDir)));
38
+ }
39
+ else if (entry.isFile()) {
26
40
  results.push(full);
41
+ }
27
42
  }
28
43
  return results.sort();
29
44
  }
@@ -70,12 +85,13 @@ async function verifyIntegrity(projectPath, manifest) {
70
85
  }
71
86
  // ── Output ───────────────────────────────────────────────────────────────────
72
87
  function printHuman(result) {
88
+ const templateLabel = result.template ? pc.gray(` [${result.template}]`) : "";
73
89
  if (result.ok) {
74
- console.log(pc.green(`✅ OK — Standard v${result.schemaVersion}`));
90
+ console.log(pc.green(`✅ OK — Standard v${result.schemaVersion}${templateLabel}`));
75
91
  console.log(`Project: ${result.projectPath}`);
76
92
  return;
77
93
  }
78
- console.log(pc.red(`❌ FAILED — Standard v${result.schemaVersion}`));
94
+ console.log(pc.red(`❌ FAILED — Standard v${result.schemaVersion}${templateLabel}`));
79
95
  console.log(`Project: ${result.projectPath}`);
80
96
  console.log("");
81
97
  for (const issue of result.issues) {
@@ -104,11 +120,14 @@ export const checkCommand = new Command("check")
104
120
  if (!opts.skipIntegrity && lock?.filesIntegrity) {
105
121
  integrityIssues = await verifyIntegrity(standardResult.projectPath, lock.filesIntegrity);
106
122
  }
107
- // 4) Template-specific
123
+ // 4) Template-specific validation
108
124
  let templateIssues = [];
109
125
  if (lock?.template === "react-ts") {
110
126
  templateIssues = await validateReactTs(standardResult.projectPath);
111
127
  }
128
+ else if (lock?.template === "react-next") {
129
+ templateIssues = await validateReactNext(standardResult.projectPath);
130
+ }
112
131
  // 5) Packs
113
132
  let packIssues = [];
114
133
  if (lock?.packs?.length) {
@@ -142,8 +161,9 @@ export const checkCommand = new Command("check")
142
161
  const ok = standardResult.issues.length +
143
162
  blockingIntegrityIssues.length +
144
163
  templateIssues.length +
145
- blockingPackIssues.length === 0;
146
- const result = { ...standardResult, issues, ok };
164
+ blockingPackIssues.length ===
165
+ 0;
166
+ const result = { ...standardResult, issues, ok, template: lock?.template };
147
167
  if (opts.json)
148
168
  console.log(JSON.stringify(result, null, 2));
149
169
  else
@@ -0,0 +1,153 @@
1
+ // cli/src/engine/validators/reactNext.ts
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
+ async function readJson(p) {
5
+ const raw = await fs.readFile(p, "utf8");
6
+ return JSON.parse(raw);
7
+ }
8
+ function detectUnpinnedActions(content) {
9
+ const unpinned = [];
10
+ const lines = content.split(/\r?\n/);
11
+ for (const line of lines) {
12
+ const m = line.match(/^\s*-?\s*uses:\s+([^\s@]+)@([^\s#]+)/);
13
+ if (!m)
14
+ continue;
15
+ const ref = m[2];
16
+ if (!/^[0-9a-f]{40}$/.test(ref))
17
+ unpinned.push(line.trim());
18
+ }
19
+ return unpinned;
20
+ }
21
+ export async function validateReactNext(projectPath) {
22
+ const issues = [];
23
+ // ── package.json scripts ──────────────────────────────────────────────────
24
+ const pkgPath = path.join(projectPath, "package.json");
25
+ if (!(await fs.pathExists(pkgPath))) {
26
+ issues.push({ code: "MISSING_FILE", message: "Missing package.json", path: "package.json" });
27
+ return issues;
28
+ }
29
+ const pkg = await readJson(pkgPath);
30
+ const scripts = pkg?.scripts ?? {};
31
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
32
+ const requiredScripts = ["dev", "build", "lint", "format", "test", "typecheck"];
33
+ for (const s of requiredScripts) {
34
+ if (!scripts[s]) {
35
+ issues.push({
36
+ code: "MISSING_SCRIPT",
37
+ message: `Missing npm script: "${s}"`,
38
+ path: "package.json",
39
+ hint: `Add it under scripts: "${s}": "..."`
40
+ });
41
+ }
42
+ }
43
+ // ── Next.js required deps ─────────────────────────────────────────────────
44
+ const requiredDeps = ["next", "react", "react-dom"];
45
+ for (const dep of requiredDeps) {
46
+ if (!deps[dep]) {
47
+ issues.push({
48
+ code: "MISSING_DEP",
49
+ message: `Missing required dependency: "${dep}"`,
50
+ path: "package.json",
51
+ hint: `Run: npm install ${dep}`
52
+ });
53
+ }
54
+ }
55
+ // ── App Router structure ──────────────────────────────────────────────────
56
+ const requiredFiles = [
57
+ "src/app/layout.tsx",
58
+ "src/app/page.tsx",
59
+ "src/app/globals.css",
60
+ "next.config.mjs",
61
+ "tsconfig.json",
62
+ ];
63
+ for (const f of requiredFiles) {
64
+ if (!(await fs.pathExists(path.join(projectPath, f)))) {
65
+ issues.push({
66
+ code: "MISSING_FILE",
67
+ message: `Missing required file: ${f}`,
68
+ path: f
69
+ });
70
+ }
71
+ }
72
+ // ── Security files ────────────────────────────────────────────────────────
73
+ const secureFiles = [
74
+ ".github/dependabot.yml",
75
+ ".github/workflows/codeql.yml",
76
+ "SECURITY.md"
77
+ ];
78
+ for (const f of secureFiles) {
79
+ if (!(await fs.pathExists(path.join(projectPath, f)))) {
80
+ issues.push({
81
+ code: "MISSING_SECURE_FILE",
82
+ message: `Missing security file: ${f}`,
83
+ path: f
84
+ });
85
+ }
86
+ }
87
+ // ── codeql.yml checks ─────────────────────────────────────────────────────
88
+ const codeqlPath = path.join(projectPath, ".github/workflows/codeql.yml");
89
+ if (await fs.pathExists(codeqlPath)) {
90
+ const codeqlContent = await fs.readFile(codeqlPath, "utf8");
91
+ if (!codeqlContent.includes("security-extended")) {
92
+ issues.push({
93
+ code: "CODEQL_WEAK_QUERIES",
94
+ message: 'codeql.yml does not use "security-extended" queries',
95
+ path: ".github/workflows/codeql.yml",
96
+ hint: "Add: queries: security-extended under the init step"
97
+ });
98
+ }
99
+ if (!codeqlContent.includes("schedule")) {
100
+ issues.push({
101
+ code: "CODEQL_NO_SCHEDULE",
102
+ message: "codeql.yml has no scheduled scan",
103
+ path: ".github/workflows/codeql.yml",
104
+ hint: "Add a weekly cron schedule"
105
+ });
106
+ }
107
+ const unpinned = detectUnpinnedActions(codeqlContent);
108
+ for (const line of unpinned) {
109
+ issues.push({
110
+ code: "UNPINNED_ACTION",
111
+ message: `GitHub Action not pinned by SHA: ${line}`,
112
+ path: ".github/workflows/codeql.yml",
113
+ hint: "Pin actions by full 40-char commit SHA"
114
+ });
115
+ }
116
+ }
117
+ // ── ci.yml checks ─────────────────────────────────────────────────────────
118
+ const ciPath = path.join(projectPath, ".github/workflows/ci.yml");
119
+ if (await fs.pathExists(ciPath)) {
120
+ const ciContent = await fs.readFile(ciPath, "utf8");
121
+ if (!ciContent.includes("npm audit")) {
122
+ issues.push({
123
+ code: "MISSING_AUDIT_STEP",
124
+ message: 'ci.yml does not run "npm audit"',
125
+ path: ".github/workflows/ci.yml",
126
+ hint: "Add a step: run: npm audit --audit-level=high"
127
+ });
128
+ }
129
+ const unpinned = detectUnpinnedActions(ciContent);
130
+ for (const line of unpinned) {
131
+ issues.push({
132
+ code: "UNPINNED_ACTION",
133
+ message: `GitHub Action not pinned by SHA: ${line}`,
134
+ path: ".github/workflows/ci.yml",
135
+ hint: "Pin actions by full 40-char commit SHA"
136
+ });
137
+ }
138
+ }
139
+ // ── TypeScript strict mode ─────────────────────────────────────────────────
140
+ const tsconfigPath = path.join(projectPath, "tsconfig.json");
141
+ if (await fs.pathExists(tsconfigPath)) {
142
+ const tsconfig = await readJson(tsconfigPath);
143
+ if (!tsconfig?.compilerOptions?.strict) {
144
+ issues.push({
145
+ code: "TS_STRICT_DISABLED",
146
+ message: 'tsconfig.json does not have "strict": true',
147
+ path: "tsconfig.json",
148
+ hint: 'Add "strict": true under compilerOptions'
149
+ });
150
+ }
151
+ }
152
+ return issues;
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-ma/tpl",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "Generate, enforce and maintain clean project architectures",
5
5
  "type": "module",
6
6
  "repository": {