From 7587849edc1dc67af537be79d095fb28fba43ef9 Mon Sep 17 00:00:00 2001 From: Timothy Leung Date: Wed, 11 Dec 2024 13:57:16 +0000 Subject: [PATCH 1/6] Validate site or widget config --- .../cli/src/commands/site/deploy/index.ts | 3 +- packages/cli/src/commands/site/index.ts | 3 +- .../site/logSiteCommandConfigFileOverride.ts | 2 +- packages/cli/src/util/configLoader.ts | 9 +- .../foundry-config-json/src/config.test.ts | 56 ++++++++--- packages/foundry-config-json/src/config.ts | 92 ++++++++++++++----- 6 files changed, 123 insertions(+), 42 deletions(-) diff --git a/packages/cli/src/commands/site/deploy/index.ts b/packages/cli/src/commands/site/deploy/index.ts index 1fff141cc..0cc4836b7 100644 --- a/packages/cli/src/commands/site/deploy/index.ts +++ b/packages/cli/src/commands/site/deploy/index.ts @@ -33,7 +33,8 @@ const command: CommandModule< command: "deploy", describe: "Deploy a new site version", builder: async (argv) => { - const config: LoadedFoundryConfig | undefined = await configLoader(); + const config: LoadedFoundryConfig<"site"> | undefined = + await configLoader(); const siteConfig: SiteConfig | undefined = config?.foundryConfig.site; const directory = siteConfig?.directory; const autoVersion = siteConfig?.autoVersion; diff --git a/packages/cli/src/commands/site/index.ts b/packages/cli/src/commands/site/index.ts index b76d6e9db..fc09182d2 100644 --- a/packages/cli/src/commands/site/index.ts +++ b/packages/cli/src/commands/site/index.ts @@ -29,7 +29,8 @@ const command: CommandModule = { command: "site", describe: "Manage your site", builder: async (argv) => { - const config: LoadedFoundryConfig | undefined = await configLoader(); + const config: LoadedFoundryConfig<"site"> | undefined = + await configLoader(); const application = config?.foundryConfig.site.application; const foundryUrl = config?.foundryConfig.foundryUrl; return argv diff --git a/packages/cli/src/commands/site/logSiteCommandConfigFileOverride.ts b/packages/cli/src/commands/site/logSiteCommandConfigFileOverride.ts index 9da03128b..6293a669c 100644 --- a/packages/cli/src/commands/site/logSiteCommandConfigFileOverride.ts +++ b/packages/cli/src/commands/site/logSiteCommandConfigFileOverride.ts @@ -21,7 +21,7 @@ import type { CommonSiteArgs } from "./CommonSiteArgs.js"; export async function logSiteCommandConfigFileOverride( args: Arguments, - config: FoundryConfig | undefined, + config: FoundryConfig<"site"> | undefined, ) { if ( config?.site.application != null diff --git a/packages/cli/src/util/configLoader.ts b/packages/cli/src/util/configLoader.ts index e9781af7f..cf768cd96 100644 --- a/packages/cli/src/util/configLoader.ts +++ b/packages/cli/src/util/configLoader.ts @@ -18,12 +18,13 @@ import { ExitProcessError } from "@osdk/cli.common"; import type { LoadedFoundryConfig } from "@osdk/foundry-config-json"; import { loadFoundryConfig } from "@osdk/foundry-config-json"; -let configPromise: Promise | undefined = - undefined; +let configPromise: + | Promise | undefined> + | undefined = undefined; -function getConfig(): Promise { +function getConfig(): Promise | undefined> { if (configPromise == null) { - configPromise = loadFoundryConfig().catch((e) => { + configPromise = loadFoundryConfig("site").catch((e) => { throw new ExitProcessError(2, e instanceof Error ? e.message : undefined); }); } diff --git a/packages/foundry-config-json/src/config.test.ts b/packages/foundry-config-json/src/config.test.ts index 8eb80e9a3..d625ff734 100644 --- a/packages/foundry-config-json/src/config.test.ts +++ b/packages/foundry-config-json/src/config.test.ts @@ -24,7 +24,7 @@ vi.mock("find-up"); vi.mock("node:fs"); vi.mock("node:path"); -describe("loadFoundryConfig", () => { +describe("loadFoundryConfig - site", () => { beforeEach(() => { vi.mocked(findUp).mockResolvedValue("/path/foundry.config.json"); vi.mocked(extname).mockReturnValue(".json"); @@ -46,7 +46,7 @@ describe("loadFoundryConfig", () => { vi.mocked(fsPromises.readFile).mockResolvedValue( JSON.stringify(correctConfig), ); - await expect(loadFoundryConfig()).resolves.toEqual({ + await expect(loadFoundryConfig("site")).resolves.toEqual({ configFilePath: "/path/foundry.config.json", foundryConfig: { ...correctConfig, @@ -66,7 +66,7 @@ describe("loadFoundryConfig", () => { vi.mocked(fsPromises.readFile).mockResolvedValue( JSON.stringify(correctConfig), ); - await expect(loadFoundryConfig()).resolves.toEqual({ + await expect(loadFoundryConfig("site")).resolves.toEqual({ configFilePath: "/path/foundry.config.json", foundryConfig: { ...correctConfig, @@ -89,7 +89,7 @@ describe("loadFoundryConfig", () => { vi.mocked(fsPromises.readFile).mockResolvedValue( JSON.stringify(correctConfig), ); - await expect(loadFoundryConfig()).resolves.toEqual({ + await expect(loadFoundryConfig("site")).resolves.toEqual({ configFilePath: "/path/foundry.config.json", foundryConfig: { ...correctConfig, @@ -113,7 +113,7 @@ describe("loadFoundryConfig", () => { JSON.stringify(inCorrectConfig), ); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "The configuration file does not match", ); }); @@ -132,7 +132,7 @@ describe("loadFoundryConfig", () => { JSON.stringify(inCorrectConfig), ); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "The configuration file does not match the expected schema: data/site/autoVersion must match exactly one schema in oneOf", ); }); @@ -151,7 +151,7 @@ describe("loadFoundryConfig", () => { JSON.stringify(inCorrectConfig), ); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "The configuration file does not match the expected schema: data/site/uploadOnly must be boolean", ); }); @@ -159,7 +159,7 @@ describe("loadFoundryConfig", () => { it("should throw an error if the configuration file cannot be read", async () => { vi.mocked(fsPromises.readFile).mockRejectedValue(new Error("Read error")); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "Couldn't read or parse config", ); }); @@ -173,7 +173,7 @@ describe("loadFoundryConfig", () => { JSON.stringify(fakeConfig), ); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "The configuration file does not match", ); }); @@ -190,7 +190,7 @@ describe("loadFoundryConfig", () => { JSON.stringify(fakeConfig), ); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "The configuration file does not match", ); }); @@ -208,20 +208,50 @@ describe("loadFoundryConfig", () => { JSON.stringify(fakeConfig), ); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "The configuration file does not match", ); }); it("should return undefined if the configuration file is not found", async () => { vi.mocked(findUp).mockResolvedValue(undefined); - await expect(loadFoundryConfig()).resolves.toBeUndefined(); + await expect(loadFoundryConfig("site")).resolves.toBeUndefined(); }); it("should throw if config file extension isn't supported", async () => { vi.mocked(extname).mockResolvedValue(".yaml"); - await expect(loadFoundryConfig()).rejects.toThrow( + await expect(loadFoundryConfig("site")).rejects.toThrow( "Unsupported file extension:", ); }); }); + +describe("loadFoundryConfig - widget", () => { + beforeEach(() => { + vi.mocked(findUp).mockResolvedValue("/path/foundry.config.json"); + vi.mocked(extname).mockReturnValue(".json"); + }); + + it("should load and parse the configuration file correctly", async () => { + const correctConfig = { + foundryUrl: "http://localhost", + widget: { + rid: "test-rid", + directory: "/test/directory", + autoVersion: { + type: "git-describe", + }, + }, + }; + + vi.mocked(fsPromises.readFile).mockResolvedValue( + JSON.stringify(correctConfig), + ); + await expect(loadFoundryConfig("widget")).resolves.toEqual({ + configFilePath: "/path/foundry.config.json", + foundryConfig: { + ...correctConfig, + }, + }); + }); +}); diff --git a/packages/foundry-config-json/src/config.ts b/packages/foundry-config-json/src/config.ts index a66d9e7ad..ff4940b75 100644 --- a/packages/foundry-config-json/src/config.ts +++ b/packages/foundry-config-json/src/config.ts @@ -18,17 +18,26 @@ import type { JSONSchemaType } from "ajv"; import { promises as fsPromises } from "node:fs"; import { extname } from "node:path"; -export interface GitDescribeAutoVersionConfig { - type: "git-describe"; - tagPrefix?: string; +export interface LoadedFoundryConfig { + foundryConfig: FoundryConfig; + configFilePath: string; } -export interface PackageJsonAutoVersionConfig { - type: "package-json"; + +export type FoundryConfig = T extends "site" + ? FoundrySiteConfig + : T extends "widget" ? FoundryWidgetConfig + : never; + +export interface FoundrySiteConfig { + foundryUrl: string; + site: SiteConfig; } -export type AutoVersionConfig = - | GitDescribeAutoVersionConfig - | PackageJsonAutoVersionConfig; -export type AutoVersionConfigType = AutoVersionConfig["type"]; + +export interface FoundryWidgetConfig { + foundryUrl: string; + widget: WidgetConfig; +} + export interface SiteConfig { application: string; directory: string; @@ -36,21 +45,31 @@ export interface SiteConfig { uploadOnly?: boolean; } -export interface FoundryConfig { - foundryUrl: string; - site: SiteConfig; +export interface WidgetConfig { + rid: string; + directory: string; + autoVersion?: AutoVersionConfig; } -export interface LoadedFoundryConfig { - foundryConfig: FoundryConfig; - configFilePath: string; +export type AutoVersionConfig = + | GitDescribeAutoVersionConfig + | PackageJsonAutoVersionConfig; +export type AutoVersionConfigType = AutoVersionConfig["type"]; + +export interface GitDescribeAutoVersionConfig { + type: "git-describe"; + tagPrefix?: string; +} + +export interface PackageJsonAutoVersionConfig { + type: "package-json"; } const CONFIG_FILE_NAMES: string[] = [ "foundry.config.json", ]; -const CONFIG_FILE_SCHEMA: JSONSchemaType = { +const FOUNDRY_SITE_CONFIG_SCHEMA = { type: "object", properties: { foundryUrl: { type: "string" }, @@ -84,6 +103,33 @@ const CONFIG_FILE_SCHEMA: JSONSchemaType = { }, required: ["foundryUrl", "site"], additionalProperties: false, +} satisfies JSONSchemaType>; + +const FOUNDRY_WIDGET_CONFIG_SCHEMA = { + type: "object", + properties: { + foundryUrl: { type: "string" }, + widget: { + type: "object", + properties: { + rid: { type: "string" }, + directory: { type: "string" }, + autoVersion: + FOUNDRY_SITE_CONFIG_SCHEMA.properties.site.properties.autoVersion, + uploadOnly: { type: "boolean", nullable: true }, + }, + required: ["rid", "directory"], + }, + }, + required: ["foundryUrl", "widget"], + additionalProperties: false, +} satisfies JSONSchemaType>; + +const FOUNDRY_CONFIG_SCHEMA: { + [P in "site" | "widget"]: JSONSchemaType>; +} = { + site: FOUNDRY_SITE_CONFIG_SCHEMA, + widget: FOUNDRY_WIDGET_CONFIG_SCHEMA, }; /** @@ -91,19 +137,21 @@ const CONFIG_FILE_SCHEMA: JSONSchemaType = { * @returns A promise that resolves to the configuration JSON object, or undefined if not found. * @throws Will throw an error if the configuration file is found but cannot be read or parsed. */ -export async function loadFoundryConfig(): Promise< - LoadedFoundryConfig | undefined +export async function loadFoundryConfig( + type: "site" | "widget", +): Promise< + LoadedFoundryConfig | undefined > { const ajvModule = await import("ajv"); const Ajv = ajvModule.default.default; // https://github.com/ajv-validator/ajv/issues/2132 const ajv = new Ajv({ allErrors: true }); - const validate = ajv.compile(CONFIG_FILE_SCHEMA); + const validate = ajv.compile(FOUNDRY_CONFIG_SCHEMA[type]); const { findUp } = await import("find-up"); const configFilePath = await findUp(CONFIG_FILE_NAMES); if (configFilePath) { - let foundryConfig: FoundryConfig; + let foundryConfig: FoundryConfig; try { const fileContent = await fsPromises.readFile(configFilePath, "utf-8"); foundryConfig = parseConfigFile(fileContent, configFilePath); @@ -127,10 +175,10 @@ export async function loadFoundryConfig(): Promise< return undefined; } -function parseConfigFile( +function parseConfigFile( fileContent: string, configFilePath: string, -): FoundryConfig { +): FoundryConfig { const extension = extname(configFilePath); switch (extension) { case ".json": From cd2d84e760bc95233e1f9cc8296d55122cf2b0a2 Mon Sep 17 00:00:00 2001 From: Timothy Leung Date: Wed, 11 Dec 2024 14:14:13 +0000 Subject: [PATCH 2/6] Use shared auto version for vite manifest plugin --- .../foundry.config.json | 3 +- packages/example-generator/src/run.ts | 51 +++++++++++---- packages/foundry-config-json/src/config.ts | 10 ++- packages/widget.vite-plugin/package.json | 1 + packages/widget.vite-plugin/src/plugin.ts | 62 +++++++------------ pnpm-lock.yaml | 3 + 6 files changed, 73 insertions(+), 57 deletions(-) diff --git a/examples/example-widget-react-sdk-2.x/foundry.config.json b/examples/example-widget-react-sdk-2.x/foundry.config.json index ddc6de5f3..57d7cd568 100644 --- a/examples/example-widget-react-sdk-2.x/foundry.config.json +++ b/examples/example-widget-react-sdk-2.x/foundry.config.json @@ -4,8 +4,7 @@ "rid": "ri.viewregistry.main.view.fake", "directory": "./dist", "autoVersion": { - "type": "git-describe", - "tagPrefix": "" + "type": "package-json" } } } diff --git a/packages/example-generator/src/run.ts b/packages/example-generator/src/run.ts index 243fe86ae..4e24c9cc3 100644 --- a/packages/example-generator/src/run.ts +++ b/packages/example-generator/src/run.ts @@ -163,7 +163,10 @@ async function fixMonorepolint(tmpDir: tmp.DirResult): Promise { process.exit(1); } process.chdir(path.dirname(mrlConfig)); - const mrlPaths = templatesWithSdkVersions(TEMPLATES).map(( + const mrlPaths = [ + ...templatesWithSdkVersions(TEMPLATES), + ...templatesWithSdkVersions(WIDGET_TEMPLATES), + ].map(( [template, sdkVersion], ) => path.join( @@ -185,7 +188,12 @@ async function checkExamples( resolvedOutput: string, tmpDir: tmp.DirResult, ): Promise { - for (const [template, sdkVersion] of templatesWithSdkVersions(TEMPLATES)) { + for ( + const [template, sdkVersion] of [ + ...templatesWithSdkVersions(TEMPLATES), + ...templatesWithSdkVersions(WIDGET_TEMPLATES), + ] + ) { const exampleId = sdkVersionedTemplateExampleId(template, sdkVersion); consola.info(`Checking contents of ${exampleId}`); // realpath because globby in .gitignore filter requires symlinks in tmp directory to be resolved @@ -253,16 +261,11 @@ async function copyExamples( tmpDir: tmp.DirResult, ): Promise { consola.info("Copying generated packages to output directory"); - for (const [template, sdkVersion] of templatesWithSdkVersions(TEMPLATES)) { - const exampleId = sdkVersionedTemplateExampleId(template, sdkVersion); - const exampleOutputPath = path.join(resolvedOutput, exampleId); - const exampleTmpPath = path.join(tmpDir.name, exampleId); - fs.rmSync(exampleOutputPath, { recursive: true, force: true }); - fs.mkdirSync(exampleOutputPath, { recursive: true }); - fs.cpSync(exampleTmpPath, exampleOutputPath, { recursive: true }); - } for ( - const [template, sdkVersion] of templatesWithSdkVersions(WIDGET_TEMPLATES) + const [template, sdkVersion] of [ + ...templatesWithSdkVersions(TEMPLATES), + ...templatesWithSdkVersions(WIDGET_TEMPLATES), + ] ) { const exampleId = sdkVersionedTemplateExampleId(template, sdkVersion); const exampleOutputPath = path.join(resolvedOutput, exampleId); @@ -347,10 +350,36 @@ const UPDATE_README: Mutator = { }), }; +const UPDATE_WIDGET_FOUNDRY_CONFIG_JSON: Mutator = { + filePattern: "foundry.config.json", + mutate: (template, content, _sdkVersion) => { + if (!WIDGET_TEMPLATES.find(t => t.id === template.id)) { + return { + type: "modify", + newContent: content, + }; + } + // Use package-json auto version strategy in vite manifest + return { + type: "modify", + newContent: content.replace( + `{ + "type": "git-describe", + "tagPrefix": "" + }`, + `{ + "type": "package-json" + }`, + ), + }; + }, +}; + const MUTATORS: Mutator[] = [ DELETE_NPM_RC, UPDATE_PACKAGE_JSON, UPDATE_README, + UPDATE_WIDGET_FOUNDRY_CONFIG_JSON, ]; function templateCanonicalId(template: Template): string { diff --git a/packages/foundry-config-json/src/config.ts b/packages/foundry-config-json/src/config.ts index ff4940b75..0333def10 100644 --- a/packages/foundry-config-json/src/config.ts +++ b/packages/foundry-config-json/src/config.ts @@ -137,11 +137,15 @@ const FOUNDRY_CONFIG_SCHEMA: { * @returns A promise that resolves to the configuration JSON object, or undefined if not found. * @throws Will throw an error if the configuration file is found but cannot be read or parsed. */ +export async function loadFoundryConfig( + type: "site", +): Promise | undefined>; +export async function loadFoundryConfig( + type: "widget", +): Promise | undefined>; export async function loadFoundryConfig( type: "site" | "widget", -): Promise< - LoadedFoundryConfig | undefined -> { +): Promise | undefined> { const ajvModule = await import("ajv"); const Ajv = ajvModule.default.default; // https://github.com/ajv-validator/ajv/issues/2132 const ajv = new Ajv({ allErrors: true }); diff --git a/packages/widget.vite-plugin/package.json b/packages/widget.vite-plugin/package.json index 083cf7e8b..27944d418 100644 --- a/packages/widget.vite-plugin/package.json +++ b/packages/widget.vite-plugin/package.json @@ -42,6 +42,7 @@ "devDependencies": { "@blueprintjs/core": "^5.16.0", "@blueprintjs/icons": "^5.15.0", + "@osdk/foundry-config-json": "workspace:~", "@osdk/monorepo.api-extractor": "workspace:~", "@osdk/monorepo.tsconfig": "workspace:~", "@osdk/monorepo.tsup": "workspace:~", diff --git a/packages/widget.vite-plugin/src/plugin.ts b/packages/widget.vite-plugin/src/plugin.ts index 07a99b4ed..99caed792 100644 --- a/packages/widget.vite-plugin/src/plugin.ts +++ b/packages/widget.vite-plugin/src/plugin.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { autoVersion, loadFoundryConfig } from "@osdk/foundry-config-json"; import { MANIFEST_FILE_LOCATION, type ParameterConfig, @@ -34,20 +35,14 @@ import { PALANTIR_PATH, SETUP_PATH } from "./constants.js"; export const DIR_DIST = typeof __dirname !== "undefined" ? __dirname : path.dirname(fileURLToPath(import.meta.url)); -export interface Options { - /** - * By default, looks in the directory from which vite is invoked - */ - packageJsonPath?: string; -} +export interface Options {} const CONFIG_FILE_SUFFIX = ".config"; const DEFINE_CONFIG_FUNCTION = "defineConfig"; -export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { - const { packageJsonPath = path.join(process.cwd(), "package.json") } = - options; - const baseDir = path.dirname(packageJsonPath); +export function FoundryWidgetVitePlugin(_options: Options = {}): Plugin { + const baseDir = process.cwd(); + const foundryConfigPromise = loadFoundryConfig("widget"); const entrypointToJsSourceFileMap: Record> = {}; const jsSourceFileToEntrypointMap: Record = {}; @@ -159,7 +154,7 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { body += readResult; } }); - req.on("end", () => { + req.on("end", async () => { if (body.length === 0) { res.statusCode = 400; res.statusMessage = @@ -188,17 +183,6 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { return; } - const foundryConfigJsonPath = path.join( - baseDir, - "foundry.config.json", - ); - if (!fs.existsSync(foundryConfigJsonPath)) { - res.statusCode = 500; - res.statusMessage = "foundry.config.json file not found."; - res.end(); - return; - } - if (process.env.FOUNDRY_TOKEN == null) { res.statusCode = 500; res.statusMessage = @@ -207,30 +191,21 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { return; } - const foundryConfig = fs.readJSONSync(foundryConfigJsonPath); - if (foundryConfig.foundryUrl == null) { + const foundryConfig = await foundryConfigPromise; + if (foundryConfig == null) { res.statusCode = 500; - res.statusMessage = - "foundry.config.json is missing the \"foundryUrl\" field, unable to start dev mode."; + res.statusMessage = "foundry.config.json file not found."; res.end(); return; } let url: URL; try { - url = new URL(foundryConfig.foundryUrl); + url = new URL(foundryConfig.foundryConfig.foundryUrl); } catch (error) { res.statusCode = 500; res.statusMessage = - `"foundryUrl" in foundry.config.json is invalid, found: ${foundryConfig.foundryUrl}`; - res.end(); - return; - } - - if (foundryConfig.widget?.rid == null) { - res.statusCode = 500; - res.statusMessage = - "foundry.config.json is missing the \"widget.rid\" field, unable to start dev mode."; + `"foundryUrl" in foundry.config.json is invalid, found: ${foundryConfig.foundryConfig.foundryUrl}`; res.end(); return; } @@ -239,7 +214,7 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { // Unfortunately, moduleParsed is not called during vite's dev mode for performance reasons, so the config file // will need to be parsed/read a different way fetch( - `${url.origin}/view-registry/api/dev-mode/${foundryConfig.widget.rid}/settings`, + `${url.origin}/view-registry/api/dev-mode/${foundryConfig.foundryConfig.widget.rid}/settings`, { body: JSON.stringify({ entrypointJs: [ @@ -267,7 +242,7 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { res.end( JSON.stringify({ redirectUrl: - `${url.origin}/workspace/custom-views/preview/${foundryConfig.widget.rid}`, + `${url.origin}/workspace/custom-views/preview/${foundryConfig.foundryConfig.widget.rid}`, }), ); } else { @@ -466,8 +441,13 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { // file of the shape we want will be handled in generateBundle }, // We hook into the produced bundle information to generate a widget configuration file that includes both the entrypoint info and any inferred parameter information. - generateBundle(options, bundle) { - const packageJsonFile = fs.readJSONSync(packageJsonPath); + async generateBundle(options, bundle) { + const foundryConfig = await foundryConfigPromise; + const widgetVersion = await autoVersion( + foundryConfig?.foundryConfig.widget.autoVersion + ?? { "type": "package-json" }, + ); + const widgetConfigManifest: WidgetManifest = { version: "1.0.0", widgets: {}, @@ -508,7 +488,7 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin { })) : [], rid: entrypointFileIdToConfigMap[chunk.facadeModuleId].rid, - version: packageJsonFile.version ?? "0.0.0", + version: widgetVersion, parameters: entrypointFileIdToConfigMap[chunk.facadeModuleId].parameters ?? {}, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7dfccab0a..bfc33f3c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3840,6 +3840,9 @@ importers: '@blueprintjs/icons': specifier: ^5.15.0 version: 5.15.0(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@osdk/foundry-config-json': + specifier: workspace:~ + version: link:../foundry-config-json '@osdk/monorepo.api-extractor': specifier: workspace:~ version: link:../monorepo.api-extractor From 6f6151774cf234a774243e73adfa59d47b5b63b1 Mon Sep 17 00:00:00 2001 From: Timothy Leung Date: Wed, 11 Dec 2024 14:43:39 +0000 Subject: [PATCH 3/6] Add changeset --- .changeset/famous-timers-tap.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/famous-timers-tap.md diff --git a/.changeset/famous-timers-tap.md b/.changeset/famous-timers-tap.md new file mode 100644 index 000000000..326de2d33 --- /dev/null +++ b/.changeset/famous-timers-tap.md @@ -0,0 +1,9 @@ +--- +"@osdk/foundry-config-json": patch +"@osdk/widget-manifest-vite-plugin": patch +"@osdk/example-generator": patch +"@osdk/cli.common": patch +"@osdk/cli": patch +--- + +Use @osdk/foundry-config-json in @osdk/widget-manifest-vite-plugin for shared config and auto versioning From d1adf863a6764a6f51006041cbae7c642dbac41f Mon Sep 17 00:00:00 2001 From: Timothy Leung Date: Wed, 11 Dec 2024 15:28:21 +0000 Subject: [PATCH 4/6] Update e2e.sandbox.todowidget --- packages/e2e.sandbox.todowidget/foundry.config.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/e2e.sandbox.todowidget/foundry.config.json b/packages/e2e.sandbox.todowidget/foundry.config.json index bee5b8963..3c59b288c 100644 --- a/packages/e2e.sandbox.todowidget/foundry.config.json +++ b/packages/e2e.sandbox.todowidget/foundry.config.json @@ -4,8 +4,7 @@ "rid": "ri.viewregistry..view.fake", "directory": "./dist", "autoVersion": { - "type": "git-describe", - "tagPrefix": "" + "type": "package-json" } } } From 582ccd4c83b65d6bdb6152c43e002fffd23436a3 Mon Sep 17 00:00:00 2001 From: Timothy Leung Date: Thu, 12 Dec 2024 10:13:41 +0000 Subject: [PATCH 5/6] deploy command --- packages/cli/package.json | 1 + packages/cli/src/cli.ts | 4 +- .../cli/src/commands/site/deploy/index.ts | 8 +- ...logSiteDeployCommandConfigFileOverride.ts} | 2 +- packages/cli/src/commands/site/index.ts | 11 +- .../src/commands/widget/CommonWidgetArgs.ts | 25 +++ .../widget/deploy/WidgetDeployArgs.ts | 21 +++ .../cli/src/commands/widget/deploy/index.ts | 58 ++++++ ...ogWidgetDeployCommandConfigFileOverride.ts | 31 ++++ .../widget/deploy/widgetDeployCommand.mts | 171 ++++++++++++++++++ packages/cli/src/commands/widget/index.ts | 85 +++++++++ .../logWidgetCommandConfigFileOverride.ts | 40 ++++ packages/cli/src/net/WidgetRid.ts | 17 ++ packages/cli/src/util/configLoader.ts | 41 ++++- .../cli/src/yargs/logConfigFileMiddleware.ts | 4 +- packages/foundry-config-json/src/index.ts | 1 + packages/monorepo.cspell/dict.osdk.txt | 1 + pnpm-lock.yaml | 3 + 18 files changed, 504 insertions(+), 20 deletions(-) rename packages/cli/src/commands/site/deploy/{logDeployCommandConfigFileOverride.ts => logSiteDeployCommandConfigFileOverride.ts} (96%) create mode 100644 packages/cli/src/commands/widget/CommonWidgetArgs.ts create mode 100644 packages/cli/src/commands/widget/deploy/WidgetDeployArgs.ts create mode 100644 packages/cli/src/commands/widget/deploy/index.ts create mode 100644 packages/cli/src/commands/widget/deploy/logWidgetDeployCommandConfigFileOverride.ts create mode 100644 packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts create mode 100644 packages/cli/src/commands/widget/index.ts create mode 100644 packages/cli/src/commands/widget/logWidgetCommandConfigFileOverride.ts create mode 100644 packages/cli/src/net/WidgetRid.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 88de858f9..8b2dd7cc1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "@osdk/monorepo.tsup": "workspace:~", "@osdk/shared.net.errors": "workspace:~", "@osdk/shared.net.fetch": "workspace:~", + "@osdk/widget-api.unstable": "workspace:~", "@types/archiver": "^6.0.2", "@types/ngeohash": "^0.6.8", "@types/node": "^18.0.0", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 49249a817..7bebca2f7 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -21,7 +21,7 @@ import { consola } from "consola"; import type { Argv } from "yargs"; import auth from "./commands/auth/index.js"; import site from "./commands/site/index.js"; -import { logConfigFileMiddleware } from "./yargs/logConfigFileMiddleware.js"; +import widget from "./commands/widget/index.js"; export async function cli(args: string[] = process.argv) { consola.info( @@ -33,7 +33,6 @@ export async function cli(args: string[] = process.argv) { // Special handling where failures happen before yargs does its error handling within .fail try { return await base - .middleware(logConfigFileMiddleware) .command(site) .command({ command: "unstable", @@ -43,6 +42,7 @@ export async function cli(args: string[] = process.argv) { return argv .command(typescript) .command(auth) + .command(widget) .demandCommand(); }, handler: (_args) => {}, diff --git a/packages/cli/src/commands/site/deploy/index.ts b/packages/cli/src/commands/site/deploy/index.ts index 0cc4836b7..ab696e022 100644 --- a/packages/cli/src/commands/site/deploy/index.ts +++ b/packages/cli/src/commands/site/deploy/index.ts @@ -23,7 +23,7 @@ import type { import type { CommandModule } from "yargs"; import configLoader from "../../../util/configLoader.js"; import type { CommonSiteArgs } from "../CommonSiteArgs.js"; -import { logDeployCommandConfigFileOverride } from "./logDeployCommandConfigFileOverride.js"; +import { logSiteDeployCommandConfigFileOverride } from "./logSiteDeployCommandConfigFileOverride.js"; import type { SiteDeployArgs } from "./SiteDeployArgs.js"; const command: CommandModule< @@ -33,8 +33,7 @@ const command: CommandModule< command: "deploy", describe: "Deploy a new site version", builder: async (argv) => { - const config: LoadedFoundryConfig<"site"> | undefined = - await configLoader(); + const config = await configLoader("site"); const siteConfig: SiteConfig | undefined = config?.foundryConfig.site; const directory = siteConfig?.directory; const autoVersion = siteConfig?.autoVersion; @@ -134,7 +133,6 @@ const command: CommandModule< } const gitTagPrefixValue = args.gitTagPrefix ?? gitTagPrefix; - // Future proofing for when we support other autoVersion types if (gitTagPrefixValue != null && autoVersionType !== "git-describe") { throw new YargsCheckError( `--gitTagPrefix is only supported when --autoVersion=git-describe`, @@ -155,7 +153,7 @@ const command: CommandModule< return true; }).middleware((args) => - logDeployCommandConfigFileOverride( + logSiteDeployCommandConfigFileOverride( args, siteConfig, ) diff --git a/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts b/packages/cli/src/commands/site/deploy/logSiteDeployCommandConfigFileOverride.ts similarity index 96% rename from packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts rename to packages/cli/src/commands/site/deploy/logSiteDeployCommandConfigFileOverride.ts index 1ab71c189..619b7c823 100644 --- a/packages/cli/src/commands/site/deploy/logDeployCommandConfigFileOverride.ts +++ b/packages/cli/src/commands/site/deploy/logSiteDeployCommandConfigFileOverride.ts @@ -19,7 +19,7 @@ import { consola } from "consola"; import type { Arguments } from "yargs"; import type { SiteDeployArgs } from "./SiteDeployArgs.js"; -export async function logDeployCommandConfigFileOverride( +export async function logSiteDeployCommandConfigFileOverride( args: Arguments, config: SiteConfig | undefined, ) { diff --git a/packages/cli/src/commands/site/index.ts b/packages/cli/src/commands/site/index.ts index fc09182d2..f0f2ceefd 100644 --- a/packages/cli/src/commands/site/index.ts +++ b/packages/cli/src/commands/site/index.ts @@ -20,6 +20,7 @@ import type { LoadedFoundryConfig } from "@osdk/foundry-config-json"; import type { CommandModule } from "yargs"; import type { ThirdPartyAppRid } from "../../net/ThirdPartyAppRid.js"; import configLoader from "../../util/configLoader.js"; +import { logConfigFileMiddleware } from "../../yargs/logConfigFileMiddleware.js"; import type { CommonSiteArgs } from "./CommonSiteArgs.js"; import deploy from "./deploy/index.js"; import { logSiteCommandConfigFileOverride } from "./logSiteCommandConfigFileOverride.js"; @@ -29,8 +30,7 @@ const command: CommandModule = { command: "site", describe: "Manage your site", builder: async (argv) => { - const config: LoadedFoundryConfig<"site"> | undefined = - await configLoader(); + const config = await configLoader("site"); const application = config?.foundryConfig.site.application; const foundryUrl = config?.foundryConfig.foundryUrl; return argv @@ -74,9 +74,10 @@ const command: CommandModule = { } return true; }) - .middleware((args) => - logSiteCommandConfigFileOverride(args, config?.foundryConfig) - ) + .middleware((args) => { + logConfigFileMiddleware("site"); + logSiteCommandConfigFileOverride(args, config?.foundryConfig); + }) .demandCommand(); }, handler: async (args) => {}, diff --git a/packages/cli/src/commands/widget/CommonWidgetArgs.ts b/packages/cli/src/commands/widget/CommonWidgetArgs.ts new file mode 100644 index 000000000..bb652800f --- /dev/null +++ b/packages/cli/src/commands/widget/CommonWidgetArgs.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CliCommonArgs } from "@osdk/cli.common"; +import type { WidgetRid } from "../../net/WidgetRid.js"; + +export interface CommonWidgetArgs extends CliCommonArgs { + rid: WidgetRid; + foundryUrl: string; + token?: string; + tokenFile?: string; +} diff --git a/packages/cli/src/commands/widget/deploy/WidgetDeployArgs.ts b/packages/cli/src/commands/widget/deploy/WidgetDeployArgs.ts new file mode 100644 index 000000000..f05c4daf7 --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/WidgetDeployArgs.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../CommonWidgetArgs.js"; + +export interface WidgetDeployArgs extends CommonWidgetArgs { + directory: string; +} diff --git a/packages/cli/src/commands/widget/deploy/index.ts b/packages/cli/src/commands/widget/deploy/index.ts new file mode 100644 index 000000000..88add6ec4 --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/index.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { WidgetConfig } from "@osdk/foundry-config-json"; +import type { CommandModule } from "yargs"; +import configLoader from "../../../util/configLoader.js"; +import type { CommonWidgetArgs } from "../CommonWidgetArgs.js"; +import { logWidgetDeployCommandConfigFileOverride } from "./logWidgetDeployCommandConfigFileOverride.js"; +import type { WidgetDeployArgs } from "./WidgetDeployArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + WidgetDeployArgs +> = { + command: "deploy", + describe: "Deploy a new widget version", + builder: async (argv) => { + const config = await configLoader("widget"); + const widgetConfig: WidgetConfig | undefined = config?.foundryConfig.widget; + const directory = widgetConfig?.directory; + + return argv + .options({ + directory: { + type: "string", + description: "Directory containing widget files", + ...directory + ? { default: directory } + : { demandOption: true }, + }, + }) + .group( + ["directory"], + "Deploy Options", + ).middleware((args) => + logWidgetDeployCommandConfigFileOverride(args, widgetConfig) + ); + }, + handler: async (args) => { + const command = await import("./widgetDeployCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/deploy/logWidgetDeployCommandConfigFileOverride.ts b/packages/cli/src/commands/widget/deploy/logWidgetDeployCommandConfigFileOverride.ts new file mode 100644 index 000000000..f7583668b --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/logWidgetDeployCommandConfigFileOverride.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { WidgetConfig } from "@osdk/foundry-config-json"; +import { consola } from "consola"; +import type { Arguments } from "yargs"; +import type { WidgetDeployArgs } from "./WidgetDeployArgs.js"; + +export async function logWidgetDeployCommandConfigFileOverride( + args: Arguments, + config: WidgetConfig | undefined, +) { + if (config?.directory != null && args.directory !== config.directory) { + consola.debug( + `Overriding "directory" from config file with ${args.directory}`, + ); + } +} diff --git a/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts b/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts new file mode 100644 index 000000000..b2b224a78 --- /dev/null +++ b/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts @@ -0,0 +1,171 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { consola } from "consola"; + +import { createInternalClientContext } from "#net"; +import { ExitProcessError } from "@osdk/cli.common"; +import type { WidgetManifest } from "@osdk/widget-api.unstable"; +import { MANIFEST_FILE_LOCATION } from "@osdk/widget-api.unstable"; +import archiver from "archiver"; +import * as fs from "node:fs"; +import path from "node:path"; +import { Readable } from "node:stream"; +import prettyBytes from "pretty-bytes"; +import { createFetch } from "../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../net/WidgetRid.js"; +import { loadToken } from "../../../util/token.js"; +import type { WidgetDeployArgs } from "./WidgetDeployArgs.js"; + +export default async function widgetDeployCommand( + { + rid, + foundryUrl, + directory, + token, + tokenFile, + }: WidgetDeployArgs, +) { + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + + consola.debug( + `Using directory for widget files: "${path.resolve(directory)}`, + ); + const stat = await fs.promises.stat(directory); + if (!stat.isDirectory()) { + throw new ExitProcessError( + 2, + "Specified path exists but is not a directory", + ); + } + + const widgetVersion = await findWidgetVersion(rid, directory); + + consola.start("Zipping widget files"); + const archive = archiver("zip").directory(directory, false); + logArchiveStats(archive); + + consola.start("Uploading widget files"); + await Promise.all([ + uploadVersion( + clientCtx, + rid, + widgetVersion, + Readable.toWeb(archive) as ReadableStream, // This cast is because the dom fetch doesn't align type wise with streams + ), + archive.finalize(), + ]); + consola.success("Upload complete"); + + consola.start("Publishing widget manifest"); + await publishManifest(clientCtx, rid, widgetVersion); + consola.success(`Deployed ${widgetVersion} successfully`); +} + +async function findWidgetVersion( + rid: WidgetRid, + directory: string, +): Promise { + try { + const manifestContent = await fs.promises.readFile( + path.resolve(directory, MANIFEST_FILE_LOCATION), + "utf8", + ); + const manifest: WidgetManifest = JSON.parse(manifestContent); + const widget = Object.values(manifest.widgets).find(w => w.rid === rid); + if (widget == null) { + throw new Error(`Unable to find widget ${rid} in manifest`); + } + if (widget.version == null) { + throw new Error(`Found widget ${rid} in manifest but missing version`); + } + return widget.version; + } catch (e) { + throw new ExitProcessError( + 2, + `Unable to process manifest at ${MANIFEST_FILE_LOCATION}`, + undefined, + e instanceof Error ? e : undefined, + ); + } +} + +async function uploadVersion( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, + zipFile: ReadableStream | Blob | BufferSource, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/artifacts/api/repositories/${widgetRid}/contents/release/siteasset/versions/zip/${version}`; + + await fetch( + url, + { + method: "PUT", + body: zipFile, + headers: { + "Content-Type": "application/octet-stream", + }, + duplex: "half", // Node hates me + } satisfies RequestInit & { duplex: "half" } as any, + ); +} + +async function publishManifest( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/view-registry/api/repositories/${widgetRid}/publish-manifest`; + + await fetch( + url, + { + method: "POST", + body: JSON.stringify({ version }), + headers: { + "Content-Type": "application/json", + }, + duplex: "half", // Node hates me + } satisfies RequestInit & { duplex: "half" } as any, + ); +} + +function logArchiveStats(archive: archiver.Archiver): void { + let archiveStats = { fileCount: 0, bytes: 0 }; + archive.on("progress", (progress) => { + archiveStats = { + fileCount: progress.entries.total, + bytes: progress.fs.totalBytes, + }; + }); + archive.on("finish", () => { + consola.info( + `Zipped ${ + prettyBytes(archiveStats.bytes, { binary: true }) + } total over ${archiveStats.fileCount} files`, + ); + }); +} diff --git a/packages/cli/src/commands/widget/index.ts b/packages/cli/src/commands/widget/index.ts new file mode 100644 index 000000000..247cdf3e6 --- /dev/null +++ b/packages/cli/src/commands/widget/index.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type CliCommonArgs, YargsCheckError } from "@osdk/cli.common"; +import { consola } from "consola"; +import type { CommandModule } from "yargs"; +import type { WidgetRid } from "../../net/WidgetRid.js"; +import configLoader from "../../util/configLoader.js"; +import { logConfigFileMiddleware } from "../../yargs/logConfigFileMiddleware.js"; +import type { CommonWidgetArgs } from "./CommonWidgetArgs.js"; +import deploy from "./deploy/index.js"; +import { logWidgetCommandConfigFileOverride } from "./logWidgetCommandConfigFileOverride.js"; + +const command: CommandModule = { + command: "widget", + describe: "Manage your widget", + builder: async (argv) => { + const config = await configLoader("widget"); + // TODO: remove + consola.info(config); + const rid = config?.foundryConfig.widget.rid; + const foundryUrl = config?.foundryConfig.foundryUrl; + return argv.options({ + rid: { + type: "string", + coerce: (rid) => rid as WidgetRid, + ...rid + ? { default: rid } + : { demandOption: true }, + description: "Widget resource identifier (rid)", + }, + foundryUrl: { + coerce: (foundryUrl) => foundryUrl.replace(/\/$/, ""), + type: "string", + ...foundryUrl + ? { default: foundryUrl } + : { demandOption: true }, + description: "URL for the Foundry stack", + }, + token: { + type: "string", + conflicts: "tokenFile", + description: "Foundry API token", + }, + tokenFile: { + type: "string", + conflicts: "token", + description: "Path to file containing Foundry API token", + }, + }) + .group( + ["rid", "foundryUrl", "token", "tokenFile"], + "Common Options", + ) + // .command(version) + .command(deploy) + .check((args) => { + if (!args.foundryUrl.startsWith("https://")) { + throw new YargsCheckError("foundryUrl must start with https://"); + } + return true; + }) + .middleware((args) => { + logConfigFileMiddleware("widget"); + logWidgetCommandConfigFileOverride(args, config?.foundryConfig); + }) + .demandCommand(); + }, + handler: async (args) => {}, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/logWidgetCommandConfigFileOverride.ts b/packages/cli/src/commands/widget/logWidgetCommandConfigFileOverride.ts new file mode 100644 index 000000000..4042e127c --- /dev/null +++ b/packages/cli/src/commands/widget/logWidgetCommandConfigFileOverride.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { FoundryConfig } from "@osdk/foundry-config-json"; +import { consola } from "consola"; +import type { Arguments } from "yargs"; +import type { CommonWidgetArgs } from "./CommonWidgetArgs.js"; + +export async function logWidgetCommandConfigFileOverride( + args: Arguments, + config: FoundryConfig<"widget"> | undefined, +) { + if ( + config?.widget.rid != null + && args.rid !== config.widget.rid + ) { + consola.debug( + `Overriding "rid" from config file with ${args.rid}`, + ); + } + + if (config?.foundryUrl != null && args.foundryUrl !== config.foundryUrl) { + consola.debug( + `Overriding "foundryUrl" from config file with ${args.foundryUrl}`, + ); + } +} diff --git a/packages/cli/src/net/WidgetRid.ts b/packages/cli/src/net/WidgetRid.ts new file mode 100644 index 000000000..c84e93cfb --- /dev/null +++ b/packages/cli/src/net/WidgetRid.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type WidgetRid = `ri.viewregistry..view.${string}`; diff --git a/packages/cli/src/util/configLoader.ts b/packages/cli/src/util/configLoader.ts index cf768cd96..1e28ff48d 100644 --- a/packages/cli/src/util/configLoader.ts +++ b/packages/cli/src/util/configLoader.ts @@ -18,17 +18,48 @@ import { ExitProcessError } from "@osdk/cli.common"; import type { LoadedFoundryConfig } from "@osdk/foundry-config-json"; import { loadFoundryConfig } from "@osdk/foundry-config-json"; -let configPromise: +let siteConfigPromise: | Promise | undefined> | undefined = undefined; +let widgetConfigPromise: + | Promise | undefined> + | undefined = undefined; + +function getConfig( + type: "site", +): Promise | undefined>; +function getConfig( + type: "widget", +): Promise | undefined>; +function getConfig( + type: "site" | "widget", +): Promise | undefined>; +function getConfig( + type: "site" | "widget", +): Promise | undefined> { + if (type === "site") { + return getSiteConfig(); + } else { + return getWidgetConfig(); + } +} + +function getSiteConfig(): Promise | undefined> { + if (siteConfigPromise == null) { + siteConfigPromise = loadFoundryConfig("site").catch((e) => { + throw new ExitProcessError(2, e instanceof Error ? e.message : undefined); + }); + } + return siteConfigPromise; +} -function getConfig(): Promise | undefined> { - if (configPromise == null) { - configPromise = loadFoundryConfig("site").catch((e) => { +function getWidgetConfig(): Promise | undefined> { + if (widgetConfigPromise == null) { + widgetConfigPromise = loadFoundryConfig("widget").catch((e) => { throw new ExitProcessError(2, e instanceof Error ? e.message : undefined); }); } - return configPromise; + return widgetConfigPromise; } export default getConfig; diff --git a/packages/cli/src/yargs/logConfigFileMiddleware.ts b/packages/cli/src/yargs/logConfigFileMiddleware.ts index abcd1990f..6fbae6339 100644 --- a/packages/cli/src/yargs/logConfigFileMiddleware.ts +++ b/packages/cli/src/yargs/logConfigFileMiddleware.ts @@ -18,10 +18,10 @@ import { consola } from "consola"; import getConfig from "../util/configLoader.js"; let firstTime = true; -export async function logConfigFileMiddleware() { +export async function logConfigFileMiddleware(type: "site" | "widget") { if (firstTime) { firstTime = false; - const config = getConfig(); + const config = getConfig(type); const configFilePath = (await config)?.configFilePath; if (configFilePath) { consola.debug( diff --git a/packages/foundry-config-json/src/index.ts b/packages/foundry-config-json/src/index.ts index 0862e8ec1..fab3b1e78 100644 --- a/packages/foundry-config-json/src/index.ts +++ b/packages/foundry-config-json/src/index.ts @@ -24,4 +24,5 @@ export type { LoadedFoundryConfig, PackageJsonAutoVersionConfig, SiteConfig, + WidgetConfig, } from "./config.js"; diff --git a/packages/monorepo.cspell/dict.osdk.txt b/packages/monorepo.cspell/dict.osdk.txt index 0b03804bc..f837e2451 100644 --- a/packages/monorepo.cspell/dict.osdk.txt +++ b/packages/monorepo.cspell/dict.osdk.txt @@ -7,3 +7,4 @@ paperplane Pressable Tamagui unistyles +siteasset diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfc33f3c2..dd37d2704 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1220,6 +1220,9 @@ importers: '@osdk/shared.net.fetch': specifier: workspace:~ version: link:../shared.net.fetch + '@osdk/widget-api.unstable': + specifier: workspace:~ + version: link:../widget.api.unstable '@types/archiver': specifier: ^6.0.2 version: 6.0.2 From 7618fa45b686ab59be5da4cf7c7548cde720b2cf Mon Sep 17 00:00:00 2001 From: Timothy Leung Date: Thu, 12 Dec 2024 12:22:37 +0000 Subject: [PATCH 6/6] version commands --- .../cli/src/commands/site/deploy/index.ts | 1 - packages/cli/src/commands/site/index.ts | 1 - .../src/commands/site/version/delete/index.ts | 2 +- .../widget/deploy/widgetDeployCommand.mts | 9 +- packages/cli/src/commands/widget/index.ts | 6 +- .../version/delete/VersionDeleteArgs.ts | 22 +++++ .../commands/widget/version/delete/index.ts | 47 ++++++++++ .../version/delete/versionDeleteCommand.mts | 85 +++++++++++++++++++ .../cli/src/commands/widget/version/index.ts | 39 +++++++++ .../widget/version/info/VersionInfoArgs.ts | 21 +++++ .../src/commands/widget/version/info/index.ts | 41 +++++++++ .../version/info/versionInfoCommand.mts | 48 +++++++++++ .../widget/version/list/VersionListArgs.ts | 19 +++++ .../src/commands/widget/version/list/index.ts | 36 ++++++++ .../version/list/versionListCommand.mts | 66 ++++++++++++++ packages/foundry-config-json/src/config.ts | 2 +- 16 files changed, 432 insertions(+), 13 deletions(-) create mode 100644 packages/cli/src/commands/widget/version/delete/VersionDeleteArgs.ts create mode 100644 packages/cli/src/commands/widget/version/delete/index.ts create mode 100644 packages/cli/src/commands/widget/version/delete/versionDeleteCommand.mts create mode 100644 packages/cli/src/commands/widget/version/index.ts create mode 100644 packages/cli/src/commands/widget/version/info/VersionInfoArgs.ts create mode 100644 packages/cli/src/commands/widget/version/info/index.ts create mode 100644 packages/cli/src/commands/widget/version/info/versionInfoCommand.mts create mode 100644 packages/cli/src/commands/widget/version/list/VersionListArgs.ts create mode 100644 packages/cli/src/commands/widget/version/list/index.ts create mode 100644 packages/cli/src/commands/widget/version/list/versionListCommand.mts diff --git a/packages/cli/src/commands/site/deploy/index.ts b/packages/cli/src/commands/site/deploy/index.ts index ab696e022..7b1944ab8 100644 --- a/packages/cli/src/commands/site/deploy/index.ts +++ b/packages/cli/src/commands/site/deploy/index.ts @@ -17,7 +17,6 @@ import { isValidSemver, YargsCheckError } from "@osdk/cli.common"; import type { AutoVersionConfigType, - LoadedFoundryConfig, SiteConfig, } from "@osdk/foundry-config-json"; import type { CommandModule } from "yargs"; diff --git a/packages/cli/src/commands/site/index.ts b/packages/cli/src/commands/site/index.ts index f0f2ceefd..43d0f8edb 100644 --- a/packages/cli/src/commands/site/index.ts +++ b/packages/cli/src/commands/site/index.ts @@ -16,7 +16,6 @@ import type { CliCommonArgs } from "@osdk/cli.common"; import { YargsCheckError } from "@osdk/cli.common"; -import type { LoadedFoundryConfig } from "@osdk/foundry-config-json"; import type { CommandModule } from "yargs"; import type { ThirdPartyAppRid } from "../../net/ThirdPartyAppRid.js"; import configLoader from "../../util/configLoader.js"; diff --git a/packages/cli/src/commands/site/version/delete/index.ts b/packages/cli/src/commands/site/version/delete/index.ts index 5ae0686e1..59192bd95 100644 --- a/packages/cli/src/commands/site/version/delete/index.ts +++ b/packages/cli/src/commands/site/version/delete/index.ts @@ -29,7 +29,7 @@ const command: CommandModule< .positional("version", { type: "string", demandOption: true, - description: "Version to set as live", + description: "Version to delete", }) .option("yes", { alias: "y", diff --git a/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts b/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts index b2b224a78..ccbd5f46e 100644 --- a/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts +++ b/packages/cli/src/commands/widget/deploy/widgetDeployCommand.mts @@ -99,9 +99,9 @@ async function findWidgetVersion( } catch (e) { throw new ExitProcessError( 2, - `Unable to process manifest at ${MANIFEST_FILE_LOCATION}`, - undefined, - e instanceof Error ? e : undefined, + `Unable to process manifest at ${MANIFEST_FILE_LOCATION}${ + e instanceof Error ? `: ${e.message}` : "" + }`, ); } } @@ -148,8 +148,7 @@ async function publishManifest( headers: { "Content-Type": "application/json", }, - duplex: "half", // Node hates me - } satisfies RequestInit & { duplex: "half" } as any, + }, ); } diff --git a/packages/cli/src/commands/widget/index.ts b/packages/cli/src/commands/widget/index.ts index 247cdf3e6..4213793d8 100644 --- a/packages/cli/src/commands/widget/index.ts +++ b/packages/cli/src/commands/widget/index.ts @@ -15,7 +15,6 @@ */ import { type CliCommonArgs, YargsCheckError } from "@osdk/cli.common"; -import { consola } from "consola"; import type { CommandModule } from "yargs"; import type { WidgetRid } from "../../net/WidgetRid.js"; import configLoader from "../../util/configLoader.js"; @@ -23,14 +22,13 @@ import { logConfigFileMiddleware } from "../../yargs/logConfigFileMiddleware.js" import type { CommonWidgetArgs } from "./CommonWidgetArgs.js"; import deploy from "./deploy/index.js"; import { logWidgetCommandConfigFileOverride } from "./logWidgetCommandConfigFileOverride.js"; +import version from "./version/index.js"; const command: CommandModule = { command: "widget", describe: "Manage your widget", builder: async (argv) => { const config = await configLoader("widget"); - // TODO: remove - consola.info(config); const rid = config?.foundryConfig.widget.rid; const foundryUrl = config?.foundryConfig.foundryUrl; return argv.options({ @@ -65,7 +63,7 @@ const command: CommandModule = { ["rid", "foundryUrl", "token", "tokenFile"], "Common Options", ) - // .command(version) + .command(version) .command(deploy) .check((args) => { if (!args.foundryUrl.startsWith("https://")) { diff --git a/packages/cli/src/commands/widget/version/delete/VersionDeleteArgs.ts b/packages/cli/src/commands/widget/version/delete/VersionDeleteArgs.ts new file mode 100644 index 000000000..376fd6870 --- /dev/null +++ b/packages/cli/src/commands/widget/version/delete/VersionDeleteArgs.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; + +export interface VersionDeleteArgs extends CommonWidgetArgs { + version: string; + yes?: boolean; +} diff --git a/packages/cli/src/commands/widget/version/delete/index.ts b/packages/cli/src/commands/widget/version/delete/index.ts new file mode 100644 index 000000000..56b671451 --- /dev/null +++ b/packages/cli/src/commands/widget/version/delete/index.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; +import type { VersionDeleteArgs } from "./VersionDeleteArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + VersionDeleteArgs +> = { + command: "delete ", + describe: "Delete widget version", + builder: (argv) => { + return argv + .positional("version", { + type: "string", + demandOption: true, + description: "Version to delete", + }) + .option("yes", { + alias: "y", + type: "boolean", + description: "Automatically confirm destructive changes", + }) + .group(["yes"], "Delete Options"); + }, + handler: async (args) => { + const command = await import("./versionDeleteCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/delete/versionDeleteCommand.mts b/packages/cli/src/commands/widget/version/delete/versionDeleteCommand.mts new file mode 100644 index 000000000..565be56e2 --- /dev/null +++ b/packages/cli/src/commands/widget/version/delete/versionDeleteCommand.mts @@ -0,0 +1,85 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createInternalClientContext } from "#net"; +import { consola } from "consola"; +import { colorize } from "consola/utils"; +import { handlePromptCancel } from "../../../../consola/handlePromptCancel.js"; +import { createFetch } from "../../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../../net/WidgetRid.js"; +import { loadToken } from "../../../../util/token.js"; +import type { VersionDeleteArgs } from "./VersionDeleteArgs.js"; + +export default async function versionDeleteCommand( + { version, yes, rid, foundryUrl, token, tokenFile }: VersionDeleteArgs, +) { + if (!yes) { + const confirmed = await consola.prompt( + `Are you sure you want to delete the version ${version}?\n${ + colorize("bold", "This action cannot be undone.") + }`, + { type: "confirm" }, + ); + handlePromptCancel(confirmed); + } + + consola.start(`Deleting version ${version}`); + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + // TODO: Look at type of locator and decide whether to confirm delete site version + await Promise.all([ + deleteViewRelease(clientCtx, rid, version), + deleteVersion(clientCtx, rid, version), + ]); + consola.success(`Deleted version ${version}`); +} + +async function deleteViewRelease( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/view-registry/api/views/${widgetRid}/releases/${version}`; + await fetch( + url, + { + method: "DELETE", + }, + ); +} + +async function deleteVersion( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/artifacts/api/repositories/${widgetRid}/contents/release/siteasset/versions/${version}`; + + await fetch( + url, + { + method: "DELETE", + }, + ); +} diff --git a/packages/cli/src/commands/widget/version/index.ts b/packages/cli/src/commands/widget/version/index.ts new file mode 100644 index 000000000..b17f0fabd --- /dev/null +++ b/packages/cli/src/commands/widget/version/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../CommonWidgetArgs.js"; +import deleteCmd from "./delete/index.js"; +import info from "./info/index.js"; +import list from "./list/index.js"; + +const command: CommandModule< + CommonWidgetArgs, + CommonWidgetArgs +> = { + command: "version", + describe: "Manage widget versions", + builder: (argv) => { + return argv + .command(list) + .command(info) + .command(deleteCmd) + .demandCommand(); + }, + handler: async (args) => {}, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/info/VersionInfoArgs.ts b/packages/cli/src/commands/widget/version/info/VersionInfoArgs.ts new file mode 100644 index 000000000..e2c6eddf6 --- /dev/null +++ b/packages/cli/src/commands/widget/version/info/VersionInfoArgs.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; + +export interface VersionInfoArgs extends CommonWidgetArgs { + version: string; +} diff --git a/packages/cli/src/commands/widget/version/info/index.ts b/packages/cli/src/commands/widget/version/info/index.ts new file mode 100644 index 000000000..b7cc6c618 --- /dev/null +++ b/packages/cli/src/commands/widget/version/info/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; +import type { VersionInfoArgs } from "./VersionInfoArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + VersionInfoArgs +> = { + command: "info ", + describe: "Load info about widget version", + builder: (argv) => { + return argv + .positional("version", { + type: "string", + demandOption: true, + description: "Version to load", + }); + }, + handler: async (args) => { + const command = await import("./versionInfoCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/info/versionInfoCommand.mts b/packages/cli/src/commands/widget/version/info/versionInfoCommand.mts new file mode 100644 index 000000000..f5ecd911a --- /dev/null +++ b/packages/cli/src/commands/widget/version/info/versionInfoCommand.mts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createInternalClientContext } from "#net"; +import { consola } from "consola"; +import { createFetch } from "../../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../../net/WidgetRid.js"; +import { loadToken } from "../../../../util/token.js"; +import type { VersionInfoArgs } from "./VersionInfoArgs.js"; + +export default async function versionInfoCommand( + { version, foundryUrl, rid, token, tokenFile }: VersionInfoArgs, +) { + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + consola.start("Loading version info"); + const response = await getViewRelease(clientCtx, rid, version); + consola.success(`Loaded version info for ${version}`); + consola.log(JSON.stringify(response, null, 2)); +} + +async function getViewRelease( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, + version: string, +): Promise { + const fetch = createFetch(ctx.tokenProvider); + const url = + `${ctx.foundryUrl}/view-registry/api/views/${widgetRid}/releases/${version}`; + const response = await fetch(url); + return response.json(); +} diff --git a/packages/cli/src/commands/widget/version/list/VersionListArgs.ts b/packages/cli/src/commands/widget/version/list/VersionListArgs.ts new file mode 100644 index 000000000..3c37e904d --- /dev/null +++ b/packages/cli/src/commands/widget/version/list/VersionListArgs.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; + +export interface VersionListArgs extends CommonWidgetArgs {} diff --git a/packages/cli/src/commands/widget/version/list/index.ts b/packages/cli/src/commands/widget/version/list/index.ts new file mode 100644 index 000000000..21bed6b01 --- /dev/null +++ b/packages/cli/src/commands/widget/version/list/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CommandModule } from "yargs"; +import type { CommonWidgetArgs } from "../../CommonWidgetArgs.js"; +import type { VersionListArgs } from "./VersionListArgs.js"; + +const command: CommandModule< + CommonWidgetArgs, + VersionListArgs +> = { + command: "list", + describe: "List widget versions", + builder: (argv) => { + return argv; + }, + handler: async (args) => { + const command = await import("./versionListCommand.mjs"); + await command.default(args); + }, +}; + +export default command; diff --git a/packages/cli/src/commands/widget/version/list/versionListCommand.mts b/packages/cli/src/commands/widget/version/list/versionListCommand.mts new file mode 100644 index 000000000..0aa4a80ac --- /dev/null +++ b/packages/cli/src/commands/widget/version/list/versionListCommand.mts @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createInternalClientContext } from "#net"; +import { consola } from "consola"; +import { createFetch } from "../../../../net/createFetch.mjs"; +import type { InternalClientContext } from "../../../../net/internalClientContext.mjs"; +import type { WidgetRid } from "../../../../net/WidgetRid.js"; +import { loadToken } from "../../../../util/token.js"; +import type { VersionListArgs } from "./VersionListArgs.js"; + +export default async function versionListCommand( + { foundryUrl, rid, token, tokenFile }: VersionListArgs, +) { + const loadedToken = await loadToken(token, tokenFile); + const tokenProvider = () => loadedToken; + const clientCtx = createInternalClientContext(foundryUrl, tokenProvider); + consola.start("Fetching versions"); + + const response = await listViewReleases(clientCtx, rid); + if (response.releases.length === 0) { + consola.info("No widget versions found"); + return; + } + + consola.success("Found versions:"); + + const semver = await import("semver"); + const sortedVersions = semver.rsort( + response.releases.map(v => v.version).filter(v => semver.valid(v)), + ); + for (const version of sortedVersions) { + consola.log( + ` - ${version}`, + ); + } +} + +async function listViewReleases( + ctx: InternalClientContext, + // TODO: make repository rid + widgetRid: WidgetRid, +): Promise<{ + releases: Array<{ + rid: WidgetRid; + version: string; + }>; +}> { + const fetch = createFetch(ctx.tokenProvider); + const url = `${ctx.foundryUrl}/view-registry/api/views/${widgetRid}/releases`; + const response = await fetch(url); + return response.json(); +} diff --git a/packages/foundry-config-json/src/config.ts b/packages/foundry-config-json/src/config.ts index 0333def10..371236c00 100644 --- a/packages/foundry-config-json/src/config.ts +++ b/packages/foundry-config-json/src/config.ts @@ -167,7 +167,7 @@ export async function loadFoundryConfig( if (!validate(foundryConfig)) { throw new Error( - `The configuration file does not match the expected schema: ${ + `The configuration file ${configFilePath} does not match the expected schema: ${ ajv.errorsText(validate.errors) }`, );