Skip to content
Merged
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
66 changes: 59 additions & 7 deletions cli/gonext/cmd/plugin/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func runInit(args []string, stdout, stderr io.Writer) int {
fmt.Fprintln(stderr, initUsage)
}

template := fs.String("template", "go", "template name (go|rust)")
template := fs.String("template", "go", "template name (go|rust|typescript)")
pluginName := fs.String("name", "", "plugin slug for the manifest (default: project dir basename)")
force := fs.Bool("force", false, "overwrite existing files at the target")

Expand Down Expand Up @@ -74,10 +74,10 @@ func runInit(args []string, stdout, stderr io.Writer) int {
}

switch *template {
case "go", "rust":
case "go", "rust", "typescript":
// supported
default:
fmt.Fprintf(stderr, "gonext plugin init: unknown template %q (supported: go, rust)\n", *template)
fmt.Fprintf(stderr, "gonext plugin init: unknown template %q (supported: go, rust, typescript)\n", *template)
return ExitUsage
}

Expand Down Expand Up @@ -108,6 +108,16 @@ func runInit(args []string, stdout, stderr io.Writer) int {
fmt.Fprintln(stdout, " cd "+projectDir)
fmt.Fprintln(stdout, " cargo build --target wasm32-wasip1 --release")
fmt.Fprintln(stdout, " make bundle # packs the .gnplugin ZIP")
case "typescript":
if err := writeTemplateTypeScript(projectDir, slug, *force); err != nil {
fmt.Fprintf(stderr, "gonext plugin init: %s\n", err)
return ExitFail
}
fmt.Fprintf(stdout, "Initialized GoNext TypeScript plugin in %s\n", projectDir)
fmt.Fprintln(stdout, "Next steps:")
fmt.Fprintln(stdout, " cd "+projectDir)
fmt.Fprintln(stdout, " npm install")
fmt.Fprintln(stdout, " npx gonext-sdk-build # compiles src/index.ts to plugin.wasm via Javy")
}
return ExitOK
}
Expand All @@ -124,19 +134,21 @@ Flags:
--force overwrite existing files at the target

Templates:
go TinyGo-targeted Go plugin using packages/go/sdk
rust Rust crate compiled to wasm32-wasip1 using packages/rust/gonext-sdk
go TinyGo-targeted Go plugin using packages/go/sdk
rust Rust crate compiled to wasm32-wasip1 using packages/rust/gonext-sdk
typescript TypeScript plugin compiled to WASM via Javy (packages/ts/sdk-plugin)

Example:
gonext plugin init --template=go ./my-plugin
gonext plugin init --template=rust ./my-rust-plugin`
gonext plugin init --template=rust ./my-rust-plugin
gonext plugin init --template=typescript ./my-ts-plugin`

// templatesFS embeds the templates directory tree. Each file is
// rendered by trivial token substitution — {{PLUGIN_NAME}} becomes
// the manifest slug. We deliberately don't pull in text/template
// because the rendering is straight-line.
//
//go:embed templates/go/* templates/rust/* templates/rust/src/*
//go:embed templates/go/* templates/rust/* templates/rust/src/* templates/typescript/* templates/typescript/src/*
var templatesFS embed.FS

// writeTemplateGo renders the Go template into dir. Returns an error
Expand Down Expand Up @@ -236,6 +248,46 @@ func writeTemplateRust(dir, slug string, force bool) error {
})
}

