fix: keep only source-map extracted node_modules, exclude pnpm artifacts

Restore node_modules to exact state from cli.js.map extraction:
- Move .ignored/ and .ignored_* files back to original package paths
- Remove pnpm symlinks (replaced by real source-map directories)
- Update .gitignore: only exclude /dist/, .pnpm/, .bin/, .ignored* dirs
- All package dist/ folders are now preserved as part of source-map files

After clone, run `pnpm install` to get pnpm-managed symlinks for building.
The committed node_modules contains the original TypeScript/JS source files
as bundled in cli.js.
This commit is contained in:
janlaywss
2026-03-31 21:09:32 +08:00
parent 0d5abe8837
commit a192e187e8
2211 changed files with 329136 additions and 84 deletions

View File

@@ -0,0 +1,12 @@
export * from './lib/key.mjs';
export * from './lib/errors.mjs';
export { usePrefix } from './lib/use-prefix.mjs';
export { useState } from './lib/use-state.mjs';
export { useEffect } from './lib/use-effect.mjs';
export { useMemo } from './lib/use-memo.mjs';
export { useRef } from './lib/use-ref.mjs';
export { useKeypress } from './lib/use-keypress.mjs';
export { makeTheme } from './lib/make-theme.mjs';
export { usePagination } from './lib/pagination/use-pagination.mjs';
export { createPrompt } from './lib/create-prompt.mjs';
export { Separator } from './lib/Separator.mjs';

View File

@@ -0,0 +1,21 @@
import colors from 'yoctocolors-cjs';
import figures from '@inquirer/figures';
/**
* Separator object
* Used to space/separate choices group
*/
export class Separator {
separator = colors.dim(Array.from({ length: 15 }).join(figures.line));
type = 'separator';
constructor(separator) {
if (separator) {
this.separator = separator;
}
}
static isSeparator(choice) {
return Boolean(choice &&
typeof choice === 'object' &&
'type' in choice &&
choice.type === 'separator');
}
}

View File

@@ -0,0 +1,84 @@
import * as readline from 'node:readline';
import { AsyncResource } from 'node:async_hooks';
import MuteStream from 'mute-stream';
import { onExit as onSignalExit } from 'signal-exit';
import ScreenManager from './screen-manager.mjs';
import { PromisePolyfill } from './promise-polyfill.mjs';
import { withHooks, effectScheduler } from './hook-engine.mjs';
import { AbortPromptError, CancelPromptError, ExitPromptError } from './errors.mjs';
export function createPrompt(view) {
const prompt = (config, context = {}) => {
// Default `input` to stdin
const { input = process.stdin, signal } = context;
const cleanups = new Set();
// Add mute capabilities to the output
const output = new MuteStream();
output.pipe(context.output ?? process.stdout);
const rl = readline.createInterface({
terminal: true,
input,
output,
});
const screen = new ScreenManager(rl);
const { promise, resolve, reject } = PromisePolyfill.withResolver();
/** @deprecated pass an AbortSignal in the context options instead. See {@link https://github.com/SBoudrias/Inquirer.js#canceling-prompt} */
const cancel = () => reject(new CancelPromptError());
if (signal) {
const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
if (signal.aborted) {
abort();
return Object.assign(promise, { cancel });
}
signal.addEventListener('abort', abort);
cleanups.add(() => signal.removeEventListener('abort', abort));
}
cleanups.add(onSignalExit((code, signal) => {
reject(new ExitPromptError(`User force closed the prompt with ${code} ${signal}`));
}));
// Re-renders only happen when the state change; but the readline cursor could change position
// and that also requires a re-render (and a manual one because we mute the streams).
// We set the listener after the initial workLoop to avoid a double render if render triggered
// by a state change sets the cursor to the right position.
const checkCursorPos = () => screen.checkCursorPos();
rl.input.on('keypress', checkCursorPos);
cleanups.add(() => rl.input.removeListener('keypress', checkCursorPos));
return withHooks(rl, (cycle) => {
// The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
// triggers after the process is done (which happens after timeouts are done triggering.)
// We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
const hooksCleanup = AsyncResource.bind(() => effectScheduler.clearAll());
rl.on('close', hooksCleanup);
cleanups.add(() => rl.removeListener('close', hooksCleanup));
cycle(() => {
try {
const nextView = view(config, (value) => {
setImmediate(() => resolve(value));
});
const [content, bottomContent] = typeof nextView === 'string' ? [nextView] : nextView;
screen.render(content, bottomContent);
effectScheduler.run();
}
catch (error) {
reject(error);
}
});
return Object.assign(promise
.then((answer) => {
effectScheduler.clearAll();
return answer;
}, (error) => {
effectScheduler.clearAll();
throw error;
})
// Wait for the promise to settle, then cleanup.
.finally(() => {
cleanups.forEach((cleanup) => cleanup());
screen.done({ clearContent: Boolean(context?.clearPromptOnDone) });
output.end();
})
// Once cleanup is done, let the expose promise resolve/reject to the internal one.
.then(() => promise), { cancel });
});
};
return prompt;
}

