Skip to content
Draft
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
5 changes: 5 additions & 0 deletions snippets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/snippets
/dist
.cache/
*.test
.DS_Store
50 changes: 50 additions & 0 deletions snippets/README.md
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.
105 changes: 105 additions & 0 deletions snippets/cmd/snippets/main.go
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]
Comment on lines +17 to +18
Copy link
Copy Markdown
Member

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.

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")
}
87 changes: 87 additions & 0 deletions snippets/docs/AUTHORING.md
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` |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 hash=0? I realize the bots will do fine with it, but I'm not totally ready to surrender a touch of humanity.


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.
5 changes: 5 additions & 0 deletions snippets/go.mod
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
4 changes: 4 additions & 0 deletions snippets/go.sum
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=
49 changes: 49 additions & 0 deletions snippets/internal/adapters/ldapplication/descriptor.go
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
}
Loading
Loading