create-backbone-template 0.1.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.
- package/README.md +33 -0
- package/bin/create-backbone-template.js +5 -0
- package/package.json +30 -0
- package/src/create-backbone-template.js +204 -0
- package/template/.agents/skills/agent-browser/SKILL.md +55 -0
- package/template/.agents/skills/create-plan/SKILL.md +52 -0
- package/template/.agents/skills/create-plan/agents/openai.yaml +4 -0
- package/template/.agents/skills/create-pr-presentation/SKILL.md +86 -0
- package/template/.agents/skills/create-pr-presentation/agents/openai.yaml +4 -0
- package/template/.agents/skills/implement-plan/SKILL.md +26 -0
- package/template/.agents/skills/implement-plan/agents/openai.yaml +4 -0
- package/template/.agents/skills/review-plan/SKILL.md +38 -0
- package/template/.agents/skills/review-plan/agents/openai.yaml +4 -0
- package/template/.env.schema +30 -0
- package/template/.env.test +6 -0
- package/template/.oxlintrc.json +67 -0
- package/template/.vscode/extensions.json +3 -0
- package/template/.vscode/settings.json +23 -0
- package/template/AGENTS.md +55 -0
- package/template/Cargo.lock +2648 -0
- package/template/Cargo.toml +29 -0
- package/template/Justfile +140 -0
- package/template/README.md +72 -0
- package/template/TODO.md +1 -0
- package/template/_gitignore +12 -0
- package/template/buf.gen.yaml +7 -0
- package/template/buf.yaml +10 -0
- package/template/client/.oxfmtrc.json +8 -0
- package/template/client/.oxlintrc.json +57 -0
- package/template/client/README.md +19 -0
- package/template/client/_gitignore +5 -0
- package/template/client/index.html +12 -0
- package/template/client/package.json +47 -0
- package/template/client/packages/design-system/package.json +19 -0
- package/template/client/packages/design-system/src/index.ts +2 -0
- package/template/client/packages/design-system-basic/package.json +18 -0
- package/template/client/packages/design-system-basic/src/button.stories.tsx +50 -0
- package/template/client/packages/design-system-basic/src/button.tsx +26 -0
- package/template/client/packages/design-system-basic/src/empty-state.stories.tsx +18 -0
- package/template/client/packages/design-system-basic/src/empty-state.tsx +17 -0
- package/template/client/packages/design-system-basic/src/form-field.stories.tsx +15 -0
- package/template/client/packages/design-system-basic/src/form-field.tsx +10 -0
- package/template/client/packages/design-system-basic/src/form.stories.tsx +27 -0
- package/template/client/packages/design-system-basic/src/form.tsx +9 -0
- package/template/client/packages/design-system-basic/src/heading.stories.tsx +14 -0
- package/template/client/packages/design-system-basic/src/heading.tsx +25 -0
- package/template/client/packages/design-system-basic/src/index.tsx +15 -0
- package/template/client/packages/design-system-basic/src/inline.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/inline.tsx +5 -0
- package/template/client/packages/design-system-basic/src/layout.stories.tsx +24 -0
- package/template/client/packages/design-system-basic/src/layout.tsx +14 -0
- package/template/client/packages/design-system-basic/src/loader.stories.tsx +8 -0
- package/template/client/packages/design-system-basic/src/loader.tsx +11 -0
- package/template/client/packages/design-system-basic/src/navigation.stories.tsx +16 -0
- package/template/client/packages/design-system-basic/src/navigation.tsx +18 -0
- package/template/client/packages/design-system-basic/src/notice.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/notice.tsx +5 -0
- package/template/client/packages/design-system-basic/src/stack.stories.tsx +17 -0
- package/template/client/packages/design-system-basic/src/stack.tsx +5 -0
- package/template/client/packages/design-system-basic/src/styles.css +254 -0
- package/template/client/packages/design-system-basic/src/text-input.stories.tsx +13 -0
- package/template/client/packages/design-system-basic/src/text-input.tsx +5 -0
- package/template/client/packages/design-system-basic/src/text.stories.tsx +21 -0
- package/template/client/packages/design-system-basic/src/text.tsx +5 -0
- package/template/client/packages/design-system-contract/package.json +15 -0
- package/template/client/packages/design-system-contract/src/button.ts +10 -0
- package/template/client/packages/design-system-contract/src/empty-state.ts +9 -0
- package/template/client/packages/design-system-contract/src/form-field.ts +9 -0
- package/template/client/packages/design-system-contract/src/form.ts +9 -0
- package/template/client/packages/design-system-contract/src/heading.ts +9 -0
- package/template/client/packages/design-system-contract/src/index.ts +13 -0
- package/template/client/packages/design-system-contract/src/inline.ts +7 -0
- package/template/client/packages/design-system-contract/src/layout.ts +8 -0
- package/template/client/packages/design-system-contract/src/loader.ts +7 -0
- package/template/client/packages/design-system-contract/src/navigation.ts +13 -0
- package/template/client/packages/design-system-contract/src/notice.ts +8 -0
- package/template/client/packages/design-system-contract/src/stack.ts +8 -0
- package/template/client/packages/design-system-contract/src/text-input.ts +5 -0
- package/template/client/packages/design-system-contract/src/text.ts +9 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/external-ui-import.tsx +5 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/raw-dom-jsx.tsx +3 -0
- package/template/client/packages/design-system-lint/fixtures/invalid/two-violations.tsx +7 -0
- package/template/client/packages/design-system-lint/fixtures/valid/design-system-only.tsx +13 -0
- package/template/client/packages/design-system-lint/package.json +23 -0
- package/template/client/packages/design-system-lint/src/check-design-system-architecture.ts +22 -0
- package/template/client/packages/design-system-lint/src/design-system-architecture.ts +286 -0
- package/template/client/packages/design-system-lint/src/oxlint-plugin.ts +11 -0
- package/template/client/packages/design-system-lint/src/page-architecture.ts +382 -0
- package/template/client/packages/design-system-lint/src/rules.ts +111 -0
- package/template/client/packages/design-system-lint/test/design-system-architecture.test.ts +243 -0
- package/template/client/packages/design-system-lint/test/oxlint-fixtures.test.ts +159 -0
- package/template/client/packages/design-system-lint/test/page-architecture.test.ts +175 -0
- package/template/client/packages/design-system-lint/test/rules.test.ts +65 -0
- package/template/client/packages/design-system-lint/tsconfig.json +29 -0
- package/template/client/src/App.tsx +77 -0
- package/template/client/src/design-system-components.test.tsx +75 -0
- package/template/client/src/gen/helloworld/v1/helloworld_pb.ts +63 -0
- package/template/client/src/main.tsx +18 -0
- package/template/client/src/pages/hello/hello-page.stories.tsx +20 -0
- package/template/client/src/pages/hello/hello-page.test.tsx +90 -0
- package/template/client/src/pages/hello/hello-page.tsx +126 -0
- package/template/client/src/pages/page.ts +20 -0
- package/template/client/src/testing/create-preview-events.test.ts +36 -0
- package/template/client/src/testing/create-preview-events.ts +30 -0
- package/template/client/src/vite-env.d.ts +1 -0
- package/template/client/tsconfig.json +32 -0
- package/template/client/vite.config.ts +21 -0
- package/template/client/vite.ladle.config.ts +5 -0
- package/template/e2e/.gherkin-lintrc +20 -0
- package/template/e2e/.oxfmtrc.json +15 -0
- package/template/e2e/.oxlintrc.json +37 -0
- package/template/e2e/_gitignore +4 -0
- package/template/e2e/features/helloworld.feature +10 -0
- package/template/e2e/package.json +42 -0
- package/template/e2e/playwright.config.ts +16 -0
- package/template/e2e/support/app-gherkin.ts +4 -0
- package/template/e2e/support/fixtures.ts +236 -0
- package/template/e2e/support/gherkin-fixtures/duplicate-id.feature +9 -0
- package/template/e2e/support/gherkin-fixtures/duplicate-id.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/extra-implementation.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/extra-step.spec.ts +10 -0
- package/template/e2e/support/gherkin-fixtures/happy-path.spec.ts +4 -0
- package/template/e2e/support/gherkin-fixtures/missing-id.feature +4 -0
- package/template/e2e/support/gherkin-fixtures/missing-id.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/missing-implementation.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/missing-step.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/playwright.config.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/scenario-outline.feature +9 -0
- package/template/e2e/support/gherkin-fixtures/scenario-outline.spec.ts +7 -0
- package/template/e2e/support/gherkin-fixtures/step-mismatch.spec.ts +9 -0
- package/template/e2e/support/gherkin-fixtures/valid-implementations.ts +23 -0
- package/template/e2e/support/gherkin-fixtures/valid-scenarios.feature +26 -0
- package/template/e2e/support/gherkin.test.ts +184 -0
- package/template/e2e/support/gherkin.ts +321 -0
- package/template/e2e/support/oxlint-plugin.test.ts +328 -0
- package/template/e2e/support/oxlint-plugin.ts +485 -0
- package/template/e2e/tests/helloworld.spec.ts +39 -0
- package/template/e2e/tsconfig.json +26 -0
- package/template/e2e/tsconfig.oxlint-plugin.json +12 -0
- package/template/package.json +9 -0
- package/template/pnpm-lock.yaml +10723 -0
- package/template/pnpm-workspace.yaml +8 -0
- package/template/pr-slide/README.md +95 -0
- package/template/pr-slide/package.json +23 -0
- package/template/pr-slide/src/cli.js +262 -0
- package/template/pr-slide/src/generate-pr-deck.js +833 -0
- package/template/pr-slide/src/git-context.js +91 -0
- package/template/pr-slide/src/presentation-paths.js +9 -0
- package/template/pr-slide/src/presentations.js +53 -0
- package/template/pr-slide/test/generate-pr-deck.test.js +118 -0
- package/template/pr-slide/test/presentation-paths.test.js +14 -0
- package/template/pr-slide/test/presentations.test.js +50 -0
- package/template/proto/helloworld/v1/helloworld.proto +15 -0
- package/template/scripts/run-e2e.sh +10 -0
- package/template/server/Cargo.toml +26 -0
- package/template/server/build.rs +9 -0
- package/template/server/dylint/backbone_server_lints/.cargo/config.toml +6 -0
- package/template/server/dylint/backbone_server_lints/Cargo.lock +1581 -0
- package/template/server/dylint/backbone_server_lints/Cargo.toml +21 -0
- package/template/server/dylint/backbone_server_lints/README.md +5 -0
- package/template/server/dylint/backbone_server_lints/_gitignore +1 -0
- package/template/server/dylint/backbone_server_lints/rust-toolchain +3 -0
- package/template/server/dylint/backbone_server_lints/src/lib.rs +612 -0
- package/template/server/dylint/backbone_server_lints/ui/lib.rs +4 -0
- package/template/server/dylint/backbone_server_lints/ui/lib.stderr +10 -0
- package/template/server/dylint/backbone_server_lints/ui/long_file.rs +303 -0
- package/template/server/dylint/backbone_server_lints/ui/long_file.stderr +6 -0
- package/template/server/dylint/backbone_server_lints/ui/main.rs +59 -0
- package/template/server/dylint/backbone_server_lints/ui/main.stderr +85 -0
- package/template/server/migrations/20260520120000_create_projects.sql +12 -0
- package/template/server/migrations/20260524160000_create_hello_world_inputs.sql +12 -0
- package/template/server/src/config.rs +27 -0
- package/template/server/src/db/hello_world.rs +34 -0
- package/template/server/src/db/hello_world_tests.rs +11 -0
- package/template/server/src/db/mod.rs +39 -0
- package/template/server/src/lib.rs +10 -0
- package/template/server/src/main.rs +43 -0
- package/template/server/src/rpc/greeter/mod.rs +31 -0
- package/template/server/src/rpc/greeter/say_hello.rs +27 -0
- package/template/server/src/rpc/mod.rs +8 -0
- package/template/server/src/state.rs +13 -0
- package/template/skills-lock.json +11 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createClient } from "@connectrpc/connect"
|
|
2
|
+
import { createConnectTransport } from "@connectrpc/connect-web"
|
|
3
|
+
import { Navigation } from "@backbone/design-system"
|
|
4
|
+
import { useMemo, useState } from "react"
|
|
5
|
+
import { Route, Routes } from "react-router"
|
|
6
|
+
import { GreeterService } from "./gen/helloworld/v1/helloworld_pb"
|
|
7
|
+
import {
|
|
8
|
+
HelloPage,
|
|
9
|
+
type HelloPageDynamicProps,
|
|
10
|
+
type HelloPageStaticProps,
|
|
11
|
+
} from "./pages/hello/hello-page"
|
|
12
|
+
|
|
13
|
+
function requireEnv(name: string): string {
|
|
14
|
+
const value = import.meta.env[name]
|
|
15
|
+
|
|
16
|
+
if (value === undefined || value === "") {
|
|
17
|
+
throw new Error(`Missing required environment variable: ${name}`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const transport = createConnectTransport({
|
|
24
|
+
baseUrl: requireEnv("VITE_SERVER_URL"),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export function App() {
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<Navigation currentHref="/" items={[{ href: "/", label: "Hello" }]} />
|
|
31
|
+
<Routes>
|
|
32
|
+
<Route element={<HelloRoute />} path="/" />
|
|
33
|
+
</Routes>
|
|
34
|
+
</>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function HelloRoute() {
|
|
39
|
+
const greeter = useMemo(() => createClient(GreeterService, transport), [])
|
|
40
|
+
const [name, setName] = useState("World")
|
|
41
|
+
const [greeting, setGreeting] = useState("Hello, World!")
|
|
42
|
+
const [isCalling, setIsCalling] = useState(false)
|
|
43
|
+
const [error, setError] = useState<string | null>(null)
|
|
44
|
+
|
|
45
|
+
const staticProps: HelloPageStaticProps = {
|
|
46
|
+
eyebrow: "Backbone",
|
|
47
|
+
title: "ConnectRPC helloworld",
|
|
48
|
+
greeting,
|
|
49
|
+
name,
|
|
50
|
+
nameLabel: "Name",
|
|
51
|
+
namePlaceholder: "World",
|
|
52
|
+
submitLabel: isCalling ? "Calling..." : "Say hello",
|
|
53
|
+
isSubmitting: isCalling,
|
|
54
|
+
error,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const dynamicProps: HelloPageDynamicProps = {
|
|
58
|
+
onNameChanged: setName,
|
|
59
|
+
onSubmitted: sayHello,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function sayHello() {
|
|
63
|
+
setIsCalling(true)
|
|
64
|
+
setError(null)
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await greeter.sayHello({ name })
|
|
68
|
+
setGreeting(response.greeting)
|
|
69
|
+
} catch (caught) {
|
|
70
|
+
setError(caught instanceof Error ? caught.message : "Request failed")
|
|
71
|
+
} finally {
|
|
72
|
+
setIsCalling(false)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return <HelloPage {...staticProps} {...dynamicProps} />
|
|
77
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { Button, EmptyState, Loader, Navigation } from "@backbone/design-system"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { describe, test } from "vitest"
|
|
5
|
+
|
|
6
|
+
describe("Button", () => {
|
|
7
|
+
test("renders primary, secondary, and danger variants", () => {
|
|
8
|
+
const primary = renderToStaticMarkup(<Button>Say hello</Button>)
|
|
9
|
+
const secondary = renderToStaticMarkup(<Button variant="secondary">View history</Button>)
|
|
10
|
+
const danger = renderToStaticMarkup(<Button variant="danger">Clear history</Button>)
|
|
11
|
+
|
|
12
|
+
assert.match(primary, /ds-button--primary/)
|
|
13
|
+
assert.match(secondary, /ds-button--secondary/)
|
|
14
|
+
assert.match(danger, /ds-button--danger/)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("renders loading state as disabled without hiding the label", () => {
|
|
18
|
+
const html = renderToStaticMarkup(<Button loading>Importing...</Button>)
|
|
19
|
+
|
|
20
|
+
assert.match(html, /ds-button--loading/)
|
|
21
|
+
assert.match(html, /disabled/)
|
|
22
|
+
assert.match(html, /Importing/)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe("EmptyState", () => {
|
|
27
|
+
test("renders title and description", () => {
|
|
28
|
+
const html = renderToStaticMarkup(
|
|
29
|
+
<EmptyState description="Say hello to create the first saved input." title="No inputs yet" />,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assert.match(html, /No inputs yet/)
|
|
33
|
+
assert.match(html, /Say hello/)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("renders an optional action", () => {
|
|
37
|
+
const html = renderToStaticMarkup(
|
|
38
|
+
<EmptyState
|
|
39
|
+
action={<Button>Say hello</Button>}
|
|
40
|
+
description="Create a saved hello-world input."
|
|
41
|
+
title="No inputs yet"
|
|
42
|
+
/>,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert.match(html, /ds-empty-state__actions/)
|
|
46
|
+
assert.match(html, /Say hello/)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("Loader", () => {
|
|
51
|
+
test("renders a configurable loading message", () => {
|
|
52
|
+
const html = renderToStaticMarkup(<Loader message="Loading hello-world inputs..." />)
|
|
53
|
+
|
|
54
|
+
assert.match(html, /ds-loader/)
|
|
55
|
+
assert.match(html, /Loading hello-world inputs/)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe("Navigation", () => {
|
|
60
|
+
test("renders navigation links", () => {
|
|
61
|
+
const html = renderToStaticMarkup(
|
|
62
|
+
<Navigation
|
|
63
|
+
currentHref="/hello"
|
|
64
|
+
items={[
|
|
65
|
+
{ href: "/hello", label: "Hello" },
|
|
66
|
+
{ href: "/history", label: "History" },
|
|
67
|
+
]}
|
|
68
|
+
/>,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
assert.match(html, /href="\/hello"/)
|
|
72
|
+
assert.match(html, /href="\/history"/)
|
|
73
|
+
assert.match(html, /aria-current="page"/)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @generated by protoc-gen-es v2.12.0 with parameter "target=ts"
|
|
2
|
+
// @generated from file helloworld/v1/helloworld.proto (package helloworld.v1, syntax proto3)
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
|
|
5
|
+
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
|
|
6
|
+
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
|
|
7
|
+
import type { Message } from "@bufbuild/protobuf";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Describes the file helloworld/v1/helloworld.proto.
|
|
11
|
+
*/
|
|
12
|
+
export const file_helloworld_v1_helloworld: GenFile = /*@__PURE__*/
|
|
13
|
+
fileDesc("Ch5oZWxsb3dvcmxkL3YxL2hlbGxvd29ybGQucHJvdG8SDWhlbGxvd29ybGQudjEiHwoPU2F5SGVsbG9SZXF1ZXN0EgwKBG5hbWUYASABKAkiJAoQU2F5SGVsbG9SZXNwb25zZRIQCghncmVldGluZxgBIAEoCTJdCg5HcmVldGVyU2VydmljZRJLCghTYXlIZWxsbxIeLmhlbGxvd29ybGQudjEuU2F5SGVsbG9SZXF1ZXN0Gh8uaGVsbG93b3JsZC52MS5TYXlIZWxsb1Jlc3BvbnNlYgZwcm90bzM");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @generated from message helloworld.v1.SayHelloRequest
|
|
17
|
+
*/
|
|
18
|
+
export type SayHelloRequest = Message<"helloworld.v1.SayHelloRequest"> & {
|
|
19
|
+
/**
|
|
20
|
+
* @generated from field: string name = 1;
|
|
21
|
+
*/
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Describes the message helloworld.v1.SayHelloRequest.
|
|
27
|
+
* Use `create(SayHelloRequestSchema)` to create a new message.
|
|
28
|
+
*/
|
|
29
|
+
export const SayHelloRequestSchema: GenMessage<SayHelloRequest> = /*@__PURE__*/
|
|
30
|
+
messageDesc(file_helloworld_v1_helloworld, 0);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @generated from message helloworld.v1.SayHelloResponse
|
|
34
|
+
*/
|
|
35
|
+
export type SayHelloResponse = Message<"helloworld.v1.SayHelloResponse"> & {
|
|
36
|
+
/**
|
|
37
|
+
* @generated from field: string greeting = 1;
|
|
38
|
+
*/
|
|
39
|
+
greeting: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Describes the message helloworld.v1.SayHelloResponse.
|
|
44
|
+
* Use `create(SayHelloResponseSchema)` to create a new message.
|
|
45
|
+
*/
|
|
46
|
+
export const SayHelloResponseSchema: GenMessage<SayHelloResponse> = /*@__PURE__*/
|
|
47
|
+
messageDesc(file_helloworld_v1_helloworld, 1);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @generated from service helloworld.v1.GreeterService
|
|
51
|
+
*/
|
|
52
|
+
export const GreeterService: GenService<{
|
|
53
|
+
/**
|
|
54
|
+
* @generated from rpc helloworld.v1.GreeterService.SayHello
|
|
55
|
+
*/
|
|
56
|
+
sayHello: {
|
|
57
|
+
methodKind: "unary";
|
|
58
|
+
input: typeof SayHelloRequestSchema;
|
|
59
|
+
output: typeof SayHelloResponseSchema;
|
|
60
|
+
},
|
|
61
|
+
}> = /*@__PURE__*/
|
|
62
|
+
serviceDesc(file_helloworld_v1_helloworld, 0);
|
|
63
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StrictMode } from "react"
|
|
2
|
+
import { createRoot } from "react-dom/client"
|
|
3
|
+
import { BrowserRouter } from "react-router"
|
|
4
|
+
import { App } from "./App"
|
|
5
|
+
|
|
6
|
+
const rootElement = document.getElementById("root")
|
|
7
|
+
|
|
8
|
+
if (rootElement === null) {
|
|
9
|
+
throw new Error("Root element not found")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
createRoot(rootElement).render(
|
|
13
|
+
<StrictMode>
|
|
14
|
+
<BrowserRouter>
|
|
15
|
+
<App />
|
|
16
|
+
</BrowserRouter>
|
|
17
|
+
</StrictMode>,
|
|
18
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createPreviewDynamicProps } from "../../testing/create-preview-events"
|
|
2
|
+
import {
|
|
3
|
+
HelloPage,
|
|
4
|
+
type HelloPageDynamicProps,
|
|
5
|
+
helloPageDynamicPropKeys,
|
|
6
|
+
helloPagePreviewStates,
|
|
7
|
+
} from "./hello-page"
|
|
8
|
+
|
|
9
|
+
const { dynamicProps } = createPreviewDynamicProps<HelloPageDynamicProps>(
|
|
10
|
+
helloPageDynamicPropKeys,
|
|
11
|
+
{
|
|
12
|
+
record: true,
|
|
13
|
+
},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
export const Ready = () => <HelloPage {...helloPagePreviewStates.ready} {...dynamicProps} />
|
|
17
|
+
|
|
18
|
+
export const Calling = () => <HelloPage {...helloPagePreviewStates.calling} {...dynamicProps} />
|
|
19
|
+
|
|
20
|
+
export const Error = () => <HelloPage {...helloPagePreviewStates.error} {...dynamicProps} />
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import type { ReactElement } from "react"
|
|
3
|
+
import { renderToStaticMarkup } from "react-dom/server"
|
|
4
|
+
import { describe, test } from "vitest"
|
|
5
|
+
import { HelloPage, type HelloPageDynamicProps, type HelloPageStaticProps } from "./hello-page"
|
|
6
|
+
|
|
7
|
+
type ElementWithProps<Props> = ReactElement<Props>
|
|
8
|
+
|
|
9
|
+
const defaultStaticProps: HelloPageStaticProps = {
|
|
10
|
+
eyebrow: "Backbone",
|
|
11
|
+
title: "ConnectRPC helloworld",
|
|
12
|
+
greeting: "Hello, World!",
|
|
13
|
+
name: "World",
|
|
14
|
+
nameLabel: "Name",
|
|
15
|
+
namePlaceholder: "World",
|
|
16
|
+
submitLabel: "Say hello",
|
|
17
|
+
isSubmitting: false,
|
|
18
|
+
error: null,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createDynamicProps(): HelloPageDynamicProps & {
|
|
22
|
+
calls: Array<{ name: keyof HelloPageDynamicProps; args: unknown[] }>
|
|
23
|
+
} {
|
|
24
|
+
const calls: Array<{ name: keyof HelloPageDynamicProps; args: unknown[] }> = []
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
calls,
|
|
28
|
+
onNameChanged(value) {
|
|
29
|
+
calls.push({ name: "onNameChanged", args: [value] })
|
|
30
|
+
},
|
|
31
|
+
onSubmitted() {
|
|
32
|
+
calls.push({ name: "onSubmitted", args: [] })
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("HelloPage", () => {
|
|
38
|
+
test("renders from plain params", () => {
|
|
39
|
+
const html = renderToStaticMarkup(
|
|
40
|
+
<HelloPage {...defaultStaticProps} {...createDynamicProps()} />,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert.match(html, /ConnectRPC helloworld/)
|
|
44
|
+
assert.match(html, /Hello, World!/)
|
|
45
|
+
assert.match(html, /value="World"/)
|
|
46
|
+
assert.match(html, /Say hello/)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("wires input and form events", () => {
|
|
50
|
+
const dynamicProps = createDynamicProps()
|
|
51
|
+
const element = HelloPage({ ...defaultStaticProps, ...dynamicProps })
|
|
52
|
+
const layoutProps = element.props as { middle: ReactElement }
|
|
53
|
+
const outerStack = layoutProps.middle as ElementWithProps<{
|
|
54
|
+
children: ReactElement[]
|
|
55
|
+
}>
|
|
56
|
+
const form = outerStack.props.children[1] as ElementWithProps<{
|
|
57
|
+
children: ReactElement
|
|
58
|
+
onSubmit(event: { preventDefault(): void }): void
|
|
59
|
+
}>
|
|
60
|
+
const formField = form.props.children as ElementWithProps<{
|
|
61
|
+
children: ReactElement
|
|
62
|
+
}>
|
|
63
|
+
const inline = formField.props.children as ElementWithProps<{
|
|
64
|
+
children: ReactElement[]
|
|
65
|
+
}>
|
|
66
|
+
const input = inline.props.children[0] as ElementWithProps<{
|
|
67
|
+
onChange(event: { target: { value: string } }): void
|
|
68
|
+
}>
|
|
69
|
+
|
|
70
|
+
input.props.onChange({ target: { value: "Alice" } })
|
|
71
|
+
form.props.onSubmit({ preventDefault() {} })
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(dynamicProps.calls, [
|
|
74
|
+
{ name: "onNameChanged", args: ["Alice"] },
|
|
75
|
+
{ name: "onSubmitted", args: [] },
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test("renders error notice only when error is present", () => {
|
|
80
|
+
const withoutError = renderToStaticMarkup(
|
|
81
|
+
<HelloPage {...defaultStaticProps} {...createDynamicProps()} />,
|
|
82
|
+
)
|
|
83
|
+
const withError = renderToStaticMarkup(
|
|
84
|
+
<HelloPage {...defaultStaticProps} {...createDynamicProps()} error="Request failed" />,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
assert.doesNotMatch(withoutError, /Request failed/)
|
|
88
|
+
assert.match(withError, /Request failed/)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Button,
|
|
3
|
+
Form,
|
|
4
|
+
FormField,
|
|
5
|
+
Heading,
|
|
6
|
+
Inline,
|
|
7
|
+
Layout,
|
|
8
|
+
Notice,
|
|
9
|
+
Stack,
|
|
10
|
+
Text,
|
|
11
|
+
TextInput,
|
|
12
|
+
} from "@backbone/design-system"
|
|
13
|
+
import type { Page } from "../page"
|
|
14
|
+
|
|
15
|
+
export type HelloPageStaticProps = {
|
|
16
|
+
eyebrow: string
|
|
17
|
+
title: string
|
|
18
|
+
greeting: string
|
|
19
|
+
name: string
|
|
20
|
+
nameLabel: string
|
|
21
|
+
namePlaceholder: string
|
|
22
|
+
submitLabel: string
|
|
23
|
+
isSubmitting: boolean
|
|
24
|
+
error: string | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type HelloPageDynamicProps = {
|
|
28
|
+
onNameChanged(value: string): void
|
|
29
|
+
onSubmitted(): void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const helloPageDynamicPropKeys = ["onNameChanged", "onSubmitted"] as const
|
|
33
|
+
|
|
34
|
+
export const helloPagePreviewStates = {
|
|
35
|
+
ready: {
|
|
36
|
+
eyebrow: "Backbone",
|
|
37
|
+
title: "ConnectRPC helloworld",
|
|
38
|
+
greeting: "Hello, World!",
|
|
39
|
+
name: "World",
|
|
40
|
+
nameLabel: "Name",
|
|
41
|
+
namePlaceholder: "World",
|
|
42
|
+
submitLabel: "Say hello",
|
|
43
|
+
isSubmitting: false,
|
|
44
|
+
error: null,
|
|
45
|
+
},
|
|
46
|
+
calling: {
|
|
47
|
+
eyebrow: "Backbone",
|
|
48
|
+
title: "ConnectRPC helloworld",
|
|
49
|
+
greeting: "Hello, World!",
|
|
50
|
+
name: "World",
|
|
51
|
+
nameLabel: "Name",
|
|
52
|
+
namePlaceholder: "World",
|
|
53
|
+
submitLabel: "Calling...",
|
|
54
|
+
isSubmitting: true,
|
|
55
|
+
error: null,
|
|
56
|
+
},
|
|
57
|
+
error: {
|
|
58
|
+
eyebrow: "Backbone",
|
|
59
|
+
title: "ConnectRPC helloworld",
|
|
60
|
+
greeting: "Hello, World!",
|
|
61
|
+
name: "World",
|
|
62
|
+
nameLabel: "Name",
|
|
63
|
+
namePlaceholder: "World",
|
|
64
|
+
submitLabel: "Say hello",
|
|
65
|
+
isSubmitting: false,
|
|
66
|
+
error: "Request failed",
|
|
67
|
+
},
|
|
68
|
+
} satisfies Record<string, HelloPageStaticProps>
|
|
69
|
+
|
|
70
|
+
export type HelloPageProps = HelloPageStaticProps & HelloPageDynamicProps
|
|
71
|
+
|
|
72
|
+
export const HelloPage: Page<HelloPageStaticProps, HelloPageDynamicProps> = ({
|
|
73
|
+
eyebrow,
|
|
74
|
+
title,
|
|
75
|
+
greeting,
|
|
76
|
+
name,
|
|
77
|
+
nameLabel,
|
|
78
|
+
namePlaceholder,
|
|
79
|
+
submitLabel,
|
|
80
|
+
isSubmitting,
|
|
81
|
+
error,
|
|
82
|
+
onNameChanged,
|
|
83
|
+
onSubmitted,
|
|
84
|
+
}) => {
|
|
85
|
+
return (
|
|
86
|
+
<Layout
|
|
87
|
+
middle={
|
|
88
|
+
<Stack gap="lg">
|
|
89
|
+
<Stack gap="sm">
|
|
90
|
+
<Text variant="eyebrow">{eyebrow}</Text>
|
|
91
|
+
<Heading id="app-title">{title}</Heading>
|
|
92
|
+
<Text tone="muted" variant="lede">
|
|
93
|
+
{greeting}
|
|
94
|
+
</Text>
|
|
95
|
+
</Stack>
|
|
96
|
+
|
|
97
|
+
<Form
|
|
98
|
+
ariaLabelledBy="app-title"
|
|
99
|
+
onSubmit={(event) => {
|
|
100
|
+
event.preventDefault()
|
|
101
|
+
onSubmitted()
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<FormField inputId="name" label={nameLabel}>
|
|
105
|
+
<Inline>
|
|
106
|
+
<TextInput
|
|
107
|
+
id="name"
|
|
108
|
+
name="name"
|
|
109
|
+
onChange={(event) => onNameChanged(event.target.value)}
|
|
110
|
+
placeholder={namePlaceholder}
|
|
111
|
+
value={name}
|
|
112
|
+
/>
|
|
113
|
+
<Button disabled={isSubmitting} type="submit">
|
|
114
|
+
{submitLabel}
|
|
115
|
+
</Button>
|
|
116
|
+
</Inline>
|
|
117
|
+
</FormField>
|
|
118
|
+
</Form>
|
|
119
|
+
|
|
120
|
+
{error && <Notice tone="danger">{error}</Notice>}
|
|
121
|
+
</Stack>
|
|
122
|
+
}
|
|
123
|
+
middleWidthPx={680}
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
|
|
3
|
+
export type SerializableValue =
|
|
4
|
+
| string
|
|
5
|
+
| number
|
|
6
|
+
| boolean
|
|
7
|
+
| null
|
|
8
|
+
| readonly SerializableValue[]
|
|
9
|
+
| { readonly [key: string]: SerializableValue }
|
|
10
|
+
|
|
11
|
+
type SerializableProps<TProps> = {
|
|
12
|
+
[Key in keyof TProps]: TProps[Key] extends SerializableValue ? TProps[Key] : never
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PageProps<StaticProps, DynamicProps> = StaticProps & DynamicProps
|
|
16
|
+
|
|
17
|
+
export type Page<
|
|
18
|
+
StaticProps extends SerializableProps<StaticProps>,
|
|
19
|
+
DynamicProps extends object,
|
|
20
|
+
> = (props: PageProps<StaticProps, DynamicProps>) => ReactElement
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { describe, test } from "vitest"
|
|
3
|
+
import { createPreviewDynamicProps } from "./create-preview-events"
|
|
4
|
+
|
|
5
|
+
type ExampleDynamicProps = {
|
|
6
|
+
onNameChanged(value: string): void
|
|
7
|
+
onSubmitted(): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const dynamicPropKeys = ["onNameChanged", "onSubmitted"] as const
|
|
11
|
+
|
|
12
|
+
describe("createPreviewDynamicProps", () => {
|
|
13
|
+
test("creates default functions for required dynamic prop keys", () => {
|
|
14
|
+
const { dynamicProps } = createPreviewDynamicProps<ExampleDynamicProps>(dynamicPropKeys)
|
|
15
|
+
|
|
16
|
+
assert.equal(typeof dynamicProps.onNameChanged, "function")
|
|
17
|
+
assert.equal(typeof dynamicProps.onSubmitted, "function")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test("records dynamic prop names and args", () => {
|
|
21
|
+
const { calls, dynamicProps } = createPreviewDynamicProps<ExampleDynamicProps>(
|
|
22
|
+
dynamicPropKeys,
|
|
23
|
+
{
|
|
24
|
+
record: true,
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
dynamicProps.onNameChanged("Alice")
|
|
29
|
+
dynamicProps.onSubmitted()
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(calls, [
|
|
32
|
+
{ name: "onNameChanged", args: ["Alice"] },
|
|
33
|
+
{ name: "onSubmitted", args: [] },
|
|
34
|
+
])
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type DynamicPropKey<TDynamicProps> = keyof TDynamicProps & string
|
|
2
|
+
|
|
3
|
+
export type PreviewDynamicPropCall<TDynamicProps> = {
|
|
4
|
+
name: DynamicPropKey<TDynamicProps>
|
|
5
|
+
args: unknown[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CreatePreviewDynamicPropsOptions = {
|
|
9
|
+
record?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createPreviewDynamicProps<TDynamicProps extends object>(
|
|
13
|
+
dynamicPropKeys: readonly DynamicPropKey<TDynamicProps>[],
|
|
14
|
+
options: CreatePreviewDynamicPropsOptions = {},
|
|
15
|
+
) {
|
|
16
|
+
const calls: Array<PreviewDynamicPropCall<TDynamicProps>> = []
|
|
17
|
+
const dynamicProps = {} as TDynamicProps
|
|
18
|
+
|
|
19
|
+
for (const dynamicPropKey of dynamicPropKeys) {
|
|
20
|
+
Object.assign(dynamicProps, {
|
|
21
|
+
[dynamicPropKey]: (...args: unknown[]) => {
|
|
22
|
+
if (options.record === true) {
|
|
23
|
+
calls.push({ name: dynamicPropKey, args })
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { calls, dynamicProps }
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"exactOptionalPropertyTypes": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUnusedLocals": true,
|
|
17
|
+
"noUnusedParameters": true,
|
|
18
|
+
"noUncheckedSideEffectImports": true,
|
|
19
|
+
"verbatimModuleSyntax": true,
|
|
20
|
+
"moduleDetection": "force",
|
|
21
|
+
"allowUnreachableCode": false,
|
|
22
|
+
"allowUnusedLabels": false,
|
|
23
|
+
"forceConsistentCasingInFileNames": true,
|
|
24
|
+
"module": "ESNext",
|
|
25
|
+
"moduleResolution": "Bundler",
|
|
26
|
+
"resolveJsonModule": true,
|
|
27
|
+
"isolatedModules": true,
|
|
28
|
+
"noEmit": true,
|
|
29
|
+
"jsx": "react-jsx"
|
|
30
|
+
},
|
|
31
|
+
"include": ["src", "vite.config.ts", "vite.ladle.config.ts"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react"
|
|
2
|
+
import { defineConfig } from "vite"
|
|
3
|
+
|
|
4
|
+
const viteDevServerPort = process.env["VITE_DEV_SERVER_PORT"]
|
|
5
|
+
const devServerPort =
|
|
6
|
+
viteDevServerPort === undefined || viteDevServerPort === ""
|
|
7
|
+
? undefined
|
|
8
|
+
: Number(viteDevServerPort)
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [react()],
|
|
12
|
+
server:
|
|
13
|
+
devServerPort === undefined
|
|
14
|
+
? {
|
|
15
|
+
host: "127.0.0.1",
|
|
16
|
+
}
|
|
17
|
+
: {
|
|
18
|
+
host: "127.0.0.1",
|
|
19
|
+
port: devServerPort,
|
|
20
|
+
},
|
|
21
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"allowed-tags": ["on", { "patterns": ["^@id:[a-z0-9]+(?:[.-][a-z0-9]+)*$"] }],
|
|
3
|
+
"file-name": ["on", { "style": "kebab-case" }],
|
|
4
|
+
"indentation": ["on", { "Feature": 0, "Scenario": 2, "Step": 4, "scenario tag": 2 }],
|
|
5
|
+
"keywords-in-logical-order": "on",
|
|
6
|
+
"name-length": ["on", { "Feature": 80, "Scenario": 80, "Step": 100 }],
|
|
7
|
+
"new-line-at-eof": ["on", "yes"],
|
|
8
|
+
"no-dupe-scenario-names": "on",
|
|
9
|
+
"no-duplicate-tags": "on",
|
|
10
|
+
"no-empty-file": "on",
|
|
11
|
+
"no-files-without-scenarios": "on",
|
|
12
|
+
"no-multiple-empty-lines": "on",
|
|
13
|
+
"no-scenario-outlines-without-examples": "on",
|
|
14
|
+
"no-trailing-spaces": "on",
|
|
15
|
+
"no-unnamed-features": "on",
|
|
16
|
+
"no-unnamed-scenarios": "on",
|
|
17
|
+
"one-space-between-tags": "on",
|
|
18
|
+
"only-one-when": "on",
|
|
19
|
+
"scenario-size": ["on", { "steps-length": 8 }]
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ignorePatterns": [
|
|
3
|
+
"node_modules/**",
|
|
4
|
+
"playwright-report/**",
|
|
5
|
+
"support/dist/**",
|
|
6
|
+
"support/gherkin-fixtures/**",
|
|
7
|
+
"support/gherkin.test.ts",
|
|
8
|
+
"test-results/**"
|
|
9
|
+
],
|
|
10
|
+
"printWidth": 100,
|
|
11
|
+
"semi": false,
|
|
12
|
+
"singleQuote": false,
|
|
13
|
+
"tabWidth": 2,
|
|
14
|
+
"trailingComma": "all"
|
|
15
|
+
}
|