View File

@@ -0,0 +1,21 @@
export class AbortPromptError extends Error {
name = 'AbortPromptError';
message = 'Prompt was aborted';
constructor(options) {
super();
this.cause = options?.cause;
}
}
export class CancelPromptError extends Error {
name = 'CancelPromptError';
message = 'Prompt was canceled';
}
export class ExitPromptError extends Error {
name = 'ExitPromptError';
}
export class HookError extends Error {
name = 'HookError';
}
export class ValidationError extends Error {
name = 'ValidationError';
}

View File

@@ -0,0 +1,110 @@
/* eslint @typescript-eslint/no-explicit-any: ["off"] */
import { AsyncLocalStorage, AsyncResource } from 'node:async_hooks';
import { HookError, ValidationError } from './errors.mjs';
const hookStorage = new AsyncLocalStorage();
function createStore(rl) {
const store = {
rl,
hooks: [],
hooksCleanup: [],
hooksEffect: [],
index: 0,
handleChange() { },
};
return store;
}
// Run callback in with the hook engine setup.
export function withHooks(rl, cb) {
const store = createStore(rl);
return hookStorage.run(store, () => {
function cycle(render) {
store.handleChange = () => {
store.index = 0;
render();
};
store.handleChange();
}
return cb(cycle);
});
}
// Safe getStore utility that'll return the store or throw if undefined.
function getStore() {
const store = hookStorage.getStore();
if (!store) {
throw new HookError('[Inquirer] Hook functions can only be called from within a prompt');
}
return store;
}
export function readline() {
return getStore().rl;
}
// Merge state updates happening within the callback function to avoid multiple renders.
export function withUpdates(fn) {
const wrapped = (...args) => {
const store = getStore();
let shouldUpdate = false;
const oldHandleChange = store.handleChange;
store.handleChange = () => {
shouldUpdate = true;
};
const returnValue = fn(...args);
if (shouldUpdate) {
oldHandleChange();
}
store.handleChange = oldHandleChange;
return returnValue;
};
return AsyncResource.bind(wrapped);
}
export function withPointer(cb) {
const store = getStore();
const { index } = store;
const pointer = {
get() {
return store.hooks[index];
},
set(value) {
store.hooks[index] = value;
},
initialized: index in store.hooks,
};
const returnValue = cb(pointer);
store.index++;
return returnValue;
}
export function handleChange() {
getStore().handleChange();
}
export const effectScheduler = {
queue(cb) {
const store = getStore();
const { index } = store;
store.hooksEffect.push(() => {
store.hooksCleanup[index]?.();
const cleanFn = cb(readline());
if (cleanFn != null && typeof cleanFn !== 'function') {
throw new ValidationError('useEffect return value must be a cleanup function or nothing.');
}
store.hooksCleanup[index] = cleanFn;
});
},
run() {
const store = getStore();
withUpdates(() => {
store.hooksEffect.forEach((effect) => {
effect();
});
// Warning: Clean the hooks before exiting the `withUpdates` block.
// Failure to do so means an updates would hit the same effects again.
store.hooksEffect.length = 0;
})();
},
clearAll() {
const store = getStore();
store.hooksCleanup.forEach((cleanFn) => {
cleanFn?.();
});
store.hooksEffect.length = 0;
store.hooksCleanup.length = 0;
},
};

View File

@@ -0,0 +1,18 @@
export const isUpKey = (key) =>
// The up key
key.name === 'up' ||
// Vim keybinding
key.name === 'k' ||
// Emacs keybinding
(key.ctrl && key.name === 'p');
export const isDownKey = (key) =>
// The down key
key.name === 'down' ||
// Vim keybinding
key.name === 'j' ||
// Emacs keybinding
(key.ctrl && key.name === 'n');
export const isSpaceKey = (key) => key.name === 'space';
export const isBackspaceKey = (key) => key.name === 'backspace';
export const isNumberKey = (key) => '123456789'.includes(key.name);
export const isEnterKey = (key) => key.name === 'enter' || key.name === 'return';

