Skip to content

Commit 438af46

Browse files
committed
chore(release): verify package contents for beta publish
1 parent 3c2c9ac commit 438af46

4 files changed

Lines changed: 74 additions & 179 deletions

File tree

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@ tests/results/
4040
test-update.ts
4141
repomix-output.xml
4242
scripts/
43+
tui/node_modules/
4344
tui/package-lock.json
4445
tui/types/

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "@tarquinen/opencode-dcp",
4-
"version": "3.2.4-beta0",
4+
"version": "3.2.6-beta0",
55
"type": "module",
66
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
77
"main": "./dist/index.js",
@@ -20,10 +20,25 @@
2020
"server",
2121
"tui"
2222
],
23+
"files": [
24+
"dist/",
25+
"index.ts",
26+
"lib/**/*.ts",
27+
"tui/index.tsx",
28+
"tui/data/*.ts",
29+
"tui/routes/*.tsx",
30+
"tui/shared/*.ts",
31+
"tui/slots/*.tsx",
32+
"README.md",
33+
"LICENSE",
34+
"dcp.schema.json"
35+
],
2336
"scripts": {
2437
"clean": "rm -rf dist",
2538
"build": "npm run clean && tsc",
26-
"prepublishOnly": "npm run build",
39+
"verify:package": "node scripts/verify-package.mjs",
40+
"check:package": "npm run build && npm run verify:package",
41+
"prepublishOnly": "npm run check:package",
2742
"dev": "opencode plugin dev",
2843
"typecheck": "tsc --noEmit",
2944
"tui:link-host-runtime": "node scripts/link-tui-host-runtime.mjs",

scripts/verify-package.mjs

Lines changed: 54 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,228 +1,107 @@
1-
import { builtinModules, createRequire } from "node:module"
2-
import { existsSync, readFileSync, statSync } from "node:fs"
31
import { execFileSync } from "node:child_process"
4-
import path from "node:path"
5-
import process from "node:process"
6-
import { fileURLToPath } from "node:url"
2+
import { existsSync, readFileSync } from "node:fs"
73

8-
const require = createRequire(import.meta.url)
9-
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
10-
11-
const builtinNames = new Set([
12-
...builtinModules,
13-
...builtinModules.map((name) => name.replace(/^node:/, "")),
14-
])
4+
const repoRoot = new URL("../", import.meta.url)
5+
const packageJsonPath = new URL("./package.json", repoRoot)
6+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"))
157

168
const requiredRepoFiles = [
179
"dist/index.js",
1810
"dist/index.d.ts",
1911
"dist/lib/config.js",
12+
"dist/tui/index.d.ts",
13+
"index.ts",
14+
"lib/config.ts",
15+
"tui/index.tsx",
16+
"tui/data/context.ts",
17+
"tui/routes/summary.tsx",
18+
"tui/shared/names.ts",
19+
"tui/shared/theme.ts",
20+
"tui/shared/types.ts",
21+
"tui/slots/sidebar-content.tsx",
2022
"README.md",
2123
"LICENSE",
24+
"dcp.schema.json",
2225
]
2326

2427
const requiredTarballFiles = [
2528
"package.json",
2629
"dist/index.js",
2730
"dist/index.d.ts",
2831
"dist/lib/config.js",
32+
"dist/tui/index.js",
33+
"dist/tui/index.d.ts",
34+
"index.ts",
35+
"lib/config.ts",
36+
"tui/index.tsx",
37+
"tui/data/context.ts",
38+
"tui/routes/summary.tsx",
39+
"tui/shared/names.ts",
40+
"tui/shared/theme.ts",
41+
"tui/shared/types.ts",
42+
"tui/slots/sidebar-content.tsx",
2943
"README.md",
3044
"LICENSE",
45+
"dcp.schema.json",
3146
]
3247

3348
const forbiddenTarballPatterns = [
49+
/^tui\/node_modules\//,
3450
/^node_modules\//,
35-
/^lib\//,
36-
/^index\.ts$/,
3751
/^tests\//,
3852
/^scripts\//,
3953
/^docs\//,
4054
/^assets\//,
4155
/^notes\//,
4256
/^\.github\//,
4357
/^package-lock\.json$/,
44-
/^tsconfig\.json$/,
4558
]
4659

47-
const packageInfoCache = new Map()
48-
49-
function fail(message) {
60+
const fail = (message) => {
5061
console.error(`package verification failed: ${message}`)
5162
process.exit(1)
5263
}
5364

54-
function assertRepoFilesExist() {
55-
for (const relativePath of requiredRepoFiles) {
56-
if (!existsSync(path.join(root, relativePath))) {
57-
fail(`missing required file: ${relativePath}`)
58-
}
65+
for (const relativePath of requiredRepoFiles) {
66+
const absolutePath = new URL(`./${relativePath}`, repoRoot)
67+
if (!existsSync(absolutePath)) {
68+
fail(`missing required repo file '${relativePath}'`)
5969
}
6070
}
6171

62-
function assertPackageJsonShape() {
63-
const pkg = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"))
64-
65-
if (pkg.main !== "./dist/index.js") {
66-
fail(`package.json main must remain ./dist/index.js, found ${pkg.main ?? "<missing>"}`)
67-
}
68-
69-
const files = Array.isArray(pkg.files) ? pkg.files : []
70-
for (const entry of ["dist/", "README.md", "LICENSE"]) {
71-
if (!files.includes(entry)) {
72-
fail(`package.json files must include ${entry}`)
73-
}
74-
}
72+
if (packageJson.exports?.["./tui"]?.import !== "./tui/index.tsx") {
73+
fail("expected package.json exports['./tui'].import to be './tui/index.tsx'")
7574
}
7675

77-
function getImportStatements(source) {
78-
const pattern = /^\s*import\s+([^\n;]+?)\s+from\s+["']([^"']+)["']/gm
79-
return Array.from(source.matchAll(pattern), (match) => ({
80-
clause: match[1].trim(),
81-
specifier: match[2],
82-
}))
76+
if (packageJson.exports?.["."]?.import !== "./dist/index.js") {
77+
fail("expected package.json exports['.'].import to be './dist/index.js'")
8378
}
8479

85-
function getImportKind(clause) {
86-
if (clause.startsWith("type ")) return "type"
87-
if (clause.startsWith("* as ")) return "namespace"
88-
if (clause.startsWith("{")) return "named"
89-
if (clause.includes(",")) {
90-
const [, trailing = ""] = clause.split(",", 2)
91-
return trailing.trim().startsWith("* as ") ? "default+namespace" : "default+named"
92-
}
93-
return "default"
94-
}
80+
const packOutput = execFileSync("npm", ["pack", "--dry-run", "--json"], {
81+
cwd: repoRoot,
82+
encoding: "utf8",
83+
})
9584

96-
function getPackageName(specifier) {
97-
if (specifier.startsWith("@")) {
98-
const parts = specifier.split("/")
99-
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier
100-
}
101-
return specifier.split("/")[0]
85+
const packResult = JSON.parse(packOutput)
86+
if (!Array.isArray(packResult) || packResult.length !== 1 || !Array.isArray(packResult[0]?.files)) {
87+
fail("unexpected npm pack JSON output")
10288
}
10389

104-
function resolveLocalImport(importerPath, specifier) {
105-
const basePath = path.resolve(path.dirname(importerPath), specifier)
106-
const candidates = [
107-
basePath,
108-
`${basePath}.ts`,
109-
`${basePath}.tsx`,
110-
`${basePath}.js`,
111-
`${basePath}.mjs`,
112-
path.join(basePath, "index.ts"),
113-
path.join(basePath, "index.tsx"),
114-
path.join(basePath, "index.js"),
115-
path.join(basePath, "index.mjs"),
116-
]
90+
const tarballFiles = new Set(packResult[0].files.map((entry) => entry.path))
11791

118-
for (const candidate of candidates) {
119-
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate
92+
for (const relativePath of requiredTarballFiles) {
93+
if (!tarballFiles.has(relativePath)) {
94+
fail(`tarball is missing required file '${relativePath}'`)
12095
}
121-
122-
fail(`unable to resolve local import ${specifier} from ${path.relative(root, importerPath)}`)
12396
}
12497

125-
function findPackageInfo(packageName, importerPath) {
126-
const cacheKey = `${packageName}::${path.dirname(importerPath)}`
127-
if (packageInfoCache.has(cacheKey)) {
128-
return packageInfoCache.get(cacheKey)
129-
}
130-
131-
let entry
132-
try {
133-
entry = require.resolve(packageName, { paths: [path.dirname(importerPath)] })
134-
} catch {
135-
packageInfoCache.set(cacheKey, null)
136-
return null
137-
}
138-
139-
let current = path.dirname(entry)
140-
while (true) {
141-
const manifest = path.join(current, "package.json")
142-
if (existsSync(manifest)) {
143-
const info = JSON.parse(readFileSync(manifest, "utf8"))
144-
packageInfoCache.set(cacheKey, info)
145-
return info
146-
}
147-
const parent = path.dirname(current)
148-
if (parent === current) {
149-
packageInfoCache.set(cacheKey, null)
150-
return null
98+
for (const relativePath of tarballFiles) {
99+
for (const pattern of forbiddenTarballPatterns) {
100+
if (pattern.test(relativePath)) {
101+
fail(`tarball contains forbidden path '${relativePath}'`)
151102
}
152-
current = parent
153103
}
154104
}
155105

156-
function packageLooksCommonJs(pkg) {
157-
if (!pkg) return false
158-
if (pkg.type === "commonjs") return true
159-
160-
const main = typeof pkg.main === "string" ? pkg.main : ""
161-
return /(?:^|\/)(cjs|umd)(?:\/|$)/.test(main) || main.endsWith(".cjs")
162-
}
163-
164-
function validateRuntimeImportGraph() {
165-
const pending = [path.join(root, "index.ts")]
166-
const seen = new Set()
167-
168-
while (pending.length > 0) {
169-
const filePath = pending.pop()
170-
if (!filePath || seen.has(filePath)) continue
171-
seen.add(filePath)
172-
173-
const source = readFileSync(filePath, "utf8")
174-
for (const entry of getImportStatements(source)) {
175-
if (entry.specifier.startsWith(".")) {
176-
pending.push(resolveLocalImport(filePath, entry.specifier))
177-
continue
178-
}
179-
180-
const packageName = getPackageName(entry.specifier)
181-
if (builtinNames.has(packageName)) continue
182-
183-
const kind = getImportKind(entry.clause)
184-
if (kind === "type" || kind === "namespace") continue
185-
186-
const pkg = findPackageInfo(packageName, filePath)
187-
if (packageLooksCommonJs(pkg)) {
188-
fail(
189-
`${path.relative(root, filePath)} uses ${kind} import from CommonJS-style package ${packageName}`,
190-
)
191-
}
192-
}
193-
}
194-
}
195-
196-
function validatePackedFiles() {
197-
const output = execFileSync("npm", ["pack", "--dry-run", "--json"], {
198-
cwd: root,
199-
encoding: "utf8",
200-
})
201-
202-
const [result] = JSON.parse(output)
203-
if (!result || !Array.isArray(result.files)) {
204-
fail("npm pack --dry-run --json did not return file metadata")
205-
}
206-
207-
const packedPaths = result.files.map((file) => file.path)
208-
for (const required of requiredTarballFiles) {
209-
if (!packedPaths.includes(required)) {
210-
fail(`packed tarball is missing ${required}`)
211-
}
212-
}
213-
214-
const forbidden = packedPaths.find((file) =>
215-
forbiddenTarballPatterns.some((pattern) => pattern.test(file)),
216-
)
217-
if (forbidden) {
218-
fail(`packed tarball contains forbidden path ${forbidden}`)
219-
}
220-
221-
console.log(`package verification passed for ${result.name}@${result.version}`)
222-
console.log(`tarball entries: ${result.entryCount}`)
223-
}
224-
225-
assertRepoFilesExist()
226-
assertPackageJsonShape()
227-
validateRuntimeImportGraph()
228-
validatePackedFiles()
106+
console.log(`package verification passed for ${packageJson.name}@${packageJson.version}`)
107+
console.log(`tarball entries: ${packResult[0].entryCount}`)

0 commit comments

Comments
 (0)