diff --git a/.changeset/fix-create-extension-bin-entrypoint.md b/.changeset/fix-create-extension-bin-entrypoint.md new file mode 100644 index 0000000000..51809d3a95 --- /dev/null +++ b/.changeset/fix-create-extension-bin-entrypoint.md @@ -0,0 +1,5 @@ +--- +"create-lynx-extension": patch +--- + +Fix npm bin symlink entrypoint detection for the create extension CLI. diff --git a/packages/lynx/create-lynx-extension/src/cli.ts b/packages/lynx/create-lynx-extension/src/cli.ts index 5d41ba1af7..8a23de8c4c 100644 --- a/packages/lynx/create-lynx-extension/src/cli.ts +++ b/packages/lynx/create-lynx-extension/src/cli.ts @@ -2,6 +2,7 @@ // Copyright 2026 The Lynx Authors. All rights reserved. // Licensed under the Apache License Version 2.0 that can be found in the // LICENSE file in the root directory of this source tree. +import fs from 'node:fs'; import path from 'node:path'; import { stdin as input, stdout as output } from 'node:process'; import readline from 'node:readline'; @@ -277,9 +278,23 @@ export function reportCliError( process.exitCode = 1; } -function isCliEntrypoint(): boolean { - return process.argv[1] !== undefined - && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +export function isCliEntrypoint( + entrypoint: string | undefined = process.argv[1], + moduleUrl: string = import.meta.url, +): boolean { + return entrypoint !== undefined + && normalizeEntrypointPath(entrypoint) + === normalizeEntrypointPath(fileURLToPath(moduleUrl)); +} + +function normalizeEntrypointPath(filePath: string): string { + const resolvedPath = path.resolve(filePath); + + try { + return fs.realpathSync(resolvedPath); + } catch { + return resolvedPath; + } } /* v8 ignore next 5 */ diff --git a/packages/lynx/create-lynx-extension/test/cli.test.ts b/packages/lynx/create-lynx-extension/test/cli.test.ts index 1c66013781..ab43257db3 100644 --- a/packages/lynx/create-lynx-extension/test/cli.test.ts +++ b/packages/lynx/create-lynx-extension/test/cli.test.ts @@ -5,11 +5,17 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { PassThrough } from 'node:stream'; +import { pathToFileURL } from 'node:url'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { CliRuntime } from '../src/cli.js'; -import { main, parseArgs, reportCliError } from '../src/cli.js'; +import { + isCliEntrypoint, + main, + parseArgs, + reportCliError, +} from '../src/cli.js'; const tempDirs: string[] = []; @@ -136,6 +142,32 @@ describe('create-lynx-extension CLI', () => { expect(runtime.error).toHaveBeenCalledWith('string failure'); expect(process.exitCode).toBe(1); }); + + it('recognizes npm bin symlinks as executable entrypoints', () => { + const binPath = path.join('node_modules', '.bin', 'create-lynx-extension'); + const cliPath = path.join( + 'node_modules', + 'create-lynx-extension', + 'dist', + 'cli.js', + ); + const resolvedCliPath = path.resolve(cliPath); + + vi.spyOn(fs, 'realpathSync').mockImplementation( + ((filePath) => { + const resolvedPath = path.resolve(String(filePath)); + + if (resolvedPath === path.resolve(binPath)) { + return resolvedCliPath; + } + + return resolvedPath; + }) as typeof fs.realpathSync, + ); + + expect(isCliEntrypoint(binPath, pathToFileURL(resolvedCliPath).href)) + .toBe(true); + }); }); function createRuntime(options: { isTTY?: boolean } = {}): CliRuntime {