View File

@@ -0,0 +1,30 @@
import { defaultTheme } from './theme.mjs';
function isPlainObject(value) {
if (typeof value !== 'object' || value === null)
return false;
let proto = value;
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return Object.getPrototypeOf(value) === proto;
}
function deepMerge(...objects) {
const output = {};
for (const obj of objects) {
for (const [key, value] of Object.entries(obj)) {
const prevValue = output[key];
output[key] =
isPlainObject(prevValue) && isPlainObject(value)
? deepMerge(prevValue, value)
: value;
}
}
return output;
}
export function makeTheme(...themes) {
const themesToMerge = [
defaultTheme,
...themes.filter((theme) => theme != null),
];
return deepMerge(...themesToMerge);
}

View File

@@ -0,0 +1,59 @@
import { breakLines } from '../utils.mjs';
function split(content, width) {
return breakLines(content, width).split('\n');
}
/**
* Rotates an array of items by an integer number of positions.
* @param {number} count The number of positions to rotate by
* @param {T[]} items The items to rotate
*/
function rotate(count, items) {
const max = items.length;
const offset = ((count % max) + max) % max;
return [...items.slice(offset), ...items.slice(0, offset)];
}
/**
* Renders a page of items as lines that fit within the given width ensuring
* that the number of lines is not greater than the page size, and the active
* item renders at the provided position, while prioritizing that as many lines
* of the active item get rendered as possible.
*/
export function lines({ items, width, renderItem, active, position: requested, pageSize, }) {
const layouts = items.map((item, index) => ({
item,
index,
isActive: index === active,
}));
const layoutsInPage = rotate(active - requested, layouts).slice(0, pageSize);
const renderItemAt = (index) => layoutsInPage[index] == null ? [] : split(renderItem(layoutsInPage[index]), width);
// Create a blank array of lines for the page
const pageBuffer = Array.from({ length: pageSize });
// Render the active item to decide the position
const activeItem = renderItemAt(requested).slice(0, pageSize);
const position = requested + activeItem.length <= pageSize ? requested : pageSize - activeItem.length;
// Add the lines of the active item into the page
pageBuffer.splice(position, activeItem.length, ...activeItem);
// Fill the page under the active item
let bufferPointer = position + activeItem.length;
let layoutPointer = requested + 1;
while (bufferPointer < pageSize && layoutPointer < layoutsInPage.length) {
for (const line of renderItemAt(layoutPointer)) {
pageBuffer[bufferPointer++] = line;
if (bufferPointer >= pageSize)
break;
}
layoutPointer++;
}
// Fill the page over the active item
bufferPointer = position - 1;
layoutPointer = requested - 1;
while (bufferPointer >= 0 && layoutPointer >= 0) {
for (const line of renderItemAt(layoutPointer).reverse()) {
pageBuffer[bufferPointer--] = line;
if (bufferPointer < 0)
break;
}
layoutPointer--;
}
return pageBuffer.filter((line) => typeof line === 'string');
}

View File

@@ -0,0 +1,27 @@
/**
* Creates the next position for the active item considering a finite list of
* items to be rendered on a page.
*/
export function finite({ active, pageSize, total, }) {
const middle = Math.floor(pageSize / 2);
if (total <= pageSize || active < middle)
return active;
if (active >= total - middle)
return active + pageSize - total;
return middle;
}
/**
* Creates the next position for the active item considering an infinitely
* looping list of items to be rendered on the page.
*/
export function infinite({ active, lastActive, total, pageSize, pointer, }) {
if (total <= pageSize)
return active;
// Move the position only when the user moves down, and when the
// navigation fits within a single page
if (lastActive < active && active - lastActive < pageSize) {
// Limit it to the middle of the list
return Math.min(Math.floor(pageSize / 2), pointer + active - lastActive);
}
return pointer;
}

View File

