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
24 changes: 24 additions & 0 deletions packages/visual-editor/THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,30 @@ Exhibit B - “Incompatible With Secondary Licenses” Notice

-----------

The following npm package may be included in this product:

- picocolors@1.1.1

This package contains the following license:

ISC License

Copyright (c) 2021-2024 Oleksii Raspopov, Kostiantyn Denysov, Anton Verinov

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

-----------

The following npm package may be included in this product:

- lucide-react@0.414.0
Expand Down
7 changes: 6 additions & 1 deletion packages/visual-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"files": [
"dist"
],
"bin": {
"yextve": "dist/cli/yextve.js"
},
"module": "dist/visual-editor.js",
"exports": {
".": {
Expand Down Expand Up @@ -45,7 +48,8 @@
"scripts": {
"build:components": "tsc && vite build",
"build:plugin": "rm -rf dist/plugin && tsc -p tsconfig.plugin.json --emitDeclarationOnly && vite build --config vite.config.plugin.ts",
"build": "pnpm run build:components && pnpm run build:plugin",
"build:cli": "tsup src/cli/yextve.ts --format esm --platform node --target node20 --out-dir dist/cli",
"build": "pnpm run build:components && pnpm run build:plugin && pnpm run build:cli",
"export-section-library-directory-locator": "tsx scripts/exportDirectoryLocatorSectionLibrary.ts",
"convert-templates-to-section-library": "tsx scripts/convertTemplatesToSectionLibrary.ts",
"test": "pnpm run test:editor && pnpm run test:components",
Expand Down Expand Up @@ -98,6 +102,7 @@
"lucide-react": "^0.414.0",
"lz-string": "1.5.0",
"next-themes": "^0.3.0",
"picocolors": "^1.1.1",
"pure-react-carousel": "^1.32.0",
"react-collapsed": "^4.1.2",
"react-color": "^2.19.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type EntityLayoutMetadata,
type LibraryMetadata,
type PageSetType,
} from "../src/sectionLibrary.ts";
} from "../src/types/sectionLibrary.ts";
import { exportDirectoryLocatorSectionLibrary } from "./exportDirectoryLocatorSectionLibrary.ts";

const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,18 +394,18 @@ const writeComponentRegistry = (
'import { DropZone, type Config } from "@puckeditor/core";',
'import { resolveDirectoryRootProps } from "@yext/visual-editor/section-library-support";',
"",
'const rootStyle = { display: "flex", flexDirection: "column", minHeight: "100vh" } as const;',
"",
"// The Puck Root configuration for directory page sets",
'export const directoryRootConfig: NonNullable<Config["root"]> = {',
" resolveData: (data: any, params: any) => ({",
" ...data,",
" props: resolveDirectoryRootProps(data.props ?? {}, params.metadata?.streamDocument ?? {}),",
" }),",
' render: () => <DropZone zone="default-zone" style={rootStyle} disallow={[]}/>,',
' render: () => <DropZone zone="default-zone" style={{ display: "flex", flexDirection: "column", minHeight: "100vh" }} disallow={[]}/>,',
"};",
"",
"// The Puck Root configuration for locator page sets",
'export const locatorRootConfig: NonNullable<Config["root"]> = {',
' render: () => <DropZone zone="default-zone" style={rootStyle} disallow={[]}/>,',
' render: () => <DropZone zone="default-zone" style={{ display: "flex", flexDirection: "column", minHeight: "100vh" }} disallow={[]}/>,',
"};",
"",
].join("\n")
Expand Down
53 changes: 53 additions & 0 deletions packages/visual-editor/src/cli/commands/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { parseArgs } from "node:util";
import {
createValidationContext,
validateSectionLibrary,
} from "../../internal/sectionLibraryValidation/validateSectionLibrary.ts";
import type { ValidationStage } from "../../internal/sectionLibraryValidation/types.ts";
import { renderValidationResult } from "../output.ts";

type ValidateCommandIo = {
stdout: Pick<NodeJS.WriteStream, "write" | "isTTY">;
};

export const runValidateCommand = (
args: string[],
io: ValidateCommandIo,
rootDir: string
): number => {
const { values } = parseArgs({
args,
strict: true,
allowPositionals: false,
options: {
yextCI: { type: "boolean" },
Comment thread
benlife5 marked this conversation as resolved.
"skip-api-check": { type: "boolean" },
"skip-repo-structure-check": { type: "boolean" },
"skip-code-check": { type: "boolean" },
},
});

const skippedStages = new Set<ValidationStage>();
if (values["skip-api-check"]) {
skippedStages.add("api");
}
if (values["skip-repo-structure-check"]) {
skippedStages.add("structure");
}
if (values["skip-code-check"]) {
skippedStages.add("code");
}

const context = createValidationContext(rootDir, {
yextCI: values.yextCI,
skippedStages,
});

const result = validateSectionLibrary(context);

io.stdout.write(
renderValidationResult(result, !!io.stdout.isTTY && !context.yextCI)
);

return result.issues.length === 0 ? 0 : 1;
};
52 changes: 52 additions & 0 deletions packages/visual-editor/src/cli/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pc from "picocolors";
import { formatValidationIssue } from "../internal/sectionLibraryValidation/validationError.ts";
import type {
ValidationResult,
ValidationStage,
} from "../internal/sectionLibraryValidation/types.ts";

const stages: { stage: ValidationStage; label: string }[] = [
{ stage: "api", label: "Library metadata" },
{ stage: "structure", label: "Repository structure" },
{ stage: "code", label: "Code checks" },
];

export const renderValidationResult = (
result: ValidationResult,
colorEnabled: boolean
): string => {
const colors = pc.createColors(colorEnabled && pc.isColorSupported);
const lines: string[] = [];

for (const { stage, label } of stages) {
if (result.context.skippedStages.has(stage)) {
lines.push(`${label}: ${colors.yellow("skipped")}`);
continue;
}

const issues = result.issues.filter((issue) => issue.category === stage);

if (issues.length === 0) {
lines.push(`${label}: ${colors.green("passed")}`);
continue;
}

lines.push(
`${label}: ${colors.red(
`failed (${issues.length} ${issues.length === 1 ? "error" : "errors"})`
)}`
);

lines.push(...issues.map((issue) => ` ${formatValidationIssue(issue)}`));
}

lines.push("");
lines.push(
result.issues.length === 0
? colors.green("Validation passed. 0 errors.")
: colors.red(
`Validation failed. ${result.issues.length} ${result.issues.length === 1 ? "error" : "errors"}.`
)
);
return `${lines.join("\n")}\n`;
};
92 changes: 92 additions & 0 deletions packages/visual-editor/src/cli/yextve.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import path from "node:path";
import fs from "fs-extra";
import { describe, expect, it } from "vitest";
import { createTempRoot } from "../internal/sectionLibraryValidation/testUtils.ts";
import { runCli } from "./yextve.ts";
import packageJson from "../../package.json" with { type: "json" };

describe("yextve", () => {
it("prints help and version", () => {
const help = invoke(["--help"]);
expect(help.exitCode).toBe(0);
expect(help.stdout).toContain(
"npx --package=@yext/visual-editor@latest yextve validate"
);
expect(help.stdout).not.toContain("scripts");
});

it("prints version", () => {
const version = invoke(["--version"]);
expect(version).toEqual({
exitCode: 0,
stdout: `${packageJson.version}\n`,
stderr: "",
});
});

it.each([
{ args: [] },
{ args: ["unknown"] },
{ args: ["validate", "project-path"] },
{ args: ["validate", "--unknown"] },
])("returns usage exit 2 for invalid arguments: $args", ({ args }) => {
const result = invoke(args);
expect(result.exitCode).toBe(2);
expect(result.stderr).toContain("Usage:");
});

it("renders all skipped stages and succeeds", () => {
const result = invoke([
"validate",
"--skip-api-check",
"--skip-repo-structure-check",
"--skip-code-check",
]);

expect(result.exitCode).toBe(0);
expect(result.stdout.match(/skipped/g)).toHaveLength(3);
expect(result.stdout).toContain("Validation passed. 0 errors.");
expect(result.stdout).not.toContain("\u001b[");
});

it("skips only API validation in Yext CI", () => {
const rootDir = createTempRoot();
fs.outputFileSync(
path.join(rootDir, "src", "library", "Unsafe.ts"),
"eval(code);"
);

const result = invoke(["validate", "--yextCI"], rootDir);

expect(result.exitCode).toBe(1);
expect(result.stdout).toContain("Library metadata: skipped");
expect(result.stdout).toContain("Repository structure: failed");
expect(result.stdout).toContain("Code checks: failed");
});
});

// invoke calls the cli and records stdout and stderr
const invoke = (args: string[], rootDir: string = createTempRoot()) => {
let stdout = "";
let stderr = "";
const exitCode = runCli(
args,
{
stdout: {
isTTY: false,
write: (value) => {
stdout += value;
return true;
},
},
stderr: {
write: (value) => {
stderr += value;
return true;
},
},
},
rootDir
);
return { exitCode, stdout, stderr };
};
90 changes: 90 additions & 0 deletions packages/visual-editor/src/cli/yextve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env node
import fs from "node:fs";
import { fileURLToPath } from "node:url";
import packageJson from "../../package.json" with { type: "json" };
import { runValidateCommand } from "./commands/validate.ts";

const usage = `Usage:
yextve validate [--skip-api-check] [--skip-repo-structure-check] [--skip-code-check]
yextve --help
yextve --version

Validate the Section Library in the current working directory.

Options:
--skip-api-check Skip library.json metadata validation.
--skip-repo-structure-check Skip repository structure validation.
--skip-code-check Skip import and code-safety validation.
--help Show this help.
--version Show the package version.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Examples:
npx --package=@yext/visual-editor@latest yextve validate
`;

type CliIo = {
stdout: Pick<NodeJS.WriteStream, "write" | "isTTY">;
stderr: Pick<NodeJS.WriteStream, "write">;
};

export const runCli = (
args: string[],
io: CliIo = { stdout: process.stdout, stderr: process.stderr },
rootDir: string = process.cwd()
): number => {
try {
// handle help
if (args.length === 1 && args[0] === "--help") {
io.stdout.write(usage);
return 0;
}

// handle invalid help calls
if (args.slice(1).includes("--help")) {
if (args.length === 2) {
io.stdout.write(usage);
return 0;
}
io.stderr.write(usage);
return 2;
}

// handle version
if (args.length === 1 && args[0] === "--version") {
io.stdout.write(`${packageJson.version}\n`);
return 0;
}

// handle invalid args
if (args[0] !== "validate") {
io.stderr.write(usage);
return 2;
}

// run command
return runValidateCommand(args.slice(1), io, rootDir);
} catch (error) {
if (
error instanceof TypeError &&
(error as TypeError & { code?: string }).code?.startsWith(
"ERR_PARSE_ARGS"
)
) {
io.stderr.write(`${error.message}\n\n${usage}`);
return 2;
}

io.stderr.write(
`Validation could not be completed: ${error instanceof Error ? error.message : String(error)}\n`
);
return 2;
}
};

if (
process.argv[1] &&
fs.realpathSync(process.argv[1]) ===
fs.realpathSync(fileURLToPath(import.meta.url))
) {
process.exitCode = runCli(process.argv.slice(2));
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/visual-editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ export * from "./types/index.ts";
export * from "./editor/index.ts";
export * from "./components/index.ts";
export * from "./fields/index.ts";
export type { SectionConfig } from "./sectionLibrary.ts";
export type { SectionConfig } from "./types/sectionLibrary.ts";
export { LocalEditorShell } from "./local-editor/LocalEditorShell.tsx";
Loading
Loading