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.
Files changed (182) hide show
  1. package/README.md +33 -0
  2. package/bin/create-backbone-template.js +5 -0
  3. package/package.json +30 -0
  4. package/src/create-backbone-template.js +204 -0
  5. package/template/.agents/skills/agent-browser/SKILL.md +55 -0
  6. package/template/.agents/skills/create-plan/SKILL.md +52 -0
  7. package/template/.agents/skills/create-plan/agents/openai.yaml +4 -0
  8. package/template/.agents/skills/create-pr-presentation/SKILL.md +86 -0
  9. package/template/.agents/skills/create-pr-presentation/agents/openai.yaml +4 -0
  10. package/template/.agents/skills/implement-plan/SKILL.md +26 -0
  11. package/template/.agents/skills/implement-plan/agents/openai.yaml +4 -0
  12. package/template/.agents/skills/review-plan/SKILL.md +38 -0
  13. package/template/.agents/skills/review-plan/agents/openai.yaml +4 -0
  14. package/template/.env.schema +30 -0
  15. package/template/.env.test +6 -0
  16. package/template/.oxlintrc.json +67 -0
  17. package/template/.vscode/extensions.json +3 -0
  18. package/template/.vscode/settings.json +23 -0
  19. package/template/AGENTS.md +55 -0
  20. package/template/Cargo.lock +2648 -0
  21. package/template/Cargo.toml +29 -0
  22. package/template/Justfile +140 -0
  23. package/template/README.md +72 -0
  24. package/template/TODO.md +1 -0
  25. package/template/_gitignore +12 -0
  26. package/template/buf.gen.yaml +7 -0
  27. package/template/buf.yaml +10 -0
  28. package/template/client/.oxfmtrc.json +8 -0
  29. package/template/client/.oxlintrc.json +57 -0
  30. package/template/client/README.md +19 -0
  31. package/template/client/_gitignore +5 -0
  32. package/template/client/index.html +12 -0
  33. package/template/client/package.json +47 -0
  34. package/template/client/packages/design-system/package.json +19 -0
  35. package/template/client/packages/design-system/src/index.ts +2 -0
  36. package/template/client/packages/design-system-basic/package.json +18 -0
  37. package/template/client/packages/design-system-basic/src/button.stories.tsx +50 -0
  38. package/template/client/packages/design-system-basic/src/button.tsx +26 -0
  39. package/template/client/packages/design-system-basic/src/empty-state.stories.tsx +18 -0
  40. package/template/client/packages/design-system-basic/src/empty-state.tsx +17 -0
  41. package/template/client/packages/design-system-basic/src/form-field.stories.tsx +15 -0
  42. package/template/client/packages/design-system-basic/src/form-field.tsx +10 -0
  43. package/template/client/packages/design-system-basic/src/form.stories.tsx +27 -0
  44. package/template/client/packages/design-system-basic/src/form.tsx +9 -0
  45. package/template/client/packages/design-system-basic/src/heading.stories.tsx +14 -0
  46. package/template/client/packages/design-system-basic/src/heading.tsx +25 -0
  47. package/template/client/packages/design-system-basic/src/index.tsx +15 -0
  48. package/template/client/packages/design-system-basic/src/inline.stories.tsx +13 -0
  49. package/template/client/packages/design-system-basic/src/inline.tsx +5 -0
  50. package/template/client/packages/design-system-basic/src/layout.stories.tsx +24 -0
  51. package/template/client/packages/design-system-basic/src/layout.tsx +14 -0
  52. package/template/client/packages/design-system-basic/src/loader.stories.tsx +8 -0
  53. package/template/client/packages/design-system-basic/src/loader.tsx +11 -0
  54. package/template/client/packages/design-system-basic/src/navigation.stories.tsx +16 -0
  55. package/template/client/packages/design-system-basic/src/navigation.tsx +18 -0
  56. package/template/client/packages/design-system-basic/src/notice.stories.tsx +13 -0
  57. package/template/client/packages/design-system-basic/src/notice.tsx +5 -0
  58. package/template/client/packages/design-system-basic/src/stack.stories.tsx +17 -0
  59. package/template/client/packages/design-system-basic/src/stack.tsx +5 -0
  60. package/template/client/packages/design-system-basic/src/styles.css +254 -0
  61. package/template/client/packages/design-system-basic/src/text-input.stories.tsx +13 -0
  62. package/template/client/packages/design-system-basic/src/text-input.tsx +5 -0
  63. package/template/client/packages/design-system-basic/src/text.stories.tsx +21 -0
  64. package/template/client/packages/design-system-basic/src/text.tsx +5 -0
  65. package/template/client/packages/design-system-contract/package.json +15 -0
  66. package/template/client/packages/design-system-contract/src/button.ts +10 -0
  67. package/template/client/packages/design-system-contract/src/empty-state.ts +9 -0
  68. package/template/client/packages/design-system-contract/src/form-field.ts +9 -0
  69. package/template/client/packages/design-system-contract/src/form.ts +9 -0
  70. package/template/client/packages/design-system-contract/src/heading.ts +9 -0
  71. package/template/client/packages/design-system-contract/src/index.ts +13 -0
  72. package/template/client/packages/design-system-contract/src/inline.ts +7 -0
  73. package/template/client/packages/design-system-contract/src/layout.ts +8 -0
  74. package/template/client/packages/design-system-contract/src/loader.ts +7 -0
  75. package/template/client/packages/design-system-contract/src/navigation.ts +13 -0
  76. package/template/client/packages/design-system-contract/src/notice.ts +8 -0
  77. package/template/client/packages/design-system-contract/src/stack.ts +8 -0
  78. package/template/client/packages/design-system-contract/src/text-input.ts +5 -0
  79. package/template/client/packages/design-system-contract/src/text.ts +9 -0
  80. package/template/client/packages/design-system-lint/fixtures/invalid/external-ui-import.tsx +5 -0
  81. package/template/client/packages/design-system-lint/fixtures/invalid/raw-dom-jsx.tsx +3 -0
  82. package/template/client/packages/design-system-lint/fixtures/invalid/two-violations.tsx +7 -0
  83. package/template/client/packages/design-system-lint/fixtures/valid/design-system-only.tsx +13 -0
  84. package/template/client/packages/design-system-lint/package.json +23 -0
  85. package/template/client/packages/design-system-lint/src/check-design-system-architecture.ts +22 -0
  86. package/template/client/packages/design-system-lint/src/design-system-architecture.ts +286 -0
  87. package/template/client/packages/design-system-lint/src/oxlint-plugin.ts +11 -0
  88. package/template/client/packages/design-system-lint/src/page-architecture.ts +382 -0
  89. package/template/client/packages/design-system-lint/src/rules.ts +111 -0
  90. package/template/client/packages/design-system-lint/test/design-system-architecture.test.ts +243 -0
  91. package/template/client/packages/design-system-lint/test/oxlint-fixtures.test.ts +159 -0
  92. package/template/client/packages/design-system-lint/test/page-architecture.test.ts +175 -0
  93. package/template/client/packages/design-system-lint/test/rules.test.ts +65 -0
  94. package/template/client/packages/design-system-lint/tsconfig.json +29 -0
  95. package/template/client/src/App.tsx +77 -0
  96. package/template/client/src/design-system-components.test.tsx +75 -0
  97. package/template/client/src/gen/helloworld/v1/helloworld_pb.ts +63 -0
  98. package/template/client/src/main.tsx +18 -0
  99. package/template/client/src/pages/hello/hello-page.stories.tsx +20 -0
  100. package/template/client/src/pages/hello/hello-page.test.tsx +90 -0
  101. package/template/client/src/pages/hello/hello-page.tsx +126 -0
  102. package/template/client/src/pages/page.ts +20 -0
  103. package/template/client/src/testing/create-preview-events.test.ts +36 -0
  104. package/template/client/src/testing/create-preview-events.ts +30 -0
  105. package/template/client/src/vite-env.d.ts +1 -0
  106. package/template/client/tsconfig.json +32 -0
  107. package/template/client/vite.config.ts +21 -0
  108. package/template/client/vite.ladle.config.ts +5 -0
  109. package/template/e2e/.gherkin-lintrc +20 -0
  110. package/template/e2e/.oxfmtrc.json +15 -0
  111. package/template/e2e/.oxlintrc.json +37 -0
  112. package/template/e2e/_gitignore +4 -0
  113. package/template/e2e/features/helloworld.feature +10 -0
  114. package/template/e2e/package.json +42 -0
  115. package/template/e2e/playwright.config.ts +16 -0
  116. package/template/e2e/support/app-gherkin.ts +4 -0
  117. package/template/e2e/support/fixtures.ts +236 -0
  118. package/template/e2e/support/gherkin-fixtures/duplicate-id.feature +9 -0
  119. package/template/e2e/support/gherkin-fixtures/duplicate-id.spec.ts +7 -0
  120. package/template/e2e/support/gherkin-fixtures/extra-implementation.spec.ts +7 -0
  121. package/template/e2e/support/gherkin-fixtures/extra-step.spec.ts +10 -0
  122. package/template/e2e/support/gherkin-fixtures/happy-path.spec.ts +4 -0
  123. package/template/e2e/support/gherkin-fixtures/missing-id.feature +4 -0
  124. package/template/e2e/support/gherkin-fixtures/missing-id.spec.ts +7 -0
  125. package/template/e2e/support/gherkin-fixtures/missing-implementation.spec.ts +7 -0
  126. package/template/e2e/support/gherkin-fixtures/missing-step.spec.ts +7 -0
  127. package/template/e2e/support/gherkin-fixtures/playwright.config.ts +7 -0
  128. package/template/e2e/support/gherkin-fixtures/scenario-outline.feature +9 -0
  129. package/template/e2e/support/gherkin-fixtures/scenario-outline.spec.ts +7 -0
  130. package/template/e2e/support/gherkin-fixtures/step-mismatch.spec.ts +9 -0
  131. package/template/e2e/support/gherkin-fixtures/valid-implementations.ts +23 -0
  132. package/template/e2e/support/gherkin-fixtures/valid-scenarios.feature +26 -0
  133. package/template/e2e/support/gherkin.test.ts +184 -0
  134. package/template/e2e/support/gherkin.ts +321 -0
  135. package/template/e2e/support/oxlint-plugin.test.ts +328 -0
  136. package/template/e2e/support/oxlint-plugin.ts +485 -0
  137. package/template/e2e/tests/helloworld.spec.ts +39 -0
  138. package/template/e2e/tsconfig.json +26 -0
  139. package/template/e2e/tsconfig.oxlint-plugin.json +12 -0
  140. package/template/package.json +9 -0
  141. package/template/pnpm-lock.yaml +10723 -0
  142. package/template/pnpm-workspace.yaml +8 -0
  143. package/template/pr-slide/README.md +95 -0
  144. package/template/pr-slide/package.json +23 -0
  145. package/template/pr-slide/src/cli.js +262 -0
  146. package/template/pr-slide/src/generate-pr-deck.js +833 -0
  147. package/template/pr-slide/src/git-context.js +91 -0
  148. package/template/pr-slide/src/presentation-paths.js +9 -0
  149. package/template/pr-slide/src/presentations.js +53 -0
  150. package/template/pr-slide/test/generate-pr-deck.test.js +118 -0
  151. package/template/pr-slide/test/presentation-paths.test.js +14 -0
  152. package/template/pr-slide/test/presentations.test.js +50 -0
  153. package/template/proto/helloworld/v1/helloworld.proto +15 -0
  154. package/template/scripts/run-e2e.sh +10 -0
  155. package/template/server/Cargo.toml +26 -0
  156. package/template/server/build.rs +9 -0
  157. package/template/server/dylint/backbone_server_lints/.cargo/config.toml +6 -0
  158. package/template/server/dylint/backbone_server_lints/Cargo.lock +1581 -0
  159. package/template/server/dylint/backbone_server_lints/Cargo.toml +21 -0
  160. package/template/server/dylint/backbone_server_lints/README.md +5 -0
  161. package/template/server/dylint/backbone_server_lints/_gitignore +1 -0
  162. package/template/server/dylint/backbone_server_lints/rust-toolchain +3 -0
  163. package/template/server/dylint/backbone_server_lints/src/lib.rs +612 -0
  164. package/template/server/dylint/backbone_server_lints/ui/lib.rs +4 -0
  165. package/template/server/dylint/backbone_server_lints/ui/lib.stderr +10 -0
  166. package/template/server/dylint/backbone_server_lints/ui/long_file.rs +303 -0
  167. package/template/server/dylint/backbone_server_lints/ui/long_file.stderr +6 -0
  168. package/template/server/dylint/backbone_server_lints/ui/main.rs +59 -0
  169. package/template/server/dylint/backbone_server_lints/ui/main.stderr +85 -0
  170. package/template/server/migrations/20260520120000_create_projects.sql +12 -0
  171. package/template/server/migrations/20260524160000_create_hello_world_inputs.sql +12 -0
  172. package/template/server/src/config.rs +27 -0
  173. package/template/server/src/db/hello_world.rs +34 -0
  174. package/template/server/src/db/hello_world_tests.rs +11 -0
  175. package/template/server/src/db/mod.rs +39 -0
  176. package/template/server/src/lib.rs +10 -0
  177. package/template/server/src/main.rs +43 -0
  178. package/template/server/src/rpc/greeter/mod.rs +31 -0
  179. package/template/server/src/rpc/greeter/say_hello.rs +27 -0
  180. package/template/server/src/rpc/mod.rs +8 -0
  181. package/template/server/src/state.rs +13 -0
  182. 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
+ }