@@ -0,0 +1,30 @@
import { useRef } from '../use-ref.mjs';
import { readlineWidth } from '../utils.mjs';
import { lines } from './lines.mjs';
import { finite, infinite } from './position.mjs';
export function usePagination({ items, active, renderItem, pageSize, loop = true, }) {
const state = useRef({ position: 0, lastActive: 0 });
const position = loop
? infinite({
active,
lastActive: state.current.lastActive,
total: items.length,
pageSize,
pointer: state.current.position,
})
: finite({
active,
total: items.length,
pageSize,
});
state.current.position = position;
state.current.lastActive = active;
return lines({
items,
width: readlineWidth(),
renderItem,
active,
position,
pageSize,
}).join('\n');
}

View File

@@ -0,0 +1,14 @@
// TODO: Remove this class once Node 22 becomes the minimum supported version.
export class PromisePolyfill extends Promise {
// Available starting from Node 22
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
static withResolver() {
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve: resolve, reject: reject };
}
}

View File

@@ -0,0 +1,85 @@
import stripAnsi from 'strip-ansi';
import ansiEscapes from 'ansi-escapes';
import { breakLines, readlineWidth } from './utils.mjs';
const height = (content) => content.split('\n').length;
const lastLine = (content) => content.split('\n').pop() ?? '';
function cursorDown(n) {
return n > 0 ? ansiEscapes.cursorDown(n) : '';
}
export default class ScreenManager {
rl;
// These variables are keeping information to allow correct prompt re-rendering
height = 0;
extraLinesUnderPrompt = 0;
cursorPos;
constructor(rl) {
this.rl = rl;
this.rl = rl;
this.cursorPos = rl.getCursorPos();
}
write(content) {
this.rl.output.unmute();
this.rl.output.write(content);
this.rl.output.mute();
}
render(content, bottomContent = '') {
// Write message to screen and setPrompt to control backspace
const promptLine = lastLine(content);
const rawPromptLine = stripAnsi(promptLine);
// Remove the rl.line from our prompt. We can't rely on the content of
// rl.line (mainly because of the password prompt), so just rely on it's
// length.
let prompt = rawPromptLine;
if (this.rl.line.length > 0) {
prompt = prompt.slice(0, -this.rl.line.length);
}
this.rl.setPrompt(prompt);
// SetPrompt will change cursor position, now we can get correct value
this.cursorPos = this.rl.getCursorPos();
const width = readlineWidth();
content = breakLines(content, width);
bottomContent = breakLines(bottomContent, width);
// Manually insert an extra line if we're at the end of the line.
// This prevent the cursor from appearing at the beginning of the
// current line.
if (rawPromptLine.length % width === 0) {
content += '\n';
}
let output = content + (bottomContent ? '\n' + bottomContent : '');
/**
* Re-adjust the cursor at the correct position.
*/
// We need to consider parts of the prompt under the cursor as part of the bottom
// content in order to correctly cleanup and re-render.
const promptLineUpDiff = Math.floor(rawPromptLine.length / width) - this.cursorPos.rows;
const bottomContentHeight = promptLineUpDiff + (bottomContent ? height(bottomContent) : 0);
// Return cursor to the input position (on top of the bottomContent)
if (bottomContentHeight > 0)
output += ansiEscapes.cursorUp(bottomContentHeight);
// Return cursor to the initial left offset.
output += ansiEscapes.cursorTo(this.cursorPos.cols);
/**
* Render and store state for future re-rendering
*/
this.write(cursorDown(this.extraLinesUnderPrompt) +
ansiEscapes.eraseLines(this.height) +
output);
this.extraLinesUnderPrompt = bottomContentHeight;
this.height = height(output);
}
checkCursorPos() {
const cursorPos = this.rl.getCursorPos();
if (cursorPos.cols !== this.cursorPos.cols) {
this.write(ansiEscapes.cursorTo(cursorPos.cols));
this.cursorPos = cursorPos;
}
}
done({ clearContent }) {
this.rl.setPrompt('');
let output = cursorDown(this.extraLinesUnderPrompt);
output += clearContent ? ansiEscapes.eraseLines(this.height) : '\n';
output += ansiEscapes.cursorShow;
this.write(output);
this.rl.close();
}
}

View File

@@ -0,0 +1,22 @@
import colors from 'yoctocolors-cjs';
import figures from '@inquirer/figures';
export const defaultTheme = {
prefix: {
idle: colors.blue('?'),
// TODO: use figure
done: colors.green(figures.tick),
},
spinner: {
interval: 80,
frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].map((frame) => colors.yellow(frame)),
},
style: {
answer: colors.cyan,
message: colors.bold,
error: (text) => colors.red(`> ${text}`),
defaultAnswer: (text) => colors.dim(`(${text})`),
help: colors.dim,
highlight: colors.cyan,
key: (text) => colors.cyan(colors.bold(`<${text}>`)),
},
};

