@facilis.dev/analyze 0.0.0-stage → 0.1.1

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 (204) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/dist/analyze.d.ts +17 -0
  4. package/dist/analyze.js +99 -0
  5. package/dist/ast/node.d.ts +29 -0
  6. package/dist/ast/node.js +85 -0
  7. package/dist/ast/walk.d.ts +8 -0
  8. package/dist/ast/walk.js +40 -0
  9. package/dist/diagnostics.d.ts +5 -0
  10. package/dist/diagnostics.js +12 -0
  11. package/dist/fs.d.ts +13 -0
  12. package/dist/fs.js +51 -0
  13. package/dist/graph-edges.d.ts +3 -0
  14. package/dist/graph-edges.js +148 -0
  15. package/dist/graph-ids.d.ts +6 -0
  16. package/dist/graph-ids.js +22 -0
  17. package/dist/graph.d.ts +15 -0
  18. package/dist/graph.js +162 -0
  19. package/dist/index.d.ts +24 -0
  20. package/dist/index.js +17 -0
  21. package/dist/json.d.ts +8 -0
  22. package/dist/json.js +31 -0
  23. package/dist/module-graph.d.ts +33 -0
  24. package/dist/module-graph.js +86 -0
  25. package/dist/module-info.d.ts +27 -0
  26. package/dist/module-info.js +143 -0
  27. package/dist/node.d.ts +16 -0
  28. package/dist/node.js +41 -0
  29. package/dist/parser.d.ts +16 -0
  30. package/dist/parser.js +1 -0
  31. package/dist/passes/clients.d.ts +22 -0
  32. package/dist/passes/clients.js +167 -0
  33. package/dist/passes/config.d.ts +16 -0
  34. package/dist/passes/config.js +229 -0
  35. package/dist/passes/file-facts.d.ts +13 -0
  36. package/dist/passes/file-facts.js +130 -0
  37. package/dist/passes/i18n.d.ts +14 -0
  38. package/dist/passes/i18n.js +55 -0
  39. package/dist/passes/job-relations-diagnostics.d.ts +9 -0
  40. package/dist/passes/job-relations-diagnostics.js +38 -0
  41. package/dist/passes/job-relations.d.ts +18 -0
  42. package/dist/passes/job-relations.js +52 -0
  43. package/dist/passes/jobs.d.ts +20 -0
  44. package/dist/passes/jobs.js +133 -0
  45. package/dist/passes/jsx-text.d.ts +28 -0
  46. package/dist/passes/jsx-text.js +121 -0
  47. package/dist/passes/jsx.d.ts +24 -0
  48. package/dist/passes/jsx.js +135 -0
  49. package/dist/passes/path-expression.d.ts +26 -0
  50. package/dist/passes/path-expression.js +265 -0
  51. package/dist/passes/route-config.d.ts +24 -0
  52. package/dist/passes/route-config.js +152 -0
  53. package/dist/passes/route-description.d.ts +13 -0
  54. package/dist/passes/route-description.js +23 -0
  55. package/dist/passes/route-elevation.d.ts +18 -0
  56. package/dist/passes/route-elevation.js +136 -0
  57. package/dist/passes/route-handler.d.ts +27 -0
  58. package/dist/passes/route-handler.js +87 -0
  59. package/dist/passes/route-location.d.ts +16 -0
  60. package/dist/passes/route-location.js +58 -0
  61. package/dist/passes/route-relation-resolve.d.ts +51 -0
  62. package/dist/passes/route-relation-resolve.js +172 -0
  63. package/dist/passes/route-relations-diagnostics.d.ts +15 -0
  64. package/dist/passes/route-relations-diagnostics.js +79 -0
  65. package/dist/passes/route-relations-follow.d.ts +16 -0
  66. package/dist/passes/route-relations-follow.js +116 -0
  67. package/dist/passes/route-relations-usage.d.ts +50 -0
  68. package/dist/passes/route-relations-usage.js +44 -0
  69. package/dist/passes/route-relations-walk.d.ts +25 -0
  70. package/dist/passes/route-relations-walk.js +199 -0
  71. package/dist/passes/route-relations.d.ts +17 -0
  72. package/dist/passes/route-relations.js +43 -0
  73. package/dist/passes/route-table.d.ts +21 -0
  74. package/dist/passes/route-table.js +104 -0
  75. package/dist/passes/route-tables.d.ts +10 -0
  76. package/dist/passes/route-tables.js +42 -0
  77. package/dist/passes/routes.d.ts +18 -0
  78. package/dist/passes/routes.js +152 -0
  79. package/dist/passes/schema-column-types.d.ts +9 -0
  80. package/dist/passes/schema-column-types.js +55 -0
  81. package/dist/passes/schema-columns.d.ts +35 -0
  82. package/dist/passes/schema-columns.js +197 -0
  83. package/dist/passes/schema-dsl.d.ts +24 -0
  84. package/dist/passes/schema-dsl.js +117 -0
  85. package/dist/passes/schema-file-links-ddl.d.ts +18 -0
  86. package/dist/passes/schema-file-links-ddl.js +106 -0
  87. package/dist/passes/schema-file-links.d.ts +19 -0
  88. package/dist/passes/schema-file-links.js +144 -0
  89. package/dist/passes/schema-foreign-keys.d.ts +21 -0
  90. package/dist/passes/schema-foreign-keys.js +80 -0
  91. package/dist/passes/schema-policies.d.ts +15 -0
  92. package/dist/passes/schema-policies.js +201 -0
  93. package/dist/passes/schema-re-exports.d.ts +5 -0
  94. package/dist/passes/schema-re-exports.js +77 -0
  95. package/dist/passes/schema-relations.d.ts +8 -0
  96. package/dist/passes/schema-relations.js +182 -0
  97. package/dist/passes/schema-scope.d.ts +7 -0
  98. package/dist/passes/schema-scope.js +20 -0
  99. package/dist/passes/schema-tables.d.ts +13 -0
  100. package/dist/passes/schema-tables.js +149 -0
  101. package/dist/passes/schema.d.ts +9 -0
  102. package/dist/passes/schema.js +55 -0
  103. package/dist/passes/screen-components.d.ts +22 -0
  104. package/dist/passes/screen-components.js +143 -0
  105. package/dist/passes/screen-intrinsics.d.ts +18 -0
  106. package/dist/passes/screen-intrinsics.js +21 -0
  107. package/dist/passes/screen-preview.d.ts +29 -0
  108. package/dist/passes/screen-preview.js +174 -0
  109. package/dist/passes/screens.d.ts +12 -0
  110. package/dist/passes/screens.js +278 -0
  111. package/dist/passes/static-read.d.ts +47 -0
  112. package/dist/passes/static-read.js +151 -0
  113. package/dist/passes/views.d.ts +20 -0
  114. package/dist/passes/views.js +101 -0
  115. package/dist/passes/workflow-tables.d.ts +18 -0
  116. package/dist/passes/workflow-tables.js +71 -0
  117. package/dist/passes/workflows.d.ts +8 -0
  118. package/dist/passes/workflows.js +160 -0
  119. package/dist/path-resolver.d.ts +20 -0
  120. package/dist/path-resolver.js +134 -0
  121. package/dist/revision.d.ts +2 -0
  122. package/dist/revision.js +23 -0
  123. package/dist/source.d.ts +4 -0
  124. package/dist/source.js +13 -0
  125. package/dist/static-eval.d.ts +36 -0
  126. package/dist/static-eval.js +215 -0
  127. package/dist/types-graph.d.ts +144 -0
  128. package/dist/types-graph.js +4 -0
  129. package/dist/types-jobs.d.ts +28 -0
  130. package/dist/types-jobs.js +14 -0
  131. package/dist/types-relations.d.ts +49 -0
  132. package/dist/types-relations.js +11 -0
  133. package/dist/types-workflows.d.ts +36 -0
  134. package/dist/types-workflows.js +3 -0
  135. package/dist/types.d.ts +226 -0
  136. package/dist/types.js +1 -0
  137. package/package.json +49 -4
  138. package/src/analyze.ts +119 -0
  139. package/src/ast/node.ts +102 -0
  140. package/src/ast/walk.ts +37 -0
  141. package/src/diagnostics.ts +22 -0
  142. package/src/fs.ts +59 -0
  143. package/src/graph-edges.ts +167 -0
  144. package/src/graph-ids.ts +31 -0
  145. package/src/graph.ts +194 -0
  146. package/src/index.ts +26 -0
  147. package/src/json.ts +34 -0
  148. package/src/module-graph.ts +117 -0
  149. package/src/module-info.ts +162 -0
  150. package/src/node.ts +54 -0
  151. package/src/parser.ts +19 -0
  152. package/src/passes/clients.ts +233 -0
  153. package/src/passes/config.ts +278 -0
  154. package/src/passes/file-facts.ts +142 -0
  155. package/src/passes/i18n.ts +66 -0
  156. package/src/passes/job-relations-diagnostics.ts +60 -0
  157. package/src/passes/job-relations.ts +76 -0
  158. package/src/passes/jobs.ts +181 -0
  159. package/src/passes/jsx-text.ts +126 -0
  160. package/src/passes/jsx.ts +142 -0
  161. package/src/passes/path-expression.ts +297 -0
  162. package/src/passes/route-config.ts +188 -0
  163. package/src/passes/route-description.ts +33 -0
  164. package/src/passes/route-elevation.ts +161 -0
  165. package/src/passes/route-handler.ts +105 -0
  166. package/src/passes/route-location.ts +62 -0
  167. package/src/passes/route-relation-resolve.ts +228 -0
  168. package/src/passes/route-relations-diagnostics.ts +115 -0
  169. package/src/passes/route-relations-follow.ts +146 -0
  170. package/src/passes/route-relations-usage.ts +98 -0
  171. package/src/passes/route-relations-walk.ts +255 -0
  172. package/src/passes/route-relations.ts +62 -0
  173. package/src/passes/route-table.ts +123 -0
  174. package/src/passes/route-tables.ts +50 -0
  175. package/src/passes/routes.ts +188 -0
  176. package/src/passes/schema-column-types.ts +62 -0
  177. package/src/passes/schema-columns.ts +287 -0
  178. package/src/passes/schema-dsl.ts +136 -0
  179. package/src/passes/schema-file-links-ddl.ts +125 -0
  180. package/src/passes/schema-file-links.ts +213 -0
  181. package/src/passes/schema-foreign-keys.ts +121 -0
  182. package/src/passes/schema-policies.ts +270 -0
  183. package/src/passes/schema-re-exports.ts +95 -0
  184. package/src/passes/schema-relations.ts +239 -0
  185. package/src/passes/schema-scope.ts +29 -0
  186. package/src/passes/schema-tables.ts +216 -0
  187. package/src/passes/schema.ts +67 -0
  188. package/src/passes/screen-components.ts +162 -0
  189. package/src/passes/screen-intrinsics.ts +29 -0
  190. package/src/passes/screen-preview.ts +295 -0
  191. package/src/passes/screens.ts +291 -0
  192. package/src/passes/static-read.ts +196 -0
  193. package/src/passes/views.ts +125 -0
  194. package/src/passes/workflow-tables.ts +87 -0
  195. package/src/passes/workflows.ts +213 -0
  196. package/src/path-resolver.ts +146 -0
  197. package/src/revision.ts +25 -0
  198. package/src/source.ts +15 -0
  199. package/src/static-eval.ts +242 -0
  200. package/src/types-graph.ts +176 -0
  201. package/src/types-jobs.ts +40 -0
  202. package/src/types-relations.ts +64 -0
  203. package/src/types-workflows.ts +42 -0
  204. package/src/types.ts +299 -0
