Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: autocomplete (rebase) #4327

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 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
1 change: 1 addition & 0 deletions packages/webpack-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"fastest-levenshtein": "^1.0.12",
"import-local": "^3.0.2",
"interpret": "^3.1.1",
"omelette": "^0.4.17",
"rechoir": "^0.8.0",
"webpack-merge": "^5.7.3"
},
Expand Down
7 changes: 7 additions & 0 deletions packages/webpack-cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import type { ClientConfiguration, Configuration as DevServerConfig } from "webp
import { type Colorette } from "colorette";
import { type Command, type CommandOptions, type Option, type ParseOptions } from "commander";
import { type prepare } from "rechoir";

import { type stringifyStream } from "@discoveryjs/json-ext";
import { IAutocompleteTree } from "./utils/autocomplete";

import { type stringifyChunked } from "@discoveryjs/json-ext";

/**
Expand Down Expand Up @@ -74,6 +78,9 @@ interface IWebpackCLI {
): Promise<WebpackCompiler>;
needWatchStdin(compiler: Compiler | MultiCompiler): boolean;
runWebpack(options: WebpackRunOptions, isWatchCommand: boolean): Promise<void>;
getAutocompleteTree(): IAutocompleteTree;
executeAutoComplete(): Promise<void>;
setupAutocompleteForShell(): Promise<void>;
}

interface WebpackCLIColors extends Colorette {
Expand Down
152 changes: 152 additions & 0 deletions packages/webpack-cli/src/utils/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import * as Fs from "fs";

// eslint-disable-next-line @typescript-eslint/no-var-requires
Copy link
Member

Choose a reason for hiding this comment

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

needs to be removed

Copy link
Author

Choose a reason for hiding this comment

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

On it.

const omelette = require("omelette");

export const appNameOnAutocomplete = "webpack-cli";

function getAutoCompleteObject() {
return omelette(appNameOnAutocomplete);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
HyperDanisH marked this conversation as resolved.
Show resolved Hide resolved
export function setupAutoCompleteForShell(path?: string, shell?: string): any {
const autoCompleteObject = getAutoCompleteObject();
let initFile = path;

if (shell) {
autoCompleteObject.shell = shell;
} else {
autoCompleteObject.shell = autoCompleteObject.getActiveShell();
}

if (!initFile) {
initFile = autoCompleteObject.getDefaultShellInitFile();
}

let initFileContent;

try {
initFileContent = Fs.readFileSync(initFile as string, { encoding: "utf-8" });
} catch (exception) {
throw `Can't read init file (${initFile}): ${exception}`;
}

try {
// For bash we need to enable bash_completion before webpack cli completion
if (
autoCompleteObject.shell === "bash" &&
initFileContent.indexOf("begin bash_completion configuration") === -1
) {
const sources = `[ -f /usr/local/etc/bash_completion ] && . /usr/local/etc/bash_completion
[ -f /usr/share/bash-completion/bash_completion ] && . /usr/share/bash-completion/bash_completion
[ -f /etc/bash_completion ] && . /etc/bash_completion`;

const template = `
# begin bash_completion configuration for ${appNameOnAutocomplete} completion
${sources}
# end bash_completion configuration for ${appNameOnAutocomplete} completion
`;

Fs.appendFileSync(initFile as string, template);
}

if (initFileContent.indexOf(`begin ${appNameOnAutocomplete} completion`) === -1) {
autoCompleteObject.setupShellInitFile(initFile);
}
} catch (exception) {
throw `Can't setup autocomplete. Please make sure that init file (${initFile}) exist and you have write permissions: ${exception}`;
}
}

export function getReplyHandler(
lineEndsWithWhitespaceChar: boolean,
): (args: string[], autocompleteTree: IAutocompleteTree) => string[] {
return function getReply(args: string[], autocompleteTree: IAutocompleteTree): string[] {
const currentArg = head(args);
const commandsAndCategories = Object.keys(autocompleteTree);

if (currentArg === undefined) {
// no more args - show all of the items at the current level
return commandsAndCategories;
} else {
// check what arg points to
const entity = autocompleteTree[currentArg];
if (entity) {
// arg points to an existing command or category
const restOfArgs = tail(args);
if (restOfArgs.length || lineEndsWithWhitespaceChar) {
if (entity instanceof Array) {
// it is command
const getCommandReply = getCommandReplyHandler(lineEndsWithWhitespaceChar);
return getCommandReply(restOfArgs, entity);
} else {
// it is category
return getReply(restOfArgs, entity);
}
} else {
// if last arg has no trailing whitespace, it should be added
return [currentArg];
}
} else {
// arg points to nothing specific - return commands and categories which start with arg
return commandsAndCategories.filter((commandOrCategory) =>
commandOrCategory.startsWith(currentArg),
);
}
}
};
}

function getCommandReplyHandler(
lineEndsWithWhitespaceChar: boolean,
): (args: string[], optionNames: IOptionNames[]) => string[] {
return function getCommandReply(args: string[], optionsNames: IOptionNames[]): string[] {
const currentArg = head(args);
if (currentArg === undefined) {
// no more args, returning remaining optionsNames
return optionsNames.map((option) => (option.long as string) || (option.short as string));
} else {
const restOfArgs = tail(args);
if (restOfArgs.length || lineEndsWithWhitespaceChar) {
const filteredOptions = optionsNames.filter(
(option) => option.long !== currentArg && option.short !== currentArg,
);
return getCommandReply(restOfArgs, filteredOptions);
} else {
const candidates: string[] = [];
for (const option of optionsNames) {
if (option.long && option.long.startsWith(currentArg)) {
candidates.push(option.long);
} else if (option.short && option.short.startsWith(currentArg)) {
candidates.push(option.short);
}
}
return candidates;
}
}
};
}

interface IOptionNames {
short?: string;
long?: string;
}

export interface IAutocompleteTree {
[entity: string]: IAutocompleteTree | IOptionNames[];
}

// utility functions (to avoid loading lodash for performance reasons)

function last(line: string): string {
return line.substr(-1, 1);
}

function head<T>(array: T[]): T {
return array[0];
}

function tail<T>(array: T[]): T[] {
return array.slice(1);
}
129 changes: 129 additions & 0 deletions packages/webpack-cli/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
CommandAction,
WebpackCLIBuiltInOption,
WebpackCLICommand,
WebpackCLICommandOptions,
WebpackCLIExternalCommandInfo,
WebpackCLIOptions,
} from "../types";
import { IAutocompleteTree } from "./autocomplete";

const WEBPACK_PACKAGE_IS_CUSTOM = !!process.env.WEBPACK_PACKAGE;
const WEBPACK_PACKAGE = WEBPACK_PACKAGE_IS_CUSTOM
? (process.env.WEBPACK_PACKAGE as string)
: "webpack";

export const getKnownCommands = (): WebpackCLIOptions[] => {
// Built-in internal commands
const buildCommandOptions = {
name: "build [entries...]",
alias: ["bundle", "b"],
description: "Run webpack (default command, can be omitted).",
usage: "[entries...] [options]",
dependencies: [WEBPACK_PACKAGE],
};
const watchCommandOptions = {
name: "watch [entries...]",
alias: "w",
description: "Run webpack and watch for files changes.",
usage: "[entries...] [options]",
dependencies: [WEBPACK_PACKAGE],
};
const versionCommandOptions = {
name: "version",
alias: "v",
usage: "[options]",
description:
"Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.",
};
const setupAutocompleteCommandOptions = {
name: "setup-autocomplete",
alias: "a",
usage: "[options]",
description: "Setup tab completion for your shell",
};
const helpCommandOptions = {
name: "help [command] [option]",
alias: "h",
description: "Display help for commands and options.",
};
// Built-in external commands
const externalBuiltInCommandsInfo: WebpackCLIExternalCommandInfo[] = [
{
name: "serve [entries...]",
alias: ["server", "s"],
pkg: "@webpack-cli/serve",
},
{
name: "info",
alias: "i",
pkg: "@webpack-cli/info",
},
{
name: "init",
alias: ["create", "new", "c", "n"],
pkg: "@webpack-cli/generators",
},
{
name: "loader",
alias: "l",
pkg: "@webpack-cli/generators",
},
{
name: "plugin",
alias: "p",
pkg: "@webpack-cli/generators",
},
{
name: "configtest [config-path]",
alias: "t",
pkg: "@webpack-cli/configtest",
},
];

const knownCommands = [
buildCommandOptions,
watchCommandOptions,
versionCommandOptions,
helpCommandOptions,
setupAutocompleteCommandOptions,
...externalBuiltInCommandsInfo,
];

return knownCommands;
};

export const getExternalBuiltInCommandsInfo = (): WebpackCLIExternalCommandInfo[] => {
return [
{
name: "serve [entries...]",
alias: ["server", "s"],
pkg: "@webpack-cli/serve",
},
{
name: "info",
alias: "i",
pkg: "@webpack-cli/info",
},
{
name: "init",
alias: ["create", "new", "c", "n"],
pkg: "@webpack-cli/generators",
},
{
name: "loader",
alias: "l",
pkg: "@webpack-cli/generators",
},
{
name: "plugin",
alias: "p",
pkg: "@webpack-cli/generators",
},
{
name: "configtest [config-path]",
alias: "t",
pkg: "@webpack-cli/configtest",
},
];
};
Loading