diff --git a/.chronus/changes/vscode-openapi-preview-2025-1-14-15-22-4.md b/.chronus/changes/vscode-openapi-preview-2025-1-14-15-22-4.md new file mode 100644 index 00000000000..7b3e2c20f0c --- /dev/null +++ b/.chronus/changes/vscode-openapi-preview-2025-1-14-15-22-4.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - typespec-vscode +--- + +add openapi3 preview diff --git a/.prettierignore b/.prettierignore index cae241dd97f..f8bdd37deda 100644 --- a/.prettierignore +++ b/.prettierignore @@ -61,5 +61,8 @@ packages/http-client-csharp/generator/TestProjects/**/tspCodeModel.json packages/http-client-java/generator/http-client-generator-test/src/main/**/*.json packages/http-client-java/generator/http-client-generator-clientcore-test/src/main/**/*.json +# swagger-ui-dist lib files +packages/typespec-vscode/swagger-ui/swagger-ui* + .gitattributes CODEOWNERS diff --git a/eslint.config.js b/eslint.config.js index cb8b6901197..78aa98885fa 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -148,6 +148,7 @@ export default tsEslint.config( "packages/compiler/templates/**/*", // Ignore the templates which might have invalid code and not follow exactly our rules. "**/venv/**/*", // Ignore python virtual env "**/.vscode-test-web/**/*", // Ignore VSCode test web project + "packages/typespec-vscode/swagger-ui/swagger-ui*", // Ignore swagger-ui-dist files // TODO: enable "**/.scripts/**/*", "eng/tsp-core/scripts/**/*", diff --git a/packages/typespec-vscode/.vscodeignore b/packages/typespec-vscode/.vscodeignore index f668ef356f7..edeef04f302 100644 --- a/packages/typespec-vscode/.vscodeignore +++ b/packages/typespec-vscode/.vscodeignore @@ -9,6 +9,7 @@ !dist/**/*.js !dist/**/*.cjs !dist/**/*.js.map +!dist/**/*.css !dist/**/language-configuration.json dist/test/ @@ -18,3 +19,4 @@ dist/test/ !LICENSE !icons/ !snippets.json +!swagger-ui/ diff --git a/packages/typespec-vscode/package.json b/packages/typespec-vscode/package.json index aec6abec84f..bdca4e22e9b 100644 --- a/packages/typespec-vscode/package.json +++ b/packages/typespec-vscode/package.json @@ -116,12 +116,17 @@ "explorer/context": [ { "command": "typespec.importFromOpenApi3", - "group": "typespec@2" + "group": "typespec@3" }, { "command": "typespec.generateCode", "when": "explorerResourceIsFolder || resourceLangId == typespec", "group": "typespec@1" + }, + { + "command": "typespec.showOpenApi3", + "when": "resourceLangId == typespec", + "group": "typespec@2" } ], "editor/context": [ @@ -129,6 +134,11 @@ "command": "typespec.generateCode", "when": "resourceLangId == typespec", "group": "typespec@1" + }, + { + "command": "typespec.showOpenApi3", + "when": "resourceLangId == typespec", + "group": "typespec@2" } ] }, @@ -180,6 +190,11 @@ "command": "typespec.importFromOpenApi3", "title": "Import TypeSpec from OpenApi3", "category": "TypeSpec" + }, + { + "command": "typespec.showOpenApi3", + "title": "Show OpenAPI3 Documentation", + "category": "TypeSpec" } ], "semanticTokenScopes": [ @@ -232,7 +247,7 @@ "copy-tmlanguage": "node scripts/copy-tmlanguage.js", "generate-language-configuration": "node scripts/generate-language-configuration.js", "generate-third-party-notices": "typespec-build-tool generate-third-party-notices", - "package-vsix": "vsce package", + "package-vsix": "vsce package --no-dependencies", "deploy": "vsce publish", "open-in-browser": "vscode-test-web --extensionDevelopmentPath=. .", "test:e2e": "pnpm test:web", @@ -244,8 +259,8 @@ "@rollup/plugin-typescript": "~12.1.0", "@types/mocha": "^10.0.9", "@types/node": "~22.10.10", - "@types/vscode": "~1.96.0", "@types/semver": "^7.5.8", + "@types/vscode": "~1.96.0", "@typespec/compiler": "workspace:~", "@typespec/internal-build-utils": "workspace:~", "@vitest/coverage-v8": "^3.0.4", @@ -256,7 +271,9 @@ "mocha": "^11.1.0", "rimraf": "~6.0.1", "rollup": "~4.31.0", + "rollup-plugin-copy": "^3.5.0", "semver": "^7.6.3", + "swagger-ui-dist": "^5.18.2", "typescript": "~5.7.3", "vitest": "^3.0.5", "vscode-languageclient": "~9.0.1", diff --git a/packages/typespec-vscode/rollup.config.ts b/packages/typespec-vscode/rollup.config.ts index 3d2f2727578..280a18a5157 100644 --- a/packages/typespec-vscode/rollup.config.ts +++ b/packages/typespec-vscode/rollup.config.ts @@ -2,6 +2,7 @@ import commonjs from "@rollup/plugin-commonjs"; import resolve from "@rollup/plugin-node-resolve"; import typescript from "@rollup/plugin-typescript"; import { dirname } from "path"; +import copy from "rollup-plugin-copy"; import { defineConfig } from "rollup"; import { fileURLToPath } from "url"; @@ -41,7 +42,17 @@ export default defineConfig([ exports: "named", inlineDynamicImports: true, }, - plugins: [...plugins, ts("dist/src")], + plugins: [ + ...plugins, + ts("dist/src"), + copy({ + targets: [ + { src: "node_modules/swagger-ui-dist/swagger-ui.css", dest: "dist" }, + { src: "node_modules/swagger-ui-dist/swagger-ui-bundle.js", dest: "dist" }, + { src: "node_modules/swagger-ui-dist/swagger-ui-standalone-preset.js", dest: "dist" }, + ], + }), + ], }, { ...baseConfig, diff --git a/packages/typespec-vscode/src/extension.ts b/packages/typespec-vscode/src/extension.ts index 87e33a09f91..5ade868312f 100644 --- a/packages/typespec-vscode/src/extension.ts +++ b/packages/typespec-vscode/src/extension.ts @@ -18,6 +18,7 @@ import { createTypeSpecProject } from "./vscode-cmd/create-tsp-project.js"; import { emitCode } from "./vscode-cmd/emit-code/emit-code.js"; import { importFromOpenApi3 } from "./vscode-cmd/import-from-openapi3.js"; import { installCompilerGlobally } from "./vscode-cmd/install-tsp-compiler.js"; +import { clearOpenApi3PreviewTempFolders, showOpenApi3 } from "./vscode-cmd/openapi3-preview.js"; let client: TspLanguageClient | undefined; /** @@ -114,6 +115,12 @@ export async function activate(context: ExtensionContext) { }), ); + context.subscriptions.push( + commands.registerCommand(CommandName.ShowOpenApi3, async (uri: vscode.Uri) => { + await showOpenApi3(uri, context, client!); + }), + ); + context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { if (e.affectsConfiguration(SettingName.TspServerPath)) { @@ -171,6 +178,7 @@ export async function activate(context: ExtensionContext) { export async function deactivate() { await client?.stop(); + await clearOpenApi3PreviewTempFolders(); } async function recreateLSPClient(context: ExtensionContext) { diff --git a/packages/typespec-vscode/src/tsp-language-client.ts b/packages/typespec-vscode/src/tsp-language-client.ts index c0c23ee7a02..78bebc0c6fe 100644 --- a/packages/typespec-vscode/src/tsp-language-client.ts +++ b/packages/typespec-vscode/src/tsp-language-client.ts @@ -224,4 +224,24 @@ export class TspLanguageClient { const lc = new LanguageClient(id, name, { run: exe, debug: exe }, options); return new TspLanguageClient(lc, exe); } + + async compileOpenApi3( + mainTspFile: string, + srcFolder: string, + outputFolder: string, + ): Promise { + const result = await this.runCliCommand( + [ + "compile", + mainTspFile, + "--emit=@typespec/openapi3", + "--option", + "@typespec/openapi3.file-type=json", + "--option", + `@typespec/openapi3.emitter-output-dir=${outputFolder}`, + ], + srcFolder, + ); + return result; + } } diff --git a/packages/typespec-vscode/src/types.ts b/packages/typespec-vscode/src/types.ts index e01a8235d45..f91c8aff96f 100644 --- a/packages/typespec-vscode/src/types.ts +++ b/packages/typespec-vscode/src/types.ts @@ -11,6 +11,7 @@ export const enum CommandName { OpenUrl = "typespec.openUrl", GenerateCode = "typespec.generateCode", ImportFromOpenApi3 = "typespec.importFromOpenApi3", + ShowOpenApi3 = "typespec.showOpenApi3", } export interface InstallGlobalCliCommandArgs { diff --git a/packages/typespec-vscode/src/utils.ts b/packages/typespec-vscode/src/utils.ts index da85671117e..cf500f6f73e 100644 --- a/packages/typespec-vscode/src/utils.ts +++ b/packages/typespec-vscode/src/utils.ts @@ -1,6 +1,7 @@ import type { ModuleResolutionResult, NodePackage, ResolveModuleHost } from "@typespec/compiler"; import { spawn, SpawnOptions } from "child_process"; -import { readFile, realpath, stat } from "fs/promises"; +import { mkdir, readFile, realpath, stat } from "fs/promises"; +import { tmpdir } from "os"; import { dirname } from "path"; import { CancellationToken } from "vscode"; import { Executable } from "vscode-languageclient/node.js"; @@ -27,6 +28,24 @@ export async function isDirectory(path: string) { } } +export async function createTempDir(): Promise { + try { + const tempDir = tmpdir(); + const realTempDir = await realpath(tempDir); + const uid = createGuid(); + const subDir = joinPaths(realTempDir, uid); + await mkdir(subDir); + return subDir; + } catch (e) { + logger.error("Failed to create temp folder", [e]); + return undefined; + } +} + +function createGuid() { + return crypto.randomUUID(); +} + export function isWhitespaceStringOrUndefined(str: string | undefined): boolean { return str === undefined || str.trim() === ""; } @@ -389,3 +408,33 @@ export async function loadPackageJsonFile( if (!packageJson) return undefined; return packageJson as NodePackage; } + +export async function parseJsonFromFile(filePath: string): Promise { + try { + const fileContent = await readFile(filePath, "utf-8"); + const content = JSON.parse(fileContent); + return content; + } catch (e) { + logger.error(`Failed to load JSON file: ${filePath}`, [e]); + return; + } +} + +/** + * Throttle the function to be called at most once in every blockInMs milliseconds. This utility + * is useful when your event handler will trigger the same event multiple times in a short period. + * + * @param fn Underlying function to be throttled + * @param blockInMs Block time in milliseconds + * @returns a throttled function + */ +export function throttle any>(fn: T, blockInMs: number): T { + let time: number | undefined; + return function (this: any, ...args: Parameters) { + const now = Date.now(); + if (time === undefined || now - time >= blockInMs) { + time = now; + fn.apply(this, args); + } + } as T; +} diff --git a/packages/typespec-vscode/src/vscode-cmd/openapi3-preview.ts b/packages/typespec-vscode/src/vscode-cmd/openapi3-preview.ts new file mode 100644 index 00000000000..07bcd35a241 --- /dev/null +++ b/packages/typespec-vscode/src/vscode-cmd/openapi3-preview.ts @@ -0,0 +1,316 @@ +import { readdir, rm } from "fs/promises"; +import * as semver from "semver"; +import * as vscode from "vscode"; +import logger from "../log/logger.js"; +import { getBaseFileName, getDirectoryPath, joinPaths } from "../path-utils.js"; +import { TspLanguageClient } from "../tsp-language-client.js"; +import { TraverseMainTspFileInWorkspace } from "../typespec-utils.js"; +import { createTempDir, parseJsonFromFile, throttle } from "../utils.js"; + +const TITLE = "Preview in OpenAPI3"; + +export async function showOpenApi3( + uri: vscode.Uri, + context: vscode.ExtensionContext, + client: TspLanguageClient, +) { + const selectedFile = uri?.fsPath ?? vscode.window.activeTextEditor?.document.uri.fsPath; + if (!selectedFile || !selectedFile.endsWith(".tsp")) { + logger.error( + "Please select a Typespec file", + selectedFile ? [`Currently '${selectedFile}' is selected`] : [], + { + showOutput: true, + showPopup: true, + }, + ); + return; + } + + const mainTspFile = selectedFile.endsWith("main.tsp") ? selectedFile : await getMainTspFile(); + if (mainTspFile === undefined) { + logger.error(`No 'main.tsp' file can be determined from '${selectedFile}' in workspace.`, [], { + showOutput: true, + showPopup: true, + }); + return; + } + + await loadOpenApi3PreviewPanel(mainTspFile, context, client); +} + +async function getMainTspFile(): Promise { + const filePath = await TraverseMainTspFileInWorkspace(); + + switch (filePath.length) { + case 0: + logger.error("No 'main.tsp' file found in the workspace.", [], { + showOutput: true, + showPopup: true, + }); + return undefined; + case 1: + return filePath[0]; + default: + return await vscode.window.showQuickPick(filePath, { + title: TITLE, + placeHolder: "Select the 'main.tsp' file", + }); + } +} + +const openApi3TempFolders = new Map(); +const openApi3PreviewPanels = new Map(); +const selectedOpenApi3OutputFiles = new Map(); + +async function loadOpenApi3PreviewPanel( + mainTspFile: string, + context: vscode.ExtensionContext, + client: TspLanguageClient, +) { + const compilerVersion = client.initializeResult?.serverInfo?.version; + if (!compilerVersion || semver.lt(compilerVersion, "0.65.0")) { + logger.error("OpenAPI3 preview requires TypeSpec compiler version 0.65.0 or later.", [], { + showOutput: true, + showPopup: true, + }); + return; + } + + if (openApi3PreviewPanels.has(mainTspFile)) { + // if panel is already opened, there should be a watcher to automatically update the content + // no need to generate the openapi3 files + const panel = openApi3PreviewPanels.get(mainTspFile)!; + const outputFolder = await getOutputFolder(mainTspFile); + if (!outputFolder) { + panel.dispose(); + logger.error( + "Unexpected error. Please try again.", + ["OpenAPI3 preview panel is available, but output folder is not."], + { + showOutput: true, + showPopup: true, + }, + ); + return; + } + + const fileContent = await selectAndGetOpenApi3Content(mainTspFile, outputFolder, true, context); + if (fileContent === undefined) { + return; + } + + void panel.webview.postMessage({ command: "load", param: fileContent }); + panel.reveal(); + } else { + const getOpenApi3Output = async (selectOutput: boolean): Promise => { + return await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Loading OpenAPI3 files...", + }, + async (): Promise => { + const srcFolder = getDirectoryPath(mainTspFile); + const outputFolder = await getOutputFolder(mainTspFile); + if (!outputFolder) { + logger.error("Failed to create temporary folder for OpenAPI3 files", [], { + showOutput: true, + showPopup: true, + }); + return undefined; + } + + const result = await client.compileOpenApi3(mainTspFile, srcFolder, outputFolder); + if (result === undefined || result.exitCode !== 0) { + logger.error( + "Failed to generate OpenAPI3 files.", + result?.stderr ? [result.stderr] : [], + ); + return; + } else { + return await selectAndGetOpenApi3Content( + mainTspFile, + outputFolder, + selectOutput, + context, + ); + } + }, + ); + }; + + const fileContent = await getOpenApi3Output(true); + if (fileContent === undefined) { + return; + } + + const panel = vscode.window.createWebviewPanel( + "webview", + `OpenAPI3 for ${mainTspFile}`, + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }, + { + retainContextWhenHidden: true, + enableScripts: true, + localResourceRoots: [context.extensionUri], + }, + ); + openApi3PreviewPanels.set(mainTspFile, panel); + + const watch = vscode.workspace.createFileSystemWatcher("**/*.{tsp}"); + const throttledChangeHandler = throttle(async () => { + const content = await getOpenApi3Output(false); + void panel.webview.postMessage({ command: "load", param: content }); + }, 1000); + watch.onDidChange(throttledChangeHandler); + watch.onDidCreate(throttledChangeHandler); + watch.onDidDelete(throttledChangeHandler); + + panel.onDidDispose(() => { + openApi3PreviewPanels.delete(mainTspFile); + selectedOpenApi3OutputFiles.delete(mainTspFile); + watch.dispose(); + }); + + panel.webview.onDidReceiveMessage(async (message) => { + switch (message.event) { + case "initialized": + void panel.webview.postMessage({ command: "load", param: fileContent }); + break; + default: + logger.error(`Unknown WebView event received: ${message}`); + break; + } + }); + + loadHtml(context.extensionUri, panel); + } +} + +let html: string | undefined; + +function loadHtml(extensionUri: vscode.Uri, panel: vscode.WebviewPanel) { + if (html === undefined) { + const distRoot = vscode.Uri.joinPath(extensionUri, "dist"); + const css = panel.webview.asWebviewUri(vscode.Uri.joinPath(distRoot, "swagger-ui.css")); + const bundleJs = panel.webview.asWebviewUri( + vscode.Uri.joinPath(distRoot, "swagger-ui-bundle.js"), + ); + + const presetJs = panel.webview.asWebviewUri( + vscode.Uri.joinPath(distRoot, "swagger-ui-standalone-preset.js"), + ); + // initialization script is customized to load openapi3 spec from openapi emitter output + const initJs = panel.webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, "swagger-ui", "swagger-initializer.js"), + ); + + html = ` + + + + + + + +
+ + + + + `; + } + + panel.webview.html = html; +} + +async function getOutputFolder(mainTspFile: string): Promise { + let tmpFolder = openApi3TempFolders.get(mainTspFile); + if (!tmpFolder) { + tmpFolder = await createTempDir(); + if (tmpFolder) { + openApi3TempFolders.set(mainTspFile, tmpFolder); + } + } + return tmpFolder; +} + +export async function clearOpenApi3PreviewTempFolders() { + for (const folder of openApi3TempFolders.values()) { + try { + await rm(folder, { recursive: true, force: true }); + } catch (e) { + logger.error(`Failed to delete temporary folder: ${folder}`, [e]); + } + } +} + +async function selectAndGetOpenApi3Content( + mainTspFile: string, + outputFolder: string, + selectOutput: boolean, + context: vscode.ExtensionContext, +): Promise { + let outputs: string[] | undefined; + try { + outputs = await readdir(outputFolder); + } catch (e) { + logger.error(`Failed to read output folder: ${outputFolder}`, [e], { + showOutput: true, + showPopup: true, + }); + return; + } + + if (outputs.length === 0) { + logger.error("No openAPI3 file generated.", [], { + showOutput: true, + showPopup: true, + }); + return; + } else if (outputs.length === 1) { + const first = outputs[0]; + const filePath = joinPaths(outputFolder, first); + selectedOpenApi3OutputFiles.set(mainTspFile, filePath); + return parseOpenApi3File(filePath); + } else { + if (selectedOpenApi3OutputFiles.has(mainTspFile) && !selectOutput) { + return parseOpenApi3File(selectedOpenApi3OutputFiles.get(mainTspFile)!); + } + const files = outputs.map((file) => { + const filePath = joinPaths(outputFolder, file); + return { + label: getBaseFileName(file), + detail: filePath, + path: filePath, + iconPath: { + light: vscode.Uri.file(context.asAbsolutePath(`./icons/openapi3.light.svg`)), + dark: vscode.Uri.file(context.asAbsolutePath(`./icons/openapi3.dark.svg`)), + }, + }; + }); + const selected = await vscode.window.showQuickPick(files, { + title: TITLE, + placeHolder: "Multiple OpenAPI3 files found. Select one to preview", + }); + if (selected) { + selectedOpenApi3OutputFiles.set(mainTspFile, selected.path); + return parseOpenApi3File(selected.path); + } else { + logger.info("No OpenAPI3 file selected"); + return; + } + } +} + +async function parseOpenApi3File(filePath: string): Promise { + const json = await parseJsonFromFile(filePath); + if (json) { + return json; + } + + logger.error(`Failed to load OpenAPI3 file: ${filePath}`, [], { + showOutput: true, + showPopup: true, + }); + return; +} diff --git a/packages/typespec-vscode/swagger-ui/swagger-initializer.js b/packages/typespec-vscode/swagger-ui/swagger-initializer.js new file mode 100644 index 00000000000..8ebb50a1bb0 --- /dev/null +++ b/packages/typespec-vscode/swagger-ui/swagger-initializer.js @@ -0,0 +1,25 @@ +/** + * Customized initialization script for Swagger UI. + */ +window.onload = function () { + window.addEventListener("message", (event) => { + const message = event.data; // The JSON data our extension sent + + switch (message.command) { + case "load": + window.ui = SwaggerUIBundle({ + spec: message.param, + deepLinking: true, // keep expanded tag or operation after reloading + dom_id: "#swagger-ui", + presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], + plugins: [], + layout: "BaseLayout", + }); + break; + default: + break; + } + }); + const vscode = acquireVsCodeApi(); + vscode.postMessage({ event: "initialized" }); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a861350df25..c51284a31cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2149,9 +2149,15 @@ importers: rollup: specifier: ~4.31.0 version: 4.31.0 + rollup-plugin-copy: + specifier: ^3.5.0 + version: 3.5.0 semver: specifier: ^7.6.3 version: 7.6.3 + swagger-ui-dist: + specifier: ^5.18.2 + version: 5.18.2 typescript: specifier: ~5.7.3 version: 5.7.3 @@ -6011,9 +6017,15 @@ packages: '@types/express@5.0.0': resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + '@types/geojson@7946.0.14': resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -6050,6 +6062,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} @@ -7489,6 +7504,9 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + colorspace@1.1.4: resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} @@ -8759,6 +8777,10 @@ packages: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} @@ -8916,6 +8938,10 @@ packages: globalyzer@0.1.0: resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + globby@10.0.1: + resolution: {integrity: sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==} + engines: {node: '>=8'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -9498,6 +9524,10 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-plain-object@3.0.1: + resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} + engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -11647,6 +11677,10 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup-plugin-copy@3.5.0: + resolution: {integrity: sha512-wI8D5dvYovRMx/YYKtUNt3Yxaw4ORC9xo6Gt9t22kveWz1enG9QrhVlagzwrxSC455xD1dHMKhIJkbsQ7d48BA==} + engines: {node: '>=8.3'} + rollup-plugin-visualizer@5.14.0: resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} engines: {node: '>=18'} @@ -18265,8 +18299,17 @@ snapshots: '@types/qs': 6.9.17 '@types/serve-static': 1.15.7 + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 22.10.10 + '@types/geojson@7946.0.14': {} + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.10.10 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -18299,6 +18342,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/minimatch@5.1.2': {} + '@types/minimist@1.2.5': {} '@types/mocha@10.0.10': {} @@ -20327,6 +20372,8 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + colorette@1.4.0: {} + colorspace@1.1.4: dependencies: color: 3.2.1 @@ -21920,6 +21967,12 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-minipass@2.1.0: dependencies: minipass: 3.3.6 @@ -22101,6 +22154,17 @@ snapshots: globalyzer@0.1.0: {} + globby@10.0.1: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -22844,6 +22908,8 @@ snapshots: dependencies: isobject: 3.0.1 + is-plain-object@3.0.1: {} + is-potential-custom-element-name@1.0.1: {} is-promise@4.0.0: {} @@ -25607,6 +25673,14 @@ snapshots: robust-predicates@3.0.2: {} + rollup-plugin-copy@3.5.0: + dependencies: + '@types/fs-extra': 8.1.5 + colorette: 1.4.0 + fs-extra: 8.1.0 + globby: 10.0.1 + is-plain-object: 3.0.1 + rollup-plugin-visualizer@5.14.0(rollup@4.34.6): dependencies: open: 8.4.2