@@ -0,0 +1,216 @@
1
+ import {
2
+ callArguments,
3
+ identifierName,
4
+ memberName,
5
+ objectEntries,
6
+ propertyName,
7
+ propertyValue,
8
+ unwrapExpression,
9
+ type AstNode,
10
+ } from "../ast/node.ts";
11
+ import { diagnostic } from "../diagnostics.ts";
12
+ import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
13
+ import { staticString } from "../static-eval.ts";
14
+ import type { ConfigFacts, Diagnostic, TableFact, UnknownFact } from "../types.ts";
15
+ import { buildColumnFact, type ColumnReference } from "./schema-columns.ts";
16
+ import { scopeOf, type ScopedColumn } from "./schema-scope.ts";
17
+ import {
18
+ createDslResolver,
19
+ nodeRef,
20
+ schemaDiagnostics,
21
+ schemaUnknown,
22
+ topLevelDeclarators,
23
+ type DslResolver,
24
+ } from "./schema-dsl.ts";
25
+ import { buildPolicyFacts } from "./schema-policies.ts";
26
+
27
+ export interface TableDraft {
28
+ readonly fact: TableFact;
29
+ readonly binding: string | null;
30
+ readonly references: readonly ColumnReference[];
31
+ }
32
+
33
+ export interface TableScan {
34
+ readonly drafts: readonly TableDraft[];
35
+ readonly diagnostics: readonly Diagnostic[];
36
+ }
37
+
38
+ interface TableCall {
39
+ readonly call: AstNode;
40
+ readonly methods: readonly string[];
41
+ }
42
+
43
+ /** Local `pgEnum("name", values)` consts; a column built from one is a known enum builder. */
44
+ function collectEnumBindings(
45
+ file: ProjectFile,
46
+ resolver: DslResolver,
47
+ graph: ModuleGraph,
48
+ ): Map<string, string> {
49
+ const enums = new Map<string, string>();
50
+ for (const declarator of topLevelDeclarators(file.program)) {
51
+ const binding = identifierName(declarator.id);
52
+ const init = unwrapExpression(declarator.init);
53
+ if (binding === undefined || init?.type !== "CallExpression") continue;
54
+ const symbol = resolver(init.callee);
55
+ if (symbol?.pkg !== "pg-core" || symbol.name !== "pgEnum") continue;
56
+ const name = staticString(callArguments(init)[0], { graph, file: file.path });
57
+ enums.set(binding, name.ok ? name.value : binding);
58
+ }
59
+ return enums;
60
+ }
61
+
62
+ /** The `pgTable` call inside `pgTable(...).enableRLS()`, plus the chain methods after it. */
63
+ function findTableCall(init: unknown, resolver: DslResolver): TableCall | null {
64
+ let current = unwrapExpression(init);
65
+ const methods: string[] = [];
66
+ let guard = 0;
67
+ while (current?.type === "CallExpression" && guard < 32) {
68
+ guard += 1;
69
+ const callee = unwrapExpression(current.callee);
70
+ if (callee === undefined) return null;
71
+ if (callee.type === "MemberExpression") {
72
+ const name = memberName(callee);
73
+ if (name === undefined) return null;
74
+ methods.unshift(name);
75
+ current = unwrapExpression(callee.object);
76
+ continue;
77
+ }
78
+ const symbol = resolver(callee);
79
+ if (symbol?.pkg !== "pg-core" || symbol.name !== "pgTable") return null;
80
+ return { call: current, methods };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function buildTableDraft(input: {
86
+ readonly graph: ModuleGraph;
87
+ readonly file: ProjectFile;
88
+ readonly resolver: DslResolver;
89
+ readonly enums: ReadonlyMap<string, string>;
90
+ readonly config: ConfigFacts;
91
+ readonly binding: string | null;
92
+ readonly tableCall: TableCall;
93
+ }): { draft: TableDraft; diagnostics: readonly Diagnostic[] } {
94
+ const { graph, file, resolver, enums, config, binding, tableCall } = input;
95
+ const source = nodeRef(file, tableCall.call);
96
+ const args = callArguments(tableCall.call);
97
+ const unknowns: UnknownFact[] = [];
98
+ const diagnostics: Diagnostic[] = [];
99
+ const nameResult = staticString(args[0], { graph, file: file.path });
100
+ let name: string;
101
+ if (nameResult.ok && nameResult.value.length > 0) {
102
+ name = nameResult.value;
103
+ } else {
104
+ name = binding ?? "unknown";
105
+ unknowns.push(schemaUnknown('table name of "' + name + '" is not a static string', source));
106
+ }
107
+
108
+ const columns: ScopedColumn[] = [];
109
+ const references: ColumnReference[] = [];
110
+ const columnsArgument = unwrapExpression(args[1]);
111
+ if (columnsArgument?.type !== "ObjectExpression") {
112
+ unknowns.push(
113
+ schemaUnknown('columns of table "' + name + '" are not an object literal', source),
114
+ );
115
+ } else {
116
+ for (const entry of objectEntries(columnsArgument)) {
117
+ if (entry.type === "SpreadElement") {
118
+ unknowns.push(
119
+ schemaUnknown(
120
+ 'spread columns in table "' + name + '" are outside the DSL',
121
+ nodeRef(file, entry),
122
+ ),
123
+ );
124
+ continue;
125
+ }
126
+ const property = propertyName(entry);
127
+ if (property === undefined) {
128
+ unknowns.push(
129
+ schemaUnknown(
130
+ 'computed column key in table "' + name + '" is outside the DSL',
131
+ nodeRef(file, entry),
132
+ ),
133
+ );
134
+ continue;
135
+ }
136
+ const result = buildColumnFact({
137
+ property,
138
+ expression: propertyValue(entry),
139
+ propertyNode: entry,
140
+ file,
141
+ graph,
142
+ resolver,
143
+ enums,
144
+ });
145
+ diagnostics.push(...result.diagnostics);
146
+ if (result.reference !== null) {
147
+ references.push({ column: property, target: result.reference });
148
+ }
149
+ columns.push({ column: result.column, tenant: result.tenant });
150
+ }
151
+ }
152
+
153
+ const policyScan = buildPolicyFacts({
154
+ extras: args[2],
155
+ file,
156
+ graph,
157
+ resolver,
158
+ });
159
+ diagnostics.push(...policyScan.diagnostics);
160
+ const rlsEnabled = tableCall.methods.includes("enableRLS");
161
+ const scope = scopeOf(columns, config);
162
+ const fact: TableFact = {
163
+ name,
164
+ binding,
165
+ columns: columns.map((entry) => entry.column),
166
+ primaryKey: columns
167
+ .filter((entry) => entry.column.primaryKey)
168
+ .map((entry) => entry.column.property),
169
+ foreignKeys: [],
170
+ policies: policyScan.policies,
171
+ rlsEnabled,
172
+ scope,
173
+ source,
174
+ unknown: unknowns,
175
+ };
176
+ diagnostics.push(...schemaDiagnostics(unknowns));
177
+ if (scope !== null && (!rlsEnabled || policyScan.policies.length === 0)) {
178
+ diagnostics.push(
179
+ diagnostic(
180
+ "rls-on-scoped-table",
181
+ "error",
182
+ 'scoped table "' + name + '" must call .enableRLS() and declare a policy',
183
+ source,
184
+ ),
185
+ );
186
+ }
187
+ return { draft: { fact, binding, references }, diagnostics };
188
+ }
189
+
190
+ export function collectTableDrafts(
191
+ graph: ModuleGraph,
192
+ file: ProjectFile,
193
+ config: ConfigFacts,
194
+ ): TableScan {
195
+ const resolver = createDslResolver(graph, file);
196
+ const enums = collectEnumBindings(file, resolver, graph);
197
+ const drafts: TableDraft[] = [];
198
+ const diagnostics: Diagnostic[] = [];
199
+ for (const declarator of topLevelDeclarators(file.program)) {
200
+ const binding = identifierName(declarator.id) ?? null;
201
+ const tableCall = findTableCall(declarator.init, resolver);
202
+ if (tableCall === null) continue;
203
+ const result = buildTableDraft({
204
+ graph,
205
+ file,
206
+ resolver,
207
+ enums,
208
+ config,
209
+ binding,
210
+ tableCall,
211
+ });
212
+ drafts.push(result.draft);
213
+ diagnostics.push(...result.diagnostics);
214
+ }
215
+ return { drafts, diagnostics };
216
+ }
@@ -0,0 +1,67 @@
1
+ import type { ModuleGraph, ProjectFile } from "../module-graph.ts";
2
+ import type { ConfigFacts, Diagnostic, RelationFact, TableFact } from "../types.ts";
3
+ import { isSchemaFile } from "./schema-dsl.ts";
4
+ import { collectFileLinkDrafts } from "./schema-file-links.ts";
5
+ import { buildTableLookup, finalizeTables, withFrameworkTables } from "./schema-foreign-keys.ts";
6
+ import { collectTableDrafts, type TableDraft } from "./schema-tables.ts";
7
+ import { collectReExportedDrafts } from "./schema-re-exports.ts";
8
+ import { buildRelationFacts } from "./schema-relations.ts";
9
+
10
+ export interface SchemaResult {
11
+ readonly tables: readonly TableFact[];
12
+ readonly relations: readonly RelationFact[];
13
+ readonly diagnostics: readonly Diagnostic[];
14
+ }
15
+
16
+ function schemaFiles(graph: ModuleGraph): ProjectFile[] {
17
+ return [...graph.files.values()]
18
+ .filter((file) => isSchemaFile(graph, file.path))
19
+ .toSorted((left, right) => left.path.localeCompare(right.path));
20
+ }
21
+
22
+ /** Tables declared twice (invalid TS, but analyzable) keep the first declaration. */
23
+ function dedupeDrafts(drafts: readonly TableDraft[]): TableDraft[] {
24
+ const seen = new Set<string>();
25
+ const out: TableDraft[] = [];
26
+ for (const draft of drafts) {
27
+ if (seen.has(draft.fact.name)) continue;
28
+ seen.add(draft.fact.name);
29
+ out.push(draft);
30
+ }
31
+ return out;
32
+ }
33
+
34
+ /** Interpret the Drizzle DSL under `db/**`, including framework tables reached by re-export. */
35
+ export function buildSchemaFacts(graph: ModuleGraph, config: ConfigFacts): SchemaResult {
36
+ const files = schemaFiles(graph);
37
+ const collected: TableDraft[] = [];
38
+ const externalTables: TableDraft[] = [];
39
+ const diagnostics: Diagnostic[] = [];
40
+ for (const file of files) {
41
+ const scan = collectTableDrafts(graph, file, config);
42
+ collected.push(...scan.drafts);
43
+ diagnostics.push(...scan.diagnostics);
44
+ // Generated link tables follow the file's own declarations, which is their source order:
45
+ // `const todoFiles = fileLinks(todos, ...)` comes after the `todos` it references.
46
+ const links = collectFileLinkDrafts(graph, file, scan.drafts);
47
+ collected.push(...links.drafts);
48
+ externalTables.push(...links.externalTables);
49
+ diagnostics.push(...links.diagnostics);
50
+ }
51
+
52
+ const seenNames = new Set(collected.map((draft) => draft.fact.name));
53
+ const visitedFiles = new Set(files.map((file) => file.path));
54
+ for (const file of files) {
55
+ const scan = collectReExportedDrafts(graph, file, config, seenNames, visitedFiles);
56
+ collected.push(...scan.drafts);
57
+ diagnostics.push(...scan.diagnostics);
58
+ }
59
+
60
+ const drafts = dedupeDrafts(collected);
61
+ const lookup = withFrameworkTables(buildTableLookup(drafts), externalTables);
62
+ const finalized = finalizeTables(graph, drafts, lookup);
63
+ diagnostics.push(...finalized.diagnostics);
64
+ const relations = buildRelationFacts(graph, files, lookup);
65
+ diagnostics.push(...relations.diagnostics);
66
+ return { tables: finalized.tables, relations: relations.relations, diagnostics };
67
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * JSX component resolution for the static page preview: find a component's returned
3
+ * expression and resolve a JSX name to a project file through the module graph.
4
+ *
5
+ * The resolver is deliberately one level deep per call; the preview builder owns the depth,
6
+ * cycle and node budgets.
7
+ */
8
+
9
+ import { asNode, nodeArray, type AstNode } from "../ast/node.ts";
10
+ import type { ModuleGraph } from "../module-graph.ts";
11
+ import { defaultExportDeclaration } from "../module-info.ts";
12
+ import { sourceRef } from "../source.ts";
13
+
14
+ export interface ComponentRef {
15
+ readonly file: string;
16
+ readonly name: string;
17
+ }
18
+
19
+ export function lineRef(
20
+ graph: ModuleGraph,
21
+ file: string,
22
+ node: AstNode,
23
+ ): { file: string; line: number } {
24
+ const source = graph.file(file)?.source;
25
+ return source === undefined
26
+ ? { file, line: 1 }
27
+ : sourceRef(file, source, typeof node.start === "number" ? node.start : 0);
28
+ }
29
+
30
+ function componentDeclaration(program: AstNode, name: string): AstNode | undefined {
31
+ const wanted = name === "default" ? null : name;
32
+ for (const statement of nodeArray(program, "body")) {
33
+ const declaration =
34
+ statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration"
35
+ ? asNode(statement.declaration)
36
+ : statement;
37
+ if (declaration === undefined) continue;
38
+ if (declaration.type === "FunctionDeclaration") {
39
+ const id = asNode(declaration.id);
40
+ if (
41
+ id !== undefined &&
42
+ typeof id.name === "string" &&
43
+ (wanted === null || id.name === wanted)
44
+ ) {
45
+ return declaration;
46
+ }
47
+ }
48
+ if (declaration.type !== "VariableDeclaration") continue;
49
+ for (const item of nodeArray(declaration, "declarations")) {
50
+ const id = asNode(item.id);
51
+ const init = asNode(item.init);
52
+ if (id === undefined || init === undefined) continue;
53
+ if (typeof id.name !== "string" || (wanted !== null && id.name !== wanted)) continue;
54
+ if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
55
+ return init;
56
+ }
57
+ }
58
+ if (name === "default") return defaultExportDeclaration(program);
59
+ return undefined;
60
+ }
61
+
62
+ export function returnedExpression(body: AstNode): AstNode | undefined {
63
+ if (body.type === "BlockStatement") {
64
+ for (const statement of nodeArray(body, "body")) {
65
+ const found = returnedExpression(statement);
66
+ if (found !== undefined) return found;
67
+ }
68
+ return undefined;
69
+ }
70
+ if (body.type === "ReturnStatement") return asNode(body.argument);
71
+ if (body.type === "IfStatement") {
72
+ const consequent = asNode(body.consequent);
73
+ const alternate = asNode(body.alternate);
74
+ return (
75
+ (consequent === undefined ? undefined : returnedExpression(consequent)) ??
76
+ (alternate === undefined ? undefined : returnedExpression(alternate))
77
+ );
78
+ }
79
+ if (body.type === "TryStatement") {
80
+ const block = asNode(body.block);
81
+ const handler = asNode(body.handler);
82
+ const finalizer = asNode(body.finalizer);
83
+ const handlerBody = handler === undefined ? undefined : asNode(handler.body);
84
+ return (
85
+ (block === undefined ? undefined : returnedExpression(block)) ??
86
+ (handlerBody === undefined ? undefined : returnedExpression(handlerBody)) ??
87
+ (finalizer === undefined ? undefined : returnedExpression(finalizer))
88
+ );
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ export function componentExpression(
94
+ graph: ModuleGraph,
95
+ filePath: string,
96
+ name: string,
97
+ ): AstNode | undefined {
98
+ const file = graph.file(filePath);
99
+ if (file === undefined) return undefined;
100
+ const declaration = componentDeclaration(file.program, name);
101
+ if (declaration === undefined) return undefined;
102
+ if (declaration.type === "ArrowFunctionExpression") {
103
+ const body = asNode(declaration.body);
104
+ return body === undefined
105
+ ? undefined
106
+ : body.type === "BlockStatement"
107
+ ? returnedExpression(body)
108
+ : body;
109
+ }
110
+ if (declaration.type === "FunctionDeclaration" || declaration.type === "FunctionExpression") {
111
+ const body = asNode(declaration.body);
112
+ return body === undefined ? undefined : returnedExpression(body);
113
+ }
114
+ return declaration;
115
+ }
116
+
117
+ export function resolveComponent(
118
+ graph: ModuleGraph,
119
+ originFile: string,
120
+ name: string,
121
+ ): ComponentRef | null {
122
+ const local = name.includes(".") ? name.slice(0, name.indexOf(".")) : name;
123
+ const binding = graph.resolveBinding(originFile, local);
124
+ if (binding === null) return null;
125
+ const file = graph.file(binding.file);
126
+ if (file === undefined) return null;
127
+ return { file: file.path, name: binding.name };
128
+ }
129
+
130
+ export function firstComponentName(graph: ModuleGraph, filePath: string): string | null {
131
+ const file = graph.file(filePath);
132
+ if (file === undefined) return null;
133
+ for (const statement of nodeArray(file.program, "body")) {
134
+ const declaration =
135
+ statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration"
136
+ ? asNode(statement.declaration)
137
+ : statement;
138
+ if (declaration === undefined) continue;
139
+ if (declaration.type === "FunctionDeclaration") {
140
+ const id = asNode(declaration.id);
141
+ if (id !== undefined && typeof id.name === "string") return id.name;
142
+ }
143
+ if (declaration.type !== "VariableDeclaration") continue;
144
+ for (const item of nodeArray(declaration, "declarations")) {
145
+ const id = asNode(item.id);
146
+ const init = asNode(item.init);
147
+ if (id === undefined || init === undefined || typeof id.name !== "string") continue;
148
+ if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")
149
+ return id.name;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+
155
+ export function canExpand(file: string): boolean {
156
+ if (file.startsWith("src/components/ui/")) return false;
157
+ return (
158
+ file.startsWith("src/pages/") ||
159
+ file.startsWith("src/layouts/") ||
160
+ file.startsWith("src/components/")
161
+ );
162
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which intrinsic JSX elements the page preview draws, and which copy they carry.
3
+ *
4
+ * Sections are the page's layout skeleton and are always drawn; content intrinsics are the
5
+ * copy-bearing blocks and controls that make the schematic page readable (D-170). Layout-only
6
+ * wrappers such as `<div>` are flattened, and a copy-bearing text element with no readable copy
7
+ * is flattened too, so the preview never draws empty boxes for spacing wrappers.
8
+ */
9
+
10
+ import type { AstNode } from "../ast/node.ts";
11
+ import { isContentIntrinsic, isRoleIntrinsic, isSectionIntrinsic } from "./jsx.ts";
12
+ import { jsxContentFields, jsxTextFields, type JsxTextFields } from "./jsx-text.ts";
13
+
14
+ /** What a drawable intrinsic contributes: its copy and whether it spends the content budget. */
15
+ export interface IntrinsicPlan {
16
+ /** Content intrinsics have their own node budget and only draw when they carry something. */
17
+ readonly content: boolean;
18
+ readonly fields: JsxTextFields | undefined;
19
+ }
20
+
21
+ /** Classify one intrinsic element, or null when it is flattened into its parent. */
22
+ export function planIntrinsic(node: AstNode, name: string): IntrinsicPlan | null {
23
+ const section = isSectionIntrinsic(name);
24
+ if (!section && !isContentIntrinsic(name)) return null;
25
+ const fields = section ? jsxTextFields(node) : jsxContentFields(node, name);
26
+ // A control or media element is a visible block even without copy; a bare span is not.
27
+ if (!section && fields === undefined && !isRoleIntrinsic(name)) return null;
28
+ return { content: !section, fields };
29
+ }