Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions e2e/bun.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { mkdtemp, rm } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { expect, test } from 'vitest'
import { runCommand } from '../scripts/utils.ts'

const ROOT = path.resolve(import.meta.dirname, '..')

// runCommand uses node:child_process exec, and E2E CI runs on ubuntu-latest,
// so these command strings intentionally use POSIX shell quoting.
function shellQuote(value: string): string {
return `'${value.replaceAll(`'`, `'\\''`)}'`
}

test('bun', async () => {
// install dependencies
await runCommand('bun install', {
Expand All @@ -12,3 +22,45 @@ test('bun', async () => {
})
expect(output).include(`Hello, Gunshi with Bun!`)
})

test('bun source completion', async () => {
const fixture = path.resolve(ROOT, 'packages/plugin-completion/examples/basic.node.ts')
const command = `bun ${shellQuote(fixture)}`
const script = await runCommand(`${command} complete zsh`, {
cwd: ROOT
})
expect(script).include('bun')

const output = await runCommand(`${command} complete -- --config vite.config`, {
cwd: ROOT
})
expect(output).include('vite.config.ts')
})

test('bun single binary completion', async () => {
const fixture = path.resolve(ROOT, 'packages/plugin-completion/examples/basic.node.ts')
const tmpDir = await mkdtemp(path.join(os.tmpdir(), 'gunshi-completion-bun-'))
const outfile = path.join(tmpDir, 'gunshi-completion-bun')

try {
await runCommand(
`bun build --compile ${shellQuote(fixture)} --outfile ${shellQuote(outfile)}`,
{
cwd: ROOT,
timeout: 60_000
}
)

const script = await runCommand(`${shellQuote(outfile)} complete zsh`, {
cwd: ROOT
})
expect(script).include(outfile)

const output = await runCommand(`${shellQuote(outfile)} complete -- --config vite.config`, {
cwd: ROOT
})
expect(output).include('vite.config.ts')
} finally {
await rm(tmpDir, { force: true, recursive: true })
}
})
12 changes: 3 additions & 9 deletions packages/plugin-completion/README.md

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can keep the README simpler here.

Instead of adding a dedicated Runtime Support section with Bun-specific implementation details, it may be enough to update the existing warning and the Shell Completion Setup prerequisites so users know that Node.js and Bun are supported while Deno is not supported yet.

The current Bun-specific text explains internal details such as the compiled-executable detection heuristic, which feels more appropriate for code comments, tests, or the PR description than for end-user documentation.

For the README, I think it is clearer to keep the focus on what users need to do to set up shell completions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ea008a9. I removed the dedicated Runtime Support section and kept the README focused on the warning and setup prerequisites.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This completion plugin is powered by [`@bomb.sh/tab`](https://github.com/bombshe
<!-- eslint-disable markdown/no-missing-label-refs -->

> [!WARNING]
> This package support Node.js runtime only. Deno and Bun support are coming soon.
> This package supports Node.js and Bun runtimes only. Deno support is not available yet.

<!-- eslint-enable markdown/no-missing-label-refs -->

Expand Down Expand Up @@ -122,14 +122,8 @@ This section provides detailed instructions for setting up shell completions in

### Prerequisites

Shell completion requires Node.js runtime. Ensure your CLI is running with Node.js (not Deno or Bun).

<!-- eslint-disable markdown/no-missing-label-refs -->

> [!WARNING]
> This package support Node.js runtime only. Deno and Bun support are coming soon.

<!-- eslint-enable markdown/no-missing-label-refs -->
Shell completion requires your CLI command to be executable from the generated completion script.
The plugin currently supports completion script generation for Node.js, Bun source execution, and Bun single-file executables.

### Setup by Shell

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-completion/docs/functions/default.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# Function: default()

```ts
function default(options): PluginWithoutExtension;
function default(options?): PluginWithoutExtension;
```

completion plugin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ Completion configuration, which structure is similar `bombsh/tab`'s `CompletionC

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="args"></a> `args?` | `Record`\<`string`, \{ `handler`: [`CompletionHandler`](../type-aliases/CompletionHandler.md); \}\> | The command arguments for the completion. |
| <a id="handler"></a> `handler?` | [`CompletionHandler`](../type-aliases/CompletionHandler.md) | The [`handler`](../type-aliases/CompletionHandler.md) for the completion. |
| <a id="property-args"></a> `args?` | `Record`\<`string`, \{ `handler`: [`CompletionHandler`](../type-aliases/CompletionHandler.md); \}\> | The command arguments for the completion. |
| <a id="property-handler"></a> `handler?` | [`CompletionHandler`](../type-aliases/CompletionHandler.md) | The [`handler`](../type-aliases/CompletionHandler.md) for the completion. |
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ Completion plugin options.

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="config"></a> `config?` | `object` | The completion configuration |
| <a id="property-config"></a> `config?` | `object` | The completion configuration |
| `config.entry?` | [`CompletionConfig`](CompletionConfig.md) | The entry point [`completion configuration`](CompletionConfig.md). |
| `config.subCommands?` | `Record`\<`string`, [`CompletionConfig`](CompletionConfig.md)\> | The handlers for sub-commands. |
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ Parameters for [`the completion handler`](../type-aliases/CompletionHandler.md).

| Property | Type | Description |
| ------ | ------ | ------ |
| <a id="locale"></a> `locale?` | `Locale` | The locale to use for i18n. |
| <a id="property-locale"></a> `locale?` | `Locale` | The locale to use for i18n. |
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[@gunshi/plugin-completion](../index.md) / CompletionHandler

# Type Alias: CompletionHandler()
# Type Alias: CompletionHandler

```ts
type CompletionHandler = (params) => Completion[];
Expand Down
153 changes: 153 additions & 0 deletions packages/plugin-completion/src/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @author kazuya kawaguchi (a.k.a. kazupon)
* @license MIT
*/

import { describe, expect, test } from 'vitest'
import {
detectRuntimeFromGlobals,
joinExecParts,
resolveBunExecParts,
resolveNodeExecParts,
shellQuote
} from './utils.ts'

import type { ProcessLike } from './utils.ts'

function createProcess(overrides: Partial<ProcessLike> = {}): ProcessLike {
return {
argv: ['/usr/local/bin/node', './cli.ts'],
execArgv: [],
execPath: '/usr/local/bin/node',
release: {
name: 'node'
},
...overrides
}
}

describe('detectRuntimeFromGlobals', () => {
test('prioritizes Bun over Deno and Node-compatible process', () => {
expect(
detectRuntimeFromGlobals({
Bun: {},
Deno: {},
process: createProcess()
})
).toBe('bun')
})

test('prioritizes Deno over Node-compatible process', () => {
expect(
detectRuntimeFromGlobals({
Deno: {},
process: createProcess()
})
).toBe('deno')
})

test('detects Node from process release name', () => {
expect(
detectRuntimeFromGlobals({
process: createProcess()
})
).toBe('node')
})
})

describe('resolveNodeExecParts', () => {
test('resolves Node source execution', () => {
expect(resolveNodeExecParts(createProcess())).toEqual(['/usr/local/bin/node', './cli.ts'])
})
})

describe('resolveBunExecParts', () => {
test('resolves Bun source execution', () => {
expect(
resolveBunExecParts(
createProcess({
argv: ['/usr/local/bin/bun', './src/cli.ts'],
execPath: '/usr/local/bin/bun'
})
)
).toEqual(['/usr/local/bin/bun', './src/cli.ts'])
})

test('resolves Bun source execution with execArgv', () => {
expect(
resolveBunExecParts(
createProcess({
argv: ['/usr/local/bin/bun', './src/cli.ts'],
execArgv: ['--smol'],
execPath: '/usr/local/bin/bun'
})
)
).toEqual(['/usr/local/bin/bun', '--smol', './src/cli.ts'])
})

test('resolves Bun single binary on Unix', () => {
expect(
resolveBunExecParts(
createProcess({
argv: ['/tmp/my-cli'],
execPath: '/tmp/my-cli'
})
)
).toEqual(['/tmp/my-cli'])
})

test('resolves Bun single binary on Windows', () => {
expect(
resolveBunExecParts(
createProcess({
argv: ['C:\\tools\\my-cli.exe'],
execPath: 'C:\\tools\\my-cli.exe'
})
)
).toEqual(['C:\\tools\\my-cli.exe'])
})

test('resolves Bun Unix virtual compiled entry', () => {
expect(
resolveBunExecParts(
createProcess({
argv: ['/usr/local/bin/bun', '/$bunfs/root/main.js'],
execPath: '/usr/local/bin/bun'
})
)
).toEqual(['/usr/local/bin/bun'])
})

test('resolves Bun Windows virtual compiled entry', () => {
expect(
resolveBunExecParts(
createProcess({
argv: ['C:\\tools\\bun.exe', 'B:\\~BUN\\root.js'],
execPath: 'C:\\tools\\bun.exe'
})
)
).toEqual(['C:\\tools\\bun.exe'])
})
})

describe('shellQuote', () => {
test('escapes spaces and single quotes', () => {
expect(shellQuote("/tmp/my cli's/bin")).toBe(`'/tmp/my cli'\\''s/bin'`)
})

test('joins quoted exec parts without extra spaces', () => {
expect(joinExecParts(['/tmp/my cli/bin', "--flag=it's", './cli.ts'])).toBe(
String.raw`'/tmp/my cli/bin' '--flag=it'\\''s' ./cli.ts`
)
})

test('escapes metacharacters for generated double-quoted shell assignment', () => {
expect(
joinExecParts(['/tmp/$(touch pwn)/my cli', '--loader=`touch pwn`', './"quoted".ts'])
).toBe("'/tmp/\\$(touch pwn)/my cli' '--loader=\\`touch pwn\\`' './\\\"quoted\\\".ts'")
})

test('preserves backslashes through generated double-quoted shell assignment', () => {
expect(joinExecParts(['C:\\tools\\my cli.exe'])).toBe("'C:\\\\tools\\\\my cli.exe'")
})
})
Loading
Loading