pack-check.mjs 5.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. /** Inspect the actual tarball; never substitute the source files list for release evidence. */
  2. import assert from 'node:assert/strict'
  3. import fs from 'node:fs'
  4. import os from 'node:os'
  5. import path from 'node:path'
  6. import { fileURLToPath } from 'node:url'
  7. import { execFileSync } from 'node:child_process'
  8. import { createHash } from 'node:crypto'
  9. import { isBuiltin } from 'node:module'
  10. import { build } from 'esbuild'
  11. import { parse } from 'yaml'
  12. import { skills as skillNames, thinScripts } from './artifact-contract.mjs'
  13. import { packageFiles, checkPublishableManifest } from '../../../scripts/release/tar.mjs'
  14. const packageRoot = fileURLToPath(new URL('../', import.meta.url))
  15. const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'scriptor-pack-'))
  16. const run = (exe, args, cwd = temp) => execFileSync(exe, args, { cwd, encoding: 'utf8', windowsHide: true, maxBuffer: 16 * 1024 * 1024 })
  17. let tarball = process.argv[2] && path.resolve(process.argv[2])
  18. if (!tarball) {
  19. assert.ok(process.env.npm_execpath, 'Run via pnpm pack-check, or pass an existing tarball')
  20. run(process.execPath, [process.env.npm_execpath, 'pack', '--pack-destination', temp], packageRoot)
  21. const packed = fs.readdirSync(temp).filter(name => name.endsWith('.tgz'))
  22. assert.equal(packed.length, 1)
  23. tarball = path.join(temp, packed[0])
  24. }
  25. // Validate all archive metadata before materializing regular files. No tar links or PAX global overrides.
  26. const extracted = path.join(temp, 'package')
  27. const packedFiles = packageFiles(tarball)
  28. for (const [relative, data] of packedFiles) {
  29. const target = path.join(extracted, ...relative.split('/'))
  30. fs.mkdirSync(path.dirname(target), { recursive: true })
  31. fs.writeFileSync(target, data)
  32. }
  33. const entries = [...packedFiles.keys()].map(file => `package/${file}`)
  34. const read = relative => fs.readFileSync(path.join(extracted, relative), 'utf8')
  35. const manifest = JSON.parse(read('package.json'))
  36. assert.equal(manifest.name, '@linfengqaqtat/dsh-scriptor')
  37. checkPublishableManifest(manifest)
  38. assert.equal(manifest.license, 'GPL-3.0-only')
  39. assert.equal(manifest.dsh.bundle.patch, './cordis.patch.yml')
  40. assert.equal(manifest.exports['./client'], './lib/client.js')
  41. assert.deepEqual(parse(read('cordis.patch.yml')), [{ insert: [{ id: 'webnovel', name: manifest.name }] }])
  42. for (const hook of ['preinstall', 'install', 'postinstall', 'prepare']) assert.equal(manifest.scripts?.[hook], undefined)
  43. for (const [name, version] of Object.entries({ ...manifest.dependencies, ...manifest.peerDependencies })) {
  44. assert.ok(!name.startsWith('@webnovel/'), `Unpublished runtime dependency: ${name}`)
  45. assert.ok(!/^(workspace:|file:|link:)/.test(version), `Local runtime dependency: ${name}`)
  46. }
  47. const scripts = thinScripts.map(([file]) => file)
  48. assert.deepEqual(fs.readdirSync(path.join(extracted, 'skills')).sort(), [...skillNames].sort())
  49. for (const skill of skillNames) {
  50. const text = read(`skills/${skill}/SKILL.md`)
  51. assert.equal(parse(text.split('---')[1]).name, skill)
  52. }
  53. for (const script of scripts) assert.match(read(script), /new URL\(['"]\.\.\/\.\.\/\.\.\/lib\/index\.js['"], import\.meta\.url\)/)
  54. for (const entry of entries) {
  55. const rel = entry.slice('package/'.length)
  56. assert.ok(/^(package\.json|README\.md|THIRD_PARTY_NOTICES\.md|licenses\/[^/]+\.txt|LICENSE|cordis\.patch\.yml|lib\/(index|client)\.js|skills\/[\w-]+\/.+\.(md|mjs))$/.test(rel), `Unexpected release file: ${rel}`)
  57. const text = read(rel)
  58. assert.ok(!text.includes(path.resolve(packageRoot, '../..')), `Workspace path embedded in ${rel}`)
  59. assert.ok(!/-----BEGIN .*PRIVATE KEY-----|sk-[A-Za-z0-9]{24,}/.test(text), `Credential in ${rel}`)
  60. if (!rel.endsWith('.md')) assert.ok(!/file:\/\/[A-Za-z/]*:|[A-Z]:[\\/]wk[\\/]/.test(text), `Local runtime path in ${rel}`)
  61. }
  62. assert.ok(read('LICENSE').includes('GNU GENERAL PUBLIC LICENSE'))
  63. assert.ok(read('THIRD_PARTY_NOTICES.md').includes('Original notices'))
  64. assert.ok(entries.some(entry => entry.startsWith('package/licenses/')))
  65. assert.ok(read('README.md').includes('--from-default-profile web'))
  66. assert.ok(read('lib/client.js').includes(`id: ${JSON.stringify(manifest.name)}`))
  67. const compiled = await build({ entryPoints: [path.join(extracted, 'lib/index.js')], bundle: true, packages: 'external', write: false, metafile: true, platform: 'node', format: 'esm', logLevel: 'silent' })
  68. const imports = [...new Set(Object.values(compiled.metafile.outputs).flatMap(output => output.imports).map(item => item.path))].sort()
  69. for (const name of imports) assert.ok(isBuiltin(name) || manifest.peerDependencies?.[name] || manifest.dependencies?.[name], `Undeclared packed import: ${name}`)
  70. assert.equal(Object.keys(compiled.metafile.inputs).length, 1, 'Packed Host must be self-contained apart from declared peers')
  71. const report = { package: manifest.name, version: manifest.version, tarball, sha256: createHash('sha256').update(fs.readFileSync(tarball)).digest('hex'), files: entries.length, skills: skillNames.length, scripts: scripts.length, imports, ok: true }
  72. fs.writeFileSync(path.join(temp, 'report.json'), JSON.stringify(report, null, 2) + '\n')
  73. console.log(JSON.stringify({ ...report, report: path.join(temp, 'report.json') }, null, 2))