protobuf-fastdsl 0.1.2 → 0.1.3

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/dist/index.d.ts CHANGED
@@ -20,8 +20,36 @@ interface ProtobufMessage {
20
20
  name: string;
21
21
  fields: ProtobufField[];
22
22
  }
23
+ /** Generic interface template, e.g. `interface Wrapper<T> { val: pb<1, T>; }` */
24
+ interface GenericProtobufTemplate {
25
+ name: string;
26
+ typeParams: string[];
27
+ fields: GenericFieldTemplate[];
28
+ }
29
+ interface GenericFieldTemplate {
30
+ name: string;
31
+ fieldNumber: number;
32
+ rawTypeName: string;
33
+ isTypeParam: boolean;
34
+ isOptional: boolean;
35
+ isRepeated: boolean;
36
+ }
23
37
  type MessageRegistry = Map<string, ProtobufMessage>;
24
38
 
39
+ interface ParsedFileEntry {
40
+ concrete: ProtobufMessage[];
41
+ templates: Map<string, GenericProtobufTemplate>;
42
+ }
43
+ interface ImportedDefinitions {
44
+ concrete: ProtobufMessage[];
45
+ templates: Map<string, GenericProtobufTemplate>;
46
+ }
47
+ /**
48
+ * Recursively resolve import-type declarations from a source file.
49
+ * Returns all protobuf interfaces and templates reachable through imports.
50
+ */
51
+ declare function resolveImports(code: string, importerPath: string, cache: Map<string, ParsedFileEntry>): ImportedDefinitions;
52
+
25
53
  /**
26
54
  * Deterministic mangled name for a type node.
27
55
  * `Foo` → `Foo`
@@ -51,12 +79,12 @@ interface AnalysisResult {
51
79
  *
52
80
  * Then post-processes: monomorphize → resolve wire types → topo sort.
53
81
  */
54
- declare function analyze(code: string, filePath: string): AnalysisResult;
82
+ declare function analyze(code: string, filePath: string, imported?: ImportedDefinitions): AnalysisResult;
55
83
  /**
56
84
  * Backward-compatible wrapper: returns only the MessageRegistry.
57
85
  * Uses the same single-walk analysis internally.
58
86
  */
59
- declare function analyzeSource(code: string, filePath: string): MessageRegistry;
87
+ declare function analyzeSource(code: string, filePath: string, imported?: ImportedDefinitions): MessageRegistry;
60
88
 
61
89
  /**
62
90
  * Generate fully self-contained encode/decode source code.
@@ -83,4 +111,4 @@ declare function replaceCallSites(code: string, registry: MessageRegistry): {
83
111
 
84
112
  declare function protobufVitePlugin(): Plugin;
85
113
 
86
- export { analyze, analyzeSource, applyReplacements, protobufVitePlugin as default, generateCode, replaceCallSites, typeNodeToMangledName };
114
+ export { analyze, analyzeSource, applyReplacements, protobufVitePlugin as default, generateCode, replaceCallSites, resolveImports, typeNodeToMangledName };
package/dist/index.js CHANGED
@@ -166,13 +166,30 @@ function resolveTypeArg(node, sf, templates, out) {
166
166
  }
167
167
 
168
168
  // src/ast/analyzer.ts
169
- function analyze(code, filePath) {
169
+ function analyze(code, filePath, imported) {
170
170
  const sf = ts4.createSourceFile(filePath, code, ts4.ScriptTarget.Latest, true);
171
171
  const concrete = [];
172
172
  const templates = /* @__PURE__ */ new Map();
173
173
  const mono = /* @__PURE__ */ new Map();
174
174
  const callSites = [];
175
175
  const deferredTypeArgs = [];
