@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.
- package/dist/commands/check.js +31 -11
- package/dist/engine/validators/reactNext.js +153 -0
- package/package.json +1 -1
package/dist/commands/check.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 ===
|
|
146
|
-
|
|
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
|
+
}
|