Repository: microsoft/typespec
Pull request #6914: [hsjs] Support large numerics using decimal.js.
Author: login: witemple-msft
Created: 2025-04-08T22:53:58Z | Merged: 2025-04-09T19:07:49Z

This PR does a few things:

- Represents large numerics decimal128, decimal, float, and numeric as Decimal from decimal.js.
- Alters scaffold mocking to be sensitive to these new representations.
- Allows encoding "seconds" for `TypeSpec.duration` with target `TypeSpec.float`.
- Compute dependencies in scaffolding dynamically from HSJS's own dependencies rather than requiring the scaffolding tool to respecify dependency constraints for all 3p dependencies.

Unblocks spector test `encoding/duration` from compiling.


Diff:
diff --git a/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-8-18-49-0.md b/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-8-18-49-0.md
new file mode 100644
index 00000000000..1cd1d0c7d2c
--- /dev/null
+++ b/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-8-18-49-0.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+  - "@typespec/http-server-js"
+---
+
+Add support for TypeSpec.decimal, TypeSpec.decimal128, TypeSpec.float, and TypeSpec.numeric, all represented as `Decimal` from the 'decimal.js' package.
\ No newline at end of file
diff --git a/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-9-10-50-48.md b/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-9-10-50-48.md
new file mode 100644
index 00000000000..0a6b5c461b0
--- /dev/null
+++ b/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-9-10-50-48.md
@@ -0,0 +1,7 @@
+---
+changeKind: internal
+packages:
+  - "@typespec/spector"
+---
+
+Aligned `@types/morgan` devDependency versions.
diff --git a/packages/http-server-js/.testignore b/packages/http-server-js/.testignore
index 6d4a00c0728..5c73ba114d1 100644
--- a/packages/http-server-js/.testignore
+++ b/packages/http-server-js/.testignore
@@ -1,5 +1,4 @@
 encode/datetime
-encode/duration
 payload/multipart
 payload/xml
 response/status-code-range
diff --git a/packages/http-server-js/build-helpers.ts b/packages/http-server-js/build-helpers.ts
index 2f8c618e519..9877c5736fb 100644
--- a/packages/http-server-js/build-helpers.ts
+++ b/packages/http-server-js/build-helpers.ts
@@ -6,10 +6,10 @@
 import fs from "node:fs/promises";
 import path from "node:path";
 
-const HELPER_DECLARATION_PATH = path.resolve("generated-defs", "helpers");
-const HELPER_SRC_PATH = path.resolve("src", "helpers");
+const GENERATED_DEFS = path.resolve("generated-defs");
 
-console.log("Building JS server generator helpers.");
+const HELPER_DECLARATION_PATH = path.resolve(GENERATED_DEFS, "helpers");
+const HELPER_SRC_PATH = path.resolve("src", "helpers");
 
 async function* visitAllFiles(base: string): AsyncIterable<string> {
   const contents = await fs.readdir(base, { withFileTypes: true });
@@ -23,7 +23,39 @@ async function* visitAllFiles(base: string): AsyncIterable<string> {
   }
 }
 
