@williamthorsen/preflight 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/preflight.js +7 -0
- package/dist/esm/.cache +1 -0
- package/dist/esm/bin/preflight.d.ts +1 -0
- package/dist/esm/bin/preflight.js +12 -0
- package/dist/esm/bin/route.d.ts +1 -0
- package/dist/esm/bin/route.js +85 -0
- package/dist/esm/cli.d.ts +11 -0
- package/dist/esm/cli.js +79 -0
- package/dist/esm/config.d.ts +6 -0
- package/dist/esm/config.js +66 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/init/initCommand.d.ts +6 -0
- package/dist/esm/init/initCommand.js +29 -0
- package/dist/esm/init/scaffold.d.ts +6 -0
- package/dist/esm/init/scaffold.js +63 -0
- package/dist/esm/init/templates.d.ts +1 -0
- package/dist/esm/init/templates.js +26 -0
- package/dist/esm/lib/terminal.d.ts +4 -0
- package/dist/esm/lib/terminal.js +19 -0
- package/dist/esm/reportPreflight.d.ts +2 -0
- package/dist/esm/reportPreflight.js +53 -0
- package/dist/esm/runPreflight.d.ts +2 -0
- package/dist/esm/runPreflight.js +73 -0
- package/dist/esm/types.d.ts +38 -0
- package/dist/esm/types.js +6 -0
- package/package.json +46 -0
package/bin/preflight.js
ADDED
package/dist/esm/.cache
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fa0798ecd9314d7615561bcc9e5f533bf154ce5280b52c4e8bfc6faf00ae5380
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { routeCommand } from "./route.js";
|
|
3
|
+
let exitCode;
|
|
4
|
+
try {
|
|
5
|
+
exitCode = await routeCommand(process.argv.slice(2));
|
|
6
|
+
} catch (error) {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
process.stderr.write(`preflight: unexpected error: ${message}
|
|
9
|
+
`);
|
|
10
|
+
exitCode = 1;
|
|
11
|
+
}
|
|
12
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function routeCommand(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { parseRunArgs, runCommand } from "../cli.js";
|
|
3
|
+
import { initCommand } from "../init/initCommand.js";
|
|
4
|
+
function showHelp() {
|
|
5
|
+
console.info(`
|
|
6
|
+
Usage: preflight <command> [options]
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
run [names...] Run preflight checklists
|
|
10
|
+
init Scaffold a starter config file
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--help, -h Show this help message
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
function showRunHelp() {
|
|
17
|
+
console.info(`
|
|
18
|
+
Usage: preflight run [names...] [options]
|
|
19
|
+
|
|
20
|
+
Run preflight checklists. If no names are given, all checklists are run.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--config, -c <path> Path to the config file
|
|
24
|
+
--help, -h Show this help message
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
function showInitHelp() {
|
|
28
|
+
console.info(`
|
|
29
|
+
Usage: preflight init [options]
|
|
30
|
+
|
|
31
|
+
Scaffold a starter .config/preflight.config.ts file.
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--dry-run Preview changes without writing files
|
|
35
|
+
--force Overwrite an existing config file
|
|
36
|
+
--help, -h Show this help message
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
async function routeCommand(args) {
|
|
40
|
+
const command = args[0];
|
|
41
|
+
const flags = args.slice(1);
|
|
42
|
+
if (command === "--help" || command === "-h" || command === void 0) {
|
|
43
|
+
showHelp();
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
if (command === "run") {
|
|
47
|
+
if (flags.some((f) => f === "--help" || f === "-h")) {
|
|
48
|
+
showRunHelp();
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = parseRunArgs(flags);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
process.stderr.write(`Error: ${message}
|
|
57
|
+
`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
return runCommand(parsed);
|
|
61
|
+
}
|
|
62
|
+
if (command === "init") {
|
|
63
|
+
if (flags.some((f) => f === "--help" || f === "-h")) {
|
|
64
|
+
showInitHelp();
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const knownInitFlags = /* @__PURE__ */ new Set(["--dry-run", "--force", "--help", "-h"]);
|
|
68
|
+
const unknownFlags = flags.filter((f) => !knownInitFlags.has(f));
|
|
69
|
+
if (unknownFlags.length > 0) {
|
|
70
|
+
process.stderr.write(`Error: Unknown option: ${unknownFlags[0]}
|
|
71
|
+
`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const dryRun = flags.includes("--dry-run");
|
|
75
|
+
const force = flags.includes("--force");
|
|
76
|
+
return initCommand({ dryRun, force });
|
|
77
|
+
}
|
|
78
|
+
process.stderr.write(`Error: Unknown command: ${command}
|
|
79
|
+
`);
|
|
80
|
+
showHelp();
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
routeCommand
|
|
85
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface ParsedRunArgs {
|
|
2
|
+
configPath?: string;
|
|
3
|
+
names: string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function parseRunArgs(flags: string[]): ParsedRunArgs;
|
|
6
|
+
interface RunCommandOptions {
|
|
7
|
+
names: string[];
|
|
8
|
+
configPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function runCommand({ names, configPath }: RunCommandOptions): Promise<number>;
|
|
11
|
+
export {};
|
package/dist/esm/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { loadPreflightConfig } from "./config.js";
|
|
3
|
+
import { reportPreflight } from "./reportPreflight.js";
|
|
4
|
+
import { runPreflight } from "./runPreflight.js";
|
|
5
|
+
function parseRunArgs(flags) {
|
|
6
|
+
const result = { names: [] };
|
|
7
|
+
for (let i = 0; i < flags.length; i++) {
|
|
8
|
+
const arg = flags[i];
|
|
9
|
+
if (arg === void 0) break;
|
|
10
|
+
if (arg === "--config" || arg === "-c") {
|
|
11
|
+
i++;
|
|
12
|
+
const configValue = flags[i];
|
|
13
|
+
if (configValue === void 0) {
|
|
14
|
+
throw new Error("--config requires a path argument");
|
|
15
|
+
}
|
|
16
|
+
result.configPath = configValue;
|
|
17
|
+
} else if (arg.startsWith("--config=")) {
|
|
18
|
+
const configValue = arg.slice("--config=".length);
|
|
19
|
+
if (configValue === "") {
|
|
20
|
+
throw new Error("--config requires a path argument");
|
|
21
|
+
}
|
|
22
|
+
result.configPath = configValue;
|
|
23
|
+
} else if (arg.startsWith("-")) {
|
|
24
|
+
throw new Error(`unknown flag '${arg}'`);
|
|
25
|
+
} else {
|
|
26
|
+
result.names.push(arg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function resolveFixLocation(checklist, configDefault) {
|
|
32
|
+
return checklist.fixLocation ?? configDefault ?? "END";
|
|
33
|
+
}
|
|
34
|
+
async function runCommand({ names, configPath }) {
|
|
35
|
+
let config;
|
|
36
|
+
try {
|
|
37
|
+
config = await loadPreflightConfig(configPath);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
process.stderr.write(`Error: ${message}
|
|
41
|
+
`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
let checklists = config.checklists;
|
|
45
|
+
if (names.length > 0) {
|
|
46
|
+
const availableNames = new Set(config.checklists.map((c) => c.name));
|
|
47
|
+
const unknownNames = names.filter((n) => !availableNames.has(n));
|
|
48
|
+
if (unknownNames.length > 0) {
|
|
49
|
+
const available = [...availableNames].join(", ");
|
|
50
|
+
process.stderr.write(`Error: unknown checklist(s): ${unknownNames.join(", ")}. Available: ${available}
|
|
51
|
+
`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
const requestedNames = new Set(names);
|
|
55
|
+
checklists = config.checklists.filter((c) => requestedNames.has(c.name));
|
|
56
|
+
}
|
|
57
|
+
const showHeader = checklists.length > 1;
|
|
58
|
+
let allPassed = true;
|
|
59
|
+
for (const checklist of checklists) {
|
|
60
|
+
if (showHeader) {
|
|
61
|
+
process.stdout.write(`
|
|
62
|
+
--- ${checklist.name} ---
|
|
63
|
+
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
const report = await runPreflight(checklist);
|
|
67
|
+
const fixLocation = resolveFixLocation(checklist, config.fixLocation);
|
|
68
|
+
const output = reportPreflight(report, { fixLocation });
|
|
69
|
+
process.stdout.write(output + "\n");
|
|
70
|
+
if (!report.passed) {
|
|
71
|
+
allPassed = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return allPassed ? 0 : 1;
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
parseRunArgs,
|
|
78
|
+
runCommand
|
|
79
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PreflightCheckList, PreflightConfig, StagedPreflightCheckList } from './types.ts';
|
|
2
|
+
export declare const CONFIG_FILE_PATH = ".config/preflight.config.ts";
|
|
3
|
+
export declare function definePreflightConfig(config: PreflightConfig): PreflightConfig;
|
|
4
|
+
export declare function definePreflightCheckList(checklist: PreflightCheckList): PreflightCheckList;
|
|
5
|
+
export declare function defineStagedPreflightCheckList(checklist: StagedPreflightCheckList): StagedPreflightCheckList;
|
|
6
|
+
export declare function loadPreflightConfig(configPath?: string): Promise<PreflightConfig>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const CONFIG_FILE_PATH = ".config/preflight.config.ts";
|
|
4
|
+
function definePreflightConfig(config) {
|
|
5
|
+
return config;
|
|
6
|
+
}
|
|
7
|
+
function definePreflightCheckList(checklist) {
|
|
8
|
+
return checklist;
|
|
9
|
+
}
|
|
10
|
+
function defineStagedPreflightCheckList(checklist) {
|
|
11
|
+
return checklist;
|
|
12
|
+
}
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function assertIsPreflightConfig(raw) {
|
|
17
|
+
if (!isRecord(raw)) {
|
|
18
|
+
throw new TypeError(`Preflight config must be an object, got ${Array.isArray(raw) ? "array" : typeof raw}`);
|
|
19
|
+
}
|
|
20
|
+
if (!Array.isArray(raw.checklists)) {
|
|
21
|
+
throw new TypeError("Preflight config must have a 'checklists' array");
|
|
22
|
+
}
|
|
23
|
+
for (const [i, entry] of raw.checklists.entries()) {
|
|
24
|
+
if (!isRecord(entry)) {
|
|
25
|
+
throw new Error(`checklists[${i}]: must be an object`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof entry.name !== "string" || entry.name === "") {
|
|
28
|
+
throw new Error(`checklists[${i}]: 'name' is required and must be a non-empty string`);
|
|
29
|
+
}
|
|
30
|
+
const hasChecks = "checks" in entry;
|
|
31
|
+
const hasGroups = "groups" in entry;
|
|
32
|
+
if (!hasChecks && !hasGroups) {
|
|
33
|
+
throw new Error(`checklists[${i}]: must have either 'checks' or 'groups'`);
|
|
34
|
+
}
|
|
35
|
+
if (hasChecks && hasGroups) {
|
|
36
|
+
throw new Error(`checklists[${i}]: cannot have both 'checks' and 'groups'`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function loadPreflightConfig(configPath) {
|
|
41
|
+
const resolvedPath = path.resolve(process.cwd(), configPath ?? CONFIG_FILE_PATH);
|
|
42
|
+
if (!existsSync(resolvedPath)) {
|
|
43
|
+
throw new Error(`Preflight config not found: ${resolvedPath}`);
|
|
44
|
+
}
|
|
45
|
+
const { createJiti } = await import("jiti");
|
|
46
|
+
const jiti = createJiti(import.meta.url);
|
|
47
|
+
const imported = await jiti.import(resolvedPath);
|
|
48
|
+
if (!isRecord(imported)) {
|
|
49
|
+
throw new Error(`Config file must export an object, got ${Array.isArray(imported) ? "array" : typeof imported}`);
|
|
50
|
+
}
|
|
51
|
+
const resolved = imported.default ?? imported.config;
|
|
52
|
+
if (resolved === void 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Config file must have a default export or a named `config` export (e.g., `export default definePreflightConfig({ ... })`)"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
assertIsPreflightConfig(resolved);
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
CONFIG_FILE_PATH,
|
|
62
|
+
definePreflightCheckList,
|
|
63
|
+
definePreflightConfig,
|
|
64
|
+
defineStagedPreflightCheckList,
|
|
65
|
+
loadPreflightConfig
|
|
66
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { FixLocation, PreflightCheck, PreflightCheckList, PreflightConfig, PreflightReport, PreflightResult, ReportOptions, StagedPreflightCheckList, } from './types.ts';
|
|
2
|
+
export { isFlatCheckList } from './types.ts';
|
|
3
|
+
export { definePreflightCheckList, definePreflightConfig, defineStagedPreflightCheckList } from './config.ts';
|
|
4
|
+
export { runPreflight } from './runPreflight.ts';
|
|
5
|
+
export { reportPreflight } from './reportPreflight.ts';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isFlatCheckList } from "./types.js";
|
|
2
|
+
import { definePreflightCheckList, definePreflightConfig, defineStagedPreflightCheckList } from "./config.js";
|
|
3
|
+
import { runPreflight } from "./runPreflight.js";
|
|
4
|
+
import { reportPreflight } from "./reportPreflight.js";
|
|
5
|
+
export {
|
|
6
|
+
definePreflightCheckList,
|
|
7
|
+
definePreflightConfig,
|
|
8
|
+
defineStagedPreflightCheckList,
|
|
9
|
+
isFlatCheckList,
|
|
10
|
+
reportPreflight,
|
|
11
|
+
runPreflight
|
|
12
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { printError, printStep } from "../lib/terminal.js";
|
|
2
|
+
import { scaffoldConfig } from "./scaffold.js";
|
|
3
|
+
function initCommand({ dryRun, force }) {
|
|
4
|
+
if (dryRun) {
|
|
5
|
+
console.info("[dry-run mode]");
|
|
6
|
+
}
|
|
7
|
+
printStep("Scaffolding config");
|
|
8
|
+
try {
|
|
9
|
+
const ok = scaffoldConfig({ dryRun, force });
|
|
10
|
+
if (!ok) {
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
} catch (error) {
|
|
14
|
+
printError(`Failed to scaffold config: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
if (!dryRun) {
|
|
18
|
+
printStep("Next steps");
|
|
19
|
+
console.info(`
|
|
20
|
+
1. Customize .config/preflight.config.ts with your checklists and checks.
|
|
21
|
+
2. Test by running: npx @williamthorsen/preflight run
|
|
22
|
+
3. Commit the generated file.
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
initCommand
|
|
29
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { printError, printSkip, printSuccess } from "../lib/terminal.js";
|
|
4
|
+
import { preflightConfigTemplate } from "./templates.js";
|
|
5
|
+
const CONFIG_PATH = ".config/preflight.config.ts";
|
|
6
|
+
function normalizeTrailingWhitespace(content) {
|
|
7
|
+
return content.split("\n").map((line) => line.trimEnd()).join("\n").trimEnd();
|
|
8
|
+
}
|
|
9
|
+
function tryWriteFile(filePath, content) {
|
|
10
|
+
try {
|
|
11
|
+
writeFileSync(filePath, content, "utf8");
|
|
12
|
+
return true;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
+
printError(`Failed to write ${filePath}: ${message}`);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeIfAbsent(filePath, content, dryRun, overwrite) {
|
|
20
|
+
const fileAlreadyExists = existsSync(filePath);
|
|
21
|
+
if (fileAlreadyExists && !overwrite) {
|
|
22
|
+
try {
|
|
23
|
+
const existing = readFileSync(filePath, "utf8");
|
|
24
|
+
if (normalizeTrailingWhitespace(existing) === normalizeTrailingWhitespace(content)) {
|
|
25
|
+
printSuccess(`${filePath} (up to date)`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
printError(`Could not read ${filePath}: ${message}`);
|
|
31
|
+
}
|
|
32
|
+
if (dryRun) {
|
|
33
|
+
printSkip(`[dry-run] Would skip ${filePath} (already exists)`);
|
|
34
|
+
} else {
|
|
35
|
+
printSkip(`${filePath} (already exists)`);
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (dryRun) {
|
|
40
|
+
const verb = fileAlreadyExists ? "overwrite" : "create";
|
|
41
|
+
printSuccess(`[dry-run] Would ${verb} ${filePath}`);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
printError(`Failed to create directory for ${filePath}: ${message}`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (tryWriteFile(filePath, content)) {
|
|
52
|
+
const verb = fileAlreadyExists ? "Overwrote" : "Created";
|
|
53
|
+
printSuccess(`${verb} ${filePath}`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function scaffoldConfig({ dryRun, force }) {
|
|
59
|
+
return writeIfAbsent(CONFIG_PATH, preflightConfigTemplate, dryRun, force);
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
scaffoldConfig
|
|
63
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const preflightConfigTemplate = "import { definePreflightConfig } from '@williamthorsen/preflight';\n\n/**\n * Preflight configuration.\n *\n * Each checklist contains checks that run before a deployment or other operation.\n * Checks run concurrently within a checklist. Use `fix` to provide remediation hints.\n */\nexport default definePreflightConfig({\n checklists: [\n {\n name: 'deploy',\n checks: [\n {\n name: 'environment variables set',\n check: () => Boolean(process.env['NODE_ENV']),\n fix: 'Set NODE_ENV before deploying',\n },\n ],\n },\n ],\n});\n";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const preflightConfigTemplate = `import { definePreflightConfig } from '@williamthorsen/preflight';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Preflight configuration.
|
|
5
|
+
*
|
|
6
|
+
* Each checklist contains checks that run before a deployment or other operation.
|
|
7
|
+
* Checks run concurrently within a checklist. Use \`fix\` to provide remediation hints.
|
|
8
|
+
*/
|
|
9
|
+
export default definePreflightConfig({
|
|
10
|
+
checklists: [
|
|
11
|
+
{
|
|
12
|
+
name: 'deploy',
|
|
13
|
+
checks: [
|
|
14
|
+
{
|
|
15
|
+
name: 'environment variables set',
|
|
16
|
+
check: () => Boolean(process.env['NODE_ENV']),
|
|
17
|
+
fix: 'Set NODE_ENV before deploying',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
`;
|
|
24
|
+
export {
|
|
25
|
+
preflightConfigTemplate
|
|
26
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function printStep(message) {
|
|
2
|
+
console.info(`
|
|
3
|
+
> ${message}`);
|
|
4
|
+
}
|
|
5
|
+
function printSuccess(message) {
|
|
6
|
+
console.info(` \u2705 ${message}`);
|
|
7
|
+
}
|
|
8
|
+
function printSkip(message) {
|
|
9
|
+
console.info(` \u26A0\uFE0F ${message}`);
|
|
10
|
+
}
|
|
11
|
+
function printError(message) {
|
|
12
|
+
console.error(` \u274C ${message}`);
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
printError,
|
|
16
|
+
printSkip,
|
|
17
|
+
printStep,
|
|
18
|
+
printSuccess
|
|
19
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const ICON_PASSED = "\u2705";
|
|
2
|
+
const ICON_FAILED = "\u274C";
|
|
3
|
+
const ICON_SKIPPED = "\u26AA";
|
|
4
|
+
function formatDuration(ms) {
|
|
5
|
+
return `${Math.round(ms)}ms`;
|
|
6
|
+
}
|
|
7
|
+
function getIcon(status) {
|
|
8
|
+
if (status === "passed") return ICON_PASSED;
|
|
9
|
+
if (status === "failed") return ICON_FAILED;
|
|
10
|
+
return ICON_SKIPPED;
|
|
11
|
+
}
|
|
12
|
+
function collectInlineDetails(result, includeFix) {
|
|
13
|
+
const details = [];
|
|
14
|
+
if (result.error !== void 0) {
|
|
15
|
+
details.push(` Error: ${result.error.message}`);
|
|
16
|
+
}
|
|
17
|
+
if (includeFix && result.fix !== void 0) {
|
|
18
|
+
details.push(` Fix: ${result.fix}`);
|
|
19
|
+
}
|
|
20
|
+
return details;
|
|
21
|
+
}
|
|
22
|
+
function reportPreflight(report, options) {
|
|
23
|
+
const fixLocation = options?.fixLocation ?? "END";
|
|
24
|
+
const lines = [];
|
|
25
|
+
const collectedFixes = [];
|
|
26
|
+
for (const result of report.results) {
|
|
27
|
+
const icon = getIcon(result.status);
|
|
28
|
+
lines.push(`${icon} ${result.name} (${formatDuration(result.durationMs)})`);
|
|
29
|
+
if (result.status === "failed") {
|
|
30
|
+
const includeFix = fixLocation === "INLINE";
|
|
31
|
+
lines.push(...collectInlineDetails(result, includeFix));
|
|
32
|
+
if (!includeFix && result.fix !== void 0) {
|
|
33
|
+
collectedFixes.push(result.fix);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let passed = 0;
|
|
38
|
+
let failed = 0;
|
|
39
|
+
let skipped = 0;
|
|
40
|
+
for (const r of report.results) {
|
|
41
|
+
if (r.status === "passed") passed++;
|
|
42
|
+
else if (r.status === "failed") failed++;
|
|
43
|
+
else skipped++;
|
|
44
|
+
}
|
|
45
|
+
lines.push("", `${passed} passed, ${failed} failed, ${skipped} skipped (${formatDuration(report.durationMs)})`);
|
|
46
|
+
if (fixLocation === "END" && collectedFixes.length > 0) {
|
|
47
|
+
lines.push("", "Fixes:", ...collectedFixes.map((fix) => ` ${fix}`));
|
|
48
|
+
}
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
reportPreflight
|
|
53
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { isFlatCheckList } from "./types.js";
|
|
3
|
+
function buildResult(name, status, durationMs, fix, error) {
|
|
4
|
+
const result = { name, status, durationMs };
|
|
5
|
+
if (fix !== void 0) {
|
|
6
|
+
result.fix = fix;
|
|
7
|
+
}
|
|
8
|
+
if (error !== void 0) {
|
|
9
|
+
result.error = error;
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
async function executeCheck(check) {
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
try {
|
|
16
|
+
const passed = await check.check();
|
|
17
|
+
const durationMs = performance.now() - start;
|
|
18
|
+
return buildResult(check.name, passed ? "passed" : "failed", durationMs, check.fix);
|
|
19
|
+
} catch (error_) {
|
|
20
|
+
const durationMs = performance.now() - start;
|
|
21
|
+
const error = error_ instanceof Error ? error_ : new Error(String(error_));
|
|
22
|
+
return buildResult(check.name, "failed", durationMs, check.fix, error);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function skipCheck(check) {
|
|
26
|
+
return buildResult(check.name, "skipped", 0, check.fix);
|
|
27
|
+
}
|
|
28
|
+
async function runPreconditions(preconditions, results) {
|
|
29
|
+
if (preconditions.length === 0) return true;
|
|
30
|
+
const preconditionResults = await Promise.all(preconditions.map(executeCheck));
|
|
31
|
+
results.push(...preconditionResults);
|
|
32
|
+
return preconditionResults.every((r) => r.status === "passed");
|
|
33
|
+
}
|
|
34
|
+
async function runFlatChecks(checklist, results, preconditionsPassed) {
|
|
35
|
+
if (!preconditionsPassed) {
|
|
36
|
+
results.push(...checklist.checks.map(skipCheck));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const checkResults = await Promise.all(checklist.checks.map(executeCheck));
|
|
40
|
+
results.push(...checkResults);
|
|
41
|
+
}
|
|
42
|
+
async function runStagedChecks(checklist, results, preconditionsPassed) {
|
|
43
|
+
if (!preconditionsPassed) {
|
|
44
|
+
for (const group of checklist.groups) {
|
|
45
|
+
results.push(...group.map(skipCheck));
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let shouldSkipRemaining = false;
|
|
50
|
+
for (const group of checklist.groups) {
|
|
51
|
+
if (shouldSkipRemaining) {
|
|
52
|
+
results.push(...group.map(skipCheck));
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const groupResults = await Promise.all(group.map(executeCheck));
|
|
56
|
+
results.push(...groupResults);
|
|
57
|
+
if (groupResults.some((r) => r.status === "failed")) {
|
|
58
|
+
shouldSkipRemaining = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function runPreflight(checklist) {
|
|
63
|
+
const start = performance.now();
|
|
64
|
+
const results = [];
|
|
65
|
+
const preconditionsPassed = await runPreconditions(checklist.preconditions ?? [], results);
|
|
66
|
+
await (isFlatCheckList(checklist) ? runFlatChecks(checklist, results, preconditionsPassed) : runStagedChecks(checklist, results, preconditionsPassed));
|
|
67
|
+
const durationMs = performance.now() - start;
|
|
68
|
+
const passed = results.every((r) => r.status !== "failed");
|
|
69
|
+
return { results, passed, durationMs };
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
runPreflight
|
|
73
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type FixLocation = 'INLINE' | 'END';
|
|
2
|
+
export interface PreflightCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
check: () => boolean | Promise<boolean>;
|
|
5
|
+
fix?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PreflightResult {
|
|
8
|
+
name: string;
|
|
9
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
10
|
+
fix?: string;
|
|
11
|
+
error?: Error;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
}
|
|
14
|
+
export interface PreflightReport {
|
|
15
|
+
results: PreflightResult[];
|
|
16
|
+
passed: boolean;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PreflightCheckList {
|
|
20
|
+
name: string;
|
|
21
|
+
preconditions?: PreflightCheck[];
|
|
22
|
+
checks: PreflightCheck[];
|
|
23
|
+
fixLocation?: FixLocation;
|
|
24
|
+
}
|
|
25
|
+
export interface StagedPreflightCheckList {
|
|
26
|
+
name: string;
|
|
27
|
+
preconditions?: PreflightCheck[];
|
|
28
|
+
groups: PreflightCheck[][];
|
|
29
|
+
fixLocation?: FixLocation;
|
|
30
|
+
}
|
|
31
|
+
export interface ReportOptions {
|
|
32
|
+
fixLocation?: FixLocation;
|
|
33
|
+
}
|
|
34
|
+
export interface PreflightConfig {
|
|
35
|
+
fixLocation?: FixLocation;
|
|
36
|
+
checklists: Array<PreflightCheckList | StagedPreflightCheckList>;
|
|
37
|
+
}
|
|
38
|
+
export declare function isFlatCheckList(checklist: PreflightCheckList | StagedPreflightCheckList): checklist is PreflightCheckList;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@williamthorsen/preflight",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Run pre-deployment checks to verify environment and configuration",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"checks",
|
|
8
|
+
"deploy",
|
|
9
|
+
"preflight",
|
|
10
|
+
"verification"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/preflight#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/williamthorsen/node-monorepo-tools/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/williamthorsen/node-monorepo-tools.git",
|
|
19
|
+
"directory": "packages/preflight"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./dist/esm/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"preflight": "bin/preflight.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"jiti": "2.6.1"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.17.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {}
|
|
46
|
+
}
|