-
Notifications
You must be signed in to change notification settings - Fork 2
feat: Add consolidated snippets. #396
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
base: main
Are you sure you want to change the base?
Changes from all commits
ac28710
46ebb1e
fc2fd82
b8d82b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| /snippets | ||
| /dist | ||
| .cache/ | ||
| *.test | ||
| .DS_Store |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # sdk-snippets | ||
|
|
||
| Single source of truth for LaunchDarkly SDK code snippets rendered into the | ||
| LD application UI and the LD docs site. | ||
|
|
||
| First slice: `python-server-sdk` "Getting started" flow. | ||
|
|
||
| ## Layout | ||
|
|
||
| ``` | ||
| cmd/snippets/ CLI entrypoint (render, verify, validate) | ||
| internal/ generator Go code | ||
| sdks/<id>/ one directory per SDK | ||
| sdk.yaml SDK descriptor | ||
| snippets/ .snippet.md files (YAML frontmatter + Markdown + code blocks) | ||
| validators/languages/ per-language Docker validator harnesses | ||
| ``` | ||
|
|
||
| ## CLI | ||
|
|
||
| ``` | ||
| snippets render --target=ld-application --out=<app-checkout> | ||
| snippets verify --target=ld-application --out=<app-checkout> | ||
| snippets validate --sdk=python-server-sdk | ||
| ``` | ||
|
|
||
| `render` rewrites the code regions between `SDK_SNIPPET:RENDER` markers. | ||
| `verify` recomputes hashes and fails if any managed region has drifted. | ||
| `validate` runs each snippet in a Docker container against a real LaunchDarkly | ||
| environment. Required env vars (matching the convention used by the `hello-*` | ||
| sample apps): | ||
|
|
||
| ```sh | ||
| export LAUNCHDARKLY_SDK_KEY=<server-side SDK key> | ||
| export LAUNCHDARKLY_FLAG_KEY=<flag key the snippet evaluates> | ||
| snippets validate --sdk=python-server-sdk | ||
| ``` | ||
|
|
||
| Neither variable is committed to this repo. They are forwarded into the per- | ||
| snippet Docker run. | ||
|
|
||
| ## Adapter targets | ||
|
|
||
| | Target | Renders into | | ||
| |---|---| | ||
| | `ld-application` | the LaunchDarkly application's "Get Started" flows (TSX/JSX) | | ||
| | `ld-docs` | the LaunchDarkly documentation site (MDX) — not yet implemented | | ||
|
|
||
| The adapter is selected via `--target`. Today only `ld-application` is wired | ||
| up; `ld-docs` will land in a later slice. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/launchdarkly/sdk-meta/snippets/internal/adapters/ldapplication" | ||
| "github.com/launchdarkly/sdk-meta/snippets/internal/validate" | ||
| "github.com/launchdarkly/sdk-meta/snippets/internal/version" | ||
| ) | ||
|
|
||
| const usage = `snippets — LaunchDarkly SDK snippet generator | ||
|
|
||
| usage: | ||
| snippets render --target=ld-application --out=<app-checkout> [--sdks=./sdks] | ||
| snippets verify --target=ld-application --out=<app-checkout> [--sdks=./sdks] | ||
| snippets validate --sdk=<sdk-id> [--sdks=./sdks] [--validators=./validators] | ||
| snippets version | ||
|
|
||
| First-pass support: target=ld-application, sdk=python-server-sdk. | ||
| ` | ||
|
|
||
| func main() { | ||
| if len(os.Args) < 2 { | ||
| fmt.Fprint(os.Stderr, usage) | ||
| os.Exit(2) | ||
| } | ||
| switch os.Args[1] { | ||
| case "render": | ||
| runRender(os.Args[2:]) | ||
| case "verify": | ||
| runVerify(os.Args[2:]) | ||
| case "validate": | ||
| runValidate(os.Args[2:]) | ||
| case "version": | ||
| fmt.Println(version.Version) | ||
| case "-h", "--help", "help": | ||
| fmt.Print(usage) | ||
| default: | ||
| fmt.Fprintf(os.Stderr, "unknown subcommand %q\n\n%s", os.Args[1], usage) | ||
| os.Exit(2) | ||
| } | ||
| } | ||
|
|
||
| func runRender(args []string) { | ||
| fs := flag.NewFlagSet("render", flag.ExitOnError) | ||
| target := fs.String("target", "", "adapter target: `ld-application`") | ||
| out := fs.String("out", "", "path to the consumer checkout") | ||
| sdks := fs.String("sdks", "./sdks", "path to the sdks/ directory") | ||
| _ = fs.Parse(args) | ||
|
|
||
| if *target != "ld-application" || *out == "" { | ||
| fmt.Fprintf(os.Stderr, "render: --target=ld-application and --out are required\n") | ||
| os.Exit(2) | ||
| } | ||
| changed, err := ldapplication.Render(*sdks, *out) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "render failed: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| if len(changed) == 0 { | ||
| fmt.Println("no changes") | ||
| return | ||
| } | ||
| for _, p := range changed { | ||
| fmt.Println("rewrote", p) | ||
| } | ||
| } | ||
|
|
||
| func runVerify(args []string) { | ||
| fs := flag.NewFlagSet("verify", flag.ExitOnError) | ||
| target := fs.String("target", "", "adapter target: `ld-application`") | ||
| out := fs.String("out", "", "path to the consumer checkout") | ||
| sdks := fs.String("sdks", "./sdks", "path to the sdks/ directory") | ||
| _ = fs.Parse(args) | ||
|
|
||
| if *target != "ld-application" || *out == "" { | ||
| fmt.Fprintf(os.Stderr, "verify: --target=ld-application and --out are required\n") | ||
| os.Exit(2) | ||
| } | ||
| if err := ldapplication.Verify(*sdks, *out); err != nil { | ||
| fmt.Fprintf(os.Stderr, "verify failed: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| fmt.Println("ok") | ||
| } | ||
|
|
||
| func runValidate(args []string) { | ||
| fs := flag.NewFlagSet("validate", flag.ExitOnError) | ||
| sdk := fs.String("sdk", "", "sdk id to validate (required)") | ||
| sdks := fs.String("sdks", "./sdks", "path to the sdks/ directory") | ||
| validators := fs.String("validators", "./validators", "path to the validators/ directory") | ||
| _ = fs.Parse(args) | ||
|
|
||
| if *sdk == "" { | ||
| fmt.Fprintf(os.Stderr, "validate: --sdk is required\n") | ||
| os.Exit(2) | ||
| } | ||
| if err := validate.Run(validate.Config{SDKsDir: *sdks, ValidatorsDir: *validators, SDK: *sdk}); err != nil { | ||
| fmt.Fprintf(os.Stderr, "validate failed: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| fmt.Println("ok") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # Authoring snippets | ||
|
|
||
| A snippet is a single Markdown file with YAML frontmatter and one fenced code | ||
| block. See `sdks/python-server-sdk/snippets/getting-started/` for the canonical | ||
| examples. | ||
|
|
||
| ## Frontmatter fields used in the first slice | ||
|
|
||
| | Field | Required | Notes | | ||
| |---|---|---| | ||
| | `id` | yes | globally unique; convention `<sdk>/<group>/<name>` | | ||
| | `sdk` | yes | matches a directory under `sdks/` | | ||
| | `kind` | yes | `bootstrap`, `install`, `hello-world`, `run` | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that list exclusive and what's the distinction? |
||
| | `lang` | yes | language tag for the fenced code block | | ||
| | `description` | recommended | one-liner shown to the consumer | | ||
| | `inputs` | optional | each input has `type`, `description`, `runtime-default` | | ||
| | `ld-application.slot` | optional | logical slot label for the ld-application adapter | | ||
| | `validation.entrypoint` | optional | filename to run inside the validator (e.g. `main.py`) | | ||
| | `validation.requirements` | optional | line written into `requirements.txt` | | ||
|
|
||
| ## Templating | ||
|
|
||
| Inside the code block: | ||
|
|
||
| - `{{ name }}` — substitutes an input value | ||
| - `{{ if name }}...{{ end }}` — emits the inner content only when `name` has a non-empty value | ||
|
|
||
| Conditionals do not nest. The inner content of a conditional may still contain `{{ name }}`. | ||
|
|
||
| ## Render modes | ||
|
|
||
| Same template renders three different ways: | ||
|
|
||
| | Mode | What it produces | | ||
| |---|---| | ||
| | `ld-application` | JS template-literal expression with `${name}` interpolations and `${name ? \`...\` : ''}` ternaries inside JSX. | | ||
| | `runtime` (validator) | Plain code with concrete values substituted. | | ||
| | `verify` | Same as `ld-application`, then compared byte-for-byte against the consumer file. | | ||
|
|
||
| ## Render markers in consumer files | ||
|
|
||
| The generator never edits files outside a marker comment. Mark each generated | ||
| region with one comment of the host syntax, immediately before the JSX element | ||
| whose body should be replaced: | ||
|
|
||
| - TS/JS expression context: `// SDK_SNIPPET:RENDER:<id> hash=<h> version=<v>` | ||
| - JSX children context: `{/* SDK_SNIPPET:RENDER:<id> hash=<h> version=<v> */}` | ||
| - Block comment anywhere: `/* SDK_SNIPPET:RENDER:<id> hash=<h> version=<v> */` | ||
|
|
||
| `hash` and `version` are filled in by `snippets render`. On first wiring, use | ||
| `hash=000000000000` as a placeholder — the next render rewrites it. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's not a terribly convenient number to remember to use. Possible to make it |
||
|
|
||
| The element directly following a marker MUST be a capitalized JSX component | ||
| tag (e.g. `<Snippet>`, `<CodeBlock>`). Lowercase HTML tags (`<pre>`, `<code>`) | ||
| are not recognized; wrap the content in a component first if you need to mark | ||
| it. The marker hash covers only the *children* of the element (between `>` | ||
| and `</`), matching the `scope=content` contract — attributes (`lang`, | ||
| `withCopyButton`, `label`, `className`, …) are the consumer's to choose and | ||
| can be edited without re-running `snippets render`. | ||
|
|
||
| ## CLI quick reference | ||
|
|
||
| ```sh | ||
| # Validate a snippet end-to-end in Docker, against a real LD environment | ||
| export LAUNCHDARKLY_SDK_KEY=... # server-side key | ||
| export LAUNCHDARKLY_FLAG_KEY=... # flag the snippet evaluates | ||
| snippets validate --sdk=python-server-sdk | ||
|
|
||
| # Rewrite all marked regions in an ld-application checkout | ||
| snippets render --target=ld-application --out=/path/to/ld-application | ||
|
|
||
| # Confirm consumer file matches what we'd render (no edits) | ||
| snippets verify --target=ld-application --out=/path/to/ld-application | ||
| ``` | ||
|
|
||
| ## Validator inputs | ||
|
|
||
| Validation runs against a real LaunchDarkly environment. The caller supplies: | ||
|
|
||
| | Env var | Mapped to inputs of `type` | | ||
| |---|---| | ||
| | `LAUNCHDARKLY_SDK_KEY` | passed to the snippet via the same env var inside the Docker container; the snippet reads it the same way the `hello-*` sample apps do | | ||
| | `LAUNCHDARKLY_FLAG_KEY` | substituted into any input of `type: flag-key` at render-for-validation time | | ||
|
|
||
| Snippet frontmatter does **not** carry default values for `flag-key` / `sdk-key` / | ||
| `mobile-key` / `client-side-id` types — those always come from the caller's | ||
| environment so real keys never end up committed to this repo. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| module github.com/launchdarkly/sdk-meta/snippets | ||
|
|
||
| go 1.24 | ||
|
|
||
| require gopkg.in/yaml.v3 v3.0.1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= | ||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package ldapplication | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "os" | ||
|
|
||
| "gopkg.in/yaml.v3" | ||
| ) | ||
|
|
||
| // descriptor models the complete sdk.yaml schema (not just the | ||
| // ld-application subset) so we can decode with KnownFields(true) and | ||
| // catch typos like `Entrpoint:` or `get-startted-file:` rather than | ||
| // silently dropping them. Fields we don't read yet are still listed so | ||
| // the strict decode doesn't reject them. | ||
| type descriptor struct { | ||
| ID string `yaml:"id"` | ||
| SDKMetaID string `yaml:"sdk-meta-id"` | ||
| DisplayName string `yaml:"display-name"` | ||
| Type string `yaml:"type"` | ||
| Languages []descLanguage `yaml:"languages"` | ||
| PackageManagers []string `yaml:"package-managers"` | ||
| Regions []string `yaml:"regions"` | ||
| HelloWorldRepo string `yaml:"hello-world-repo"` | ||
| LDApplication struct { | ||
| GetStartedFile string `yaml:"get-started-file"` | ||
| } `yaml:"ld-application"` | ||
| Docs struct { | ||
| ReferencePage string `yaml:"reference-page"` | ||
| } `yaml:"docs"` | ||
| } | ||
|
|
||
| type descLanguage struct { | ||
| ID string `yaml:"id"` | ||
| Extensions []string `yaml:"extensions"` | ||
| } | ||
|
|
||
| func loadDescriptor(path string) (*descriptor, error) { | ||
| raw, err := os.ReadFile(path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var d descriptor | ||
| dec := yaml.NewDecoder(bytes.NewReader(raw)) | ||
| dec.KnownFields(true) | ||
| if err := dec.Decode(&d); err != nil { | ||
| return nil, err | ||
| } | ||
| return &d, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know the difference between these two just looking at it. Maybe some description or we can land on different language once I've actually looked at the PR.