From 96c4cc81537f4d1a5563bdfad966e6b1369e2340 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 8 Jan 2026 14:26:41 -0800 Subject: [PATCH 1/4] Initial files migrated from azure-sdk-tools. --- packages/typespec-apiview/.c8rc.json | 3 + packages/typespec-apiview/.editorconfig | 12 + packages/typespec-apiview/.eslintrc.cjs | 6 + packages/typespec-apiview/.prettierignore | 319 ++++ packages/typespec-apiview/.prettierrc.yaml | 9 + packages/typespec-apiview/.vscode/launch.json | 30 + packages/typespec-apiview/CHANGELOG.md | 104 ++ packages/typespec-apiview/README.md | 105 ++ packages/typespec-apiview/ci.yml | 81 + packages/typespec-apiview/cspell.yaml | 68 + packages/typespec-apiview/package.json | 82 + packages/typespec-apiview/src/apiview.ts | 1341 ++++++++++++++++ packages/typespec-apiview/src/emitter.ts | 123 ++ packages/typespec-apiview/src/index.ts | 4 + packages/typespec-apiview/src/lib.ts | 55 + .../typespec-apiview/src/namespace-model.ts | 311 ++++ packages/typespec-apiview/src/schemas.ts | 253 +++ .../typespec-apiview/src/testing/index.ts | 6 + packages/typespec-apiview/src/util.ts | 44 + packages/typespec-apiview/src/version.ts | 1 + .../test/apiview-options.test.ts | 64 + .../typespec-apiview/test/apiview.test.ts | 1366 +++++++++++++++++ packages/typespec-apiview/test/test-host.ts | 125 ++ packages/typespec-apiview/tsconfig.json | 29 + packages/typespec-apiview/vitest.config.ts | 26 + 25 files changed, 4567 insertions(+) create mode 100644 packages/typespec-apiview/.c8rc.json create mode 100644 packages/typespec-apiview/.editorconfig create mode 100644 packages/typespec-apiview/.eslintrc.cjs create mode 100644 packages/typespec-apiview/.prettierignore create mode 100644 packages/typespec-apiview/.prettierrc.yaml create mode 100644 packages/typespec-apiview/.vscode/launch.json create mode 100644 packages/typespec-apiview/CHANGELOG.md create mode 100644 packages/typespec-apiview/README.md create mode 100644 packages/typespec-apiview/ci.yml create mode 100644 packages/typespec-apiview/cspell.yaml create mode 100644 packages/typespec-apiview/package.json create mode 100644 packages/typespec-apiview/src/apiview.ts create mode 100644 packages/typespec-apiview/src/emitter.ts create mode 100644 packages/typespec-apiview/src/index.ts create mode 100644 packages/typespec-apiview/src/lib.ts create mode 100644 packages/typespec-apiview/src/namespace-model.ts create mode 100644 packages/typespec-apiview/src/schemas.ts create mode 100644 packages/typespec-apiview/src/testing/index.ts create mode 100644 packages/typespec-apiview/src/util.ts create mode 100644 packages/typespec-apiview/src/version.ts create mode 100644 packages/typespec-apiview/test/apiview-options.test.ts create mode 100644 packages/typespec-apiview/test/apiview.test.ts create mode 100644 packages/typespec-apiview/test/test-host.ts create mode 100644 packages/typespec-apiview/tsconfig.json create mode 100644 packages/typespec-apiview/vitest.config.ts diff --git a/packages/typespec-apiview/.c8rc.json b/packages/typespec-apiview/.c8rc.json new file mode 100644 index 0000000000..6ce87a95de --- /dev/null +++ b/packages/typespec-apiview/.c8rc.json @@ -0,0 +1,3 @@ +{ + "reporter": ["cobertura", "json", "text"] +} \ No newline at end of file diff --git a/packages/typespec-apiview/.editorconfig b/packages/typespec-apiview/.editorconfig new file mode 100644 index 0000000000..88bf97c508 --- /dev/null +++ b/packages/typespec-apiview/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/packages/typespec-apiview/.eslintrc.cjs b/packages/typespec-apiview/.eslintrc.cjs new file mode 100644 index 0000000000..a02edcee2a --- /dev/null +++ b/packages/typespec-apiview/.eslintrc.cjs @@ -0,0 +1,6 @@ +require("@typespec/eslint-config-typespec/patch/modern-module-resolution"); + +module.exports = { + extends: "@typespec/eslint-config-typespec", + parserOptions: { tsconfigRootDir: __dirname }, +}; diff --git a/packages/typespec-apiview/.prettierignore b/packages/typespec-apiview/.prettierignore new file mode 100644 index 0000000000..645633c070 --- /dev/null +++ b/packages/typespec-apiview/.prettierignore @@ -0,0 +1,319 @@ +#------------------------------------------------------------------------------------------------------------------- +# Keep this section in sync with .gitignore +#------------------------------------------------------------------------------------------------------------------- + +## Ignore generated code +PackageTest/NugetPackageTest/Generated +src/generator/AutoRest.NodeJS.Tests/AcceptanceTests/*.js + +## Ignore user-specific files, temporary files, build results, etc. +compare-results/* + +# User-specific files +*.suo +*.user +*.sln.docstates +.vs +launchSettings.json + +# Build results +binaries/ +[Dd]ebug*/ +[Rr]elease/ + +[Tt]est[Rr]esult* +[Bb]uild[Ll]og.* +[Bb]uild.out + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.vspscc +*.vssscc +.builds + +*.pidb + +*.log* +*.scc +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf + +# Visual Studio profiler +*.psess +*.vsp + +# VS Code settings +*.vscode + +# Code analysis +*.CodeAnalysisLog.xml + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a Visual Studio add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish + +# Publish Web Output +*.[Pp]ublish.xml + +# Others +[Bb]in +[Oo]bj +sql +*.Cache +ClientBin +[Ss]tyle[Cc]op.* +~$* +*.dbmdl + +# Build tasks +[Tt]ools/*.dll +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# Azure Tooling # +node_modules +.ntvs_analysis.dat + +# Eclipse # +*.pydevproject +.project +.metadata +bin/** +tmp/** +tmp/**/* +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# Xamarin # +*.userprefs + +# Other Tooling # +.classpath +.project +target +build +reports +.gradle +.idea +*.iml +Tools/7-Zip +.gitrevision + +# Sensitive files +*.keys +*.pfx +*.cer +*.pem +*.jks + +# Backup & report files from converting a project to a new version of VS. +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML + +# Mac OS # +.DS_Store +.DS_Store? + +# Windows # +Thumbs.db + +# Mono +*dll.mdb +*exe.mdb + +#old nuget restore folder +.nuget/ +src/generator/AutoRest.Ruby*Tests/Gemfile.lock +src/generator/AutoRest.Ruby*/*/RspecTests/Generated/* + +#netcore +/NetCore +*.lock.json + +#dnx installation +dnx-clr-win-x86*/ +dnx-coreclr-win-x86*/ +/dnx + +# Gemfile.lock +Gemfile.lock + +# go ignore +src/generator/AutoRest.Go.Tests/pkg/* +src/generator/AutoRest.Go.Tests/bin/* +src/generator/AutoRest.Go.Tests/src/github.com/* +src/generator/AutoRest.Go.Tests/src/tests/generated/* +src/generator/AutoRest.Go.Tests/src/tests/vendor/* +src/generator/AutoRest.Go.Tests/src/tests/glide.lock + +autorest/**/*.js +core/**/*.js + +*.js.map + +# backup files +*~ + +#client runtime +src/client/**/* + +src/extension/old/**/* +*.d.ts + +src/bootstrapper +src/extension/out +src/next-gen + +package/nuget/tools +package/chocolatey/*.nupkg + +Samples/**/*.map + +# npm (we do want to test for most recent versions) +**/package-lock.json +**/dist/ +src/*/nm +/nm/ +*.tgz + + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +packages/*/coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +# Common toolchain intermediate files +temp + +# Rush files +common/temp/** +package-deps.json + +# Code generation output for regression tests +core/test/regression + +#------------------------------------------------------------------------------------------------------------------- +# Prettier-specific overrides +#------------------------------------------------------------------------------------------------------------------- + +# Rush files +common/changes/ +common/scripts/ +CHANGELOG.* + +# Package manager files +pnpm-lock.yaml +yarn.lock +package-lock.json +shrinkwrap.json + +# Build outputs +dist + +# MICROSOFT SECURITY.md +/SECURITY.md \ No newline at end of file diff --git a/packages/typespec-apiview/.prettierrc.yaml b/packages/typespec-apiview/.prettierrc.yaml new file mode 100644 index 0000000000..99d98ab781 --- /dev/null +++ b/packages/typespec-apiview/.prettierrc.yaml @@ -0,0 +1,9 @@ +trailingComma: "all" +printWidth: 120 +quoteProps: "consistent" +endOfLine: lf +tabWidth: 2 +arrowParens: always +plugins: + - "./node_modules/@typespec/prettier-plugin-typespec" +overrides: [{ "files": "*.tsp", "options": { "parser": "typespec" } }] diff --git a/packages/typespec-apiview/.vscode/launch.json b/packages/typespec-apiview/.vscode/launch.json new file mode 100644 index 0000000000..5510aa3c23 --- /dev/null +++ b/packages/typespec-apiview/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug TypeSpec APIView Emitter", + "program": "${workspaceFolder}/node_modules/@typespec/compiler/entrypoints/cli.js", + "args": [ + "compile", + "C:/repos/azure-rest-api-specs/specification/orbital/Microsoft.PlanetaryComputer/main.tsp", + "--emit=C:/repos/azure-sdk-tools/tools/apiview/emitters/typespec-apiview/dist/src/index.js" + ], + "smartStep": true, + "sourceMaps": true, + "skipFiles": ["/**/*.js"], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js", + "${workspaceFolder}/node_modules/**/*.js" + ], + "cwd": "${workspaceFolder}", + "presentation": { + "order": 1 + } + } + ] +} \ No newline at end of file diff --git a/packages/typespec-apiview/CHANGELOG.md b/packages/typespec-apiview/CHANGELOG.md new file mode 100644 index 0000000000..371e983694 --- /dev/null +++ b/packages/typespec-apiview/CHANGELOG.md @@ -0,0 +1,104 @@ +# Release History + +## Version 0.7.2 (05-14-2025) +Fixed issue where `cannot deindent with an indent` could cause the generator to crash. + +## Version 0.7.1 (05-14-2025) +Support new TypeSpec compiler. +Fixed issue where `HasSuffixSpace` could cause the generator to crash. + +## Version 0.7.0 (04-03-2025) +Support new TypeSpec compiler. + +## Version 0.6.0 (03-20-2025) +Support new TypeSpec compiler. +**BREAKING CHANGE**: Removed support for `--version` parameter. Multi-versioned specs will be emitter as-is. + +## Version 0.5.1 (02-27-2025) +Support new TypeSpec syntax. + +## Version 0.5.0 (01-08-2025) +Support new internal APIView tree-structure. + +## Version 0.4.9 (07-09-2024) +Fix issue where "unknown" was rendered as "any". +Support value syntax for objects and arrays. +Support const statements in namespaces. + +## Version 0.4.8 (04-18-2024) +Display suppressions in APIView. +Resolve visual anomalies. + +## Version 0.4.7 (03-22-2024) +Support TypeSpec string templates. +Fix display issue with templated aliases. +Ensure alias statements end with semicolon. + +## Version 0.4.6 (03-08-2024) +Support CrossLanguagePackageId. + +## Version 0.4.5 (01-29-2024) +Support named template arguments. +Support unnamed unions. +Support cross-language definition IDs. + +## Version 0.4.4 (04-18-2023) +Support future beta releases of TypeSpec. + +## Version 0.4.3 (04-17-2023) +Support latest release of TypeSpec. + +## Version 0.4.2 (03-16-2023) +Support latest release of TypeSpec. + +## Version 0.4.1 (03-13-2023) +Fixed issue where enums with spread members would cause the generator to crash. + +## Version 0.4.0 (03-06-2023) +Update for rename of Cadl to TypeSpec. + +## Version 0.3.5 (02-10-2023) +Support latest release of Cadl compiler. +**BREAKING CHANGE**: Removed the `--namespace` emitter option. +Added the `--service` emitter option to support filtering output for multi-service specs. +Emitter options `--output-file` and `--version` cannot be used with multi-service specs unless the + `--service` option is provided. +Added the `--include-global-namespace` option to permit including the global namespace in the token file. +Fixed issue where namespaces that are not proper subnamespaces may be included in the token file. + +## Version 0.3.4 (01-13-2023) +Support latest release of Cadl compiler. + +## Version 0.3.3 (01-03-2023) +Fixed issue where some type references were not navigable. + +## Version 0.3.2 (12-20-2022) +Changed structure of APIView navigation so that aliases appear under a separate "Alias" section, instead of + within the existing "Models" section. Will likely result in a non-API-related diff with prior APIView versions. + +## Version 0.3.1 (12-9-2022) +Support Cadl scalars. + +## Version 0.3.0 (11-15-2022) +Add support for aliases and augment decorators. + +## Version 0.2.1 (10-27-2022) +Change behavior of `version` emitter option so that if it is not supplied, APIView will be generated for the + un-projected Cadl, rendering all versioning decorators. Supplying `version` allows the user to project a + specific version. + +## Version 0.2.0 (10-26-2022) +Support `namespace` emitter option to filter the appropriate namespace when it cannot be automatically resolved. + This is primarily intended for creating APIViews for libraries. +Support `version` emitter option to choose which version of a multi-versioned spec to emit. Specs with a single + version can omit this. Multi-version specs can omit this if emitting the latest version. +No longer suppress `@doc`, `@summary`, and `@example` decorators. These can be toggled using the APIView UI. +Support rendering multi-line strings. +Change default path for generating artifacts. + +## Version 0.1.1 (10-13-2022) +Support compiler-level noEmit option. +Support `output-dir` emitter option. + +## Version 0.1.0 (10-5-2022) +Initial release. \ No newline at end of file diff --git a/packages/typespec-apiview/README.md b/packages/typespec-apiview/README.md new file mode 100644 index 0000000000..cf5839de2f --- /dev/null +++ b/packages/typespec-apiview/README.md @@ -0,0 +1,105 @@ +# TypeSpec APIView Emitter + +This package provides the [TypeSpec](https://github.com/microsoft/typespec) emitter to produce APIView token file output from TypeSpec source. + +## Install + +Add `@azure-tools/typespec-apiview` to your `package.json` and run `npm install`. + +## Emit APIView spec + +1. Via the command line + +```bash +tsp compile {path to typespec project} --emit=@azure-tools/typespec-apiview +``` + +2. Via the config + +Add the following to the `typespec-project.yaml` file. + +```yaml +emitters: + @azure-tools/typespec-apiview: true +``` + +For configuration [see options](#emitter-options) + +## Create API View + +1. Log in to the API View site (apiview.dev)[https://apiview.dev]. +2. Click the blue "Create Review" button in the bottom-right corner of the screen. +3. In the first block, load the token file generated by the `typespec-apiview` emitter. + +## Revise API View + +1. Log in to the API View site (apiview.dev)[https://apiview.dev]. +2. Navigate to your review. +3. Click the "Revisions" tab in the top left. +4. Click the blue "Add Revision" button in the bottom-right corner of the screen. +5. In the first block, load the token file generated by the `typespec-apiview` emitter. + +## Use APIView-specific decorators: + +Currently there are no APIView-specific decorators... + +## Emitter options: + +Emitter options can be configured via the `tspconfig.yaml` configuration: + +```yaml +emitters: + '@azure-tools/typespec-apiview': + : + + +# For example +emitters: + '@azure-tools/typespec-apiview': + output-file: my-custom-apiview.json +``` + +or via the command line with + +```bash +--option "@azure-tools/typespec-apiview.=" + +# For example +--option "@azure-tools/typespec-apiview.output-file=my-custom-apiview.json" +``` + +### `emitter-output-dir` + +Configure the name of the output directory. Default is `tsc-output/@azure-tools/typespec-apiview`. + +### `include-global-namespace` + +Normally, APIView will filter all namespaces and only output those in the service namespace and any +subnamespaces. This is to filter out types that come from the TypeSpec compiler and supporting libraries. +This setting, if `true`, tells APIView to output the contents of the global (empty) namespace, which +would normally be excluded. + +### `service` + +Filter output to a single service definition. If omitted, all service defintions will be +output as separate APIView token files. + +### `output-file` + +Configure the name of the output JSON token file relative to the `output-dir`. For multi-service +specs, this option cannot be supplied unless the `service` option is also set. If outputting +all services in a multi-service spec, the output filename will be the service root namespace with the +`-apiview.json` suffix. Otherwise, the default is `apiview.json`. + +### `version` + +For multi-versioned TypeSpec, this parameter is used to control which version to emit. This +is not required for single-version specs. For multi-versioned specs, the unprojected TypeSpec will +be rendered if this is not supplied. For multi-service specs, this option cannot be supplied +unless the `service` option is also set. + +## See also + +- [TypeSpec Getting Started](https://github.com/microsoft/typespec#getting-started) +- [TypeSpec Tutorial](https://github.com/microsoft/typespec/blob/main/docs/tutorial.md) +- [TypeSpec for the OpenAPI Developer](https://github.com/microsoft/typespec/blob/main/docs/typespec-for-openapi-dev.md) diff --git a/packages/typespec-apiview/ci.yml b/packages/typespec-apiview/ci.yml new file mode 100644 index 0000000000..0c15b46d1f --- /dev/null +++ b/packages/typespec-apiview/ci.yml @@ -0,0 +1,81 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. +trigger: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - tools/apiview/emitters/typespec-apiview + +pr: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - tools/apiview/emitters/typespec-apiview + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-publish-js.yml + parameters: + BuildStageName: Build + ArtifactName: apiview + PackageJsonPath: $(Build.SourcesDirectory)/tools/apiview/emitters/typespec-apiview + BuildStages: + - stage: 'Build' + variables: + - template: /eng/pipelines/templates/variables/image.yml + - name: NodeVersion + value: '22.x' + - name: TypeSpecEmitterDirectory + value: 'tools/apiview/emitters/typespec-apiview' + jobs: + - job: 'Build' + + pool: + name: $(LINUXPOOL) + image: $(LINUXVMIMAGE) + os: linux + + steps: + - task: NodeTool@0 + inputs: + versionSpec: '$(NodeVersion)' + displayName: 'Use NodeJS $(NodeVersion)' + + - script: | + npm ci + workingDirectory: $(TypeSpecEmitterDirectory) + displayName: "Install npm packages for TypeSpec emitter" + + - script: | + npm ls -a || true + workingDirectory: $(TypeSpecEmitterDirectory) + displayName: "List npm packages for TypeSpec emitter" + condition: succeededOrFailed() + + - script: | + npm run-script build + workingDirectory: $(TypeSpecEmitterDirectory) + displayName: "Build TypeSpec emitter" + + - script: | + npm run-script test + workingDirectory: $(TypeSpecEmitterDirectory) + displayName: "Test TypeSpec emitter" + + - pwsh: | + npm pack $(TypeSpecEmitterDirectory) + Copy-Item ./*.tgz $(Build.ArtifactStagingDirectory) + displayName: "Pack TypeSpec Emitter" + + - template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml + parameters: + ArtifactName: apiview + ArtifactPath: $(Build.ArtifactStagingDirectory) diff --git a/packages/typespec-apiview/cspell.yaml b/packages/typespec-apiview/cspell.yaml new file mode 100644 index 0000000000..7081335ee1 --- /dev/null +++ b/packages/typespec-apiview/cspell.yaml @@ -0,0 +1,68 @@ +version: "0.2" +language: en +allowCompoundWords: true +dictionaries: + - node + - typescript +words: + - autorest + - azsdkengsys + - azurecr + - blockful + - blockless + - typespec + - Contoso + - CRUDL + - devdriven + - dogfood + - eastus + - esbuild + - globby + - Hdvcmxk + - inmemory + - instanceid + - intrinsics + - ints + - jsyaml + - msbuild + - MSRC + - munge + - mylib + - nostdlib + - oapi + - oneof + - onig + - onigasm + - onwarn + - openapi + - openapiv + - picocolors + - pnpm + - proto + - protobuf + - protoc + - regen + - rpaas + - rushx + - safeint + - strs + - TSES + - unassignable + - Uncapitalize + - uninstantiated + - unioned + - unprojected + - unsourced + - unversioned + - vsix + - vswhere + - westus + - xplat + - deindent +ignorePaths: + - "**/node_modules/**" + - "**/dist/**" + - "**/dist-dev/**" + - "**/tsp-output/**" +enableFiletypes: + - tsp \ No newline at end of file diff --git a/packages/typespec-apiview/package.json b/packages/typespec-apiview/package.json new file mode 100644 index 0000000000..34f721d617 --- /dev/null +++ b/packages/typespec-apiview/package.json @@ -0,0 +1,82 @@ +{ + "name": "@azure-tools/typespec-apiview", + "version": "0.7.2", + "author": "Microsoft Corporation", + "description": "Library for emitting APIView token files from TypeSpec", + "homepage": "https://github.com/Azure/azure-sdk-tools", + "readme": "https://github.com/Azure/azure-sdk-tools/blob/master/README.md", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Azure/azure-sdk-tools.git" + }, + "bugs": { + "url": "https://github.com/Azure/azure-sdk-tools/issues" + }, + "keywords": [ + "typespec", + "apiview" + ], + "type": "module", + "main": "dist/src/index.js", + "exports": { + ".": "./dist/src/index.js", + "./testing": "./dist/src/testing/index.js" + }, + "typesVersions": { + "*": { + "*": [ + "./dist/src/index.d.ts" + ], + "testing": [ + "./dist/src/testing/index.d.ts" + ] + } + }, + "tspMain": "dist/src/index.js", + "engines": { + "node": ">=16.0.0" + }, + "scripts": { + "clean": "rimraf ./dist ./temp ./tsp-output", + "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", + "purge": "rimraf ./node_modules ./package-lock.json", + "build": "npm run clean && npm run prebuild && tsc -p . && npm run lint-typespec-library", + "watch": "tsc -p . --watch", + "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", + "test": "vitest", + "lint": "eslint . --ext .ts --max-warnings=0", + "lint:fix": "eslint . --fix --ext .ts", + "api-view": "node ./node_modules/@typespec/compiler/entrypoints/cli.js compile main.tsp --emit=@azure-tools/typespec-apiview" + }, + "files": [ + "lib/*.tsp", + "dist/**", + "!dist/test/**" + ], + "peerDependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/versioning": ">=0.67 <1.0" + }, + "devDependencies": { + "@azure-tools/typespec-azure-core": ">=0.53 <1.0", + "@types/node": "~18.11.19", + "@typespec/eslint-plugin": ">=0.67 <1.0", + "@typespec/library-linter": ">=0.67 <1.0", + "@typespec/prettier-plugin-typespec": ">=0.67 <1.0", + "@typespec/rest": ">=0.67 <1.0", + "@typespec/http": "^1.0.0", + "@vitest/coverage-v8": "^3.0.7", + "c8": "^9.1.0", + "cspell": "^8.8.1", + "eslint": "^9.11.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-unicorn": "^53.0.0", + "eslint-plugin-vitest": "^0.5.4", + "prettier": "~3.2.5", + "rimraf": "~5.0.7", + "source-map-support": "^0.5.19", + "typescript": "~5.4.5", + "vitest": "^3.0.7" + } +} diff --git a/packages/typespec-apiview/src/apiview.ts b/packages/typespec-apiview/src/apiview.ts new file mode 100644 index 0000000000..b5ba7dbba3 --- /dev/null +++ b/packages/typespec-apiview/src/apiview.ts @@ -0,0 +1,1341 @@ +import { + Expression, + getNamespaceFullName, + getSourceLocation, + Namespace, + navigateProgram, + Program +} from "@typespec/compiler"; +import { + AliasStatementNode, + ArrayExpressionNode, + ArrayLiteralNode, + AugmentDecoratorStatementNode, + BaseNode, + BooleanLiteralNode, + CallExpressionNode, + ConstStatementNode, + DecoratorExpressionNode, + DirectiveExpressionNode, + EnumMemberNode, + EnumSpreadMemberNode, + EnumStatementNode, + IdentifierNode, + InterfaceStatementNode, + IntersectionExpressionNode, + MemberExpressionNode, + ModelExpressionNode, + ModelPropertyNode, + ModelSpreadPropertyNode, + ModelStatementNode, + NumericLiteralNode, + ObjectLiteralNode, + ObjectLiteralPropertyNode, + ObjectLiteralSpreadPropertyNode, + OperationSignatureDeclarationNode, + OperationSignatureReferenceNode, + OperationStatementNode, + ScalarStatementNode, + StringLiteralNode, + StringTemplateExpressionNode, + StringTemplateHeadNode, + StringTemplateSpanNode, + SyntaxKind, + TemplateArgumentNode, + TemplateParameterDeclarationNode, + TupleExpressionNode, + TypeReferenceNode, + UnionExpressionNode, + UnionStatementNode, + UnionVariantNode, + ValueOfExpressionNode +} from "@typespec/compiler/ast"; +import { generateId, NamespaceModel } from "./namespace-model.js"; +import { LIB_VERSION } from "./version.js"; +import { CodeDiagnostic, CodeDiagnosticLevel, CodeFile, NavigationItem, ReviewLine, ReviewToken, ReviewTokenOptions, TokenKind } from "./schemas.js"; +import { NamespaceStack, reviewLineText } from "./util.js"; + +export class ApiView { + name: string; + packageName: string; + crossLanguagePackageId: string | undefined; + /** Stores the current line. All helper methods append to this. */ + currentLine: ReviewLine; + /** Stores the parent of the current line. */ + currentParent: ReviewLine | undefined; + /** Stores the stack of parent lines. */ + parentStack: ReviewLine[]; + + reviewLines: ReviewLine[] = []; + navigationItems: NavigationItem[] = []; + diagnostics: CodeDiagnostic[] = []; + packageVersion: string; + + namespaceStack = new NamespaceStack(); + typeDeclarations = new Set(); + includeGlobalNamespace: boolean; + + constructor(name: string, packageName: string, includeGlobalNamespace?: boolean) { + this.name = name; + this.packageName = packageName; + this.packageVersion = "ALL"; + this.includeGlobalNamespace = includeGlobalNamespace ?? false; + this.crossLanguagePackageId = packageName; + this.currentLine = { + LineId: "", + CrossLanguageId: "", + Tokens: [], + Children: [], + } + this.parentStack = [] + this.currentParent = undefined; + this.emitHeader(); + } + + /** Compiles the APIView model for output. */ + compile(program: Program) { + let allNamespaces = new Map(); + + // collect namespaces in program + navigateProgram(program, { + namespace(obj) { + const name = getNamespaceFullName(obj); + allNamespaces.set(name, obj); + }, + }); + allNamespaces = new Map([...allNamespaces].sort()); + + for (const [name, ns] of allNamespaces.entries()) { + if (!this.shouldEmitNamespace(name)) { + continue; + } + // use a fake name to make the global namespace clear + const namespaceName = name === "" ? "::GLOBAL::" : name; + const nsModel = new NamespaceModel(namespaceName, ns, program); + if (nsModel.shouldEmit()) { + this.tokenizeNamespaceModel(nsModel); + this.buildNavigation(nsModel); + } + } + // Enable this if desired to debug the output + //this.appendDebugInfo(); + } + + private appendDebugInfo() { + + function processLines(lines: ReviewLine[]) { + for (const line of lines) { + const lineId = line.LineId; + const relatedLineId = line.RelatedToLine; + const isContextEnd = line.IsContextEndLine + let string = ""; + if (lineId && lineId !== "") { + string += ` LINE_ID: ${lineId} `; + } + if (relatedLineId && relatedLineId !== "") { + string += ` RELATED_TO: ${relatedLineId} `; + } + if (isContextEnd) { + string += " IS_CONTEXT_END TRUE "; + } + if (string !== "") { + line.Tokens.push({ + Kind: TokenKind.Comment, + Value: `// ${string.trim()}`, + }) + } + processLines(line.Children); + } + } + + processLines(this.reviewLines); + } + + /** Attempts to resolve any type references marked as __MISSING__. */ + resolveMissingTypeReferences() { + for (const token of this.currentLine.Tokens) { + if (token.Kind === TokenKind.TypeName && token.NavigateToId === "__MISSING__") { + token.NavigateToId = this.definitionIdFor(token.Value!, this.packageName); + } + } + } + + /** Apply workarounds to the model before output */ + private adjustLines(lines: ReviewLine[]) { + let currentContext: string | undefined = undefined; + let contextMatchFound: boolean = false; + for (const line of lines) { + // run the normal adjust line logic + this.adjustLine(line); + + const lineId = line.LineId; + if (lineId === "Azure.Test.ConstrainedComplex") { + let test = "best"; + } + const relatedTo = line.RelatedToLine; + const isContextEnd = line.IsContextEndLine; + + if (isContextEnd && relatedTo) { + throw new Error("Context end line should not have a relatedTo line."); + } + + if (currentContext) { + if (lineId === currentContext) { + contextMatchFound = true; + line.RelatedToLine = undefined; + line.IsContextEndLine = false; + } + if (relatedTo && currentContext !== relatedTo) { + if (!contextMatchFound) { + // catches the scenario where the relatedTo line is different within what should be the current context + throw new Error("Mismatched contexts. Expected ${currentContext}, got ${lineId}"); + } else { + // covers the instance where there never is an IsContextEndLine set, which happens if there's no closing brace + // on a separate line + currentContext = relatedTo; + } + } else { + // key to this whole method. This copies RelatedToLine to all lines between the start and end of a context + line.RelatedToLine = currentContext; + } + if (isContextEnd) { + currentContext = undefined; + contextMatchFound = false; + line.RelatedToLine = undefined; + } + } else { + // if currentContext isn't set and we encounter a relatedTo line, set the current context + if (relatedTo) { + currentContext = relatedTo; + // if currentContext isn't set but there's a lineId with childrent, set the current context + } else if (lineId && line.Children.length > 0) { + currentContext = lineId; + contextMatchFound = true; + // If a context end is found without a start, then ignore the contextEnd + } else if (isContextEnd) { + line.IsContextEndLine = false; + } + } + } + } + + private adjustLine(line: ReviewLine) { + for (const token of line.Tokens) { + this.adjustToken(token); + } + this.adjustLines(line.Children); + } + + private adjustToken(token: ReviewToken) { + // the server has a bizarre "true" default for HasSuffixSpace that we + // need to account for. Also, we can delete the property if it's true + // since that's the server default. + token.HasSuffixSpace = token.HasSuffixSpace ?? false; + if (token.HasSuffixSpace) { + delete token.HasSuffixSpace; + } + } + + /** Output the APIView model to the CodeFile JSON format. */ + asCodeFile(): CodeFile { + this.adjustLines(this.reviewLines); + return { + Name: this.name, + PackageName: this.packageName, + PackageVersion: this.packageVersion, + ParserVersion: LIB_VERSION, + Language: "TypeSpec", + LanguageVariant: undefined, + CrossLanguagePackageId: this.crossLanguagePackageId, + ReviewLines: this.reviewLines, + Diagnostics: this.diagnostics, + Navigation: this.navigationItems, + }; + + } + + /** Outputs the APIView model to a string approximation of what will display on the web. */ + asString(): string { + return this.reviewLines.map(l => reviewLineText(l, 0)).join("\n"); + } + + private token(kind: TokenKind, value: string, options?: ReviewTokenOptions) { + this.currentLine.Tokens.push({ + Kind: kind, + Value: value, + ...options, + }); + } + + private indent() { + // ensure no trailing space at the end of the line + try { + const lastToken = this.currentLine.Tokens[this.currentLine.Tokens.length - 1]; + lastToken.HasSuffixSpace = false; + } catch (e) { + // no tokens, so nothing to do + return; + } + + if (this.currentParent) { + this.currentParent.Children.push(this.currentLine); + this.parentStack.push(this.currentParent); + } else { + this.reviewLines.push(this.currentLine); + } + this.currentParent = this.currentLine; + this.currentLine = { + LineId: "", + CrossLanguageId: "", + Tokens: [], + Children: [], + } + } + + private deindent() { + if (!this.currentParent) { + return; + } + // Ensure that the last line before the deindent has no blank lines + const lastChild = this.currentParent.Children.pop(); + if (lastChild && lastChild.Tokens.length > 0) { + this.currentParent.Children.push(lastChild); + } + this.currentParent = this.parentStack.pop(); + this.currentLine = { + LineId: "", + CrossLanguageId: "", + Tokens: [], + Children: [], + } + } + + /** Set the exact number of desired newlines. */ + private blankLines(count: number = 0) { + this.newline(); + const parentLines = this.currentParent ? this.currentParent.Children : this.reviewLines; + let newlineCount = 0; + if (parentLines.length) { + for (let i = parentLines.length - 1; i >= 0; i--) { + if (parentLines[i].Tokens.length === 0) { + newlineCount++; + } else { + break; + } + } + } + if (newlineCount == count) { + return; + } else if (newlineCount > count) { + const toRemove = newlineCount - count; + for (let i = 0; i < toRemove; i++) { + parentLines.pop(); + } + } else { + for (let i = newlineCount; i < count; i++) { + this.newline(); + } + } + } + + private newline() { + // ensure no trailing space at the end of the line + if (this.currentLine.Tokens.length > 0) { + const lastToken = this.currentLine.Tokens[this.currentLine.Tokens.length - 1]; + lastToken.HasSuffixSpace = false; + const firstToken = this.currentLine.Tokens[0]; + firstToken.HasPrefixSpace = false; + } + + if (this.currentParent) { + this.currentParent.Children.push(this.currentLine); + } else { + this.reviewLines.push(this.currentLine); + } + this.currentLine = { + LineId: "", + CrossLanguageId: "", + Tokens: [], + Children: [], + } + } + + private getLastLine(): ReviewLine | undefined { + if (!this.currentParent) { + return undefined; + } + const lastChild = this.currentParent.Children[this.currentParent.Children.length - 1]; + const lastGrandchild = lastChild?.Children[lastChild.Children.length - 1]; + if (lastGrandchild?.Children.length > 0) { + throw new Error("Unexpected great-grandchild in getLastLine()!"); + } + return lastGrandchild ?? lastChild; + } + + /** + * Places the provided token in the tree based on the provided characters. + * param token The token to snap. + * param characters The characters to snap to. + */ + private snapToken(punctuationToken: ReviewToken, characters: string) { + const allowed = new Set(characters.split("")); + const lastLine = this.getLastLine() ?? this.currentLine; + + // iterate through tokens in reverse order + for (let i = lastLine.Tokens.length - 1; i >= 0; i--) { + const token = lastLine.Tokens[i]; + if (token.Kind === TokenKind.Text) { + // skip blank whitespace tokens + const value = token.Value.trim(); + if (value.length === 0) { + continue; + } else { + // no snapping, so render in place + this.currentLine.Tokens.push(punctuationToken); + return; + } + } else if (token.Kind === TokenKind.Punctuation) { + // ensure no whitespace after the trim character + if (allowed.has(token.Value)) { + token.HasSuffixSpace = false; + punctuationToken.HasSuffixSpace = false; + lastLine.Tokens.push(punctuationToken); + } else { + // no snapping, so render in place + this.currentLine.Tokens.push(punctuationToken); + return; + } + } else { + // no snapping, so render in place + this.currentLine.Tokens.push(punctuationToken); + return; + } + } + } + + private lineMarker(options?: {value?: string, addCrossLanguageId?: boolean, relatedLineId?: string}) { + this.currentLine.LineId = options?.value ?? this.namespaceStack.value(); + this.currentLine.CrossLanguageId = options?.addCrossLanguageId ? (options?.value ?? this.namespaceStack.value()) : undefined; + this.currentLine.RelatedToLine = options?.relatedLineId; + } + + private punctuation(value: string, options?: ReviewTokenOptions & {snapTo?: string, isContextEndLine?: boolean}) { + const snapTo = options?.snapTo; + delete options?.snapTo; + const isContextEndLine = options?.isContextEndLine; + delete options?.isContextEndLine; + + const token = { + Kind: TokenKind.Punctuation, + Value: value, + ...options, + } + + if (snapTo) { + this.snapToken(token, snapTo); + } else { + this.currentLine.Tokens.push(token); + } + if (isContextEndLine) { + this.currentLine.IsContextEndLine = true; + } + } + + private text(text: string, options?: ReviewTokenOptions) { + this.token(TokenKind.Text, text, options); + } + + private keyword(keyword: string, options?: ReviewTokenOptions) { + this.token(TokenKind.Keyword, keyword, options); + } + + private typeDeclaration(typeName: string, typeId: string | undefined, addCrossLanguageId: boolean, options?: ReviewTokenOptions) { + if (typeId) { + if (this.typeDeclarations.has(typeId)) { + throw new Error(`Duplication ID "${typeId}" for declaration will result in bugs.`); + } + this.typeDeclarations.add(typeId); + } + this.lineMarker({value: typeId, addCrossLanguageId: true}); + this.token(TokenKind.TypeName, typeName, options); + } + + private typeReference(typeName: string, options?: ReviewTokenOptions) { + options = options ?? {}; + options.NavigateToId = options.NavigateToId ?? "__MISSING__"; + this.token(TokenKind.TypeName, typeName, {...options}); + } + + private member(name: string, options?: ReviewTokenOptions) { + this.token(TokenKind.MemberName, name, options); + } + + private stringLiteral(value: string, options?: ReviewTokenOptions) { + const lines = value.split("\n"); + if (lines.length === 1) { + this.currentLine.Tokens.push({ + Kind: TokenKind.StringLiteral, + Value: `\u0022${value}\u0022`, + ...options + }); + } else { + this.punctuation(`"""`, options); + this.newline(); + for (const line of lines) { + this.literal(line, options); + this.newline(); + } + this.punctuation(`"""`, options); + } + } + + private literal(value: string, options?: ReviewTokenOptions) { + this.token(TokenKind.StringLiteral, value, options); + } + + private diagnostic(message: string, targetId: string, level: CodeDiagnosticLevel) { + this.diagnostics.push({ + Text: message, + TargetId: targetId, + Level: level, + }) + } + + private shouldEmitNamespace(name: string): boolean { + if (name === "" && this.includeGlobalNamespace) { + return true; + } + if (name === this.packageName) { + return true; + } + if (!name.startsWith(this.packageName)) { + return false; + } + const suffix = name.substring(this.packageName.length); + return suffix.startsWith("."); + } + + private emitHeader() { + const toolVersion = LIB_VERSION; + const headerText = `// Package parsed using @azure-tools/typespec-apiview (version:${toolVersion})`; + this.literal(headerText, {SkipDiff: true}); + this.namespaceStack.push("GLOBAL"); + this.lineMarker(); + this.namespaceStack.pop(); + // TODO: Source URL? + this.blankLines(2) + } + + private tokenize(node: BaseNode) { + let obj; + let last = 0; // track the final index of an array + let parentNamespace: string; + switch (node.kind) { + case SyntaxKind.AliasStatement: + obj = node as AliasStatementNode; + this.namespaceStack.push(obj.id.sv); + this.keyword("alias", {HasSuffixSpace: true}); + this.typeDeclaration(obj.id.sv, this.namespaceStack.value(), true); + this.tokenizeTemplateParameters(obj.templateParameters); + this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.tokenize(obj.value); + this.namespaceStack.pop(); + break; + case SyntaxKind.ArrayExpression: + obj = node as ArrayExpressionNode; + this.tokenize(obj.elementType); + this.punctuation("[]"); + break; + case SyntaxKind.ArrayLiteral: + obj = node as ArrayLiteralNode; + this.punctuation("#["); + last = obj.values.length - 1; + obj.values.forEach((val, i) => { + this.tokenize(val); + if (i !== last) { + this.punctuation(",", {HasSuffixSpace: true}); + } + }); + this.punctuation("]"); + break; + case SyntaxKind.AugmentDecoratorStatement: + obj = node as AugmentDecoratorStatementNode; + const decoratorName = this.getNameForNode(obj.target); + this.namespaceStack.push(decoratorName); + this.punctuation("@@"); + this.tokenizeIdentifier(obj.target, "keyword"); + this.lineMarker(); + if (obj.arguments.length) { + const last = obj.arguments.length - 1; + this.punctuation("("); + this.tokenize(obj.targetType); + if (obj.arguments.length) { + this.punctuation(",", {HasSuffixSpace: true}); + } + for (let x = 0; x < obj.arguments.length; x++) { + const arg = obj.arguments[x]; + this.tokenize(arg); + if (x !== last) { + this.punctuation(",", {HasSuffixSpace: true}); + } + } + this.punctuation(")"); + this.namespaceStack.pop(); + } + break; + case SyntaxKind.BooleanLiteral: + obj = node as BooleanLiteralNode; + this.literal(obj.value.toString()); + break; + case SyntaxKind.BlockComment: + throw new Error(`Case "BlockComment" not implemented`); + case SyntaxKind.TypeSpecScript: + throw new Error(`Case "TypeSpecScript" not implemented`); + case SyntaxKind.ConstStatement: + obj = node as ConstStatementNode; + this.namespaceStack.push(obj.id.sv); + this.keyword("const", {HasSuffixSpace: true}); + this.tokenizeIdentifier(obj.id, "declaration"); + this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.tokenize(obj.value); + this.namespaceStack.pop(); + break; + case SyntaxKind.DecoratorExpression: + obj = node as DecoratorExpressionNode; + parentNamespace = this.namespaceStack.value(); + this.namespaceStack.push(generateId(obj)!); + this.punctuation("@"); + this.tokenizeIdentifier(obj.target, "keyword"); + this.lineMarker({relatedLineId: parentNamespace}); + if (obj.arguments.length) { + last = obj.arguments.length - 1; + this.punctuation("("); + for (let x = 0; x < obj.arguments.length; x++) { + const arg = obj.arguments[x]; + this.tokenize(arg); + if (x !== last) { + this.punctuation(",", {HasSuffixSpace: true}); + } + } + this.punctuation(")"); + } + this.namespaceStack.pop(); + break; + case SyntaxKind.DirectiveExpression: + obj = node as DirectiveExpressionNode; + parentNamespace = this.namespaceStack.value(); + this.namespaceStack.push(generateId(node)!); + this.lineMarker({relatedLineId: parentNamespace}); + this.keyword(`#${obj.target.sv}`, {HasSuffixSpace: true}); + for (const arg of obj.arguments) { + switch (arg.kind) { + case SyntaxKind.StringLiteral: + this.stringLiteral(arg.value, {HasSuffixSpace: true}); + break; + case SyntaxKind.Identifier: + this.stringLiteral(arg.sv, {HasSuffixSpace: true}); + break; + } + } + this.newline(); + this.namespaceStack.pop(); + break; + case SyntaxKind.EmptyStatement: + throw new Error(`Case "EmptyStatement" not implemented`); + case SyntaxKind.EnumMember: + obj = node as EnumMemberNode; + this.tokenizeDecoratorsAndDirectives(obj.decorators, obj.directives, false); + this.tokenizeIdentifier(obj.id, "member"); + this.lineMarker({addCrossLanguageId: true}); + if (obj.value) { + this.punctuation(":", {HasSuffixSpace: true}); + this.tokenize(obj.value); + } + break; + case SyntaxKind.EnumSpreadMember: + obj = node as EnumSpreadMemberNode; + this.punctuation("..."); + this.tokenize(obj.target); + this.lineMarker(); + break; + case SyntaxKind.EnumStatement: + this.tokenizeEnumStatement(node as EnumStatementNode); + break; + case SyntaxKind.JsNamespaceDeclaration: + throw new Error(`Case "JsNamespaceDeclaration" not implemented`); + case SyntaxKind.JsSourceFile: + throw new Error(`Case "JsSourceFile" not implemented`); + case SyntaxKind.Identifier: + obj = node as IdentifierNode; + const id = this.namespaceStack.value(); + this.typeReference(obj.sv, {NavigateToId: id}); + break; + case SyntaxKind.ImportStatement: + throw new Error(`Case "ImportStatement" not implemented`); + case SyntaxKind.IntersectionExpression: + obj = node as IntersectionExpressionNode; + for (let x = 0; x < obj.options.length; x++) { + const opt = obj.options[x]; + this.tokenize(opt); + if (x !== obj.options.length - 1) { + this.punctuation("&", {HasPrefixSpace: true, HasSuffixSpace: true}); + } + } + break; + case SyntaxKind.InterfaceStatement: + this.tokenizeInterfaceStatement(node as InterfaceStatementNode); + break; + case SyntaxKind.InvalidStatement: + throw new Error(`Case "InvalidStatement" not implemented`); + case SyntaxKind.LineComment: + throw new Error(`Case "LineComment" not implemented`); + case SyntaxKind.MemberExpression: + this.tokenizeIdentifier(node as MemberExpressionNode, "reference"); + break; + case SyntaxKind.ModelExpression: + this.indent(); + this.tokenizeModelExpression(node as ModelExpressionNode, {isOperationSignature: false}); + this.deindent(); + break; + case SyntaxKind.ModelProperty: + this.tokenizeModelProperty(node as ModelPropertyNode, false); + break; + case SyntaxKind.ModelSpreadProperty: + obj = node as ModelSpreadPropertyNode; + this.punctuation("..."); + this.tokenize(obj.target); + this.lineMarker(); + break; + case SyntaxKind.ModelStatement: + obj = node as ModelStatementNode; + this.tokenizeModelStatement(obj); + break; + case SyntaxKind.NamespaceStatement: + throw new Error(`Case "NamespaceStatement" not implemented`); + case SyntaxKind.NeverKeyword: + this.keyword("never"); + break; + case SyntaxKind.NumericLiteral: + obj = node as NumericLiteralNode; + this.literal(obj.value.toString()); + break; + case SyntaxKind.ObjectLiteral: + obj = node as ObjectLiteralNode; + this.punctuation("#{"); + this.indent(); + last = obj.properties.length - 1; + obj.properties.forEach((prop, i) => { + this.tokenize(prop); + if (i !== last) { + this.punctuation(",", {HasSuffixSpace: false}); + } + this.newline(); + }); + this.deindent(); + this.punctuation("}"); + break; + case SyntaxKind.ObjectLiteralProperty: + obj = node as ObjectLiteralPropertyNode; + this.tokenizeIdentifier(obj.id, "member"); + this.punctuation(":", {HasSuffixSpace: true}); + this.tokenize(obj.value); + break; + case SyntaxKind.ObjectLiteralSpreadProperty: + obj = node as ObjectLiteralSpreadPropertyNode; + // TODO: Whenever there is an example? + throw new Error(`Case "ObjectLiteralSpreadProperty" not implemented`); + case SyntaxKind.OperationStatement: + this.tokenizeOperationStatement(node as OperationStatementNode); + break; + case SyntaxKind.OperationSignatureDeclaration: + obj = node as OperationSignatureDeclarationNode; + this.punctuation("("); + if (obj.parameters.properties.length) { + this.indent(); + this.tokenizeModelExpression(obj.parameters, {isOperationSignature: true}); + this.deindent(); + } + this.punctuation("):", {HasSuffixSpace: true}); + this.tokenizeReturnType(obj, {isExpanded: true}); + break; + case SyntaxKind.OperationSignatureReference: + obj = node as OperationSignatureReferenceNode; + this.keyword("is", {HasPrefixSpace: true, HasSuffixSpace: true}); + this.tokenize(obj.baseOperation); + break; + case SyntaxKind.Return: + throw new Error(`Case "Return" not implemented`); + case SyntaxKind.StringLiteral: + obj = node as StringLiteralNode; + this.stringLiteral(obj.value); + break; + case SyntaxKind.ScalarStatement: + this.tokenizeScalarStatement(node as ScalarStatementNode); + break; + case SyntaxKind.TemplateParameterDeclaration: + obj = node as TemplateParameterDeclarationNode; + this.tokenize(obj.id); + if (obj.constraint) { + this.keyword("extends", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.tokenize(obj.constraint); + } + if (obj.default) { + this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.tokenize(obj.default); + } + break; + case SyntaxKind.TupleExpression: + obj = node as TupleExpressionNode; + this.punctuation("[", {HasSuffixSpace: true}); + for (let x = 0; x < obj.values.length; x++) { + const val = obj.values[x]; + this.tokenize(val); + if (x !== obj.values.length - 1) { + this.punctuation(",", {HasSuffixSpace: true}); + } + } + this.punctuation("]"); + break; + case SyntaxKind.TypeReference: + obj = node as TypeReferenceNode; + this.tokenizeIdentifier(obj.target, "reference"); + this.tokenizeTemplateInstantiation(obj); + break; + case SyntaxKind.UnionExpression: + obj = node as UnionExpressionNode; + for (let x = 0; x < obj.options.length; x++) { + const opt = obj.options[x]; + this.tokenize(opt); + if (x !== obj.options.length - 1) { + this.punctuation("|", {HasPrefixSpace: true, HasSuffixSpace: true}); + } + } + break; + case SyntaxKind.UnionStatement: + this.tokenizeUnionStatement(node as UnionStatementNode); + break; + case SyntaxKind.UnionVariant: + this.tokenizeUnionVariant(node as UnionVariantNode); + break; + case SyntaxKind.UnknownKeyword: + this.keyword("unknown"); + break; + case SyntaxKind.UsingStatement: + throw new Error(`Case "UsingStatement" not implemented`); + case SyntaxKind.ValueOfExpression: + this.keyword("valueof", {HasSuffixSpace: true}); + this.tokenize((node as ValueOfExpressionNode).target); + break; + case SyntaxKind.VoidKeyword: + this.keyword("void"); + break; + case SyntaxKind.TemplateArgument: + + case SyntaxKind.StringTemplateExpression: + obj = node as StringTemplateExpressionNode; + const stringValue = this.buildTemplateString(obj); + const multiLine = stringValue.includes("\n"); + // single line case + if (!multiLine) { + this.stringLiteral(stringValue); + break; + } + // otherwise multiline case + const lines = stringValue.split("\n"); + this.punctuation(`"""`); + this.indent(); + for (const line of lines) { + this.literal(line); + this.newline(); + } + this.deindent(); + this.punctuation(`"""`); + break; + case SyntaxKind.StringTemplateSpan: + obj = node as StringTemplateSpanNode; + this.punctuation("${"); + this.tokenize(obj.expression); + this.punctuation("}"); + this.tokenize(obj.literal); + break; + case SyntaxKind.StringTemplateHead: + case SyntaxKind.StringTemplateMiddle: + case SyntaxKind.StringTemplateTail: + obj = node as StringTemplateHeadNode; + this.literal(obj.value); + break; + case SyntaxKind.CallExpression: + obj = node as CallExpressionNode; + this.tokenize(obj.target); + this.punctuation("(", {HasSuffixSpace: false}); + for (let x = 0; x < obj.arguments.length; x++) { + const arg = obj.arguments[x]; + this.tokenize(arg); + if (x !== obj.arguments.length - 1) { + this.punctuation(",", {HasSuffixSpace: true, snapTo: "}"}); + } + } + this.punctuation(")"); + break; + default: + // All Projection* cases should fail here... + throw new Error(`Case "${SyntaxKind[node.kind].toString()}" not implemented`); + } + } + + private tokenizeTemplateInstantiation(obj: TypeReferenceNode) { + if (!obj.arguments.length) { + return; + } + + // if any argument is a ModelExpression, then we need to expand the template to multiple lines + const isExpanded = obj.arguments.some(arg => arg.argument.kind === SyntaxKind.ModelExpression); + + this.punctuation("<"); + if (isExpanded) { + this.indent(); + } + for (let x = 0; x < obj.arguments.length; x++) { + const arg = obj.arguments[x]; + this.tokenizeTemplateArgument(arg); + if (x !== obj.arguments.length - 1) { + this.punctuation(",", {HasSuffixSpace: true, snapTo: "}"}); + if (isExpanded && arg.argument.kind) { + this.blankLines(0); + } + } + } + if (isExpanded) { + this.newline(); + this.deindent(); + } + this.punctuation(">"); + } + + private buildExpressionString(node: Expression) { + switch (node.kind) { + case SyntaxKind.StringLiteral: + return `"${(node as StringLiteralNode).value}"`; + case SyntaxKind.NumericLiteral: + return (node as NumericLiteralNode).value.toString(); + case SyntaxKind.BooleanLiteral: + return (node as BooleanLiteralNode).value.toString(); + case SyntaxKind.StringTemplateExpression: + return this.buildTemplateString(node as StringTemplateExpressionNode); + case SyntaxKind.VoidKeyword: + return "void"; + case SyntaxKind.NeverKeyword: + return "never"; + case SyntaxKind.TypeReference: + const obj = node as TypeReferenceNode; + switch (obj.target.kind) { + case SyntaxKind.Identifier: + return (obj.target as IdentifierNode).sv; + case SyntaxKind.MemberExpression: + return this.getFullyQualifiedIdentifier(obj.target as MemberExpressionNode); + } + default: + throw new Error(`Unsupported expression kind: ${SyntaxKind[node.kind]}`); + //unsupported ArrayExpressionNode | MemberExpressionNode | ModelExpressionNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode | AnyKeywordNode; + } + } + + /** Constructs a single string with template markers. */ + private buildTemplateString(node: StringTemplateExpressionNode): string { + let result = node.head.value; + for (const span of node.spans) { + result += "${" + this.buildExpressionString(span.expression) + "}"; + result += span.literal.value; + } + return result; + } + + private tokenizeModelStatement(node: ModelStatementNode) { + this.namespaceStack.push(node.id.sv); + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); + this.keyword("model", {HasSuffixSpace: true}); + this.tokenizeIdentifier(node.id, "declaration"); + if (node.extends) { + this.keyword("extends", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.tokenize(node.extends); + } + if (node.is) { + this.keyword("is", {HasPrefixSpace: true, HasSuffixSpace: true}); + this.tokenize(node.is); + } + this.tokenizeTemplateParameters(node.templateParameters); + if (node.properties.length) { + this.punctuation("{", {HasPrefixSpace: true}); + this.indent(); + for (const prop of node.properties) { + const propName = this.getNameForNode(prop); + this.namespaceStack.push(propName); + this.tokenize(prop); + this.punctuation(";"); + this.namespaceStack.pop(); + this.newline() + } + this.deindent(); + this.punctuation("}", {isContextEndLine: true}); + } else { + this.punctuation("{}", {HasPrefixSpace: true}); + } + this.namespaceStack.pop(); + } + + private tokenizeScalarStatement(node: ScalarStatementNode) { + this.namespaceStack.push(node.id.sv); + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); + this.keyword("scalar", {HasSuffixSpace: true}); + this.tokenizeIdentifier(node.id, "declaration"); + if (node.extends) { + this.keyword("extends", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.tokenize(node.extends); + } + this.tokenizeTemplateParameters(node.templateParameters); + this.newline() + this.namespaceStack.pop(); + } + + private tokenizeInterfaceStatement(node: InterfaceStatementNode) { + this.namespaceStack.push(node.id.sv); + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); + this.keyword("interface", {HasSuffixSpace: true}); + this.tokenizeIdentifier(node.id, "declaration"); + this.tokenizeTemplateParameters(node.templateParameters); + this.punctuation("{", {HasPrefixSpace: true}); + this.indent(); + for (let x = 0; x < node.operations.length; x++) { + const op = node.operations[x]; + this.tokenizeOperationStatement(op, true); + this.blankLines(1) + } + this.deindent(); + this.punctuation("}", {isContextEndLine: true}); + this.namespaceStack.pop(); + } + + private tokenizeEnumStatement(node: EnumStatementNode) { + this.namespaceStack.push(node.id.sv); + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); + this.keyword("enum", {HasSuffixSpace: true}); + this.tokenizeIdentifier(node.id, "declaration"); + this.punctuation("{", {HasPrefixSpace: true}); + this.indent(); + for (const member of node.members) { + const memberName = this.getNameForNode(member); + this.namespaceStack.push(memberName); + this.tokenize(member); + this.punctuation(","); + this.namespaceStack.pop(); + this.newline() + } + this.deindent(); + this.punctuation("}", {isContextEndLine: true}); + this.namespaceStack.pop(); + } + + private tokenizeUnionStatement(node: UnionStatementNode) { + this.namespaceStack.push(node.id.sv); + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); + this.keyword("union", {HasSuffixSpace: true}); + this.tokenizeIdentifier(node.id, "declaration"); + this.punctuation("{", {HasPrefixSpace: true}); + this.indent(); + for (let x = 0; x < node.options.length; x++) { + const variant = node.options[x]; + const variantName = this.getNameForNode(variant); + this.namespaceStack.push(variantName); + this.tokenize(variant); + this.namespaceStack.pop(); + if (x !== node.options.length - 1) { + this.punctuation(","); + } + this.newline() + } + this.deindent(); + this.punctuation("}", {isContextEndLine: true}); + this.namespaceStack.pop(); + } + + private tokenizeUnionVariant(node: UnionVariantNode) { + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); + if (node.id !== undefined) { + this.tokenizeIdentifier(node.id, "member"); + this.punctuation(":", {HasSuffixSpace: true}); + } + this.lineMarker({addCrossLanguageId: true}); + this.tokenize(node.value); + } + + private tokenizeModelProperty(node: ModelPropertyNode, inline: boolean) { + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, inline); + this.tokenizeIdentifier(node.id, "member"); + this.lineMarker(); + this.punctuation(node.optional ? "?:" : ":", {HasSuffixSpace: true}); + this.tokenize(node.value); + if (node.default) { + this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.tokenize(node.default); + } + } + + /** Expands and tokenizes a model expression (anonymous model) */ + private tokenizeModelExpression( + node: ModelExpressionNode, + options: {isOperationSignature: boolean}) { + const isOperationSignature = options.isOperationSignature; + + // display {} for empty model or nothing for empty operation signature + if (!node.properties.length) { + if (!isOperationSignature) { + this.punctuation("{}", {HasPrefixSpace: true}); + } + return; + } + + if (!isOperationSignature) { + this.punctuation("{"); + this.indent(); + } + this.namespaceStack.push("anonymous"); + for (let x = 0; x < node.properties.length; x++) { + const prop = node.properties[x]; + const propName = this.getNameForNode(prop); + this.namespaceStack.push(propName); + switch (prop.kind) { + case SyntaxKind.ModelProperty: + this.tokenizeModelProperty(prop, false); + break; + case SyntaxKind.ModelSpreadProperty: + this.tokenize(prop); + } + this.namespaceStack.pop(); + if (isOperationSignature) { + if (x !== node.properties.length - 1) { + this.punctuation(",", {HasSuffixSpace: true, snapTo: "}"}); + } + } else { + this.punctuation(";", {HasSuffixSpace: true, snapTo: "}"}); + } + this.blankLines(0); + } + if (!isOperationSignature) { + this.deindent(); + this.punctuation("}"); + this.newline(); + } + this.namespaceStack.pop(); + } + + private tokenizeOperationStatement(node: OperationStatementNode, suppressOpKeyword: boolean = false) { + this.namespaceStack.push(node.id.sv); + this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); + if (!suppressOpKeyword) { + this.keyword("op", {HasSuffixSpace: true}); + } + this.tokenizeIdentifier(node.id, "declaration"); + this.tokenizeTemplateParameters(node.templateParameters); + this.tokenize(node.signature); + this.punctuation(";", {isContextEndLine: true}); + this.namespaceStack.pop(); + } + + private tokenizeNamespaceModel(model: NamespaceModel) { + this.namespaceStack.push(model.name); + if (model.node.kind === SyntaxKind.NamespaceStatement) { + this.tokenizeDecoratorsAndDirectives(model.node.decorators, model.node.directives, false); + } + this.keyword("namespace", {HasSuffixSpace: true}); + this.typeDeclaration(model.name, this.namespaceStack.value(), true, {HasSuffixSpace: true}); + this.punctuation("{", {HasPrefixSpace: true}); + this.indent(); + for (const node of model.augmentDecorators) { + this.tokenize(node); + this.blankLines(1); + } + for (const node of model.operations.values()) { + this.tokenize(node); + this.blankLines(1); + } + for (const node of model.resources.values()) { + this.tokenize(node); + this.blankLines(1); + } + for (const node of model.models.values()) { + this.tokenize(node); + this.blankLines(1); + } + for (const node of model.aliases.values()) { + this.tokenize(node); + this.punctuation(";"); + this.blankLines(1); + } + for (const node of model.constants.values()) { + this.tokenize(node); + this.punctuation(";"); + this.blankLines(0); + } + this.deindent(); + this.punctuation("}", {isContextEndLine: true}); + this.blankLines(1); + this.namespaceStack.pop(); + } + + private tokenizeDecoratorsAndDirectives( + decorators: readonly DecoratorExpressionNode[] | undefined, + directives: readonly DirectiveExpressionNode[] | undefined, + inline: boolean, + ) { + const docDecorators = ["doc", "summary", "example"]; + if ((directives || []).length === 0 && (decorators || []).length === 0) { + return; + } + for (const directive of directives ?? []) { + this.tokenize(directive); + } + // render each decorator + for (const node of decorators || []) { + const isDoc = docDecorators.includes((node.target as IdentifierNode).sv); + this.tokenize(node); + if (isDoc) { + // if any token in a line is documentation, then the whole line is + for (const token of this.currentLine.Tokens) { + token.IsDocumentation = true; + } + } + if (!inline) { + this.newline() + } + } + } + + private getFullyQualifiedIdentifier(node: MemberExpressionNode, suffix?: string): string { + switch (node.base.kind) { + case SyntaxKind.Identifier: + return `${node.base.sv}.${suffix}`; + case SyntaxKind.MemberExpression: + return this.getFullyQualifiedIdentifier(node.base, `${node.base.id.sv}.${suffix}`); + } + } + + private tokenizeIdentifier( + node: IdentifierNode | MemberExpressionNode | StringLiteralNode, + style: "declaration" | "reference" | "member" | "keyword", + ) { + switch (node.kind) { + case SyntaxKind.MemberExpression: + const defId = this.getFullyQualifiedIdentifier(node, node.id.sv); + switch (style) { + case "reference": + this.typeReference(defId); + break; + case "member": + this.member(defId); + break; + case "keyword": + this.keyword(defId); + break; + case "declaration": + throw new Error(`MemberExpression cannot be a "declaration".`); + } + break; + case SyntaxKind.StringLiteral: + if (style !== "member") { + throw new Error(`StringLiteral type can only be a member name. Unexpectedly "${style}"`); + } + this.stringLiteral(node.value); + break; + case SyntaxKind.Identifier: + switch (style) { + case "declaration": + this.typeDeclaration(node.sv, this.namespaceStack.value(), true, {HasSuffixSpace: false}); + break; + case "reference": + const defId = this.definitionIdFor(node.sv, this.packageName); + this.typeReference(node.sv, {NavigateToId: defId}); + break; + case "member": + this.member(this.getRawText(node)); + break; + case "keyword": + this.keyword(node.sv); + break; + } + } + } + + private getRawText(node: IdentifierNode): string { + return getSourceLocation(node).file.text.slice(node.pos, node.end); + } + + private tokenizeTemplateParameters(nodes: readonly TemplateParameterDeclarationNode[]) { + if (nodes.length) { + this.punctuation("<"); + for (let x = 0; x < nodes.length; x++) { + const param = nodes[x]; + this.tokenize(param); + if (x !== nodes.length - 1) { + this.punctuation(",", {HasSuffixSpace: true}); + } + } + this.punctuation(">"); + } + } + + private tokenizeTemplateArgument(obj: TemplateArgumentNode) { + if (obj.name) { + this.text(obj.name.sv); + this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + } + if (obj.argument.kind === SyntaxKind.ModelExpression) { + this.tokenizeModelExpression(obj.argument, {isOperationSignature: false}); + } else { + this.tokenize(obj.argument); + } + } + + private tokenizeReturnType(node: OperationSignatureDeclarationNode, options: { isExpanded: boolean}) { + if (options.isExpanded && node.parameters.properties.length) { + const offset = this.currentLine.Tokens.length; + this.tokenize(node.returnType); + const returnTokens = this.currentLine.Tokens.slice(offset); + const returnTypeString = returnTokens + .filter((x) => x.Value) + .flatMap((x) => x.Value) + .join(""); + this.namespaceStack.push(returnTypeString); + this.lineMarker(); + this.namespaceStack.pop(); + } else { + this.tokenize(node.returnType); + } + } + + private buildNavigation(ns: NamespaceModel) { + this.namespaceStack.reset(); + this.navigationItems.push(new NavigationItem(ns, this.namespaceStack)); + } + + private getNameForNode(node: BaseNode | NamespaceModel): string { + const id = generateId(node); + if (id) { + return id.split(".").splice(-1)[0]; + } else { + throw new Error("Unable to get name for node."); + } + } + + private definitionIdFor(value: string, prefix: string): string | undefined { + if (value.includes(".")) { + const fullName = `${prefix}.${value}`; + return this.typeDeclarations.has(fullName) ? fullName : undefined; + } + for (const item of this.typeDeclarations) { + if (item.split(".").splice(-1)[0] === value) { + return item; + } + } + return undefined; + } +} diff --git a/packages/typespec-apiview/src/emitter.ts b/packages/typespec-apiview/src/emitter.ts new file mode 100644 index 0000000000..9b908dd46d --- /dev/null +++ b/packages/typespec-apiview/src/emitter.ts @@ -0,0 +1,123 @@ +import { + EmitContext, + emitFile, + getNamespaceFullName, + listServices, + Namespace, + NoTarget, + Program, + resolvePath, + Service, +} from "@typespec/compiler"; +import path from "path"; +import { ApiView } from "./apiview.js"; +import { ApiViewEmitterOptions, reportDiagnostic } from "./lib.js"; + +export interface ResolvedApiViewEmitterOptions { + emitterOutputDir: string; + outputFile?: string; + service?: string; + includeGlobalNamespace: boolean; +} + +export async function $onEmit(context: EmitContext) { + const options = resolveOptions(context); + const emitter = createApiViewEmitter(context.program, options); + await emitter.emitApiView(); +} + +export function resolveOptions(context: EmitContext): ResolvedApiViewEmitterOptions { + const resolvedOptions = { ...context.options }; + + return { + emitterOutputDir: context.emitterOutputDir, + outputFile: resolvedOptions["output-file"], + service: resolvedOptions["service"], + includeGlobalNamespace: resolvedOptions["include-global-namespace"] ?? false, + }; +} + +function resolveNamespaceString(namespace: Namespace): string | undefined { + // FIXME: Fix this wonky workaround when getNamespaceString is fixed. + const value = getNamespaceFullName(namespace); + return value === "" ? undefined : value; +} + +/** + * Ensures that single-value options are not used in multi-service specs unless the + * `--service` option is specified. Single-service specs need not pass this option. + */ +function validateMultiServiceOptions(program: Program, services: Service[], options: ResolvedApiViewEmitterOptions) { + for (const [name, val] of [["output-file", options.outputFile]]) { + if (val && !options.service && services.length > 1) { + reportDiagnostic(program, { + code: "invalid-option", + target: NoTarget, + format: { + name: name! + } + }) + } + } +} + +/** + * If the `--service` option is provided, ensures the service exists and returns the filtered list. + */ +function applyServiceFilter(program: Program, services: Service[], options: ResolvedApiViewEmitterOptions): Service[] { + if (!options.service) { + return services; + } + const filtered = services.filter( (x) => x.title === options.service); + if (!filtered.length) { + reportDiagnostic(program, { + code: "invalid-service", + target: NoTarget, + format: { + value: options.service + } + }); + } + return filtered; +} + +function createApiViewEmitter(program: Program, options: ResolvedApiViewEmitterOptions) { + return { emitApiView }; + + async function emitApiView() { + let services = listServices(program); + if (!services.length) { + reportDiagnostic(program, { + code: "no-services-found", + target: NoTarget + }) + return; + } + // applies the default "apiview.json" filename if not provided and there's only a single service + if (services.length === 1) { + options.outputFile = options.outputFile ?? "apiview.json" + } + validateMultiServiceOptions(program, services, options); + services = applyServiceFilter(program, services, options); + + for (const service of services) { + const namespaceString = resolveNamespaceString(service.type) ?? "Unknown" + const serviceTitle = service.title ? service.title : namespaceString; + + const apiview = new ApiView(serviceTitle, namespaceString, options.includeGlobalNamespace); + apiview.compile(program); + apiview.resolveMissingTypeReferences(); + + if (!program.compilerOptions.noEmit && !program.hasError()) { + const outputFolder = path.dirname(options.emitterOutputDir); + await program.host.mkdirp(outputFolder); + const outputFile = options.outputFile ?? `${namespaceString}-apiview.json`; + const outputPath = resolvePath(outputFolder, outputFile); + await emitFile(program, { + path: outputPath, + content: JSON.stringify(apiview.asCodeFile()) + "\n" + }); + } + } + } +} diff --git a/packages/typespec-apiview/src/index.ts b/packages/typespec-apiview/src/index.ts new file mode 100644 index 0000000000..bc7cadcc0b --- /dev/null +++ b/packages/typespec-apiview/src/index.ts @@ -0,0 +1,4 @@ +export const namespace = "ApiView"; + +export { $lib } from "./lib.js"; +export * from "./emitter.js"; diff --git a/packages/typespec-apiview/src/lib.ts b/packages/typespec-apiview/src/lib.ts new file mode 100644 index 0000000000..1f454841dc --- /dev/null +++ b/packages/typespec-apiview/src/lib.ts @@ -0,0 +1,55 @@ +import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/compiler"; + +export interface ApiViewEmitterOptions { + "output-file"?: string; + "service"?: string; + "include-global-namespace"?: boolean, + "mapping-path"?: string; +} + +const ApiViewEmitterOptionsSchema: JSONSchemaType = { + type: "object", + additionalProperties: false, + properties: { + "output-file": { type: "string", nullable: true }, + "service": { type: "string", nullable: true }, + "include-global-namespace": {type: "boolean", nullable: true}, + "mapping-path": { type: "string", nullable: true }, + }, + required: [], +}; + + +export const $lib = createTypeSpecLibrary({ + name: "@azure-tools/typespec-apiview", + diagnostics: { + "no-services-found": { + severity: "error", + messages: { + default: "No services found. Ensure there is a namespace in the spec annotated with the `@service` decorator." + } + }, + "invalid-service": { + severity: "error", + messages: { + default: paramMessage`Service "${"value"}" was not found. Please check for typos.`, + } + }, + "invalid-option": { + severity: "error", + messages: { + default: paramMessage`Option "--${"name"}" cannot be used with multi-service specs unless "--service" is also supplied.`, + } + }, + "version-not-found": { + severity: "error", + messages: { + default: paramMessage`Version "${"version"}" not found for service "${"serviceName"}". Allowed values: ${"allowed"}.`, + } + }, + }, + emitter: { + options: ApiViewEmitterOptionsSchema, + }, +}); +export const { reportDiagnostic } = $lib; diff --git a/packages/typespec-apiview/src/namespace-model.ts b/packages/typespec-apiview/src/namespace-model.ts new file mode 100644 index 0000000000..31521d0187 --- /dev/null +++ b/packages/typespec-apiview/src/namespace-model.ts @@ -0,0 +1,311 @@ +import { + Namespace, + Program, +} from "@typespec/compiler"; +import { + Node, + AliasStatementNode, + ModelStatementNode, + OperationStatementNode, + InterfaceStatementNode, + EnumStatementNode, + NamespaceStatementNode, + ModelExpressionNode, + IntersectionExpressionNode, + SyntaxKind, + BaseNode, + IdentifierNode, + ModelPropertyNode, + EnumMemberNode, + ModelSpreadPropertyNode, + EnumSpreadMemberNode, + DecoratorExpressionNode, + MemberExpressionNode, + UnionStatementNode, + UnionExpressionNode, + UnionVariantNode, + AugmentDecoratorStatementNode, + ScalarStatementNode, + TypeReferenceNode, + JsNamespaceDeclarationNode, + DirectiveExpressionNode, + StringLiteralNode, + ObjectLiteralNode, + ConstStatementNode, + visitChildren +} from "@typespec/compiler/ast"; +export class NamespaceModel { + kind = SyntaxKind.NamespaceStatement; + name: string; + node: NamespaceStatementNode | JsNamespaceDeclarationNode; + operations = new Map(); + resources = new Map< + string, + | AliasStatementNode + | ModelStatementNode + | ModelExpressionNode + | IntersectionExpressionNode + | EnumStatementNode + | ScalarStatementNode + | UnionStatementNode + | UnionExpressionNode + | ObjectLiteralNode + >(); + models = new Map< + string, + | ModelStatementNode + | ModelExpressionNode + | IntersectionExpressionNode + | EnumStatementNode + | ScalarStatementNode + | UnionStatementNode + | UnionExpressionNode + | ObjectLiteralNode + >(); + aliases = new Map(); + augmentDecorators = new Array(); + constants = new Array(); + + constructor(name: string, ns: Namespace, program: Program) { + this.name = name; + this.node = ns.node!; // Assuming ns.node is never undefined + + // Gather operations + for (const [opName, op] of ns.operations) { + if (op.node) { + this.operations.set(opName, op.node); + } else { + throw new Error(`Operation node for ${opName} is undefined.`); + } + } + for (const [intName, int] of ns.interfaces) { + if (int.node) { + this.operations.set(intName, int.node); + } else { + throw new Error(`Interface node for ${intName} is undefined.`); + } + } + + // Gather models and resources + for (const [modelName, model] of ns.models) { + if (model.node !== undefined) { + let isResource = false; + for (const dec of model.decorators) { + if (dec.decorator.name === "$resource") { + isResource = true; + break; + } + } + if (isResource) { + this.resources.set(modelName, model.node); + } else { + this.models.set(modelName, model.node); + } + } else { + throw new Error("Unexpectedly found undefined model node."); + } + } + for (const [enumName, en] of ns.enums) { + if (en.node) { + this.models.set(enumName, en.node); + } else { + throw new Error(`Enum node for ${enumName} is undefined.`); + } + } + for (const [unionName, un] of ns.unions) { + if (un.node) { + this.models.set(unionName, un.node); + } else { + throw new Error(`Union node for ${unionName} is undefined.`); + } + } + for (const [scalarName, sc] of ns.scalars) { + if (sc.node) { + this.models.set(scalarName, sc.node); + } else { + throw new Error(`Scalar node for ${scalarName} is undefined.`); + } + } + + // Gather aliases + for (const alias of findNodes(SyntaxKind.AliasStatement, program, ns)) { + this.aliases.set(alias.id.sv, alias); + } + + // collect augment decorators + for (const augment of findNodes(SyntaxKind.AugmentDecoratorStatement, program, ns)) { + this.augmentDecorators.push(augment); + } + + // collect constants + for (const constant of findNodes(SyntaxKind.ConstStatement, program, ns)) { + this.constants.push(constant); + } + + // sort operations and models + this.operations = new Map([...this.operations].sort(caseInsensitiveSort)); + this.resources = new Map([...this.resources].sort(caseInsensitiveSort)); + this.models = new Map([...this.models].sort(caseInsensitiveSort)); + this.aliases = new Map([...this.aliases].sort(caseInsensitiveSort)); + } + + /** + * Don't emit an empty namespace + * @returns true if there are models, resources or operations + */ + shouldEmit(): boolean { + return ( + (this.node as NamespaceStatementNode).decorators !== undefined || + this.models.size > 0 || + this.operations.size > 0 || + this.resources.size > 0 + ); + } +} + +function findNodes(kind: T, program: Program, namespace: Namespace): (Node & { kind: T })[] { + const nodes: Node[] = []; + for (const file of program.sourceFiles.values()) { + visitChildren(file, function visit(node) { + if (node.kind === kind && inNamespace(node, program, namespace)) { + nodes.push(node); + } + visitChildren(node, visit); + }); + } + return nodes as any; +} + +function inNamespace(node: Node, program: Program, namespace: Namespace): boolean { + for (let n: Node | undefined = node; n; n = n.parent) { + switch (n.kind) { + case SyntaxKind.NamespaceStatement: + return program.checker.getTypeForNode(n) === namespace; + case SyntaxKind.TypeSpecScript: + if (n.inScopeNamespaces.length > 0 && inNamespace(n.inScopeNamespaces[0], program, namespace)) { + return true; + } + return false; + } + } + return false; +} + +export function generateId(obj: BaseNode | NamespaceModel | undefined): string | undefined { + let node; + if (obj === undefined) { + return undefined; + } + if (obj instanceof NamespaceModel) { + return obj.name; + } + let name: string; + let parentId: string | undefined; + switch (obj.kind) { + case SyntaxKind.NamespaceStatement: + node = obj as NamespaceStatementNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.DecoratorExpression: + node = obj as DecoratorExpressionNode; + switch (node.target.kind) { + case SyntaxKind.Identifier: + return `@${node.target.sv}`; + case SyntaxKind.MemberExpression: + return generateId(node.target); + } + case SyntaxKind.EnumMember: + node = obj as EnumMemberNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.EnumSpreadMember: + node = obj as EnumSpreadMemberNode; + return generateId(node.target); + case SyntaxKind.EnumStatement: + node = obj as EnumStatementNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.Identifier: + node = obj as IdentifierNode; + return node.sv; + case SyntaxKind.InterfaceStatement: + node = obj as InterfaceStatementNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.MemberExpression: + node = obj as MemberExpressionNode; + name = node.id.sv; + parentId = generateId(node.base); + break; + case SyntaxKind.ModelProperty: + node = obj as ModelPropertyNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.ModelSpreadProperty: + node = obj as ModelSpreadPropertyNode; + name = generateId(node.target)!; + parentId = generateId(node.parent); + break; + case SyntaxKind.ModelStatement: + node = obj as ModelStatementNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.OperationStatement: + node = obj as OperationStatementNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.StringLiteral: + node = obj as StringLiteralNode; + name = node.value; + parentId = undefined; + break; + case SyntaxKind.UnionStatement: + node = obj as UnionStatementNode; + name = node.id.sv; + parentId = generateId(node.parent); + break; + case SyntaxKind.UnionVariant: + node = obj as UnionVariantNode; + if (node.id?.sv !== undefined) { + name = node.id.sv; + } else { + // TODO: Should never have default value of _ + name = generateId(node.value) ?? "_"; + } + parentId = generateId(node.parent); + break; + case SyntaxKind.TypeReference: + node = obj as TypeReferenceNode; + name = generateId(node.target)!; + parentId = undefined; + break; + case SyntaxKind.DirectiveExpression: + node = obj as DirectiveExpressionNode; + name = `#${generateId(node.target)!}`; + for (const arg of node.arguments) { + name += `_${generateId(arg)}`; + } + parentId = generateId(node.parent); + break; + default: + return undefined; + } + if (parentId !== undefined) { + return `${parentId}.${name}`; + } else { + return name; + } +} + +function caseInsensitiveSort(a: [string, any], b: [string, any]): number { + const aLower = a[0].toLowerCase(); + const bLower = b[0].toLowerCase(); + return aLower > bLower ? 1 : aLower < bLower ? -1 : 0; +} diff --git a/packages/typespec-apiview/src/schemas.ts b/packages/typespec-apiview/src/schemas.ts new file mode 100644 index 0000000000..8031aecab9 --- /dev/null +++ b/packages/typespec-apiview/src/schemas.ts @@ -0,0 +1,253 @@ +// These schemas are all adapted from the TypeSpec definition here: +// https://github.com/Azure/azure-sdk-tools/blob/main/tools/apiview/parsers/apiview-treestyle-parser-schema/main.tsp + +import { AliasStatementNode, EnumStatementNode, InterfaceStatementNode, IntersectionExpressionNode, ModelExpressionNode, ModelStatementNode, ObjectLiteralNode, OperationStatementNode, ScalarStatementNode, SyntaxKind, UnionExpressionNode, UnionStatementNode } from "@typespec/compiler/ast"; +import { NamespaceModel } from "./namespace-model.js"; +import { NamespaceStack } from "./util.js"; + +// CORE API VIEW SCHEMAS + +export enum TokenKind { + Text = 0, + Punctuation = 1, + Keyword = 2, + TypeName = 3, + MemberName = 4, + StringLiteral = 5, + Literal = 6, + Comment = 7 +} + +/** ReviewFile represents entire API review object. This will be processed to render review lines. */ +export interface CodeFile { + Name: string; + PackageName: string; + PackageVersion: string; + /** version of the APIview language parser used to create token file*/ + ParserVersion: string; + Language: string; + /** Language variant is applicable only for java variants*/ + LanguageVariant: string | undefined; + CrossLanguagePackageId: string | undefined; + ReviewLines: ReviewLine[]; + /** Add any system generated comments. Each comment is linked to review line ID */ + Diagnostics: CodeDiagnostic[] | undefined; + /** Navigation items are used to create a tree view in the navigation panel. Each navigation item is linked to a review line ID. This is optional. + * If navigation items are not provided then navigation panel will be automatically generated using the review lines. Navigation items should be provided only if you want to customize the navigation panel. + */ + Navigation: NavigationItem[] | undefined; +} + +export interface ReviewLineOptions { + /** Set current line as hidden code line by default. .NET has hidden APIs and architects don't want to see them by default. */ + IsHidden?: boolean; + /** Set current line as context end line. For e.g. line with token } or empty line after the class to mark end of context. */ + IsContextEndLine?: boolean; + /** Set ID of related line to ensure current line is not visible when a related line is hidden. + * One e.g. is a code line for class attribute should set class line's Line ID as related line ID. + */ + RelatedToLine?: string; +} + +/** ReviewLine object corresponds to each line displayed on API review. If an empty line is required then add a code line object without any token. */ +export interface ReviewLine extends ReviewLineOptions { + /** lineId is only required if we need to support commenting on a line that contains this token. + * Usually code line for documentation or just punctuation is not required to have lineId. lineId should be a unique value within + * the review token file to use it assign to review comments as well as navigation Id within the review page. + * for e.g Azure.Core.HttpHeader.Common, azure.template.template_main + */ + LineId: string | undefined; + CrossLanguageId: string | undefined; + /** list of tokens that constructs a line in API review */ + Tokens: ReviewToken[]; + /** Add any child lines as children. For e.g. all classes and namespace level methods are added as a children of namespace(module) level code line. + * Similarly all method level code lines are added as children of it's class code line.*/ + Children: ReviewLine[]; +} + +export interface ReviewTokenOptions { + /** NavigationDisplayName is used to create a tree node in the navigation panel. Navigation nodes will be created only if token contains navigation display name.*/ + NavigationDisplayName?: string; + /** navigateToId should be set if the underlying token is required to be displayed as HREF to another type within the review. + * For e.g. a param type which is class name in the same package + */ + NavigateToId?: string; + /** set skipDiff to true if underlying token needs to be ignored from diff calculation. For e.g. package metadata or dependency versions + * are usually excluded when comparing two revisions to avoid reporting them as API changes*/ + SkipDiff?: boolean; + /** This is set if API is marked as deprecated */ + IsDeprecated?: boolean; + /** Set this to true if a prefix space is required before the next value. */ + HasPrefixSpace?: boolean; + /** Set this to true if a suffix space required before next token. For e.g, punctuation right after method name */ + HasSuffixSpace?: boolean; + /** Set isDocumentation to true if current token is part of documentation */ + IsDocumentation?: boolean; + /** Language specific style css class names */ + RenderClasses?: Array; +} + +/** Token corresponds to each component within a code line. A separate token is required for keyword, punctuation, type name, text etc. */ +export interface ReviewToken extends ReviewTokenOptions { + Kind: TokenKind; + Value: string; +} + +// CODE DIAGNOSTIC SCHEMAS + +export enum CodeDiagnosticLevel { + Info = 1, + Warning = 2, + Error = 3, + /** Fatal level diagnostic will block API review approval and it will show an error message to the user. Approver will have to + * override fatal level system comments before approving a review.*/ + Fatal = 4 +} + +/** System comment object is to add system generated comment. It can be one of the 4 different types of system comments. */ +export interface CodeDiagnostic { + /** Auto generated system comment to be displayed under targeted line. */ + Text: string; + /** Diagnostic ID is auto generated ID by CSharp analyzer. */ + DiagnosticId?: string; + /** Id of ReviewLine object where this diagnostic needs to be displayed */ + TargetId: string; + Level: CodeDiagnosticLevel; + HelpLinkUri?: string; +} + +// NAVIGATION SCHEMAS + +export class NavigationItem { + Text: string; + NavigationId: string | undefined; + ChildItems: NavigationItem[]; + Tags: ApiViewNavigationTag; + + constructor( + objNode: + | AliasStatementNode + | NamespaceModel + | ModelStatementNode + | OperationStatementNode + | InterfaceStatementNode + | EnumStatementNode + | ModelExpressionNode + | IntersectionExpressionNode + | ScalarStatementNode + | UnionStatementNode + | UnionExpressionNode + | ObjectLiteralNode, + stack: NamespaceStack + ) { + let obj; + switch (objNode.kind) { + case SyntaxKind.NamespaceStatement: + stack.push(objNode.name); + this.Text = objNode.name; + this.Tags = { TypeKind: ApiViewNavigationKind.Module }; + const operationItems = new Array(); + for (const node of objNode.operations.values()) { + operationItems.push(new NavigationItem(node, stack)); + } + const resourceItems = new Array(); + for (const node of objNode.resources.values()) { + resourceItems.push(new NavigationItem(node, stack)); + } + const modelItems = new Array(); + for (const node of objNode.models.values()) { + modelItems.push(new NavigationItem(node, stack)); + } + const aliasItems = new Array(); + for (const node of objNode.aliases.values()) { + aliasItems.push(new NavigationItem(node, stack)); + } + this.ChildItems = []; + if (operationItems.length) { + this.ChildItems.push({ Text: "Operations", ChildItems: operationItems, Tags: { TypeKind: ApiViewNavigationKind.Method }, NavigationId: "" }); + } + if (resourceItems.length) { + this.ChildItems.push({ Text: "Resources", ChildItems: resourceItems, Tags: { TypeKind: ApiViewNavigationKind.Class }, NavigationId: "" }); + } + if (modelItems.length) { + this.ChildItems.push({ Text: "Models", ChildItems: modelItems, Tags: { TypeKind: ApiViewNavigationKind.Class }, NavigationId: "" }); + } + if (aliasItems.length) { + this.ChildItems.push({ Text: "Aliases", ChildItems: aliasItems, Tags: { TypeKind: ApiViewNavigationKind.Class }, NavigationId: "" }); + } + break; + case SyntaxKind.ModelStatement: + obj = objNode as ModelStatementNode; + stack.push(obj.id.sv); + this.Text = obj.id.sv; + this.Tags = { TypeKind: ApiViewNavigationKind.Class }; + this.ChildItems = []; + break; + case SyntaxKind.EnumStatement: + obj = objNode as EnumStatementNode; + stack.push(obj.id.sv); + this.Text = obj.id.sv; + this.Tags = { TypeKind: ApiViewNavigationKind.Enum }; + this.ChildItems = []; + break; + case SyntaxKind.OperationStatement: + obj = objNode as OperationStatementNode; + stack.push(obj.id.sv); + this.Text = obj.id.sv; + this.Tags = { TypeKind: ApiViewNavigationKind.Method }; + this.ChildItems = []; + break; + case SyntaxKind.InterfaceStatement: + obj = objNode as InterfaceStatementNode; + stack.push(obj.id.sv); + this.Text = obj.id.sv; + this.Tags = { TypeKind: ApiViewNavigationKind.Method }; + this.ChildItems = []; + for (const child of obj.operations) { + this.ChildItems.push(new NavigationItem(child, stack)); + } + break; + case SyntaxKind.UnionStatement: + obj = objNode as UnionStatementNode; + stack.push(obj.id.sv); + this.Text = obj.id.sv; + this.Tags = { TypeKind: ApiViewNavigationKind.Enum }; + this.ChildItems = []; + break; + case SyntaxKind.AliasStatement: + obj = objNode as AliasStatementNode; + stack.push(obj.id.sv); + this.Text = obj.id.sv; + this.Tags = { TypeKind: ApiViewNavigationKind.Class }; + this.ChildItems = []; + break; + case SyntaxKind.ModelExpression: + throw new Error(`Navigation unsupported for "ModelExpression".`); + case SyntaxKind.IntersectionExpression: + throw new Error(`Navigation unsupported for "IntersectionExpression".`); + case SyntaxKind.ScalarStatement: + obj = objNode as ScalarStatementNode; + stack.push(obj.id.sv); + this.Text = obj.id.sv; + this.Tags = { TypeKind: ApiViewNavigationKind.Class }; + this.ChildItems = []; + break; + default: + throw new Error(`Navigation unsupported for "${objNode.kind.toString()}".`); + } + this.NavigationId = stack.value(); + stack.pop(); + } +} + +export interface ApiViewNavigationTag { + TypeKind: ApiViewNavigationKind; +} + +export const enum ApiViewNavigationKind { + Class = "class", + Enum = "enum", + Method = "method", + Module = "namespace", + Package = "assembly", +} diff --git a/packages/typespec-apiview/src/testing/index.ts b/packages/typespec-apiview/src/testing/index.ts new file mode 100644 index 0000000000..28750309ad --- /dev/null +++ b/packages/typespec-apiview/src/testing/index.ts @@ -0,0 +1,6 @@ +import { createTestLibrary, findTestPackageRoot, TypeSpecTestLibrary } from "@typespec/compiler/testing"; + +export const ApiViewTestLibrary: TypeSpecTestLibrary = createTestLibrary({ + name: "@azure-tools/typespec-apiview", + packageRoot: await findTestPackageRoot(import.meta.url), +}); diff --git a/packages/typespec-apiview/src/util.ts b/packages/typespec-apiview/src/util.ts new file mode 100644 index 0000000000..cc272e8460 --- /dev/null +++ b/packages/typespec-apiview/src/util.ts @@ -0,0 +1,44 @@ +import { ReviewLine, ReviewToken } from "./schemas.js"; + +export function reviewLineText(line: ReviewLine, indent: number): string { + const indentString = " ".repeat(indent); + let tokenText = ""; + for (const token of line.Tokens) { + tokenText += reviewTokenText(token, tokenText); + } + const childrenText = line.Children.map(c => reviewLineText(c, indent + 2)).join("\n"); + if (childrenText !== "") { + return `${indentString}${tokenText}\n${childrenText}`; + } else { + return `${indentString}${tokenText}`; + } +} + +function reviewTokenText(token: ReviewToken, preview: string): string { + const previewEndsInSpace = preview.endsWith(" "); + const hasSuffixSpace = token.HasSuffixSpace !== undefined ? token.HasSuffixSpace : true; + const suffixSpace = hasSuffixSpace ? " " : ""; + const prefixSpace = (token.HasPrefixSpace && !previewEndsInSpace) ? " " : ""; + const value = token.Value; + return `${prefixSpace}${value}${suffixSpace}`; +} + +export class NamespaceStack { + stack = new Array(); + + push(val: string) { + this.stack.push(val); + } + + pop(): string | undefined { + return this.stack.pop(); + } + + value(): string { + return this.stack.join("."); + } + + reset() { + this.stack = Array(); + } +} diff --git a/packages/typespec-apiview/src/version.ts b/packages/typespec-apiview/src/version.ts new file mode 100644 index 0000000000..3dabc791bd --- /dev/null +++ b/packages/typespec-apiview/src/version.ts @@ -0,0 +1 @@ +export const LIB_VERSION = "0.7.2"; diff --git a/packages/typespec-apiview/test/apiview-options.test.ts b/packages/typespec-apiview/test/apiview-options.test.ts new file mode 100644 index 0000000000..3cc89b314e --- /dev/null +++ b/packages/typespec-apiview/test/apiview-options.test.ts @@ -0,0 +1,64 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { apiViewFor, apiViewText, compare, diagnosticsFor } from "./test-host.js"; +import { describe, it } from "vitest"; + +describe("apiview-options: tests", () => { + + it("omits namespaces that aren't proper subnamespaces", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test"} ) + namespace Azure.Test { + model Foo {}; + } + + namespace Azure.Test.Sub { + model SubFoo {}; + }; + + namespace Azure.TestBad { + model BadFoo {}; + }; + `; + const expect = ` + namespace Azure.Test { + model Foo {} + } + + namespace Azure.Test.Sub { + model SubFoo {} + } + ` + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + }); + + it("outputs the global namespace when --include-global-namespace is set", async () => { + const input = ` + model SomeGlobal {}; + + @TypeSpec.service( #{ title: "Test"} ) + namespace Azure.Test { + model Foo {}; + } + `; + const expect = ` + namespace ::GLOBAL:: { + model SomeGlobal {} + } + + @TypeSpec.service(#{ + title: "Test" + }) + namespace Azure.Test { + model Foo {} + } + ` + const apiview = await apiViewFor(input, { + "include-global-namespace": true + }); + // TODO: Update once bug is fixed: https://github.com/microsoft/typespec/issues/3165 + const actual = apiViewText(apiview); + compare(expect, actual, 3); + }); +}); diff --git a/packages/typespec-apiview/test/apiview.test.ts b/packages/typespec-apiview/test/apiview.test.ts new file mode 100644 index 0000000000..bb6df8db5f --- /dev/null +++ b/packages/typespec-apiview/test/apiview.test.ts @@ -0,0 +1,1366 @@ +import { apiViewFor, apiViewText, compare } from "./test-host.js"; +import { CodeFile, ReviewLine } from "../src/schemas.js"; +import { describe, it } from "vitest"; +import { fail } from "assert"; +import { isDeepStrictEqual } from "util"; + +interface ReviewLineData { + relatedToCount: number; + isContextEndCount: number; +} + +describe("apiview: tests", () => { + + function validateReviewLineIds(definitionIds: Set, line: ReviewLine) { + // ensure that there are no repeated definition IDs. + if (line.LineId !== undefined) { + if (definitionIds.has(line.LineId)) { + fail(`Duplicate defintion ID ${line.LineId}.`); + } + if (line.LineId !== "") { + definitionIds.add(line.LineId); + } + for (const child of line.Children) { + validateReviewLineIds(definitionIds, child); + } + } + } + + /** Validates that there are no repeat defintion IDs. */ + function validateLineIds(apiview: CodeFile) { + const definitionIds = new Set(); + for (const line of apiview.ReviewLines) { + validateReviewLineIds(definitionIds, line); + } + } + + /** Validates that related lines point to a valid line. */ + function getRelatedLineMetadata(apiview: CodeFile): Map { + + function getReviewLinesMetadata(lines: ReviewLine[] | undefined): Map | undefined { + if (lines === undefined || lines.length === 0) return undefined; + const mainMap = new Map(); + let lastKey: string | undefined = undefined; + for (const line of lines) { + const related = line.RelatedToLine; + const lineId = line.LineId + const isEndContext = line.IsContextEndLine; + if (related) { + lastKey = related; + if (!mainMap.has(related)) { + mainMap.set(related, { relatedToCount: 0, isContextEndCount: 0 }); + } + mainMap.get(related)!.relatedToCount++; + } + if (isEndContext) { + if (lastKey === undefined) { + fail("isEndContext without a related line."); + } + if (!mainMap.has(lastKey)) { + mainMap.set(lastKey, { relatedToCount: 0, isContextEndCount: 0 }); + } + mainMap.get(lastKey)!.isContextEndCount++; + } + if (line.Children?.length > 0) { + if (lineId === undefined) { + fail("Children without a line ID."); + } + lastKey = lineId; + const childMap = getReviewLinesMetadata(line.Children); + if (childMap !== undefined && childMap.size > 0) { + for (const [key, value] of childMap) { + mainMap.set(key, value); + } + } + } + } + return mainMap; + } + const countMap = getReviewLinesMetadata(apiview.ReviewLines); + return countMap ?? new Map(); + } + + function compareCounts(lhs: Map, rhs: Map) { + // ensure the keys are the same + const lhsKeys = new Set([...lhs.keys()]); + const rhsKeys = new Set([...rhs.keys()]); + const combined = new Set([...lhsKeys, ...rhsKeys]); + if (combined.size != lhsKeys.size) { + fail(`Keys mismatch: ${JSON.stringify([...lhsKeys])} vs ${JSON.stringify([...rhsKeys])}`); + } + isDeepStrictEqual(lhs, rhs); + } + + + describe("models", () => { + it("composition", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model Animal { + species: string; + } + + model Pet { + name?: string; + } + + model Dog { + ...Animal; + ...Pet; + } + + model Cat { + species: string; + name?: string = "fluffy"; + } + + model Pig extends Animal {} + } + `; + const expect = ` + namespace Azure.Test { + model Animal { + species: string; + } + + model Cat { + species: string; + name?: string = "fluffy"; + } + + model Dog { + ...Animal; + ...Pet; + } + + model Pet { + name?: string; + } + + model Pig extends Animal {} + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Pet", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("templated", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model Thing { + property: T; + } + + model StringThing is Thing; + + model NamedStringThing is Thing; + + model Page { + size: int16; + item: T[]; + } + + model StringPage { + ...Page; + } + + model ConstrainedSimple { + prop: X; + } + + model ConstrainedComplex { + prop: X; + } + + model ConstrainedWithDefault { + prop: X; + } + } + `; + const expect = ` + namespace Azure.Test { + model ConstrainedComplex { + prop: X; + } + + model ConstrainedSimple { + prop: X; + } + + model ConstrainedWithDefault { + prop: X; + } + + model NamedStringThing is Thing {} + + model Page { + size: int16; + item: T[]; + } + + model StringPage { + ...Page; + } + + model StringThing is Thing {} + + model Thing { + property: T; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.ConstrainedComplex", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ConstrainedSimple", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.ConstrainedWithDefault", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Page", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.StringPage", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Thing", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("with default values", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model Foo { + name: string = "foo"; + array: string[] = #["a", "b"]; + obj: Record = #{val: 1, name: "foo"}; + } + } + `; + const expect = ` + namespace Azure.Test { + model Foo { + name: string = "foo"; + array: string[] = #["a", "b"]; + obj: Record = #{ + val: 1, + name: "foo" + }; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + + }); + }); + + describe("scalars", () => { + it("extends string", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + scalar Password extends string; + } + `; + const expect = ` + namespace Azure.Test { + scalar Password extends string + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ])); + }); + + it("new scalar type", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + scalar ternary; + } + `; + const expect = ` + namespace Azure.Test { + scalar ternary + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ])); + }); + + it("templated", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + @doc(T) + scalar Unreal; + } + `; + const expect = ` + namespace Azure.Test { + @doc(T) + scalar Unreal + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Unreal", { relatedToCount: 1, isContextEndCount: 0 }], + ])); + }); + }); + + describe("aliases", () => { + it("simple alias", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model Animal { + species: string; + } + + alias Creature = Animal; + } + `; + const expect = ` + namespace Azure.Test { + model Animal { + species: string; + } + + alias Creature = Animal; + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("templated alias", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model Animal { + species: string; + } + + alias Template = "Foo \${T} bar"; + } + `; + const expect = ` + namespace Azure.Test { + model Animal { + species: string; + } + + alias Template = "Foo \${T} bar"; + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + }); + + describe("augment decorators", () => { + it("simple augment", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model Animal { + species: string; + } + + @@doc(Animal, "My doc"); + } + `; + const expect = ` + namespace Azure.Test { + @@doc(Animal, "My doc") + + model Animal { + species: string; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + }); + + describe("enums", () => { + it("literal labels", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + enum SomeEnum { + Plain, + "Literal", + } + }`; + const expect = ` + namespace Azure.Test { + enum SomeEnum { + Plain, + "Literal", + } + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("string-backed values", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + enum SomeStringEnum { + A: "A", + B: "B", + } + }`; + const expect = ` + namespace Azure.Test { + enum SomeStringEnum { + A: "A", + B: "B", + } + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeStringEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("int-backed values", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + enum SomeIntEnum { + A: 1, + B: 2, + } + }`; + const expect = ` + namespace Azure.Test { + enum SomeIntEnum { + A: 1, + B: 2, + } + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeIntEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("spread labels", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + + enum SomeEnum {A} + + enum SomeSpreadEnum {...SomeEnum} + }`; + const expect = ` + namespace Azure.Test { + enum SomeEnum { + A, + } + + enum SomeSpreadEnum { + ...SomeEnum, + } + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.SomeSpreadEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + }); + + describe("unions", () => { + it("discriminated union", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + union MyUnion { + cat: Cat, + dog: Dog, + snake: Snake + } + + model Cat { + name: string; + } + + model Dog { + name: string; + } + + model Snake { + name: string; + length: int16; + } + }`; + const expect = ` + namespace Azure.Test { + model Cat { + name: string; + } + + model Dog { + name: string; + } + + union MyUnion { + cat: Cat, + dog: Dog, + snake: Snake + } + + model Snake { + name: string; + length: int16; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.MyUnion", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Snake", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("unnamed union", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + union Animals { Cat, Dog, Snake }; + + model Cat { + name: string; + } + + model Dog { + name: string; + } + + model Snake { + name: string; + length: int16; + } + }`; + const expect = ` + namespace Azure.Test { + union Animals { + Cat, + Dog, + Snake + } + + model Cat { + name: string; + } + + model Dog { + name: string; + } + + model Snake { + name: string; + length: int16; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Animals", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Snake", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + }); + + describe("operations", () => { + it("templated with simple types", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + op ResourceRead(resource: TResource, params: TParams): TResource; + + op GetFoo is ResourceRead; + + @route("/named") + op NamedGetFoo is ResourceRead; + }`; + const expect = ` + namespace Azure.Test { + op GetFoo is ResourceRead; + + @route("/named") + op NamedGetFoo is ResourceRead; + + op ResourceRead( + resource: TResource, + params: TParams + ): TResource; + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 0 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("templated with deeply nested models", async () => { + const input = ` + @service(#{title: "Service"}) + namespace Azure.Test { + op Foo is Temp< { + parameters: { + fooId: { + bar: { + baz: { + qux: string; + }; + }; + }; + }; + }>; + + op Temp( + params: T + ): void; + }`; + const expect = ` + namespace Azure.Test { + op Foo is Temp< + { + parameters: + { + fooId: + { + bar: + { + baz: + { + qux: string; + }; + }; + }; + }; + } + >; + + op Temp( + params: T + ): void; + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Temp", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("templated with model types", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model FooParams { + a: string; + b: string; + } + + op ResourceRead(resource: TResource, params: TParams): TResource; + + op GetFoo is ResourceRead< + { + @query + @doc("The name") + name: string, + ...FooParams + }, + { + parameters: { + @query + @doc("The collection id.") + fooId: string + }; + } + >; + + @route("/named") + op NamedGetFoo is ResourceRead< + TResource = { + @query + @doc("The name") + name: string, + ...FooParams + }, + TParams = { + parameters: { + @query + @doc("The collection id.") + fooId: string + }; + } + >; + }`; + const expect = ` + namespace Azure.Test { + op GetFoo is ResourceRead< + { + @query + @doc("The name") + name: string; + ...FooParams; + }, + { + parameters: + { + @query + @doc("The collection id.") + fooId: string; + }; + } + >; + + @route("/named") + op NamedGetFoo is ResourceRead< + TResource = { + @query + @doc("The name") + name: string; + ...FooParams; + }, + TParams = { + parameters: + { + @query + @doc("The collection id.") + fooId: string; + }; + } + >; + + op ResourceRead( + resource: TResource, + params: TParams + ): TResource; + + model FooParams { + a: string; + b: string; + } + }`; + // Related line mismatches found!: {"Azure.Test.NamedGetFoo":{"relatedToCount":1,"isContextEndCount":-1}} + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.FooParams", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("templated with mixed types", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + model FooParams { + a: string; + b: string; + } + + op ResourceRead(resource: TResource, params: TParams): TResource; + + op GetFoo is ResourceRead< + string, + { + parameters: { + @query + @doc("The collection id.") + fooId: string + }; + } + >; + + @route("/named") + op NamedGetFoo is ResourceRead< + TResource = string, + TParams = { + parameters: + { + @query + @doc("The collection id.") + fooId: string; + }; + } + >; + }`; + const expect = ` + namespace Azure.Test { + op GetFoo is ResourceRead< + string, + { + parameters: + { + @query + @doc("The collection id.") + fooId: string; + }; + } + >; + + @route("/named") + op NamedGetFoo is ResourceRead< + TResource = string, + TParams = { + parameters: + { + @query + @doc("The collection id.") + fooId: string; + }; + } + >; + + op ResourceRead( + resource: TResource, + params: TParams + ): TResource; + + model FooParams { + a: string; + b: string; + } + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.FooParams", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + + it("templated with empty models", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + + op ResourceRead(resource: TResource, params: TParams): TResource; + + op GetFoo is ResourceRead<{}, {}>; + + @route("/named") + op NamedGetFoo is ResourceRead< + TResource = {}, + TParams = {} + >; + }`; + const expect = ` + namespace Azure.Test { + op GetFoo is ResourceRead< + {}, + {} + >; + + @route("/named") + op NamedGetFoo is ResourceRead< + TResource = {}, + TParams = {} + >; + + op ResourceRead( + resource: TResource, + params: TParams + ): TResource; + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }] + ])); + }); + + it("with anonymous models", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + op SomeOp( + param1: { + name: string + }, + param2: { + age: int16 + } + ): string; + }`; + const expect = ` + namespace Azure.Test { + op SomeOp( + param1: + { + name: string; + }, + param2: + { + age: int16; + } + ): string; + }`; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeOp", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + }); + + describe("interfaces", () => { + it("simple interface", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + interface Foo { + @get + @route("get/{name}") + get(@path name: string): string; + + @get + @route("list") + list(): string[]; + } + } + `; + const expect = ` + namespace Azure.Test { + interface Foo { + @get + @route("get/{name}") + get( + @path + name: string + ): string; + + @get + @route("list") + list(): string[]; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Foo.get", { relatedToCount: 2, isContextEndCount: 1 }], + ["Azure.Test.Foo.list", { relatedToCount: 2, isContextEndCount: 0 }], + ])); + }); + }); + + describe("string literals", () => { + it("long strings", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + @doc(""" + A long string, + with line breaks + and stuff... + """) + model Bar {}; + } + `; + const expect = ` + namespace Azure.Test { + @doc(""" + A long string, + with line breaks + and stuff... + """) + model Bar {} + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Bar", { relatedToCount: 5, isContextEndCount: 0 }], + ])); + }); + + it("short strings", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + @doc("Short string") + model Foo {}; + } + `; + const expect = ` + namespace Azure.Test { + @doc("Short string") + model Foo {} + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 1, isContextEndCount: 0 }], + ])); + }); + }); + + describe("string templates", () => { + it("templates", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + alias myconst = "foobar"; + model Person { + simple: "Simple \${123} end"; + multiline: """ + Multi + \${123} + \${true} + line + """; + ref: "Ref this alias \${myconst} end"; + template: Template<"custom">; + } + alias Template = "Foo \${T} bar"; + }`; + + const expect = ` + namespace Azure.Test { + model Person { + simple: "Simple \${123} end"; + multiline: """ + Multi + \${123} + \${true} + line + """; + ref: "Ref this alias \${myconst} end"; + template: Template<"custom">; + } + + alias myconst = "foobar"; + + alias Template = "Foo \${T} bar"; + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Person", { relatedToCount: 0, isContextEndCount: 1 }], + ])); + }); + }); + + describe("suppressions", () => { + it("suppression on model", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + #suppress "foo" "bar" + @doc("Foo Model") + model Foo { + name: string; + } + } + `; + const expect = ` + namespace Azure.Test { + #suppress "foo" "bar" + @doc("Foo Model") + model Foo { + name: string; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 2, isContextEndCount: 1 }], + ])); + }); + + it("suppression on namespace", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + #suppress "foo" "bar" + @doc("SubNamespace") + namespace SubNamespace { + model Blah { + name: string; + } + } + } + `; + const expect = ` + namespace Azure.Test { + } + + #suppress "foo" "bar" + @doc("SubNamespace") + namespace Azure.Test.SubNamespace { + model Blah { + name: string; + } + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SubNamespace", { relatedToCount: 2, isContextEndCount: 1 }], + ])); + }); + + it("suppression on operation", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + #suppress "foo" "bar" + op someOp(): void; + } + `; + const expect = ` + namespace Azure.Test { + #suppress "foo" "bar" + op someOp(): void; + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.someOp", { relatedToCount: 1, isContextEndCount: 0 }], + ])); + }); + }); + + describe("constants", () => { + it("renders constants", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + const a = 123; + const b = #{name: "abc"}; + const c = a; + } + `; + const expect = ` + namespace Azure.Test { + const a = 123; + const b = #{ + name: "abc" + }; + const c = a; + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ])); + }); + }); + + it("renders examples with call expression constants", async () => { + const input = ` + @TypeSpec.service( #{ title: "Test" } ) + namespace Azure.Test { + const SomeExample: SomeData = #{ + timestamp: utcDateTime.fromISO("2020-12-09T13:50:19.9995668-08:00"), + name: "test" + }; + + @example(SomeExample) + model SomeData { + timestamp: utcDateTime; + name: string; + } + } + `; + const expect = ` + namespace Azure.Test { + @example(SomeExample) + model SomeData { + timestamp: utcDateTime; + name: string; + } + + const SomeExample = #{ + timestamp: utcDateTime.fromISO("2020-12-09T13:50:19.9995668-08:00"), + name: "test" + }; + } + `; + const apiview = await apiViewFor(input, {}); + const actual = apiViewText(apiview); + compare(expect, actual, 6); + validateLineIds(apiview); + const counts = getRelatedLineMetadata(apiview); + compareCounts(counts, new Map([ + ["Azure.Test", { relatedToCount: 1, isContextEndCount: 1 }], + ])); + }); +}); diff --git a/packages/typespec-apiview/test/test-host.ts b/packages/typespec-apiview/test/test-host.ts new file mode 100644 index 0000000000..efcb068778 --- /dev/null +++ b/packages/typespec-apiview/test/test-host.ts @@ -0,0 +1,125 @@ +import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; +import { RestTestLibrary } from "@typespec/rest/testing"; +import { HttpTestLibrary } from "@typespec/http/testing"; +import { VersioningTestLibrary } from "@typespec/versioning/testing"; +import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; +import { ApiViewTestLibrary } from "../src/testing/index.js"; +import "@azure-tools/typespec-apiview"; +import { ApiViewEmitterOptions } from "../src/lib.js"; +import { Diagnostic, resolvePath } from "@typespec/compiler"; +import { strictEqual } from "assert"; +import { CodeFile } from "../src/schemas.js"; +import { reviewLineText } from "../src/util.js"; + +export async function createApiViewTestHost() { + return createTestHost({ + libraries: [ApiViewTestLibrary, RestTestLibrary, HttpTestLibrary, VersioningTestLibrary, AzureCoreTestLibrary], + }); +} + +export async function createApiViewTestRunner({ + withVersioning, +}: { withVersioning?: boolean } = {}) { + const host = await createApiViewTestHost(); + const autoUsings = [ + "TypeSpec.Rest", + "TypeSpec.Http", + ] + if (withVersioning) { + autoUsings.push("TypeSpec.Versioning"); + } + return createTestWrapper(host, { + autoUsings: autoUsings, + compilerOptions: { + emit: ["@azure-tools/typespec-apiview"], + } + }); +} + +export async function diagnosticsFor(code: string, options: ApiViewEmitterOptions): Promise { + const runner = await createApiViewTestRunner({withVersioning: true}); + const outPath = resolvePath("/apiview.json"); + const diagnostics = await runner.diagnose(code, { + noEmit: false, + emit: ["@azure-tools/typespec-apiview"], + options: { + "@azure-tools/typespec-apiview": { + ...options, + "output-file": outPath, + } + }, + miscOptions: { "disable-linter": true }, + }); + return diagnostics; +} + +export async function apiViewFor(code: string, options: ApiViewEmitterOptions): Promise { + const runner = await createApiViewTestRunner({withVersioning: true}); + const outPath = resolvePath("/apiview.json"); + await runner.compile(code, { + noEmit: false, + emit: ["@azure-tools/typespec-apiview"], + options: { + "@azure-tools/typespec-apiview": { + ...options, + "output-file": outPath, + } + }, + miscOptions: { "disable-linter": true }, + }); + + const jsonText = runner.fs.get(outPath)!; + const apiview = JSON.parse(jsonText) as CodeFile; + return apiview; +} + +export function apiViewText(apiview: CodeFile): string[] { + return apiview.ReviewLines.map(l => reviewLineText(l, 0)).join("\n").split("\n"); +} + +function getBaseIndent(lines: string[]): number { + for (const line of lines) { + if (line.trim() !== "") { + return line.length - line.trimStart().length; + } + } + return 0; +} + +/** Eliminates leading indentation and blank links that can mess with comparisons */ +function trimLines(lines: string[]): string[] { + const trimmed: string[] = []; + const indent = getBaseIndent(lines); + + // if first line is blank, skip it + if (lines[0].trim() === "") { + lines = lines.slice(1); + } + + for (const line of lines) { + if (line.trim() === "") { + // ensure blank lines are compared consistently + trimmed.push(""); + } else { + // remove any leading indentation + trimmed.push(line.substring(indent)); + } + } + + // if last line is blank, skip it + const lastLine = trimmed.pop(); + if (lastLine && lastLine.trim() !== "") { + trimmed.push(lastLine) + } + return trimmed; +} + +/** Compares an expected string to a subset of the actual output. */ +export function compare(expect: string, lines: string[], offset: number) { + // split the input into lines and ignore leading or trailing empty lines. + const expectedLines = trimLines(expect.split("\n")); + const actualLines = trimLines(lines.slice(offset)); + for (let x = 0; x < actualLines.length; x++) { + strictEqual(actualLines[x], expectedLines[x], `Actual differed from expected at line #${x + 1}\nACTUAL: '${actualLines[x]}'\nEXPECTED: '${expectedLines[x]}'`); + } +} diff --git a/packages/typespec-apiview/tsconfig.json b/packages/typespec-apiview/tsconfig.json new file mode 100644 index 0000000000..4a2b620ec9 --- /dev/null +++ b/packages/typespec-apiview/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", + "types": ["node", "vitest"], + "composite": true, + "alwaysStrict": true, + "forceConsistentCasingInFileNames": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "sourceMap": true, + "declarationMap": true, + "strict": true, + "declaration": true, + "stripInternal": true, + "noEmitHelpers": false, + "target": "es2019", + "lib": ["es2019"], + "experimentalDecorators": true, + "newLine": "LF", + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/typespec-apiview/vitest.config.ts b/packages/typespec-apiview/vitest.config.ts new file mode 100644 index 0000000000..d78b8d75d0 --- /dev/null +++ b/packages/typespec-apiview/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + }, + outputFile: { + junit: './test-results.xml', + }, + reporters: 'default', + exclude: ['node_modules', 'dist/test', 'dist'], + silent: false, + }, + esbuild: { + sourcemap: true, + }, + server: { + watch: { + ignored: [], + }, + } +}); From a30b1c7d61297ed3d40416105d8283ff6932c6ac Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 8 Jan 2026 15:06:47 -0800 Subject: [PATCH 2/4] Adapt to the monorepo --- .chronus/config.yaml | 1 + packages/typespec-apiview/.c8rc.json | 3 - packages/typespec-apiview/.editorconfig | 12 - packages/typespec-apiview/.eslintrc.cjs | 6 - packages/typespec-apiview/.prettierignore | 319 --------------- packages/typespec-apiview/.prettierrc.yaml | 9 - packages/typespec-apiview/.vscode/launch.json | 55 ++- packages/typespec-apiview/ci.yml | 81 ---- packages/typespec-apiview/cspell.yaml | 3 +- packages/typespec-apiview/eslint.config.mjs | 18 + packages/typespec-apiview/package.json | 82 ++-- packages/typespec-apiview/src/apiview.ts | 335 +++++++++------- packages/typespec-apiview/src/emitter.ts | 44 +- packages/typespec-apiview/src/index.ts | 2 +- packages/typespec-apiview/src/lib.ts | 20 +- .../typespec-apiview/src/namespace-model.ts | 63 +-- packages/typespec-apiview/src/schemas.ts | 73 +++- .../typespec-apiview/src/testing/index.ts | 6 +- packages/typespec-apiview/src/util.ts | 6 +- .../test/apiview-options.test.ts | 10 +- .../typespec-apiview/test/apiview.test.ts | 376 +++++++++++------- packages/typespec-apiview/test/test-host.ts | 58 +-- packages/typespec-apiview/tsconfig.json | 24 +- packages/typespec-apiview/vitest.config.ts | 28 +- pnpm-lock.yaml | 240 +++++------ tsconfig.ws.json | 1 + 26 files changed, 778 insertions(+), 1097 deletions(-) delete mode 100644 packages/typespec-apiview/.c8rc.json delete mode 100644 packages/typespec-apiview/.editorconfig delete mode 100644 packages/typespec-apiview/.eslintrc.cjs delete mode 100644 packages/typespec-apiview/.prettierignore delete mode 100644 packages/typespec-apiview/.prettierrc.yaml delete mode 100644 packages/typespec-apiview/ci.yml create mode 100644 packages/typespec-apiview/eslint.config.mjs diff --git a/.chronus/config.yaml b/.chronus/config.yaml index a310689384..6dc0c12e28 100644 --- a/.chronus/config.yaml +++ b/.chronus/config.yaml @@ -36,6 +36,7 @@ versionPolicies: type: lockstep step: minor packages: + - "@azure-tools/typespec-apiview" - "@azure-tools/typespec-autorest" - "@azure-tools/typespec-azure-core" - "@azure-tools/typespec-azure-portal-core" diff --git a/packages/typespec-apiview/.c8rc.json b/packages/typespec-apiview/.c8rc.json deleted file mode 100644 index 6ce87a95de..0000000000 --- a/packages/typespec-apiview/.c8rc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "reporter": ["cobertura", "json", "text"] -} \ No newline at end of file diff --git a/packages/typespec-apiview/.editorconfig b/packages/typespec-apiview/.editorconfig deleted file mode 100644 index 88bf97c508..0000000000 --- a/packages/typespec-apiview/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -# EditorConfig is awesome: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = crlf -charset = utf-8 -trim_trailing_whitespace = false -insert_final_newline = false \ No newline at end of file diff --git a/packages/typespec-apiview/.eslintrc.cjs b/packages/typespec-apiview/.eslintrc.cjs deleted file mode 100644 index a02edcee2a..0000000000 --- a/packages/typespec-apiview/.eslintrc.cjs +++ /dev/null @@ -1,6 +0,0 @@ -require("@typespec/eslint-config-typespec/patch/modern-module-resolution"); - -module.exports = { - extends: "@typespec/eslint-config-typespec", - parserOptions: { tsconfigRootDir: __dirname }, -}; diff --git a/packages/typespec-apiview/.prettierignore b/packages/typespec-apiview/.prettierignore deleted file mode 100644 index 645633c070..0000000000 --- a/packages/typespec-apiview/.prettierignore +++ /dev/null @@ -1,319 +0,0 @@ -#------------------------------------------------------------------------------------------------------------------- -# Keep this section in sync with .gitignore -#------------------------------------------------------------------------------------------------------------------- - -## Ignore generated code -PackageTest/NugetPackageTest/Generated -src/generator/AutoRest.NodeJS.Tests/AcceptanceTests/*.js - -## Ignore user-specific files, temporary files, build results, etc. -compare-results/* - -# User-specific files -*.suo -*.user -*.sln.docstates -.vs -launchSettings.json - -# Build results -binaries/ -[Dd]ebug*/ -[Rr]elease/ - -[Tt]est[Rr]esult* -[Bb]uild[Ll]og.* -[Bb]uild.out - -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.vspscc -*.vssscc -.builds - -*.pidb - -*.log* -*.scc -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf - -# Visual Studio profiler -*.psess -*.vsp - -# VS Code settings -*.vscode - -# Code analysis -*.CodeAnalysisLog.xml - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a Visual Studio add-in -_ReSharper*/ -*.[Rr]e[Ss]harper - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish - -# Publish Web Output -*.[Pp]ublish.xml - -# Others -[Bb]in -[Oo]bj -sql -*.Cache -ClientBin -[Ss]tyle[Cc]op.* -~$* -*.dbmdl - -# Build tasks -[Tt]ools/*.dll -*.class - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.ear - -# Azure Tooling # -node_modules -.ntvs_analysis.dat - -# Eclipse # -*.pydevproject -.project -.metadata -bin/** -tmp/** -tmp/**/* -*.bak -*.swp -*~.nib -local.properties -.classpath -.settings/ -.loadpath - -# Xamarin # -*.userprefs - -# Other Tooling # -.classpath -.project -target -build -reports -.gradle -.idea -*.iml -Tools/7-Zip -.gitrevision - -# Sensitive files -*.keys -*.pfx -*.cer -*.pem -*.jks - -# Backup & report files from converting a project to a new version of VS. -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML - -# Mac OS # -.DS_Store -.DS_Store? - -# Windows # -Thumbs.db - -# Mono -*dll.mdb -*exe.mdb - -#old nuget restore folder -.nuget/ -src/generator/AutoRest.Ruby*Tests/Gemfile.lock -src/generator/AutoRest.Ruby*/*/RspecTests/Generated/* - -#netcore -/NetCore -*.lock.json - -#dnx installation -dnx-clr-win-x86*/ -dnx-coreclr-win-x86*/ -/dnx - -# Gemfile.lock -Gemfile.lock - -# go ignore -src/generator/AutoRest.Go.Tests/pkg/* -src/generator/AutoRest.Go.Tests/bin/* -src/generator/AutoRest.Go.Tests/src/github.com/* -src/generator/AutoRest.Go.Tests/src/tests/generated/* -src/generator/AutoRest.Go.Tests/src/tests/vendor/* -src/generator/AutoRest.Go.Tests/src/tests/glide.lock - -autorest/**/*.js -core/**/*.js - -*.js.map - -# backup files -*~ - -#client runtime -src/client/**/* - -src/extension/old/**/* -*.d.ts - -src/bootstrapper -src/extension/out -src/next-gen - -package/nuget/tools -package/chocolatey/*.nupkg - -Samples/**/*.map - -# npm (we do want to test for most recent versions) -**/package-lock.json -**/dist/ -src/*/nm -/nm/ -*.tgz - - -# Logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -packages/*/coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next - -# Common toolchain intermediate files -temp - -# Rush files -common/temp/** -package-deps.json - -# Code generation output for regression tests -core/test/regression - -#------------------------------------------------------------------------------------------------------------------- -# Prettier-specific overrides -#------------------------------------------------------------------------------------------------------------------- - -# Rush files -common/changes/ -common/scripts/ -CHANGELOG.* - -# Package manager files -pnpm-lock.yaml -yarn.lock -package-lock.json -shrinkwrap.json - -# Build outputs -dist - -# MICROSOFT SECURITY.md -/SECURITY.md \ No newline at end of file diff --git a/packages/typespec-apiview/.prettierrc.yaml b/packages/typespec-apiview/.prettierrc.yaml deleted file mode 100644 index 99d98ab781..0000000000 --- a/packages/typespec-apiview/.prettierrc.yaml +++ /dev/null @@ -1,9 +0,0 @@ -trailingComma: "all" -printWidth: 120 -quoteProps: "consistent" -endOfLine: lf -tabWidth: 2 -arrowParens: always -plugins: - - "./node_modules/@typespec/prettier-plugin-typespec" -overrides: [{ "files": "*.tsp", "options": { "parser": "typespec" } }] diff --git a/packages/typespec-apiview/.vscode/launch.json b/packages/typespec-apiview/.vscode/launch.json index 5510aa3c23..c4a36890cc 100644 --- a/packages/typespec-apiview/.vscode/launch.json +++ b/packages/typespec-apiview/.vscode/launch.json @@ -1,30 +1,27 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Debug TypeSpec APIView Emitter", - "program": "${workspaceFolder}/node_modules/@typespec/compiler/entrypoints/cli.js", - "args": [ - "compile", - "C:/repos/azure-rest-api-specs/specification/orbital/Microsoft.PlanetaryComputer/main.tsp", - "--emit=C:/repos/azure-sdk-tools/tools/apiview/emitters/typespec-apiview/dist/src/index.js" - ], - "smartStep": true, - "sourceMaps": true, - "skipFiles": ["/**/*.js"], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js", - "${workspaceFolder}/node_modules/**/*.js" - ], - "cwd": "${workspaceFolder}", - "presentation": { - "order": 1 - } - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug TypeSpec APIView Emitter", + "program": "${workspaceFolder}/node_modules/@typespec/compiler/entrypoints/cli.js", + "args": [ + "compile", + "C:/repos/azure-rest-api-specs/specification/orbital/Microsoft.PlanetaryComputer/main.tsp", + "--emit=C:/repos/azure-sdk-tools/tools/apiview/emitters/typespec-apiview/dist/src/index.js" + ], + "smartStep": true, + "sourceMaps": true, + "skipFiles": ["/**/*.js"], + "outFiles": ["${workspaceFolder}/dist/**/*.js", "${workspaceFolder}/node_modules/**/*.js"], + "cwd": "${workspaceFolder}", + "presentation": { + "order": 1 + } + } + ] +} diff --git a/packages/typespec-apiview/ci.yml b/packages/typespec-apiview/ci.yml deleted file mode 100644 index 0c15b46d1f..0000000000 --- a/packages/typespec-apiview/ci.yml +++ /dev/null @@ -1,81 +0,0 @@ -# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. -trigger: - branches: - include: - - main - - feature/* - - release/* - - hotfix/* - paths: - include: - - tools/apiview/emitters/typespec-apiview - -pr: - branches: - include: - - main - - feature/* - - release/* - - hotfix/* - paths: - include: - - tools/apiview/emitters/typespec-apiview - -extends: - template: /eng/pipelines/templates/stages/archetype-sdk-publish-js.yml - parameters: - BuildStageName: Build - ArtifactName: apiview - PackageJsonPath: $(Build.SourcesDirectory)/tools/apiview/emitters/typespec-apiview - BuildStages: - - stage: 'Build' - variables: - - template: /eng/pipelines/templates/variables/image.yml - - name: NodeVersion - value: '22.x' - - name: TypeSpecEmitterDirectory - value: 'tools/apiview/emitters/typespec-apiview' - jobs: - - job: 'Build' - - pool: - name: $(LINUXPOOL) - image: $(LINUXVMIMAGE) - os: linux - - steps: - - task: NodeTool@0 - inputs: - versionSpec: '$(NodeVersion)' - displayName: 'Use NodeJS $(NodeVersion)' - - - script: | - npm ci - workingDirectory: $(TypeSpecEmitterDirectory) - displayName: "Install npm packages for TypeSpec emitter" - - - script: | - npm ls -a || true - workingDirectory: $(TypeSpecEmitterDirectory) - displayName: "List npm packages for TypeSpec emitter" - condition: succeededOrFailed() - - - script: | - npm run-script build - workingDirectory: $(TypeSpecEmitterDirectory) - displayName: "Build TypeSpec emitter" - - - script: | - npm run-script test - workingDirectory: $(TypeSpecEmitterDirectory) - displayName: "Test TypeSpec emitter" - - - pwsh: | - npm pack $(TypeSpecEmitterDirectory) - Copy-Item ./*.tgz $(Build.ArtifactStagingDirectory) - displayName: "Pack TypeSpec Emitter" - - - template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml - parameters: - ArtifactName: apiview - ArtifactPath: $(Build.ArtifactStagingDirectory) diff --git a/packages/typespec-apiview/cspell.yaml b/packages/typespec-apiview/cspell.yaml index 7081335ee1..8b6295d56b 100644 --- a/packages/typespec-apiview/cspell.yaml +++ b/packages/typespec-apiview/cspell.yaml @@ -27,6 +27,7 @@ words: - msbuild - MSRC - munge + - myconst - mylib - nostdlib - oapi @@ -65,4 +66,4 @@ ignorePaths: - "**/dist-dev/**" - "**/tsp-output/**" enableFiletypes: - - tsp \ No newline at end of file + - tsp diff --git a/packages/typespec-apiview/eslint.config.mjs b/packages/typespec-apiview/eslint.config.mjs new file mode 100644 index 0000000000..3202ab459d --- /dev/null +++ b/packages/typespec-apiview/eslint.config.mjs @@ -0,0 +1,18 @@ +// @ts-check +import tsEslint from "typescript-eslint"; +import { TypeSpecCommonEslintConfigs } from "../../core/eslint.config.js"; + +export default tsEslint.config( + { + ignores: ["dist/**"], + }, + ...TypeSpecCommonEslintConfigs, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + }, + }, +); diff --git a/packages/typespec-apiview/package.json b/packages/typespec-apiview/package.json index 34f721d617..7d0b092005 100644 --- a/packages/typespec-apiview/package.json +++ b/packages/typespec-apiview/package.json @@ -3,15 +3,15 @@ "version": "0.7.2", "author": "Microsoft Corporation", "description": "Library for emitting APIView token files from TypeSpec", - "homepage": "https://github.com/Azure/azure-sdk-tools", - "readme": "https://github.com/Azure/azure-sdk-tools/blob/master/README.md", + "homepage": "https://azure.github.io/typespec-azure", + "readme": "https://github.com/Azure/typespec-azure/blob/main/packages/typespec-apiview/README.md", "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/Azure/azure-sdk-tools.git" + "url": "git+https://github.com/Azure/typespec-azure.git" }, "bugs": { - "url": "https://github.com/Azure/azure-sdk-tools/issues" + "url": "https://github.com/Azure/typespec-azure/issues" }, "keywords": [ "typespec", @@ -20,34 +20,31 @@ "type": "module", "main": "dist/src/index.js", "exports": { - ".": "./dist/src/index.js", - "./testing": "./dist/src/testing/index.js" - }, - "typesVersions": { - "*": { - "*": [ - "./dist/src/index.d.ts" - ], - "testing": [ - "./dist/src/testing/index.d.ts" - ] + ".": { + "types": "./dist/src/index.d.ts", + "default": "./dist/src/index.js" + }, + "./testing": { + "types": "./dist/src/testing/index.d.ts", + "default": "./dist/src/testing/index.js" } }, "tspMain": "dist/src/index.js", "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "scripts": { - "clean": "rimraf ./dist ./temp ./tsp-output", - "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", - "purge": "rimraf ./node_modules ./package-lock.json", - "build": "npm run clean && npm run prebuild && tsc -p . && npm run lint-typespec-library", + "clean": "rimraf ./dist ./temp", + "build": "tsc -p . && npm run lint-typespec-library", "watch": "tsc -p . --watch", + "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", - "test": "vitest", - "lint": "eslint . --ext .ts --max-warnings=0", - "lint:fix": "eslint . --fix --ext .ts", - "api-view": "node ./node_modules/@typespec/compiler/entrypoints/cli.js compile main.tsp --emit=@azure-tools/typespec-apiview" + "test": "vitest run", + "test:watch": "vitest -w", + "test:ui": "vitest --ui", + "test:ci": "vitest run --coverage --reporter=junit --reporter=default", + "lint": "eslint . --max-warnings=0", + "lint:fix": "eslint . --fix" }, "files": [ "lib/*.tsp", @@ -55,28 +52,23 @@ "!dist/test/**" ], "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/versioning": ">=0.67 <1.0" + "@typespec/compiler": "workspace:^", + "@typespec/versioning": "workspace:^" }, "devDependencies": { - "@azure-tools/typespec-azure-core": ">=0.53 <1.0", - "@types/node": "~18.11.19", - "@typespec/eslint-plugin": ">=0.67 <1.0", - "@typespec/library-linter": ">=0.67 <1.0", - "@typespec/prettier-plugin-typespec": ">=0.67 <1.0", - "@typespec/rest": ">=0.67 <1.0", - "@typespec/http": "^1.0.0", - "@vitest/coverage-v8": "^3.0.7", - "c8": "^9.1.0", - "cspell": "^8.8.1", - "eslint": "^9.11.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-unicorn": "^53.0.0", - "eslint-plugin-vitest": "^0.5.4", - "prettier": "~3.2.5", - "rimraf": "~5.0.7", - "source-map-support": "^0.5.19", - "typescript": "~5.4.5", - "vitest": "^3.0.7" + "@azure-tools/typespec-azure-core": "workspace:^", + "@types/node": "~25.0.2", + "@typespec/compiler": "workspace:^", + "@typespec/http": "workspace:^", + "@typespec/library-linter": "workspace:^", + "@typespec/rest": "workspace:^", + "@typespec/tspd": "workspace:^", + "@typespec/versioning": "workspace:^", + "@vitest/coverage-v8": "^4.0.15", + "@vitest/ui": "^4.0.15", + "c8": "^10.1.3", + "rimraf": "~6.1.2", + "typescript": "~5.9.2", + "vitest": "^4.0.15" } } diff --git a/packages/typespec-apiview/src/apiview.ts b/packages/typespec-apiview/src/apiview.ts index b5ba7dbba3..e1df7827e0 100644 --- a/packages/typespec-apiview/src/apiview.ts +++ b/packages/typespec-apiview/src/apiview.ts @@ -4,9 +4,9 @@ import { getSourceLocation, Namespace, navigateProgram, - Program + Program, } from "@typespec/compiler"; -import { +import { AliasStatementNode, ArrayExpressionNode, ArrayLiteralNode, @@ -48,12 +48,21 @@ import { UnionExpressionNode, UnionStatementNode, UnionVariantNode, - ValueOfExpressionNode + ValueOfExpressionNode, } from "@typespec/compiler/ast"; import { generateId, NamespaceModel } from "./namespace-model.js"; -import { LIB_VERSION } from "./version.js"; -import { CodeDiagnostic, CodeDiagnosticLevel, CodeFile, NavigationItem, ReviewLine, ReviewToken, ReviewTokenOptions, TokenKind } from "./schemas.js"; +import { + CodeDiagnostic, + CodeDiagnosticLevel, + CodeFile, + NavigationItem, + ReviewLine, + ReviewToken, + ReviewTokenOptions, + TokenKind, +} from "./schemas.js"; import { NamespaceStack, reviewLineText } from "./util.js"; +import { LIB_VERSION } from "./version.js"; export class ApiView { name: string; @@ -86,8 +95,8 @@ export class ApiView { CrossLanguageId: "", Tokens: [], Children: [], - } - this.parentStack = [] + }; + this.parentStack = []; this.currentParent = undefined; this.emitHeader(); } @@ -122,12 +131,11 @@ export class ApiView { } private appendDebugInfo() { - function processLines(lines: ReviewLine[]) { for (const line of lines) { const lineId = line.LineId; const relatedLineId = line.RelatedToLine; - const isContextEnd = line.IsContextEndLine + const isContextEnd = line.IsContextEndLine; let string = ""; if (lineId && lineId !== "") { string += ` LINE_ID: ${lineId} `; @@ -142,10 +150,10 @@ export class ApiView { line.Tokens.push({ Kind: TokenKind.Comment, Value: `// ${string.trim()}`, - }) + }); } processLines(line.Children); - } + } } processLines(this.reviewLines); @@ -170,7 +178,7 @@ export class ApiView { const lineId = line.LineId; if (lineId === "Azure.Test.ConstrainedComplex") { - let test = "best"; + // Debugging hook } const relatedTo = line.RelatedToLine; const isContextEnd = line.IsContextEndLine; @@ -207,11 +215,11 @@ export class ApiView { // if currentContext isn't set and we encounter a relatedTo line, set the current context if (relatedTo) { currentContext = relatedTo; - // if currentContext isn't set but there's a lineId with childrent, set the current context + // if currentContext isn't set but there's a lineId with childrent, set the current context } else if (lineId && line.Children.length > 0) { currentContext = lineId; contextMatchFound = true; - // If a context end is found without a start, then ignore the contextEnd + // If a context end is found without a start, then ignore the contextEnd } else if (isContextEnd) { line.IsContextEndLine = false; } @@ -251,12 +259,11 @@ export class ApiView { Diagnostics: this.diagnostics, Navigation: this.navigationItems, }; - } /** Outputs the APIView model to a string approximation of what will display on the web. */ asString(): string { - return this.reviewLines.map(l => reviewLineText(l, 0)).join("\n"); + return this.reviewLines.map((l) => reviewLineText(l, 0)).join("\n"); } private token(kind: TokenKind, value: string, options?: ReviewTokenOptions) { @@ -272,7 +279,7 @@ export class ApiView { try { const lastToken = this.currentLine.Tokens[this.currentLine.Tokens.length - 1]; lastToken.HasSuffixSpace = false; - } catch (e) { + } catch { // no tokens, so nothing to do return; } @@ -289,7 +296,7 @@ export class ApiView { CrossLanguageId: "", Tokens: [], Children: [], - } + }; } private deindent() { @@ -307,7 +314,7 @@ export class ApiView { CrossLanguageId: "", Tokens: [], Children: [], - } + }; } /** Set the exact number of desired newlines. */ @@ -324,7 +331,7 @@ export class ApiView { } } } - if (newlineCount == count) { + if (newlineCount === count) { return; } else if (newlineCount > count) { const toRemove = newlineCount - count; @@ -346,7 +353,7 @@ export class ApiView { const firstToken = this.currentLine.Tokens[0]; firstToken.HasPrefixSpace = false; } - + if (this.currentParent) { this.currentParent.Children.push(this.currentLine); } else { @@ -357,7 +364,7 @@ export class ApiView { CrossLanguageId: "", Tokens: [], Children: [], - } + }; } private getLastLine(): ReviewLine | undefined { @@ -372,7 +379,7 @@ export class ApiView { return lastGrandchild ?? lastChild; } - /** + /** * Places the provided token in the tree based on the provided characters. * param token The token to snap. * param characters The characters to snap to. @@ -413,13 +420,22 @@ export class ApiView { } } - private lineMarker(options?: {value?: string, addCrossLanguageId?: boolean, relatedLineId?: string}) { + private lineMarker(options?: { + value?: string; + addCrossLanguageId?: boolean; + relatedLineId?: string; + }) { this.currentLine.LineId = options?.value ?? this.namespaceStack.value(); - this.currentLine.CrossLanguageId = options?.addCrossLanguageId ? (options?.value ?? this.namespaceStack.value()) : undefined; + this.currentLine.CrossLanguageId = options?.addCrossLanguageId + ? (options?.value ?? this.namespaceStack.value()) + : undefined; this.currentLine.RelatedToLine = options?.relatedLineId; } - private punctuation(value: string, options?: ReviewTokenOptions & {snapTo?: string, isContextEndLine?: boolean}) { + private punctuation( + value: string, + options?: ReviewTokenOptions & { snapTo?: string; isContextEndLine?: boolean }, + ) { const snapTo = options?.snapTo; delete options?.snapTo; const isContextEndLine = options?.isContextEndLine; @@ -429,7 +445,7 @@ export class ApiView { Kind: TokenKind.Punctuation, Value: value, ...options, - } + }; if (snapTo) { this.snapToken(token, snapTo); @@ -449,21 +465,26 @@ export class ApiView { this.token(TokenKind.Keyword, keyword, options); } - private typeDeclaration(typeName: string, typeId: string | undefined, addCrossLanguageId: boolean, options?: ReviewTokenOptions) { + private typeDeclaration( + typeName: string, + typeId: string | undefined, + addCrossLanguageId: boolean, + options?: ReviewTokenOptions, + ) { if (typeId) { if (this.typeDeclarations.has(typeId)) { throw new Error(`Duplication ID "${typeId}" for declaration will result in bugs.`); } this.typeDeclarations.add(typeId); } - this.lineMarker({value: typeId, addCrossLanguageId: true}); + this.lineMarker({ value: typeId, addCrossLanguageId: true }); this.token(TokenKind.TypeName, typeName, options); } private typeReference(typeName: string, options?: ReviewTokenOptions) { options = options ?? {}; options.NavigateToId = options.NavigateToId ?? "__MISSING__"; - this.token(TokenKind.TypeName, typeName, {...options}); + this.token(TokenKind.TypeName, typeName, { ...options }); } private member(name: string, options?: ReviewTokenOptions) { @@ -476,7 +497,7 @@ export class ApiView { this.currentLine.Tokens.push({ Kind: TokenKind.StringLiteral, Value: `\u0022${value}\u0022`, - ...options + ...options, }); } else { this.punctuation(`"""`, options); @@ -498,7 +519,7 @@ export class ApiView { Text: message, TargetId: targetId, Level: level, - }) + }); } private shouldEmitNamespace(name: string): boolean { @@ -518,26 +539,26 @@ export class ApiView { private emitHeader() { const toolVersion = LIB_VERSION; const headerText = `// Package parsed using @azure-tools/typespec-apiview (version:${toolVersion})`; - this.literal(headerText, {SkipDiff: true}); + this.literal(headerText, { SkipDiff: true }); this.namespaceStack.push("GLOBAL"); this.lineMarker(); this.namespaceStack.pop(); // TODO: Source URL? - this.blankLines(2) + this.blankLines(2); } private tokenize(node: BaseNode) { let obj; - let last = 0; // track the final index of an array + let last = 0; // track the final index of an array let parentNamespace: string; switch (node.kind) { case SyntaxKind.AliasStatement: obj = node as AliasStatementNode; this.namespaceStack.push(obj.id.sv); - this.keyword("alias", {HasSuffixSpace: true}); + this.keyword("alias", { HasSuffixSpace: true }); this.typeDeclaration(obj.id.sv, this.namespaceStack.value(), true); this.tokenizeTemplateParameters(obj.templateParameters); - this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.value); this.namespaceStack.pop(); break; @@ -553,7 +574,7 @@ export class ApiView { obj.values.forEach((val, i) => { this.tokenize(val); if (i !== last) { - this.punctuation(",", {HasSuffixSpace: true}); + this.punctuation(",", { HasSuffixSpace: true }); } }); this.punctuation("]"); @@ -570,13 +591,13 @@ export class ApiView { this.punctuation("("); this.tokenize(obj.targetType); if (obj.arguments.length) { - this.punctuation(",", {HasSuffixSpace: true}); + this.punctuation(",", { HasSuffixSpace: true }); } for (let x = 0; x < obj.arguments.length; x++) { const arg = obj.arguments[x]; this.tokenize(arg); if (x !== last) { - this.punctuation(",", {HasSuffixSpace: true}); + this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation(")"); @@ -594,9 +615,9 @@ export class ApiView { case SyntaxKind.ConstStatement: obj = node as ConstStatementNode; this.namespaceStack.push(obj.id.sv); - this.keyword("const", {HasSuffixSpace: true}); + this.keyword("const", { HasSuffixSpace: true }); this.tokenizeIdentifier(obj.id, "declaration"); - this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.value); this.namespaceStack.pop(); break; @@ -606,7 +627,7 @@ export class ApiView { this.namespaceStack.push(generateId(obj)!); this.punctuation("@"); this.tokenizeIdentifier(obj.target, "keyword"); - this.lineMarker({relatedLineId: parentNamespace}); + this.lineMarker({ relatedLineId: parentNamespace }); if (obj.arguments.length) { last = obj.arguments.length - 1; this.punctuation("("); @@ -614,7 +635,7 @@ export class ApiView { const arg = obj.arguments[x]; this.tokenize(arg); if (x !== last) { - this.punctuation(",", {HasSuffixSpace: true}); + this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation(")"); @@ -625,15 +646,15 @@ export class ApiView { obj = node as DirectiveExpressionNode; parentNamespace = this.namespaceStack.value(); this.namespaceStack.push(generateId(node)!); - this.lineMarker({relatedLineId: parentNamespace}); - this.keyword(`#${obj.target.sv}`, {HasSuffixSpace: true}); + this.lineMarker({ relatedLineId: parentNamespace }); + this.keyword(`#${obj.target.sv}`, { HasSuffixSpace: true }); for (const arg of obj.arguments) { switch (arg.kind) { case SyntaxKind.StringLiteral: - this.stringLiteral(arg.value, {HasSuffixSpace: true}); + this.stringLiteral(arg.value, { HasSuffixSpace: true }); break; case SyntaxKind.Identifier: - this.stringLiteral(arg.sv, {HasSuffixSpace: true}); + this.stringLiteral(arg.sv, { HasSuffixSpace: true }); break; } } @@ -646,9 +667,9 @@ export class ApiView { obj = node as EnumMemberNode; this.tokenizeDecoratorsAndDirectives(obj.decorators, obj.directives, false); this.tokenizeIdentifier(obj.id, "member"); - this.lineMarker({addCrossLanguageId: true}); + this.lineMarker({ addCrossLanguageId: true }); if (obj.value) { - this.punctuation(":", {HasSuffixSpace: true}); + this.punctuation(":", { HasSuffixSpace: true }); this.tokenize(obj.value); } break; @@ -668,7 +689,7 @@ export class ApiView { case SyntaxKind.Identifier: obj = node as IdentifierNode; const id = this.namespaceStack.value(); - this.typeReference(obj.sv, {NavigateToId: id}); + this.typeReference(obj.sv, { NavigateToId: id }); break; case SyntaxKind.ImportStatement: throw new Error(`Case "ImportStatement" not implemented`); @@ -678,7 +699,7 @@ export class ApiView { const opt = obj.options[x]; this.tokenize(opt); if (x !== obj.options.length - 1) { - this.punctuation("&", {HasPrefixSpace: true, HasSuffixSpace: true}); + this.punctuation("&", { HasPrefixSpace: true, HasSuffixSpace: true }); } } break; @@ -694,7 +715,7 @@ export class ApiView { break; case SyntaxKind.ModelExpression: this.indent(); - this.tokenizeModelExpression(node as ModelExpressionNode, {isOperationSignature: false}); + this.tokenizeModelExpression(node as ModelExpressionNode, { isOperationSignature: false }); this.deindent(); break; case SyntaxKind.ModelProperty: @@ -727,7 +748,7 @@ export class ApiView { obj.properties.forEach((prop, i) => { this.tokenize(prop); if (i !== last) { - this.punctuation(",", {HasSuffixSpace: false}); + this.punctuation(",", { HasSuffixSpace: false }); } this.newline(); }); @@ -737,7 +758,7 @@ export class ApiView { case SyntaxKind.ObjectLiteralProperty: obj = node as ObjectLiteralPropertyNode; this.tokenizeIdentifier(obj.id, "member"); - this.punctuation(":", {HasSuffixSpace: true}); + this.punctuation(":", { HasSuffixSpace: true }); this.tokenize(obj.value); break; case SyntaxKind.ObjectLiteralSpreadProperty: @@ -752,15 +773,15 @@ export class ApiView { this.punctuation("("); if (obj.parameters.properties.length) { this.indent(); - this.tokenizeModelExpression(obj.parameters, {isOperationSignature: true}); - this.deindent(); + this.tokenizeModelExpression(obj.parameters, { isOperationSignature: true }); + this.deindent(); } - this.punctuation("):", {HasSuffixSpace: true}); - this.tokenizeReturnType(obj, {isExpanded: true}); + this.punctuation("):", { HasSuffixSpace: true }); + this.tokenizeReturnType(obj, { isExpanded: true }); break; case SyntaxKind.OperationSignatureReference: obj = node as OperationSignatureReferenceNode; - this.keyword("is", {HasPrefixSpace: true, HasSuffixSpace: true}); + this.keyword("is", { HasPrefixSpace: true, HasSuffixSpace: true }); this.tokenize(obj.baseOperation); break; case SyntaxKind.Return: @@ -776,22 +797,22 @@ export class ApiView { obj = node as TemplateParameterDeclarationNode; this.tokenize(obj.id); if (obj.constraint) { - this.keyword("extends", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.keyword("extends", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.constraint); } if (obj.default) { - this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(obj.default); } break; case SyntaxKind.TupleExpression: obj = node as TupleExpressionNode; - this.punctuation("[", {HasSuffixSpace: true}); + this.punctuation("[", { HasSuffixSpace: true }); for (let x = 0; x < obj.values.length; x++) { const val = obj.values[x]; this.tokenize(val); if (x !== obj.values.length - 1) { - this.punctuation(",", {HasSuffixSpace: true}); + this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation("]"); @@ -807,7 +828,7 @@ export class ApiView { const opt = obj.options[x]; this.tokenize(opt); if (x !== obj.options.length - 1) { - this.punctuation("|", {HasPrefixSpace: true, HasSuffixSpace: true}); + this.punctuation("|", { HasPrefixSpace: true, HasSuffixSpace: true }); } } break; @@ -823,14 +844,14 @@ export class ApiView { case SyntaxKind.UsingStatement: throw new Error(`Case "UsingStatement" not implemented`); case SyntaxKind.ValueOfExpression: - this.keyword("valueof", {HasSuffixSpace: true}); + this.keyword("valueof", { HasSuffixSpace: true }); this.tokenize((node as ValueOfExpressionNode).target); break; case SyntaxKind.VoidKeyword: this.keyword("void"); break; case SyntaxKind.TemplateArgument: - + break; case SyntaxKind.StringTemplateExpression: obj = node as StringTemplateExpressionNode; const stringValue = this.buildTemplateString(obj); @@ -867,12 +888,12 @@ export class ApiView { case SyntaxKind.CallExpression: obj = node as CallExpressionNode; this.tokenize(obj.target); - this.punctuation("(", {HasSuffixSpace: false}); + this.punctuation("(", { HasSuffixSpace: false }); for (let x = 0; x < obj.arguments.length; x++) { const arg = obj.arguments[x]; this.tokenize(arg); if (x !== obj.arguments.length - 1) { - this.punctuation(",", {HasSuffixSpace: true, snapTo: "}"}); + this.punctuation(",", { HasSuffixSpace: true, snapTo: "}" }); } } this.punctuation(")"); @@ -889,7 +910,9 @@ export class ApiView { } // if any argument is a ModelExpression, then we need to expand the template to multiple lines - const isExpanded = obj.arguments.some(arg => arg.argument.kind === SyntaxKind.ModelExpression); + const isExpanded = obj.arguments.some( + (arg) => arg.argument.kind === SyntaxKind.ModelExpression, + ); this.punctuation("<"); if (isExpanded) { @@ -899,7 +922,7 @@ export class ApiView { const arg = obj.arguments[x]; this.tokenizeTemplateArgument(arg); if (x !== obj.arguments.length - 1) { - this.punctuation(",", {HasSuffixSpace: true, snapTo: "}"}); + this.punctuation(",", { HasSuffixSpace: true, snapTo: "}" }); if (isExpanded && arg.argument.kind) { this.blankLines(0); } @@ -934,6 +957,7 @@ export class ApiView { case SyntaxKind.MemberExpression: return this.getFullyQualifiedIdentifier(obj.target as MemberExpressionNode); } + break; default: throw new Error(`Unsupported expression kind: ${SyntaxKind[node.kind]}`); //unsupported ArrayExpressionNode | MemberExpressionNode | ModelExpressionNode | TupleExpressionNode | UnionExpressionNode | IntersectionExpressionNode | TypeReferenceNode | ValueOfExpressionNode | AnyKeywordNode; @@ -953,19 +977,19 @@ export class ApiView { private tokenizeModelStatement(node: ModelStatementNode) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); - this.keyword("model", {HasSuffixSpace: true}); + this.keyword("model", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); if (node.extends) { - this.keyword("extends", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.keyword("extends", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(node.extends); } if (node.is) { - this.keyword("is", {HasPrefixSpace: true, HasSuffixSpace: true}); + this.keyword("is", { HasPrefixSpace: true, HasSuffixSpace: true }); this.tokenize(node.is); } this.tokenizeTemplateParameters(node.templateParameters); if (node.properties.length) { - this.punctuation("{", {HasPrefixSpace: true}); + this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (const prop of node.properties) { const propName = this.getNameForNode(prop); @@ -973,12 +997,12 @@ export class ApiView { this.tokenize(prop); this.punctuation(";"); this.namespaceStack.pop(); - this.newline() + this.newline(); } this.deindent(); - this.punctuation("}", {isContextEndLine: true}); + this.punctuation("}", { isContextEndLine: true }); } else { - this.punctuation("{}", {HasPrefixSpace: true}); + this.punctuation("{}", { HasPrefixSpace: true }); } this.namespaceStack.pop(); } @@ -986,41 +1010,41 @@ export class ApiView { private tokenizeScalarStatement(node: ScalarStatementNode) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); - this.keyword("scalar", {HasSuffixSpace: true}); + this.keyword("scalar", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); if (node.extends) { - this.keyword("extends", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.keyword("extends", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(node.extends); } this.tokenizeTemplateParameters(node.templateParameters); - this.newline() + this.newline(); this.namespaceStack.pop(); } private tokenizeInterfaceStatement(node: InterfaceStatementNode) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); - this.keyword("interface", {HasSuffixSpace: true}); + this.keyword("interface", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); this.tokenizeTemplateParameters(node.templateParameters); - this.punctuation("{", {HasPrefixSpace: true}); + this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (let x = 0; x < node.operations.length; x++) { const op = node.operations[x]; this.tokenizeOperationStatement(op, true); - this.blankLines(1) + this.blankLines(1); } this.deindent(); - this.punctuation("}", {isContextEndLine: true}); + this.punctuation("}", { isContextEndLine: true }); this.namespaceStack.pop(); } private tokenizeEnumStatement(node: EnumStatementNode) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); - this.keyword("enum", {HasSuffixSpace: true}); + this.keyword("enum", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); - this.punctuation("{", {HasPrefixSpace: true}); + this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (const member of node.members) { const memberName = this.getNameForNode(member); @@ -1028,19 +1052,19 @@ export class ApiView { this.tokenize(member); this.punctuation(","); this.namespaceStack.pop(); - this.newline() + this.newline(); } this.deindent(); - this.punctuation("}", {isContextEndLine: true}); + this.punctuation("}", { isContextEndLine: true }); this.namespaceStack.pop(); } private tokenizeUnionStatement(node: UnionStatementNode) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); - this.keyword("union", {HasSuffixSpace: true}); + this.keyword("union", { HasSuffixSpace: true }); this.tokenizeIdentifier(node.id, "declaration"); - this.punctuation("{", {HasPrefixSpace: true}); + this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (let x = 0; x < node.options.length; x++) { const variant = node.options[x]; @@ -1051,10 +1075,10 @@ export class ApiView { if (x !== node.options.length - 1) { this.punctuation(","); } - this.newline() + this.newline(); } this.deindent(); - this.punctuation("}", {isContextEndLine: true}); + this.punctuation("}", { isContextEndLine: true }); this.namespaceStack.pop(); } @@ -1062,9 +1086,9 @@ export class ApiView { this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); if (node.id !== undefined) { this.tokenizeIdentifier(node.id, "member"); - this.punctuation(":", {HasSuffixSpace: true}); + this.punctuation(":", { HasSuffixSpace: true }); } - this.lineMarker({addCrossLanguageId: true}); + this.lineMarker({ addCrossLanguageId: true }); this.tokenize(node.value); } @@ -1072,10 +1096,10 @@ export class ApiView { this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, inline); this.tokenizeIdentifier(node.id, "member"); this.lineMarker(); - this.punctuation(node.optional ? "?:" : ":", {HasSuffixSpace: true}); + this.punctuation(node.optional ? "?:" : ":", { HasSuffixSpace: true }); this.tokenize(node.value); if (node.default) { - this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); this.tokenize(node.default); } } @@ -1083,61 +1107,65 @@ export class ApiView { /** Expands and tokenizes a model expression (anonymous model) */ private tokenizeModelExpression( node: ModelExpressionNode, - options: {isOperationSignature: boolean}) { - const isOperationSignature = options.isOperationSignature; + options: { isOperationSignature: boolean }, + ) { + const isOperationSignature = options.isOperationSignature; - // display {} for empty model or nothing for empty operation signature - if (!node.properties.length) { - if (!isOperationSignature) { - this.punctuation("{}", {HasPrefixSpace: true}); - } - return; + // display {} for empty model or nothing for empty operation signature + if (!node.properties.length) { + if (!isOperationSignature) { + this.punctuation("{}", { HasPrefixSpace: true }); } + return; + } - if (!isOperationSignature) { - this.punctuation("{"); - this.indent(); + if (!isOperationSignature) { + this.punctuation("{"); + this.indent(); + } + this.namespaceStack.push("anonymous"); + for (let x = 0; x < node.properties.length; x++) { + const prop = node.properties[x]; + const propName = this.getNameForNode(prop); + this.namespaceStack.push(propName); + switch (prop.kind) { + case SyntaxKind.ModelProperty: + this.tokenizeModelProperty(prop, false); + break; + case SyntaxKind.ModelSpreadProperty: + this.tokenize(prop); } - this.namespaceStack.push("anonymous"); - for (let x = 0; x < node.properties.length; x++) { - const prop = node.properties[x]; - const propName = this.getNameForNode(prop); - this.namespaceStack.push(propName); - switch (prop.kind) { - case SyntaxKind.ModelProperty: - this.tokenizeModelProperty(prop, false); - break; - case SyntaxKind.ModelSpreadProperty: - this.tokenize(prop); - } - this.namespaceStack.pop(); - if (isOperationSignature) { - if (x !== node.properties.length - 1) { - this.punctuation(",", {HasSuffixSpace: true, snapTo: "}"}); - } - } else { - this.punctuation(";", {HasSuffixSpace: true, snapTo: "}"}); + this.namespaceStack.pop(); + if (isOperationSignature) { + if (x !== node.properties.length - 1) { + this.punctuation(",", { HasSuffixSpace: true, snapTo: "}" }); } - this.blankLines(0); - } - if (!isOperationSignature) { - this.deindent(); - this.punctuation("}"); - this.newline(); + } else { + this.punctuation(";", { HasSuffixSpace: true, snapTo: "}" }); } - this.namespaceStack.pop(); + this.blankLines(0); + } + if (!isOperationSignature) { + this.deindent(); + this.punctuation("}"); + this.newline(); + } + this.namespaceStack.pop(); } - private tokenizeOperationStatement(node: OperationStatementNode, suppressOpKeyword: boolean = false) { + private tokenizeOperationStatement( + node: OperationStatementNode, + suppressOpKeyword: boolean = false, + ) { this.namespaceStack.push(node.id.sv); this.tokenizeDecoratorsAndDirectives(node.decorators, node.directives, false); if (!suppressOpKeyword) { - this.keyword("op", {HasSuffixSpace: true}); + this.keyword("op", { HasSuffixSpace: true }); } this.tokenizeIdentifier(node.id, "declaration"); this.tokenizeTemplateParameters(node.templateParameters); this.tokenize(node.signature); - this.punctuation(";", {isContextEndLine: true}); + this.punctuation(";", { isContextEndLine: true }); this.namespaceStack.pop(); } @@ -1146,9 +1174,9 @@ export class ApiView { if (model.node.kind === SyntaxKind.NamespaceStatement) { this.tokenizeDecoratorsAndDirectives(model.node.decorators, model.node.directives, false); } - this.keyword("namespace", {HasSuffixSpace: true}); - this.typeDeclaration(model.name, this.namespaceStack.value(), true, {HasSuffixSpace: true}); - this.punctuation("{", {HasPrefixSpace: true}); + this.keyword("namespace", { HasSuffixSpace: true }); + this.typeDeclaration(model.name, this.namespaceStack.value(), true, { HasSuffixSpace: true }); + this.punctuation("{", { HasPrefixSpace: true }); this.indent(); for (const node of model.augmentDecorators) { this.tokenize(node); @@ -1172,12 +1200,12 @@ export class ApiView { this.blankLines(1); } for (const node of model.constants.values()) { - this.tokenize(node); - this.punctuation(";"); - this.blankLines(0); + this.tokenize(node); + this.punctuation(";"); + this.blankLines(0); } this.deindent(); - this.punctuation("}", {isContextEndLine: true}); + this.punctuation("}", { isContextEndLine: true }); this.blankLines(1); this.namespaceStack.pop(); } @@ -1205,7 +1233,7 @@ export class ApiView { } } if (!inline) { - this.newline() + this.newline(); } } } @@ -1249,11 +1277,13 @@ export class ApiView { case SyntaxKind.Identifier: switch (style) { case "declaration": - this.typeDeclaration(node.sv, this.namespaceStack.value(), true, {HasSuffixSpace: false}); + this.typeDeclaration(node.sv, this.namespaceStack.value(), true, { + HasSuffixSpace: false, + }); break; case "reference": const defId = this.definitionIdFor(node.sv, this.packageName); - this.typeReference(node.sv, {NavigateToId: defId}); + this.typeReference(node.sv, { NavigateToId: defId }); break; case "member": this.member(this.getRawText(node)); @@ -1276,7 +1306,7 @@ export class ApiView { const param = nodes[x]; this.tokenize(param); if (x !== nodes.length - 1) { - this.punctuation(",", {HasSuffixSpace: true}); + this.punctuation(",", { HasSuffixSpace: true }); } } this.punctuation(">"); @@ -1286,16 +1316,19 @@ export class ApiView { private tokenizeTemplateArgument(obj: TemplateArgumentNode) { if (obj.name) { this.text(obj.name.sv); - this.punctuation("=", {HasSuffixSpace: true, HasPrefixSpace: true}); + this.punctuation("=", { HasSuffixSpace: true, HasPrefixSpace: true }); } if (obj.argument.kind === SyntaxKind.ModelExpression) { - this.tokenizeModelExpression(obj.argument, {isOperationSignature: false}); + this.tokenizeModelExpression(obj.argument, { isOperationSignature: false }); } else { this.tokenize(obj.argument); } } - private tokenizeReturnType(node: OperationSignatureDeclarationNode, options: { isExpanded: boolean}) { + private tokenizeReturnType( + node: OperationSignatureDeclarationNode, + options: { isExpanded: boolean }, + ) { if (options.isExpanded && node.parameters.properties.length) { const offset = this.currentLine.Tokens.length; this.tokenize(node.returnType); diff --git a/packages/typespec-apiview/src/emitter.ts b/packages/typespec-apiview/src/emitter.ts index 9b908dd46d..512f96e04d 100644 --- a/packages/typespec-apiview/src/emitter.ts +++ b/packages/typespec-apiview/src/emitter.ts @@ -26,7 +26,9 @@ export async function $onEmit(context: EmitContext) { await emitter.emitApiView(); } -export function resolveOptions(context: EmitContext): ResolvedApiViewEmitterOptions { +export function resolveOptions( + context: EmitContext, +): ResolvedApiViewEmitterOptions { const resolvedOptions = { ...context.options }; return { @@ -47,16 +49,20 @@ function resolveNamespaceString(namespace: Namespace): string | undefined { * Ensures that single-value options are not used in multi-service specs unless the * `--service` option is specified. Single-service specs need not pass this option. */ -function validateMultiServiceOptions(program: Program, services: Service[], options: ResolvedApiViewEmitterOptions) { +function validateMultiServiceOptions( + program: Program, + services: Service[], + options: ResolvedApiViewEmitterOptions, +) { for (const [name, val] of [["output-file", options.outputFile]]) { if (val && !options.service && services.length > 1) { reportDiagnostic(program, { code: "invalid-option", target: NoTarget, format: { - name: name! - } - }) + name: name!, + }, + }); } } } @@ -64,18 +70,22 @@ function validateMultiServiceOptions(program: Program, services: Service[], opti /** * If the `--service` option is provided, ensures the service exists and returns the filtered list. */ -function applyServiceFilter(program: Program, services: Service[], options: ResolvedApiViewEmitterOptions): Service[] { +function applyServiceFilter( + program: Program, + services: Service[], + options: ResolvedApiViewEmitterOptions, +): Service[] { if (!options.service) { return services; } - const filtered = services.filter( (x) => x.title === options.service); + const filtered = services.filter((x) => x.title === options.service); if (!filtered.length) { reportDiagnostic(program, { code: "invalid-service", target: NoTarget, format: { - value: options.service - } + value: options.service, + }, }); } return filtered; @@ -89,21 +99,21 @@ function createApiViewEmitter(program: Program, options: ResolvedApiViewEmitterO if (!services.length) { reportDiagnostic(program, { code: "no-services-found", - target: NoTarget - }) + target: NoTarget, + }); return; } // applies the default "apiview.json" filename if not provided and there's only a single service if (services.length === 1) { - options.outputFile = options.outputFile ?? "apiview.json" + options.outputFile = options.outputFile ?? "apiview.json"; } validateMultiServiceOptions(program, services, options); services = applyServiceFilter(program, services, options); for (const service of services) { - const namespaceString = resolveNamespaceString(service.type) ?? "Unknown" + const namespaceString = resolveNamespaceString(service.type) ?? "Unknown"; const serviceTitle = service.title ? service.title : namespaceString; - + const apiview = new ApiView(serviceTitle, namespaceString, options.includeGlobalNamespace); apiview.compile(program); apiview.resolveMissingTypeReferences(); @@ -115,9 +125,9 @@ function createApiViewEmitter(program: Program, options: ResolvedApiViewEmitterO const outputPath = resolvePath(outputFolder, outputFile); await emitFile(program, { path: outputPath, - content: JSON.stringify(apiview.asCodeFile()) + "\n" - }); - } + content: JSON.stringify(apiview.asCodeFile()) + "\n", + }); + } } } } diff --git a/packages/typespec-apiview/src/index.ts b/packages/typespec-apiview/src/index.ts index bc7cadcc0b..e776ee5822 100644 --- a/packages/typespec-apiview/src/index.ts +++ b/packages/typespec-apiview/src/index.ts @@ -1,4 +1,4 @@ export const namespace = "ApiView"; -export { $lib } from "./lib.js"; export * from "./emitter.js"; +export { $lib } from "./lib.js"; diff --git a/packages/typespec-apiview/src/lib.ts b/packages/typespec-apiview/src/lib.ts index 1f454841dc..4cd3f13e03 100644 --- a/packages/typespec-apiview/src/lib.ts +++ b/packages/typespec-apiview/src/lib.ts @@ -2,8 +2,8 @@ import { createTypeSpecLibrary, JSONSchemaType, paramMessage } from "@typespec/c export interface ApiViewEmitterOptions { "output-file"?: string; - "service"?: string; - "include-global-namespace"?: boolean, + service?: string; + "include-global-namespace"?: boolean; "mapping-path"?: string; } @@ -12,40 +12,40 @@ const ApiViewEmitterOptionsSchema: JSONSchemaType = { additionalProperties: false, properties: { "output-file": { type: "string", nullable: true }, - "service": { type: "string", nullable: true }, - "include-global-namespace": {type: "boolean", nullable: true}, + service: { type: "string", nullable: true }, + "include-global-namespace": { type: "boolean", nullable: true }, "mapping-path": { type: "string", nullable: true }, }, required: [], }; - export const $lib = createTypeSpecLibrary({ name: "@azure-tools/typespec-apiview", diagnostics: { "no-services-found": { severity: "error", messages: { - default: "No services found. Ensure there is a namespace in the spec annotated with the `@service` decorator." - } + default: + "No services found. Ensure there is a namespace in the spec annotated with the `@service` decorator.", + }, }, "invalid-service": { severity: "error", messages: { default: paramMessage`Service "${"value"}" was not found. Please check for typos.`, - } + }, }, "invalid-option": { severity: "error", messages: { default: paramMessage`Option "--${"name"}" cannot be used with multi-service specs unless "--service" is also supplied.`, - } + }, }, "version-not-found": { severity: "error", messages: { default: paramMessage`Version "${"version"}" not found for service "${"serviceName"}". Allowed values: ${"allowed"}.`, - } + }, }, }, emitter: { diff --git a/packages/typespec-apiview/src/namespace-model.ts b/packages/typespec-apiview/src/namespace-model.ts index 31521d0187..2f9dde2a6f 100644 --- a/packages/typespec-apiview/src/namespace-model.ts +++ b/packages/typespec-apiview/src/namespace-model.ts @@ -1,38 +1,35 @@ +import { Namespace, Program } from "@typespec/compiler"; import { - Namespace, - Program, -} from "@typespec/compiler"; -import { - Node, AliasStatementNode, - ModelStatementNode, - OperationStatementNode, - InterfaceStatementNode, - EnumStatementNode, - NamespaceStatementNode, - ModelExpressionNode, - IntersectionExpressionNode, - SyntaxKind, + AugmentDecoratorStatementNode, BaseNode, - IdentifierNode, - ModelPropertyNode, + ConstStatementNode, + DecoratorExpressionNode, + DirectiveExpressionNode, EnumMemberNode, - ModelSpreadPropertyNode, EnumSpreadMemberNode, - DecoratorExpressionNode, + EnumStatementNode, + IdentifierNode, + InterfaceStatementNode, + IntersectionExpressionNode, + JsNamespaceDeclarationNode, MemberExpressionNode, - UnionStatementNode, - UnionExpressionNode, - UnionVariantNode, - AugmentDecoratorStatementNode, + ModelExpressionNode, + ModelPropertyNode, + ModelSpreadPropertyNode, + ModelStatementNode, + NamespaceStatementNode, + Node, + ObjectLiteralNode, + OperationStatementNode, ScalarStatementNode, - TypeReferenceNode, - JsNamespaceDeclarationNode, - DirectiveExpressionNode, StringLiteralNode, - ObjectLiteralNode, - ConstStatementNode, - visitChildren + SyntaxKind, + TypeReferenceNode, + UnionExpressionNode, + UnionStatementNode, + UnionVariantNode, + visitChildren, } from "@typespec/compiler/ast"; export class NamespaceModel { kind = SyntaxKind.NamespaceStatement; @@ -163,7 +160,11 @@ export class NamespaceModel { } } -function findNodes(kind: T, program: Program, namespace: Namespace): (Node & { kind: T })[] { +function findNodes( + kind: T, + program: Program, + namespace: Namespace, +): (Node & { kind: T })[] { const nodes: Node[] = []; for (const file of program.sourceFiles.values()) { visitChildren(file, function visit(node) { @@ -182,7 +183,10 @@ function inNamespace(node: Node, program: Program, namespace: Namespace): boolea case SyntaxKind.NamespaceStatement: return program.checker.getTypeForNode(n) === namespace; case SyntaxKind.TypeSpecScript: - if (n.inScopeNamespaces.length > 0 && inNamespace(n.inScopeNamespaces[0], program, namespace)) { + if ( + n.inScopeNamespaces.length > 0 && + inNamespace(n.inScopeNamespaces[0], program, namespace) + ) { return true; } return false; @@ -215,6 +219,7 @@ export function generateId(obj: BaseNode | NamespaceModel | undefined): string | case SyntaxKind.MemberExpression: return generateId(node.target); } + break; case SyntaxKind.EnumMember: node = obj as EnumMemberNode; name = node.id.sv; diff --git a/packages/typespec-apiview/src/schemas.ts b/packages/typespec-apiview/src/schemas.ts index 8031aecab9..95068c62a9 100644 --- a/packages/typespec-apiview/src/schemas.ts +++ b/packages/typespec-apiview/src/schemas.ts @@ -1,7 +1,20 @@ -// These schemas are all adapted from the TypeSpec definition here: +// These schemas are all adapted from the TypeSpec definition here: // https://github.com/Azure/azure-sdk-tools/blob/main/tools/apiview/parsers/apiview-treestyle-parser-schema/main.tsp -import { AliasStatementNode, EnumStatementNode, InterfaceStatementNode, IntersectionExpressionNode, ModelExpressionNode, ModelStatementNode, ObjectLiteralNode, OperationStatementNode, ScalarStatementNode, SyntaxKind, UnionExpressionNode, UnionStatementNode } from "@typespec/compiler/ast"; +import { + AliasStatementNode, + EnumStatementNode, + InterfaceStatementNode, + IntersectionExpressionNode, + ModelExpressionNode, + ModelStatementNode, + ObjectLiteralNode, + OperationStatementNode, + ScalarStatementNode, + SyntaxKind, + UnionExpressionNode, + UnionStatementNode, +} from "@typespec/compiler/ast"; import { NamespaceModel } from "./namespace-model.js"; import { NamespaceStack } from "./util.js"; @@ -15,9 +28,9 @@ export enum TokenKind { MemberName = 4, StringLiteral = 5, Literal = 6, - Comment = 7 + Comment = 7, } - + /** ReviewFile represents entire API review object. This will be processed to render review lines. */ export interface CodeFile { Name: string; @@ -33,8 +46,8 @@ export interface CodeFile { /** Add any system generated comments. Each comment is linked to review line ID */ Diagnostics: CodeDiagnostic[] | undefined; /** Navigation items are used to create a tree view in the navigation panel. Each navigation item is linked to a review line ID. This is optional. - * If navigation items are not provided then navigation panel will be automatically generated using the review lines. Navigation items should be provided only if you want to customize the navigation panel. - */ + * If navigation items are not provided then navigation panel will be automatically generated using the review lines. Navigation items should be provided only if you want to customize the navigation panel. + */ Navigation: NavigationItem[] | undefined; } @@ -45,14 +58,14 @@ export interface ReviewLineOptions { IsContextEndLine?: boolean; /** Set ID of related line to ensure current line is not visible when a related line is hidden. * One e.g. is a code line for class attribute should set class line's Line ID as related line ID. - */ + */ RelatedToLine?: string; } /** ReviewLine object corresponds to each line displayed on API review. If an empty line is required then add a code line object without any token. */ export interface ReviewLine extends ReviewLineOptions { - /** lineId is only required if we need to support commenting on a line that contains this token. - * Usually code line for documentation or just punctuation is not required to have lineId. lineId should be a unique value within + /** lineId is only required if we need to support commenting on a line that contains this token. + * Usually code line for documentation or just punctuation is not required to have lineId. lineId should be a unique value within * the review token file to use it assign to review comments as well as navigation Id within the review page. * for e.g Azure.Core.HttpHeader.Common, azure.template.template_main */ @@ -60,7 +73,7 @@ export interface ReviewLine extends ReviewLineOptions { CrossLanguageId: string | undefined; /** list of tokens that constructs a line in API review */ Tokens: ReviewToken[]; - /** Add any child lines as children. For e.g. all classes and namespace level methods are added as a children of namespace(module) level code line. + /** Add any child lines as children. For e.g. all classes and namespace level methods are added as a children of namespace(module) level code line. * Similarly all method level code lines are added as children of it's class code line.*/ Children: ReviewLine[]; } @@ -72,7 +85,7 @@ export interface ReviewTokenOptions { * For e.g. a param type which is class name in the same package */ NavigateToId?: string; - /** set skipDiff to true if underlying token needs to be ignored from diff calculation. For e.g. package metadata or dependency versions + /** set skipDiff to true if underlying token needs to be ignored from diff calculation. For e.g. package metadata or dependency versions * are usually excluded when comparing two revisions to avoid reporting them as API changes*/ SkipDiff?: boolean; /** This is set if API is marked as deprecated */ @@ -99,9 +112,9 @@ export enum CodeDiagnosticLevel { Info = 1, Warning = 2, Error = 3, - /** Fatal level diagnostic will block API review approval and it will show an error message to the user. Approver will have to - * override fatal level system comments before approving a review.*/ - Fatal = 4 + /** Fatal level diagnostic will block API review approval and it will show an error message to the user. Approver will have to + * override fatal level system comments before approving a review.*/ + Fatal = 4, } /** System comment object is to add system generated comment. It can be one of the 4 different types of system comments. */ @@ -138,7 +151,7 @@ export class NavigationItem { | UnionStatementNode | UnionExpressionNode | ObjectLiteralNode, - stack: NamespaceStack + stack: NamespaceStack, ) { let obj; switch (objNode.kind) { @@ -160,20 +173,40 @@ export class NavigationItem { } const aliasItems = new Array(); for (const node of objNode.aliases.values()) { - aliasItems.push(new NavigationItem(node, stack)); + aliasItems.push(new NavigationItem(node, stack)); } this.ChildItems = []; if (operationItems.length) { - this.ChildItems.push({ Text: "Operations", ChildItems: operationItems, Tags: { TypeKind: ApiViewNavigationKind.Method }, NavigationId: "" }); + this.ChildItems.push({ + Text: "Operations", + ChildItems: operationItems, + Tags: { TypeKind: ApiViewNavigationKind.Method }, + NavigationId: "", + }); } if (resourceItems.length) { - this.ChildItems.push({ Text: "Resources", ChildItems: resourceItems, Tags: { TypeKind: ApiViewNavigationKind.Class }, NavigationId: "" }); + this.ChildItems.push({ + Text: "Resources", + ChildItems: resourceItems, + Tags: { TypeKind: ApiViewNavigationKind.Class }, + NavigationId: "", + }); } if (modelItems.length) { - this.ChildItems.push({ Text: "Models", ChildItems: modelItems, Tags: { TypeKind: ApiViewNavigationKind.Class }, NavigationId: "" }); + this.ChildItems.push({ + Text: "Models", + ChildItems: modelItems, + Tags: { TypeKind: ApiViewNavigationKind.Class }, + NavigationId: "", + }); } if (aliasItems.length) { - this.ChildItems.push({ Text: "Aliases", ChildItems: aliasItems, Tags: { TypeKind: ApiViewNavigationKind.Class }, NavigationId: "" }); + this.ChildItems.push({ + Text: "Aliases", + ChildItems: aliasItems, + Tags: { TypeKind: ApiViewNavigationKind.Class }, + NavigationId: "", + }); } break; case SyntaxKind.ModelStatement: diff --git a/packages/typespec-apiview/src/testing/index.ts b/packages/typespec-apiview/src/testing/index.ts index 28750309ad..ebb8713904 100644 --- a/packages/typespec-apiview/src/testing/index.ts +++ b/packages/typespec-apiview/src/testing/index.ts @@ -1,4 +1,8 @@ -import { createTestLibrary, findTestPackageRoot, TypeSpecTestLibrary } from "@typespec/compiler/testing"; +import { + createTestLibrary, + findTestPackageRoot, + TypeSpecTestLibrary, +} from "@typespec/compiler/testing"; export const ApiViewTestLibrary: TypeSpecTestLibrary = createTestLibrary({ name: "@azure-tools/typespec-apiview", diff --git a/packages/typespec-apiview/src/util.ts b/packages/typespec-apiview/src/util.ts index cc272e8460..9e5d24463e 100644 --- a/packages/typespec-apiview/src/util.ts +++ b/packages/typespec-apiview/src/util.ts @@ -6,19 +6,19 @@ export function reviewLineText(line: ReviewLine, indent: number): string { for (const token of line.Tokens) { tokenText += reviewTokenText(token, tokenText); } - const childrenText = line.Children.map(c => reviewLineText(c, indent + 2)).join("\n"); + const childrenText = line.Children.map((c) => reviewLineText(c, indent + 2)).join("\n"); if (childrenText !== "") { return `${indentString}${tokenText}\n${childrenText}`; } else { return `${indentString}${tokenText}`; } } - + function reviewTokenText(token: ReviewToken, preview: string): string { const previewEndsInSpace = preview.endsWith(" "); const hasSuffixSpace = token.HasSuffixSpace !== undefined ? token.HasSuffixSpace : true; const suffixSpace = hasSuffixSpace ? " " : ""; - const prefixSpace = (token.HasPrefixSpace && !previewEndsInSpace) ? " " : ""; + const prefixSpace = token.HasPrefixSpace && !previewEndsInSpace ? " " : ""; const value = token.Value; return `${prefixSpace}${value}${suffixSpace}`; } diff --git a/packages/typespec-apiview/test/apiview-options.test.ts b/packages/typespec-apiview/test/apiview-options.test.ts index 3cc89b314e..b0477dcaaf 100644 --- a/packages/typespec-apiview/test/apiview-options.test.ts +++ b/packages/typespec-apiview/test/apiview-options.test.ts @@ -1,9 +1,7 @@ -import { expectDiagnostics } from "@typespec/compiler/testing"; -import { apiViewFor, apiViewText, compare, diagnosticsFor } from "./test-host.js"; import { describe, it } from "vitest"; +import { apiViewFor, apiViewText, compare } from "./test-host.js"; describe("apiview-options: tests", () => { - it("omits namespaces that aren't proper subnamespaces", async () => { const input = ` @TypeSpec.service( #{ title: "Test"} ) @@ -27,7 +25,7 @@ describe("apiview-options: tests", () => { namespace Azure.Test.Sub { model SubFoo {} } - ` + `; const apiview = await apiViewFor(input, {}); const actual = apiViewText(apiview); compare(expect, actual, 6); @@ -53,9 +51,9 @@ describe("apiview-options: tests", () => { namespace Azure.Test { model Foo {} } - ` + `; const apiview = await apiViewFor(input, { - "include-global-namespace": true + "include-global-namespace": true, }); // TODO: Update once bug is fixed: https://github.com/microsoft/typespec/issues/3165 const actual = apiViewText(apiview); diff --git a/packages/typespec-apiview/test/apiview.test.ts b/packages/typespec-apiview/test/apiview.test.ts index bb6df8db5f..483978bd9e 100644 --- a/packages/typespec-apiview/test/apiview.test.ts +++ b/packages/typespec-apiview/test/apiview.test.ts @@ -1,8 +1,8 @@ -import { apiViewFor, apiViewText, compare } from "./test-host.js"; -import { CodeFile, ReviewLine } from "../src/schemas.js"; -import { describe, it } from "vitest"; import { fail } from "assert"; import { isDeepStrictEqual } from "util"; +import { describe, it } from "vitest"; +import { CodeFile, ReviewLine } from "../src/schemas.js"; +import { apiViewFor, apiViewText, compare } from "./test-host.js"; interface ReviewLineData { relatedToCount: number; @@ -10,7 +10,6 @@ interface ReviewLineData { } describe("apiview: tests", () => { - function validateReviewLineIds(definitionIds: Set, line: ReviewLine) { // ensure that there are no repeated definition IDs. if (line.LineId !== undefined) { @@ -36,14 +35,15 @@ describe("apiview: tests", () => { /** Validates that related lines point to a valid line. */ function getRelatedLineMetadata(apiview: CodeFile): Map { - - function getReviewLinesMetadata(lines: ReviewLine[] | undefined): Map | undefined { + function getReviewLinesMetadata( + lines: ReviewLine[] | undefined, + ): Map | undefined { if (lines === undefined || lines.length === 0) return undefined; const mainMap = new Map(); let lastKey: string | undefined = undefined; for (const line of lines) { const related = line.RelatedToLine; - const lineId = line.LineId + const lineId = line.LineId; const isEndContext = line.IsContextEndLine; if (related) { lastKey = related; @@ -79,19 +79,18 @@ describe("apiview: tests", () => { const countMap = getReviewLinesMetadata(apiview.ReviewLines); return countMap ?? new Map(); } - + function compareCounts(lhs: Map, rhs: Map) { // ensure the keys are the same const lhsKeys = new Set([...lhs.keys()]); const rhsKeys = new Set([...rhs.keys()]); const combined = new Set([...lhsKeys, ...rhsKeys]); - if (combined.size != lhsKeys.size) { + if (combined.size !== lhsKeys.size) { fail(`Keys mismatch: ${JSON.stringify([...lhsKeys])} vs ${JSON.stringify([...rhsKeys])}`); } isDeepStrictEqual(lhs, rhs); } - describe("models", () => { it("composition", async () => { const input = ` @@ -146,13 +145,16 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Pet", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Pet", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("templated", async () => { @@ -230,15 +232,18 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.ConstrainedComplex", { relatedToCount: 1, isContextEndCount: 1 }], - ["Azure.Test.ConstrainedSimple", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.ConstrainedWithDefault", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Page", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.StringPage", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Thing", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.ConstrainedComplex", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ConstrainedSimple", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.ConstrainedWithDefault", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Page", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.StringPage", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Thing", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("with default values", async () => { @@ -269,11 +274,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], - ])); - + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); }); @@ -295,9 +302,7 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ])); + compareCounts(counts, new Map([["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }]])); }); it("new scalar type", async () => { @@ -317,9 +322,7 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ])); + compareCounts(counts, new Map([["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }]])); }); it("templated", async () => { @@ -341,10 +344,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Unreal", { relatedToCount: 1, isContextEndCount: 0 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Unreal", { relatedToCount: 1, isContextEndCount: 0 }], + ]), + ); }); }); @@ -374,10 +380,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("templated alias", async () => { @@ -405,10 +414,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); }); @@ -438,10 +450,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Animal", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); }); @@ -467,10 +482,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.SomeEnum", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("string-backed values", async () => { @@ -494,10 +512,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.SomeStringEnum", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeStringEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("int-backed values", async () => { @@ -521,10 +542,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.SomeIntEnum", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeIntEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("spread labels", async () => { @@ -551,11 +575,14 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.SomeEnum", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.SomeSpreadEnum", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.SomeSpreadEnum", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); }); @@ -610,13 +637,16 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.MyUnion", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Snake", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.MyUnion", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Snake", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("unnamed union", async () => { @@ -665,13 +695,16 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Animals", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Snake", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Cat", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Dog", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Animals", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Snake", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); }); @@ -704,11 +737,14 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 0 }], - ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 0 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("templated with deeply nested models", async () => { @@ -760,11 +796,14 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Temp", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Temp", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("templated with model types", async () => { @@ -864,13 +903,16 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], - ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.FooParams", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.FooParams", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("templated with mixed types", async () => { @@ -950,13 +992,16 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], - ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.FooParams", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.FooParams", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("templated with empty models", async () => { @@ -997,12 +1042,15 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], - ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }] - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.GetFoo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.NamedGetFoo", { relatedToCount: 1, isContextEndCount: 1 }], + ["Azure.Test.ResourceRead", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); it("with anonymous models", async () => { @@ -1036,10 +1084,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.SomeOp", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SomeOp", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); }); @@ -1080,12 +1131,15 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], - ["Azure.Test.Foo.get", { relatedToCount: 2, isContextEndCount: 1 }], - ["Azure.Test.Foo.list", { relatedToCount: 2, isContextEndCount: 0 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 0, isContextEndCount: 1 }], + ["Azure.Test.Foo.get", { relatedToCount: 2, isContextEndCount: 1 }], + ["Azure.Test.Foo.list", { relatedToCount: 2, isContextEndCount: 0 }], + ]), + ); }); }); @@ -1117,10 +1171,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Bar", { relatedToCount: 5, isContextEndCount: 0 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Bar", { relatedToCount: 5, isContextEndCount: 0 }], + ]), + ); }); it("short strings", async () => { @@ -1142,10 +1199,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Foo", { relatedToCount: 1, isContextEndCount: 0 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 1, isContextEndCount: 0 }], + ]), + ); }); }); @@ -1193,10 +1253,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Person", { relatedToCount: 0, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Person", { relatedToCount: 0, isContextEndCount: 1 }], + ]), + ); }); }); @@ -1226,10 +1289,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.Foo", { relatedToCount: 2, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.Foo", { relatedToCount: 2, isContextEndCount: 1 }], + ]), + ); }); it("suppression on namespace", async () => { @@ -1262,10 +1328,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.SubNamespace", { relatedToCount: 2, isContextEndCount: 1 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.SubNamespace", { relatedToCount: 2, isContextEndCount: 1 }], + ]), + ); }); it("suppression on operation", async () => { @@ -1287,10 +1356,13 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ["Azure.Test.someOp", { relatedToCount: 1, isContextEndCount: 0 }], - ])); + compareCounts( + counts, + new Map([ + ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], + ["Azure.Test.someOp", { relatedToCount: 1, isContextEndCount: 0 }], + ]), + ); }); }); @@ -1318,9 +1390,7 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }], - ])); + compareCounts(counts, new Map([["Azure.Test", { relatedToCount: 3, isContextEndCount: 1 }]])); }); }); @@ -1359,8 +1429,6 @@ describe("apiview: tests", () => { compare(expect, actual, 6); validateLineIds(apiview); const counts = getRelatedLineMetadata(apiview); - compareCounts(counts, new Map([ - ["Azure.Test", { relatedToCount: 1, isContextEndCount: 1 }], - ])); + compareCounts(counts, new Map([["Azure.Test", { relatedToCount: 1, isContextEndCount: 1 }]])); }); }); diff --git a/packages/typespec-apiview/test/test-host.ts b/packages/typespec-apiview/test/test-host.ts index efcb068778..74039ffd7b 100644 --- a/packages/typespec-apiview/test/test-host.ts +++ b/packages/typespec-apiview/test/test-host.ts @@ -1,19 +1,25 @@ +import "@azure-tools/typespec-apiview"; +import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; +import { Diagnostic, resolvePath } from "@typespec/compiler"; import { createTestHost, createTestWrapper } from "@typespec/compiler/testing"; -import { RestTestLibrary } from "@typespec/rest/testing"; import { HttpTestLibrary } from "@typespec/http/testing"; +import { RestTestLibrary } from "@typespec/rest/testing"; import { VersioningTestLibrary } from "@typespec/versioning/testing"; -import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; -import { ApiViewTestLibrary } from "../src/testing/index.js"; -import "@azure-tools/typespec-apiview"; -import { ApiViewEmitterOptions } from "../src/lib.js"; -import { Diagnostic, resolvePath } from "@typespec/compiler"; import { strictEqual } from "assert"; +import { ApiViewEmitterOptions } from "../src/lib.js"; import { CodeFile } from "../src/schemas.js"; +import { ApiViewTestLibrary } from "../src/testing/index.js"; import { reviewLineText } from "../src/util.js"; export async function createApiViewTestHost() { return createTestHost({ - libraries: [ApiViewTestLibrary, RestTestLibrary, HttpTestLibrary, VersioningTestLibrary, AzureCoreTestLibrary], + libraries: [ + ApiViewTestLibrary, + RestTestLibrary, + HttpTestLibrary, + VersioningTestLibrary, + AzureCoreTestLibrary, + ], }); } @@ -21,10 +27,7 @@ export async function createApiViewTestRunner({ withVersioning, }: { withVersioning?: boolean } = {}) { const host = await createApiViewTestHost(); - const autoUsings = [ - "TypeSpec.Rest", - "TypeSpec.Http", - ] + const autoUsings = ["TypeSpec.Rest", "TypeSpec.Http"]; if (withVersioning) { autoUsings.push("TypeSpec.Versioning"); } @@ -32,12 +35,15 @@ export async function createApiViewTestRunner({ autoUsings: autoUsings, compilerOptions: { emit: ["@azure-tools/typespec-apiview"], - } + }, }); } -export async function diagnosticsFor(code: string, options: ApiViewEmitterOptions): Promise { - const runner = await createApiViewTestRunner({withVersioning: true}); +export async function diagnosticsFor( + code: string, + options: ApiViewEmitterOptions, +): Promise { + const runner = await createApiViewTestRunner({ withVersioning: true }); const outPath = resolvePath("/apiview.json"); const diagnostics = await runner.diagnose(code, { noEmit: false, @@ -45,8 +51,8 @@ export async function diagnosticsFor(code: string, options: ApiViewEmitterOption options: { "@azure-tools/typespec-apiview": { ...options, - "output-file": outPath, - } + "output-file": outPath, + }, }, miscOptions: { "disable-linter": true }, }); @@ -54,7 +60,7 @@ export async function diagnosticsFor(code: string, options: ApiViewEmitterOption } export async function apiViewFor(code: string, options: ApiViewEmitterOptions): Promise { - const runner = await createApiViewTestRunner({withVersioning: true}); + const runner = await createApiViewTestRunner({ withVersioning: true }); const outPath = resolvePath("/apiview.json"); await runner.compile(code, { noEmit: false, @@ -62,8 +68,8 @@ export async function apiViewFor(code: string, options: ApiViewEmitterOptions): options: { "@azure-tools/typespec-apiview": { ...options, - "output-file": outPath, - } + "output-file": outPath, + }, }, miscOptions: { "disable-linter": true }, }); @@ -74,7 +80,9 @@ export async function apiViewFor(code: string, options: ApiViewEmitterOptions): } export function apiViewText(apiview: CodeFile): string[] { - return apiview.ReviewLines.map(l => reviewLineText(l, 0)).join("\n").split("\n"); + return apiview.ReviewLines.map((l) => reviewLineText(l, 0)) + .join("\n") + .split("\n"); } function getBaseIndent(lines: string[]): number { @@ -90,7 +98,7 @@ function getBaseIndent(lines: string[]): number { function trimLines(lines: string[]): string[] { const trimmed: string[] = []; const indent = getBaseIndent(lines); - + // if first line is blank, skip it if (lines[0].trim() === "") { lines = lines.slice(1); @@ -109,7 +117,7 @@ function trimLines(lines: string[]): string[] { // if last line is blank, skip it const lastLine = trimmed.pop(); if (lastLine && lastLine.trim() !== "") { - trimmed.push(lastLine) + trimmed.push(lastLine); } return trimmed; } @@ -120,6 +128,10 @@ export function compare(expect: string, lines: string[], offset: number) { const expectedLines = trimLines(expect.split("\n")); const actualLines = trimLines(lines.slice(offset)); for (let x = 0; x < actualLines.length; x++) { - strictEqual(actualLines[x], expectedLines[x], `Actual differed from expected at line #${x + 1}\nACTUAL: '${actualLines[x]}'\nEXPECTED: '${expectedLines[x]}'`); + strictEqual( + actualLines[x], + expectedLines[x], + `Actual differed from expected at line #${x + 1}\nACTUAL: '${actualLines[x]}'\nEXPECTED: '${expectedLines[x]}'`, + ); } } diff --git a/packages/typespec-apiview/tsconfig.json b/packages/typespec-apiview/tsconfig.json index 4a2b620ec9..8d8cb28bf5 100644 --- a/packages/typespec-apiview/tsconfig.json +++ b/packages/typespec-apiview/tsconfig.json @@ -1,29 +1,9 @@ { + "extends": "../../core/tsconfig.base.json", "compilerOptions": { "outDir": "./dist", "rootDir": ".", - "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo", - "types": ["node", "vitest"], - "composite": true, - "alwaysStrict": true, - "forceConsistentCasingInFileNames": true, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "esModuleInterop": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "sourceMap": true, - "declarationMap": true, - "strict": true, - "declaration": true, - "stripInternal": true, - "noEmitHelpers": false, - "target": "es2019", - "lib": ["es2019"], - "experimentalDecorators": true, - "newLine": "LF", - "skipLibCheck": true + "tsBuildInfoFile": "./temp/tsconfig.tsbuildinfo" }, "include": ["src/**/*.ts", "test/**/*.ts"] } diff --git a/packages/typespec-apiview/vitest.config.ts b/packages/typespec-apiview/vitest.config.ts index d78b8d75d0..dec912f113 100644 --- a/packages/typespec-apiview/vitest.config.ts +++ b/packages/typespec-apiview/vitest.config.ts @@ -1,26 +1,4 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig, mergeConfig } from "vitest/config"; +import { defaultTypeSpecVitestConfig } from "../../core/vitest.config"; -export default defineConfig({ - test: { - environment: 'node', - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html'], - include: ['src/**/*.ts'], - }, - outputFile: { - junit: './test-results.xml', - }, - reporters: 'default', - exclude: ['node_modules', 'dist/test', 'dist'], - silent: false, - }, - esbuild: { - sourcemap: true, - }, - server: { - watch: { - ignored: [], - }, - } -}); +export default mergeConfig(defaultTypeSpecVitestConfig, defineConfig({})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6051c634a9..9ef140e4dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,10 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - cross-spawn@>=7.0.0 <7.0.5: ^7.0.5 - rollup: 4.49.0 - importers: .: @@ -33,10 +29,10 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/eslint-plugin': specifier: ^1.5.2 - version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15) + version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) c8: specifier: ^10.1.3 version: 10.1.3 @@ -120,10 +116,10 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/eslint-plugin': specifier: ^1.5.2 - version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15) + version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) c8: specifier: ^10.1.3 version: 10.1.3 @@ -198,7 +194,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -259,7 +255,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -308,7 +304,7 @@ importers: version: 7.7.1 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -354,7 +350,7 @@ importers: version: 17.0.35 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -448,7 +444,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -561,7 +557,7 @@ importers: version: 8.49.0 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -597,7 +593,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -667,7 +663,7 @@ importers: version: 5.1.2(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -715,7 +711,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -964,7 +960,7 @@ importers: version: link:../versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1037,7 +1033,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1180,7 +1176,7 @@ importers: version: 17.0.35 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1226,7 +1222,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1259,7 +1255,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1287,7 +1283,7 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1344,7 +1340,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1423,7 +1419,7 @@ importers: version: link:../xml '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1457,7 +1453,7 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1696,7 +1692,7 @@ importers: version: 5.1.2(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1763,7 +1759,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1824,7 +1820,7 @@ importers: version: 5.1.2(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1869,7 +1865,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1942,7 +1938,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2000,7 +1996,7 @@ importers: version: 0.4.14 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2210,7 +2206,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2250,7 +2246,7 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2298,7 +2294,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2399,7 +2395,7 @@ importers: version: link:../prettier-plugin-typespec '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2456,7 +2452,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2531,7 +2527,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2564,7 +2560,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2750,7 +2746,7 @@ importers: version: link:../../core/packages/samples '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2770,6 +2766,51 @@ importers: specifier: ^4.0.15 version: 4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2) + packages/typespec-apiview: + devDependencies: + '@azure-tools/typespec-azure-core': + specifier: workspace:^ + version: link:../typespec-azure-core + '@types/node': + specifier: ~25.0.2 + version: 25.0.2 + '@typespec/compiler': + specifier: workspace:^ + version: link:../../core/packages/compiler + '@typespec/http': + specifier: workspace:^ + version: link:../../core/packages/http + '@typespec/library-linter': + specifier: workspace:^ + version: link:../../core/packages/library-linter + '@typespec/rest': + specifier: workspace:^ + version: link:../../core/packages/rest + '@typespec/tspd': + specifier: workspace:^ + version: link:../../core/packages/tspd + '@typespec/versioning': + specifier: workspace:^ + version: link:../../core/packages/versioning + '@vitest/coverage-v8': + specifier: ^4.0.15 + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/ui': + specifier: ^4.0.15 + version: 4.0.15(vitest@4.0.15) + c8: + specifier: ^10.1.3 + version: 10.1.3 + rimraf: + specifier: ~6.1.2 + version: 6.1.2 + typescript: + specifier: ~5.9.2 + version: 5.9.3 + vitest: + specifier: ^4.0.15 + version: 4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2) + packages/typespec-autorest: dependencies: '@typespec/xml': @@ -2814,7 +2855,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2871,7 +2912,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2916,7 +2957,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3022,7 +3063,7 @@ importers: version: link:../../core/packages/playground '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3079,7 +3120,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3137,7 +3178,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3176,7 +3217,7 @@ importers: version: link:../../core/packages/tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3276,7 +3317,7 @@ importers: version: link:../../core/packages/xml '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15) + version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -5157,105 +5198,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -6297,28 +6322,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@reflink/reflink-linux-arm64-musl@0.1.19': resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@reflink/reflink-linux-x64-gnu@0.1.19': resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@reflink/reflink-linux-x64-musl@0.1.19': resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@reflink/reflink-win32-arm64-msvc@0.1.19': resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==} @@ -6352,7 +6373,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0 '@types/babel__core': ^7.1.9 - rollup: 4.49.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: '@types/babel__core': optional: true @@ -6363,7 +6384,7 @@ packages: resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.49.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true @@ -6372,7 +6393,7 @@ packages: resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: 4.49.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true @@ -6411,73 +6432,61 @@ packages: resolution: {integrity: sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.49.0': resolution: {integrity: sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.49.0': resolution: {integrity: sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.49.0': resolution: {integrity: sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.49.0': resolution: {integrity: sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.49.0': resolution: {integrity: sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.49.0': resolution: {integrity: sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.49.0': resolution: {integrity: sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.49.0': resolution: {integrity: sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.49.0': resolution: {integrity: sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.3': resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.49.0': resolution: {integrity: sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.49.0': resolution: {integrity: sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==} @@ -6707,7 +6716,7 @@ packages: resolution: {integrity: sha512-n4hcgGE9tm0y7OaV/DzbyFWuvyh0BkHRcNiCuN85kKJPcKqxhuBz4SYWSSTeO0kIw2eXQTM8oB/TJ0O+vLjvwQ==} peerDependencies: esbuild: '*' - rollup: 4.49.0 + rollup: '*' storybook: ^10.1.8 vite: '*' webpack: '*' @@ -8527,6 +8536,10 @@ packages: engines: {node: '>=20'} hasBin: true + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -12132,7 +12145,7 @@ packages: hasBin: true peerDependencies: rolldown: 1.x || ^1.0.0-beta - rollup: 4.49.0 + rollup: 2.x || 3.x || 4.x peerDependenciesMeta: rolldown: optional: true @@ -19217,7 +19230,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.15(vitest@4.0.15)': + '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.15 @@ -19234,7 +19247,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15)': + '@vitest/eslint-plugin@1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/utils': 8.49.0(eslint@9.39.2)(typescript@5.9.3) @@ -19614,7 +19627,7 @@ snapshots: '@yarnpkg/libui@3.0.2(ink@3.2.0(@types/react@19.2.7)(react@17.0.2))(react@17.0.2)': dependencies: - ink: 3.2.0(@types/react@19.2.7)(react@18.3.1) + ink: 3.2.0(@types/react@19.2.7)(react@17.0.2) react: 17.0.2 tslib: 2.8.1 @@ -19745,7 +19758,7 @@ snapshots: algoliasearch: 4.25.3 clipanion: 4.0.0-rc.4(typanion@3.14.0) diff: 5.2.0 - ink: 3.2.0(@types/react@19.2.7)(react@18.3.1) + ink: 3.2.0(@types/react@19.2.7)(react@17.0.2) ink-text-input: 4.0.3(ink@3.2.0(@types/react@19.2.7)(react@17.0.2))(react@17.0.2) react: 17.0.2 semver: 7.7.3 @@ -19929,7 +19942,7 @@ snapshots: '@yarnpkg/parsers': 3.0.3 chalk: 3.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.6 + cross-spawn: 7.0.3 fast-glob: 3.3.3 micromatch: 4.0.8 tslib: 2.8.1 @@ -21101,6 +21114,12 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -22953,7 +22972,7 @@ snapshots: ink-text-input@4.0.3(ink@3.2.0(@types/react@19.2.7)(react@17.0.2))(react@17.0.2): dependencies: chalk: 4.1.2 - ink: 3.2.0(@types/react@19.2.7)(react@18.3.1) + ink: 3.2.0(@types/react@19.2.7)(react@17.0.2) react: 17.0.2 type-fest: 0.15.1 @@ -22989,38 +23008,6 @@ snapshots: - bufferutil - utf-8-validate - ink@3.2.0(@types/react@19.2.7)(react@18.3.1): - dependencies: - ansi-escapes: 4.3.2 - auto-bind: 4.0.0 - chalk: 4.1.2 - cli-boxes: 2.2.1 - cli-cursor: 3.1.0 - cli-truncate: 2.1.0 - code-excerpt: 3.0.0 - indent-string: 4.0.0 - is-ci: 2.0.0 - lodash: 4.17.21 - patch-console: 1.0.0 - react: 18.3.1 - react-devtools-core: 4.28.5 - react-reconciler: 0.26.2(react@18.3.1) - scheduler: 0.20.2 - signal-exit: 3.0.7 - slice-ansi: 3.0.0 - stack-utils: 2.0.6 - string-width: 4.2.3 - type-fest: 0.12.0 - widest-line: 3.1.0 - wrap-ansi: 6.2.0 - ws: 7.5.10 - yoga-layout-prebuilt: 1.10.0 - optionalDependencies: - '@types/react': 19.2.7 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - inline-style-parser@0.2.7: {} inquirer@13.1.0(@types/node@25.0.2): @@ -25385,13 +25372,6 @@ snapshots: react: 17.0.2 scheduler: 0.20.2 - react-reconciler@0.26.2(react@18.3.1): - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react: 18.3.1 - scheduler: 0.20.2 - react-refresh@0.17.0: {} react-refresh@0.18.0: {} diff --git a/tsconfig.ws.json b/tsconfig.ws.json index d34c15b6c8..9e26119de3 100644 --- a/tsconfig.ws.json +++ b/tsconfig.ws.json @@ -1,6 +1,7 @@ { "references": [ { "path": "core/tsconfig.ws.json" }, + { "path": "packages/typespec-apiview/tsconfig.json" }, { "path": "packages/typespec-autorest/tsconfig.json" }, { "path": "packages/typespec-autorest-canonical/tsconfig.json" }, { "path": "packages/typespec-azure-core/tsconfig.json" }, From a5cf6ffafb0fcd7f3faaf903397b4f1dc6b1a9ac Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Thu, 8 Jan 2026 15:08:09 -0800 Subject: [PATCH 3/4] Add changelog. --- .chronus/changes/typespecApiview-2026-0-8-15-7-55.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/typespecApiview-2026-0-8-15-7-55.md diff --git a/.chronus/changes/typespecApiview-2026-0-8-15-7-55.md b/.chronus/changes/typespecApiview-2026-0-8-15-7-55.md new file mode 100644 index 0000000000..d1372f9f3c --- /dev/null +++ b/.chronus/changes/typespecApiview-2026-0-8-15-7-55.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-apiview" +--- + +Migrate typespec-apiview from azure-sdk-tools repo. \ No newline at end of file From 30e9a50a434993de265d07dce198145ee6f51ff8 Mon Sep 17 00:00:00 2001 From: Travis Prescott Date: Fri, 9 Jan 2026 10:18:45 -0800 Subject: [PATCH 4/4] Update pnpm lock file. --- pnpm-lock.yaml | 197 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 131 insertions(+), 66 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ef140e4dc..01df0636c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,10 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + cross-spawn@>=7.0.0 <7.0.5: ^7.0.5 + rollup: 4.49.0 + importers: .: @@ -29,10 +33,10 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/eslint-plugin': specifier: ^1.5.2 - version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15) c8: specifier: ^10.1.3 version: 10.1.3 @@ -116,10 +120,10 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/eslint-plugin': specifier: ^1.5.2 - version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15) c8: specifier: ^10.1.3 version: 10.1.3 @@ -194,7 +198,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -255,7 +259,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -304,7 +308,7 @@ importers: version: 7.7.1 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -350,7 +354,7 @@ importers: version: 17.0.35 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -444,7 +448,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -557,7 +561,7 @@ importers: version: 8.49.0 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -593,7 +597,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -663,7 +667,7 @@ importers: version: 5.1.2(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -711,7 +715,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -960,7 +964,7 @@ importers: version: link:../versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1033,7 +1037,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1176,7 +1180,7 @@ importers: version: 17.0.35 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1222,7 +1226,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1255,7 +1259,7 @@ importers: version: link:../compiler '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1283,7 +1287,7 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1340,7 +1344,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1419,7 +1423,7 @@ importers: version: link:../xml '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1453,7 +1457,7 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1692,7 +1696,7 @@ importers: version: 5.1.2(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1759,7 +1763,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1820,7 +1824,7 @@ importers: version: 5.1.2(vite@7.2.7(@types/node@25.0.2)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1865,7 +1869,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1938,7 +1942,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -1996,7 +2000,7 @@ importers: version: 0.4.14 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2206,7 +2210,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2246,7 +2250,7 @@ importers: version: 25.0.2 '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2294,7 +2298,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2395,7 +2399,7 @@ importers: version: link:../prettier-plugin-typespec '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2452,7 +2456,7 @@ importers: version: link:../internal-build-utils '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2527,7 +2531,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2560,7 +2564,7 @@ importers: version: link:../tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2746,7 +2750,7 @@ importers: version: link:../../core/packages/samples '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2794,7 +2798,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2855,7 +2859,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2912,7 +2916,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -2957,7 +2961,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3063,7 +3067,7 @@ importers: version: link:../../core/packages/playground '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3120,7 +3124,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3178,7 +3182,7 @@ importers: version: link:../../core/packages/versioning '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3217,7 +3221,7 @@ importers: version: link:../../core/packages/tspd '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -3317,7 +3321,7 @@ importers: version: link:../../core/packages/xml '@vitest/coverage-v8': specifier: ^4.0.15 - version: 4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.15(vitest@4.0.15) '@vitest/ui': specifier: ^4.0.15 version: 4.0.15(vitest@4.0.15) @@ -5198,89 +5202,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -6322,24 +6342,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@reflink/reflink-linux-arm64-musl@0.1.19': resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@reflink/reflink-linux-x64-gnu@0.1.19': resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@reflink/reflink-linux-x64-musl@0.1.19': resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@reflink/reflink-win32-arm64-msvc@0.1.19': resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==} @@ -6373,7 +6397,7 @@ packages: peerDependencies: '@babel/core': ^7.0.0 '@types/babel__core': ^7.1.9 - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: 4.49.0 peerDependenciesMeta: '@types/babel__core': optional: true @@ -6384,7 +6408,7 @@ packages: resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: 4.49.0 peerDependenciesMeta: rollup: optional: true @@ -6393,7 +6417,7 @@ packages: resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: 4.49.0 peerDependenciesMeta: rollup: optional: true @@ -6432,61 +6456,73 @@ packages: resolution: {integrity: sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.49.0': resolution: {integrity: sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.49.0': resolution: {integrity: sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.49.0': resolution: {integrity: sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.49.0': resolution: {integrity: sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.49.0': resolution: {integrity: sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.49.0': resolution: {integrity: sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.49.0': resolution: {integrity: sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.49.0': resolution: {integrity: sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.49.0': resolution: {integrity: sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.3': resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.49.0': resolution: {integrity: sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.49.0': resolution: {integrity: sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==} @@ -6716,7 +6752,7 @@ packages: resolution: {integrity: sha512-n4hcgGE9tm0y7OaV/DzbyFWuvyh0BkHRcNiCuN85kKJPcKqxhuBz4SYWSSTeO0kIw2eXQTM8oB/TJ0O+vLjvwQ==} peerDependencies: esbuild: '*' - rollup: '*' + rollup: 4.49.0 storybook: ^10.1.8 vite: '*' webpack: '*' @@ -8536,10 +8572,6 @@ packages: engines: {node: '>=20'} hasBin: true - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -12145,7 +12177,7 @@ packages: hasBin: true peerDependencies: rolldown: 1.x || ^1.0.0-beta - rollup: 2.x || 3.x || 4.x + rollup: 4.49.0 peerDependenciesMeta: rolldown: optional: true @@ -19230,7 +19262,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@4.0.15(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/coverage-v8@4.0.15(vitest@4.0.15)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.15 @@ -19247,7 +19279,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15(@types/node@25.0.2)(@vitest/ui@4.0.15)(happy-dom@20.0.11)(jsdom@25.0.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.5.2(eslint@9.39.2)(typescript@5.9.3)(vitest@4.0.15)': dependencies: '@typescript-eslint/scope-manager': 8.49.0 '@typescript-eslint/utils': 8.49.0(eslint@9.39.2)(typescript@5.9.3) @@ -19627,7 +19659,7 @@ snapshots: '@yarnpkg/libui@3.0.2(ink@3.2.0(@types/react@19.2.7)(react@17.0.2))(react@17.0.2)': dependencies: - ink: 3.2.0(@types/react@19.2.7)(react@17.0.2) + ink: 3.2.0(@types/react@19.2.7)(react@18.3.1) react: 17.0.2 tslib: 2.8.1 @@ -19758,7 +19790,7 @@ snapshots: algoliasearch: 4.25.3 clipanion: 4.0.0-rc.4(typanion@3.14.0) diff: 5.2.0 - ink: 3.2.0(@types/react@19.2.7)(react@17.0.2) + ink: 3.2.0(@types/react@19.2.7)(react@18.3.1) ink-text-input: 4.0.3(ink@3.2.0(@types/react@19.2.7)(react@17.0.2))(react@17.0.2) react: 17.0.2 semver: 7.7.3 @@ -19942,7 +19974,7 @@ snapshots: '@yarnpkg/parsers': 3.0.3 chalk: 3.0.0 clipanion: 4.0.0-rc.4(typanion@3.14.0) - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 fast-glob: 3.3.3 micromatch: 4.0.8 tslib: 2.8.1 @@ -21114,12 +21146,6 @@ snapshots: '@epic-web/invariant': 1.0.0 cross-spawn: 7.0.6 - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -22972,7 +22998,7 @@ snapshots: ink-text-input@4.0.3(ink@3.2.0(@types/react@19.2.7)(react@17.0.2))(react@17.0.2): dependencies: chalk: 4.1.2 - ink: 3.2.0(@types/react@19.2.7)(react@17.0.2) + ink: 3.2.0(@types/react@19.2.7)(react@18.3.1) react: 17.0.2 type-fest: 0.15.1 @@ -23008,6 +23034,38 @@ snapshots: - bufferutil - utf-8-validate + ink@3.2.0(@types/react@19.2.7)(react@18.3.1): + dependencies: + ansi-escapes: 4.3.2 + auto-bind: 4.0.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + cli-cursor: 3.1.0 + cli-truncate: 2.1.0 + code-excerpt: 3.0.0 + indent-string: 4.0.0 + is-ci: 2.0.0 + lodash: 4.17.21 + patch-console: 1.0.0 + react: 18.3.1 + react-devtools-core: 4.28.5 + react-reconciler: 0.26.2(react@18.3.1) + scheduler: 0.20.2 + signal-exit: 3.0.7 + slice-ansi: 3.0.0 + stack-utils: 2.0.6 + string-width: 4.2.3 + type-fest: 0.12.0 + widest-line: 3.1.0 + wrap-ansi: 6.2.0 + ws: 7.5.10 + yoga-layout-prebuilt: 1.10.0 + optionalDependencies: + '@types/react': 19.2.7 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + inline-style-parser@0.2.7: {} inquirer@13.1.0(@types/node@25.0.2): @@ -25372,6 +25430,13 @@ snapshots: react: 17.0.2 scheduler: 0.20.2 + react-reconciler@0.26.2(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react: 18.3.1 + scheduler: 0.20.2 + react-refresh@0.17.0: {} react-refresh@0.18.0: {}