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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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-44-11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/compiler"
---

Export missing type DiscriminatedUnionLegacy
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
5 changes: 5 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
######################
/packages/http-client-python/ @iscai-msft @tadelesh @msyyc

######################
# JavaScript
######################
/packages/http-client-js/ @joheredi

######################
# Emiter Shared
######################
Expand Down
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,12 @@ 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
Expand Up @@ -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"
}
Expand Down
2 changes: 2 additions & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions eng/common/config/area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const AreaPaths: Record<keyof typeof AreaLabels, string[]> = {
"emitter:client:csharp": ["packages/http-client-csharp/"],
"emitter:client:java": ["packages/http-client-java/"],
"emitter:client:python": ["packages/http-client-python/"],
"emitter:client:js": ["packages/http-client-js/"],
"emitter:graphql": ["packages/graphql/"],
"emitter:json-schema": ["packages/json-schema/"],
"emitter:protobuf": ["packages/protobuf/"],
Expand Down
4 changes: 4 additions & 0 deletions eng/common/config/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export const AreaLabels = defineLabels({
color: "e1b300",
description: "Issue for the Python client emitter: @typespec/http-client-python",
},
"emitter:client:js": {
color: "e1b300",
description: "Issue for the JavaScript client emitter: @typespec/http-client-js",
},
"emitter:graphql": {
color: "957300",
description: "Issues for @typespec/graphql emitter",
Expand Down
4 changes: 4 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -138,6 +139,7 @@ export default tsEslint.config(
{
ignores: [
"**/dist/**/*",
"**/dist-test/**/*",
"**/.temp/**/*",
"**/temp/**/*",
"**/generated-defs/*",
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion packages/compiler/src/core/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export { DiscriminatedUnion, getDiscriminatedUnion } from "./discriminator-utils.js";
export {
DiscriminatedUnion,
DiscriminatedUnionLegacy,
getDiscriminatedUnion,
} from "./discriminator-utils.js";
export { getLocationContext } from "./location-context.js";
export * from "./operation-utils.js";
export * from "./path-interpolation.js";
Expand Down
35 changes: 34 additions & 1 deletion packages/compiler/src/experimental/typekit/kits/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -198,9 +209,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: 2 additions & 0 deletions packages/emitter-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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\"",
Expand Down
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
Expand Up @@ -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);
Expand Down
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";
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading