@triflare/fx 1.0.3 → 2.0.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/README.md +32 -0
- package/bin/fx.js +4 -1162
- package/package.json +5 -3
- package/product.yml +1 -1
- package/src/brand.js +9 -3
- package/src/cli/commands/build.js +98 -0
- package/src/cli/commands/catalog.js +42 -0
- package/src/cli/commands/check.js +44 -0
- package/src/cli/commands/completion.js +54 -0
- package/src/cli/commands/decompile.js +266 -0
- package/src/cli/commands/dev.js +179 -0
- package/src/cli/commands/extension.js +110 -0
- package/src/cli/commands/index.js +39 -0
- package/src/cli/commands/init.js +169 -0
- package/src/cli/commands/package.js +137 -0
- package/src/cli/commands/sprite/add.js +85 -0
- package/src/cli/commands/sprite/build.js +62 -0
- package/src/cli/commands/sprite/helpers.js +66 -0
- package/src/cli/commands/sprite/index.js +29 -0
- package/src/cli/commands/sprite/list.js +31 -0
- package/src/cli/commands/sprite/remove.js +51 -0
- package/src/cli/commands/vendor/add.js +98 -0
- package/src/cli/commands/vendor/helpers.js +50 -0
- package/src/cli/commands/vendor/index.js +39 -0
- package/src/cli/commands/vendor/install.js +64 -0
- package/src/cli/commands/vendor/list.js +45 -0
- package/src/cli/commands/vendor/migrate.js +47 -0
- package/src/cli/commands/vendor/outdated.js +51 -0
- package/src/cli/commands/vendor/remove.js +34 -0
- package/src/cli/commands/vendor/update.js +49 -0
- package/src/cli/commands/vendor/verify.js +44 -0
- package/src/cli/completion/bash.js +10 -0
- package/src/cli/completion/fish.js +9 -0
- package/src/cli/completion/index.js +97 -0
- package/src/cli/completion/zsh.js +11 -0
- package/src/cli/context.js +257 -0
- package/src/cli/define-command.js +140 -0
- package/src/cli/errors.js +111 -0
- package/src/cli/format.js +31 -0
- package/src/cli/prompts.js +183 -0
- package/src/cli/reporter.js +194 -0
- package/src/cli/run.js +248 -0
- package/src/cli/templates/index.js +112 -0
- package/src/cli/theme.js +84 -0
- package/src/config-edit.js +280 -1
- package/src/config.js +137 -3
- package/src/extensions.js +72 -19
- package/src/index.js +21 -0
- package/src/vendor.js +1323 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { outdatedVendor } from "../../../vendor.js";
|
|
3
|
+
import { UsageError } from "../../errors.js";
|
|
4
|
+
import { outcome } from "../../define-command.js";
|
|
5
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
export const vendorOutdatedCommand = {
|
|
8
|
+
name: "outdated",
|
|
9
|
+
summary: "Compare upstream sources with fx.lock",
|
|
10
|
+
description: "Check whether any vendored extension changed upstream.",
|
|
11
|
+
examples: ["fx vendor outdated"],
|
|
12
|
+
async run({ cwd, reporter, product }) {
|
|
13
|
+
const configPath = requireConfigPath(cwd, product);
|
|
14
|
+
const configDir = path.dirname(configPath);
|
|
15
|
+
const loaded = loadVendorConfig(configPath);
|
|
16
|
+
if (loaded.vendor.length === 0)
|
|
17
|
+
throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
|
|
18
|
+
|
|
19
|
+
const results = await outdatedVendor({ configDir, config: loaded });
|
|
20
|
+
let outdated = 0;
|
|
21
|
+
let errors = 0;
|
|
22
|
+
for (const result of results) {
|
|
23
|
+
if (result.status === "outdated") outdated += 1;
|
|
24
|
+
if (result.status === "error") errors += 1;
|
|
25
|
+
const label =
|
|
26
|
+
result.status === "outdated"
|
|
27
|
+
? "outdated"
|
|
28
|
+
: result.status === "error"
|
|
29
|
+
? "error"
|
|
30
|
+
: "up to date";
|
|
31
|
+
const tone =
|
|
32
|
+
result.status === "outdated"
|
|
33
|
+
? "warning"
|
|
34
|
+
: result.status === "error"
|
|
35
|
+
? "danger"
|
|
36
|
+
: "success";
|
|
37
|
+
reporter.item(result.name, reporter.tone(tone, label), { width: 24 });
|
|
38
|
+
if (result.error) reporter.warn(result.error);
|
|
39
|
+
}
|
|
40
|
+
if (outdated > 0)
|
|
41
|
+
reporter.warn(
|
|
42
|
+
`${outdated} extension(s) changed upstream; run "${product.command} vendor update"`,
|
|
43
|
+
);
|
|
44
|
+
else if (errors === 0) reporter.success("Everything is up to date");
|
|
45
|
+
|
|
46
|
+
return outcome({
|
|
47
|
+
data: results,
|
|
48
|
+
exitCode: errors > 0 ? 1 : 0,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { removeVendoredExtension } from "../../../vendor.js";
|
|
3
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
export const vendorRemoveCommand = {
|
|
6
|
+
name: "remove",
|
|
7
|
+
summary: "Remove a vendored extension",
|
|
8
|
+
description:
|
|
9
|
+
"Remove an extension from the project config, fx.lock, and the vendor directory.",
|
|
10
|
+
arguments: ["<name>"],
|
|
11
|
+
examples: ["fx vendor remove signals"],
|
|
12
|
+
async run({ args, cwd, reporter, prompts, yes, product }) {
|
|
13
|
+
const name = args[0];
|
|
14
|
+
const configPath = requireConfigPath(cwd, product);
|
|
15
|
+
const configDir = path.dirname(configPath);
|
|
16
|
+
const loaded = loadVendorConfig(configPath);
|
|
17
|
+
|
|
18
|
+
await prompts.confirmDestructive({
|
|
19
|
+
message: `Remove vendored extension ${JSON.stringify(name)}?`,
|
|
20
|
+
yes,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = removeVendoredExtension({
|
|
24
|
+
configPath,
|
|
25
|
+
configDir,
|
|
26
|
+
config: loaded,
|
|
27
|
+
name,
|
|
28
|
+
});
|
|
29
|
+
reporter.success(
|
|
30
|
+
`Removed ${name} from ${path.basename(configPath)}, fx.lock and ${result.file}`,
|
|
31
|
+
);
|
|
32
|
+
return { name, file: result.file };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { updateVendor } from "../../../vendor.js";
|
|
3
|
+
import { UsageError } from "../../errors.js";
|
|
4
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
5
|
+
|
|
6
|
+
export const vendorUpdateCommand = {
|
|
7
|
+
name: "update",
|
|
8
|
+
summary: "Update vendored extensions and show diffs",
|
|
9
|
+
description: `Re-fetch upstream sources, update the vendored copies, and show a
|
|
10
|
+
diff for each change. Use --dry-run to preview without writing.`,
|
|
11
|
+
arguments: ["[name]"],
|
|
12
|
+
options: [
|
|
13
|
+
{ flags: "--dry-run", description: "Show diffs without writing files" },
|
|
14
|
+
],
|
|
15
|
+
examples: ["fx vendor update", "fx vendor update signals --dry-run"],
|
|
16
|
+
async run({ args, options, cwd, reporter, product }) {
|
|
17
|
+
const configPath = requireConfigPath(cwd, product);
|
|
18
|
+
const configDir = path.dirname(configPath);
|
|
19
|
+
const loaded = loadVendorConfig(configPath);
|
|
20
|
+
if (loaded.vendor.length === 0)
|
|
21
|
+
throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
|
|
22
|
+
|
|
23
|
+
const result = await updateVendor({
|
|
24
|
+
configPath,
|
|
25
|
+
configDir,
|
|
26
|
+
config: loaded,
|
|
27
|
+
name: args[0],
|
|
28
|
+
write: !options.dryRun,
|
|
29
|
+
});
|
|
30
|
+
for (const entry of result.entries) {
|
|
31
|
+
if (!entry.changed) {
|
|
32
|
+
reporter.info(`${entry.name} is up to date`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
reporter.text(entry.diff);
|
|
36
|
+
}
|
|
37
|
+
if (options.dryRun && result.changed > 0)
|
|
38
|
+
reporter.info(`Dry run: ${result.changed} extension(s) would change`);
|
|
39
|
+
else if (result.changed > 0)
|
|
40
|
+
reporter.success(`Updated ${result.changed} extension(s)`);
|
|
41
|
+
else reporter.success("Everything is up to date");
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
dryRun: Boolean(options.dryRun),
|
|
45
|
+
changed: result.changed,
|
|
46
|
+
entries: result.entries.map(({ name, changed }) => ({ name, changed })),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { LOCK_FILENAME, verifyVendor } from "../../../vendor.js";
|
|
3
|
+
import { UsageError } from "../../errors.js";
|
|
4
|
+
import { outcome } from "../../define-command.js";
|
|
5
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
export const vendorVerifyCommand = {
|
|
8
|
+
name: "verify",
|
|
9
|
+
summary: "Check vendored files against fx.lock offline",
|
|
10
|
+
description:
|
|
11
|
+
"Verify every vendored extension without making network requests.",
|
|
12
|
+
examples: ["fx vendor verify"],
|
|
13
|
+
run({ cwd, reporter, product }) {
|
|
14
|
+
const configPath = requireConfigPath(cwd, product);
|
|
15
|
+
const configDir = path.dirname(configPath);
|
|
16
|
+
const loaded = loadVendorConfig(configPath);
|
|
17
|
+
if (loaded.vendor.length === 0)
|
|
18
|
+
throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
|
|
19
|
+
|
|
20
|
+
const result = verifyVendor({ configDir, config: loaded });
|
|
21
|
+
for (const warning of result.warnings) reporter.warn(warning);
|
|
22
|
+
for (const entry of result.entries)
|
|
23
|
+
if (entry.status === "ok")
|
|
24
|
+
reporter.item(entry.name, entry.file, { width: 24 });
|
|
25
|
+
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
for (const problem of result.problems) reporter.error(problem);
|
|
28
|
+
return outcome({
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
data: { entries: result.entries, problems: result.problems },
|
|
31
|
+
error: {
|
|
32
|
+
code: "VENDOR_VERIFY_FAILED",
|
|
33
|
+
message: `Vendor verification failed for ${result.problems.length} extension(s)`,
|
|
34
|
+
details: { problems: result.problems },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
reporter.success(
|
|
40
|
+
`Verified ${result.entries.length} extension(s) against ${LOCK_FILENAME}`,
|
|
41
|
+
);
|
|
42
|
+
return { entries: result.entries };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Generate a bash completion script. */
|
|
2
|
+
export function bashCompletion(command) {
|
|
3
|
+
return `# bash completion for ${command}
|
|
4
|
+
_${command}_complete() {
|
|
5
|
+
local IFS=$'\\n'
|
|
6
|
+
COMPREPLY=( $("${command}" __complete bash -- "\${COMP_WORDS[@]:1}") )
|
|
7
|
+
}
|
|
8
|
+
complete -o bashdefault -o default -F _${command}_complete ${command}
|
|
9
|
+
`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Generate a fish completion script. */
|
|
2
|
+
export function fishCompletion(command) {
|
|
3
|
+
return `# fish completion for ${command}
|
|
4
|
+
function __${command}_complete
|
|
5
|
+
${command} __complete fish -- (commandline -opc)[2..-1]
|
|
6
|
+
end
|
|
7
|
+
complete -c ${command} -f -a '(__${command}_complete)'
|
|
8
|
+
`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion support.
|
|
3
|
+
*
|
|
4
|
+
* Completion is intentionally data-driven: the hidden `__complete` command
|
|
5
|
+
* walks the live commander tree, so new commands and options are completed
|
|
6
|
+
* without maintaining a second list.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const VALUE_OPTIONS = {
|
|
10
|
+
"--color": ["auto", "always", "never"],
|
|
11
|
+
"--cwd": [],
|
|
12
|
+
"--config": [],
|
|
13
|
+
"--template": ["cat", "empty", "blank"],
|
|
14
|
+
"--target": ["html", "zip", "windows", "macos", "linux"],
|
|
15
|
+
"--size": ["480x360", "640x360", "1920x1080"],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function commandFor(program, words) {
|
|
19
|
+
let command = program;
|
|
20
|
+
for (const word of words) {
|
|
21
|
+
if (word.startsWith("-")) continue;
|
|
22
|
+
const next = command.commands.find(
|
|
23
|
+
(entry) => entry.name() === word || entry.aliases().includes(word),
|
|
24
|
+
);
|
|
25
|
+
if (!next) break;
|
|
26
|
+
command = next;
|
|
27
|
+
}
|
|
28
|
+
return command;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function commandWords(command) {
|
|
32
|
+
const words = new Set();
|
|
33
|
+
for (const entry of command.commands) {
|
|
34
|
+
if (entry._hidden) continue;
|
|
35
|
+
words.add(entry.name());
|
|
36
|
+
for (const alias of entry.aliases()) words.add(alias);
|
|
37
|
+
}
|
|
38
|
+
return [...words];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function optionWords(command) {
|
|
42
|
+
const words = new Set();
|
|
43
|
+
for (let current = command; current; current = current.parent) {
|
|
44
|
+
for (const option of current.options) {
|
|
45
|
+
if (option.hidden) continue;
|
|
46
|
+
if (option.long) words.add(option.long);
|
|
47
|
+
if (option.short) words.add(option.short);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...words];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Candidate completions for the words typed after the command name. */
|
|
54
|
+
export function completionCandidates(program, words) {
|
|
55
|
+
const current = words.at(-1) ?? "";
|
|
56
|
+
const before = words.slice(0, -1).filter((word) => word !== "");
|
|
57
|
+
const command = commandFor(program, before);
|
|
58
|
+
|
|
59
|
+
// `--color=` style value completion.
|
|
60
|
+
if (current.includes("=")) {
|
|
61
|
+
const [flag, prefix] = current.split("=", 2);
|
|
62
|
+
const values = VALUE_OPTIONS[flag] ?? [];
|
|
63
|
+
return values
|
|
64
|
+
.filter((value) => value.startsWith(prefix))
|
|
65
|
+
.map((value) => `${flag}=${value}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A value is expected for the previous token.
|
|
69
|
+
const previous = before.at(-1);
|
|
70
|
+
if (previous && Object.hasOwn(VALUE_OPTIONS, previous)) {
|
|
71
|
+
const values = VALUE_OPTIONS[previous];
|
|
72
|
+
return values.filter((value) => value.startsWith(current));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const candidates = new Set();
|
|
76
|
+
if (current.startsWith("-")) {
|
|
77
|
+
for (const option of optionWords(command)) {
|
|
78
|
+
if (option.startsWith(current)) candidates.add(option);
|
|
79
|
+
}
|
|
80
|
+
return [...candidates].sort();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Groups with subcommands (the root, vendor, sprite, ...) offer their
|
|
84
|
+
// command words; leaf commands have none.
|
|
85
|
+
if (command.commands.length > 0) {
|
|
86
|
+
for (const word of commandWords(command))
|
|
87
|
+
if (word.startsWith(current)) candidates.add(word);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Positional values that are useful and finite.
|
|
91
|
+
if (command.name() === "completion") {
|
|
92
|
+
for (const shell of ["bash", "zsh", "fish"])
|
|
93
|
+
if (shell.startsWith(current)) candidates.add(shell);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return [...candidates].sort();
|
|
97
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Generate a zsh completion script. */
|
|
2
|
+
export function zshCompletion(command) {
|
|
3
|
+
return `#compdef ${command}
|
|
4
|
+
_${command}_complete() {
|
|
5
|
+
local -a completions
|
|
6
|
+
completions=("\${(@f)$("${command}" __complete zsh -- "\${words[@]:1}")}")
|
|
7
|
+
_describe '${command}' completions
|
|
8
|
+
}
|
|
9
|
+
compdef _${command}_complete ${command}
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared project discovery for every command.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the input path, the nearest (or explicitly selected) `fx.yml`, and
|
|
5
|
+
* the extension catalog once, so commands do not repeat subtly different
|
|
6
|
+
* versions of the same logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { configFilename, outputDirectory, sourceExtension } from "../brand.js";
|
|
12
|
+
import { loadConfigFrom, parseConfig } from "../config.js";
|
|
13
|
+
import { loadExtensions } from "../extensions.js";
|
|
14
|
+
import { loadVendorExtensions, migrateProjectExtensions } from "../vendor.js";
|
|
15
|
+
import { UsageError } from "./errors.js";
|
|
16
|
+
|
|
17
|
+
const warnedLegacy = new Set();
|
|
18
|
+
const migrationDeclined = new Set();
|
|
19
|
+
|
|
20
|
+
/** Split a positional input into a file or directory target. */
|
|
21
|
+
export function resolveInput(input, cwd = process.cwd()) {
|
|
22
|
+
if (!input) return { dir: path.resolve(cwd) };
|
|
23
|
+
const resolved = path.resolve(cwd, input);
|
|
24
|
+
let stat;
|
|
25
|
+
try {
|
|
26
|
+
stat = fs.statSync(resolved);
|
|
27
|
+
} catch {
|
|
28
|
+
throw new UsageError(`input not found: ${input}`, {
|
|
29
|
+
hint: `check the path, or run from the project directory`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return stat.isDirectory() ? { dir: resolved } : { file: resolved };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function inputSearchDir(target) {
|
|
36
|
+
return target.file ? path.dirname(target.file) : target.dir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether `--no-config` disables configuration loading. */
|
|
40
|
+
export function configDisabled(options = {}) {
|
|
41
|
+
return options.config === false || options.noConfig === true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Load configuration from an explicit path or by searching upward. */
|
|
45
|
+
export function loadProjectConfig({ cwd = process.cwd(), searchDir, options }) {
|
|
46
|
+
if (configDisabled(options)) {
|
|
47
|
+
return {
|
|
48
|
+
project: null,
|
|
49
|
+
fx: null,
|
|
50
|
+
package: null,
|
|
51
|
+
vendor: [],
|
|
52
|
+
vendorDir: "extensions",
|
|
53
|
+
path: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const explicit = typeof options.config === "string" ? options.config : null;
|
|
58
|
+
if (!explicit) return loadConfigFrom(searchDir);
|
|
59
|
+
const explicitPath = path.resolve(cwd, explicit);
|
|
60
|
+
if (!fs.existsSync(explicitPath))
|
|
61
|
+
throw new UsageError(`config file not found: ${explicitPath}`);
|
|
62
|
+
if (!fs.statSync(explicitPath).isFile())
|
|
63
|
+
throw new UsageError(`config path is not a file: ${explicitPath}`);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const parsed = parseConfig(fs.readFileSync(explicitPath, "utf8"));
|
|
67
|
+
return { ...parsed, path: explicitPath };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof UsageError) throw error;
|
|
70
|
+
throw new UsageError(error.message, { cause: error });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function warnLegacyExtensions(loaded, reporter) {
|
|
75
|
+
if (!loaded?.project?.extensions?.length) return;
|
|
76
|
+
const key = loaded.path ?? "<default>";
|
|
77
|
+
if (warnedLegacy.has(key)) return;
|
|
78
|
+
warnedLegacy.add(key);
|
|
79
|
+
reporter.warn(
|
|
80
|
+
`project.extensions is deprecated and will be removed in a future major version; ` +
|
|
81
|
+
`run "${configFilename()} vendor migrate" to move extensions into vendor:`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Offer to migrate legacy project.extensions before extensions are loaded.
|
|
87
|
+
* `--yes` accepts without a terminal; otherwise the prompt only runs on one.
|
|
88
|
+
*/
|
|
89
|
+
async function upgradeLegacyExtensions({
|
|
90
|
+
loaded,
|
|
91
|
+
cwd,
|
|
92
|
+
searchDir,
|
|
93
|
+
options = {},
|
|
94
|
+
reporter,
|
|
95
|
+
prompts,
|
|
96
|
+
}) {
|
|
97
|
+
if (!loaded?.path || !loaded.project?.extensions?.length) return loaded;
|
|
98
|
+
warnLegacyExtensions(loaded, reporter);
|
|
99
|
+
if (migrationDeclined.has(loaded.path)) return loaded;
|
|
100
|
+
if (options.upgradePrompt === false || process.env.FX_NO_PROMPT === "1")
|
|
101
|
+
return loaded;
|
|
102
|
+
|
|
103
|
+
const auto = Boolean(options.yes);
|
|
104
|
+
const interactive = !auto && Boolean(prompts?.enabled);
|
|
105
|
+
if (!auto && !interactive) return loaded;
|
|
106
|
+
if (!auto) {
|
|
107
|
+
const upgrade = await prompts.confirm({
|
|
108
|
+
message: "Upgrade project.extensions to the vendor system?",
|
|
109
|
+
initialValue: true,
|
|
110
|
+
});
|
|
111
|
+
if (!upgrade) {
|
|
112
|
+
migrationDeclined.add(loaded.path);
|
|
113
|
+
return loaded;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const result = await migrateProjectExtensions({
|
|
119
|
+
configPath: loaded.path,
|
|
120
|
+
configDir: path.dirname(loaded.path),
|
|
121
|
+
config: loaded,
|
|
122
|
+
nameFor: interactive
|
|
123
|
+
? (name, source) =>
|
|
124
|
+
prompts.text({
|
|
125
|
+
message: `Name for ${source}`,
|
|
126
|
+
defaultValue: name,
|
|
127
|
+
})
|
|
128
|
+
: undefined,
|
|
129
|
+
});
|
|
130
|
+
for (const warning of result.warnings) reporter.warn(warning);
|
|
131
|
+
if (result.diff) reporter.text(result.diff);
|
|
132
|
+
if (result.migrated.length > 0)
|
|
133
|
+
reporter.success(
|
|
134
|
+
`Vendored ${result.migrated.length} extension(s); backup: ${path.relative(
|
|
135
|
+
cwd,
|
|
136
|
+
result.backup,
|
|
137
|
+
)}`,
|
|
138
|
+
);
|
|
139
|
+
return loadProjectConfig({ cwd, searchDir, options, reporter });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// A failed migration would otherwise warn (and retry) on every rebuild.
|
|
142
|
+
migrationDeclined.add(loaded.path);
|
|
143
|
+
reporter.warn(`Could not migrate project.extensions: ${error.message}`);
|
|
144
|
+
return loaded;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Load vendored and legacy extensions into one catalog. */
|
|
149
|
+
export async function loadProjectExtensions({ configDir, loaded, reporter }) {
|
|
150
|
+
const catalog = {};
|
|
151
|
+
const extensionUrls = {};
|
|
152
|
+
const unsandboxed = [];
|
|
153
|
+
const config = loaded?.project || {};
|
|
154
|
+
const matchedUnsandboxed = new Set();
|
|
155
|
+
const unnecessaryUnsandboxed = new Set();
|
|
156
|
+
|
|
157
|
+
if (loaded?.vendor && loaded.vendor.length > 0) {
|
|
158
|
+
const vendored = await loadVendorExtensions({ configDir, config: loaded });
|
|
159
|
+
Object.assign(catalog, vendored.catalog);
|
|
160
|
+
Object.assign(extensionUrls, vendored.extensionUrls);
|
|
161
|
+
unsandboxed.push(...vendored.unsandboxed);
|
|
162
|
+
for (const name of vendored.matchedUnsandboxed || [])
|
|
163
|
+
matchedUnsandboxed.add(name);
|
|
164
|
+
for (const name of vendored.unnecessaryUnsandboxed || [])
|
|
165
|
+
unnecessaryUnsandboxed.add(name);
|
|
166
|
+
for (const warning of vendored.warnings) reporter.warn(warning);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (config.extensions && config.extensions.length > 0) {
|
|
170
|
+
warnLegacyExtensions(loaded, reporter);
|
|
171
|
+
const legacy = await loadExtensions(config.extensions, {
|
|
172
|
+
baseDir: configDir,
|
|
173
|
+
expectUnsandboxed: config.unsandboxed,
|
|
174
|
+
reportUnmatchedUnsandboxed: false,
|
|
175
|
+
});
|
|
176
|
+
Object.assign(catalog, legacy.catalog);
|
|
177
|
+
Object.assign(extensionUrls, legacy.extensionUrls);
|
|
178
|
+
unsandboxed.push(...legacy.unsandboxed);
|
|
179
|
+
for (const name of legacy.matchedUnsandboxed || [])
|
|
180
|
+
matchedUnsandboxed.add(name);
|
|
181
|
+
for (const name of legacy.unnecessaryUnsandboxed || [])
|
|
182
|
+
unnecessaryUnsandboxed.add(name);
|
|
183
|
+
for (const warning of legacy.warnings) reporter.warn(warning);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const name of new Set(config.unsandboxed || [])) {
|
|
187
|
+
if (unnecessaryUnsandboxed.has(name)) {
|
|
188
|
+
reporter.warn(
|
|
189
|
+
`project.unsandboxed entry "${name}" loads in the sandbox; remove it from fx.yml`,
|
|
190
|
+
);
|
|
191
|
+
} else if (!matchedUnsandboxed.has(name)) {
|
|
192
|
+
reporter.warn(
|
|
193
|
+
`project.unsandboxed entry "${name}" does not match any loaded extension`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { catalog, extensionUrls, unsandboxed };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Resolve everything a build-like command needs.
|
|
203
|
+
*
|
|
204
|
+
* @param {{
|
|
205
|
+
* input?: string,
|
|
206
|
+
* cwd: string,
|
|
207
|
+
* options: object,
|
|
208
|
+
* reporter: object,
|
|
209
|
+
* prompts?: object,
|
|
210
|
+
* includeExtensions?: boolean,
|
|
211
|
+
* migrateLegacy?: boolean,
|
|
212
|
+
* }} options
|
|
213
|
+
*/
|
|
214
|
+
export async function loadProjectState({
|
|
215
|
+
input,
|
|
216
|
+
cwd,
|
|
217
|
+
options,
|
|
218
|
+
reporter,
|
|
219
|
+
prompts = null,
|
|
220
|
+
includeExtensions = true,
|
|
221
|
+
migrateLegacy = true,
|
|
222
|
+
}) {
|
|
223
|
+
const target = resolveInput(input, cwd);
|
|
224
|
+
const searchDir = inputSearchDir(target);
|
|
225
|
+
let loaded = loadProjectConfig({ cwd, searchDir, options, reporter });
|
|
226
|
+
if (migrateLegacy)
|
|
227
|
+
loaded = await upgradeLegacyExtensions({
|
|
228
|
+
loaded,
|
|
229
|
+
cwd,
|
|
230
|
+
searchDir,
|
|
231
|
+
options,
|
|
232
|
+
reporter,
|
|
233
|
+
prompts,
|
|
234
|
+
});
|
|
235
|
+
const configDir = loaded.path ? path.dirname(loaded.path) : searchDir;
|
|
236
|
+
const extensions = includeExtensions
|
|
237
|
+
? await loadProjectExtensions({ configDir, loaded, reporter })
|
|
238
|
+
: { catalog: {}, extensionUrls: {}, unsandboxed: [] };
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
target,
|
|
242
|
+
searchDir,
|
|
243
|
+
loaded,
|
|
244
|
+
configDir,
|
|
245
|
+
...extensions,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Default output directory for a directory build. */
|
|
250
|
+
export function defaultOutputDirectory(cwd) {
|
|
251
|
+
return path.join(cwd, outputDirectory());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Source extension used in user-facing messages. */
|
|
255
|
+
export function sourceSuffix() {
|
|
256
|
+
return sourceExtension();
|
|
257
|
+
}
|