@typespec/http-server-js 0.58.0-alpha.12-dev.0 → 0.58.0-alpha.12-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.testignore +26 -0
- package/dist/generated-defs/helpers/datetime.d.ts +4 -0
- package/dist/generated-defs/helpers/datetime.d.ts.map +1 -0
- package/dist/generated-defs/helpers/datetime.js +256 -0
- package/dist/generated-defs/helpers/datetime.js.map +1 -0
- package/dist/generated-defs/helpers/index.d.ts.map +1 -1
- package/dist/generated-defs/helpers/index.js +1 -0
- package/dist/generated-defs/helpers/index.js.map +1 -1
- package/dist/src/common/declaration.js +1 -1
- package/dist/src/common/declaration.js.map +1 -1
- package/dist/src/common/reference.js +1 -1
- package/dist/src/common/reference.js.map +1 -1
- package/dist/src/common/scalar.d.ts +175 -22
- package/dist/src/common/scalar.d.ts.map +1 -1
- package/dist/src/common/scalar.js +420 -93
- package/dist/src/common/scalar.js.map +1 -1
- package/dist/src/common/serialization/index.d.ts +2 -2
- package/dist/src/common/serialization/index.d.ts.map +1 -1
- package/dist/src/common/serialization/index.js +9 -3
- package/dist/src/common/serialization/index.js.map +1 -1
- package/dist/src/common/serialization/json.d.ts +2 -2
- package/dist/src/common/serialization/json.d.ts.map +1 -1
- package/dist/src/common/serialization/json.js +144 -42
- package/dist/src/common/serialization/json.js.map +1 -1
- package/dist/src/helpers/datetime.d.ts +92 -0
- package/dist/src/helpers/datetime.d.ts.map +1 -0
- package/dist/src/helpers/datetime.js +151 -0
- package/dist/src/helpers/datetime.js.map +1 -0
- package/dist/src/http/server/index.d.ts.map +1 -1
- package/dist/src/http/server/index.js +17 -12
- package/dist/src/http/server/index.js.map +1 -1
- package/dist/src/http/server/multipart.js +1 -1
- package/dist/src/http/server/multipart.js.map +1 -1
- package/dist/src/lib.d.ts +10 -1
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +6 -0
- package/dist/src/lib.js.map +1 -1
- package/dist/src/util/case.d.ts +9 -0
- package/dist/src/util/case.d.ts.map +1 -1
- package/dist/src/util/case.js +18 -0
- package/dist/src/util/case.js.map +1 -1
- package/dist/src/util/differentiate.d.ts +4 -4
- package/dist/src/util/differentiate.d.ts.map +1 -1
- package/dist/src/util/differentiate.js +10 -10
- package/dist/src/util/differentiate.js.map +1 -1
- package/eng/scripts/emit-e2e.js +315 -0
- package/eng/scripts/tspconfig.yaml +6 -0
- package/generated-defs/helpers/datetime.ts +263 -0
- package/generated-defs/helpers/index.ts +1 -0
- package/package.json +23 -9
- package/src/common/declaration.ts +1 -1
- package/src/common/reference.ts +1 -1
- package/src/common/scalar.ts +709 -103
- package/src/common/serialization/index.ts +11 -4
- package/src/common/serialization/json.ts +174 -52
- package/src/helpers/datetime.ts +235 -0
- package/src/http/server/index.ts +29 -15
- package/src/http/server/multipart.ts +1 -1
- package/src/lib.ts +6 -0
- package/src/util/case.ts +19 -0
- package/src/util/differentiate.ts +15 -8
- package/temp/tsconfig.tsbuildinfo +1 -1
- package/test/datetime.test.ts +226 -0
- package/test/e2e/helpers.ts +59 -0
- package/test/e2e/http/parameters/basic/main.test.e2e.ts +36 -0
- package/test/e2e/http/parameters/body-optionality/main.test.e2e.ts +45 -0
- package/test/e2e/http/parameters/spread/main.test.e2e.ts +92 -0
- package/test/e2e/http/type/model/empty/main.test.e2e.ts +35 -0
- package/test/e2e/spector.ts +33 -0
- package/test/scalar.test.ts +345 -0
- package/vitest.config.e2e.ts +20 -0
- package/vitest.config.ts +8 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import { run } from "@typespec/internal-build-utils";
|
|
4
|
+
import pkg from "fs-extra";
|
|
5
|
+
import { copyFile, mkdir, rm } from "fs/promises";
|
|
6
|
+
import { globby } from "globby";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
import ora from "ora";
|
|
9
|
+
import pLimit from "p-limit";
|
|
10
|
+
import { basename, dirname, join, resolve } from "path";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { hideBin } from "yargs/helpers";
|
|
14
|
+
import yargs from "yargs/yargs";
|
|
15
|
+
|
|
16
|
+
const { pathExists, stat, readFile, writeFile } = pkg;
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
const projectRoot = join(__dirname, "../..");
|
|
22
|
+
const tspConfig = join(__dirname, "tspconfig.yaml");
|
|
23
|
+
|
|
24
|
+
const basePath = join(projectRoot, "node_modules", "@typespec", "http-specs", "specs");
|
|
25
|
+
const ignoreFilePath = join(projectRoot, ".testignore");
|
|
26
|
+
const logDirRoot = join(projectRoot, "temp", "emit-e2e-logs");
|
|
27
|
+
const reportFilePath = join(logDirRoot, "report.txt");
|
|
28
|
+
|
|
29
|
+
// Remove the log directory if it exists.
|
|
30
|
+
async function clearLogDirectory() {
|
|
31
|
+
if (await pathExists(logDirRoot)) {
|
|
32
|
+
await rm(logDirRoot, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse command-line arguments.
|
|
37
|
+
const argv = yargs(hideBin(process.argv))
|
|
38
|
+
.option("interactive", {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
describe: "Enable interactive mode",
|
|
41
|
+
default: false,
|
|
42
|
+
})
|
|
43
|
+
.positional("paths", {
|
|
44
|
+
describe: "Optional list of specific file or directory paths to process (relative to basePath)",
|
|
45
|
+
type: "string",
|
|
46
|
+
array: true,
|
|
47
|
+
default: [],
|
|
48
|
+
})
|
|
49
|
+
.option("build", {
|
|
50
|
+
type: "boolean",
|
|
51
|
+
describe: "Build the generated projects",
|
|
52
|
+
default: false,
|
|
53
|
+
})
|
|
54
|
+
.help().argv;
|
|
55
|
+
|
|
56
|
+
// Read and parse the ignore file.
|
|
57
|
+
async function getIgnoreList() {
|
|
58
|
+
try {
|
|
59
|
+
const content = await readFile(ignoreFilePath, "utf8");
|
|
60
|
+
return content
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.filter((line) => line.trim() && !line.startsWith("#"))
|
|
63
|
+
.map((line) => line.trim());
|
|
64
|
+
} catch {
|
|
65
|
+
console.warn(pc.yellow("No ignore file found."));
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Recursively process paths (files or directories relative to basePath).
|
|
71
|
+
async function processPaths(paths, ignoreList) {
|
|
72
|
+
const results = [];
|
|
73
|
+
for (const relativePath of paths) {
|
|
74
|
+
const fullPath = resolve(basePath, relativePath);
|
|
75
|
+
|
|
76
|
+
if (!(await pathExists(fullPath))) {
|
|
77
|
+
console.warn(pc.yellow(`Path not found: ${relativePath}`));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const stats = await stat(fullPath);
|
|
82
|
+
if (stats.isFile() && fullPath.endsWith("main.tsp")) {
|
|
83
|
+
if (ignoreList.some((ignore) => relativePath.startsWith(ignore))) continue;
|
|
84
|
+
results.push({ fullPath, relativePath });
|
|
85
|
+
} else if (stats.isDirectory()) {
|
|
86
|
+
const patterns = ["**/main.tsp"];
|
|
87
|
+
const discoveredPaths = await globby(patterns, { cwd: fullPath });
|
|
88
|
+
const validFiles = discoveredPaths
|
|
89
|
+
.map((p) => ({
|
|
90
|
+
fullPath: join(fullPath, p),
|
|
91
|
+
relativePath: join(relativePath, p),
|
|
92
|
+
}))
|
|
93
|
+
.filter((file) => !ignoreList.some((ignore) => file.relativePath.startsWith(ignore)));
|
|
94
|
+
results.push(...validFiles);
|
|
95
|
+
} else {
|
|
96
|
+
console.warn(pc.yellow(`Skipping unsupported path: ${relativePath}`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Deduplicate.
|
|
101
|
+
const filesByDir = new Map();
|
|
102
|
+
for (const file of results) {
|
|
103
|
+
const dir = dirname(file.relativePath);
|
|
104
|
+
const existing = filesByDir.get(dir);
|
|
105
|
+
if (!existing) {
|
|
106
|
+
filesByDir.set(dir, file);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return Array.from(filesByDir.values());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Run a shell command silently.
|
|
113
|
+
async function runCommand(command, args, options = {}) {
|
|
114
|
+
// Remove clutter by not printing anything; capture output by setting stdio to 'pipe'.
|
|
115
|
+
return await run(command, args, {
|
|
116
|
+
stdio: "pipe",
|
|
117
|
+
env: { NODE_ENV: "test", ...process.env },
|
|
118
|
+
silent: true,
|
|
119
|
+
...options,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Process a single file.
|
|
124
|
+
async function processFile(file, options) {
|
|
125
|
+
const { fullPath, relativePath } = file;
|
|
126
|
+
const { build, interactive } = options;
|
|
127
|
+
const outputDir = join("test", "e2e", "generated", dirname(relativePath));
|
|
128
|
+
const specCopyPath = join(outputDir, "spec.tsp");
|
|
129
|
+
const logDir = join(projectRoot, "temp", "emit-e2e-logs", dirname(relativePath));
|
|
130
|
+
|
|
131
|
+
let spinner;
|
|
132
|
+
if (interactive) {
|
|
133
|
+
spinner = ora({ text: `Processing: ${relativePath}`, color: "cyan" }).start();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
if (await pathExists(outputDir)) {
|
|
138
|
+
if (spinner) spinner.text = `Clearing directory: ${outputDir}`;
|
|
139
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
if (spinner) spinner.text = `Creating directory: ${outputDir}`;
|
|
142
|
+
await mkdir(outputDir, { recursive: true });
|
|
143
|
+
|
|
144
|
+
if (spinner) spinner.text = `Copying spec to: ${specCopyPath}`;
|
|
145
|
+
await copyFile(fullPath, specCopyPath);
|
|
146
|
+
|
|
147
|
+
if (spinner) spinner.text = `Compiling: ${relativePath}`;
|
|
148
|
+
await runCommand("npx", [
|
|
149
|
+
"tsp",
|
|
150
|
+
"compile",
|
|
151
|
+
fullPath,
|
|
152
|
+
"--emit",
|
|
153
|
+
resolve(import.meta.dirname, "../.."),
|
|
154
|
+
"--config",
|
|
155
|
+
tspConfig,
|
|
156
|
+
"--output-dir",
|
|
157
|
+
outputDir,
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
if (spinner) spinner.text = `Formatting with Prettier: ${relativePath}`;
|
|
161
|
+
await runCommand("npx", ["prettier", outputDir, "--write"]);
|
|
162
|
+
|
|
163
|
+
if (build) {
|
|
164
|
+
if (spinner) spinner.text = `Building project: ${relativePath}`;
|
|
165
|
+
await runCommand("npm", ["run", "build"], { cwd: outputDir });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (spinner) {
|
|
169
|
+
spinner.succeed(`Finished processing: ${relativePath}`);
|
|
170
|
+
}
|
|
171
|
+
return { status: "succeeded", relativePath };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (spinner) {
|
|
174
|
+
spinner.fail(`Failed processing: ${relativePath}`);
|
|
175
|
+
}
|
|
176
|
+
const errorDetails = error.stdout || error.stderr || error.message;
|
|
177
|
+
|
|
178
|
+
// Write error details to a log file.
|
|
179
|
+
await mkdir(logDir, { recursive: true });
|
|
180
|
+
const logFilePath = join(logDir, `${basename(relativePath, ".tsp")}-error.log`);
|
|
181
|
+
await writeFile(logFilePath, errorDetails, "utf8");
|
|
182
|
+
|
|
183
|
+
if (interactive) {
|
|
184
|
+
const { action } = await inquirer.prompt([
|
|
185
|
+
{
|
|
186
|
+
type: "list",
|
|
187
|
+
name: "action",
|
|
188
|
+
message: `Processing failed for ${relativePath}. What would you like to do?`,
|
|
189
|
+
choices: [
|
|
190
|
+
{ name: "Retry", value: "retry" },
|
|
191
|
+
{ name: "Skip to next file", value: "next" },
|
|
192
|
+
{ name: "Abort processing", value: "abort" },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
if (action === "retry") {
|
|
198
|
+
if (spinner) spinner.start(`Retrying: ${relativePath}`);
|
|
199
|
+
return await processFile(file, options);
|
|
200
|
+
} else if (action === "next") {
|
|
201
|
+
console.log(pc.yellow(`Skipping: ${relativePath}`));
|
|
202
|
+
} else if (action === "abort") {
|
|
203
|
+
console.log(pc.red("Aborting processing."));
|
|
204
|
+
throw new Error("Processing aborted by user");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { status: "failed", relativePath, errorDetails };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Process all files.
|
|
212
|
+
async function processFiles(files, options) {
|
|
213
|
+
const { interactive } = options;
|
|
214
|
+
const succeeded = [];
|
|
215
|
+
const failed = [];
|
|
216
|
+
|
|
217
|
+
if (interactive) {
|
|
218
|
+
// Sequential processing so each spinner is visible.
|
|
219
|
+
for (const file of files) {
|
|
220
|
+
try {
|
|
221
|
+
const result = await processFile(file, options);
|
|
222
|
+
if (result.status === "succeeded") {
|
|
223
|
+
succeeded.push(result.relativePath);
|
|
224
|
+
} else {
|
|
225
|
+
failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Global progress spinner.
|
|
233
|
+
const total = files.length;
|
|
234
|
+
let completed = 0;
|
|
235
|
+
const globalSpinner = ora({ text: `Processing 0/${total} files...`, color: "cyan" }).start();
|
|
236
|
+
const limit = pLimit(4);
|
|
237
|
+
const tasks = files.map((file) =>
|
|
238
|
+
limit(() =>
|
|
239
|
+
processFile(file, options).then((result) => {
|
|
240
|
+
completed++;
|
|
241
|
+
globalSpinner.text = `Processing ${completed}/${total} files...`;
|
|
242
|
+
return result;
|
|
243
|
+
}),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
const results = await Promise.all(tasks);
|
|
247
|
+
globalSpinner.succeed(`Processed ${total} files`);
|
|
248
|
+
for (const result of results) {
|
|
249
|
+
if (result.status === "succeeded") {
|
|
250
|
+
succeeded.push(result.relativePath);
|
|
251
|
+
} else {
|
|
252
|
+
failed.push({ relativePath: result.relativePath, errorDetails: result.errorDetails });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log(pc.bold(pc.green("\nProcessing Complete:")));
|
|
258
|
+
console.log(pc.green(`Succeeded: ${succeeded.length}`));
|
|
259
|
+
console.log(pc.red(`Failed: ${failed.length}`));
|
|
260
|
+
|
|
261
|
+
if (failed.length > 0) {
|
|
262
|
+
console.log(pc.red("\nFailed Specs:"));
|
|
263
|
+
failed.forEach((f) => {
|
|
264
|
+
console.log(pc.red(` - ${f.relativePath}`));
|
|
265
|
+
});
|
|
266
|
+
console.log(pc.blue(`\nLogs available at: ${logDirRoot}`));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Ensure the log directory exists before writing the report.
|
|
270
|
+
await mkdir(logDirRoot, { recursive: true });
|
|
271
|
+
const report = [
|
|
272
|
+
"Succeeded Files:",
|
|
273
|
+
...succeeded.map((f) => ` - ${f}`),
|
|
274
|
+
"Failed Files:",
|
|
275
|
+
...failed.map((f) => ` - ${f.relativePath}\n Error: ${f.errorDetails}`),
|
|
276
|
+
].join("\n");
|
|
277
|
+
await writeFile(reportFilePath, report, "utf8");
|
|
278
|
+
console.log(pc.blue(`Report written to: ${reportFilePath}`));
|
|
279
|
+
}
|
|
280
|
+
// Main execution function
|
|
281
|
+
async function main() {
|
|
282
|
+
const startTime = process.hrtime.bigint(); // ✅ High precision time tracking
|
|
283
|
+
let exitCode = 0; // ✅ Track success/failure
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
await clearLogDirectory(); // ✅ Clear logs at the start
|
|
287
|
+
|
|
288
|
+
const ignoreList = await getIgnoreList();
|
|
289
|
+
const paths = argv._.length
|
|
290
|
+
? await processPaths(argv._, ignoreList)
|
|
291
|
+
: await processPaths(["."], ignoreList);
|
|
292
|
+
|
|
293
|
+
if (paths.length === 0) {
|
|
294
|
+
console.log(pc.yellow("⚠️ No files to process."));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await processFiles(paths, {
|
|
299
|
+
interactive: argv.interactive,
|
|
300
|
+
build: argv.build,
|
|
301
|
+
});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(pc.red(`❌ Fatal Error: ${error.message}`));
|
|
304
|
+
exitCode = 1; // ✅ Ensure graceful failure handling
|
|
305
|
+
} finally {
|
|
306
|
+
// ✅ Always log execution time before exit
|
|
307
|
+
const endTime = process.hrtime.bigint();
|
|
308
|
+
const duration = Number(endTime - startTime) / 1e9; // Convert nanoseconds to seconds
|
|
309
|
+
console.log(pc.blue(`⏱️ Total execution time: ${duration.toFixed(2)} seconds`));
|
|
310
|
+
|
|
311
|
+
process.exit(exitCode); // ✅ Ensures proper exit handling
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await main();
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation
|
|
2
|
+
// Licensed under the MIT license.
|
|
3
|
+
|
|
4
|
+
import { Module } from "../../src/ctx.js";
|
|
5
|
+
|
|
6
|
+
export let module: Module = undefined as any;
|
|
7
|
+
|
|
8
|
+
// prettier-ignore
|
|
9
|
+
const lines = [
|
|
10
|
+
"// Copyright (c) Microsoft Corporation",
|
|
11
|
+
"// Licensed under the MIT license.",
|
|
12
|
+
"",
|
|
13
|
+
"// #region Duration",
|
|
14
|
+
"",
|
|
15
|
+
"/**",
|
|
16
|
+
" * Regular expression for matching ISO8601 duration strings.",
|
|
17
|
+
" *",
|
|
18
|
+
" * Yields:",
|
|
19
|
+
" * - 0: the full match",
|
|
20
|
+
" * - 1: the sign (optional)",
|
|
21
|
+
" * - 2: years (optional)",
|
|
22
|
+
" * - 3: months (optional)",
|
|
23
|
+
" * - 4: weeks (optional)",
|
|
24
|
+
" * - 5: days (optional)",
|
|
25
|
+
" * - 6: hours (optional)",
|
|
26
|
+
" * - 7: minutes (optional)",
|
|
27
|
+
" * - 8: seconds (optional)",
|
|
28
|
+
" */",
|
|
29
|
+
"const ISO8601_DURATION_REGEX =",
|
|
30
|
+
" /^(-)?P(?:((?:\\d*[.,])?\\d+)Y)?(?:((?:\\d*[.,])?\\d+)M)?(?:((?:\\d*[.,])?\\d+)W)?(?:((?:\\d*[.,])?\\d+)D)?(?:T(?:((?:\\d*[.,])?\\d+)H)?(?:((?:\\d*[.,])?\\d+)M)?(?:((?:\\d*[.,])?\\d+)S)?)?$/;",
|
|
31
|
+
"",
|
|
32
|
+
"/**",
|
|
33
|
+
" * A duration of time, measured in years, months, weeks, days, hours, minutes, and seconds.",
|
|
34
|
+
" *",
|
|
35
|
+
" * The values may be fractional and are not normalized (e.g. 36 hours is not the same duration as 1 day and 12 hours",
|
|
36
|
+
" * when accounting for Daylight Saving Time changes or leap seconds).",
|
|
37
|
+
" *",
|
|
38
|
+
" * @see https://en.wikipedia.org/wiki/ISO_8601#Durations",
|
|
39
|
+
" */",
|
|
40
|
+
"export interface Duration {",
|
|
41
|
+
" /**",
|
|
42
|
+
" * \"+\" if the duration is positive, \"-\" if the duration is negative.",
|
|
43
|
+
" */",
|
|
44
|
+
" sign: \"+\" | \"-\";",
|
|
45
|
+
" /**",
|
|
46
|
+
" * The number of years in the duration.",
|
|
47
|
+
" */",
|
|
48
|
+
" years: number;",
|
|
49
|
+
" /**",
|
|
50
|
+
" * The number of months in the duration.",
|
|
51
|
+
" */",
|
|
52
|
+
" months: number;",
|
|
53
|
+
" /**",
|
|
54
|
+
" * The number of weeks in the duration.",
|
|
55
|
+
" */",
|
|
56
|
+
" weeks: number;",
|
|
57
|
+
" /**",
|
|
58
|
+
" * The number of days in the duration.",
|
|
59
|
+
" */",
|
|
60
|
+
" days: number;",
|
|
61
|
+
" /**",
|
|
62
|
+
" * The number of hours in the duration.",
|
|
63
|
+
" */",
|
|
64
|
+
" hours: number;",
|
|
65
|
+
" /**",
|
|
66
|
+
" * The number of minutes in the duration.",
|
|
67
|
+
" */",
|
|
68
|
+
" minutes: number;",
|
|
69
|
+
" /**",
|
|
70
|
+
" * The number of seconds in the duration.",
|
|
71
|
+
" */",
|
|
72
|
+
" seconds: number;",
|
|
73
|
+
"}",
|
|
74
|
+
"",
|
|
75
|
+
"export const Duration = Object.freeze({",
|
|
76
|
+
" /**",
|
|
77
|
+
" * Parses an ISO8601 duration string into an object.",
|
|
78
|
+
" *",
|
|
79
|
+
" * @see https://en.wikipedia.org/wiki/ISO_8601#Durations",
|
|
80
|
+
" *",
|
|
81
|
+
" * @param duration - the duration string to parse",
|
|
82
|
+
" * @returns an object containing the parsed duration",
|
|
83
|
+
" */",
|
|
84
|
+
" parseISO8601(duration: string, maxLength: number = 100): Duration {",
|
|
85
|
+
" duration = duration.trim();",
|
|
86
|
+
" if (duration.length > maxLength)",
|
|
87
|
+
" throw new Error(`ISO8601 duration string is too long: ${duration}`);",
|
|
88
|
+
"",
|
|
89
|
+
" const match = duration.match(ISO8601_DURATION_REGEX);",
|
|
90
|
+
"",
|
|
91
|
+
" if (!match) throw new Error(`Invalid ISO8601 duration: ${duration}`);",
|
|
92
|
+
"",
|
|
93
|
+
" return {",
|
|
94
|
+
" sign: match[1] === undefined ? \"+\" : (match[1] as Duration[\"sign\"]),",
|
|
95
|
+
" years: parseFloatNormal(match[2]),",
|
|
96
|
+
" months: parseFloatNormal(match[3]),",
|
|
97
|
+
" weeks: parseFloatNormal(match[4]),",
|
|
98
|
+
" days: parseFloatNormal(match[5]),",
|
|
99
|
+
" hours: parseFloatNormal(match[6]),",
|
|
100
|
+
" minutes: parseFloatNormal(match[7]),",
|
|
101
|
+
" seconds: parseFloatNormal(match[8]),",
|
|
102
|
+
" };",
|
|
103
|
+
"",
|
|
104
|
+
" function parseFloatNormal(match: string | undefined): number {",
|
|
105
|
+
" if (match === undefined) return 0;",
|
|
106
|
+
"",
|
|
107
|
+
" const normalized = match.replace(\",\", \".\");",
|
|
108
|
+
"",
|
|
109
|
+
" const parsed = parseFloat(normalized);",
|
|
110
|
+
"",
|
|
111
|
+
" if (isNaN(parsed))",
|
|
112
|
+
" throw new Error(`Unreachable: Invalid number in ISO8601 duration string: ${match}`);",
|
|
113
|
+
"",
|
|
114
|
+
" return parsed;",
|
|
115
|
+
" }",
|
|
116
|
+
" },",
|
|
117
|
+
" /**",
|
|
118
|
+
" * Writes a Duration to an ISO8601 duration string.",
|
|
119
|
+
" *",
|
|
120
|
+
" * @see https://en.wikipedia.org/wiki/ISO_8601#Durations",
|
|
121
|
+
" *",
|
|
122
|
+
" * @param duration - the duration to write to a string",
|
|
123
|
+
" * @returns a string in ISO8601 duration format",
|
|
124
|
+
" */",
|
|
125
|
+
" toISO8601(duration: Duration): string {",
|
|
126
|
+
" const sign = duration.sign === \"+\" ? \"\" : \"-\";",
|
|
127
|
+
"",
|
|
128
|
+
" const years =",
|
|
129
|
+
" duration.years !== 0 && !isNaN(Number(duration.years)) ? `${duration.years}Y` : \"\";",
|
|
130
|
+
" const months =",
|
|
131
|
+
" duration.months !== 0 && !isNaN(Number(duration.months)) ? `${duration.months}M` : \"\";",
|
|
132
|
+
" const weeks =",
|
|
133
|
+
" duration.weeks !== 0 && !isNaN(Number(duration.weeks)) ? `${duration.weeks}W` : \"\";",
|
|
134
|
+
" const days = duration.days !== 0 && !isNaN(Number(duration.days)) ? `${duration.days}D` : \"\";",
|
|
135
|
+
"",
|
|
136
|
+
" let time = \"\";",
|
|
137
|
+
"",
|
|
138
|
+
" const _hours = duration.hours !== 0 && !isNaN(Number(duration.hours));",
|
|
139
|
+
" const _minutes = duration.minutes !== 0 && !isNaN(Number(duration.minutes));",
|
|
140
|
+
" const _seconds = duration.seconds !== 0 && !isNaN(Number(duration.seconds));",
|
|
141
|
+
"",
|
|
142
|
+
" if (_hours || _minutes || _seconds) {",
|
|
143
|
+
" const hours = _hours ? `${duration.hours}H` : \"\";",
|
|
144
|
+
" const minutes = _minutes ? `${duration.minutes}M` : \"\";",
|
|
145
|
+
" const seconds = _seconds ? `${duration.seconds}S` : \"\";",
|
|
146
|
+
"",
|
|
147
|
+
" time = `T${hours}${minutes}${seconds}`;",
|
|
148
|
+
" }",
|
|
149
|
+
"",
|
|
150
|
+
" return `${sign}P${years}${months}${weeks}${days}${time}`;",
|
|
151
|
+
" },",
|
|
152
|
+
"",
|
|
153
|
+
" /**",
|
|
154
|
+
" * Gets the total number of seconds in a duration.",
|
|
155
|
+
" *",
|
|
156
|
+
" * This method will throw an Error if the duration contains any years, months, weeks, or days, as those require a reference",
|
|
157
|
+
" * point to calculate the total number of seconds.",
|
|
158
|
+
" *",
|
|
159
|
+
" * WARNING: If the total number of seconds is larger than the maximum safe integer in JavaScript, this method will",
|
|
160
|
+
" * lose precision. @see Duration.totalSecondsBigInt for a BigInt alternative.",
|
|
161
|
+
" *",
|
|
162
|
+
" * @param duration - the duration to calculate the total number of seconds for",
|
|
163
|
+
" * @returns the total number of seconds in the duration",
|
|
164
|
+
" */",
|
|
165
|
+
" totalSeconds(duration: Duration): number {",
|
|
166
|
+
" if (",
|
|
167
|
+
" duration.years !== 0 ||",
|
|
168
|
+
" duration.months !== 0 ||",
|
|
169
|
+
" duration.weeks !== 0 ||",
|
|
170
|
+
" duration.days !== 0",
|
|
171
|
+
" ) {",
|
|
172
|
+
" throw new Error(",
|
|
173
|
+
" \"Cannot calculate total seconds for a duration with years, months, weeks, or days.\",",
|
|
174
|
+
" );",
|
|
175
|
+
" }",
|
|
176
|
+
"",
|
|
177
|
+
" return (",
|
|
178
|
+
" duration.seconds +",
|
|
179
|
+
" duration.minutes * 60 +",
|
|
180
|
+
" duration.hours * 60 * 60 +",
|
|
181
|
+
" duration.weeks * 7 * 24 * 60 * 60",
|
|
182
|
+
" );",
|
|
183
|
+
" },",
|
|
184
|
+
"",
|
|
185
|
+
" /**",
|
|
186
|
+
" * Gets the total number of seconds in a duration.",
|
|
187
|
+
" *",
|
|
188
|
+
" * This method will throw an Error if the duration contains any years, months, weeks, or days, as those require a reference",
|
|
189
|
+
" * point to calculate the total number of seconds. It will also throw an error if any of the components are not integers.",
|
|
190
|
+
" *",
|
|
191
|
+
" * @param duration - the duration to calculate the total number of seconds for",
|
|
192
|
+
" * @returns the total number of seconds in the duration",
|
|
193
|
+
" */",
|
|
194
|
+
" totalSecondsBigInt(duration: Duration): bigint {",
|
|
195
|
+
" if (",
|
|
196
|
+
" duration.years !== 0 ||",
|
|
197
|
+
" duration.months !== 0 ||",
|
|
198
|
+
" duration.weeks !== 0 ||",
|
|
199
|
+
" duration.days !== 0",
|
|
200
|
+
" ) {",
|
|
201
|
+
" throw new Error(",
|
|
202
|
+
" \"Cannot calculate total seconds for a duration with years, months, weeks, or days.\",",
|
|
203
|
+
" );",
|
|
204
|
+
" }",
|
|
205
|
+
"",
|
|
206
|
+
" if (",
|
|
207
|
+
" !Number.isInteger(duration.seconds) ||",
|
|
208
|
+
" !Number.isInteger(duration.minutes) ||",
|
|
209
|
+
" !Number.isInteger(duration.hours) ||",
|
|
210
|
+
" !Number.isInteger(duration.weeks)",
|
|
211
|
+
" ) {",
|
|
212
|
+
" throw new Error(",
|
|
213
|
+
" \"Cannot calculate total seconds as a BigInt for a duration with non-integer components.\",",
|
|
214
|
+
" );",
|
|
215
|
+
" }",
|
|
216
|
+
"",
|
|
217
|
+
" return (",
|
|
218
|
+
" BigInt(duration.seconds) +",
|
|
219
|
+
" BigInt(duration.minutes) * 60n +",
|
|
220
|
+
" BigInt(duration.hours) * 60n * 60n +",
|
|
221
|
+
" BigInt(duration.weeks) * 7n * 24n * 60n * 60n",
|
|
222
|
+
" );",
|
|
223
|
+
" },",
|
|
224
|
+
"",
|
|
225
|
+
" /**",
|
|
226
|
+
" * Creates a duration from a total number of seconds.",
|
|
227
|
+
" *",
|
|
228
|
+
" * The result is not normalized, so it will only contain a seconds field.",
|
|
229
|
+
" */",
|
|
230
|
+
" fromTotalSeconds(seconds: number): Duration {",
|
|
231
|
+
" return {",
|
|
232
|
+
" sign: seconds < 0 ? \"-\" : \"+\",",
|
|
233
|
+
" years: 0,",
|
|
234
|
+
" months: 0,",
|
|
235
|
+
" weeks: 0,",
|
|
236
|
+
" days: 0,",
|
|
237
|
+
" hours: 0,",
|
|
238
|
+
" minutes: 0,",
|
|
239
|
+
" seconds: Math.abs(seconds),",
|
|
240
|
+
" };",
|
|
241
|
+
" },",
|
|
242
|
+
"});",
|
|
243
|
+
"",
|
|
244
|
+
"// #endregion",
|
|
245
|
+
"",
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
export async function createModule(parent: Module): Promise<Module> {
|
|
249
|
+
if (module) return module;
|
|
250
|
+
|
|
251
|
+
module = {
|
|
252
|
+
name: "datetime",
|
|
253
|
+
cursor: parent.cursor.enter("datetime"),
|
|
254
|
+
imports: [],
|
|
255
|
+
declarations: [],
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
module.declarations.push(lines);
|
|
259
|
+
|
|
260
|
+
parent.declarations.push(module);
|
|
261
|
+
|
|
262
|
+
return module;
|
|
263
|
+
}
|
|
@@ -16,6 +16,7 @@ export async function createModule(parent: Module): Promise<Module> {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
// Child modules
|
|
19
|
+
await import("./datetime.js").then((m) => m.createModule(module));
|
|
19
20
|
await import("./header.js").then((m) => m.createModule(module));
|
|
20
21
|
await import("./http.js").then((m) => m.createModule(module));
|
|
21
22
|
await import("./multipart.js").then((m) => m.createModule(module));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typespec/http-server-js",
|
|
3
|
-
"version": "0.58.0-alpha.12-dev.
|
|
3
|
+
"version": "0.58.0-alpha.12-dev.2",
|
|
4
4
|
"author": "Microsoft Corporation",
|
|
5
5
|
"description": "TypeSpec HTTP server code generator for JavaScript",
|
|
6
6
|
"homepage": "https://github.com/microsoft/typespec",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"hsj-scaffold": "./dist/src/scripts/scaffold/bin.mjs"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@typespec/compiler": "^0.67.
|
|
34
|
-
"@typespec/http": "^0.67.
|
|
35
|
-
"@typespec/openapi3": "^0.67.
|
|
33
|
+
"@typespec/compiler": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
34
|
+
"@typespec/http": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
35
|
+
"@typespec/openapi3": "^0.67.1 || >=0.68.0-dev <0.68.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@typespec/openapi3": {
|
|
@@ -45,14 +45,25 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "~22.13.9",
|
|
48
|
-
"@typespec/compiler": "^0.67.
|
|
49
|
-
"@typespec/http": "^0.67.
|
|
50
|
-
"@typespec/
|
|
48
|
+
"@typespec/compiler": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
49
|
+
"@typespec/http": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
50
|
+
"@typespec/http-specs": "^0.1.0-alpha.15 || >=0.1.0-alpha.16-dev <0.1.0-alpha.16",
|
|
51
|
+
"@typespec/internal-build-utils": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
52
|
+
"@typespec/openapi3": "^0.67.1 || >=0.68.0-dev <0.68.0",
|
|
53
|
+
"@typespec/spector": "^0.1.0-alpha.9 || >=0.1.0-alpha.10-dev <0.1.0-alpha.10",
|
|
51
54
|
"@vitest/coverage-v8": "^3.0.7",
|
|
52
55
|
"@vitest/ui": "^3.0.7",
|
|
56
|
+
"fs-extra": "^11.2.0",
|
|
57
|
+
"globby": "~14.1.0",
|
|
58
|
+
"inquirer": "^12.2.0",
|
|
59
|
+
"ora": "^8.1.1",
|
|
60
|
+
"p-limit": "^6.2.0",
|
|
61
|
+
"pathe": "^2.0.3",
|
|
62
|
+
"picocolors": "~1.1.1",
|
|
53
63
|
"tsx": "^4.19.3",
|
|
54
64
|
"typescript": "~5.8.2",
|
|
55
|
-
"vitest": "^3.0.7"
|
|
65
|
+
"vitest": "^3.0.7",
|
|
66
|
+
"yargs": "~17.7.2"
|
|
56
67
|
},
|
|
57
68
|
"scripts": {
|
|
58
69
|
"clean": "rimraf ./dist ./temp",
|
|
@@ -66,6 +77,9 @@
|
|
|
66
77
|
"test:ci": "vitest run --coverage --reporter=junit --reporter=default",
|
|
67
78
|
"lint": "eslint . --max-warnings=0",
|
|
68
79
|
"lint:fix": "eslint . --fix",
|
|
69
|
-
"regen-docs": "echo Doc generation disabled for this package."
|
|
80
|
+
"regen-docs": "echo Doc generation disabled for this package.",
|
|
81
|
+
"test:e2e": "npm run emit:e2e && npm run run:e2e",
|
|
82
|
+
"emit:e2e": "node eng/scripts/emit-e2e.js",
|
|
83
|
+
"run:e2e": "vitest run --config ./vitest.config.e2e.js"
|
|
70
84
|
}
|
|
71
85
|
}
|
package/src/common/reference.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function emitTypeReference(
|
|
|
70
70
|
switch (type.kind) {
|
|
71
71
|
case "Scalar":
|
|
72
72
|
// Get the scalar and return it directly, as it is a primitive.
|
|
73
|
-
return getJsScalar(ctx
|
|
73
|
+
return getJsScalar(ctx, module, type, position).type;
|
|
74
74
|
case "Model": {
|
|
75
75
|
// First handle arrays.
|
|
76
76
|
if (isArrayModelType(ctx.program, type)) {
|