// writeTemplateTypeScript renders the TypeScript template into dir.
// Mirrors writeTemplateGo (same walk-and-rename-tmpl pattern); the
// duplication is deliberate — the TS template uses .npmignore via
// package.json's "files" field, so no .gitignore special-case here.
func writeTemplateTypeScript(dir, slug string, force bool) error {
root := "templates/typescript"
return fs.WalkDir(templatesFS, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
rel, err := filepath.Rel(root, path)
if err != nil {
return fmt.Errorf("relativise template path %q: %w", path, err)
}
target := filepath.Join(dir, strings.TrimSuffix(rel, ".tmpl"))

if !force {
if _, err := os.Stat(target); err == nil {
return fmt.Errorf("file already exists: %s (use --force to overwrite)", target)
}
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return fmt.Errorf("create dir for %q: %w", target, err)
}
data, err := templatesFS.ReadFile(path)
if err != nil {
return fmt.Errorf("read template %q: %w", path, err)
}
rendered := strings.ReplaceAll(string(data), "{{PLUGIN_NAME}}", slug)
rendered = strings.ReplaceAll(rendered, "{{PLUGIN_NAME_LITERAL}}", slug)
if err := os.WriteFile(target, []byte(rendered), 0o644); err != nil {
return fmt.Errorf("write %q: %w", target, err)
}
return nil
})
}

// sanitizeSlug converts a directory basename into a plugin-manifest-
// safe slug: lowercase ASCII, hyphens for non-alphanumerics, no
// leading/trailing hyphens, falling back to "my-plugin" if nothing
Expand Down
36 changes: 36 additions & 0 deletions cli/gonext/cmd/plugin/templates/typescript/README.md.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# {{slug}}

GoNext plugin scaffolded with `gonext plugin init --template=typescript`.

## Build

```bash
pnpm install
npx gonext-sdk-build
```

The pipeline:

1. `tsc` compiles `src/index.ts` to `dist/index.js`.
2. Javy compiles `dist/index.js` to `dist/plugin.wasm`.
3. `manifest.json` is validated and written to `dist/manifest.json`.

Javy must be on `$PATH` (or pass `--javy <path>`). Install it from the
[Javy releases page](https://github.com/bytecodealliance/javy/releases).

## Sign and ship

```bash
gonext plugin sign dist/
```

See [`docs/02-plugin-system.md`](https://github.com/Singleton-Solution/GoNext/blob/main/docs/02-plugin-system.md)
for the install + activation flow.

## Develop

```bash
pnpm typecheck # tsc --noEmit
gonext plugin test dist/ # contract checks
gonext plugin dev # auto-build + upload + log-tail loop
```
18 changes: 18 additions & 0 deletions cli/gonext/cmd/plugin/templates/typescript/manifest.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"apiVersion": "gonext.io/v1",
"name": "{{slug}}",
"version": "0.1.0",
"entry": "plugin.wasm",
"capabilities": [
"kv.read",
"kv.write",
"audit.emit"
],
"hooks": {
"actions": ["save_post"],
"filters": ["the_content"]
},
"requires": {
"host": ">=0.1.0"
}
}
18 changes: 18 additions & 0 deletions cli/gonext/cmd/plugin/templates/typescript/package.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "{{slug}}",
"version": "0.1.0",
"private": true,
"description": "GoNext plugin built with the TypeScript SDK and compiled to WASM via Javy.",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"build": "gonext-sdk-build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@gonext/sdk-plugin": "^0.0.1"
},
"devDependencies": {
"typescript": "^5.6.0"
}
}
31 changes: 31 additions & 0 deletions cli/gonext/cmd/plugin/templates/typescript/src/index.ts.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* {{slug}} — GoNext plugin scaffolded by `gonext plugin init`.
*
* Register your hook handlers below. The dispatcher Javy emits will
* route the host's `gn_handle_hook` calls into these handlers based
* on the hook name. See `manifest.json` for the actions/filters this
* plugin subscribes to.
*/
import {
pluginInit,
registerAction,
registerFilter,
host,
} from '@gonext/sdk-plugin';

// Action: fires whenever a post is saved. Actions are fire-and-forget;
// the return value is ignored. Use them for side effects like
// caching, audit, or KV writes.
registerAction('save_post', async (args) => {
host.log.info('{{slug}}: save_post fired with ' + JSON.stringify(args));
host.kv.set('last-save-ms', String(host.nowMs()));
host.audit.emit('plugin.{{slug}}.save_post', { args });
});

// Filter: transforms a value through the chain. The return value
// is JSON-encoded back to the host bus.
registerFilter('the_content', async (value) => {
return `<div data-plugin="{{slug}}">${value}</div>`;
});

