codama-renderers-dart 0.2.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.
@@ -0,0 +1,3 @@
1
+ export * from "./getRenderMapVisitor.js";
2
+ export * from "./getTypeManifestVisitor.js";
3
+ export * from "./renderVisitor.js";
@@ -0,0 +1,154 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join, dirname, relative, posix } from "node:path";
3
+
4
+ import { type RootNode } from "@codama/nodes";
5
+ import { rootNodeVisitor, visit } from "@codama/visitors-core";
6
+ import { deleteDirectory, writeRenderMap } from "@codama/renderers-core";
7
+
8
+ import type { Fragment } from "../utils/fragment.js";
9
+ import type { RenderOptions } from "../utils/options.js";
10
+ import { DartImportMap, DART_EXTERNAL_PACKAGE_MAP } from "../utils/importMap.js";
11
+ import { formatDartDirectory } from "../utils/formatCode.js";
12
+ import { getRenderMapVisitor } from "./getRenderMapVisitor.js";
13
+
14
+ /**
15
+ * Creates a visitor that renders Codama nodes as Dart files.
16
+ *
17
+ * @param outputDir - The directory to write generated files into (e.g., 'lib/src/generated')
18
+ * @param options - Rendering options
19
+ */
20
+ export function renderVisitor(
21
+ outputDir: string,
22
+ options: RenderOptions = {},
23
+ ) {
24
+ const {
25
+ deleteFolderBeforeRendering = true,
26
+ formatCode = false,
27
+ nameApi,
28
+ dependencyMap,
29
+ } = options;
30
+
31
+ return rootNodeVisitor((root: RootNode) => {
32
+ // 1. Optionally delete the output directory
33
+ if (deleteFolderBeforeRendering && existsSync(outputDir)) {
34
+ deleteDirectory(outputDir);
35
+ }
36
+
37
+ // 2. Build the render map
38
+ const renderMap = visit(
39
+ root,
40
+ getRenderMapVisitor({ nameApi, dependencyMap }),
41
+ );
42
+
43
+ // 3. Build a map of definedType module keys to their render map paths
44
+ const typePathMap: Record<string, string> = {};
45
+ for (const renderPath of renderMap.keys()) {
46
+ const match = renderPath.match(/^(?:.*\/)?types\/([a-z_]+)\.dart$/);
47
+ if (match) {
48
+ typePathMap[`definedType:${match[1]}`] = renderPath;
49
+ }
50
+ }
51
+
52
+ // 4. Resolve imports and write files
53
+ for (const [filePath, frag] of renderMap.entries()) {
54
+ const fullPath = join(outputDir, filePath);
55
+ const dir = dirname(fullPath);
56
+
57
+ // Ensure directory exists
58
+ if (!existsSync(dir)) {
59
+ mkdirSync(dir, { recursive: true });
60
+ }
61
+
62
+ // Compute per-file internal import map (relative paths to defined types)
63
+ const fileDir = dirname(filePath);
64
+ const internalMap: Record<string, string> = {};
65
+ for (const [key, typePath] of Object.entries(typePathMap)) {
66
+ // Don't import yourself
67
+ if (typePath === filePath) continue;
68
+ let rel = posix.relative(fileDir, typePath);
69
+ if (!rel.startsWith(".")) {
70
+ rel = `./${rel}`;
71
+ }
72
+ internalMap[key] = rel;
73
+ }
74
+
75
+ // Resolve fragment content with imports
76
+ const content = resolveFragmentContent(frag, dependencyMap ?? {}, internalMap);
77
+ writeFileSync(fullPath, content, "utf-8");
78
+ }
79
+
80
+ // 4. Optionally format
81
+ if (formatCode) {
82
+ formatDartDirectory(outputDir);
83
+ }
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Resolve a fragment into its final Dart file content,
89
+ * prepending the resolved import statements.
90
+ */
91
+ function resolveFragmentContent(
92
+ frag: Fragment,
93
+ dependencyMap: Record<string, string>,
94
+ internalMap: Record<string, string> = {},
95
+ ): string {
96
+ const { content, imports } = frag;
97
+
98
+ const importStr = imports.toString({
99
+ ...internalMap,
100
+ ...dependencyMap,
101
+ });
102
+
103
+ if (!importStr) {
104
+ return content + "\n";
105
+ }
106
+
107
+ // Split content into header comment and rest
108
+ const lines = content.split("\n");
109
+ const headerLines: string[] = [];
110
+ let restIndex = 0;
111
+
112
+ for (let i = 0; i < lines.length; i++) {
113
+ if (
114
+ lines[i].startsWith("//") ||
115
+ lines[i].trim() === ""
116
+ ) {
117
+ headerLines.push(lines[i]);
118
+ restIndex = i + 1;
119
+ } else {
120
+ break;
121
+ }
122
+ }
123
+
124
+ const header = headerLines.join("\n");
125
+ const rest = lines.slice(restIndex).join("\n");
126
+
127
+ // Clean up: remove the use() placeholder text from content
128
+ const cleanContent = cleanFragmentContent(rest);
129
+
130
+ return `${header}\n\n${importStr}\n\n${cleanContent}\n`;
131
+ }
132
+
133
+ /**
134
+ * Remove use() placeholder fragments from content.
135
+ * The use() function produces just the type name in content,
136
+ * but when used standalone (not in a template), it leaves
137
+ * lines like just "Uint8List" that need to be removed.
138
+ */
139
+ function cleanFragmentContent(content: string): string {
140
+ // Remove lines that are just standalone identifier names from use() calls.
141
+ // These are artifacts of the import tracking pattern where use() is called
142
+ // purely for its import side-effect. Matches: PascalCase, camelCase,
143
+ // and multi-word identifiers like "getStructEncoder".
144
+ const lines = content.split("\n");
145
+ const cleanLines = lines.filter((line) => {
146
+ const trimmed = line.trim();
147
+ // Skip lines that are a single identifier (no spaces, no punctuation)
148
+ if (/^[a-zA-Z][a-zA-Z0-9]*$/.test(trimmed)) {
149
+ return false;
150
+ }
151
+ return true;
152
+ });
153
+ return cleanLines.join("\n");
154
+ }