Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions news/changelog-1.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ All changes included in 1.7:
- ([#11509](https://github.com/quarto-dev/quarto-cli/issues/11509)): Fix link-decoration regression in HTML formats.
- ([#11532](https://github.com/quarto-dev/quarto-cli/issues/11532)): Fix regression for [#660](https://github.com/quarto-dev/quarto-cli/issues/660), which causes files to have incorrect permissions when Quarto is installed in a location not writable by the current user.
- ([#11580](https://github.com/quarto-dev/quarto-cli/issues/11580)): Fix regression with documents containing `categories` fields that are not strings.

## `quarto check`

- ([#11608](https://github.com/quarto-dev/quarto-cli/pull/11608)): Do not issue error message when calling `quarto check info`.
24 changes: 15 additions & 9 deletions src/command/check/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,20 @@ import { notebookContext } from "../../render/notebook/notebook-context.ts";
import { typstBinaryPath } from "../../core/typst.ts";
import { quartoCacheDir } from "../../core/appdirs.ts";
import { isWindows } from "../../deno_ral/platform.ts";
import { makeStringEnumTypeEnforcer } from "../../typing/dynamic.ts";

const kIndent = " ";
export const kTargets = [
"install",
"info",
"jupyter",
"knitr",
"versions",
"all",
] as const;
export type Target = typeof kTargets[number];
export const enforceTargetType = makeStringEnumTypeEnforcer(...kTargets);

export type Target =
| "install"
| "jupyter"
| "knitr"
| "versions"
| "info"
| "all";
const kIndent = " ";

export async function check(target: Target): Promise<void> {
const services = renderServices(notebookContext());
Expand All @@ -82,7 +86,9 @@ export async function check(target: Target): Promise<void> {
}
}

// Currently this doesn't check anything, but
// Currently this doesn't check anything
// but it's a placeholder for future checks
// and the message is useful for troubleshooting
async function checkInfo(_services: RenderServices) {
const cacheDir = quartoCacheDir();
completeMessage("Checking environment information...");
Expand Down
18 changes: 4 additions & 14 deletions src/command/check/cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@
* Copyright (C) 2021-2022 Posit Software, PBC
*/

import { error } from "../../deno_ral/log.ts";

import { Command } from "cliffy/command/mod.ts";
import { check, Target } from "./check.ts";

const kTargets = ["install", "jupyter", "knitr", "versions", "all"];
import { check, enforceTargetType } from "./check.ts";

export const checkCommand = new Command()
.name("check")
Expand All @@ -22,13 +18,7 @@ export const checkCommand = new Command()
.example("Check Jupyter engine", "quarto check jupyter")
.example("Check Knitr engine", "quarto check knitr")
.example("Check installation and all engines", "quarto check all")
.action(async (_options: unknown, target?: string) => {
target = target || "all";
if (!kTargets.includes(target)) {
error(
"Invalid target '" + target + "' (valid targets are " +
kTargets.join(", ") + ").",
);
}
await check(target as Target);
.action(async (_options: unknown, targetStr?: string) => {
targetStr = targetStr || "all";
await check(enforceTargetType(targetStr));
});
86 changes: 45 additions & 41 deletions src/command/create/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import {
import { basename, dirname, join } from "../../deno_ral/path.ts";
import { existsSync } from "../../deno_ral/fs.ts";
import { isMac, isWindows } from "../../deno_ral/platform.ts";
import {
enforcer,
makeStringEnumTypeFunctions,
objectPredicate,
stringTypePredicate,
} from "../../typing/dynamic.ts";

export interface Editor {
// A short, command line friendly id
Expand Down Expand Up @@ -77,11 +83,14 @@ interface EditorInfo {
open: (path: string, createResult: CreateResult) => () => Promise<void>;
}

interface ScanAction {
action: "path" | "which" | "env";
const scanActionActions = ["path", "which", "env"] as const;
type ScanActionAction = typeof scanActionActions[number];

type ScanAction = {
action: ScanActionAction;
arg: string;
filter?: (path: string) => string;
}
};

function vscodeEditorInfo(): EditorInfo {
const editorInfo: EditorInfo = {
Expand Down Expand Up @@ -110,29 +119,26 @@ function vscodeEditorInfo(): EditorInfo {
action: "which",
arg: "code.exe",
});
const pathActions = windowsAppPaths("Microsoft VS Code", "code.exe").map(
(path) => {
return {
action: "path",
arg: path,
} as ScanAction;
},
);
const pathActions: ScanAction[] = windowsAppPaths(
"Microsoft VS Code",
"code.exe",
).map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...pathActions);
} else if (isMac) {
editorInfo.actions.push({
action: "which",
arg: "code",
});

const pathActions = macosAppPaths(
const pathActions: ScanAction[] = macosAppPaths(
"Visual Studio Code.app/Contents/Resources/app/bin/code",
).map((path) => {
return {
action: "path",
arg: path,
} as ScanAction;
});
).map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...pathActions);
} else {
editorInfo.actions.push({
Expand Down Expand Up @@ -174,13 +180,14 @@ function positronEditorInfo(): EditorInfo {
action: "which",
arg: "Positron.exe",
});
const pathActions = windowsAppPaths("Positron", "Positron.exe").map(
(path) => {
return {
action: "path",
arg: path,
} as ScanAction;
},
const pathActions: ScanAction[] = windowsAppPaths(
"Positron",
"Positron.exe",
).map(
(path) => ({
action: "path",
arg: path,
}),
);
editorInfo.actions.push(...pathActions);
} else if (isMac) {
Expand All @@ -189,13 +196,13 @@ function positronEditorInfo(): EditorInfo {
arg: "positron",
});

const pathActions = macosAppPaths(
const pathActions: ScanAction[] = macosAppPaths(
"Positron.app/Contents/Resources/app/bin/code",
).map((path) => {
return {
action: "path",
arg: path,
} as ScanAction;
};
});
editorInfo.actions.push(...pathActions);
} else {
Expand Down Expand Up @@ -249,22 +256,19 @@ function rstudioEditorInfo(): EditorInfo {
},
});

const paths = windowsAppPaths(join("RStudio", "bin"), rstudioExe).map(
(path) => {
return {
action: "path",
arg: path,
} as ScanAction;
},
);
const paths: ScanAction[] = windowsAppPaths(
join("RStudio", "bin"),
rstudioExe,
).map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...paths);
} else if (isMac) {
const paths = macosAppPaths("RStudio.app").map((path) => {
return {
action: "path",
arg: path,
} as ScanAction;
});
const paths: ScanAction[] = macosAppPaths("RStudio.app").map((path) => ({
action: "path",
arg: path,
}));
editorInfo.actions.push(...paths);
} else {
editorInfo.actions.push({
Expand Down
16 changes: 16 additions & 0 deletions src/core/lib/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
* Copyright (C) 2020-2022 Posit Software, PBC
*/

export class DynamicTypeCheckError extends Error {
constructor(
message: string,
printName = true,
printStack = true,
) {
super(message);
this.name = "Dynamic Type-Checking Error";
this.printName = printName;
this.printStack = printStack;
}

public readonly printName: boolean;
public readonly printStack: boolean;
}

export class InternalError extends Error {
constructor(
message: string,
Expand Down
72 changes: 72 additions & 0 deletions src/typing/dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* dynamic.ts
*
* Tools for managing the interface between dynamic and static typing
* in Quarto.
*
* Ideally, every usage of `any` or `as` would appear in this file.
*
* Copyright (C) 2024 Posit Software, PBC
*/

import { DynamicTypeCheckError } from "../core/lib/error.ts";

export const makeStringEnumTypeFunctions = <T extends string>(
...values: T[]
): {
predicate: (value: unknown) => value is T;
enforce: (value: unknown) => T;
} => {
const valueSet: Set<string> = new Set(values);
const predicate = (value: unknown): value is T => {
return typeof value === "string" && valueSet.has(value);
};
const enforce = (value: unknown): T => {
if (predicate(value)) {
return value;
}
throw new DynamicTypeCheckError(
"Invalid value '" + value + "' (valid values are " +
values.join(", ") + ").",
);
};
return { predicate, enforce };
};

export const makeStringEnumTypeEnforcer = <T extends string>(
...values: T[]
): (value: unknown) => T => {
return makeStringEnumTypeFunctions(...values).enforce;
};

export const enforcer = <T>(
predicate: (value: unknown) => value is T,
msg?: (value: unknown) => string,
) => {
if (!msg) {
msg = (_value: unknown) => "Invalid value.";
}
return (value: unknown): T => {
if (predicate(value)) {
return value;
}
throw new DynamicTypeCheckError(msg(value));
};
};

export const enforceStringType = (value: unknown): string => {
if (stringTypePredicate(value)) {
return value;
}
throw new DynamicTypeCheckError("Expected a string.");
};

export const stringTypePredicate = (value: unknown): value is string => {
return typeof value === "string";
};

export const objectPredicate = (
value: unknown,
): value is Record<string, unknown> => {
return typeof value === "object" && value !== null;
};
19 changes: 19 additions & 0 deletions tests/unit/typing/dynamic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* dynamic.test.ts
*
* Copyright (C) 2024 Posit Software, PBC
*
*/

import { makeStringEnumTypeEnforcer } from "../../../src/typing/dynamic.ts";
import { unitTest } from "../../test.ts";
import { assert, assertThrows } from "testing/asserts";

// deno-lint-ignore require-await
unitTest("makeStringEnumTypeEnforcer", async () => {
const check = makeStringEnumTypeEnforcer("a", "b", "c");
assert(check("a") === "a");
assert(check("b") === "b");
assert(check("c") === "c");
assertThrows(() => check("d"), Error, "Invalid value 'd' (valid values are a, b, c).");
});
Loading