@checkstack/scripts 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +15 -5
  2. package/src/commands/create.ts +16 -23
  3. package/src/commands/plugin-pack.ts +17 -28
  4. package/src/dev-tui/App.render.test.tsx +135 -0
  5. package/src/dev-tui/App.smoke.test.tsx +142 -0
  6. package/src/dev-tui/App.tsx +522 -0
  7. package/src/dev-tui/alert-buffer.test.ts +62 -0
  8. package/src/dev-tui/alert-buffer.ts +51 -0
  9. package/src/dev-tui/alt-screen.test.ts +66 -0
  10. package/src/dev-tui/alt-screen.ts +65 -0
  11. package/src/dev-tui/cli.tsx +89 -0
  12. package/src/dev-tui/fake-supervisor.ts +76 -0
  13. package/src/dev-tui/graceful-shutdown.test.ts +61 -0
  14. package/src/dev-tui/graceful-shutdown.ts +32 -0
  15. package/src/dev-tui/kill-tree.test.ts +47 -0
  16. package/src/dev-tui/kill-tree.ts +64 -0
  17. package/src/dev-tui/layout.test.ts +89 -0
  18. package/src/dev-tui/layout.ts +126 -0
  19. package/src/dev-tui/log-level.test.ts +94 -0
  20. package/src/dev-tui/log-level.ts +104 -0
  21. package/src/dev-tui/plain-runner.ts +60 -0
  22. package/src/dev-tui/process-config.test.ts +42 -0
  23. package/src/dev-tui/process-config.ts +61 -0
  24. package/src/dev-tui/readiness.test.ts +54 -0
  25. package/src/dev-tui/readiness.ts +44 -0
  26. package/src/dev-tui/scrollback.test.ts +83 -0
  27. package/src/dev-tui/scrollback.ts +82 -0
  28. package/src/dev-tui/supervisor.ts +231 -0
  29. package/src/dev-tui/text.test.ts +72 -0
  30. package/src/dev-tui/text.ts +101 -0
  31. package/src/dev-tui/types.ts +29 -0
  32. package/src/scaffold/index.ts +22 -0
  33. package/src/scaffold/resolve-versions.test.ts +49 -0
  34. package/src/scaffold/resolve-versions.ts +55 -0
  35. package/src/scaffold/rewrite-workspace-versions.test.ts +102 -0
  36. package/src/scaffold/rewrite-workspace-versions.ts +111 -0
  37. package/src/scaffold/scaffold-plugin.test.ts +209 -0
  38. package/src/scaffold/scaffold-plugin.ts +309 -0
  39. package/src/templates/backend/.changeset/initial.md.hbs +1 -1
  40. package/src/templates/backend/drizzle/0000_init.sql +7 -0
  41. package/src/templates/backend/drizzle/meta/0000_snapshot.json +65 -0
  42. package/src/templates/backend/drizzle/meta/_journal.json +13 -0
  43. package/src/templates/backend/drizzle.config.ts.hbs +5 -1
  44. package/src/templates/backend/package.json.hbs +7 -3
  45. package/src/templates/backend/src/index.ts.hbs +1 -1
  46. package/src/templates/backend/src/router.ts.hbs +1 -1
  47. package/src/templates/backend/src/service.ts.hbs +1 -1
  48. package/src/templates/common/.changeset/initial.md.hbs +1 -1
  49. package/src/templates/common/README.md.hbs +28 -11
  50. package/src/templates/common/package.json.hbs +1 -1
  51. package/src/templates/common/src/plugin-metadata.ts.hbs +1 -1
  52. package/src/templates/frontend/.changeset/initial.md.hbs +1 -1
  53. package/src/templates/frontend/package.json.hbs +2 -2
  54. package/src/templates/frontend/src/api.ts.hbs +2 -2
  55. package/src/templates/frontend/src/components/{{pluginNamePascal}}ListPage.tsx.hbs +1 -1
  56. package/src/templates/frontend/src/index.tsx.hbs +10 -4
  57. package/src/templates/standalone-root/.changeset/config.json.hbs +11 -0
  58. package/src/templates/standalone-root/.changeset/initial.md.hbs +9 -0
  59. package/src/templates/standalone-root/README.md.hbs +75 -0
  60. package/src/templates/standalone-root/eslint.config.mjs.hbs +37 -0
  61. package/src/templates/standalone-root/package.json.hbs +27 -0
  62. package/src/templates/standalone-root/tsconfig.json.hbs +13 -0
  63. package/src/templates.test.ts +20 -0
  64. package/src/tui/components.test.tsx +28 -0
  65. package/src/tui/components.tsx +159 -0
  66. package/src/tui/index.ts +31 -0
  67. package/src/tui/theme.test.ts +54 -0
  68. package/src/tui/theme.ts +60 -0
  69. package/src/utils/template.ts +42 -0
