From 36fb25421308395f662dd4421ce8fe98c826571d Mon Sep 17 00:00:00 2001 From: Steve Calvert Date: Sat, 10 Oct 2026 13:18:48 -0700 Subject: [PATCH 1/2] Report the package's own version and smoke-test the packed package yargs' argument-less .version() guesses from the package.json above its own node_modules, so an installed md-code printed the consumer project's version or 'unknown'. Pass VERSION explicitly. Add scripts/smoke-test-package.mjs (npm run test:package), which packs, installs into a temp project, and checks the bins, sync/check, the library import, and the tarball contents; run it in CI. --- .github/workflows/ci.yml | 3 + AGENTS.md | 2 + CONTRIBUTING.md | 3 + package.json | 3 +- scripts/smoke-test-package.mjs | 126 +++++++++++++++++++++++++++++++++ src/cli.ts | 3 +- 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 scripts/smoke-test-package.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70e195e..0866437 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,3 +40,6 @@ jobs: - name: Run tests run: npm run test + + - name: Smoke test the packed package + run: npm run test:package diff --git a/AGENTS.md b/AGENTS.md index 57c6f68..e0e432f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ Follow these guidelines when modifying the repository. - `npm run dev` – build in watch mode. - `npm run typecheck` – run TypeScript type checking with `tsc --noEmit`. +- `npm run test:package` – after `npm run build`, pack the package, install it + in a temp project, and check the CLI and library work as published. ## License diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee36dfe..9f3fe9c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,6 +67,9 @@ npm test # Run tests in watch mode npm run test:watch + +# Pack, install and run the built package (run `npm run build` first) +npm run test:package ``` ### Test Fixtures diff --git a/package.json b/package.json index 6dd522f..acc11f1 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "prepublishOnly": "npm run build", "test": "vitest run", "test:watch": "vitest", - "test:all": "npm run lint && npm run typecheck && npm run build && npm run test", + "test:all": "npm run lint && npm run typecheck && npm run build && npm run test && npm run test:package", + "test:package": "node scripts/smoke-test-package.mjs", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/scripts/smoke-test-package.mjs b/scripts/smoke-test-package.mjs new file mode 100644 index 0000000..ee1d32d --- /dev/null +++ b/scripts/smoke-test-package.mjs @@ -0,0 +1,126 @@ +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import assert from 'node:assert/strict'; + +const repoRoot = resolve(import.meta.dirname, '..'); +const { version } = JSON.parse( + readFileSync(join(repoRoot, 'package.json'), 'utf-8'), +); +const allowedFiles = new Set([ + 'LICENSE', + 'README.md', + 'package.json', + 'markdown-code.png', +]); +const isWindows = process.platform === 'win32'; + +function run(command, args, cwd) { + return execFileSync(command, args, { + cwd, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); +} + +function npm(args, cwd) { + if (isWindows) { + return execFileSync('npm.cmd', args, { + cwd, + encoding: 'utf-8', + shell: true, + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + } + return run('npm', args, cwd); +} + +function mdCode(bin, args, cwd) { + return npm(['exec', '--no', '--', bin, ...args], cwd); +} + +const workDir = mkdtempSync(join(tmpdir(), 'md-code-package-')); + +try { + const [packed] = JSON.parse( + npm(['pack', '--json', '--pack-destination', workDir], repoRoot), + ); + const unexpected = packed.files + .map((file) => file.path) + .filter((path) => !path.startsWith('dist/') && !allowedFiles.has(path)); + assert.deepEqual(unexpected, [], 'unexpected files in the package'); + assert.ok( + packed.files.some((file) => file.path === 'dist/index.d.ts'), + 'package is missing dist/index.d.ts', + ); + + const project = join(workDir, 'project'); + mkdirSync(join(project, 'snippets'), { recursive: true }); + writeFileSync( + join(project, 'package.json'), + JSON.stringify({ name: 'smoke', version: '0.0.0', private: true }), + ); + npm( + [ + 'install', + '--no-audit', + '--no-fund', + '--ignore-scripts', + join(workDir, packed.filename), + ], + project, + ); + + for (const bin of ['md-code', 'markdown-code']) { + assert.equal( + mdCode(bin, ['--version'], project), + version, + `${bin} --version`, + ); + } + + writeFileSync(join(project, 'snippets', 'add.js'), 'const sum = 1 + 1;\n'); + writeFileSync( + join(project, 'README.md'), + '# Smoke\n\n```js snippet=add.js\nstale\n```\n', + ); + writeFileSync( + join(project, '.markdown-coderc.json'), + JSON.stringify({ snippetRoot: './snippets', markdownGlob: 'README.md' }), + ); + + mdCode('md-code', ['sync'], project); + assert.equal( + readFileSync(join(project, 'README.md'), 'utf-8'), + '# Smoke\n\n```js snippet=add.js\nconst sum = 1 + 1;\n```\n', + ); + mdCode('md-code', ['check'], project); + + const exported = run( + 'node', + [ + '--input-type=module', + '--eval', + "const m = await import('markdown-code'); console.log([typeof m.syncMarkdownFiles, m.VERSION].join(' '));", + ], + project, + ); + assert.equal(exported, `function ${version}`, 'library import'); + assert.ok( + existsSync( + join(project, 'node_modules', 'markdown-code', 'dist', 'index.d.ts'), + ), + ); + + console.log(`markdown-code@${version} package smoke test passed`); +} finally { + rmSync(workDir, { recursive: true, force: true }); +} diff --git a/src/cli.ts b/src/cli.ts index 45a4fb8..ef1ce16 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,6 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import { VERSION } from './version.js'; yargs(hideBin(process.argv)) .scriptName('md-code') @@ -38,7 +39,7 @@ yargs(hideBin(process.argv)) }) .help() .alias('help', 'h') - .version() + .version(VERSION) .alias('version', 'v') .demandCommand(1, 'You need at least one command before moving on') .parse(); From c2fa887fc248dc2bb3de813dd6bb2f43e1f5a172 Mon Sep 17 00:00:00 2001 From: Steve Calvert Date: Sat, 10 Oct 2026 13:23:47 -0700 Subject: [PATCH 2/2] Run npm without a shell in the package smoke test Use node with npm_execpath so Windows paths with spaces work without shell quoting, and stop with a hint when not run through npm or when dist/ is missing. --- scripts/smoke-test-package.mjs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/smoke-test-package.mjs b/scripts/smoke-test-package.mjs index ee1d32d..5407b8e 100644 --- a/scripts/smoke-test-package.mjs +++ b/scripts/smoke-test-package.mjs @@ -21,7 +21,15 @@ const allowedFiles = new Set([ 'package.json', 'markdown-code.png', ]); -const isWindows = process.platform === 'win32'; +const npmCli = process.env.npm_execpath; + +if (!npmCli) { + throw new Error('Run this through npm: npm run test:package'); +} + +if (!existsSync(join(repoRoot, 'dist', 'index.js'))) { + throw new Error('dist/ is missing. Run npm run build first.'); +} function run(command, args, cwd) { return execFileSync(command, args, { @@ -32,15 +40,7 @@ function run(command, args, cwd) { } function npm(args, cwd) { - if (isWindows) { - return execFileSync('npm.cmd', args, { - cwd, - encoding: 'utf-8', - shell: true, - stdio: ['ignore', 'pipe', 'pipe'], - }).trim(); - } - return run('npm', args, cwd); + return run(process.execPath, [npmCli, ...args], cwd); } function mdCode(bin, args, cwd) {