@oss-ma/tpl 1.0.33 → 1.0.34

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,10 +7,10 @@ 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");
@@ -70,12 +70,13 @@ async function verifyIntegrity(projectPath, manifest) {
70
70
  }
71
71
  // ── Output ───────────────────────────────────────────────────────────────────
72
72
  function printHuman(result) {
73
+ const templateLabel = result.template ? pc.gray(` [${result.template}]`) : "";
73
74
  if (result.ok) {
74
- console.log(pc.green(`✅ OK — Standard v${result.schemaVersion}`));
75
+ console.log(pc.green(`✅ OK — Standard v${result.schemaVersion}${templateLabel}`));
75
76
  console.log(`Project: ${result.projectPath}`);
76
77
  return;
77
78
  }
78
- console.log(pc.red(`❌ FAILED — Standard v${result.schemaVersion}`));
79
+ console.log(pc.red(`❌ FAILED — Standard v${result.schemaVersion}${templateLabel}`));
79
80
  console.log(`Project: ${result.projectPath}`);
80
81
  console.log("");
81
82
  for (const issue of result.issues) {
@@ -104,11 +105,14 @@ export const checkCommand = new Command("check")
104
105
  if (!opts.skipIntegrity && lock?.filesIntegrity) {
105
106
  integrityIssues = await verifyIntegrity(standardResult.projectPath, lock.filesIntegrity);
106
107
  }
107
- // 4) Template-specific
108
+ // 4) Template-specific validation
108
109
  let templateIssues = [];
109
110
  if (lock?.template === "react-ts") {
110
111
  templateIssues = await validateReactTs(standardResult.projectPath);
111
112
  }
113
+ else if (lock?.template === "react-next") {
114
+ templateIssues = await validateReactNext(standardResult.projectPath);
115
+ }
112
116
  // 5) Packs
113
117
  let packIssues = [];
114
118
  if (lock?.packs?.length) {
@@ -142,8 +146,9 @@ export const checkCommand = new Command("check")
142
146
  const ok = standardResult.issues.length +
143
147
  blockingIntegrityIssues.length +
144
148
  templateIssues.length +
145
- blockingPackIssues.length === 0;
146
- const result = { ...standardResult, issues, ok };
149
+ blockingPackIssues.length ===
150
+ 0;
151
+ const result = { ...standardResult, issues, ok, template: lock?.template };
147
152
  if (opts.json)
148
153
  console.log(JSON.stringify(result, null, 2));
149
154
  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.34",
4
4
  "description": "Generate, enforce and maintain clean project architectures",
5
5
  "type": "module",
6
6
  "repository": {