@@ -0,0 +1,65 @@
1
+ {
2
+ "id": "00000000-0000-0000-0000-000000000001",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.items": {
8
+ "name": "items",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "name": {
19
+ "name": "name",
20
+ "type": "text",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "description": {
25
+ "name": "description",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": false
29
+ },
30
+ "created_at": {
31
+ "name": "created_at",
32
+ "type": "timestamp",
33
+ "primaryKey": false,
34
+ "notNull": true,
35
+ "default": "now()"
36
+ },
37
+ "updated_at": {
38
+ "name": "updated_at",
39
+ "type": "timestamp",
40
+ "primaryKey": false,
41
+ "notNull": true,
42
+ "default": "now()"
43
+ }
44
+ },
45
+ "indexes": {},
46
+ "foreignKeys": {},
47
+ "compositePrimaryKeys": {},
48
+ "uniqueConstraints": {},
49
+ "policies": {},
50
+ "checkConstraints": {},
51
+ "isRLSEnabled": false
52
+ }
53
+ },
54
+ "enums": {},
55
+ "schemas": {},
56
+ "sequences": {},
57
+ "roles": {},
58
+ "policies": {},
59
+ "views": {},
60
+ "_meta": {
61
+ "columns": {},
62
+ "schemas": {},
63
+ "tables": {}
64
+ }
65
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1748736000000,
9
+ "tag": "0000_init",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -2,7 +2,11 @@ import { defineConfig } from "drizzle-kit";
2
2
 
