mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(settings): expose remaining import errors
This commit is contained in:
@@ -12,3 +12,11 @@ export function projectConfigImportAvailabilityStatus(input: {
|
||||
}
|
||||
return input.availableCount === 1 ? "one" : "many";
|
||||
}
|
||||
|
||||
export function projectConfigImportPreviewIsOpenable(
|
||||
preview: { ok: true; status: string } | { ok: false; error: { code: string } } | null | undefined,
|
||||
): boolean {
|
||||
return preview?.ok === true
|
||||
? preview.status === "available"
|
||||
: preview?.error.code === "invalid_source_config";
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
stripProjectConfigImportSearchParams,
|
||||
} from "./route";
|
||||
import { projectConfigImportApplyFailureRetryAction } from "./retry";
|
||||
import { projectConfigImportAvailabilityStatus } from "./availability";
|
||||
import {
|
||||
projectConfigImportAvailabilityStatus,
|
||||
projectConfigImportPreviewIsOpenable,
|
||||
} from "./availability";
|
||||
import {
|
||||
createProjectConfigImportSourceRegistry,
|
||||
type ProjectConfigImportSourceDescriptor,
|
||||
@@ -301,4 +304,19 @@ describe("project config import availability", () => {
|
||||
"none",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps invalid advertised sources openable", () => {
|
||||
expect(
|
||||
projectConfigImportPreviewIsOpenable({
|
||||
ok: false,
|
||||
error: { code: "invalid_source_config" },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
projectConfigImportPreviewIsOpenable({
|
||||
ok: false,
|
||||
error: { code: "source_config_not_found" },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,10 @@ import {
|
||||
projectConfigImportApplyFailureRetryAction,
|
||||
type ProjectConfigImportRetryAction,
|
||||
} from "./retry";
|
||||
import { projectConfigImportAvailabilityStatus } from "./availability";
|
||||
import {
|
||||
projectConfigImportAvailabilityStatus,
|
||||
projectConfigImportPreviewIsOpenable,
|
||||
} from "./availability";
|
||||
|
||||
const EMPTY_IMPORT_SOURCES: readonly ProjectConfigImportAdvertisedSource[] = [];
|
||||
type ProjectConfigImportPreviewSuccess = Extract<ProjectConfigImportPreviewResult, { ok: true }>;
|
||||
@@ -302,23 +305,22 @@ export function useProjectConfigImportAvailability(input: {
|
||||
sources,
|
||||
enabled: input.enabled,
|
||||
});
|
||||
const availableSources = sources.filter((_, index) => {
|
||||
const data = previews[index]?.data;
|
||||
return data?.ok === true && data.status === "available";
|
||||
});
|
||||
const availableKinds = new Set(availableSources.map((source) => source.kind));
|
||||
const openableSources = sources.filter((_, index) =>
|
||||
projectConfigImportPreviewIsOpenable(previews[index]?.data),
|
||||
);
|
||||
const availableKinds = new Set(openableSources.map((source) => source.kind));
|
||||
const availableSourceKeys = new Set(
|
||||
availableSources.map((source) => stableProjectConfigImportSourceKey(source.source)),
|
||||
openableSources.map((source) => stableProjectConfigImportSourceKey(source.source)),
|
||||
);
|
||||
const isLoading = previews.some((preview) => preview.isLoading || preview.isPending);
|
||||
|
||||
return {
|
||||
status: projectConfigImportAvailabilityStatus({
|
||||
availableCount: availableSources.length,
|
||||
availableCount: openableSources.length,
|
||||
isLoading,
|
||||
}),
|
||||
source: availableSources.length === 1 ? availableSources[0] : null,
|
||||
sources: availableSources,
|
||||
source: openableSources.length === 1 ? openableSources[0] : null,
|
||||
sources: openableSources,
|
||||
availableKinds,
|
||||
availableSourceKeys,
|
||||
};
|
||||
|
||||
@@ -248,6 +248,31 @@ cwd = "apps/local-web"
|
||||
});
|
||||
});
|
||||
|
||||
test("merges root conductor.json before scoped JSON until TOML migration", () => {
|
||||
const repo = makeRepo();
|
||||
writeFileSync(
|
||||
join(repo, "conductor.json"),
|
||||
JSON.stringify({ scripts: { setup: "legacy setup" }, runScriptMode: "nonconcurrent" }),
|
||||
);
|
||||
writeSharedJson(repo, { scripts: { run: { dev: { command: "npm run dev" } } } });
|
||||
|
||||
const preview = inspect(repo);
|
||||
|
||||
expect(preview.inputs).toEqual([
|
||||
{ role: "legacy", relativePath: "conductor.json" },
|
||||
{ role: "shared", relativePath: ".conductor/settings.json" },
|
||||
]);
|
||||
expect(preview.preview).toMatchObject({
|
||||
worktree: { setup: "legacy setup" },
|
||||
scripts: { dev: { command: "npm run dev" } },
|
||||
});
|
||||
expect(preview.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ key: "runScriptMode", outcome: "unsupported" }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test("reports missing and empty repository-local Conductor configs", () => {
|
||||
const missingRepo = makeRepo();
|
||||
const emptyRepo = makeRepo();
|
||||
@@ -420,6 +445,47 @@ command = "npm run dev -- --port \${CONDUCTOR_PORT:-3000}"
|
||||
});
|
||||
});
|
||||
|
||||
test("rewrites bare Conductor ports inside shell arithmetic", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(
|
||||
repo,
|
||||
`
|
||||
[scripts.run.dev]
|
||||
command = "npm run dev -- --hmr-port $((CONDUCTOR_PORT + 1))"
|
||||
`,
|
||||
);
|
||||
|
||||
expect(inspect(repo).preview).toMatchObject({
|
||||
scripts: {
|
||||
dev: {
|
||||
type: "service",
|
||||
command: "npm run dev -- --hmr-port $((PASEO_PORT + 1))",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves shell arithmetic expansion in script arguments", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(
|
||||
repo,
|
||||
`
|
||||
[scripts.run.dev]
|
||||
command = "npm run dev"
|
||||
args = ["--hmr-port=$((CONDUCTOR_PORT + 1))"]
|
||||
`,
|
||||
);
|
||||
|
||||
expect(inspect(repo).preview).toMatchObject({
|
||||
scripts: {
|
||||
dev: {
|
||||
type: "service",
|
||||
command: `npm run dev '--hmr-port='"$((PASEO_PORT + 1))"`,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("does not import scripts available only in Conductor cloud", () => {
|
||||
const repo = makeRepo();
|
||||
writeSharedToml(
|
||||
|
||||
@@ -113,15 +113,15 @@ function discoverConductorSources(
|
||||
const worktreeIncludePath = join(repoRoot, ".worktreeinclude");
|
||||
const files: SourceFile[] = [];
|
||||
|
||||
if (!existsSync(sharedTomlPath) && existsSync(rootLegacyPath)) {
|
||||
files.push(readSourceFile(repoRoot, rootLegacyPath, "legacy", true, source));
|
||||
}
|
||||
if (existsSync(sharedJsonPath)) {
|
||||
files.push(readSourceFile(repoRoot, sharedJsonPath, "shared", true, source));
|
||||
}
|
||||
if (existsSync(sharedTomlPath)) {
|
||||
files.push(readSourceFile(repoRoot, sharedTomlPath, "shared", true, source));
|
||||
}
|
||||
if (!existsSync(sharedJsonPath) && !existsSync(sharedTomlPath) && existsSync(rootLegacyPath)) {
|
||||
files.push(readSourceFile(repoRoot, rootLegacyPath, "legacy", true, source));
|
||||
}
|
||||
if (existsSync(localJsonPath)) {
|
||||
files.push(readSourceFile(repoRoot, localJsonPath, "local", true, source));
|
||||
}
|
||||
@@ -516,12 +516,34 @@ function rewriteVariables(
|
||||
|
||||
function replaceShellVariable(command: string, from: string, to: string): string {
|
||||
const pattern = new RegExp(`\\$\\{${from}(?=[}:#%+\\-=?])|\\$${from}(?![A-Za-z0-9_])`, "g");
|
||||
return command.replace(pattern, (match) => (match.startsWith("${") ? `\${${to}` : `$${to}`));
|
||||
return replaceArithmeticVariable(
|
||||
command.replace(pattern, (match) => (match.startsWith("${") ? `\${${to}` : `$${to}`)),
|
||||
from,
|
||||
to,
|
||||
);
|
||||
}
|
||||
|
||||
function containsShellVariable(command: string, name: string): boolean {
|
||||
const pattern = new RegExp(`\\$\\{${name}(?=[}:#%+\\-=?])|\\$${name}(?![A-Za-z0-9_])`);
|
||||
return pattern.test(command);
|
||||
return pattern.test(command) || containsArithmeticVariable(command, name);
|
||||
}
|
||||
|
||||
function replaceArithmeticVariable(command: string, from: string, to: string): string {
|
||||
return command.replace(/\$\(\(([\s\S]*?)\)\)/g, (expression, body: string) => {
|
||||
const identifier = new RegExp(`(^|[^A-Za-z0-9_])${from}(?![A-Za-z0-9_])`, "g");
|
||||
const rewrittenBody = body.replace(identifier, (_match, prefix: string) => `${prefix}${to}`);
|
||||
return rewrittenBody === body ? expression : `$((` + rewrittenBody + `))`;
|
||||
});
|
||||
}
|
||||
|
||||
function containsArithmeticVariable(command: string, name: string): boolean {
|
||||
const identifier = new RegExp(`(^|[^A-Za-z0-9_])${name}(?![A-Za-z0-9_])`);
|
||||
for (const match of command.matchAll(/\$\(\(([\s\S]*?)\)\)/g)) {
|
||||
if (identifier.test(match[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function appendArgs(command: string, args: string[]): string {
|
||||
@@ -565,7 +587,7 @@ function normalizeAvailableIn(value: unknown): string | string[] | undefined {
|
||||
|
||||
function shellQuoteArgument(value: string): string {
|
||||
const variablePattern =
|
||||
/\$(?:\{[A-Za-z_][A-Za-z0-9_]*(?:(?:[^{}])|\{[^{}]*\})*\}|[A-Za-z_][A-Za-z0-9_]*)/g;
|
||||
/\$\(\([\s\S]*?\)\)|\$(?:\{[A-Za-z_][A-Za-z0-9_]*(?:(?:[^{}])|\{[^{}]*\})*\}|[A-Za-z_][A-Za-z0-9_]*)/g;
|
||||
const parts: string[] = [];
|
||||
let offset = 0;
|
||||
for (const match of value.matchAll(variablePattern)) {
|
||||
|
||||
Reference in New Issue
Block a user