+async function buildPackageJsonTs() {
+  console.log("Building package.json.ts");
+  const packageJson = await fs.readFile(path.resolve("package.json"), "utf-8");
+
+  const parsed = JSON.parse(packageJson);
+
+  const mergedDependencies: Record<string, string> = {
+    ...parsed.devDependencies,
+    ...parsed.dependencies,
+  };
+
+  const fileText = [
+    "// Copyright (c) Microsoft Corporation",
+    "// Licensed under the MIT license.",
+    "",
+    "// prettier-ignore",
+    "",
+    `export const hsjsDependencies: Record<string, string> = {`,
+    ...Object.entries(mergedDependencies).map(([name, version]) => {
+      return `  ${JSON.stringify(name)}: ${JSON.stringify(version)},`;
+    }),
+    "};",
+    "",
+  ].join("\n");
+
+  await fs.writeFile(path.resolve(GENERATED_DEFS, "package.json.ts"), fileText);
+}
+
 async function main() {
+  console.log("Building JS server generator helpers.");
+
+  await buildPackageJsonTs();
+
   const allFiles: string[] = [];
   const indices = new Map<string, string[]>();
 
diff --git a/packages/http-server-js/generated-defs/package.json.ts b/packages/http-server-js/generated-defs/package.json.ts
new file mode 100644
index 00000000000..63404a37199
--- /dev/null
+++ b/packages/http-server-js/generated-defs/package.json.ts
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation
+// Licensed under the MIT license.
+
+// prettier-ignore
+
+export const hsjsDependencies: Record<string, string> = {
+  "@types/express": "^5.0.1",
+  "@types/morgan": "^1.9.9",
+  "@types/node": "~22.13.11",
+  "@types/swagger-ui-express": "^4.1.8",
+  "@typespec/compiler": "workspace:^",
+  "@typespec/http": "workspace:^",
+  "@typespec/http-specs": "workspace:^",
+  "@typespec/internal-build-utils": "workspace:^",
+  "@typespec/openapi3": "workspace:^",
+  "@typespec/spector": "workspace:^",
+  "@typespec/tspd": "workspace:^",
+  "@vitest/coverage-v8": "^3.0.9",
+  "@vitest/ui": "^3.0.9",
+  "decimal.js": "^10.5.0",
+  "express": "^5.1.0",
+  "fs-extra": "^11.2.0",
+  "globby": "~14.1.0",
+  "inquirer": "^12.5.0",
+  "morgan": "^1.10.0",
+  "ora": "^8.1.1",
+  "p-limit": "^6.2.0",
+  "pathe": "^2.0.3",
+  "picocolors": "~1.1.1",
+  "swagger-ui-express": "^5.0.1",
+  "tsx": "^4.19.3",
+  "typescript": "~5.8.2",
+  "vitest": "^3.0.9",
+  "yargs": "~17.7.2",
+  "prettier": "~3.5.3",
+  "yaml": "~2.7.0",
+};
diff --git a/packages/http-server-js/package.json b/packages/http-server-js/package.json
index 8c6284acbca..099637af9a9 100644
--- a/packages/http-server-js/package.json
+++ b/packages/http-server-js/package.json
@@ -63,7 +63,10 @@
     "yaml": "~2.7.0"
   },
   "devDependencies": {
+    "@types/express": "^5.0.1",
+    "@types/morgan": "^1.9.9",
     "@types/node": "~22.13.11",
+    "@types/swagger-ui-express": "^4.1.8",
     "@typespec/compiler": "workspace:^",
     "@typespec/http": "workspace:^",
     "@typespec/http-specs": "workspace:^",
@@ -73,13 +76,17 @@
     "@typespec/tspd": "workspace:^",
     "@vitest/coverage-v8": "^3.0.9",
     "@vitest/ui": "^3.0.9",
+    "decimal.js": "^10.5.0",
+    "express": "^5.1.0",
     "fs-extra": "^11.2.0",
     "globby": "~14.1.0",
     "inquirer": "^12.5.0",
+    "morgan": "^1.10.0",
     "ora": "^8.1.1",
     "p-limit": "^6.2.0",
     "pathe": "^2.0.3",
     "picocolors": "~1.1.1",
+    "swagger-ui-express": "^5.0.1",
     "tsx": "^4.19.3",
     "typescript": "~5.8.2",
     "vitest": "^3.0.9",
diff --git a/packages/http-server-js/src/common/scalar.ts b/packages/http-server-js/src/common/scalar.ts
index 5dfd758938b..9e15bfa3b5c 100644
--- a/packages/http-server-js/src/common/scalar.ts
+++ b/packages/http-server-js/src/common/scalar.ts
@@ -143,6 +143,21 @@ const DURATION_BIGINT_ENCODING: Dependent<ScalarEncoding> = (_, module) => {
   };
 };
 