View File

@@ -0,0 +1,11 @@
import { withPointer, effectScheduler } from './hook-engine.mjs';
export function useEffect(cb, depArray) {
withPointer((pointer) => {
const oldDeps = pointer.get();
const hasChanged = !Array.isArray(oldDeps) || depArray.some((dep, i) => !Object.is(dep, oldDeps[i]));
if (hasChanged) {
effectScheduler.queue(cb);
}
pointer.set(depArray);
});
}

View File

@@ -0,0 +1,20 @@
import { useRef } from './use-ref.mjs';
import { useEffect } from './use-effect.mjs';
import { withUpdates } from './hook-engine.mjs';
export function useKeypress(userHandler) {
const signal = useRef(userHandler);
signal.current = userHandler;
useEffect((rl) => {
let ignore = false;
const handler = withUpdates((_input, event) => {
if (ignore)
return;
void signal.current(event, rl);
});
rl.input.on('keypress', handler);
return () => {
ignore = true;
rl.input.removeListener('keypress', handler);
};
}, []);
}

View File

@@ -0,0 +1,14 @@
import { withPointer } from './hook-engine.mjs';
export function useMemo(fn, dependencies) {
return withPointer((pointer) => {
const prev = pointer.get();
if (!prev ||
prev.dependencies.length !== dependencies.length ||
prev.dependencies.some((dep, i) => dep !== dependencies[i])) {
const value = fn();
pointer.set({ value, dependencies });
return value;
}
return prev.value;
});
}

View File

@@ -0,0 +1,36 @@
import { AsyncResource } from 'node:async_hooks';
import { useState } from './use-state.mjs';
import { useEffect } from './use-effect.mjs';
import { makeTheme } from './make-theme.mjs';
export function usePrefix({ status = 'idle', theme, }) {
const [showLoader, setShowLoader] = useState(false);
const [tick, setTick] = useState(0);
const { prefix, spinner } = makeTheme(theme);
useEffect(() => {
if (status === 'loading') {
let tickInterval;
let inc = -1;
// Delay displaying spinner by 300ms, to avoid flickering
const delayTimeout = setTimeout(AsyncResource.bind(() => {
setShowLoader(true);
tickInterval = setInterval(AsyncResource.bind(() => {
inc = inc + 1;
setTick(inc % spinner.frames.length);
}), spinner.interval);
}), 300);
return () => {
clearTimeout(delayTimeout);
clearInterval(tickInterval);
};
}
else {
setShowLoader(false);
}
}, [status]);
if (showLoader) {
return spinner.frames[tick];
}
// There's a delay before we show the loader. So we want to ignore `loading` here, and pass idle instead.
const iconName = status === 'loading' ? 'idle' : status;
return typeof prefix === 'string' ? prefix : prefix[iconName];
}

View File

@@ -0,0 +1,4 @@
import { useState } from './use-state.mjs';
export function useRef(val) {
return useState({ current: val })[0];
}

View File

@@ -0,0 +1,19 @@
import { withPointer, handleChange } from './hook-engine.mjs';
export function useState(defaultValue) {
return withPointer((pointer) => {
const setFn = (newValue) => {
// Noop if the value is still the same.
if (pointer.get() !== newValue) {
pointer.set(newValue);
// Trigger re-render
handleChange();
}
};
if (pointer.initialized) {
return [pointer.get(), setFn];
}
const value = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
pointer.set(value);
return [value, setFn];
});
}

View File

@@ -0,0 +1,25 @@
import cliWidth from 'cli-width';
import wrapAnsi from 'wrap-ansi';
import { readline } from './hook-engine.mjs';
/**
* Force line returns at specific width. This function is ANSI code friendly and it'll
* ignore invisible codes during width calculation.
* @param {string} content
* @param {number} width
* @return {string}
*/
export function breakLines(content, width) {
return content
.split('\n')
.flatMap((line) => wrapAnsi(line, width, { trim: false, hard: true })
.split('\n')
.map((str) => str.trimEnd()))
.join('\n');
}
/**
* Returns the width of the active readline, or 80 as default value.
* @returns {number}
*/
export function readlineWidth() {
return cliWidth({ defaultWidth: 80, output: readline().output });
}