Skip to content

Commit

Permalink
✨ 新增了nodekit/pnpm
Browse files Browse the repository at this point in the history
初步增加了 pnpm_publish 函数用于发包
改进了async_generator的函数泛型定义,现在应用更加广泛
shell现在支持自定义stdio,实现管道输出
  • Loading branch information
Gaubee committed Oct 13, 2024
1 parent d49f162 commit 84e8c21
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 19 deletions.
147 changes: 147 additions & 0 deletions nodekit/src/pnpm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Buffer } from "node:buffer";
import { $ } from "./shell.ts";

export interface PnpmPublishOptions {
/**
* Tells the registry whether this package should
* be published as public or restricted
*/
access?: "public" | "restricted";
/**
* Does everything a publish would do except
* actually publishing to the registry
*/
dryRun?: boolean;
/**
* Packages are proceeded to be published even
* if their current version is already in the
* registry. This is useful when a
* "prepublishOnly" script bumps the version of
* the package before it is published
*/
force?: boolean;
/**
* Ignores any publish related lifecycle
* scripts (prepublishOnly, postpublish, and
* the like)
*/
ignoreScripts?: boolean;
/**
* Don't check if current branch is your
* publish branch, clean, and up to date
*/
noGitChecks?: boolean;
/**
* When publishing packages that require
* two-factor authentication, this option can
* specify a one-time password
*/
otp?: string;
/**
* Sets branch name to publish. Default is "master"
*/
publishBranch?: string;
/**
* Publish all packages from the workspace
*/
recursive?: boolean;
/**
* Save the list of the newly published
* packages to "pnpm-publish-summary.json".
* Useful when some other tooling is used to
* report the list of published packages.
*/
reportSummary?: boolean;
/**
* Registers the published package with the
* given tag. By default, the "latest" tag is
* used.
*/
tag?: string;
/// Filtering options (run the command only on packages that satisfy at least one of the selectors):

/**
* Defines files to ignore when
* filtering for changed projects
* since the specified
* commit/branch. Usage example:
* filter: "...[origin/master]"
* changedFilesIgnorePattern: "**\/README.md" build
*/
changedFilesIgnorePattern?: string;
/**
* If no projects are matched by
* the command, exit with throw `Error("code:1")` (fail)
*/
failIfNoMatch?: boolean;
/**
* - `!<selector>` If a selector starts with ! (or \! in zsh), it means the packages matching the selector must be excluded. E.g. `filter: "!foo"` selects all packages except "foo"
* - `.` Includes all packages that are under the current working directory
* - `...^<pattern>` Includes only the direct and indirect dependents of the matched packages without including the matched packages themselves. E.g.: `filter: "...^foo"`
* - `...<pattern>` Includes all direct and indirect dependents of the matched packages. E.g.: `filter: ["...foo", "...@bar/*"]`
* - `./<dir>` Includes all packages that are inside a given subdirectory. E.g.: `filter: "./components"`
* - `[<since>]` Includes all packages changed since the specified commit/branch. E.g.: `"[master]"`, `"[HEAD\~2]"`. It may be used together with `"..."`. So, for instance, `"...[HEAD\~1]"` selects all packages changed in the last commit and their dependents
* - `{<dir>}` Includes all projects that are under the specified directory. It may be used with "..." to select dependents/dependencies as well. It also may be combined with `"[<since>]"`. For instance, all changed projects inside a directory: `"{packages}[origin/master]"`
* - `<pattern>` Restricts the scope to package names matching the given pattern. E.g.: `"foo"`, `"@bar/*"`
* - `<pattern>...` Includes all direct and indirect dependencies of the matched packages. E.g.: `"foo..."`
* - `<pattern>^...` Includes only the direct and indirect dependencies of the matched packages without including the matched packages themselves. E.g.: `"foo^..."`
*/
filter?: string | string[];
/**
* Restricts the scope to package names matching the given pattern similar to {@link filter}, but it ignores devDependencies when searching for dependencies and dependents.
*/
filterProd?: string | string[];
/**
* Defines files related to tests. Useful with the changed since filter. When selecting only changed packages and their dependent packages, the dependent packages will be ignored in case a package has changes only in tests. Usage example: `{ filter:"...[origin/master]", testPattern=["test/*", "test"] }`
*/
testPattern?: string | string[];
}
/**
* Publishes a package to the npm registry.
* run command `pnpm publish`,
* Visit https://pnpm.io/9.x/cli/publish for documentation about this command.
*/
export const pnpm_publish = async (options: PnpmPublishOptions) => {
const args = ["--json"];
for (const [key, value] of Object.entries(options)) {
if (value == null) {
continue;
}
const cli_key = "--" + key.replace(/[A-Z]/g, (C) => `-${C.toLowerCase()}`);
if (value === true) {
args.push(cli_key);
} else if (typeof value === "string") {
args.push(cli_key, value);
} else if (Array.isArray(value)) {
args.push(cli_key, ...value);
}
}

const job = Promise.withResolvers<PublishResult>();
try {
await $("pnpm", args, {
stdio: async (io) => {
const chunks = [];
for await (const chunk of io.stdout) {
chunks.push(chunk);
}
const result = Buffer.concat(chunks);
job.resolve(JSON.parse(result.toString("utf8")));
},
});
} catch (e) {
job.reject(e);
}
return job.promise;
};