pluginInit();
16 changes: 16 additions & 0 deletions cli/gonext/cmd/plugin/templates/typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ES2020",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"noEmit": false,
"declaration": false
},
"include": ["src/**/*.ts"]
}
44 changes: 44 additions & 0 deletions examples/plugins/sdk-ts-hello/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# gn-sdk-ts-hello

Worked example of a GoNext plugin written in TypeScript via the
[`@gonext/sdk-plugin`](../../../packages/ts/sdk-plugin) SDK and compiled
to WebAssembly through [Javy](https://github.com/bytecodealliance/javy).

## What it does

- Subscribes to one action (`save_post`): records a KV timestamp and
emits an audit row.
- Subscribes to one filter (`the_content`): wraps the value in a marker
`<div>` so the plugin is visible in rendered output.

Read [`src/index.ts`](src/index.ts) — the whole plugin is ~30 lines
including comments.

## Build

```bash
pnpm install
pnpm build
```

The pipeline runs `tsc` then `javy compile` and writes:

```
dist/
plugin.wasm # WASM module loaded by the host
manifest.json # validated against gonext.io/v1
```

Javy must be on `$PATH` (or pass `--javy <path>` to
`gonext-sdk-build`). Install it from the
[Javy releases page](https://github.com/bytecodealliance/javy/releases).

## Sign and install

```bash
gonext plugin sign dist/
gonext plugin test dist/ # contract checks
```

See [`docs/02-plugin-system.md`](../../../docs/02-plugin-system.md) for
the full install + activation flow.
18 changes: 18 additions & 0 deletions examples/plugins/sdk-ts-hello/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"apiVersion": "gonext.io/v1",
"name": "gn-sdk-ts-hello",
"version": "0.1.0",
"entry": "plugin.wasm",
"capabilities": [
"kv.write",
"audit.emit",
"hooks.subscribe"
],
"hooks": {
"actions": ["save_post"],
"filters": ["the_content"]
},
"requires": {
"host": ">=0.1.0"
}
}
18 changes: 18 additions & 0 deletions examples/plugins/sdk-ts-hello/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "gn-sdk-ts-hello",
"version": "0.1.0",
"private": true,
"description": "Worked example: GoNext plugin written in TypeScript using @gonext/sdk-plugin. Builds to a Javy-compiled plugin.wasm via gonext-sdk-build.",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"build": "gonext-sdk-build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@gonext/sdk-plugin": "workspace:*"
},
"devDependencies": {
"typescript": "^5.6.0"
}
}
44 changes: 44 additions & 0 deletions examples/plugins/sdk-ts-hello/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* gn-sdk-ts-hello — worked TypeScript plugin example.
*
* Demonstrates the minimum surface a real plugin uses:
* - one action (save_post): KV write + audit emission
* - one filter (the_content): pass-through value transform
* - explicit host imports rather than the namespaced `host` facade
* so the example shows both styles.
*
* Build:
* pnpm install
* pnpm build # tsc + javy -> dist/plugin.wasm + dist/manifest.json
*
* The plugin runs sandboxed inside the wazero host. All side effects
* go through the typed `gn_*` wrappers — nothing escapes that surface.
*/
import {
audit,
kv,
log,
nowMs,
pluginInit,
registerAction,
registerFilter,
} from '@gonext/sdk-plugin';

const PLUGIN_SLUG = 'gn-sdk-ts-hello';

// Action: record the last save time and emit an audit row.
registerAction('save_post', async (args) => {
const ts = nowMs();
log.info(`${PLUGIN_SLUG}: save_post observed at ${ts}`);
kv.set('last-save-ms', String(ts));
audit.emit('plugin.save_post.observed', { args, ts });
});

// Filter: wrap the post content in a marker div. The host's
// `the_content` filter chain composes this with any other plugins
// subscribing to the same hook.
registerFilter('the_content', async (value) => {
return `<div data-plugin="${PLUGIN_SLUG}">${value}</div>`;
});

pluginInit();
15 changes: 15 additions & 0 deletions examples/plugins/sdk-ts-hello/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ES2020",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true,
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}
Loading
Loading