Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing @typespec/http-client-js #6178

Merged
merged 17 commits into from
Mar 5, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/feature-js-emitter-2025-1-27-5-45-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/http-client"
---

Improvements to HttpClient context provider
7 changes: 7 additions & 0 deletions .chronus/changes/feature-js-emitter-2025-1-27-6-2-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/emitter-framework"
---

Improvements on the TestHarness
7 changes: 7 additions & 0 deletions .chronus/changes/feature-js-emitter-2025-2-4-18-12-50.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- typespec-vscode
---

Update references to JS emitter
8 changes: 8 additions & 0 deletions .chronus/changes/feature-js-emitter-2025-2-4-18-51-17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
- "@typespec/http-client-js"
---

Introducing the JS Http Client emitter
5 changes: 5 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -19,6 +19,11 @@
######################
/packages/http-client-python/ @iscai-msft @tadelesh @msyyc @timotheeguerin @lmazuel

######################
# JavaScript
######################
/packages/http-client-js/ @joheredi @timotheeguerin @timovv @qiaozha @MaryGao @xirzec @bterlson @markcowl @allenjzhang @witemple-msft @chrisradek @AlitzelMendez

######################
# Emiter Shared
######################
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -61,5 +61,11 @@ packages/http-client-csharp/generator/TestProjects/**/tspCodeModel.json
packages/http-client-java/generator/http-client-generator-test/src/main/**/*.json
packages/http-client-java/generator/http-client-generator-clientcore-test/src/main/**/*.json

# auto generated http-client-js files
packages/http-client-js/test/e2e/generated/

# built test files for http-client-js
packages/http-client-js/dist-test

