@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.
package/dist/commands/check.js
CHANGED
|
@@ -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 ===
|
|
146
|
-
|
|
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
|
+
}
|