Skip to content
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
22 changes: 22 additions & 0 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@
schedule: [],
},
customManagers: [
{
customType: 'regex',
description: 'Bump the container images the schema generators run in',
managerFilePatterns: [
'/^internal/schemas/generator/.*\\.go$/',
],
matchStrings: [
'Image\\s*=\\s*"(?<depName>[^":]+):(?<currentValue>[^"]+)"',
],
datasourceTemplate: 'docker',
},
{
customType: 'regex',
description: 'Bump the Renovate version used by the config validator and the bot',
Expand Down Expand Up @@ -148,6 +159,17 @@
],
enabled: false,
},
{
// Ensure TypeScript generator package.json and package-lock.json are updated together.
description: 'Group updates to the pinned TypeScript schema generator toolchain',
matchManagers: [
'npm',
],
matchFileNames: [
'internal/schemas/generator/typescript-toolchain/package.json',
],
groupName: 'typescript schema generator toolchain',
},
{
description: 'Group all go version updates',
matchDatasources: [
Expand Down
6 changes: 4 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ version: "2"

run:
# Tests behind the compilegate tag shell out to the Go toolchain to build the
# generated models module, so they can't run in the hermetic Nix sandbox that
# runs our unit tests. Lint them anyway, so they don't rot.
# generated models module, and tests behind the dockergate tag need a live
# Docker daemon, so neither can run in the hermetic Nix sandbox that runs
# our unit tests. Lint them anyway, so they don't rot.
build-tags:
- compilegate
- dockergate

output:
formats:
Expand Down
38 changes: 28 additions & 10 deletions apis/dev/v1alpha1/project_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,30 @@ const (
// ProjectSchemas.Languages. Each corresponds to a schema generator in
// internal/schemas/generator.
const (
SchemaLanguageGo = "go"
SchemaLanguageJSON = "json"
SchemaLanguageKCL = "kcl"
SchemaLanguagePython = "python"
SchemaLanguageGo = "go"
SchemaLanguageJSON = "json"
SchemaLanguageKCL = "kcl"
SchemaLanguagePython = "python"
SchemaLanguageTypeScript = "typescript"
)

// SupportedSchemaLanguages returns the set of language identifiers accepted
// in ProjectSchemas.Languages.
func SupportedSchemaLanguages() []string {
return []string{
SchemaLanguageGo,
SchemaLanguageJSON,
SchemaLanguageKCL,
SchemaLanguagePython,
SchemaLanguageTypeScript,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

// DefaultSchemaLanguages returns the languages generated when
// ProjectSchemas.Languages is not specified. TypeScript is excluded:
// generating it starts a Docker container running a Node.js toolchain, so a
// project must opt in by listing "typescript" explicitly.
func DefaultSchemaLanguages() []string {
return []string{
SchemaLanguageGo,
SchemaLanguageJSON,
Expand Down Expand Up @@ -133,16 +148,19 @@ type ProjectPackageMetadata struct {
// produced both for the project's own XRDs and for its declared dependencies.
type ProjectSchemas struct {
// Languages restricts schema generation to the listed languages.
// Supported values are "go", "json", "kcl", and "python". If not
// specified, schemas are generated for all supported languages.
// If not specified, schemas are generated for DefaultSchemaLanguages().
// TypeScript generation starts a Docker container running a Node.js
// toolchain, so it must be listed explicitly to be included.
// +kubebuilder:validation:items:Enum=go;json;kcl;python;typescript
Languages []string `json:"languages,omitempty"`
}

// GetLanguages returns the configured schema languages, or nil if no Schemas
// config is set. It is safe to call on a nil receiver.
// GetLanguages returns the effective set of schema languages: the configured
// Languages if any are set, or DefaultSchemaLanguages() otherwise. It is safe
// to call on a nil receiver.
func (s *ProjectSchemas) GetLanguages() []string {
if s == nil {
return nil
if s == nil || len(s.Languages) == 0 {
return DefaultSchemaLanguages()
}
return s.Languages
}
Expand Down
75 changes: 75 additions & 0 deletions apis/dev/v1alpha1/project_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright 2026 The Crossplane Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"testing"

"github.com/google/go-cmp/cmp"
)

func TestProjectSchemasGetLanguages(t *testing.T) {
t.Parallel()

cases := map[string]struct {
reason string
schemas *ProjectSchemas
want []string
}{
"NilReceiver": {
reason: "an absent Schemas config defaults to every language except TypeScript",
schemas: nil,
want: DefaultSchemaLanguages(),
},
"UnspecifiedLanguages": {
reason: "a Schemas config with no Languages defaults the same as a nil receiver",
schemas: &ProjectSchemas{},
want: DefaultSchemaLanguages(),
},
"ExplicitLanguages": {
reason: "an explicit list is returned unchanged",
schemas: &ProjectSchemas{Languages: []string{SchemaLanguagePython}},
want: []string{SchemaLanguagePython},
},
"ExplicitTypeScript": {
reason: "TypeScript is only included when named explicitly",
schemas: &ProjectSchemas{Languages: []string{SchemaLanguageTypeScript}},
want: []string{SchemaLanguageTypeScript},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
t.Parallel()

got := tc.schemas.GetLanguages()
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("GetLanguages(): -want, +got:\n%s\n%s", diff, tc.reason)
}
})
}
}

func TestDefaultSchemaLanguagesExcludesTypeScript(t *testing.T) {
t.Parallel()

for _, lang := range DefaultSchemaLanguages() {
if lang == SchemaLanguageTypeScript {
t.Errorf("DefaultSchemaLanguages() includes %q; TypeScript generation starts a Docker container and must be opt-in", SchemaLanguageTypeScript)
}
}
}
6 changes: 3 additions & 3 deletions apis/dev/v1alpha1/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ func (s *ProjectSpec) Validate() error {
}

// Validate returns errors for an invalid ProjectSchemas. A nil receiver is
// valid (it means "generate schemas for all languages"); an explicitly empty
// Languages list is rejected because it would disable all schema generation,
// which is almost certainly a mistake.
// valid (it means "generate schemas for DefaultSchemaLanguages()"); an
// explicitly empty Languages list is rejected because it would disable all
// schema generation, which is almost certainly a mistake.
func (s *ProjectSchemas) Validate() []error {
if s == nil {
return nil
Expand Down
66 changes: 59 additions & 7 deletions cmd/crossplane/function/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ var (
pythonTemplates embed.FS
//go:embed templates/go-templating/*
goTemplatingTemplates embed.FS
//go:embed all:templates/typescript
typescriptTemplates embed.FS

// The go template contains a go.mod, so we can't embed it as an
// embed.FS. Instead we have to embed it as a tar archive and extract it
Expand All @@ -70,7 +72,7 @@ var (
type generateCmd struct {
Name string `arg:"" help:"Name of the function to generate. Must be a valid DNS-1035 label."`
PipelinePath string `arg:"" help:"Path to a Composition YAML file to add a pipeline step to." optional:""`
Language string `default:"go-templating" enum:"go,go-templating,kcl,python" help:"Language to use for the function." short:"l"`
Language string `default:"go-templating" enum:"go,go-templating,kcl,python,typescript" help:"Language to use for the function." short:"l"`
ProjectFile string `default:"${project_file}" help:"Path to project definition file." short:"f"`

projFS afero.Fs
Expand Down Expand Up @@ -120,14 +122,10 @@ func (c *generateCmd) AfterApply() error {
// validateLanguageAgainstSchemas refuses to generate a function in a language
// whose schemas the project doesn't generate. Such a function would have no
// models to import, which is surprising, so we fail up front rather than
// scaffolding a function that can't compile. An empty schemaLangs means the
// project generates all languages (matching generator.Filter), so any function
// language is fine.
// scaffolding a function that can't compile.
func validateLanguageAgainstSchemas(functionLang string, schemaLangs []string) error {
if len(schemaLangs) == 0 {
return nil
}
required := functionSchemaLanguage(functionLang)

if !slices.Contains(schemaLangs, required) {
return errors.Errorf("cannot generate a %q function: the project only generates %v schemas; add %q to spec.schemas.languages or choose a different language", functionLang, schemaLangs, required)
}
Expand Down Expand Up @@ -180,6 +178,7 @@ func (c *generateCmd) Run(sp terminal.SpinnerPrinter, cfg *config.Config) error
"go-templating": c.generateGoTemplatingFiles,
"kcl": c.generateKCLFiles,
"python": c.generatePythonFiles,
"typescript": c.generateTypeScriptFiles,
}

generator, ok := generators[c.Language]
Expand Down Expand Up @@ -420,6 +419,59 @@ func (c *generateCmd) generateGoTemplatingFiles(fs afero.Fs) error {
return renderTemplates(fs, tmpls, tmplData)
}

type typescriptTemplateData struct {
Name string
HasSchemas bool
SchemasPath string
}

func (c *generateCmd) generateTypeScriptFiles(targetFS afero.Fs) error {
hasSchemas, err := afero.DirExists(c.schemasFS, "typescript")
if err != nil {
return errors.Wrap(err, "cannot inspect typescript schemas directory")
}
if hasSchemas {
entries, err := afero.ReadDir(c.schemasFS, "typescript")
if err != nil {
return errors.Wrap(err, "cannot read typescript schemas directory")
}
hasSchemas = len(entries) > 0
}

// Compute the relative path from the function dir to schemas/typescript/.
fnDir := filepath.Join("/", c.proj.Spec.Paths.Functions, c.Name)
relRoot, err := filepath.Rel(fnDir, "/")
if err != nil {
return errors.Wrap(err, "cannot determine path to schemas directory")
}
schemasPath := filepath.ToSlash(filepath.Join(relRoot, c.proj.Spec.Paths.Schemas, "typescript"))

data := typescriptTemplateData{
Name: c.Name,
HasSchemas: hasSchemas,
SchemasPath: schemasPath,
}

// Parse top-level templates
tmpls, err := template.ParseFS(typescriptTemplates, "templates/typescript/*.*")
Comment thread
stevendborrelli marked this conversation as resolved.
if err != nil {
return errors.Wrap(err, "cannot parse top-level TypeScript templates")
}
if err := renderTemplates(targetFS, tmpls, data); err != nil {
return err
}

// Create src directory and parse src templates
if err := targetFS.Mkdir("src", 0o755); err != nil {
return errors.Wrap(err, "cannot create src directory")
}
tmpls, err = template.ParseFS(typescriptTemplates, "templates/typescript/src/*.*")
if err != nil {
return errors.Wrap(err, "cannot parse TypeScript source templates")
}
return renderTemplates(afero.NewBasePathFs(targetFS, "src"), tmpls, data)
}

func renderTemplates(targetFS afero.Fs, tmpls *template.Template, data any) error {
for _, tmpl := range tmpls.Templates() {
fname := tmpl.Name()
Expand Down
Loading
Loading