3
3
  export default defineConfig({
4
4
  schema: "./src/schema.ts",
5
- out: "./migrations",
5
+ // The platform's plugin loader applies migrations from `<plugin>/drizzle`,
6
+ // so generated SQL must land there (NOT `./migrations`). The scaffold ships
7
+ // an initial `0000_init` migration for the example `items` table; running
8
+ // `bunx drizzle-kit generate` after editing the schema appends here.
9
+ out: "./drizzle",
6
10
  dialect: "postgresql",
7
11
  dbCredentials: {
8
12
  url: process.env.DATABASE_URL || "postgresql://localhost:5432/checkstack",
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@checkstack/{{pluginName}}",
2
+ "name": "{{scoped pluginName}}",
3
3
  "version": "0.0.1",
4
4
  "description": "{{pluginDescription}}",
5
5
  "author": "Checkstack contributors",
@@ -11,7 +11,11 @@
11
11
  },
12
12
  "checkstack": {
13
13
  "type": "backend",
14
- "pluginId": "{{pluginBaseName}}"
14
+ "pluginId": "{{pluginBaseName}}",
15
+ "bundle": [
16
+ "{{scoped pluginBaseName}}-common",
17
+ "{{scoped pluginBaseName}}-frontend"
18
+ ]
15
19
  },
16
20
  "scripts": {
17
21
  "dev": "checkstack-dev",
@@ -24,7 +28,7 @@
24
28
  "dependencies": {
25
29
  "@checkstack/backend-api": "workspace:*",
26
30
  "@checkstack/common": "workspace:*",
27
- "@checkstack/{{pluginBaseName}}-common": "workspace:*",
31
+ "{{scoped pluginBaseName}}-common": "workspace:*",
28
32
  "@orpc/server": "^1.13.2",
29
33
  "drizzle-orm": "^0.45.1"
30
34
  },
@@ -6,7 +6,7 @@ import {
6
6
  {{pluginNameCamel}}AccessRules,
7
7
  pluginMetadata,
8
8
  {{pluginNameCamel}}Contract,
9
- } from "@checkstack/{{pluginBaseName}}-common";
9
+ } from "{{scoped pluginBaseName}}-common";
10
10
  import * as schema from "./schema";
11
11
  import { create{{pluginNamePascal}}Router } from "./router";
12
12
 
@@ -1,6 +1,6 @@
1
1
  import { implement } from "@orpc/server";
2
2
  import { autoAuthMiddleware, correlationMiddleware, type RpcContext, type SafeDatabase } from "@checkstack/backend-api";
3
- import { {{pluginNameCamel}}Contract } from "@checkstack/{{pluginBaseName}}-common";
3
+ import { {{pluginNameCamel}}Contract } from "{{scoped pluginBaseName}}-common";
4
4
  import type * as schema from "./schema";
5
5
  import { {{pluginNamePascal}}Service } from "./service";
6
6
 
@@ -5,7 +5,7 @@ import { {{pluginNameCamel}}Items } from "./schema";
5
5
  import type {
6
6
  Create{{pluginNamePascal}}Item,
7
7
  Update{{pluginNamePascal}}Item,
8
- } from "@checkstack/{{pluginBaseName}}-common";
8
+ } from "{{scoped pluginBaseName}}-common";
9
9
 
10
10
  export class {{pluginNamePascal}}Service {
11
11
  constructor(private readonly database: SafeDatabase<typeof schema>) {}
@@ -1,4 +1,4 @@
1
- --- "@checkstack/{{pluginName}}": patch --- Initial release of
1
+ --- "{{scoped pluginName}}": patch --- Initial release of
2
2
  {{pluginNamePascal}}
3
3
  plugin
4
4
 
@@ -1,11 +1,28 @@
1
- #
2
- {{pluginNamePascal}}
3
- Common Common package for the
4
- {{pluginNamePascal}}
5
- plugin. Contains shared contracts, types, and access rules. ## Structure -
6
- `src/access.ts - Access rule definitions - `src/schemas.ts` - Zod schemas
7
- and type definitions - `src/rpc-contract.ts` - oRPC contract definition -
8
- `src/index.ts` - Barrel exports ## Usage This package is consumed by both: -
9
- `@checkstack/{{pluginBaseName}}-backend` - Implements the contract -
10
- `@checkstack/{{pluginBaseName}}-frontend` - Consumes the contract ##
11
- Development ```bash # Type check bun run typecheck # Lint bun run lint ```
1
+ # {{pluginNamePascal}} Common
2
+
3
+ Common package for the {{pluginNamePascal}} plugin. Contains shared
4
+ contracts, types, and access rules.
5
+
6
+ ## Structure
7
+
8
+ - `src/access.ts` - Access rule definitions
9
+ - `src/schemas.ts` - Zod schemas and type definitions
10
+ - `src/rpc-contract.ts` - oRPC contract definition
11
+ - `src/index.ts` - Barrel exports
12
+
13
+ ## Usage
14
+
15
+ This package is consumed by both:
16
+
17
+ - `{{scoped pluginBaseName}}-backend` - Implements the contract
18
+ - `{{scoped pluginBaseName}}-frontend` - Consumes the contract
19
+
20
+ ## Development
21
+
22
+ ```bash
23
+ # Type check
24
+ bun run typecheck
25
+
26
+ # Lint
27
+ bun run lint
28
+ ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@checkstack/{{pluginName}}",
2
+ "name": "{{scoped pluginName}}",
3
3
  "version": "0.0.1",
4
4
  "description": "{{pluginDescription}}",
5
5
  "author": "Checkstack contributors",
@@ -3,4 +3,4 @@ metadata for the
3
3
  {{pluginNamePascal}}
4
4
  plugin. * Exported from the common package so both backend and frontend can *
5
5
  reference it. */ export const pluginMetadata = definePluginMetadata({ pluginId:
6
- "{{pluginId}}", });
6
+ "{{pluginBaseName}}", });
@@ -1,4 +1,4 @@
1
- --- "@checkstack/{{pluginName}}": patch --- Initial release of
1
+ --- "{{scoped pluginName}}": patch --- Initial release of
2
2
  {{pluginNamePascal}}
3
3
  frontend plugin
4
4
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "@checkstack/{{pluginName}}",
2
+ "name": "{{scoped pluginName}}",
3
3
  "version": "0.0.1",
4
4
  "description": "{{pluginDescription}}",
5
5
  "author": "Checkstack contributors",
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@checkstack/frontend-api": "workspace:*",
26
26
  "@checkstack/common": "workspace:*",
27
- "@checkstack/{{pluginBaseName}}-common": "workspace:*",
27
+ "{{scoped pluginBaseName}}-common": "workspace:*",
28
28
  "@checkstack/ui": "workspace:*",
29
29
  "react": "^18.3.1",
30
30
  "react-router-dom": "^7.1.1",
@@ -3,7 +3,7 @@ export type {
3
3
  {{pluginNamePascal}}Item,
4
4
  Create{{pluginNamePascal}}Item,
5
5
  Update{{pluginNamePascal}}Item,
6
- } from "@checkstack/{{pluginBaseName}}-common";
6
+ } from "{{scoped pluginBaseName}}-common";
7
7
 
8
8
  // Re-export the Api definition for usePluginClient usage
9
- export { {{pluginNamePascal}}Api } from "@checkstack/{{pluginBaseName}}-common";
9
+ export { {{pluginNamePascal}}Api } from "{{scoped pluginBaseName}}-common";
@@ -1,5 +1,5 @@
1
1
  import { usePluginClient } from "@checkstack/frontend-api";
2
- import { {{pluginNamePascal}}Api } from "@checkstack/{{pluginBaseName}}-common";
2
+ import { {{pluginNamePascal}}Api } from "{{scoped pluginBaseName}}-common";
3
3
  import type { {{pluginNamePascal}}Item } from "../api";
4
4
  import { Button, Card, PageLayout } from "@checkstack/ui";
5
5
  import { useNavigate } from "react-router-dom";
@@ -1,15 +1,21 @@
1
1
  import { createFrontendPlugin } from "@checkstack/frontend-api";
2
- import { {{pluginNamePascal}}ListPage } from "./components/{{pluginNamePascal}}ListPage";
3
- import { {{pluginNameCamel}}Routes, pluginMetadata, {{pluginNameCamel}}Access } from "@checkstack/{{pluginBaseName}}-common";
2
+ import { {{pluginNameCamel}}Routes, pluginMetadata, {{pluginNameCamel}}Access } from "{{scoped pluginBaseName}}-common";
4
3
 
5
4
  export default createFrontendPlugin({
6
5
  metadata: pluginMetadata,
7
6
 
8
- // Register routes using typed route definitions
7
+ // Register routes using typed route definitions. Each route declares a `load`
8
+ // thunk: the framework code-splits the page and wraps it in Suspense + a
9
+ // per-plugin error boundary, so the page chunk is fetched on navigation
10
+ // (never in the initial app load). Use `load` for pages; light, always-on
11
+ // slot extensions (navbar/user-menu) use eager `component` instead.
9
12
  routes: [
10
13
  {
11
14
  route: {{pluginNameCamel}}Routes.routes.home,
12
- element: <{{pluginNamePascal}}ListPage />,
15
+ load: () =>
16
+ import("./components/{{pluginNamePascal}}ListPage").then((m) => ({
17
+ default: m.{{pluginNamePascal}}ListPage,
18
+ })),
13
19
  title: "{{pluginNamePascal}}",
14
20
  accessRule: {{pluginNameCamel}}Access.read,
15
21
  },
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ "{{scoped pluginBaseName}}-common": patch
3
+ "{{scoped pluginBaseName}}-backend": patch
4
+ "{{scoped pluginBaseName}}-frontend": patch
5
+ ---
6
+
7
+ Initial release of the {{pluginNamePascal}} plugin.
8
+
9
+ {{pluginDescription}}
@@ -0,0 +1,75 @@
1
+ # {{pluginNamePascal}} plugin
2
+
3
+ {{pluginDescription}}
4
+
5
+ This is a standalone [Checkstack](https://enyineer.github.io/checkstack/)
6
+ plugin workspace generated by `create-checkstack-plugin`. It contains a
7
+ `common` contract package, a `backend` package implementing it, and a
8
+ `frontend` package consuming it.
9
+
10
+ ## Prerequisites
11
+
12
+ - [Bun](https://bun.sh) 1.1 or newer.
13
+ - A local Postgres database. The dev server defaults to
14
+ `postgresql://checkstack:checkstack@localhost:5432/checkstack`. A quick
15
+ way to get one:
16
+
17
+ ```bash
18
+ docker run --name checkstack-pg -e POSTGRES_USER=checkstack \
19
+ -e POSTGRES_PASSWORD=checkstack -e POSTGRES_DB=checkstack \
20
+ -p 5432:5432 -d postgres:16-alpine
21
+ ```
22
+
23
+ No Redis is required: the dev server auto-loads in-memory queue and cache
24
+ providers.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ bun install
30
+ ```
31
+
32
+ ## Develop
33
+
34
+ ```bash
35
+ bun run dev
36
+ ```
37
+
38
+ This boots the Checkstack dev server with synthetic dev auth enabled, runs
39
+ the backend with hot reload, and starts the frontend Vite dev server. Every
40
+ access rule the plugin registers is auto-granted to a `dev-user`, so no
41
+ login is needed.
42
+
43
+ Once it is up, the plugin's CRUD API is served under `/api/{{pluginBaseName}}/*`.
44
+ For example, list the example `items`:
45
+
46
+ ```bash
47
+ curl -X POST http://localhost:3000/api/{{pluginBaseName}}/getItems \
48
+ -H 'content-type: application/json' \
49
+ -d '{}'
50
+ ```
51
+
52
+ ## What is in the box
53
+
54
+ - `packages/{{pluginBaseName}}-common` - the oRPC contract, Zod schemas, and
55
+ access rules shared by backend and frontend.
56
+ - `packages/{{pluginBaseName}}-backend` - a single `items` table (Drizzle)
57
+ plus CRUD procedures (`getItems`, `getItem`, `createItem`, `updateItem`,
58
+ `deleteItem`).
59
+ - `packages/{{pluginBaseName}}-frontend` - one list page wired to the typed
60
+ client.
61
+
62
+ > The default skeleton deliberately omits a reactive `defineEntity`
63
+ > state machine to keep first boot minimal. See the Checkstack docs on
64
+ > reactive entities when you are ready to add one.
65
+
66
+ ## Pack for distribution
67
+
68
+ ```bash
69
+ bun run pack
70
+ ```
71
+
72
+ This produces a bundle tarball under
73
+ `packages/{{pluginBaseName}}-backend/dist/` containing the backend primary
74
+ plus the `-common` and `-frontend` siblings, ready to upload via the Plugin
75
+ Manager.
@@ -0,0 +1,37 @@
1
+ import js from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+
5
+ // Standalone Checkstack plugin lint config. Mirrors the subset of the
6
+ // monorepo's rules that apply to plugin source. The monorepo-only custom
7
+ // `checkstack/*` rules and the project-references plumbing are intentionally
8
+ // omitted - they depend on the monorepo's internal tooling.
9
+ export default tseslint.config(
10
+ {
11
+ ignores: [
12
+ "**/node_modules/**",
13
+ "**/dist/**",
14
+ "**/.tsbuild/**",
15
+ "**/drizzle/**",
16
+ "**/*.test.ts",
17
+ "**/*.test.tsx",
18
+ "**/*.e2e.ts",
19
+ ],
20
+ },
21
+ js.configs.recommended,
22
+ ...tseslint.configs.recommended,
23
+ {
24
+ plugins: {
25
+ "react-hooks": reactHooks,
26
+ },
27
+ rules: {
28
+ "@typescript-eslint/no-explicit-any": "error",
29
+ "@typescript-eslint/no-unused-vars": [
30
+ "error",
31
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
32
+ ],
33
+ "react-hooks/rules-of-hooks": "error",
34
+ "react-hooks/exhaustive-deps": "warn",
35
+ },
36
+ },
37
+ );
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "{{pluginBaseName}}-plugin",
3
+ "version": "0.0.1",
4
+ "description": "{{pluginDescription}}",
5
+ "private": true,
6
+ "license": "Elastic-2.0",
7
+ "type": "module",
8
+ "workspaces": [
9
+ "packages/*"
10
+ ],
11
+ "scripts": {
12
+ "dev": "bun run --filter '{{scoped pluginBaseName}}-backend' dev",
13
+ "pack": "bun run --filter '{{scoped pluginBaseName}}-backend' pack -- --bundle",
14
+ "typecheck": "bun run --filter '*' typecheck",
15
+ "lint": "bun run --filter '*' lint",
16
+ "test": "bun run --filter '*' test",
17
+ "changeset": "changeset"
18
+ },
19
+ "devDependencies": {
20
+ "@changesets/cli": "^2.31.0",
21
+ "@eslint/js": "^9.39.4",
22
+ "eslint": "^9.39.4",
23
+ "eslint-plugin-react-hooks": "^7.1.1",
24
+ "typescript": "^5.7.2",
25
+ "typescript-eslint": "^8.59.3"
26
+ }
27
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "noEmit": true,
4
+ "composite": false
5
+ },
6
+ "files": [],
7
+ "include": [],
8
+ "references": [
9
+ { "path": "./packages/{{pluginBaseName}}-common" },
10
+ { "path": "./packages/{{pluginBaseName}}-backend" },
11
+ { "path": "./packages/{{pluginBaseName}}-frontend" }
12
+ ]
13
+ }
@@ -200,6 +200,26 @@ describe("CLI Template Scaffolding", () => {
200
200
  expect(pkg.scripts?.dev).toBe("checkstack-dev");
201
201
  }
202
202
  });
