poe-code 3.0.275 → 3.0.276
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/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/packages/package-lint/dist/model.js +41 -9
- package/packages/package-lint/dist/report.js +7 -1
- package/packages/package-lint/dist/rules/exports-subpath-resolvable.js +0 -0
- package/packages/package-lint/dist/rules/index.js +9 -0
- package/packages/package-lint/dist/rules/published-bin-must-be-executable.js +32 -1
- package/packages/package-lint/dist/source-imports.js +13 -10
package/package.json
CHANGED
|
@@ -14,6 +14,11 @@ function toStringRecord(value) {
|
|
|
14
14
|
}
|
|
15
15
|
return out;
|
|
16
16
|
}
|
|
17
|
+
function toBinRecord(value, packageName) {
|
|
18
|
+
if (typeof value === "string")
|
|
19
|
+
return { [packageName]: value };
|
|
20
|
+
return toStringRecord(value);
|
|
21
|
+
}
|
|
17
22
|
function toStringArray(value) {
|
|
18
23
|
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
19
24
|
}
|
|
@@ -46,7 +51,7 @@ async function loadPackage(fs, rootDir, relDir, isRoot) {
|
|
|
46
51
|
repositoryDirectory: typeof repository?.directory === "string" ? repository.directory : undefined,
|
|
47
52
|
ecosystem,
|
|
48
53
|
exports: pkg.exports,
|
|
49
|
-
bin:
|
|
54
|
+
bin: toBinRecord(pkg.bin, typeof pkg.name === "string" ? pkg.name : relDir),
|
|
50
55
|
files: Array.isArray(pkg.files)
|
|
51
56
|
? pkg.files.filter((f) => typeof f === "string")
|
|
52
57
|
: [],
|
|
@@ -88,6 +93,29 @@ function normalizeWorkingDir(wd) {
|
|
|
88
93
|
p = p.slice(2);
|
|
89
94
|
return p.endsWith("/") ? p.slice(0, -1) : p;
|
|
90
95
|
}
|
|
96
|
+
function splitShellLine(line) {
|
|
97
|
+
return line
|
|
98
|
+
.trim()
|
|
99
|
+
.split(" ")
|
|
100
|
+
.map((part) => part.trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
function hasPublishRun(run) {
|
|
104
|
+
for (const line of run.split("\n")) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (trimmed.length === 0 || trimmed.startsWith("#"))
|
|
107
|
+
continue;
|
|
108
|
+
const parts = splitShellLine(trimmed);
|
|
109
|
+
if (parts[0] === "npm" && parts[1] === "publish")
|
|
110
|
+
return true;
|
|
111
|
+
if (parts.includes("semantic-release"))
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function isPypiPublishAction(uses) {
|
|
117
|
+
return uses === "pypa/gh-action-pypi-publish" || uses.startsWith("pypa/gh-action-pypi-publish@");
|
|
118
|
+
}
|
|
91
119
|
function parseLockstepGroup(value) {
|
|
92
120
|
if (typeof value !== "string")
|
|
93
121
|
return { dirs: [], valid: false };
|
|
@@ -115,17 +143,19 @@ function parseReleaseWorkflow(file, raw) {
|
|
|
115
143
|
const lockstepGroups = [];
|
|
116
144
|
const jobs = doc?.jobs && typeof doc.jobs === "object" ? doc.jobs : {};
|
|
117
145
|
for (const job of Object.values(jobs)) {
|
|
146
|
+
const workflowJob = job && typeof job === "object" ? job : {};
|
|
118
147
|
const activeLockstepGroups = [];
|
|
119
|
-
const steps = Array.isArray(
|
|
120
|
-
|
|
121
|
-
|
|
148
|
+
const steps = Array.isArray(workflowJob.steps) ? workflowJob.steps : [];
|
|
149
|
+
const defaultWorkingDir = typeof workflowJob.defaults?.run?.["working-directory"] === "string"
|
|
150
|
+
? normalizeWorkingDir(workflowJob.defaults.run["working-directory"])
|
|
151
|
+
: undefined;
|
|
122
152
|
for (const step of steps) {
|
|
123
153
|
const run = typeof step.run === "string" ? step.run : "";
|
|
124
154
|
const uses = typeof step.uses === "string" ? step.uses : "";
|
|
125
155
|
const wd = typeof step["working-directory"] === "string"
|
|
126
156
|
? normalizeWorkingDir(step["working-directory"])
|
|
127
|
-
:
|
|
128
|
-
if (
|
|
157
|
+
: defaultWorkingDir;
|
|
158
|
+
if (hasPublishRun(run)) {
|
|
129
159
|
const targetDir = wd ?? ".";
|
|
130
160
|
targetDirs.add(targetDir);
|
|
131
161
|
for (const group of activeLockstepGroups) {
|
|
@@ -133,14 +163,14 @@ function parseReleaseWorkflow(file, raw) {
|
|
|
133
163
|
group.publishedDirs.push(targetDir);
|
|
134
164
|
}
|
|
135
165
|
}
|
|
136
|
-
if (uses
|
|
166
|
+
if (isPypiPublishAction(uses)) {
|
|
137
167
|
const pd = step.with?.["packages-dir"];
|
|
138
168
|
if (typeof pd === "string")
|
|
139
169
|
targetDirs.add(dirFromArtifactPath(pd));
|
|
140
170
|
}
|
|
141
171
|
if (uses === "./.github/actions/prepare-lockstep-release") {
|
|
142
172
|
const parsed = parseLockstepGroup(step.with?.packages);
|
|
143
|
-
const validVersion = typeof step.with?.version === "string" && step.with.version.length > 0;
|
|
173
|
+
const validVersion = typeof step.with?.version === "string" && step.with.version.trim().length > 0;
|
|
144
174
|
const group = { ...parsed, valid: parsed.valid && validVersion, publishedDirs: [] };
|
|
145
175
|
lockstepGroups.push(group);
|
|
146
176
|
activeLockstepGroups.push(group);
|
|
@@ -159,7 +189,9 @@ async function loadReleaseWorkflows(fs, rootDir) {
|
|
|
159
189
|
return [];
|
|
160
190
|
}
|
|
161
191
|
const files = entries
|
|
162
|
-
.filter((e) => !e.isDirectory() &&
|
|
192
|
+
.filter((e) => !e.isDirectory() &&
|
|
193
|
+
e.name.startsWith("release") &&
|
|
194
|
+
(e.name.endsWith(".yml") || e.name.endsWith(".yaml")))
|
|
163
195
|
.map((e) => e.name)
|
|
164
196
|
.sort();
|
|
165
197
|
const workflows = await Promise.all(files.map(async (file) => parseReleaseWorkflow(file, await fs.readFile(path.join(dir, file)))));
|
|
@@ -55,7 +55,13 @@ export function formatReport(result, opts) {
|
|
|
55
55
|
const errors = result.violations.filter((v) => v.severity === "error").length;
|
|
56
56
|
const warnings = result.violations.length - errors;
|
|
57
57
|
if (result.summary.ok) {
|
|
58
|
-
|
|
58
|
+
if (result.skipped.length > 0) {
|
|
59
|
+
const passed = result.summary.rules - result.skipped.length;
|
|
60
|
+
lines.push(color.green(`✓ ${passed} rules passed · ${result.skipped.length} skipped`));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
lines.push(color.green(`✓ all ${result.summary.rules} rules passed`));
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
else {
|
|
61
67
|
const summary = `${failedRules} rules failed · ${result.violations.length} violations`;
|
|
Binary file
|
|
@@ -23,6 +23,15 @@ export const rules = [
|
|
|
23
23
|
publishedBinMustBeExecutable
|
|
24
24
|
];
|
|
25
25
|
export function runRules(model, build, only) {
|
|
26
|
+
if (only && only.length > 0) {
|
|
27
|
+
const known = new Set(rules.map((rule) => rule.id));
|
|
28
|
+
const unknown = only.filter((id) => !known.has(id));
|
|
29
|
+
if (unknown.length > 0) {
|
|
30
|
+
throw new Error(`Unknown package-lint rule: ${unknown.join(", ")}. Known rules: ${rules
|
|
31
|
+
.map((rule) => rule.id)
|
|
32
|
+
.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
26
35
|
const selected = only && only.length > 0 ? rules.filter((r) => only.includes(r.id)) : rules;
|
|
27
36
|
const violations = [];
|
|
28
37
|
const skipped = [];
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { isGenuinelyPublished } from "../model.js";
|
|
2
2
|
const id = "published-bin-must-be-executable";
|
|
3
|
+
function splitShellLine(line) {
|
|
4
|
+
return line
|
|
5
|
+
.trim()
|
|
6
|
+
.split(" ")
|
|
7
|
+
.map((part) => part.trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
function stripQuotes(value) {
|
|
11
|
+
let out = value;
|
|
12
|
+
if (((out.startsWith('"') && out.endsWith('"')) || (out.startsWith("'") && out.endsWith("'"))) &&
|
|
13
|
+
out.length >= 2) {
|
|
14
|
+
out = out.slice(1, -1);
|
|
15
|
+
}
|
|
16
|
+
return out.replaceAll("\\", "/");
|
|
17
|
+
}
|
|
18
|
+
function prepackRunsSetBinExecutable(prepack) {
|
|
19
|
+
for (const line of prepack.split("\n")) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (trimmed.length === 0 || trimmed.startsWith("#"))
|
|
22
|
+
continue;
|
|
23
|
+
for (const part of splitShellLine(trimmed)) {
|
|
24
|
+
const token = stripQuotes(part);
|
|
25
|
+
if (token === "scripts/set-bin-executable.mjs" ||
|
|
26
|
+
token === "./scripts/set-bin-executable.mjs" ||
|
|
27
|
+
token.endsWith("/scripts/set-bin-executable.mjs")) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
3
34
|
/**
|
|
4
35
|
* `tsc` emits bin files with mode 0644, so a package that builds its bin with
|
|
5
36
|
* the compiler ships a binary that loses its executable bit and fails with
|
|
@@ -19,7 +50,7 @@ export const publishedBinMustBeExecutable = {
|
|
|
19
50
|
const bins = Object.values(pkg.bin);
|
|
20
51
|
if (bins.length === 0)
|
|
21
52
|
continue;
|
|
22
|
-
if ((pkg.scripts.prepack ?? "")
|
|
53
|
+
if (prepackRunsSetBinExecutable(pkg.scripts.prepack ?? ""))
|
|
23
54
|
continue;
|
|
24
55
|
violations.push({
|
|
25
56
|
rule: id,
|
|
@@ -36,7 +36,12 @@ function extractImportsFromAst(text, fileName) {
|
|
|
36
36
|
}
|
|
37
37
|
else if (ts.isExportDeclaration(statement) && statement.moduleSpecifier) {
|
|
38
38
|
if (ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
39
|
-
|
|
39
|
+
let typeOnly = statement.isTypeOnly;
|
|
40
|
+
if (!typeOnly && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
41
|
+
const elements = statement.exportClause.elements;
|
|
42
|
+
typeOnly = elements.length > 0 && elements.every((e) => e.isTypeOnly);
|
|
43
|
+
}
|
|
44
|
+
out.push({ specifier: statement.moduleSpecifier.text, typeOnly });
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
else if (ts.isImportEqualsDeclaration(statement)) {
|
|
@@ -62,13 +67,12 @@ function extractImportsFromAst(text, fileName) {
|
|
|
62
67
|
return out;
|
|
63
68
|
}
|
|
64
69
|
export function extractRelevantImports(text, fileName) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}));
|
|
70
|
+
return extractImportsFromAst(text, fileName);
|
|
71
|
+
}
|
|
72
|
+
function isSourceFile(name) {
|
|
73
|
+
if (name.endsWith(".d.ts") || name.endsWith(".d.mts") || name.endsWith(".d.cts"))
|
|
74
|
+
return false;
|
|
75
|
+
return (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".mts") || name.endsWith(".cts"));
|
|
72
76
|
}
|
|
73
77
|
async function listSourceFiles(fs, dir) {
|
|
74
78
|
let entries;
|
|
@@ -84,8 +88,7 @@ async function listSourceFiles(fs, dir) {
|
|
|
84
88
|
if (entry.isDirectory()) {
|
|
85
89
|
files.push(...(await listSourceFiles(fs, full)));
|
|
86
90
|
}
|
|
87
|
-
else if ((entry.name
|
|
88
|
-
!entry.name.endsWith(".d.ts")) {
|
|
91
|
+
else if (isSourceFile(entry.name)) {
|
|
89
92
|
files.push(full);
|
|
90
93
|
}
|
|
91
94
|
}
|