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,382 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import ts from "typescript"
|
|
4
|
+
|
|
5
|
+
export type PageArchitecturePaths = {
|
|
6
|
+
pagesSrcDir: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type PageArchitectureResult = {
|
|
10
|
+
errors: string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PageFile = {
|
|
14
|
+
absolutePath: string
|
|
15
|
+
pageName: string
|
|
16
|
+
relativePath: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type PageNames = {
|
|
20
|
+
component: string
|
|
21
|
+
dynamicPropKeys: string
|
|
22
|
+
dynamicProps: string
|
|
23
|
+
staticProps: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function checkPageArchitecture(paths: PageArchitecturePaths): PageArchitectureResult {
|
|
27
|
+
const errors: string[] = []
|
|
28
|
+
|
|
29
|
+
for (const pageFile of findPageFiles(paths.pagesSrcDir)) {
|
|
30
|
+
errors.push(...checkPageFile(pageFile))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { errors }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findPageFiles(pagesSrcDir: string): PageFile[] {
|
|
37
|
+
if (!fs.existsSync(pagesSrcDir)) {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return findFiles(pagesSrcDir)
|
|
42
|
+
.filter((filePath) => filePath.endsWith("-page.tsx"))
|
|
43
|
+
.map((absolutePath) => {
|
|
44
|
+
const relativePath = normalizePath(path.relative(pagesSrcDir, absolutePath))
|
|
45
|
+
const pageName = path.basename(absolutePath, "-page.tsx")
|
|
46
|
+
|
|
47
|
+
return { absolutePath, pageName, relativePath }
|
|
48
|
+
})
|
|
49
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function checkPageFile(pageFile: PageFile) {
|
|
53
|
+
const sourceText = fs.readFileSync(pageFile.absolutePath, "utf8")
|
|
54
|
+
const sourceFile = ts.createSourceFile(
|
|
55
|
+
pageFile.absolutePath,
|
|
56
|
+
sourceText,
|
|
57
|
+
ts.ScriptTarget.Latest,
|
|
58
|
+
true,
|
|
59
|
+
ts.ScriptKind.TSX,
|
|
60
|
+
)
|
|
61
|
+
const expectedNames = getExpectedNames(pageFile.pageName)
|
|
62
|
+
const exportedTypeAliases = getExportedTypeAliases(sourceFile)
|
|
63
|
+
const exportedVariables = getExportedVariables(sourceFile)
|
|
64
|
+
const errors: string[] = []
|
|
65
|
+
|
|
66
|
+
errors.push(...checkImports(sourceFile, pageFile))
|
|
67
|
+
errors.push(...checkRequiredTypeExport(exportedTypeAliases, pageFile, expectedNames.staticProps))
|
|
68
|
+
errors.push(...checkRequiredTypeExport(exportedTypeAliases, pageFile, expectedNames.dynamicProps))
|
|
69
|
+
errors.push(
|
|
70
|
+
...checkRequiredConstExport(exportedVariables, pageFile, expectedNames.dynamicPropKeys),
|
|
71
|
+
)
|
|
72
|
+
errors.push(...checkPageComponent(exportedVariables, pageFile, expectedNames))
|
|
73
|
+
errors.push(...checkDynamicProps(exportedTypeAliases, exportedVariables, pageFile, expectedNames))
|
|
74
|
+
|
|
75
|
+
return errors
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkImports(sourceFile: ts.SourceFile, pageFile: PageFile) {
|
|
79
|
+
const errors: string[] = []
|
|
80
|
+
|
|
81
|
+
for (const statement of sourceFile.statements) {
|
|
82
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const moduleName = statement.moduleSpecifier.text
|
|
87
|
+
|
|
88
|
+
if (moduleName === "@backbone/design-system" || moduleName === "../page") {
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
errors.push(
|
|
93
|
+
`Page file "${pageFile.relativePath}" must not import "${moduleName}"; page templates may only import @backbone/design-system and ../page.`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return errors
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function checkRequiredTypeExport(
|
|
101
|
+
exportedTypeAliases: Map<string, ts.TypeAliasDeclaration>,
|
|
102
|
+
pageFile: PageFile,
|
|
103
|
+
exportName: string,
|
|
104
|
+
) {
|
|
105
|
+
if (exportedTypeAliases.has(exportName)) {
|
|
106
|
+
return []
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return [`Page file "${pageFile.relativePath}" must export type "${exportName}".`]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function checkRequiredConstExport(
|
|
113
|
+
exportedVariables: Map<string, ts.VariableDeclaration>,
|
|
114
|
+
pageFile: PageFile,
|
|
115
|
+
exportName: string,
|
|
116
|
+
) {
|
|
117
|
+
if (exportedVariables.has(exportName)) {
|
|
118
|
+
return []
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [`Page file "${pageFile.relativePath}" must export const "${exportName}".`]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkPageComponent(
|
|
125
|
+
exportedVariables: Map<string, ts.VariableDeclaration>,
|
|
126
|
+
pageFile: PageFile,
|
|
127
|
+
expectedNames: PageNames,
|
|
128
|
+
) {
|
|
129
|
+
const pageComponent = exportedVariables.get(expectedNames.component)
|
|
130
|
+
|
|
131
|
+
if (pageComponent === undefined || !isExpectedPageType(pageComponent.type, expectedNames)) {
|
|
132
|
+
return [
|
|
133
|
+
`Page component "${expectedNames.component}" in "${pageFile.relativePath}" must be typed as "Page<${expectedNames.staticProps}, ${expectedNames.dynamicProps}>".`,
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return []
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function checkDynamicProps(
|
|
141
|
+
exportedTypeAliases: Map<string, ts.TypeAliasDeclaration>,
|
|
142
|
+
exportedVariables: Map<string, ts.VariableDeclaration>,
|
|
143
|
+
pageFile: PageFile,
|
|
144
|
+
expectedNames: PageNames,
|
|
145
|
+
) {
|
|
146
|
+
const dynamicProps = exportedTypeAliases.get(expectedNames.dynamicProps)
|
|
147
|
+
|
|
148
|
+
if (dynamicProps === undefined || !ts.isTypeLiteralNode(dynamicProps.type)) {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const dynamicPropNames: string[] = []
|
|
153
|
+
const errors: string[] = []
|
|
154
|
+
|
|
155
|
+
for (const member of dynamicProps.type.members) {
|
|
156
|
+
const propName = getPropertyName(member.name)
|
|
157
|
+
|
|
158
|
+
if (propName === undefined) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
dynamicPropNames.push(propName)
|
|
163
|
+
|
|
164
|
+
if (!isFunctionMember(member)) {
|
|
165
|
+
errors.push(
|
|
166
|
+
`Dynamic prop "${propName}" in "${pageFile.relativePath}" must be a function callback.`,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!/^on[A-Z]/.test(propName)) {
|
|
171
|
+
errors.push(
|
|
172
|
+
`Dynamic prop "${propName}" in "${pageFile.relativePath}" must start with "on" followed by an uppercase letter.`,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
errors.push(
|
|
178
|
+
...checkDynamicPropKeys(
|
|
179
|
+
exportedVariables.get(expectedNames.dynamicPropKeys),
|
|
180
|
+
dynamicPropNames,
|
|
181
|
+
pageFile,
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return errors
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function checkDynamicPropKeys(
|
|
189
|
+
dynamicPropKeysDeclaration: ts.VariableDeclaration | undefined,
|
|
190
|
+
dynamicPropNames: string[],
|
|
191
|
+
pageFile: PageFile,
|
|
192
|
+
) {
|
|
193
|
+
if (dynamicPropKeysDeclaration === undefined) {
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const dynamicPropKeys = getStringArrayInitializer(dynamicPropKeysDeclaration.initializer)
|
|
198
|
+
|
|
199
|
+
if (dynamicPropKeys === undefined) {
|
|
200
|
+
return []
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const expectedKeys = new Set(dynamicPropNames)
|
|
204
|
+
const actualKeys = new Set(dynamicPropKeys)
|
|
205
|
+
const errors: string[] = []
|
|
206
|
+
|
|
207
|
+
for (const expectedKey of expectedKeys) {
|
|
208
|
+
if (!actualKeys.has(expectedKey)) {
|
|
209
|
+
errors.push(`Dynamic prop keys in "${pageFile.relativePath}" must include "${expectedKey}".`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const actualKey of actualKeys) {
|
|
214
|
+
if (!expectedKeys.has(actualKey)) {
|
|
215
|
+
errors.push(
|
|
216
|
+
`Dynamic prop keys in "${pageFile.relativePath}" must not include unknown key "${actualKey}".`,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return errors
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getExportedTypeAliases(sourceFile: ts.SourceFile) {
|
|
225
|
+
const typeAliases = new Map<string, ts.TypeAliasDeclaration>()
|
|
226
|
+
|
|
227
|
+
for (const statement of sourceFile.statements) {
|
|
228
|
+
if (ts.isTypeAliasDeclaration(statement) && hasExportModifier(statement)) {
|
|
229
|
+
typeAliases.set(statement.name.text, statement)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return typeAliases
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getExportedVariables(sourceFile: ts.SourceFile) {
|
|
237
|
+
const variables = new Map<string, ts.VariableDeclaration>()
|
|
238
|
+
|
|
239
|
+
for (const statement of sourceFile.statements) {
|
|
240
|
+
if (!ts.isVariableStatement(statement) || !hasExportModifier(statement)) {
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
245
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
246
|
+
variables.set(declaration.name.text, declaration)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return variables
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isExpectedPageType(typeNode: ts.TypeNode | undefined, expectedNames: PageNames) {
|
|
255
|
+
if (typeNode === undefined || !ts.isTypeReferenceNode(typeNode)) {
|
|
256
|
+
return false
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
getTypeName(typeNode.typeName) === "Page" &&
|
|
261
|
+
typeNode.typeArguments?.length === 2 &&
|
|
262
|
+
typeNode.typeArguments[0]?.getText() === expectedNames.staticProps &&
|
|
263
|
+
typeNode.typeArguments[1]?.getText() === expectedNames.dynamicProps
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isFunctionMember(member: ts.TypeElement) {
|
|
268
|
+
return (
|
|
269
|
+
ts.isMethodSignature(member) ||
|
|
270
|
+
(ts.isPropertySignature(member) &&
|
|
271
|
+
member.type !== undefined &&
|
|
272
|
+
ts.isFunctionTypeNode(member.type))
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getPropertyName(name: ts.PropertyName | undefined) {
|
|
277
|
+
if (name === undefined) {
|
|
278
|
+
return undefined
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
|
|
282
|
+
return name.text
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return undefined
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getStringArrayInitializer(initializer: ts.Expression | undefined): string[] | undefined {
|
|
289
|
+
const unwrapped = unwrapExpression(initializer)
|
|
290
|
+
|
|
291
|
+
if (unwrapped === undefined || !ts.isArrayLiteralExpression(unwrapped)) {
|
|
292
|
+
return undefined
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const values: string[] = []
|
|
296
|
+
|
|
297
|
+
for (const element of unwrapped.elements) {
|
|
298
|
+
const unwrappedElement = unwrapExpression(element)
|
|
299
|
+
|
|
300
|
+
if (unwrappedElement === undefined || !ts.isStringLiteral(unwrappedElement)) {
|
|
301
|
+
return undefined
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
values.push(unwrappedElement.text)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return values
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function unwrapExpression(expression: ts.Expression | undefined): ts.Expression | undefined {
|
|
311
|
+
if (expression === undefined) {
|
|
312
|
+
return undefined
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) {
|
|
316
|
+
return unwrapExpression(expression.expression)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return expression
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getExpectedNames(pageName: string): PageNames {
|
|
323
|
+
const component = `${toPascalCase(pageName)}Page`
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
component,
|
|
327
|
+
dynamicPropKeys: `${toCamelCase(pageName)}PageDynamicPropKeys`,
|
|
328
|
+
dynamicProps: `${component}DynamicProps`,
|
|
329
|
+
staticProps: `${component}StaticProps`,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getTypeName(typeName: ts.EntityName) {
|
|
334
|
+
if (ts.isIdentifier(typeName)) {
|
|
335
|
+
return typeName.text
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return typeName.getText()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function hasExportModifier(statement: ts.Node) {
|
|
342
|
+
return (
|
|
343
|
+
ts.canHaveModifiers(statement) &&
|
|
344
|
+
ts
|
|
345
|
+
.getModifiers(statement)
|
|
346
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function findFiles(rootDir: string): string[] {
|
|
351
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true })
|
|
352
|
+
const files: string[] = []
|
|
353
|
+
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
const absolutePath = path.join(rootDir, entry.name)
|
|
356
|
+
|
|
357
|
+
if (entry.isDirectory()) {
|
|
358
|
+
files.push(...findFiles(absolutePath))
|
|
359
|
+
} else if (entry.isFile()) {
|
|
360
|
+
files.push(absolutePath)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return files
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function toPascalCase(value: string) {
|
|
368
|
+
return value
|
|
369
|
+
.split("-")
|
|
370
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
371
|
+
.join("")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function toCamelCase(value: string) {
|
|
375
|
+
const pascalCase = toPascalCase(value)
|
|
376
|
+
|
|
377
|
+
return pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function normalizePath(filePath: string) {
|
|
381
|
+
return filePath.split(path.sep).join("/")
|
|
382
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
type RuleContext = {
|
|
2
|
+
report(report: { message: string; node: unknown }): void
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
type ImportDeclarationNode = {
|
|
6
|
+
source?: {
|
|
7
|
+
value?: unknown
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type JsxNameNode = {
|
|
12
|
+
name?: string
|
|
13
|
+
type?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type JsxOpeningElementNode = {
|
|
17
|
+
name?: JsxNameNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type RuleModule<VisitorName extends string, Node> = {
|
|
21
|
+
meta: {
|
|
22
|
+
docs: {
|
|
23
|
+
description: string
|
|
24
|
+
}
|
|
25
|
+
type: "problem"
|
|
26
|
+
}
|
|
27
|
+
create(context: RuleContext): Record<VisitorName, (node: Node) => void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const forbiddenUiImports = {
|
|
31
|
+
exact: new Set([
|
|
32
|
+
"@emotion/react",
|
|
33
|
+
"@emotion/styled",
|
|
34
|
+
"antd",
|
|
35
|
+
"lucide-react",
|
|
36
|
+
"react-bootstrap",
|
|
37
|
+
"reactstrap",
|
|
38
|
+
"semantic-ui-react",
|
|
39
|
+
"styled-components",
|
|
40
|
+
]),
|
|
41
|
+
prefixes: ["@chakra-ui/", "@headlessui/", "@mantine/", "@mui/", "@radix-ui/", "@react-aria/"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const noExternalUiImports: RuleModule<"ImportDeclaration", ImportDeclarationNode> = {
|
|
45
|
+
meta: {
|
|
46
|
+
type: "problem",
|
|
47
|
+
docs: {
|
|
48
|
+
description: "Disallow direct imports from UI libraries outside the design system.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
create(context) {
|
|
52
|
+
return {
|
|
53
|
+
ImportDeclaration(node) {
|
|
54
|
+
const moduleName = node.source?.value
|
|
55
|
+
|
|
56
|
+
if (typeof moduleName !== "string" || !isForbiddenUiImport(moduleName)) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
context.report({
|
|
61
|
+
node: node.source,
|
|
62
|
+
message: `Import "${moduleName}" bypasses the design system boundary.`,
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const noRawDomJsx: RuleModule<"JSXOpeningElement", JsxOpeningElementNode> = {
|
|
70
|
+
meta: {
|
|
71
|
+
type: "problem",
|
|
72
|
+
docs: {
|
|
73
|
+
description: "Disallow raw DOM JSX elements in app code.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
create(context) {
|
|
77
|
+
return {
|
|
78
|
+
JSXOpeningElement(node) {
|
|
79
|
+
const tagName = getJsxElementName(node.name)
|
|
80
|
+
|
|
81
|
+
if (!isRawDomTag(tagName)) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
context.report({
|
|
86
|
+
node: node.name,
|
|
87
|
+
message: `Raw JSX element <${tagName}> is not allowed in app code; use @backbone/design-system components.`,
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isForbiddenUiImport(moduleName: string) {
|
|
95
|
+
return (
|
|
96
|
+
forbiddenUiImports.exact.has(moduleName) ||
|
|
97
|
+
forbiddenUiImports.prefixes.some((prefix) => moduleName.startsWith(prefix))
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getJsxElementName(nameNode: JsxNameNode | undefined) {
|
|
102
|
+
if (nameNode?.type === "JSXIdentifier") {
|
|
103
|
+
return nameNode.name
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isRawDomTag(tagName: string | undefined) {
|
|
110
|
+
return typeof tagName === "string" && /^[a-z]/.test(tagName)
|
|
111
|
+
}
|