+/**
+ * Resolves the encoding of Duration values to a BigDecimal number of seconds.
+ */
+const DURATION_BIGDECIMAL_ENCODING: Dependent<ScalarEncoding> = (_, module) => {
+  module.imports.push(
+    { from: dateTimeModule, binder: ["Duration"] },
+    { from: "decimal.js", binder: ["Decimal"] },
+  );
+
+  return {
+    encodeTemplate: "new Decimal(Duration.totalSeconds({}).toString())",
+    decodeTemplate: "Duration.fromSeconds({}.toNumber())",
+  };
+};
+
 const TYPESPEC_DURATION: ScalarInfo = {
   type: function importDuration(_, module) {
     module.imports.push({ from: dateTimeModule, binder: ["Duration"] });
@@ -163,7 +178,7 @@ const TYPESPEC_DURATION: ScalarInfo = {
       },
     },
     ...Object.fromEntries(
-      ["int32", "uint32"].map((n) => [
+      ["int32", "uint32", "float32", "float64"].map((n) => [
         `TypeSpec.${n}`,
         {
           default: { via: "seconds" },
@@ -180,6 +195,10 @@ const TYPESPEC_DURATION: ScalarInfo = {
         },
       ]),
     ),
+    "TypeSpec.float": {
+      default: { via: "seconds" },
+      seconds: DURATION_BIGDECIMAL_ENCODING,
+    },
   },
   defaultEncodings: {
     byMimeType: {
@@ -202,6 +221,23 @@ const NUMBER: ScalarInfo = {
   isJsonCompatible: true,
 };
 
+const BIGDECIMAL: ScalarInfo = {
+  type(_, module) {
+    module.imports.push({ from: "decimal.js", binder: ["Decimal"] });
+
+    return "Decimal";
+  },
+  encodings: {
+    "TypeSpec.string": {
+      default: {
+        encodeTemplate: "{}.toString()",
+        decodeTemplate: "new Decimal({})",
+      },
+    },
+  },
+  isJsonCompatible: false,
+};
+
 /**
  * Declarative scalar table.
  *
@@ -287,6 +323,11 @@ const SCALARS = new Map<string, ScalarInfo>([
   ["TypeSpec.int8", NUMBER],
   ["TypeSpec.safeint", NUMBER],
 
+  ["TypeSpec.numeric", BIGDECIMAL],
+  ["TypeSpec.float", BIGDECIMAL],
+  ["TypeSpec.decimal", BIGDECIMAL],
+  ["TypeSpec.decimal128", BIGDECIMAL],
+
   [
     "TypeSpec.integer",
     {
diff --git a/packages/http-server-js/src/scripts/scaffold/bin.mts b/packages/http-server-js/src/scripts/scaffold/bin.mts
index c387b5a726a..36943343c71 100644
--- a/packages/http-server-js/src/scripts/scaffold/bin.mts
+++ b/packages/http-server-js/src/scripts/scaffold/bin.mts
@@ -3,6 +3,8 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+import { hsjsDependencies } from "../../../generated-defs/package.json.js";
+
 import { compile, formatDiagnostic, NodeHost, OperationContainer } from "@typespec/compiler";
 
 import YAML from "yaml";
@@ -126,7 +128,7 @@ function parseScaffoldArguments(args: string[]): ScaffoldingOptions {
     } else if (arg === "--force") {
       options.force = true;
     } else {
-      console.error(`[hsj] Unrecognized scaffolding argument: '${arg}'`);
+      console.error(`[hsjs] Unrecognized scaffolding argument: '${arg}'`);
       process.exit(1);
     }
 
@@ -146,7 +148,7 @@ async function confirmYesNo(message: string): Promise<void> {
     const response = await rl.question(`${message} [y/N] `);
 
     if (response.trim().toLowerCase() !== "y") {
-      console.error("[hsj] Operation cancelled.");
+      console.error("[hsjs] Operation cancelled.");
       process.exit(0);
     }
   } finally {
@@ -157,7 +159,7 @@ async function confirmYesNo(message: string): Promise<void> {
 export async function scaffold(options: ScaffoldingOptions) {
   if (options.force) {
     await confirmYesNo(
-      "[hsj] The `--force` flag is set and will overwrite existing files and settings that may have been modified. Continue?",
+      "[hsjs] The `--force` flag is set and will overwrite existing files and settings that may have been modified. Continue?",
     );
   }
 
@@ -166,9 +168,9 @@ export async function scaffold(options: ScaffoldingOptions) {
   const projectYamlPath = path.resolve(cwd, COMMON_PATHS.projectYaml);
   const mainTspPath = path.resolve(cwd, COMMON_PATHS.mainTsp);
 
-  console.info("[hsj] Scaffolding TypeScript project...");
+  console.info("[hsjs] Scaffolding TypeScript project...");
   console.info(
-    `[hsj] Using project file '${path.relative(cwd, projectYamlPath)}' and main file '${path.relative(cwd, mainTspPath)}'`,
+    `[hsjs] Using project file '${path.relative(cwd, projectYamlPath)}' and main file '${path.relative(cwd, mainTspPath)}'`,
   );
 
   let config: any;
@@ -179,7 +181,7 @@ export async function scaffold(options: ScaffoldingOptions) {
     config = YAML.parse(configText.toString("utf-8"));
   } catch {
     console.error(
-      "[hsj] Failed to read project configuration file. Is the project initialized using `tsp init`?",
+      "[hsjs] Failed to read project configuration file. Is the project initialized using `tsp init`?",
     );
     process.exit(1);
   }
@@ -204,16 +206,16 @@ export async function scaffold(options: ScaffoldingOptions) {
   };
 
   console.info(
-    `[hsj] Emitter options have 'express: ${expressOptions.isExpress}'. Generating server model: '${expressOptions.isExpress ? "Express" : "Node"}'.`,
+    `[hsjs] Emitter options have 'express: ${expressOptions.isExpress}'. Generating server model: '${expressOptions.isExpress ? "Express" : "Node"}'.`,
   );
 
   if (options["no-standalone"]) {
-    console.info("[hsj] Standalone mode disabled, generating project in current directory.");
+    console.info("[hsjs] Standalone mode disabled, generating project in current directory.");
   } else {
-    console.info("[hsj] Generating standalone project in output directory.");
+    console.info("[hsjs] Generating standalone project in output directory.");
   }
 
-  console.info("[hsj] Compiling TypeSpec project...");
+  console.info("[hsjs] Compiling TypeSpec project...");
 
   const program = await compile(NodeHost, mainTspPath, {
     noEmit: true,
@@ -228,7 +230,7 @@ export async function scaffold(options: ScaffoldingOptions) {
   });
 
   if (!jsCtx) {
-    console.error("[hsj] No services were found in the program. Exiting.");
+    console.error("[hsjs] No services were found in the program. Exiting.");
     process.exit(1);
   }
 
@@ -244,17 +246,17 @@ export async function scaffold(options: ScaffoldingOptions) {
   }
 
   if (program.hasError() || hadError) {
-    console.error("[hsj] TypeScript compilation failed. See above error output.");
+    console.error("[hsjs] TypeScript compilation failed. See above error output.");
     process.exit(1);
   }
 
-  console.info("[hsj] TypeSpec compiled successfully. Scaffolding implementation...");
+  console.info("[hsjs] TypeSpec compiled successfully. Scaffolding implementation...");
 
   const indexModule = jsCtx.srcModule;
 
   const routeControllers = await createRouteControllers(jsCtx, httpService, indexModule);
 
-  console.info("[hsj] Generating server entry point...");
+  console.info("[hsjs] Generating server entry point...");
 
   const controllerModules = new Set<Module>();
 
@@ -370,7 +372,7 @@ export async function scaffold(options: ScaffoldingOptions) {
     ]);
   }
 
-  console.info("[hsj] Writing files...");
+  console.info("[hsjs] Writing files...");
 
   const queue = createOnceQueue<Module>();
 
@@ -430,36 +432,39 @@ export async function scaffold(options: ScaffoldingOptions) {
   try {
     ownPackageJson = JSON.parse((await fs.readFile(ownPackageJsonPath)).toString("utf-8"));
   } catch {
-    console.error("[hsj] Failed to read package.json of TypeSpec project. Exiting.");
+    console.error("[hsjs] Failed to read package.json of TypeSpec project. Exiting.");
     process.exit(1);
   }
 
+  // Accumulate all dependencies
+  const externalDependencies = getAllExternalDependencies(jsCtx);
+
   let packageJsonChanged = true;
 
   if (options["no-standalone"]) {
-    console.info("[hsj] Checking package.json for changes...");
+    console.info("[hsjs] Checking package.json for changes...");
 
-    packageJsonChanged = updatePackageJson(ownPackageJson, expressOptions, options.force);
+    packageJsonChanged = updatePackageJson(ownPackageJson, options.force, externalDependencies);
 
     if (packageJsonChanged) {
-      console.info("[hsj] Writing updated package.json...");
+      console.info("[hsjs] Writing updated package.json...");
 
       try {
         await fs.writeFile(ownPackageJsonPath, JSON.stringify(ownPackageJson, null, 2) + "\n");
       } catch {
-        console.error("[hsj] Failed to write package.json.");
+        console.error("[hsjs] Failed to write package.json.");
         process.exit(1);
       }
     } else {
-      console.info("[hsj] No changes to package.json suggested.");
+      console.info("[hsjs] No changes to package.json suggested.");
     }
   } else {
     // Standalone mode, need to generate package.json from scratch
     const relativePathToSpec = path.relative(baseOutputDir, cwd);
     const packageJson = getPackageJsonForStandaloneProject(
       ownPackageJson,
-      expressOptions,
       relativePathToSpec,
+      externalDependencies,
     );
 
     const packageJsonPath = path.resolve(baseOutputDir, COMMON_PATHS.packageJson);
@@ -469,7 +474,7 @@ export async function scaffold(options: ScaffoldingOptions) {
 
   if (packageJsonChanged) {
     // Run npm install to ensure dependencies are installed.
-    console.info("[hsj] Running npm install...");
+    console.info("[hsjs] Running npm install...");
 
     try {
       await spawn("npm", ["install"], {
@@ -479,12 +484,12 @@ export async function scaffold(options: ScaffoldingOptions) {
       });
     } catch {
       console.warn(
-        "[hsj] Failed to run npm install. Check the output above for errors and install dependencies manually.",
+        "[hsjs] Failed to run npm install. Check the output above for errors and install dependencies manually.",
       );
     }
   }
 
-  console.info("[hsj] Project scaffolding complete. Building project...");
+  console.info("[hsjs] Project scaffolding complete. Building project...");
 
   try {
     await spawn("npm", ["run", "build"], {
@@ -493,21 +498,21 @@ export async function scaffold(options: ScaffoldingOptions) {
       shell: process.platform === "win32",
     });
   } catch {
-    console.error("[hsj] Failed to build project. Check the output above for errors.");
+    console.error("[hsjs] Failed to build project. Check the output above for errors.");
     process.exit(1);
   }
 
   const codeDirectory = path.relative(cwd, options["no-standalone"] ? cwd : baseOutputDir);
 
-  console.info("[hsj] Project is ready to run. Use `npm start` to launch the server.");
-  console.info("[hsj] A debug configuration has been created for Visual Studio Code.");
+  console.info("[hsjs] Project is ready to run. Use `npm start` to launch the server.");
+  console.info("[hsjs] A debug configuration has been created for Visual Studio Code.");
   console.info(
-    `[hsj] Try \`code ${codeDirectory}\` to open the project and press F5 to start debugging.`,
+    `[hsjs] Try \`code ${codeDirectory}\` to open the project and press F5 to start debugging.`,
   );
   console.info(
-    `[hsj] The newly-generated route controllers in '${path.join(codeDirectory, "src", "controllers")}' are ready to be implemented.`,
+    `[hsjs] The newly-generated route controllers in '${path.join(codeDirectory, "src", "controllers")}' are ready to be implemented.`,
   );
-  console.info("[hsj] Done.");
+  console.info("[hsjs] Done.");
 
   async function tryWrite(file: string, contents: string): Promise<void> {
     try {
@@ -519,20 +524,54 @@ export async function scaffold(options: ScaffoldingOptions) {
         .catch(() => false);
 
       if (exists && !options.force) {
-        console.warn(`[hsj] File '${relative}' already exists and will not be overwritten.`);
-        console.warn(`[hsj] Manually update the file or delete it and run scaffolding again.`);
+        console.warn(`[hsjs] File '${relative}' already exists and will not be overwritten.`);
+        console.warn(`[hsjs] Manually update the file or delete it and run scaffolding again.`);
 
         return;
       } else if (exists) {
-        console.warn(`[hsj] Overwriting file '${relative}'...`);
+        console.warn(`[hsjs] Overwriting file '${relative}'...`);
       } else {
-        console.info(`[hsj] Writing file '${relative}'...`);
+        console.info(`[hsjs] Writing file '${relative}'...`);
       }
 
       await fs.mkdir(path.dirname(file), { recursive: true });
       await fs.writeFile(file, contents);
     } catch (e: unknown) {
-      console.error(`[hsj] Failed to write file: '${(e as Error).message}'`);
+      console.error(`[hsjs] Failed to write file: '${(e as Error).message}'`);
+    }
+  }
+}
+
+function getAllExternalDependencies(ctx: JsContext): Set<string> {
+  const externalDependencies = new Set<string>();
+
+  const visited = new Set<Module>();
+
+  addModule(ctx.rootModule);
+
+  return externalDependencies;
+
+  function addModule(module: Module) {
+    visited.add(module);
+
+    for (const declaration of module.declarations) {
+      if (isModule(declaration) && !visited.has(declaration)) {
+        addModule(declaration);
+      }
+    }
+
+    for (const _import of module.imports) {
+      if (
+        typeof _import.from === "string" &&
+        !_import.from.startsWith(".") &&
+        !_import.from.startsWith("/")
+      ) {
+        externalDependencies.add(_import.from);
+      } else if (typeof _import.from !== "string") {
+        if (!visited.has(_import.from)) {
+          addModule(_import.from);
+        }
+      }
     }
   }
 }
@@ -595,7 +634,7 @@ async function createRouteController(
 
   const controllerName = containerNameCase.pascalCase + "Impl";
 
-  console.info(`[hsj] Generating controller '${controllerName}'...`);
+  console.info(`[hsjs] Generating controller '${controllerName}'...`);
 
   module.declarations.push([
     `export class ${controllerName} implements ${containerNameCase.pascalCase}<HttpContext> {`,
@@ -688,8 +727,8 @@ function* emitControllerOperationHandlers(
 
 function getPackageJsonForStandaloneProject(
   ownPackageJson: any,
-  express: PackageJsonExpressOptions,
   relativePathToSpec: string,
+  externalDependencies: Set<string>,
 ): any {
   const packageJson = {
     name: (ownPackageJson.name ?? path.basename(process.cwd())) + "-server",
@@ -702,7 +741,7 @@ function getPackageJsonForStandaloneProject(
     packageJson.private = true;
   }
 
-  updatePackageJson(packageJson, express, true, () => {});
+  updatePackageJson(packageJson, true, externalDependencies, () => {});
 
   delete packageJson.scripts["build:scaffold"];
   packageJson.scripts["build:typespec"] = 'tsp compile --output-dir=".." ' + relativePathToSpec;
@@ -719,8 +758,8 @@ interface PackageJsonExpressOptions {
 
 function updatePackageJson(
   packageJson: any,
-  express: PackageJsonExpressOptions,
   force: boolean,
+  externalDependencies: Set<string>,
   info: (...args: any[]) => void = console.info,
 ): boolean {
   let changed = false;
@@ -733,17 +772,31 @@ function updatePackageJson(
   updateObjectPath(["devDependencies", "typescript"], "^5.7.3");
   updateObjectPath(["devDependencies", "@types/node"], "^22.13.1");
 
-  if (express.isExpress) {
-    updateObjectPath(["dependencies", "express"], "^5.0.1");
-    updateObjectPath(["devDependencies", "@types/express"], "^5.0.0");
+  let hadError = false;
+
+  for (const dependency of externalDependencies) {
+    const dependencyVersion = hsjsDependencies[dependency];
 
-    if (express.openApi3) {
-      updateObjectPath(["dependencies", "swagger-ui-express"], "^5.0.1");
-      updateObjectPath(["devDependencies", "@types/swagger-ui-express"], "^4.1.7");
+    if (!dependencyVersion) {
+      hadError = true;
+      console.error("[hsjs] Failed to find version for dependency:", dependency);
+      continue;
     }
 
-    updateObjectPath(["dependencies", "morgan"], "^1.10.0");
-    updateObjectPath(["devDependencies", "@types/morgan"], "^1.9.9");
+    updateObjectPath(["dependencies", dependency], dependencyVersion);
+
+    const typesDependency = `@types/${dependency}`;
+    const typesDependencyVersion = hsjsDependencies[typesDependency];
+    if (typesDependencyVersion) {
+      updateObjectPath(["devDependencies", typesDependency], typesDependencyVersion);
+    }
+  }
+
+  if (hadError) {
+    console.error(
+      "[hsjs] FATAL: Failed to find dependency versions. This is a bug. Please report this error to https://github.com/microsoft/typespec",
+    );
+    process.exit(1);
   }
 
   return changed;
@@ -769,9 +822,9 @@ function updatePackageJson(
 
     if (!existingValue || force) {
       if (!existingValue) {
-        info(`[hsj] - Setting package.json property '${property}' to "${value}".`);
+        info(`[hsjs] - Setting package.json property '${property}' to "${value}".`);
       } else if (force) {
-        info(`[hsj] - Overwriting package.json property '${property}' to "${value}".`);
+        info(`[hsjs] - Overwriting package.json property '${property}' to "${value}".`);
       }
 
       current[path[path.length - 1]] = value;
@@ -782,10 +835,10 @@ function updatePackageJson(
     }
 
     if (current[path[path.length - 1]] !== value) {
-      info(`[hsj] - Skipping package.json property '${property}'.`);
-      info(`[hsj]   Scaffolding prefers "${value}", but it is already set to "${existingValue}".`);
+      info(`[hsjs] - Skipping package.json property '${property}'.`);
+      info(`[hsjs]   Scaffolding prefers "${value}", but it is already set to "${existingValue}".`);
       info(
-        "[hsj]   Manually update the property or remove it and run scaffolding again if needed.",
+        "[hsjs]   Manually update the property or remove it and run scaffolding again if needed.",
       );
     }
   }
diff --git a/packages/http-server-js/src/scripts/scaffold/data-mocks.ts b/packages/http-server-js/src/scripts/scaffold/data-mocks.ts
index f269496c38c..a8d234beb1f 100644
--- a/packages/http-server-js/src/scripts/scaffold/data-mocks.ts
+++ b/packages/http-server-js/src/scripts/scaffold/data-mocks.ts
@@ -190,13 +190,18 @@ function mockScalar(ctx: JsContext, module: Module, scalar: Scalar): string | un
     return JSON.stringify(true);
   }
   if ($.scalar.isNumeric(scalar) || $.scalar.extendsNumeric(scalar)) {
-    switch ((scalar as Scalar).name) {
-      case "integer":
-      case "int64":
-      case "uint64":
-        return "42n";
-      default:
-        return "42";
+    if (
+      $.scalar.extendsSafeint(scalar) ||
+      $.scalar.extendsInt32(scalar) ||
+      $.scalar.extendsUint32(scalar) ||
+      $.scalar.extendsFloat64(scalar)
+    ) {
+      return "42";
+    } else if ($.scalar.extendsInteger(scalar)) {
+      return "42n";
+    } else {
+      module.imports.push({ from: "decimal.js", binder: ["Decimal"] });
+      return "new Decimal(42)";
     }
   }
 
diff --git a/packages/spector/package.json b/packages/spector/package.json
index b9772745be2..d4a55f7eff8 100644
--- a/packages/spector/package.json
+++ b/packages/spector/package.json
@@ -62,7 +62,7 @@
     "@types/deep-equal": "^1.0.1",
     "@types/express": "^5.0.1",
     "@types/micromatch": "^4.0.9",
-    "@types/morgan": "^1.9.4",
+    "@types/morgan": "^1.9.9",
     "@types/multer": "^1.4.10",
     "@types/node": "~22.13.11",
     "@types/xml2js": "^0.4.11",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7206b84f385..fc474892716 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -913,9 +913,18 @@ importers:
         specifier: ~2.7.0
         version: 2.7.0
     devDependencies:
+      '@types/express':
+        specifier: ^5.0.1
+        version: 5.0.1
+      '@types/morgan':
+        specifier: ^1.9.9
+        version: 1.9.9
       '@types/node':
         specifier: ~22.13.11
         version: 22.13.13
+      '@types/swagger-ui-express':
+        specifier: ^4.1.8
+        version: 4.1.8
       '@typespec/compiler':
         specifier: workspace:^
         version: link:../compiler
@@ -943,6 +952,12 @@ importers:
       '@vitest/ui':
         specifier: ^3.0.9
         version: 3.0.9(vitest@3.0.9)
+      decimal.js:
+        specifier: ^10.5.0
+        version: 10.5.0
+      express:
+        specifier: ^5.1.0
+        version: 5.1.0
       fs-extra:
         specifier: ^11.2.0
         version: 11.3.0
@@ -952,6 +967,9 @@ importers:
       inquirer:
         specifier: ^12.5.0
         version: 12.5.0(@types/node@22.13.13)
+      morgan:
+        specifier: ^1.10.0
+        version: 1.10.0
       ora:
         specifier: ^8.1.1
         version: 8.2.0
@@ -964,6 +982,9 @@ importers:
       picocolors:
         specifier: ~1.1.1
         version: 1.1.1
+      swagger-ui-express:
+        specifier: ^5.0.1
+        version: 5.0.1(express@5.1.0)
       tsx:
         specifier: ^4.19.3
         version: 4.19.3
@@ -1993,7 +2014,7 @@ importers:
         specifier: ^4.0.9
         version: 4.0.9
       '@types/morgan':
-        specifier: ^1.9.4
+        specifier: ^1.9.9
         version: 1.9.9
       '@types/multer':
         specifier: ^1.4.10
@@ -5925,9 +5946,6 @@ packages:
   '@types/node@17.0.45':
     resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==}
 
-  '@types/node@18.19.83':
-    resolution: {integrity: sha512-D69JeR5SfFS5H6FLbUaS0vE4r1dGhmMBbG4Ed6BNS4wkDK8GZjsdCShT5LCN59vOHEUHnFCY9J4aclXlIphMkA==}
-
   '@types/node@18.19.86':
     resolution: {integrity: sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==}
 
@@ -5993,6 +6011,9 @@ packages:
   '@types/swagger-ui-dist@3.30.5':
     resolution: {integrity: sha512-SrXhD9L8qeIxJzN+o1kmf3wXeVf/+Km3jIdRM1+Yq3I5b/dlF5TcGr5WCVM7I/cBYpgf43/gCPIucQ13AhICiw==}
 
+  '@types/swagger-ui-express@4.1.8':
+    resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==}
+
   '@types/swagger-ui@3.52.4':
     resolution: {integrity: sha512-7NV7q8BfupqdQxr26OkM0g0YEPB9uXnKGzXadgcearvI9MoCHt3F72lPTX3fZZIlrr21DC0IK26wcDMZ37oFDA==}
 
@@ -11631,6 +11652,12 @@ packages:
   swagger-ui-dist@5.20.1:
     resolution: {integrity: sha512-qBPCis2w8nP4US7SvUxdJD3OwKcqiWeZmjN2VWhq2v+ESZEXOP/7n4DeiOiiZcGYTKMHAHUUrroHaTsjUWTEGw==}
 
+  swagger-ui-express@5.0.1:
+    resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==}
+    engines: {node: '>= v0.10.32'}
+    peerDependencies:
+      express: '>=4.0.0 || >=5.0.0-beta'
+
   symbol-tree@3.2.4:
     resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
 
@@ -17566,10 +17593,6 @@ snapshots:
 
   '@types/node@17.0.45': {}
 
-  '@types/node@18.19.83':
-    dependencies:
-      undici-types: 5.26.5
-
   '@types/node@18.19.86':
     dependencies:
       undici-types: 5.26.5
@@ -17639,6 +17662,11 @@ snapshots:
 
   '@types/swagger-ui-dist@3.30.5': {}
 
+  '@types/swagger-ui-express@4.1.8':
+    dependencies:
+      '@types/express': 5.0.1
+      '@types/serve-static': 1.15.7
+
   '@types/swagger-ui@3.52.4': {}
 
   '@types/treeify@1.0.3': {}
@@ -18518,7 +18546,7 @@ snapshots:
 
   '@yarnpkg/pnp@4.0.9':
     dependencies:
-      '@types/node': 18.19.83
+      '@types/node': 18.19.86
       '@yarnpkg/fslib': 3.1.2
 
   '@yarnpkg/pnp@4.1.0':
@@ -24977,6 +25005,11 @@ snapshots:
     dependencies:
       '@scarf/scarf': 1.4.0
 
+  swagger-ui-express@5.0.1(express@5.1.0):
+    dependencies:
+      express: 5.1.0
+      swagger-ui-dist: 5.20.1
+
   symbol-tree@3.2.4: {}
 
   syncpack@13.0.3(typescript@5.8.2):


Reviews:
user: login: chrisradek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T23:14:30Z
user: login: witemple-msft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T23:26:33Z
user: login: chrisradek
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T17:00:23Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T17:06:05Z
user: login: chrisradek
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T18:13:54Z

Review comments:
diff_hunk: @@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// Licensed under the MIT license.
+
+export const hsjsDependencies: Record<string, string> = {
+  "@types/express": "^5.0.1",
+  "@types/morgan": "^1.9.9",
+  "@types/node": "~22.13.11",
+  "@types/swagger-ui-express": "^4.1.8",
+  "@typespec/compiler": "workspace:^",
+  "@typespec/http": "workspace:^",
+  "@typespec/http-specs": "workspace:^",
+  "@typespec/internal-build-utils": "workspace:^",
+  "@typespec/openapi3": "workspace:^",
+  "@typespec/spector": "workspace:^",
+  "@typespec/tspd": "workspace:^",
+  "@vitest/coverage-v8": "^3.0.9",
+  "@vitest/ui": "^3.0.9",
+  "decimal.js": "^10.5.0",
+  "express": "^5.1.0",
+  "fs-extra": "^11.2.0",
+  "globby": "~14.1.0",
+  "inquirer": "^12.5.0",
+  "morgan": "^1.10.0",
+  "ora": "^8.1.1",
+  "p-limit": "^6.2.0",
+  "pathe": "^2.0.3",
+  "picocolors": "~1.1.1",
path: packages/http-server-js/generated-defs/package.json.ts
user: login: chrisradek
body: Does this result in all of these dependencies getting installed when someone enables `express` scaffolding? I think a lot of these are just our own dev dependencies that a user wouldn't need.
created_at: 2025-04-08T23:14:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation
+// Licensed under the MIT license.
+
+export const hsjsDependencies: Record<string, string> = {
+  "@types/express": "^5.0.1",
+  "@types/morgan": "^1.9.9",
+  "@types/node": "~22.13.11",
+  "@types/swagger-ui-express": "^4.1.8",
+  "@typespec/compiler": "workspace:^",
+  "@typespec/http": "workspace:^",
+  "@typespec/http-specs": "workspace:^",
+  "@typespec/internal-build-utils": "workspace:^",
+  "@typespec/openapi3": "workspace:^",
+  "@typespec/spector": "workspace:^",
+  "@typespec/tspd": "workspace:^",
+  "@vitest/coverage-v8": "^3.0.9",
+  "@vitest/ui": "^3.0.9",
+  "decimal.js": "^10.5.0",
+  "express": "^5.1.0",
+  "fs-extra": "^11.2.0",
+  "globby": "~14.1.0",
+  "inquirer": "^12.5.0",
+  "morgan": "^1.10.0",
+  "ora": "^8.1.1",
+  "p-limit": "^6.2.0",
+  "pathe": "^2.0.3",
+  "picocolors": "~1.1.1",
path: packages/http-server-js/generated-defs/package.json.ts
user: login: witemple-msft
body: No, only the ones that the generated project actually imported. This little module is sort of a preprocessing step that puts all my own dependency versions in this file, then the scaffolding tool actually looks at what external dependencies were actually imported and injects those constraints into the scaffolded package.json.
created_at: 2025-04-08T23:26:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+---
+changeKind: internal
+packages:
+  - "@typespec/spector"
+---
+
+Aligned @types/morgan devDependency versions.
path: .chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-9-10-50-48.md
user: login: timotheeguerin
body: ```suggestion
Aligned `@types/morgan` devDependency versions.
```
created_at: 2025-04-09T17:06:05Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-08T23:13:30Z
body: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@typespec/http-server-js`
 - :white_check_mark: `@typespec/spector`

<details>
<summary>Show changes</summary>

### `@typespec/http-server-js` - _feature_ [✏️](https://github.com/witemple-msft/typespec/edit/witemple-msft/hsjs-decimal-float/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-8-18-49-0.md?pr=/microsoft/typespec/pull/6914)
> Add support for TypeSpec.decimal, TypeSpec.decimal128, TypeSpec.float, and TypeSpec.numeric, all represented as `Decimal` from the 'decimal.js' package.
### `@typespec/spector` - _internal_ [✏️](https://github.com/witemple-msft/typespec/edit/witemple-msft/hsjs-decimal-float/.chronus/changes/witemple-msft-hsjs-decimal-float-2025-3-9-10-50-48.md?pr=/microsoft/typespec/pull/6914)
> Aligned `@types/morgan` devDependency versions.
</details>

author_association: CONTRIBUTOR
user: login: azure-sdk
created_at: 2025-04-08T23:40:13Z
body: <!-- _TSP_TRYIT_COMMENT_ -->
You can try these changes here

| [🛝 Playground]( https://cadlplayground.z22.web.core.windows.net/prs/6914/)|[🌐 Website](https://tspwebsitepr.z22.web.core.windows.net/prs/6914/)|[🛝 VSCode Extension]( https://artprodcus3.artifacts.visualstudio.com/A0fb41ef4-5012-48a9-bf39-4ee3de03ee35/590cfd2a-581c-4dcb-a12e-6568ce786175/_apis/artifact/cGlwZWxpbmVhcnRpZmFjdDovL2F6dXJlLXNkay9wcm9qZWN0SWQvNTkwY2ZkMmEtNTgxYy00ZGNiLWExMmUtNjU2OGNlNzg2MTc1L2J1aWxkSWQvNDc0MDk1NC9hcnRpZmFjdE5hbWUvdnNjb2RlLWV4dGVuc2lvbg2/content?format=zip) |
| ---|---|--- |
author_association: COLLABORATOR