diff --git a/templates/cli/internal/typegen/language.go b/templates/cli/internal/typegen/language.go index 34ab64ce6b..c4af2bba7e 100644 --- a/templates/cli/internal/typegen/language.go +++ b/templates/cli/internal/typegen/language.go @@ -263,16 +263,20 @@ var appwriteDependencies = []string{ func AppwriteDependency(directory string) string { if declared, err := os.ReadFile(filepath.Join(directory, "package.json")); err == nil { var manifest struct { - Dependencies map[string]string `json:"dependencies"` + Dependencies map[string]string `json:"dependencies"` + DevDependencies map[string]string `json:"devDependencies"` } // An unparseable package.json falls through to the deno.json check // rather than failing the detection. if json.Unmarshal(declared, &manifest) == nil { - // Truthiness, not presence: a dependency declared with an empty - // version string is skipped. - for _, name := range appwriteDependencies { - if manifest.Dependencies[name] != "" { - return name + // Keep runtime dependencies ahead of development dependencies, with + // the same package preference order within each section. + for _, dependencies := range []map[string]string{manifest.Dependencies, manifest.DevDependencies} { + // Truthiness, not presence: an empty version string is skipped. + for _, name := range appwriteDependencies { + if dependencies[name] != "" { + return name + } } } } diff --git a/tests/e2e/CLIGo126Test.php b/tests/e2e/CLIGo126Test.php index 6d1a3fdd5a..1849f9f3d3 100644 --- a/tests/e2e/CLIGo126Test.php +++ b/tests/e2e/CLIGo126Test.php @@ -48,6 +48,7 @@ final class CLIGo126Test extends Base ...Base::UPLOAD_RESPONSE, ...Base::UPLOAD_RESPONSE, ...Base::CLI_HEADERS_RESPONSES, + 'CLI_TYPES_DEPENDENCIES:passed', 'CLI_CONFORMANCE:passed', ]; diff --git a/tests/e2e/languages/cli/main.go b/tests/e2e/languages/cli/main.go index 5a80242572..245a835a9f 100644 --- a/tests/e2e/languages/cli/main.go +++ b/tests/e2e/languages/cli/main.go @@ -92,6 +92,8 @@ func main() { } } + harness.typesDependencies() + fmt.Println("CLI_TYPES_DEPENDENCIES:passed") fmt.Println("CLI_CONFORMANCE:passed") } @@ -109,6 +111,88 @@ func (h *harness) command(args ...string) *exec.Cmd { return command } +// typesDependencies runs from a monorepo root, not the nested output directory. +// Only local configuration is used; no Appwrite project is read or written. +func (h *harness) typesDependencies() { + cli, err := filepath.Abs(binary) + if err != nil { + fail(err) + } + + cases := []struct { + name string + manifest string // Empty means package.json is absent. + deno bool + dependency string + }{ + {"no-package", "", false, "appwrite"}, + {"missing-sections", `{}`, false, "appwrite"}, + {"null-sections", `{"dependencies":null,"devDependencies":null}`, false, "appwrite"}, + {"empty-sections", `{"dependencies":{},"devDependencies":{}}`, false, "appwrite"}, + {"appwrite-dependencies", `{"dependencies":{"appwrite":"1"}}`, false, "appwrite"}, + {"node-dependencies", `{"dependencies":{"node-appwrite":"1"}}`, false, "node-appwrite"}, + {"appwrite-dev", `{"devDependencies":{"appwrite":"1"}}`, false, "appwrite"}, + {"node-dev", `{"devDependencies":{"node-appwrite":"1"}}`, false, "node-appwrite"}, + {"appwrite-dev-null-dependencies", `{"dependencies":null,"devDependencies":{"appwrite":"1"}}`, false, "appwrite"}, + {"node-dev-null-dependencies", `{"dependencies":null,"devDependencies":{"node-appwrite":"1"}}`, false, "node-appwrite"}, + {"node-null-dev", `{"dependencies":{"node-appwrite":"1"},"devDependencies":null}`, false, "node-appwrite"}, + {"node-dev-empty-dependencies", `{"dependencies":{},"devDependencies":{"node-appwrite":"1"}}`, false, "node-appwrite"}, + {"node-dev-unrelated-dependencies", `{"dependencies":{"other":"1"},"devDependencies":{"node-appwrite":"1"}}`, false, "node-appwrite"}, + {"runtime-node-before-dev-appwrite", `{"dependencies":{"node-appwrite":"1"},"devDependencies":{"appwrite":"1"}}`, false, "node-appwrite"}, + {"runtime-appwrite-before-dev-node", `{"dependencies":{"appwrite":"1"},"devDependencies":{"node-appwrite":"1"}}`, false, "appwrite"}, + {"runtime-package-preference", `{"dependencies":{"node-appwrite":"1","appwrite":"1"}}`, false, "appwrite"}, + {"dev-package-preference", `{"devDependencies":{"node-appwrite":"1","appwrite":"1"}}`, false, "appwrite"}, + {"console-preference", `{"devDependencies":{"@appwrite.io/console":"1","react-native-appwrite":"1","appwrite":"1","node-appwrite":"1"}}`, false, "@appwrite.io/console"}, + {"react-native-preference", `{"devDependencies":{"react-native-appwrite":"1","appwrite":"1","node-appwrite":"1"}}`, false, "react-native-appwrite"}, + {"empty-versions", `{"dependencies":{"appwrite":""},"devDependencies":{"appwrite":"","node-appwrite":"1"}}`, false, "node-appwrite"}, + {"deno-default", "", true, "npm:node-appwrite"}, + {"deno-null-sections", `{"dependencies":null,"devDependencies":null}`, true, "npm:node-appwrite"}, + {"dev-before-deno", `{"devDependencies":{"node-appwrite":"1"}}`, true, "node-appwrite"}, + {"invalid-package", `{`, false, "appwrite"}, + {"invalid-package-deno", `{`, true, "npm:node-appwrite"}, + } + + for _, test := range cases { + directory := filepath.Join(h.home, "types", test.name) + output := filepath.Join("packages", "appwrite-types", "src") + if err := os.MkdirAll(filepath.Join(directory, output), 0o700); err != nil { + fail(err) + } + files := map[string]string{ + "appwrite.config.json": `{"projectId":"offline","collections":[{"$id":"water","databaseId":"main","name":"Fixed Water Sources","attributes":[{"key":"name","type":"string","required":true}]}]}`, + "packages/appwrite-types/package.json": `{"name":"appwrite-types","private":true}`, + } + if test.manifest != "" { + files["package.json"] = test.manifest + } + if test.deno { + files["deno.json"] = `{}` + } + for name, content := range files { + if err := os.WriteFile(filepath.Join(directory, name), []byte(content), 0o600); err != nil { + fail(err) + } + } + + command := h.command("types", output, "--language=ts", "--verbose") + command.Path = cli + command.Dir = directory + if output, err := command.CombinedOutput(); err != nil { + fail(fmt.Errorf("types %s: %w\n%s", test.name, err, output)) + } + generated, err := os.ReadFile(filepath.Join(directory, output, "appwrite.d.ts")) + if err != nil { + fail(err) + } + want := "import type { Models } from '" + test.dependency + "';" + if !strings.HasPrefix(string(generated), want+"\n") || + !strings.Contains(string(generated), "export type FixedWaterSources = Models.Row & {") || + !strings.Contains(string(generated), "name: string;") { + fail(fmt.Errorf("types %s: expected %s and collection fields, got:\n%s", test.name, want, generated)) + } + } +} + // run executes the CLI and returns its stdout, requiring a zero exit. func (h *harness) run(args ...string) string { output, err := h.command(args...).Output()