export type PublishResult = PublishError | PublishSuccess;
export interface PublishError {
error: {
code: string;
message: string;
};
}
export interface PublishSuccess {
[key: string]: unknown;
}
28 changes: 18 additions & 10 deletions nodekit/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@ export interface CreateShellOptions {
cwd?: string;
env?: Record<string, string>;
}
export interface CommandOptions extends Omit<child_process.CommonSpawnOptions, "stdio"> {
stdio?: (cp: CommandStdIO) => unknown;
}

type RequiredNonNullable<T> = {
readonly [P in keyof T]-?: NonNullable<T[P]>;
};
export type CommandStdIO = RequiredNonNullable<Pick<child_process.ChildProcess, "stdin" | "stderr" | "stdout">>;
export interface Shell {
(command: string, options: Deno.CommandOptions | string[] | string): Promise<void>;
(command: string, args: string[] | string, options?: CommandOptions): Promise<unknown>;
/**
* 当前所处目录
*/
Expand Down Expand Up @@ -47,17 +55,16 @@ export const $$: (options: CreateShellOptions) => Shell = (options: CreateShellO
const $ = Object.assign(
async (
command: string,
options: child_process.CommonSpawnOptions | string[] | string,
args: string[] | string,
options?: CommandOptions,
) => {
let safe_cmd = command;
let safe_args: string[] = [];
const safe_options: child_process.CommonSpawnOptions = {};
if (Array.isArray(options)) {
safe_args = options;
} else if (typeof options === "string") {
safe_args = options.trim().split(/\s+/);
} else {
Object.assign(safe_options, options);
const safe_options: child_process.CommonSpawnOptions = obj_omit(options ?? {}, "stdio");
if (Array.isArray(args)) {
safe_args = args;
} else if (typeof args === "string") {
safe_args = args.trim().split(/\s+/);
}
console.info(
colors.blue("⫸"),
Expand All @@ -74,9 +81,10 @@ export const $$: (options: CreateShellOptions) => Shell = (options: CreateShellO
}
}
const cp = child_process.spawn(safe_cmd, safe_args, {
stdio: options?.stdio ? "pipe" : "inherit",
...safe_options,
stdio: "inherit",
});
options?.stdio?.(cp as CommandStdIO);
const job = Promise.withResolvers<void>();
cp.addListener("exit", (code, signal) => {
if (code != null && code != 0) {
Expand Down
2 changes: 1 addition & 1 deletion util/src/generator.global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ extendsMethod(AGF.prototype, "done", done);
extendsMethod(AGF.prototype, "then", then);

declare global {
interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> extends AsyncIterator<T, TReturn, TNext> {
interface AsyncGenerator<T = unknown, TReturn = any, TNext = any> extends AsyncIteratorObject<T, TReturn, TNext> {
done: typeof done;
then: typeof then;
}
Expand Down
61 changes: 53 additions & 8 deletions util/src/generator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { arr_set_next } from "./collections.ts";
import type { PromiseMaybe } from "./promise.ts";
import { str_reverse } from "./string.ts";

/**
Expand Down Expand Up @@ -26,11 +28,11 @@ export const AGF = (() => {
}
})() as AsyncGeneratorFunction;

type AGValue<T> = T extends AsyncGenerator<infer V, unknown, unknown> ? V
type AGValue<T> = T extends AsyncIterableIterator<infer V, unknown, unknown> ? V
: unknown;
type AGReturn<T> = T extends AsyncGenerator<unknown, infer R, unknown> ? R
type AGReturn<T> = T extends AsyncIterableIterator<unknown, infer R, unknown> ? R
: unknown;
type AGNext<T> = T extends AsyncGenerator<unknown, unknown, infer N> ? N
type AGNext<T> = T extends AsyncIterableIterator<unknown, unknown, infer N> ? N
: unknown;

/**
Expand All @@ -43,22 +45,65 @@ type AGNext<T> = T extends AsyncGenerator<unknown, unknown, infer N> ? N
* })());
* ```
*/
export const ag_done = async <T extends AsyncGenerator>(
export const ag_done = async <T extends AsyncIterableIterator<any>>(
ag: T,
each?: (value: AGValue<T>) => AGNext<T>,
each: AsyncGeneratorDoneOptions<T> = {},
): Promise<AGReturn<T>> => {
let next: any;
while (true) {
const item = await ag.next(next);
if (item.done) {
return item.value as AGReturn<T>;
}
if (each) {
next = each(item.value as AGValue<T>);
if (each.next) {
next = await each.next(item.value as AGValue<T>);
}
}
};

export interface AsyncGeneratorDoneOptions<T extends AsyncIterableIterator<any>> {
next?: (value: AGValue<T>) => PromiseMaybe<AGNext<T>>;
}

export const ag_map = async <T extends AsyncIterableIterator<any>, R>(
ag: T,
each: AsyncGeneratorMapOptions<T, R>,
): Promise<R[]> => {
const result: R[] = [];
await ag_done(ag, {
next: async (value) => {
arr_set_next(result, await each.map(value));
return each.next?.(value) as any;
},
});
return result;
};
export interface AsyncGeneratorMapOptions<T extends AsyncIterableIterator<any>, R> extends AsyncGeneratorDoneOptions<T> {
map: (value: AGValue<T>) => PromiseMaybe<R>;
}

export const ag_map_reduce = async <T extends AsyncIterableIterator<any>, M, R>(
ag: T,
each: AsyncGeneratorMapReduceOptions<T, M, R>,
): Promise<R> => {
let result = each.initialValue;
await ag_done(ag, {
next: async (value) => {
const mapped = await each.map(value);
for (const item of mapped) {
result = await each.reduce(result, item);
}
return each.next?.(value) as any;
},
});
return result;
};
export interface AsyncGeneratorMapReduceOptions<T extends AsyncIterableIterator<any>, M, R> extends AsyncGeneratorDoneOptions<T> {
map: (value: AGValue<T>) => PromiseMaybe<Iterable<M>>;
reduce: (previousValue: R, currentValue: M) => PromiseMaybe<R>;
initialValue: R;
}

/**
* 持续迭代一个异步迭代器直到结束,接受 promise.then 的参数
*
Expand All @@ -74,7 +119,7 @@ export const ag_done = async <T extends AsyncGenerator>(
* ```
*/
export const ag_then = <
T extends AsyncGenerator,
T extends AsyncIterableIterator<any>,
ARGS extends Parameters<Promise<AGReturn<T>>["then"]> = Parameters<
Promise<AGReturn<T>>["then"]
>,
Expand Down
2 changes: 2 additions & 0 deletions util/src/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ export const promise_try = async <R>(fn: () => R): Promise<PromiseSettledResult<
return { status: "rejected", reason };
}
};

export type PromiseMaybe<T> = PromiseLike<Awaited<T>> | Awaited<T>;

0 comments on commit 84e8c21

Please sign in to comment.