.gitattributes
CODEOWNERS
12 changes: 5 additions & 7 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -21,13 +21,11 @@
}
},
{
"files": ["packages/http-client/**/*.tsx"],
"options": {
"parser": "alloy-ts"
}
},
{
"files": ["packages/emitter-framework/**/*.tsx"],
"files": [
"packages/http-client-js/**/*.tsx",
"packages/http-client/**/*.tsx",
"packages/emitter-framework/**/*.tsx"
],
"options": {
"parser": "alloy-ts"
}
2 changes: 2 additions & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
@@ -279,6 +279,8 @@ ignorePaths:
- packages/compiler/test/formatter/scenarios/**
- packages/http-client-java/generator/http-client-generator-test/**
- packages/http-client-java/generator/http-client-generator-clientcore-test/**
- packages/http-client-js/test/e2e/**
- packages/http-client-js/sample/**
- pnpm-lock.yaml
- "**/*.mp4"
- "**/*.plist"
4 changes: 4 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -120,6 +120,7 @@ const testFilesConfig = tsEslint.config({
const jsxFilesConfig = tsEslint.config({
files: ["**/*.tsx"],
plugins: { "react-hooks": reactHooks },
ignores: ["**/packages/http-client-js/**/*"],
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
@@ -138,6 +139,7 @@ export default tsEslint.config(
{
ignores: [
"**/dist/**/*",
"**/dist-test/**/*",
"**/.temp/**/*",
"**/temp/**/*",
"**/generated-defs/*",
@@ -146,6 +148,8 @@ export default tsEslint.config(
"**/.docusaurus/**/*",
"website/src/assets/**/*",
"packages/compiler/templates/**/*", // Ignore the templates which might have invalid code and not follow exactly our rules.
"packages/http-client-js/test/e2e/generated", // Ignore the generated http client
"packages/http-client-js/sample/output/**/*", // Ignore the generated http client
"**/venv/**/*", // Ignore python virtual env
"**/.vscode-test-web/**/*", // Ignore VSCode test web project
// TODO: enable
2 changes: 1 addition & 1 deletion packages/compiler/.scripts/build-init-templates.ts
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ const builtInTemplates: Record<string, InitTemplate> = {
"emitter-output-dir": "{output-dir}/clients/java",
},
},
"@azure-tools/typespec-ts": {
"@typespec/http-client-js": {
label: "JavaScript client",
options: {
"emitter-output-dir": "{output-dir}/clients/js",
1 change: 0 additions & 1 deletion packages/compiler/src/core/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ export {
DiscriminatedUnion,
DiscriminatedUnionLegacy,
getDiscriminatedUnion,
getDiscriminatedUnionFromInheritance,
} from "./discriminator-utils.js";
export { getLocationContext } from "./location-context.js";
export * from "./operation-utils.js";
35 changes: 34 additions & 1 deletion packages/compiler/src/experimental/typekit/kits/model.ts
Original file line number Diff line number Diff line change
@@ -105,6 +105,17 @@ export interface ModelKit {
model: Model,
options?: { includeExtended?: boolean },
): RekeyableMap<string, ModelProperty>;
/**
* Get the record representing additional properties, if there are additional properties.
* This method checks for additional properties in the following cases:
* 1. If the model is a Record type.
* 2. If the model extends a Record type.
* 3. If the model spreads a Record type.
*
* @param model The model to get the additional properties type of.
* @returns The record representing additional properties, or undefined if there are none.
*/
getAdditionalPropertiesRecord(model: Model): Model | undefined;
}

interface TypekitExtension {
@@ -197,9 +208,31 @@ defineKit<TypekitExtension>({
base = base.baseModel;
}
}

// TODO: Add Spread?
return properties;
},
getAdditionalPropertiesRecord(model) {
// model MyModel is Record<> {} should be model with additional properties
if (this.model.is(model) && model.sourceModel && this.record.is(model.sourceModel)) {
return model.sourceModel;
}

// model MyModel extends Record<> {} should be model with additional properties
if (model.baseModel && this.record.is(model.baseModel)) {
return model.baseModel;
}

// model MyModel { ...Record<>} should be model with additional properties
const spread = this.model.getSpreadType(model);
if (spread && this.model.is(spread) && this.record.is(spread)) {
return spread;
}

if (model.baseModel) {
return this.model.getAdditionalPropertiesRecord(model.baseModel);
}

return undefined;
},
},
});
2 changes: 1 addition & 1 deletion packages/compiler/templates/scaffolding.json
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@
"emitter-output-dir": "{output-dir}/clients/java"
}
},
"@azure-tools/typespec-ts": {
"@typespec/http-client-js": {
"label": "JavaScript client",
"options": {
"emitter-output-dir": "{output-dir}/clients/js"
2 changes: 2 additions & 0 deletions packages/emitter-framework/package.json
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@
"scripts": {
"build-src": "babel src -d dist/src --extensions .ts,.tsx",
"build": "tsc -p . && npm run build-src",
"clean": "rimraf ./dist",
"format": "prettier . --write",
"watch-src": "babel src -d dist/src --extensions .ts,.tsx --watch",
"watch-tsc": "tsc -p . --watch",
"watch": "concurrently --kill-others \"npm run watch-tsc\" \"npm run watch-src\"",
52 changes: 25 additions & 27 deletions packages/emitter-framework/src/testing/scenario-test/harness.ts
Original file line number Diff line number Diff line change
@@ -271,36 +271,34 @@ function parseScenario(
);

for (const line of rawLines) {
if (line.startsWith("```")) {
if (currentCodeBlock) {
// Close the code block
scenario.lines.push(currentCodeBlock);
if (!isTestCodeBlock(currentCodeBlock)) {
scenario.specBlock.content = currentCodeBlock.content;
} else {
for (const [template, fn] of Object.entries(outputCodeBlockTypes)) {
const templateRegex = new RegExp(
"^" + template.replace(/\{(\w+)\}/g, "(?<$1>[^\\s]+)") + "$",
);

const match = currentCodeBlock.heading.match(templateRegex);
if (match) {
currentCodeBlock.matchedTemplate = {
template,
fn,
namedArgs: match.groups ?? null,
};
break;
}
if (line.startsWith("```") && currentCodeBlock) {
// Close the code block
scenario.lines.push(currentCodeBlock);
if (!isTestCodeBlock(currentCodeBlock)) {
scenario.specBlock.content = currentCodeBlock.content;
} else {
for (const [template, fn] of Object.entries(outputCodeBlockTypes)) {
const templateRegex = new RegExp(
"^" + template.replace(/\{(\w+)\}/g, "(?<$1>[^\\s]+)") + "$",
);

const match = currentCodeBlock.heading.match(templateRegex);
if (match) {
currentCodeBlock.matchedTemplate = {
template,
fn,
namedArgs: match.groups ?? null,
};
break;
}
scenario.testBlocks.push(currentCodeBlock);
}
currentCodeBlock = null;
} else {
const codeBlockKind = line.includes("tsp") || line.includes("typespec") ? "spec" : "test";
// Start a new code block
currentCodeBlock = { kind: codeBlockKind, heading: line.substring(3), content: [] };
scenario.testBlocks.push(currentCodeBlock);
}
currentCodeBlock = null;
} else if (line.startsWith("```")) {
const codeBlockKind = line.includes("tsp") || line.includes("typespec") ? "spec" : "test";
// Start a new code block
currentCodeBlock = { kind: codeBlockKind, heading: line.substring(3), content: [] };
} else if (currentCodeBlock) {
// Append to code block content
currentCodeBlock.content.push(line);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Children, code, refkey as getRefkey, mapJoin } from "@alloy-js/core";
import { Children, refkey as getRefkey, mapJoin } from "@alloy-js/core";
import * as ts from "@alloy-js/typescript";
import { Interface, Model, ModelProperty, Operation, RekeyableMap } from "@typespec/compiler";
import { $ } from "@typespec/compiler/experimental/typekit";
@@ -84,14 +84,13 @@ function getExtendsType(type: Model | Interface): Children | undefined {

const extending: Children[] = [];

const recordExtends = code`Record<string, unknown>`;

if (type.baseModel) {
if ($.array.is(type.baseModel)) {
extending.push(<TypeExpression type={type.baseModel} />);
} else if ($.record.is(type.baseModel)) {
extending.push(recordExtends);
// When extending a record we need to override the element type to be unknown to avoid type errors
// Here we are in the additional properties land.
// Instead of extending we need to create an envelope property
// do nothing here.
} else {
extending.push(getRefkey(type.baseModel));
}
@@ -120,11 +119,15 @@ function membersFromType(type: Model | Interface) {
let typeMembers: RekeyableMap<string, ModelProperty | Operation> | undefined;
if ($.model.is(type)) {
typeMembers = $.model.getProperties(type);
const spread = $.model.getSpreadType(type);
if (spread && $.model.is(spread) && $.record.is(spread)) {
const additionalProperties = $.model.getAdditionalPropertiesRecord(type);
if (additionalProperties) {
typeMembers.set(
"additionalProperties",
$.modelProperty.create({ name: "additionalProperties", optional: true, type: spread }),
$.modelProperty.create({
name: "additionalProperties",
optional: true,
type: additionalProperties,
}),
);
}
} else {
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ export function InterfaceMember({ type, optional }: InterfaceMemberProps) {
const name = namer.getName(type.name, "object-member-getter");

if ($.modelProperty.is(type)) {
const optionality = (type.optional ?? optional) ? "?" : "";
const optionality = optional === true || type.optional === true ? "?" : "";

if (isNeverType(type.type)) {
return null;
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.