diff --git a/.github/workflows/coder.yaml b/.github/workflows/coder.yaml index d6fed1b850567..87fd447e1d484 100644 --- a/.github/workflows/coder.yaml +++ b/.github/workflows/coder.yaml @@ -76,6 +76,21 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + + - name: Cache Node + id: cache-node + uses: actions/cache@v3 + with: + path: | + **/node_modules + .eslintcache + key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + js-${{ runner.os }}- + + - name: Install node_modules + run: ./scripts/yarn_install.sh + - name: Install Protoc uses: arduino/setup-protoc@v1 with: diff --git a/Makefile b/Makefile index c4025bcb16b8c..74cb5073ef206 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,11 @@ coderd/database/generate: fmt/sql coderd/database/dump.sql $(wildcard coderd/dat coderd/database/generate.sh .PHONY: coderd/database/generate +apitypings/generate: site/src/api/types.ts + go run scripts/apitypings/main.go > site/src/api/types-generated.ts + cd site && yarn run format:types +.PHONY: apitypings/generate + fmt/prettier: @echo "--- prettier" # Avoid writing files in CI to reduce file write activity @@ -48,7 +53,7 @@ fmt/terraform: $(wildcard *.tf) fmt: fmt/prettier fmt/sql fmt/terraform .PHONY: fmt -gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto +gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto apitypings/generate .PHONY: gen install: bin diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go new file mode 100644 index 0000000000000..c6d575c08ca94 --- /dev/null +++ b/scripts/apitypings/main.go @@ -0,0 +1,200 @@ +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" +) + +const ( + baseDir = "./codersdk" +) + +func main() { + err := run() + if err != nil { + log.Fatal(err) + } +} + +func run() error { + var ( + astFiles []*ast.File + enums = make(map[string]string) + ) + fset := token.NewFileSet() + entries, err := os.ReadDir(baseDir) + if err != nil { + return xerrors.Errorf("reading dir %s: %w", baseDir, err) + } + + // loop each file in directory + for _, entry := range entries { + astFile, err := parser.ParseFile(fset, filepath.Join(baseDir, entry.Name()), nil, 0) + if err != nil { + return xerrors.Errorf("parsing file %s: %w", filepath.Join(baseDir, entry.Name()), err) + } + + astFiles = append(astFiles, astFile) + } + + // TypeSpec case for structs and type alias + loopSpecs(astFiles, func(spec ast.Spec) { + pos := fset.Position(spec.Pos()) + s, ok := spec.(*ast.TypeSpec) + if !ok { + return + } + out, err := handleTypeSpec(s, pos, enums) + if err != nil { + return + } + + _, _ = fmt.Printf(out) + }) + + // ValueSpec case for loading type alias values into the enum map + loopSpecs(astFiles, func(spec ast.Spec) { + s, ok := spec.(*ast.ValueSpec) + if !ok { + return + } + handleValueSpec(s, enums) + }) + + // write each type alias declaration with possible values + for _, v := range enums { + _, _ = fmt.Printf("%s\n", v) + } + + return nil +} + +func loopSpecs(astFiles []*ast.File, fn func(spec ast.Spec)) { + for _, astFile := range astFiles { + // loop each declaration in file + for _, node := range astFile.Decls { + genDecl, ok := node.(*ast.GenDecl) + if !ok { + continue + } + for _, spec := range genDecl.Specs { + fn(spec) + } + } + } +} + +func handleTypeSpec(typeSpec *ast.TypeSpec, pos token.Position, enums map[string]string) (string, error) { + jsonFields := 0 + s := fmt.Sprintf("// From %s.\n", pos.String()) + switch t := typeSpec.Type.(type) { + // Struct declaration + case *ast.StructType: + s = fmt.Sprintf("%sexport interface %s {\n", s, typeSpec.Name.Name) + for _, field := range t.Fields.List { + i, optional, err := getIdent(field.Type) + if err != nil { + continue + } + + fieldType := toTsType(i.Name) + if fieldType == "" { + continue + } + + fieldName := toJSONField(field) + if fieldName == "" { + continue + } + + s = fmt.Sprintf("%s readonly %s%s: %s\n", s, fieldName, optional, fieldType) + jsonFields++ + } + + // Do not print struct if it has no json fields + if jsonFields == 0 { + return "", xerrors.New("no json fields") + } + + return fmt.Sprintf("%s}\n\n", s), nil + // Type alias declaration + case *ast.Ident: + // save type declaration to map of types + // later we come back and add union types to this declaration + enums[typeSpec.Name.Name] = fmt.Sprintf("%sexport type %s = \n", s, typeSpec.Name.Name) + return "", xerrors.New("enums are not printed at this stage") + default: + return "", xerrors.New("not struct or alias") + } +} + +func handleValueSpec(valueSpec *ast.ValueSpec, enums map[string]string) { + valueValue := "" + i, ok := valueSpec.Type.(*ast.Ident) + if !ok { + return + } + valueType := i.Name + + for _, value := range valueSpec.Values { + bl, ok := value.(*ast.BasicLit) + if !ok { + return + } + valueValue = bl.Value + break + } + + enums[valueType] = fmt.Sprintf("%s | %s\n", enums[valueType], valueValue) +} + +func getIdent(e ast.Expr) (*ast.Ident, string, error) { + switch t := e.(type) { + case *ast.Ident: + return t, "", nil + case *ast.StarExpr: + i, ok := t.X.(*ast.Ident) + if !ok { + return nil, "", xerrors.New("failed to cast star expr to indent") + } + return i, "?", nil + default: + return nil, "", xerrors.New("unknown expr type") + } +} + +func toTsType(fieldType string) string { + switch fieldType { + case "bool": + return "boolean" + case "uint64", "uint32", "float64": + return "number" + } + + return fieldType +} + +func toJSONField(field *ast.Field) string { + if field.Tag != nil && field.Tag.Value != "" { + fieldName := strings.Trim(field.Tag.Value, "`") + for _, pair := range strings.Split(fieldName, " ") { + if strings.Contains(pair, `json:`) { + fieldName := strings.TrimPrefix(pair, `json:`) + fieldName = strings.Trim(fieldName, `"`) + fieldName = strings.Split(fieldName, ",")[0] + + return fieldName + } + } + } + + return "" +} diff --git a/site/package.json b/site/package.json index 73732708f83b6..42003ff643e1f 100644 --- a/site/package.json +++ b/site/package.json @@ -10,6 +10,7 @@ "chromatic": "chromatic", "dev": "webpack-dev-server --config=webpack.dev.ts", "format:check": "prettier --check '**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'", + "format:types": "prettier --write 'src/api/types-generated.ts'", "format:write": "prettier --write '**/*.{css,html,js,json,jsx,md,ts,tsx,yaml,yml}'", "lint": "jest --selectProjects lint", "lint:fix": "FIX=true yarn lint", diff --git a/site/src/api/types-generated.ts b/site/src/api/types-generated.ts new file mode 100644 index 0000000000000..660d60d01f8ca --- /dev/null +++ b/site/src/api/types-generated.ts @@ -0,0 +1,220 @@ +// From codersdk/buildinfo.go:10:6. +export interface BuildInfoResponse { + readonly external_url: string + readonly version: string +} + +// From codersdk/files.go:16:6. +export interface UploadResponse { + readonly hash: string +} + +// From codersdk/gitsshkey.go:14:6. +export interface GitSSHKey { + readonly public_key: string +} + +// From codersdk/gitsshkey.go:21:6. +export interface AgentGitSSHKey { + readonly private_key: string +} + +// From codersdk/organizations.go:17:6. +export interface Organization { + readonly name: string +} + +// From codersdk/organizations.go:25:6. +export interface CreateTemplateVersionRequest { + readonly storage_source: string +} + +// From codersdk/organizations.go:38:6. +export interface CreateTemplateRequest { + readonly name: string +} + +// From codersdk/parameters.go:26:6. +export interface Parameter { + readonly scope: ParameterScope + readonly name: string +} + +// From codersdk/parameters.go:38:6. +export interface CreateParameterRequest { + readonly name: string + readonly source_value: string +} + +// From codersdk/provisionerdaemons.go:37:6. +export interface ProvisionerJob { + readonly error: string + readonly status: ProvisionerJobStatus +} + +// From codersdk/provisionerdaemons.go:47:6. +export interface ProvisionerJobLog { + readonly stage: string + readonly output: string +} + +// From codersdk/templates.go:17:6. +export interface Template { + readonly name: string + readonly workspace_owner_count: number +} + +// From codersdk/templateversions.go:17:6. +export interface TemplateVersion { + readonly name: string + readonly job: ProvisionerJob +} + +// From codersdk/users.go:17:6. +export interface User { + readonly email: string + readonly username: string + readonly name: string +} + +// From codersdk/users.go:25:6. +export interface CreateFirstUserRequest { + readonly email: string + readonly username: string + readonly password: string + readonly organization: string +} + +// From codersdk/users.go:38:6. +export interface CreateUserRequest { + readonly email: string + readonly username: string + readonly password: string +} + +// From codersdk/users.go:45:6. +export interface UpdateUserProfileRequest { + readonly email: string + readonly username: string + readonly name?: string +} + +// From codersdk/users.go:52:6. +export interface LoginWithPasswordRequest { + readonly email: string + readonly password: string +} + +// From codersdk/users.go:58:6. +export interface LoginWithPasswordResponse { + readonly session_token: string +} + +// From codersdk/users.go:63:6. +export interface GenerateAPIKeyResponse { + readonly key: string +} + +// From codersdk/users.go:67:6. +export interface CreateOrganizationRequest { + readonly name: string +} + +// From codersdk/users.go:72:6. +export interface CreateWorkspaceRequest { + readonly name: string +} + +// From codersdk/workspaceagents.go:31:6. +export interface GoogleInstanceIdentityToken { + readonly json_web_token: string +} + +// From codersdk/workspaceagents.go:35:6. +export interface AWSInstanceIdentityToken { + readonly signature: string + readonly document: string +} + +// From codersdk/workspaceagents.go:42:6. +export interface WorkspaceAgentAuthenticateResponse { + readonly session_token: string +} + +// From codersdk/workspacebuilds.go:17:6. +export interface WorkspaceBuild { + readonly name: string + readonly job: ProvisionerJob +} + +// From codersdk/workspaceresources.go:23:6. +export interface WorkspaceResource { + readonly type: string + readonly name: string +} + +// From codersdk/workspaceresources.go:33:6. +export interface WorkspaceAgent { + readonly status: WorkspaceAgentStatus + readonly name: string + readonly instance_id: string + readonly architecture: string + readonly operating_system: string + readonly startup_script: string +} + +// From codersdk/workspaceresources.go:50:6. +export interface WorkspaceAgentResourceMetadata { + readonly memory_total: number + readonly disk_total: number + readonly cpu_cores: number + readonly cpu_model: string + readonly cpu_mhz: number +} + +// From codersdk/workspaceresources.go:58:6. +export interface WorkspaceAgentInstanceMetadata { + readonly jail_orchestrator: string + readonly operating_system: string + readonly platform: string + readonly platform_family: string + readonly kernel_version: string + readonly kernel_architecture: string + readonly cloud: string + readonly jail: string + readonly vnc: boolean +} + +// From codersdk/workspaces.go:18:6. +export interface Workspace { + readonly template_name: string + readonly latest_build: WorkspaceBuild + readonly outdated: boolean + readonly name: string + readonly autostart_schedule: string + readonly autostop_schedule: string +} + +// From codersdk/workspaces.go:33:6. +export interface CreateWorkspaceBuildRequest { + readonly dry_run: boolean +} + +// From codersdk/workspaces.go:94:6. +export interface UpdateWorkspaceAutostartRequest { + readonly schedule: string +} + +// From codersdk/workspaces.go:114:6. +export interface UpdateWorkspaceAutostopRequest { + readonly schedule: string +} + +// From codersdk/workspaceresources.go:15:6. +export type WorkspaceAgentStatus = "connecting" | "connected" | "disconnected" + +// From codersdk/parameters.go:16:6. +export type ParameterScope = "organization" | "template" | "user" | "workspace" + +// From codersdk/provisionerdaemons.go:26:6. +export type ProvisionerJobStatus = "pending" | "running" | "succeeded" | "canceling" | "canceled" | "failed"