@vercel/python-analysis 0.12.0 → 0.13.2

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.cjs CHANGED
@@ -66,11 +66,13 @@ __export(src_exports, {
66
66
  UvConfigWorkspaceSchema: () => UvConfigWorkspaceSchema,
67
67
  UvIndexEntrySchema: () => UvIndexEntrySchema,
68
68
  classifyPackages: () => classifyPackages,
69
+ collectImportClosure: () => collectImportClosure,
69
70
  containsTopLevelCallable: () => containsTopLevelCallable,
70
71
  createMinimalManifest: () => createMinimalManifest,
71
72
  discoverPythonPackage: () => discoverPythonPackage,
72
73
  evaluateMarker: () => evaluateMarker,
73
74
  extendDistRecord: () => extendDistRecord,
75
+ extractImports: () => extractImports,
74
76
  findAppOrHandler: () => findAppOrHandler,
75
77
  getStringConstant: () => getStringConstant,
76
78
  isPrivatePackageSource: () => isPrivatePackageSource,
@@ -226,6 +228,224 @@ async function getStringConstant(source, name) {
226
228
  return mod.getStringConstant(source, name) ?? null;
227
229
  }
228
230
 
231
+ // src/semantic/import-graph.ts
232
+ var import_fs = __toESM(require("fs"), 1);
233
+ var import_path = require("path");
234
+ async function extractImports(source) {
235
+ if (!source.includes("import")) {
236
+ return [];
237
+ }
238
+ const mod = await importWasmModule();
239
+ return mod.extractImports(source);
240
+ }
241
+ var DEFAULT_MAX_FILES = 3e4;
242
+ async function readStatKind(path4) {
243
+ try {
244
+ const stats = await import_fs.default.promises.stat(path4);
245
+ if (stats.isFile())
246
+ return "file";
247
+ if (stats.isDirectory())
248
+ return "dir";
249
+ } catch {
250
+ }
251
+ return null;
252
+ }
253
+ function statKind(path4, cache) {
254
+ const cached = cache.statKinds.get(path4);
255
+ if (cached)
256
+ return cached;
257
+ const pending = readStatKind(path4);
258
+ cache.statKinds.set(path4, pending);
259
+ return pending;
260
+ }
261
+ async function resolveNamePartsUncached(parts, searchDirs, cache) {
262
+ const rel = (0, import_path.join)(...parts);
263
+ for (const root of searchDirs) {
264
+ let target = null;
265
+ let targetKind = null;
266
+ const candidateInit = (0, import_path.join)(root, rel, "__init__.py");
267
+ if (await statKind(candidateInit, cache) === "file") {
268
+ target = candidateInit;
269
+ targetKind = "file";
270
+ } else {
271
+ const candidateModule = (0, import_path.join)(root, `${rel}.py`);
272
+ if (await statKind(candidateModule, cache) === "file") {
273
+ target = candidateModule;
274
+ targetKind = "file";
275
+ } else if (await statKind((0, import_path.join)(root, rel), cache) === "dir") {
276
+ target = (0, import_path.join)(root, rel);
277
+ targetKind = "dir";
278
+ }
279
+ }
280
+ if (target !== null) {
281
+ const parentChain = [];
282
+ let acc = root;
283
+ for (const part of parts.slice(0, -1)) {
284
+ acc = (0, import_path.join)(acc, part);
285
+ const init = (0, import_path.join)(acc, "__init__.py");
286
+ if (await statKind(init, cache) === "file") {
287
+ parentChain.push(init);
288
+ }
289
+ }
290
+ return { target, targetKind, parentChain };
291
+ }
292
+ }
293
+ return null;
294
+ }
295
+ function resolveNameParts(parts, searchDirs, searchKey, cache) {
296
+ if (parts.length === 0)
297
+ return Promise.resolve(null);
298
+ const key = `${searchKey}\0${parts.join(".")}`;
299
+ const cached = cache.names.get(key);
300
+ if (cached)
301
+ return cached;
302
+ const pending = resolveNamePartsUncached(parts, searchDirs, cache);
303
+ cache.names.set(key, pending);
304
+ return pending;
305
+ }
306
+ async function resolveSeedName(parts, searchDirs, searchKey, cache) {
307
+ for (let length = parts.length; length > 0; length--) {
308
+ const resolved = await resolveNameParts(
309
+ parts.slice(0, length),
310
+ searchDirs,
311
+ searchKey,
312
+ cache
313
+ );
314
+ if (resolved)
315
+ return resolved;
316
+ }
317
+ return null;
318
+ }
319
+ function submoduleDir(target, targetKind) {
320
+ if (targetKind === "dir")
321
+ return target;
322
+ if (targetKind === "file" && target.endsWith(`${import_path.sep}__init__.py`)) {
323
+ return (0, import_path.dirname)(target);
324
+ }
325
+ return null;
326
+ }
327
+ async function resolveImport(stmt, importerPath, roots, rootsKey, cache) {
328
+ const resolved = /* @__PURE__ */ new Set();
329
+ let search = roots;
330
+ let searchKey = rootsKey;
331
+ if (stmt.level > 0) {
332
+ let base = (0, import_path.dirname)(importerPath);
333
+ for (let i = 1; i < stmt.level; i++) {
334
+ base = (0, import_path.dirname)(base);
335
+ }
336
+ search = [base];
337
+ searchKey = base;
338
+ }
339
+ const parts = stmt.module ? stmt.module.split(".") : [];
340
+ let target;
341
+ let targetKind;
342
+ if (parts.length > 0) {
343
+ const name = await resolveNameParts(parts, search, searchKey, cache);
344
+ if (!name)
345
+ return [];
346
+ target = name.target;
347
+ targetKind = name.targetKind;
348
+ if (targetKind === "file")
349
+ resolved.add(target);
350
+ for (const init of name.parentChain) {
351
+ resolved.add(init);
352
+ }
353
+ } else if (stmt.level > 0) {
354
+ target = search[0];
355
+ targetKind = "dir";
356
+ } else {
357
+ return [];
358
+ }
359
+ const subDir = submoduleDir(target, targetKind);
360
+ if (subDir) {
361
+ for (const name of stmt.names) {
362
+ if (name === "*")
363
+ continue;
364
+ const sub = await resolveNameParts([name], [subDir], subDir, cache);
365
+ if (sub) {
366
+ if (sub.targetKind === "file")
367
+ resolved.add(sub.target);
368
+ for (const init of sub.parentChain) {
369
+ resolved.add(init);
370
+ }
371
+ }
372
+ }
373
+ }
374
+ return [...resolved];
375
+ }
376
+ function isModuleNameSeed(seed) {
377
+ return !(0, import_path.isAbsolute)(seed) && /^[A-Za-z_][\w]*(\.[A-Za-z_][\w]*)*$/.test(seed);
378
+ }
379
+ async function collectImportClosure({
380
+ seeds,
381
+ searchRoots,
382
+ maxFiles = DEFAULT_MAX_FILES
383
+ }) {
384
+ const roots = searchRoots.map((r) => (0, import_path.resolve)(r));
385
+ const rootsKey = roots.join("\0");
386
+ const cache = {
387
+ statKinds: /* @__PURE__ */ new Map(),
388
+ names: /* @__PURE__ */ new Map()
389
+ };
390
+ const visited = /* @__PURE__ */ new Set();
391
+ let frontier = [];
392
+ let truncated = false;
393
+ for (const seed of seeds) {
394
+ if (isModuleNameSeed(seed)) {
395
+ const name = await resolveSeedName(
396
+ seed.split("."),
397
+ roots,
398
+ rootsKey,
399
+ cache
400
+ );
401
+ if (name) {
402
+ if (name.targetKind === "file")
403
+ frontier.push(name.target);
404
+ frontier.push(...name.parentChain);
405
+ }
406
+ } else {
407
+ const seedPath = (0, import_path.resolve)(seed);
408
+ if (await statKind(seedPath, cache) === "file") {
409
+ frontier.push(seedPath);
410
+ }
411
+ }
412
+ }
413
+ while (frontier.length > 0) {
414
+ const batch = [...new Set(frontier)].filter((p) => !visited.has(p));
415
+ frontier = [];
416
+ if (batch.length === 0)
417
+ continue;
418
+ for (const path4 of batch) {
419
+ visited.add(path4);
420
+ }
421
+ if (visited.size > maxFiles) {
422
+ truncated = true;
423
+ break;
424
+ }
425
+ const dependencies = await Promise.all(
426
+ batch.map(async (importerPath) => {
427
+ let source;
428
+ try {
429
+ source = await import_fs.default.promises.readFile(importerPath, "utf8");
430
+ } catch {
431
+ return [];
432
+ }
433
+ const stmts = await extractImports(source);
434
+ const files = await Promise.all(
435
+ stmts.filter((s) => s.isModuleLevel && !s.inTypeChecking).map((s) => resolveImport(s, importerPath, roots, rootsKey, cache))
436
+ );
437
+ return files.flat();
438
+ })
439
+ );
440
+ for (const dep of dependencies.flat()) {
441
+ if (!visited.has(dep)) {
442
+ frontier.push(dep);
443
+ }
444
+ }
445
+ }
446
+ return { files: visited, truncated };
447
+ }
448
+
229
449
  // src/manifest/dist-metadata.ts
230
450
  var import_node_crypto = require("crypto");
231
451
  var import_node_fs = require("fs");
@@ -394,7 +614,7 @@ async function scanDistributions(sitePackagesDir) {
394
614
  return index;
395
615
  }
396
616
  function hashFile(filePath) {
397
- return new Promise((resolve, reject) => {
617
+ return new Promise((resolve2, reject) => {
398
618
  const h = (0, import_node_crypto.createHash)("sha256");
399
619
  let size = 0;
400
620
  const stream = (0, import_node_fs.createReadStream)(filePath);
@@ -404,7 +624,7 @@ function hashFile(filePath) {
404
624
  });
405
625
  stream.on("error", reject);
406
626
  stream.on("end", () => {
407
- resolve({ hash: h.digest("base64url"), size });
627
+ resolve2({ hash: h.digest("base64url"), size });
408
628
  });
409
629
  });
410
630
  }
@@ -2435,11 +2655,13 @@ var HashDigestSchema = hashDigestSchema;
2435
2655
  UvConfigWorkspaceSchema,
2436
2656
  UvIndexEntrySchema,
2437
2657
  classifyPackages,
2658
+ collectImportClosure,
2438
2659
  containsTopLevelCallable,
2439
2660
  createMinimalManifest,
2440
2661
  discoverPythonPackage,
2441
2662
  evaluateMarker,
2442
2663
  extendDistRecord,
2664
+ extractImports,
2443
2665
  findAppOrHandler,
2444
2666
  getStringConstant,
2445
2667
  isPrivatePackageSource,
package/dist/index.d.ts CHANGED
@@ -7,6 +7,8 @@
7
7
  * @module @vercel/python-analysis
8
8
  */
9
9
  export { findAppOrHandler, containsTopLevelCallable, getStringConstant, } from './semantic/entrypoints';
10
+ export { collectImportClosure, extractImports } from './semantic/import-graph';
11
+ export type { ImportClosureOptions, ImportClosureResult, ImportStmt, } from './semantic/import-graph';
10
12
  export type { Distribution, DistributionIndex, PackagePath, DirectUrlInfo, } from './manifest/dist-metadata';
11
13
  export { extendDistRecord, scanDistributions } from './manifest/dist-metadata';
12
14
  export type { PythonConfig, PythonConfigs, PythonLockFile, PythonManifest, PythonManifestOrigin, PythonPackage, PythonVersionConfig, } from './manifest/package';
@@ -14,7 +16,7 @@ export { discoverPythonPackage, PythonConfigKind, PythonLockFileKind, PythonMani
14
16
  export { createMinimalManifest, stringifyManifest, type CreateMinimalManifestOptions, } from './manifest/serialize';
15
17
  export type { ClassifyPackagesOptions, PackageClassification, UvLockFile, UvLockPackage, UvLockPackageSource, UvLockWheel, } from './manifest/uv-lock-parser';
16
18
  export { classifyPackages, isPrivatePackageSource, normalizePackageName, parseUvLock, } from './manifest/uv-lock-parser';
17
- export { evaluateMarker, isWheelCompatible, } from './manifest/wheel-compat';
19
+ export { evaluateMarker, isWheelCompatible } from './manifest/wheel-compat';
18
20
  export type { PythonSelectionResult, PythonVersionSelectionResult, } from './manifest/python-selector';
19
21
  export { selectPython, selectPythonVersion } from './manifest/python-selector';
20
22
  export { PythonAnalysisError } from './util/error';
package/dist/index.js CHANGED
@@ -139,11 +139,229 @@ async function getStringConstant(source, name) {
139
139
  return mod.getStringConstant(source, name) ?? null;
140
140
  }
141
141
 
142
+ // src/semantic/import-graph.ts
143
+ import fs from "fs";
144
+ import { isAbsolute, join as join2, dirname as dirname2, resolve, sep } from "path";
145
+ async function extractImports(source) {
146
+ if (!source.includes("import")) {
147
+ return [];
148
+ }
149
+ const mod = await importWasmModule();
150
+ return mod.extractImports(source);
151
+ }
152
+ var DEFAULT_MAX_FILES = 3e4;
153
+ async function readStatKind(path4) {
154
+ try {
155
+ const stats = await fs.promises.stat(path4);
156
+ if (stats.isFile())
157
+ return "file";
158
+ if (stats.isDirectory())
159
+ return "dir";
160
+ } catch {
161
+ }
162
+ return null;
163
+ }
164
+ function statKind(path4, cache) {
165
+ const cached = cache.statKinds.get(path4);
166
+ if (cached)
167
+ return cached;
168
+ const pending = readStatKind(path4);
169
+ cache.statKinds.set(path4, pending);
170
+ return pending;
171
+ }
172
+ async function resolveNamePartsUncached(parts, searchDirs, cache) {
173
+ const rel = join2(...parts);
174
+ for (const root of searchDirs) {
175
+ let target = null;
176
+ let targetKind = null;
177
+ const candidateInit = join2(root, rel, "__init__.py");
178
+ if (await statKind(candidateInit, cache) === "file") {
179
+ target = candidateInit;
180
+ targetKind = "file";
181
+ } else {
182
+ const candidateModule = join2(root, `${rel}.py`);
183
+ if (await statKind(candidateModule, cache) === "file") {
184
+ target = candidateModule;
185
+ targetKind = "file";
186
+ } else if (await statKind(join2(root, rel), cache) === "dir") {
187
+ target = join2(root, rel);
188
+ targetKind = "dir";
189
+ }
190
+ }
191
+ if (target !== null) {
192
+ const parentChain = [];
193
+ let acc = root;
194
+ for (const part of parts.slice(0, -1)) {
195
+ acc = join2(acc, part);
196
+ const init = join2(acc, "__init__.py");
197
+ if (await statKind(init, cache) === "file") {
198
+ parentChain.push(init);
199
+ }
200
+ }
201
+ return { target, targetKind, parentChain };
202
+ }
203
+ }
204
+ return null;
205
+ }
206
+ function resolveNameParts(parts, searchDirs, searchKey, cache) {
207
+ if (parts.length === 0)
208
+ return Promise.resolve(null);
209
+ const key = `${searchKey}\0${parts.join(".")}`;
210
+ const cached = cache.names.get(key);
211
+ if (cached)
212
+ return cached;
213
+ const pending = resolveNamePartsUncached(parts, searchDirs, cache);
214
+ cache.names.set(key, pending);
215
+ return pending;
216
+ }
217
+ async function resolveSeedName(parts, searchDirs, searchKey, cache) {
218
+ for (let length = parts.length; length > 0; length--) {
219
+ const resolved = await resolveNameParts(
220
+ parts.slice(0, length),
221
+ searchDirs,
222
+ searchKey,
223
+ cache
224
+ );
225
+ if (resolved)
226
+ return resolved;
227
+ }
228
+ return null;
229
+ }
230
+ function submoduleDir(target, targetKind) {
231
+ if (targetKind === "dir")
232
+ return target;
233
+ if (targetKind === "file" && target.endsWith(`${sep}__init__.py`)) {
234
+ return dirname2(target);
235
+ }
236
+ return null;
237
+ }
238
+ async function resolveImport(stmt, importerPath, roots, rootsKey, cache) {
239
+ const resolved = /* @__PURE__ */ new Set();
240
+ let search = roots;
241
+ let searchKey = rootsKey;
242
+ if (stmt.level > 0) {
243
+ let base = dirname2(importerPath);
244
+ for (let i = 1; i < stmt.level; i++) {
245
+ base = dirname2(base);
246
+ }
247
+ search = [base];
248
+ searchKey = base;
249
+ }
250
+ const parts = stmt.module ? stmt.module.split(".") : [];
251
+ let target;
252
+ let targetKind;
253
+ if (parts.length > 0) {
254
+ const name = await resolveNameParts(parts, search, searchKey, cache);
255
+ if (!name)
256
+ return [];
257
+ target = name.target;
258
+ targetKind = name.targetKind;
259
+ if (targetKind === "file")
260
+ resolved.add(target);
261
+ for (const init of name.parentChain) {
262
+ resolved.add(init);
263
+ }
264
+ } else if (stmt.level > 0) {
265
+ target = search[0];
266
+ targetKind = "dir";
267
+ } else {
268
+ return [];
269
+ }
270
+ const subDir = submoduleDir(target, targetKind);
271
+ if (subDir) {
272
+ for (const name of stmt.names) {
273
+ if (name === "*")
274
+ continue;
275
+ const sub = await resolveNameParts([name], [subDir], subDir, cache);
276
+ if (sub) {
277
+ if (sub.targetKind === "file")
278
+ resolved.add(sub.target);
279
+ for (const init of sub.parentChain) {
280
+ resolved.add(init);
281
+ }
282
+ }
283
+ }
284
+ }
285
+ return [...resolved];
286
+ }
287
+ function isModuleNameSeed(seed) {
288
+ return !isAbsolute(seed) && /^[A-Za-z_][\w]*(\.[A-Za-z_][\w]*)*$/.test(seed);
289
+ }
290
+ async function collectImportClosure({
291
+ seeds,
292
+ searchRoots,
293
+ maxFiles = DEFAULT_MAX_FILES
294
+ }) {
295
+ const roots = searchRoots.map((r) => resolve(r));
296
+ const rootsKey = roots.join("\0");
297
+ const cache = {
298
+ statKinds: /* @__PURE__ */ new Map(),
299
+ names: /* @__PURE__ */ new Map()
300
+ };
301
+ const visited = /* @__PURE__ */ new Set();
302
+ let frontier = [];
303
+ let truncated = false;
304
+ for (const seed of seeds) {
305
+ if (isModuleNameSeed(seed)) {
306
+ const name = await resolveSeedName(
307
+ seed.split("."),
308
+ roots,
309
+ rootsKey,
310
+ cache
311
+ );
312
+ if (name) {
313
+ if (name.targetKind === "file")
314
+ frontier.push(name.target);
315
+ frontier.push(...name.parentChain);
316
+ }
317
+ } else {
318
+ const seedPath = resolve(seed);
319
+ if (await statKind(seedPath, cache) === "file") {
320
+ frontier.push(seedPath);
321
+ }
322
+ }
323
+ }
324
+ while (frontier.length > 0) {
325
+ const batch = [...new Set(frontier)].filter((p) => !visited.has(p));
326
+ frontier = [];
327
+ if (batch.length === 0)
328
+ continue;
329
+ for (const path4 of batch) {
330
+ visited.add(path4);
331
+ }
332
+ if (visited.size > maxFiles) {
333
+ truncated = true;
334
+ break;
335
+ }
336
+ const dependencies = await Promise.all(
337
+ batch.map(async (importerPath) => {
338
+ let source;
339
+ try {
340
+ source = await fs.promises.readFile(importerPath, "utf8");
341
+ } catch {
342
+ return [];
343
+ }
344
+ const stmts = await extractImports(source);
345
+ const files = await Promise.all(
346
+ stmts.filter((s) => s.isModuleLevel && !s.inTypeChecking).map((s) => resolveImport(s, importerPath, roots, rootsKey, cache))
347
+ );
348
+ return files.flat();
349
+ })
350
+ );
351
+ for (const dep of dependencies.flat()) {
352
+ if (!visited.has(dep)) {
353
+ frontier.push(dep);
354
+ }
355
+ }
356
+ }
357
+ return { files: visited, truncated };
358
+ }
359
+
142
360
  // src/manifest/dist-metadata.ts
143
361
  import { createHash } from "crypto";
144
362
  import { createReadStream } from "fs";
145
363
  import { appendFile, readdir, readFile as readFile2 } from "fs/promises";
146
- import { join as join2 } from "path";
364
+ import { join as join3 } from "path";
147
365
 
148
366
  // src/manifest/pep508.ts
149
367
  var EXTRAS_REGEX = /^(.+)\[([^\]]+)\]$/;
@@ -222,7 +440,7 @@ async function parsePep508(depOrDeps) {
222
440
  // src/manifest/dist-metadata.ts
223
441
  async function readDistInfoFile(distInfoDir, filename) {
224
442
  try {
225
- return await readFile2(join2(distInfoDir, filename), "utf-8");
443
+ return await readFile2(join3(distInfoDir, filename), "utf-8");
226
444
  } catch {
227
445
  return void 0;
228
446
  }
@@ -238,7 +456,7 @@ async function scanDistributions(sitePackagesDir) {
238
456
  }
239
457
  const distInfoDirs = entries.filter((e) => e.endsWith(".dist-info"));
240
458
  for (const dirName of distInfoDirs) {
241
- const distInfoPath = join2(sitePackagesDir, dirName);
459
+ const distInfoPath = join3(sitePackagesDir, dirName);
242
460
  const metadataContent = await readDistInfoFile(distInfoPath, "METADATA");
243
461
  if (!metadataContent) {
244
462
  console.debug(`Missing METADATA in ${dirName}`);
@@ -307,7 +525,7 @@ async function scanDistributions(sitePackagesDir) {
307
525
  return index;
308
526
  }
309
527
  function hashFile(filePath) {
310
- return new Promise((resolve, reject) => {
528
+ return new Promise((resolve2, reject) => {
311
529
  const h = createHash("sha256");
312
530
  let size = 0;
313
531
  const stream = createReadStream(filePath);
@@ -317,7 +535,7 @@ function hashFile(filePath) {
317
535
  });
318
536
  stream.on("error", reject);
319
537
  stream.on("end", () => {
320
- resolve({ hash: h.digest("base64url"), size });
538
+ resolve2({ hash: h.digest("base64url"), size });
321
539
  });
322
540
  });
323
541
  }
@@ -339,7 +557,7 @@ async function extendDistRecord(sitePackagesDir, packageName, paths) {
339
557
  `No .dist-info directory found for package "${packageName}" in ${sitePackagesDir}`
340
558
  );
341
559
  }
342
- const recordPath = join2(sitePackagesDir, distInfoDirName, "RECORD");
560
+ const recordPath = join3(sitePackagesDir, distInfoDirName, "RECORD");
343
561
  let existingRecord;
344
562
  try {
345
563
  existingRecord = await readFile2(recordPath, "utf-8");
@@ -354,7 +572,7 @@ async function extendDistRecord(sitePackagesDir, packageName, paths) {
354
572
  const prefix = existingRecord.length > 0 && !existingRecord.endsWith("\n") ? "\n" : "";
355
573
  const lines = [];
356
574
  for (const p of newEntries) {
357
- const fullPath = join2(sitePackagesDir, p);
575
+ const fullPath = join3(sitePackagesDir, p);
358
576
  const { hash, size } = await hashFile(fullPath);
359
577
  lines.push(`${p},sha256=${hash},${size}`);
360
578
  }
@@ -963,7 +1181,7 @@ var PyProjectToolSectionSchema = pyProjectToolSectionSchema.passthrough();
963
1181
  var PyProjectTomlSchema = pyProjectTomlSchema.extend({ tool: PyProjectToolSectionSchema.optional() }).passthrough();
964
1182
 
965
1183
  // src/manifest/requirements-txt-parser.ts
966
- import { isAbsolute, join as join3, relative } from "path/posix";
1184
+ import { isAbsolute as isAbsolute2, join as join4, relative } from "path/posix";
967
1185
  var PRIMARY_INDEX_NAME = "primary";
968
1186
  var EXTRA_INDEX_PREFIX = "extra-";
969
1187
  var FIND_LINKS_PREFIX = "find-links-";
@@ -1028,11 +1246,11 @@ function wasmEntryToNormalized(entry, editable) {
1028
1246
  function rebasePath(p, workingDir, packageRoot) {
1029
1247
  if (!workingDir || !packageRoot || workingDir === packageRoot)
1030
1248
  return p;
1031
- if (isAbsolute(p))
1249
+ if (isAbsolute2(p))
1032
1250
  return p;
1033
- const abs = join3(workingDir, p);
1251
+ const abs = join4(workingDir, p);
1034
1252
  const rel = relative(packageRoot, abs);
1035
- if (isAbsolute(rel))
1253
+ if (isAbsolute2(rel))
1036
1254
  return rel;
1037
1255
  if (rel.startsWith(".."))
1038
1256
  return rel;
@@ -2350,11 +2568,13 @@ export {
2350
2568
  UvConfigWorkspaceSchema,
2351
2569
  UvIndexEntrySchema,
2352
2570
  classifyPackages,
2571
+ collectImportClosure,
2353
2572
  containsTopLevelCallable,
2354
2573
  createMinimalManifest,
2355
2574
  discoverPythonPackage,
2356
2575
  evaluateMarker,
2357
2576
  extendDistRecord,
2577
+ extractImports,
2358
2578
  findAppOrHandler,
2359
2579
  getStringConstant,
2360
2580
  isPrivatePackageSource,
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Static import-graph analysis: compute the transitive closure of modules a
3
+ * Python application imports at startup, without executing any user code.
4
+ *
5
+ * The closure feeds bytecode packing. Both error directions are safe:
6
+ * over-included modules waste capacity, and missed modules (plugin loaders,
7
+ * imports made by compiled extensions) still ship from residual capacity.
8
+ *
9
+ * Resolution semantics mirror CPython:
10
+ * - dotted name -> `a/b/__init__.py` | `a/b.py` | PEP 420 namespace dir
11
+ * - importing `a.b.c` executes every parent `__init__.py` (emitted + recursed)
12
+ * - `from a import b` probes `b` as a submodule of `a`
13
+ * - relative imports resolve against the importer's package by dot level
14
+ * - first search root wins, so app modules shadow vendor (runtime sys.path)
15
+ *
16
+ * Startup heuristics (applied on top of the raw WASM extraction):
17
+ * - imports inside function bodies are lazy and excluded
18
+ * - `if TYPE_CHECKING:` blocks never run at runtime and are excluded
19
+ * - both branches of module-level `if` / `try` are included
20
+ */
21
+ export interface ImportStmt {
22
+ /** Dotted module path for from-imports; undefined for `from . import x`. */
23
+ module?: string;
24
+ /** Relative-import level (number of leading dots). 0 = absolute. */
25
+ level: number;
26
+ /** Imported names for from-imports (each may be a submodule). */
27
+ names: string[];
28
+ /** False when nested in a function body (lazy import). */
29
+ isModuleLevel: boolean;
30
+ /** True when under `if TYPE_CHECKING:` (never runs at runtime). */
31
+ inTypeChecking: boolean;
32
+ }
33
+ /**
34
+ * Extract every import statement in Python source with its syntactic
35
+ * context. Returns an empty list for invalid syntax.
36
+ */
37
+ export declare function extractImports(source: string): Promise<ImportStmt[]>;
38
+ export interface ImportClosureOptions {
39
+ /**
40
+ * Entry points of the closure: absolute `.py` file paths and/or dotted
41
+ * module or object names (e.g. Django's ROOT_URLCONF / INSTALLED_APPS
42
+ * strings), resolved to their longest importable prefix against searchRoots.
43
+ */
44
+ seeds: string[];
45
+ /**
46
+ * Ordered module search roots (e.g. [workPath, ...sitePackageDirs]).
47
+ * First match wins, so earlier roots shadow later ones.
48
+ */
49
+ searchRoots: string[];
50
+ /**
51
+ * Approximate safety bound on parsed files, checked per frontier batch.
52
+ * On overflow the partial closure is returned with `truncated: true`
53
+ * (remaining modules fall to the residual bytecode tier).
54
+ */
55
+ maxFiles?: number;
56
+ }
57
+ export interface ImportClosureResult {
58
+ /** Absolute paths of every `.py` file imported at startup. */
59
+ files: Set<string>;
60
+ truncated: boolean;
61
+ }
62
+ /**
63
+ * Compute the transitive closure of modules imported at startup.
64
+ *
65
+ * Only module-level imports outside `if TYPE_CHECKING:` blocks are followed
66
+ * (see module docstring). Files are read and parsed frontier-by-frontier in
67
+ * parallel; cycles are cut by the visited set.
68
+ */
69
+ export declare function collectImportClosure({ seeds, searchRoots, maxFiles, }: ImportClosureOptions): Promise<ImportClosureResult>;
@@ -1,4 +1,11 @@
1
1
  // world root:component/root
2
+ export interface ImportStmt {
3
+ module?: string,
4
+ level: number,
5
+ names: Array<string>,
6
+ isModuleLevel: boolean,
7
+ inTypeChecking: boolean,
8
+ }
2
9
  export interface RecordEntry {
3
10
  path: string,
4
11
  hash?: string,
@@ -121,6 +128,7 @@ export interface Root {
121
128
  findAppOrHandler(source: string): string | undefined,
122
129
  getStringConstant(source: string, name: string): string | undefined,
123
130
  containsTopLevelCallable(source: string, name: string): boolean,
131
+ extractImports(source: string): Array<ImportStmt>,
124
132
  parseDistMetadata(content: Uint8Array): DistMetadata,
125
133
  parseRecord(content: string): Array<RecordEntry>,
126
134
  parseDirectUrl(content: string): DirectUrlInfo,
@@ -709,6 +709,11 @@ const I32_MAX = 2_147_483_647;
709
709
  const I32_MIN = -2_147_483_648;
710
710
  const _typeCheckValidI32 = (n) => typeof n === 'number' && n >= I32_MIN && n <= I32_MAX;
711
711
 
712
+ function clampGuest(i, min, max) {
713
+ if (i < min || i > max) throw new TypeError(`must be between ${min} and ${max}`);
714
+ return i;
715
+ }
716
+
712
717
  const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
713
718
  let _fs;
714
719
  async function fetchCompile (url) {
@@ -3521,6 +3526,7 @@ let gen = (function* _initGenerator () {
3521
3526
  let postReturn4;
3522
3527
  let postReturn5;
3523
3528
  let postReturn6;
3529
+ let postReturn7;
3524
3530
  function trampoline0(handle) {
3525
3531
  const handleEntry = rscTableRemove(handleTable1, handle);
3526
3532
  if (handleEntry.own) {
@@ -3716,12 +3722,13 @@ let gen = (function* _initGenerator () {
3716
3722
  },
3717
3723
  }));
3718
3724
  postReturn0 = exports1['cabi_post_evaluate-marker'];
3719
- postReturn1 = exports1['cabi_post_parse-dist-metadata'];
3720
- postReturn2 = exports1['cabi_post_parse-record'];
3721
- postReturn3 = exports1['cabi_post_parse-direct-url'];
3722
- postReturn4 = exports1['cabi_post_normalize-package-name'];
3723
- postReturn5 = exports1['cabi_post_parse-requirements-txt'];
3724
- postReturn6 = exports1['cabi_post_parse-pep508'];
3725
+ postReturn1 = exports1['cabi_post_extract-imports'];
3726
+ postReturn2 = exports1['cabi_post_parse-dist-metadata'];
3727
+ postReturn3 = exports1['cabi_post_parse-record'];
3728
+ postReturn4 = exports1['cabi_post_parse-direct-url'];
3729
+ postReturn5 = exports1['cabi_post_normalize-package-name'];
3730
+ postReturn6 = exports1['cabi_post_parse-requirements-txt'];
3731
+ postReturn7 = exports1['cabi_post_parse-pep508'];
3725
3732
  let exports1FindAppOrHandler;
3726
3733
 
3727
3734
  function findAppOrHandler(arg0) {
@@ -3841,6 +3848,77 @@ let gen = (function* _initGenerator () {
3841
3848
  });
3842
3849
  return bool2 == 0 ? false : (bool2 == 1 ? true : throwInvalidBool());
3843
3850
  }
3851
+ let exports1ExtractImports;
3852
+
3853
+ function extractImports(arg0) {
3854
+ var ptr0 = utf8Encode(arg0, realloc0, memory0);
3855
+ var len0 = utf8EncodedLen;
3856
+ _debugLog('[iface="extract-imports", function="extract-imports"][Instruction::CallWasm] enter', {
3857
+ funcName: 'extract-imports',
3858
+ paramCount: 2,
3859
+ async: false,
3860
+ postReturn: true,
3861
+ });
3862
+ const _wasm_call_currentTaskID = startCurrentTask(0, false, 'exports1ExtractImports');
3863
+ const ret = exports1ExtractImports(ptr0, len0);
3864
+ endCurrentTask(0);
3865
+ var len7 = dataView(memory0).getUint32(ret + 4, true);
3866
+ var base7 = dataView(memory0).getUint32(ret + 0, true);
3867
+ var result7 = [];
3868
+ for (let i = 0; i < len7; i++) {
3869
+ const base = base7 + i * 28;
3870
+ let variant2;
3871
+ switch (dataView(memory0).getUint8(base + 0, true)) {
3872
+ case 0: {
3873
+ variant2 = undefined;
3874
+ break;
3875
+ }
3876
+ case 1: {
3877
+ var ptr1 = dataView(memory0).getUint32(base + 4, true);
3878
+ var len1 = dataView(memory0).getUint32(base + 8, true);
3879
+ var result1 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr1, len1));
3880
+ variant2 = result1;
3881
+ break;
3882
+ }
3883
+ default: {
3884
+ throw new TypeError('invalid variant discriminant for option');
3885
+ }
3886
+ }
3887
+ var len4 = dataView(memory0).getUint32(base + 20, true);
3888
+ var base4 = dataView(memory0).getUint32(base + 16, true);
3889
+ var result4 = [];
3890
+ for (let i = 0; i < len4; i++) {
3891
+ const base = base4 + i * 8;
3892
+ var ptr3 = dataView(memory0).getUint32(base + 0, true);
3893
+ var len3 = dataView(memory0).getUint32(base + 4, true);
3894
+ var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3));
3895
+ result4.push(result3);
3896
+ }
3897
+ var bool5 = dataView(memory0).getUint8(base + 24, true);
3898
+ var bool6 = dataView(memory0).getUint8(base + 25, true);
3899
+ result7.push({
3900
+ module: variant2,
3901
+ level: clampGuest(dataView(memory0).getUint8(base + 12, true), 0, 255),
3902
+ names: result4,
3903
+ isModuleLevel: bool5 == 0 ? false : (bool5 == 1 ? true : throwInvalidBool()),
3904
+ inTypeChecking: bool6 == 0 ? false : (bool6 == 1 ? true : throwInvalidBool()),
3905
+ });
3906
+ }
3907
+ _debugLog('[iface="extract-imports", function="extract-imports"][Instruction::Return]', {
3908
+ funcName: 'extract-imports',
3909
+ paramCount: 1,
3910
+ async: false,
3911
+ postReturn: true
3912
+ });
3913
+ const retCopy = result7;
3914
+
3915
+ let cstate = getOrCreateAsyncState(0);
3916
+ cstate.mayLeave = false;
3917
+ postReturn1(ret);
3918
+ cstate.mayLeave = true;
3919
+ return retCopy;
3920
+
3921
+ }
3844
3922
  let exports1ParseDistMetadata;
3845
3923
 
3846
3924
  function parseDistMetadata(arg0) {
@@ -4171,7 +4249,7 @@ let gen = (function* _initGenerator () {
4171
4249
 
4172
4250
  let cstate = getOrCreateAsyncState(0);
4173
4251
  cstate.mayLeave = false;
4174
- postReturn1(ret);
4252
+ postReturn2(ret);
4175
4253
  cstate.mayLeave = true;
4176
4254
 
4177
4255
 
@@ -4274,7 +4352,7 @@ let gen = (function* _initGenerator () {
4274
4352
 
4275
4353
  let cstate = getOrCreateAsyncState(0);
4276
4354
  cstate.mayLeave = false;
4277
- postReturn2(ret);
4355
+ postReturn3(ret);
4278
4356
  cstate.mayLeave = true;
4279
4357
 
4280
4358
 
@@ -4434,7 +4512,7 @@ let gen = (function* _initGenerator () {
4434
4512
 
4435
4513
  let cstate = getOrCreateAsyncState(0);
4436
4514
  cstate.mayLeave = false;
4437
- postReturn3(ret);
4515
+ postReturn4(ret);
4438
4516
  cstate.mayLeave = true;
4439
4517
 
4440
4518
 
@@ -4472,7 +4550,7 @@ let gen = (function* _initGenerator () {
4472
4550
 
4473
4551
  let cstate = getOrCreateAsyncState(0);
4474
4552
  cstate.mayLeave = false;
4475
- postReturn4(ret);
4553
+ postReturn5(ret);
4476
4554
  cstate.mayLeave = true;
4477
4555
  return retCopy;
4478
4556
 
@@ -4929,7 +5007,7 @@ let gen = (function* _initGenerator () {
4929
5007
 
4930
5008
  let cstate = getOrCreateAsyncState(0);
4931
5009
  cstate.mayLeave = false;
4932
- postReturn5(ret);
5010
+ postReturn6(ret);
4933
5011
  cstate.mayLeave = true;
4934
5012
 
4935
5013
 
@@ -5144,7 +5222,7 @@ let gen = (function* _initGenerator () {
5144
5222
 
5145
5223
  let cstate = getOrCreateAsyncState(0);
5146
5224
  cstate.mayLeave = false;
5147
- postReturn6(ret);
5225
+ postReturn7(ret);
5148
5226
  cstate.mayLeave = true;
5149
5227
 
5150
5228
 
@@ -5284,6 +5362,7 @@ let gen = (function* _initGenerator () {
5284
5362
  exports1FindAppOrHandler = exports1['find-app-or-handler'];
5285
5363
  exports1GetStringConstant = exports1['get-string-constant'];
5286
5364
  exports1ContainsTopLevelCallable = exports1['contains-top-level-callable'];
5365
+ exports1ExtractImports = exports1['extract-imports'];
5287
5366
  exports1ParseDistMetadata = exports1['parse-dist-metadata'];
5288
5367
  exports1ParseRecord = exports1['parse-record'];
5289
5368
  exports1ParseDirectUrl = exports1['parse-direct-url'];
@@ -5293,7 +5372,7 @@ let gen = (function* _initGenerator () {
5293
5372
  exports1IsWheelCompatible = exports1['is-wheel-compatible'];
5294
5373
  exports1EvaluateMarker = exports1['evaluate-marker'];
5295
5374
 
5296
- return { containsTopLevelCallable, evaluateMarker, findAppOrHandler, getStringConstant, isWheelCompatible, normalizePackageName, parseDirectUrl, parseDistMetadata, parsePep508, parseRecord, parseRequirementsTxt, };
5375
+ return { containsTopLevelCallable, evaluateMarker, extractImports, findAppOrHandler, getStringConstant, isWheelCompatible, normalizePackageName, parseDirectUrl, parseDistMetadata, parsePep508, parseRecord, parseRequirementsTxt, };
5297
5376
  })();
5298
5377
  let promise, resolve, reject;
5299
5378
  function runNext (value) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/python-analysis",
3
- "version": "0.12.0",
3
+ "version": "0.13.2",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -43,15 +43,14 @@
43
43
  "@types/js-yaml": "4.0.9",
44
44
  "@types/node": "20.11.0",
45
45
  "ts-to-zod": "^3",
46
- "vitest": "2.1.4"
46
+ "vitest": "4.1.10"
47
47
  },
48
48
  "scripts": {
49
- "build": "pnpm generate:schemas && pnpm build:wasm && node scripts/build.mjs",
49
+ "build": "node scripts/build.mjs",
50
50
  "build:wasm": "node scripts/build-wasm.mjs",
51
51
  "generate:schemas": "ts-to-zod --all",
52
52
  "test": "vitest run --config ../../vitest.config.mts",
53
- "vitest-run": "vitest -c ../../vitest.config.mts",
54
- "vitest-unit": "glob --absolute 'test/*.test.ts'",
55
- "type-check": "tsc --noEmit"
53
+ "type-check": "tsc --noEmit",
54
+ "test-unit": "vitest run --config ../../vitest.config.mts"
56
55
  }
57
56
  }
@@ -1,22 +0,0 @@
1
- /**
2
- * Host-side implementation of the `vercel:python-analysis/host-utils` WIT interface.
3
- */
4
- import { AsyncLocalStorage } from 'node:async_hooks';
5
- /**
6
- * Function type for reading referenced requirement files.
7
- */
8
- export type ReadFileFn = (path: string) => string | null;
9
- export interface ReadFileContext {
10
- readFile: ReadFileFn;
11
- workingDir?: string;
12
- }
13
- export declare const readFileStorage: AsyncLocalStorage<ReadFileContext>;
14
- export declare function createHostUtils(): {
15
- readFile(path: string): string;
16
- domainToAscii(domain: string): string;
17
- domainToUnicode(domain: string): [string, boolean];
18
- nfcNormalize(s: string): string;
19
- nfdNormalize(s: string): string;
20
- nfkcNormalize(s: string): string;
21
- nfkdNormalize(s: string): string;
22
- };
@@ -1,4 +0,0 @@
1
- type ModuleType = typeof import('#wasm/vercel_python_analysis.js');
2
- type RootType = Awaited<ReturnType<ModuleType['instantiate']>>;
3
- export declare function importWasmModule(): Promise<RootType>;
4
- export {};