176
+ if (imported) {
177
+ concrete.push(...imported.concrete);
178
+ for (const [k, v] of imported.templates) templates.set(k, v);
179
+ }
180
+ const CANONICAL = /* @__PURE__ */ new Set(["protobuf_encode", "protobuf_decode"]);
181
+ const aliasToCanonical = /* @__PURE__ */ new Map();
182
+ for (const stmt of sf.statements) {
183
+ if (!ts4.isImportDeclaration(stmt) || !stmt.importClause) continue;
184
+ const bindings = stmt.importClause.namedBindings;
185
+ if (!bindings || !ts4.isNamedImports(bindings)) continue;
186
+ for (const el of bindings.elements) {
187
+ const originalName = (el.propertyName ?? el.name).text;
188
+ if (CANONICAL.has(originalName)) {
189
+ aliasToCanonical.set(el.name.text, originalName);
190
+ }
191
+ }
192
+ }
176
193
  ts4.forEachChild(sf, function visit(node) {
177
194
  if (ts4.isInterfaceDeclaration(node)) {
178
195
  if (node.typeParameters?.length) {
@@ -185,16 +202,19 @@ function analyze(code, filePath) {
185
202
  }
186
203
  if (ts4.isCallExpression(node)) {
187
204
  const e = node.expression;
188
- if (ts4.isIdentifier(e) && (e.text === "protobuf_encode" || e.text === "protobuf_decode")) {
189
- const ta = node.typeArguments;
190
- if (ta?.length) {
191
- deferredTypeArgs.push(ta[0]);
192
- callSites.push({
193
- fnName: e.text,
194
- exprStart: e.getStart(sf),
195
- typeArgsEnd: ta.end + 1,
196
- firstTypeArg: ta[0]
197
- });
205
+ if (ts4.isIdentifier(e)) {
206
+ const canonical = CANONICAL.has(e.text) ? e.text : aliasToCanonical.get(e.text);
207
+ if (canonical) {
208
+ const ta = node.typeArguments;
209
+ if (ta?.length) {
210
+ deferredTypeArgs.push(ta[0]);
211
+ callSites.push({
212
+ fnName: canonical,
213
+ exprStart: e.getStart(sf),
214
+ typeArgsEnd: ta.end + 1,
215
+ firstTypeArg: ta[0]
216
+ });
217
+ }
198
218
  }
199
219
  }
200
220
  }
@@ -219,8 +239,8 @@ function analyze(code, filePath) {
219
239
  }
220
240
  return { registry: topoSort(concrete), callSites, sourceFile: sf };
221
241
  }
222
- function analyzeSource(code, filePath) {
223
- return analyze(code, filePath).registry;
242
+ function analyzeSource(code, filePath, imported) {
243
+ return analyze(code, filePath, imported).registry;
224
244
  }
225
245
  function topoSort(messages) {
226
246
  const map = new Map(messages.map((m) => [m.name, m]));
@@ -1107,18 +1127,34 @@ function applyReplacements(code, sf, callSites, registry) {
1107
1127
  function replaceCallSites(code, registry) {
1108
1128
  const sf = ts5.createSourceFile("input.ts", code, ts5.ScriptTarget.Latest, true);
1109
1129
  const callSites = [];
1130
+ const CANONICAL = /* @__PURE__ */ new Set(["protobuf_encode", "protobuf_decode"]);
1131
+ const aliasToCanonical = /* @__PURE__ */ new Map();
1132
+ for (const stmt of sf.statements) {
1133
+ if (!ts5.isImportDeclaration(stmt) || !stmt.importClause) continue;
1134
+ const bindings = stmt.importClause.namedBindings;
1135
+ if (!bindings || !ts5.isNamedImports(bindings)) continue;
1136
+ for (const el of bindings.elements) {
1137
+ const originalName = (el.propertyName ?? el.name).text;
1138
+ if (CANONICAL.has(originalName)) {
1139
+ aliasToCanonical.set(el.name.text, originalName);
1140
+ }
1141
+ }
1142
+ }
1110
1143
  ts5.forEachChild(sf, function visit(node) {
1111
1144
  if (ts5.isCallExpression(node)) {
1112
1145
  const e = node.expression;
1113
- if (ts5.isIdentifier(e) && (e.text === "protobuf_encode" || e.text === "protobuf_decode")) {
1114
- const ta = node.typeArguments;
1115
- if (ta?.length) {
1116
- callSites.push({
1117
- fnName: e.text,
1118
- exprStart: e.getStart(sf),
1119
- typeArgsEnd: ta.end + 1,
1120
- firstTypeArg: ta[0]
1121
- });
1146
+ if (ts5.isIdentifier(e)) {
1147
+ const canonical = CANONICAL.has(e.text) ? e.text : aliasToCanonical.get(e.text);
1148
+ if (canonical) {
1149
+ const ta = node.typeArguments;
1150
+ if (ta?.length) {
1151
+ callSites.push({
1152
+ fnName: canonical,
1153
+ exprStart: e.getStart(sf),
1154
+ typeArgsEnd: ta.end + 1,
1155
+ firstTypeArg: ta[0]
1156
+ });
1157
+ }
1122
1158
  }
1123
1159
  }
1124
1160
  }
@@ -1127,19 +1163,116 @@ function replaceCallSites(code, registry) {
1127
1163
  return applyReplacements(code, sf, callSites, registry);
1128
1164
  }
1129
1165
 
1166
+ // src/ast/import-resolver.ts
1167
+ import ts6 from "typescript";
1168
+ import { readFileSync, existsSync } from "fs";
1169
+ import { dirname, resolve } from "path";
1170
+ function extractTypeImports(sf) {
1171
+ const result = [];
1172
+ for (const stmt of sf.statements) {
1173
+ if (!ts6.isImportDeclaration(stmt) || !stmt.importClause) continue;
1174
+ const spec = stmt.moduleSpecifier;
1175
+ if (!ts6.isStringLiteral(spec)) continue;
1176
+ const specifier = spec.text;
1177
+ const clause = stmt.importClause;
1178
+ const names = [];
1179
+ if (clause.isTypeOnly) {
1180
+ if (clause.namedBindings && ts6.isNamedImports(clause.namedBindings)) {
1181
+ for (const el of clause.namedBindings.elements) {
1182
+ names.push(el.name.text);
1183
+ }
1184
+ }
1185
+ } else if (clause.namedBindings && ts6.isNamedImports(clause.namedBindings)) {
1186
+ for (const el of clause.namedBindings.elements) {
1187
+ if (el.isTypeOnly) names.push(el.name.text);
1188
+ }
1189
+ }
1190
+ if (names.length > 0) result.push({ names, specifier });
1191
+ }
1192
+ return result;
1193
+ }
1194
+ function resolveModulePath(specifier, importerPath) {
1195
+ if (!specifier.startsWith(".")) return null;
1196
+ const base = resolve(dirname(importerPath), specifier);
1197
+ if (existsSync(base) && !base.endsWith(".ts") === false) return base;
1198
+ if (base.endsWith(".ts") && existsSync(base)) return base;
1199
+ const withTs = base + ".ts";
1200
+ if (existsSync(withTs)) return withTs;
1201
+ const indexTs = resolve(base, "index.ts");
1202
+ if (existsSync(indexTs)) return indexTs;
1203
+ return null;
1204
+ }
1205
+ function parseFileForDefinitions(absolutePath) {
1206
+ const code = readFileSync(absolutePath, "utf-8");
1207
+ const sf = ts6.createSourceFile(absolutePath, code, ts6.ScriptTarget.Latest, true);
1208
+ const concrete = [];
1209
+ const templates = /* @__PURE__ */ new Map();
1210
+ for (const stmt of sf.statements) {
1211
+ if (!ts6.isInterfaceDeclaration(stmt)) continue;
1212
+ if (stmt.typeParameters?.length) {
1213
+ const tpl = collectGenericInterface(stmt, sf);
1214
+ if (tpl) templates.set(tpl.name, tpl);
1215
+ } else {
1216
+ const msg = collectInterface(stmt, sf);
1217
+ if (msg) concrete.push(msg);
1218
+ }
1219
+ }
1220
+ return { concrete, templates };
1221
+ }
1222
+ function resolveImports(code, importerPath, cache) {
1223
+ const concrete = [];
1224
+ const templates = /* @__PURE__ */ new Map();
1225
+ const visiting = /* @__PURE__ */ new Set();
1226
+ function walk(filePath, fileCode) {
1227
+ const abs = resolve(filePath);
1228
+ if (visiting.has(abs)) return;
1229
+ visiting.add(abs);
1230
+ const src = fileCode ?? readFileSync(abs, "utf-8");
1231
+ const sf = ts6.createSourceFile(abs, src, ts6.ScriptTarget.Latest, true);
1232
+ const imports = extractTypeImports(sf);
1233
+ for (const imp of imports) {
1234
+ const resolved = resolveModulePath(imp.specifier, abs);
1235
+ if (!resolved) continue;
1236
+ let entry = cache.get(resolved);
1237
+ if (!entry) {
1238
+ entry = parseFileForDefinitions(resolved);
1239
+ cache.set(resolved, entry);
1240
+ }
1241
+ for (const msg of entry.concrete) {
1242
+ if (!concrete.some((m) => m.name === msg.name)) {
1243
+ concrete.push(msg);
1244
+ }
1245
+ }
1246
+ for (const [name, tpl] of entry.templates) {
1247
+ if (!templates.has(name)) templates.set(name, tpl);
1248
+ }
1249
+ walk(resolved);
1250
+ }
1251
+ }
1252
+ walk(importerPath, code);
1253
+ return { concrete, templates };
1254
+ }
1255
+
1130
1256
  // src/index.ts
1131
1257
  function protobufVitePlugin() {
1258
+ const fileCache = /* @__PURE__ */ new Map();
1132
1259
  return {
1133
1260
  name: "vite-plugin-protobuf",
1134
1261
  enforce: "pre",
1135
1262
  transform(code, id) {
1136
1263
  if (!id.endsWith(".ts") || id.endsWith(".d.ts")) return null;
1137
- const { registry, callSites, sourceFile } = analyze(code, id);
1264
+ const imported = resolveImports(code, id, fileCache);
1265
+ const { registry, callSites, sourceFile } = analyze(code, id, imported);
1138
1266
  if (registry.size === 0) return null;
1139
1267
  const generatedCode = generateCode(registry);
1140
1268
  const { transformedCode, hasReplacements } = applyReplacements(code, sourceFile, callSites, registry);
1141
1269
  if (!hasReplacements && generatedCode === "") return null;
1142
1270
  return { code: generatedCode + "\n" + transformedCode, map: null };
1271
+ },
1272
+ handleHotUpdate({ file }) {
1273
+ if (file.endsWith(".ts")) {
1274
+ fileCache.delete(file);
1275
+ }
1143
1276
  }
1144
1277
  };
1145
1278
  }
@@ -1150,5 +1283,6 @@ export {
1150
1283
  protobufVitePlugin as default,
1151
1284
  generateCode,
1152
1285
  replaceCallSites,
1286
+ resolveImports,
1153
1287
  typeNodeToMangledName
1154
1288
  };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Runtime fallback functions.
3
+ * If the Vite plugin is active, calls to protobuf_encode/decode are replaced
4
+ * at compile-time with generated type-specific functions.
5
+ * If NOT transformed, these fallbacks throw to alert the developer.
6
+ */
7
+ declare function protobuf_encode<T>(_params: T): Uint8Array;
8
+ declare function protobuf_decode<T>(_data: Uint8Array): T;
9
+
10
+ export { protobuf_decode, protobuf_encode };
@@ -0,0 +1,15 @@
1
+ // src/runtime.ts
2
+ function protobuf_encode(_params) {
3
+ throw new Error(
4
+ "protobuf_encode<T>() was not transformed by the protobuf-fastdsl Vite plugin. Make sure protobufVitePlugin() is added to your vite.config.ts plugins array."
5
+ );
6
+ }
7
+ function protobuf_decode(_data) {
8
+ throw new Error(
9
+ "protobuf_decode<T>() was not transformed by the protobuf-fastdsl Vite plugin. Make sure protobufVitePlugin() is added to your vite.config.ts plugins array."
10
+ );
11
+ }
12
+ export {
13
+ protobuf_decode,
14
+ protobuf_encode
15
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protobuf-fastdsl",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,12 +13,16 @@
13
13
  "types": "./dist/index.d.ts",
14
14
  "import": "./dist/index.js"
15
15
  },
16
+ "./runtime": {
17
+ "types": "./protobuf.d.ts",
18
+ "import": "./dist/runtime.js"
19
+ },
16
20
  "./types": {
17
21
  "types": "./protobuf.d.ts"
18
22
  }
19
23
  },
20
24
  "scripts": {
21
- "build": "tsup src/index.ts --format esm --dts",
25
+ "build": "tsup src/index.ts src/runtime.ts --format esm --dts",
22
26
  "test": "vitest run",
23
27
  "test:watch": "vitest",
24
28
  "bench": "npx tsx bench/index.ts"
package/protobuf.d.ts CHANGED
@@ -1,25 +1,25 @@
1
- // ── Protobuf field markers ────────────────────────────────────────────
2
- /** Marks a singular protobuf field: `name: pb<fieldNumber, Type>` */
3
- type pb<_ProtoNumber extends number, Type> = Type;
4
- /** Marks a repeated protobuf field: `ids: pb_repeated<fieldNumber, Type>` → Type[] */
5
- type pb_repeated<_ProtoNumber extends number, Type> = Type[];
6
-
7
- // ── Protobuf primitive types ──────────────────────────────────────────
8
- type uint_32 = number;
9
- type int_32 = number;
10
- type uint_64 = bigint;
11
- type int_64 = bigint;
12
- type sint_32 = number;
13
- type sint_64 = bigint;
14
- type bool = boolean;
15
- type float = number;
16
- type double = number;
17
- type fixed_32 = number;
18
- type fixed_64 = bigint;
19
- type sfixed_32 = number;
20
- type sfixed_64 = bigint;
21
- type bytes = Uint8Array;
22
-
23
- // ── Encode / decode stubs (replaced at compile-time by the vite plugin) ──
24
- declare function protobuf_encode<T>(params: T): Uint8Array;
25
- declare function protobuf_decode<T>(data: Uint8Array): T;
1
+ // ── Protobuf field markers ────────────────────────────────────────────
2
+ /** Marks a singular protobuf field: `name: pb<fieldNumber, Type>` */
3
+ export type pb<_ProtoNumber extends number, Type> = Type;
4
+ /** Marks a repeated protobuf field: `ids: pb_repeated<fieldNumber, Type>` → Type[] */
5
+ export type pb_repeated<_ProtoNumber extends number, Type> = Type[];
6
+
7
+ // ── Protobuf primitive types ──────────────────────────────────────────
8
+ export type uint_32 = number;
9
+ export type int_32 = number;
10
+ export type uint_64 = bigint;
11
+ export type int_64 = bigint;
12
+ export type sint_32 = number;
13
+ export type sint_64 = bigint;
14
+ export type bool = boolean;
15
+ export type float = number;
16
+ export type double = number;
17
+ export type fixed_32 = number;
18
+ export type fixed_64 = bigint;
19
+ export type sfixed_32 = number;
20
+ export type sfixed_64 = bigint;
21
+ export type bytes = Uint8Array;
22
+
23
+ // ── Encode / decode (replaced at compile-time by the vite plugin) ────
24
+ export function protobuf_encode<T>(params: T): Uint8Array;
25
+ export function protobuf_decode<T>(data: Uint8Array): T;