203
+
204
+ if (pluginType === "common") {
205
+ // Regression guard (#251 Phase 3): the common package's
206
+ // `definePluginMetadata({ pluginId })` MUST be the bare base name —
207
+ // the same value the backend's `checkstack.pluginId` uses for
208
+ // `/api/<pluginId>/*` routing. If this drifts to `<base>-common`
209
+ // (the `pluginName`), the plugin-metadata registry is keyed
210
+ // differently from the route and every request 500s with
211
+ // "Plugin metadata not found in registry". The end-to-end
212
+ // `*.it.test.ts` lane catches it too, but this pins it in the fast
213
+ // lane so the fix can't silently regress.
214
+ it("declares the routing pluginId (base name) in plugin-metadata.ts", () => {
215
+ const metaSrc = readFileSync(
216
+ path.join(targetDir, "src", "plugin-metadata.ts"),
217
+ "utf8",
218
+ );
219
+ expect(metaSrc).toContain(`pluginId:\n"${TEST_BASE_NAME}"`);
220
+ expect(metaSrc).not.toContain(`"${TEST_BASE_NAME}-common"`);
221
+ });
222
+ }
203
223
  });
204
224
  }
205
225
  });
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import React from "react";
3
+ import { renderToString } from "ink";
4
+ import { Spinner, SPINNER_FRAMES } from "./components.tsx";
5
+
6
+ describe("Spinner", () => {
7
+ it("renders the glyph for the given frame", () => {
8
+ expect(renderToString(<Spinner frame={0} />)).toContain(SPINNER_FRAMES[0]);
9
+ expect(renderToString(<Spinner frame={1} />)).toContain(SPINNER_FRAMES[1]);
10
+ });
11
+
12
+ it("wraps the frame index over the frame set", () => {
13
+ const len = SPINNER_FRAMES.length;
14
+ expect(renderToString(<Spinner frame={len} />)).toContain(
15
+ SPINNER_FRAMES[0],
16
+ );
17
+ expect(renderToString(<Spinner frame={len + 2} />)).toContain(
18
+ SPINNER_FRAMES[2],
19
+ );
20
+ });
21
+
22
+ it("is safe for a negative frame", () => {
23
+ // Should not throw or render `undefined`.
24
+ const frame = renderToString(<Spinner frame={-1} />);
25
+ expect(frame).toContain(SPINNER_FRAMES[SPINNER_FRAMES.length - 1]);
26
+ expect(frame).not.toContain("undefined");
27
+ });
28
+ });