diff --git a/packages/compiler/bin/tempblot.ts b/packages/compiler/bin/tempblot.ts index 6e45f16..a8fdd43 100755 --- a/packages/compiler/bin/tempblot.ts +++ b/packages/compiler/bin/tempblot.ts @@ -1,45 +1,50 @@ #!/usr/bin/env node -import { cloneTemplate, spawnSafe } from "@tempblot/utils" -import t from '@bomb.sh/tab'; +import { cloneTemplate, spawnSafe } from "@tempblot/utils"; +import t from "@bomb.sh/tab"; import { parse } from "@bomb.sh/args"; -import { text, isCancel } from '@clack/prompts'; -import * as path from 'node:path'; -import * as fs from 'node:fs'; +import { text, isCancel } from "@clack/prompts"; +import * as path from "node:path"; +import * as fs from "node:fs"; // tempblot get --url -const getCmd = t.command('get', 'Get a template from a git repository'); -getCmd.option('url', 'Specify the git repository URL', (complete) => { - complete('https://github.com/crutchcorn/tempblot.git', 'Default repository'); +const getCmd = t.command("get", "Get a template from a git repository"); +getCmd.option("url", "Specify the git repository URL", (complete) => { + complete("https://github.com/crutchcorn/tempblot.git", "Default repository"); }); const argv = process.argv.slice(2); const args = parse(argv, { - string: ['url'], + string: ["url"], }); -const url = args.url || await text({ - message: 'What is the URL of the git repository?', - placeholder: 'https://github.com/crutchcorn/tempblot.git', -}) as string; +const url = + args.url || + ((await text({ + message: "What is the URL of the git repository?", + placeholder: "https://github.com/crutchcorn/tempblot.git", + })) as string); if (isCancel(url)) { - console.log('Operation cancelled.'); - process.exit(0); + console.log("Operation cancelled."); + process.exit(0); } -const {tmpPath} = await cloneTemplate({ url }); +const { tmpPath } = await cloneTemplate({ url }); const configPath = path.resolve(tmpPath, "tempblot.config.ts"); if (!fs.existsSync(configPath)) { - console.error(`Configuration file not found at ${configPath}`); - process.exit(1); + console.error(`Configuration file not found at ${configPath}`); + process.exit(1); } -const {new_child_process} = spawnSafe({file: configPath, defaultFSPaths: [process.cwd(), tmpPath]}) +const { new_child_process } = spawnSafe({ + file: configPath, + defaultFSPaths: [process.cwd(), tmpPath], +}); new_child_process.stdout.pipe(process.stdout); new_child_process.stderr.pipe(process.stderr); -new_child_process.on('exit', (code) => { - process.exit(code || 0); -}); \ No newline at end of file +new_child_process.on("exit", (code) => { + process.exit(code || 0); +}); diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 7bbdd7f..fc78048 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -45,6 +45,7 @@ "@arethetypeswrong/cli": "^0.18.2", "@tempblot/config": "workspace:*", "@types/node": "^26.2.0", + "cli-testing-library": "^4.0.0", "publint": "^0.3.21", "unplugin-dts": "^1.0.0-beta.6", "vite": "^8.0.12", diff --git a/packages/compiler/tests/bin.spec.ts b/packages/compiler/tests/bin.spec.ts new file mode 100644 index 0000000..dc61496 --- /dev/null +++ b/packages/compiler/tests/bin.spec.ts @@ -0,0 +1,77 @@ +import { execFileSync } from "node:child_process"; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + symlinkSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; + +import { cleanup, render } from "cli-testing-library"; +import "cli-testing-library/vitest"; +import { expect, test } from "vitest"; + +const packageRoot = resolve(import.meta.dirname, ".."); + +test("the packed binary runs outside the project", async () => { + const temporaryRoot = mkdtempSync(join(tmpdir(), "tempblot-bin-")); + + try { + const packOutput = execFileSync( + "npm", + ["pack", "--json", "--pack-destination", temporaryRoot], + { + cwd: packageRoot, + encoding: "utf8", + env: { + ...process.env, + npm_config_cache: join(temporaryRoot, "npm-cache"), + }, + }, + ); + const [{ filename, files }] = JSON.parse(packOutput) as [ + { + filename: string; + files: { path: string }[]; + }, + ]; + + expect(files.map(({ path }) => path)).toContain("dist/bin/tempblot.js"); + + execFileSync("tar", [ + "-xzf", + join(temporaryRoot, filename), + "-C", + temporaryRoot, + ]); + + const packageDirectory = join(temporaryRoot, "package"); + const packageJson = JSON.parse( + readFileSync(join(packageDirectory, "package.json"), "utf8"), + ) as { bin: string }; + const binaryPath = resolve(packageDirectory, packageJson.bin); + + expect(statSync(binaryPath).mode & 0o111).not.toBe(0); + + const installedBinary = join( + temporaryRoot, + "consumer/node_modules/.bin/tempblot", + ); + mkdirSync(dirname(installedBinary), { recursive: true }); + symlinkSync(binaryPath, installedBinary); + + const cli = await render(installedBinary, [], { + cwd: join(temporaryRoot, "consumer"), + }); + + expect( + await cli.findByText("What is the URL of the git repository?"), + ).toBeInTheConsole(); + } finally { + await cleanup(); + rmSync(temporaryRoot, { recursive: true, force: true }); + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1db5f43..d819fa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,6 +139,9 @@ importers: '@types/node': specifier: ^26.2.0 version: 26.2.0 + cli-testing-library: + specifier: ^4.0.0 + version: 4.0.0(vitest@4.1.6(@types/node@26.2.0)(vite@8.0.12(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))) publint: specifier: ^0.3.21 version: 0.3.21 @@ -402,6 +405,10 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@8.0.0': + resolution: {integrity: sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/compat-data@7.29.3': resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} engines: {node: '>=6.9.0'} @@ -444,6 +451,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.4': + resolution: {integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -2226,6 +2237,18 @@ packages: vitest: optional: true + cli-testing-library@4.0.0: + resolution: {integrity: sha512-QfC3cJyIA8z9YltHTtHG+zWgxxPIaMctikPZBR5f3ZG9Ku+DvzLseH3aybijuyd1PQOD/RmIfI9A/O5TFhSR+w==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@jest/globals': ^29.0.0 || ^30.0.0 + vitest: ^3.2.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + '@jest/globals': + optional: true + vitest: + optional: true + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -2987,6 +3010,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3970,6 +3996,10 @@ packages: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} + slice-ansi@9.0.0: + resolution: {integrity: sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA==} + engines: {node: '>=22'} + smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} @@ -4819,6 +4849,11 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@8.0.0': + dependencies: + '@babel/helper-validator-identifier': 8.0.4 + js-tokens: 10.0.0 + '@babel/compat-data@7.29.3': {} '@babel/core@7.29.0': @@ -4881,6 +4916,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@8.0.4': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.29.2': @@ -6398,6 +6435,18 @@ snapshots: optionalDependencies: vitest: 4.1.6(@types/node@26.2.0)(vite@8.0.12(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0)) + cli-testing-library@4.0.0(vitest@4.1.6(@types/node@26.2.0)(vite@8.0.12(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0))): + dependencies: + '@babel/code-frame': 8.0.0 + picocolors: 1.1.1 + redent: 4.0.0 + slice-ansi: 9.0.0 + strip-ansi: 7.2.0 + strip-final-newline: 4.0.0 + tree-kill: 1.2.2 + optionalDependencies: + vitest: 4.1.6(@types/node@26.2.0)(vite@8.0.12(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.9.0)) + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -7274,6 +7323,8 @@ snapshots: jiti@2.7.0: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -8842,6 +8893,11 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + slice-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + smol-toml@1.6.1: {} source-map-js@1.2.1: {}