-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathpreparse.ts
More file actions
137 lines (122 loc) · 4.93 KB
/
preparse.ts
File metadata and controls
137 lines (122 loc) · 4.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import type { Hook } from '@oclif/core/hooks';
const unescapedComma = /(?<!\\),/;
const hook: Hook.Preparse = async function ({ argv, options, context }) {
if (unescapedComma.test(argv.join(' '))) {
const hasArrayFlag = Object.values(options.flags ?? {}).some(
(flagOptions) => flagOptions.type === 'option' && flagOptions.multiple === true && flagOptions.delimiter === ','
);
if (hasArrayFlag) {
const { Lifecycle } = await import('@salesforce/core/lifecycle');
await Lifecycle.getInstance().emitWarning(
'The input format for array arguments has changed. Use this format: --array-flag value1 --array-flag value2 --array-flag value3'
);
}
}
// Skip this hook if command does not have a --flags-dir flag or if it is not present in argv
if (!argv.includes('--flags-dir') || !options.flags?.['flags-dir']) return argv;
const flagsDir = argv[argv.indexOf('--flags-dir') + 1];
if (!flagsDir) {
context.debug('No flags dir provided');
return argv;
}
if (flagsDir.startsWith('-')) {
context.debug(`No flags dir provided, got ${flagsDir}`);
return argv;
}
const { default: fs } = await import('node:fs/promises');
const { default: path } = await import('node:path');
context.debug('Initial argv', argv.join(' '));
const flagsToIgnore = new Set(
Object.entries(options.flags ?? {})
.filter(
([, flagOptions]) =>
// don't ignore if flag can take multiple values
(flagOptions.type === 'option' && flagOptions.multiple !== true) || flagOptions.type === 'boolean'
)
.filter(
([flagName, flagOptions]) =>
// Using || instead of ?? is intentional here: argv.includes() returns false (not null/undefined)
// when the flag isn't found, and we need false to trigger evaluation of subsequent conditions.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(flagOptions.char && argv.includes(`-${flagOptions.char}`)) ||
// ignore if long flag is present
argv.includes(`--${flagName}`) ||
// ignore if --no- flag is present
(flagOptions.type === 'boolean' && flagOptions.allowNo && argv.includes(`--no-${flagName}`))
)
.flatMap(([flagName, flagOptions]) => {
// Also ignore the --no- flag if boolean flag allows it
if (flagOptions.type === 'boolean' && flagOptions.allowNo) {
return [flagName, `no-${flagName}`];
}
return [flagName];
})
);
context.debug('Flags to ignore', flagsToIgnore);
async function safeReadDir(filePath: string): Promise<string[]> {
try {
return await fs.readdir(filePath);
} catch (err) {
context.debug('No flags dir found');
context.debug(err);
return [];
}
}
async function safeReadFile(filePath: string): Promise<string | undefined> {
try {
return await fs.readFile(filePath, 'utf8');
} catch (err) {
context.debug(filePath, err);
}
}
const filesInDir = await safeReadDir(flagsDir);
context.debug('Files in dir', filesInDir);
const flagsToInsert = await Promise.all(
filesInDir
// ignore files that were provided as flags
.filter((f) => !flagsToIgnore.has(f))
.map(async (file) => {
const { name, ext } = path.parse(file);
const contents = await safeReadFile(path.join(flagsDir, file));
if (contents === undefined) {
return [name, undefined] satisfies [string, undefined];
}
const crlf = contents.search('\r\n') !== -1;
let values: string[];
if (ext === '.json') {
// JSON files: parse and re-stringify (existing behavior)
values = [JSON.stringify(JSON.parse(contents))];
} else {
// Non-JSON files: split by lines and filter full-line comments
const lines = contents?.trim().split(crlf ? '\r\n' : '\n') || [];
values = lines.filter((line) => {
const trimmed = line.trim();
// Filter out lines that are only whitespace + comment (full-line comments)
if (trimmed.startsWith('#') || trimmed.startsWith('//')) {
return false;
}
// Keep all other lines unchanged, including empty lines
return true;
});
}
return [name, values] satisfies [string, string[]];
})
);
const newArgv = [...argv];
context.debug('Flags to insert', flagsToInsert);
for (const [flag, values] of flagsToInsert) {
for (const value of values ?? []) {
newArgv.push(flag.length === 1 ? `-${flag}` : `--${flag}`);
if (value) newArgv.push(value);
}
}
context.debug(`Returning argv: ${newArgv.join(' ')}`);
return newArgv;
};
export default hook;