@vercel/python-analysis 0.4.1 → 0.5.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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // src/semantic/load.ts
1
+ // src/wasm/load.ts
2
2
  import { readFile } from "fs/promises";
3
3
  import { createRequire } from "module";
4
4
  import { dirname, join } from "path";
@@ -17,7 +17,7 @@ function getWasmDir() {
17
17
  }
18
18
  async function getCoreModule(path4) {
19
19
  const wasmPath = join(getWasmDir(), path4);
20
- const wasmBytes = await readFile(wasmPath);
20
+ const wasmBytes = new Uint8Array(await readFile(wasmPath));
21
21
  return WebAssembly.compile(wasmBytes);
22
22
  }
23
23
  async function importWasmModule() {
@@ -47,6 +47,96 @@ async function containsAppOrHandler(source) {
47
47
  return mod.containsAppOrHandler(source);
48
48
  }
49
49
 
50
+ // src/manifest/dist-metadata.ts
51
+ import { readdir, readFile as readFile2 } from "fs/promises";
52
+ import { join as join2 } from "path";
53
+ async function readDistInfoFile(distInfoDir, filename) {
54
+ try {
55
+ return await readFile2(join2(distInfoDir, filename), "utf-8");
56
+ } catch {
57
+ return void 0;
58
+ }
59
+ }
60
+ async function scanDistributions(sitePackagesDir) {
61
+ const mod = await importWasmModule();
62
+ const index = /* @__PURE__ */ new Map();
63
+ let entries;
64
+ try {
65
+ entries = await readdir(sitePackagesDir);
66
+ } catch {
67
+ return index;
68
+ }
69
+ const distInfoDirs = entries.filter((e) => e.endsWith(".dist-info"));
70
+ for (const dirName of distInfoDirs) {
71
+ const distInfoPath = join2(sitePackagesDir, dirName);
72
+ const metadataContent = await readDistInfoFile(distInfoPath, "METADATA");
73
+ if (!metadataContent) {
74
+ console.debug(`Missing METADATA in ${dirName}`);
75
+ continue;
76
+ }
77
+ let metadata;
78
+ try {
79
+ metadata = mod.parseDistMetadata(
80
+ new TextEncoder().encode(metadataContent)
81
+ );
82
+ } catch (e) {
83
+ console.debug(`Failed to parse METADATA for ${dirName}: ${e}`);
84
+ continue;
85
+ }
86
+ const normalizedName = mod.normalizePackageName(metadata.name);
87
+ let files = [];
88
+ const recordContent = await readDistInfoFile(distInfoPath, "RECORD");
89
+ if (recordContent) {
90
+ try {
91
+ files = mod.parseRecord(recordContent);
92
+ } catch (e) {
93
+ console.warn(`Failed to parse RECORD for ${dirName}: ${e}`);
94
+ }
95
+ }
96
+ let origin;
97
+ const directUrlContent = await readDistInfoFile(
98
+ distInfoPath,
99
+ "direct_url.json"
100
+ );
101
+ if (directUrlContent) {
102
+ try {
103
+ origin = mod.parseDirectUrl(directUrlContent);
104
+ } catch (e) {
105
+ console.debug(`Failed to parse direct_url.json for ${dirName}: ${e}`);
106
+ }
107
+ }
108
+ const installerContent = await readDistInfoFile(distInfoPath, "INSTALLER");
109
+ const installer = installerContent?.trim() || void 0;
110
+ const dist = {
111
+ name: normalizedName,
112
+ version: metadata.version,
113
+ metadataVersion: metadata.metadataVersion,
114
+ summary: metadata.summary,
115
+ description: metadata.description,
116
+ descriptionContentType: metadata.descriptionContentType,
117
+ requiresDist: metadata.requiresDist,
118
+ requiresPython: metadata.requiresPython,
119
+ providesExtra: metadata.providesExtra,
120
+ author: metadata.author,
121
+ authorEmail: metadata.authorEmail,
122
+ maintainer: metadata.maintainer,
123
+ maintainerEmail: metadata.maintainerEmail,
124
+ license: metadata.license,
125
+ licenseExpression: metadata.licenseExpression,
126
+ classifiers: metadata.classifiers,
127
+ homePage: metadata.homePage,
128
+ projectUrls: metadata.projectUrls,
129
+ platforms: metadata.platforms,
130
+ dynamic: metadata.dynamic,
131
+ files,
132
+ origin,
133
+ installer
134
+ };
135
+ index.set(normalizedName, dist);
136
+ }
137
+ return index;
138
+ }
139
+
50
140
  // src/manifest/package.ts
51
141
  import path3 from "path";
52
142
  import { match as minimatchMatch } from "minimatch";
@@ -58,7 +148,7 @@ import toml from "smol-toml";
58
148
 
59
149
  // src/util/fs.ts
60
150
  import path from "path";
61
- import { readFile as readFile2 } from "fs-extra";
151
+ import { readFile as readFile3 } from "fs-extra";
62
152
 
63
153
  // src/util/error.ts
64
154
  import util from "util";
@@ -88,7 +178,7 @@ var PythonAnalysisError = class extends Error {
88
178
  // src/util/fs.ts
89
179
  async function readFileIfExists(file) {
90
180
  try {
91
- return await readFile2(file);
181
+ return await readFile3(file);
92
182
  } catch (error) {
93
183
  if (!isErrnoException(error, "ENOENT")) {
94
184
  throw error;
@@ -991,9 +1081,9 @@ function normalizePathRequirement(rawLine) {
991
1081
  }
992
1082
  return req;
993
1083
  }
994
- function convertRequirementsToPyprojectToml(fileContent, readFile3) {
1084
+ function convertRequirementsToPyprojectToml(fileContent, readFile4) {
995
1085
  const pyproject = {};
996
- const parsed = parseRequirementsFile(fileContent, readFile3);
1086
+ const parsed = parseRequirementsFile(fileContent, readFile4);
997
1087
  const deps = [];
998
1088
  const sources = {};
999
1089
  for (const req of parsed.requirements) {
@@ -1052,11 +1142,11 @@ function buildIndexEntries(pipOptions) {
1052
1142
  }
1053
1143
  return indexes;
1054
1144
  }
1055
- function parseRequirementsFile(fileContent, readFile3) {
1145
+ function parseRequirementsFile(fileContent, readFile4) {
1056
1146
  const visited = /* @__PURE__ */ new Set();
1057
- return parseRequirementsFileInternal(fileContent, readFile3, visited);
1147
+ return parseRequirementsFileInternal(fileContent, readFile4, visited);
1058
1148
  }
1059
- function parseRequirementsFileInternal(fileContent, readFile3, visited) {
1149
+ function parseRequirementsFileInternal(fileContent, readFile4, visited) {
1060
1150
  const { cleanedContent, options, pathRequirements, editableRequirements } = extractPipArguments(fileContent);
1061
1151
  const hashMap = buildHashMap(fileContent);
1062
1152
  const requirements = parsePipRequirementsFile(cleanedContent);
@@ -1104,18 +1194,18 @@ function parseRequirementsFileInternal(fileContent, readFile3, visited) {
1104
1194
  normalized.push(norm);
1105
1195
  }
1106
1196
  }
1107
- if (readFile3) {
1197
+ if (readFile4) {
1108
1198
  for (const refPath of mergedOptions.requirementFiles) {
1109
1199
  const refPathKey = normalize(refPath);
1110
1200
  if (visited.has(refPathKey)) {
1111
1201
  continue;
1112
1202
  }
1113
1203
  visited.add(refPathKey);
1114
- const refContent = readFile3(refPath);
1204
+ const refContent = readFile4(refPath);
1115
1205
  if (refContent != null) {
1116
1206
  const refParsed = parseRequirementsFileInternal(
1117
1207
  refContent,
1118
- readFile3,
1208
+ readFile4,
1119
1209
  visited
1120
1210
  );
1121
1211
  const existingNames = new Set(
@@ -2052,6 +2142,95 @@ function createMinimalManifest(options = {}) {
2052
2142
  };
2053
2143
  }
2054
2144
 
2145
+ // src/manifest/uv-lock-parser.ts
2146
+ import toml3 from "smol-toml";
2147
+ function parseUvLock(content, path4) {
2148
+ let parsed;
2149
+ try {
2150
+ parsed = toml3.parse(content);
2151
+ } catch (error) {
2152
+ throw new PythonAnalysisError({
2153
+ message: `Could not parse uv.lock: ${error instanceof Error ? error.message : String(error)}`,
2154
+ code: "PYTHON_UV_LOCK_PARSE_ERROR",
2155
+ path: path4,
2156
+ fileContent: content
2157
+ });
2158
+ }
2159
+ const packages = (parsed.package ?? []).filter((pkg) => pkg.name && pkg.version).map((pkg) => ({
2160
+ name: pkg.name,
2161
+ version: pkg.version,
2162
+ source: pkg.source
2163
+ }));
2164
+ return { version: parsed.version, packages };
2165
+ }
2166
+ var PUBLIC_PYPI_PATTERNS = [
2167
+ "https://pypi.org",
2168
+ "https://files.pythonhosted.org",
2169
+ "pypi.org"
2170
+ ];
2171
+ function isPublicPyPIRegistry(registryUrl) {
2172
+ if (!registryUrl)
2173
+ return true;
2174
+ const normalized = registryUrl.toLowerCase();
2175
+ return PUBLIC_PYPI_PATTERNS.some((pattern) => normalized.includes(pattern));
2176
+ }
2177
+ function isPrivatePackageSource(source) {
2178
+ if (!source)
2179
+ return false;
2180
+ if (source.git)
2181
+ return true;
2182
+ if (source.path)
2183
+ return true;
2184
+ if (source.editable)
2185
+ return true;
2186
+ if (source.url)
2187
+ return true;
2188
+ if (source.virtual)
2189
+ return true;
2190
+ if (source.registry && !isPublicPyPIRegistry(source.registry)) {
2191
+ return true;
2192
+ }
2193
+ return false;
2194
+ }
2195
+ function normalizePackageName(name) {
2196
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
2197
+ }
2198
+ function classifyPackages(options) {
2199
+ const { lockFile, excludePackages = [] } = options;
2200
+ const privatePackages = [];
2201
+ const publicPackages = [];
2202
+ const packageVersions = {};
2203
+ const excludeSet = new Set(excludePackages.map(normalizePackageName));
2204
+ for (const pkg of lockFile.packages) {
2205
+ if (excludeSet.has(normalizePackageName(pkg.name))) {
2206
+ continue;
2207
+ }
2208
+ packageVersions[pkg.name] = pkg.version;
2209
+ if (isPrivatePackageSource(pkg.source)) {
2210
+ privatePackages.push(pkg.name);
2211
+ } else {
2212
+ publicPackages.push(pkg.name);
2213
+ }
2214
+ }
2215
+ return { privatePackages, publicPackages, packageVersions };
2216
+ }
2217
+ function generateRuntimeRequirements(classification) {
2218
+ const lines = [
2219
+ "# Auto-generated requirements for runtime installation",
2220
+ "# Private packages are bundled separately and not listed here.",
2221
+ ""
2222
+ ];
2223
+ for (const pkgName of classification.publicPackages) {
2224
+ const version = classification.packageVersions[pkgName];
2225
+ if (version) {
2226
+ lines.push(`${pkgName}==${version}`);
2227
+ } else {
2228
+ lines.push(pkgName);
2229
+ }
2230
+ }
2231
+ return lines.join("\n");
2232
+ }
2233
+
2055
2234
  // src/manifest/python-selector.ts
2056
2235
  function selectPython(constraints, available) {
2057
2236
  const warnings = [];
@@ -2234,9 +2413,15 @@ export {
2234
2413
  UvConfigSchema,
2235
2414
  UvConfigWorkspaceSchema,
2236
2415
  UvIndexEntrySchema,
2416
+ classifyPackages,
2237
2417
  containsAppOrHandler,
2238
2418
  createMinimalManifest,
2239
2419
  discoverPythonPackage,
2420
+ generateRuntimeRequirements,
2421
+ isPrivatePackageSource,
2422
+ normalizePackageName,
2423
+ parseUvLock,
2424
+ scanDistributions,
2240
2425
  selectPython,
2241
2426
  stringifyManifest
2242
2427
  };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Installed distribution metadata scanning.
3
+ *
4
+ * Scans a site-packages directory for .dist-info subdirectories and parses
5
+ * their metadata using WASM-based parsers.
6
+ *
7
+ * Nomenclature strives to follow that of Python's importlib.metadata module:
8
+ * https://docs.python.org/3/library/importlib.metadata.html
9
+ */
10
+ /** A file record from a RECORD file (analogous to importlib.metadata.PackagePath). */
11
+ export interface PackagePath {
12
+ path: string;
13
+ hash?: string;
14
+ size?: bigint;
15
+ }
16
+ /** PEP 610 direct URL origin info (analogous to importlib.metadata.Distribution.origin). */
17
+ export type DirectUrlInfo = {
18
+ tag: 'local-directory';
19
+ val: {
20
+ url: string;
21
+ editable: boolean;
22
+ };
23
+ } | {
24
+ tag: 'archive';
25
+ val: {
26
+ url: string;
27
+ hash?: string;
28
+ };
29
+ } | {
30
+ tag: 'vcs';
31
+ val: {
32
+ url: string;
33
+ vcs: string;
34
+ commitId?: string;
35
+ requestedRevision?: string;
36
+ };
37
+ };
38
+ /**
39
+ * An installed distribution parsed from a .dist-info directory.
40
+ * Analogous to importlib.metadata.Distribution.
41
+ */
42
+ export interface Distribution {
43
+ /** Normalized package name (PEP 503). */
44
+ name: string;
45
+ /** Package version string. */
46
+ version: string;
47
+ /** Metadata version (e.g. "2.1", "2.3"). */
48
+ metadataVersion: string;
49
+ /** One-line summary. */
50
+ summary?: string;
51
+ /** Full description. */
52
+ description?: string;
53
+ /** Description content type (e.g. "text/markdown"). */
54
+ descriptionContentType?: string;
55
+ /** PEP 508 dependency specifiers (analogous to importlib.metadata.requires()). */
56
+ requiresDist: string[];
57
+ /** Python version requirement (e.g. ">=3.8"). */
58
+ requiresPython?: string;
59
+ /** Extra names provided by this distribution. */
60
+ providesExtra: string[];
61
+ /** Author name. */
62
+ author?: string;
63
+ /** Author email. */
64
+ authorEmail?: string;
65
+ /** Maintainer name. */
66
+ maintainer?: string;
67
+ /** Maintainer email. */
68
+ maintainerEmail?: string;
69
+ /** License text. */
70
+ license?: string;
71
+ /** SPDX license expression. */
72
+ licenseExpression?: string;
73
+ /** Trove classifiers. */
74
+ classifiers: string[];
75
+ /** Home page URL. */
76
+ homePage?: string;
77
+ /** Project URLs as [label, url] pairs. */
78
+ projectUrls: [string, string][];
79
+ /** Supported platforms. */
80
+ platforms: string[];
81
+ /** Dynamic metadata fields. */
82
+ dynamic: string[];
83
+ /** Installed files from RECORD (analogous to importlib.metadata.files()). */
84
+ files: PackagePath[];
85
+ /** PEP 610 origin info (analogous to importlib.metadata.Distribution.origin). */
86
+ origin?: DirectUrlInfo;
87
+ /** Installer tool name (from INSTALLER file). */
88
+ installer?: string;
89
+ }
90
+ /** Map of normalized package name to distribution info. */
91
+ export type DistributionIndex = Map<string, Distribution>;
92
+ /**
93
+ * Scan a site-packages directory for installed distributions.
94
+ *
95
+ * Reads .dist-info directories and parses their METADATA, RECORD,
96
+ * direct_url.json, and INSTALLER files.
97
+ *
98
+ * Analogous to importlib.metadata.distributions() but returns an indexed map.
99
+ *
100
+ * @param sitePackagesDir - Absolute path to a site-packages directory
101
+ * @returns Map of normalized package name to distribution info
102
+ */
103
+ export declare function scanDistributions(sitePackagesDir: string): Promise<DistributionIndex>;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * A source of a package in a uv.lock file
3
+ */
4
+ export interface UvLockPackageSource {
5
+ registry?: string;
6
+ url?: string;
7
+ git?: string;
8
+ path?: string;
9
+ editable?: string;
10
+ virtual?: string;
11
+ }
12
+ /**
13
+ * A package entry from a parsed uv.lock file.
14
+ */
15
+ export interface UvLockPackage {
16
+ name: string;
17
+ version: string;
18
+ source?: UvLockPackageSource;
19
+ }
20
+ /**
21
+ * Parsed uv.lock file structure.
22
+ */
23
+ export interface UvLockFile {
24
+ version?: number;
25
+ packages: UvLockPackage[];
26
+ }
27
+ /**
28
+ * Parse the contents of a uv.lock file.
29
+ *
30
+ * @param content - The raw content of the uv.lock file
31
+ * @param path - Optional path to the file for error reporting
32
+ */
33
+ export declare function parseUvLock(content: string, path?: string): UvLockFile;
34
+ /**
35
+ * Check if a package source indicates it's a private package.
36
+ *
37
+ * Private packages are those from:
38
+ * - Git repositories
39
+ * - Local file paths
40
+ * - Editable installs
41
+ * - Direct URLs
42
+ * - Non-PyPI registry URLs (private PyPI mirrors, custom indexes)
43
+ */
44
+ export declare function isPrivatePackageSource(source: UvLockPackageSource | undefined): boolean;
45
+ /**
46
+ * Result of classifying packages from a uv.lock file.
47
+ */
48
+ export interface PackageClassification {
49
+ privatePackages: string[];
50
+ publicPackages: string[];
51
+ packageVersions: Record<string, string>;
52
+ }
53
+ /**
54
+ * Normalize a Python package name according to PEP 503.
55
+ */
56
+ export declare function normalizePackageName(name: string): string;
57
+ /**
58
+ * Options for classifying packages.
59
+ */
60
+ export interface ClassifyPackagesOptions {
61
+ lockFile: UvLockFile;
62
+ excludePackages?: string[];
63
+ }
64
+ /**
65
+ * Classify packages from a uv.lock file into private and public categories.
66
+ *
67
+ * This is used for determining which packages can be installed from PyPI
68
+ * at runtime vs. which must be bundled with the deployment.
69
+ */
70
+ export declare function classifyPackages(options: ClassifyPackagesOptions): PackageClassification;
71
+ /**
72
+ * Generate requirements.txt content for runtime installation.
73
+ *
74
+ * Only includes public packages that will be installed at runtime from PyPI.
75
+ * Private packages should be bundled separately.
76
+ */
77
+ export declare function generateRuntimeRequirements(classification: PackageClassification): string;
@@ -0,0 +1,2 @@
1
+ /** @module Interface wasi:random/insecure-seed@0.2.6 **/
2
+ export function insecureSeed(): [bigint, bigint];
@@ -1,18 +1,78 @@
1
1
  // world root:component/root
2
+ export interface RecordEntry {
3
+ path: string,
4
+ hash?: string,
5
+ size?: bigint,
6
+ }
7
+ export interface DistMetadata {
8
+ metadataVersion: string,
9
+ name: string,
10
+ version: string,
11
+ summary?: string,
12
+ description?: string,
13
+ descriptionContentType?: string,
14
+ requiresDist: Array<string>,
15
+ requiresPython?: string,
16
+ providesExtra: Array<string>,
17
+ author?: string,
18
+ authorEmail?: string,
19
+ maintainer?: string,
20
+ maintainerEmail?: string,
21
+ license?: string,
22
+ licenseExpression?: string,
23
+ classifiers: Array<string>,
24
+ homePage?: string,
25
+ projectUrls: Array<[string, string]>,
26
+ platforms: Array<string>,
27
+ dynamic: Array<string>,
28
+ }
29
+ export interface DirUrlInfo {
30
+ url: string,
31
+ editable: boolean,
32
+ }
33
+ export interface ArchiveUrlInfo {
34
+ url: string,
35
+ hash?: string,
36
+ }
37
+ export interface VcsUrlInfo {
38
+ url: string,
39
+ vcs: string,
40
+ commitId?: string,
41
+ requestedRevision?: string,
42
+ }
43
+ export type DirectUrlInfo = DirectUrlInfoLocalDirectory | DirectUrlInfoArchive | DirectUrlInfoVcs;
44
+ export interface DirectUrlInfoLocalDirectory {
45
+ tag: 'local-directory',
46
+ val: DirUrlInfo,
47
+ }
48
+ export interface DirectUrlInfoArchive {
49
+ tag: 'archive',
50
+ val: ArchiveUrlInfo,
51
+ }
52
+ export interface DirectUrlInfoVcs {
53
+ tag: 'vcs',
54
+ val: VcsUrlInfo,
55
+ }
2
56
  import type * as WasiCliEnvironment from './interfaces/wasi-cli-environment.js'; // wasi:cli/environment@0.2.6
3
57
  import type * as WasiCliExit from './interfaces/wasi-cli-exit.js'; // wasi:cli/exit@0.2.6
4
58
  import type * as WasiCliStderr from './interfaces/wasi-cli-stderr.js'; // wasi:cli/stderr@0.2.6
5
59
  import type * as WasiIoError from './interfaces/wasi-io-error.js'; // wasi:io/error@0.2.6
6
60
  import type * as WasiIoStreams from './interfaces/wasi-io-streams.js'; // wasi:io/streams@0.2.6
61
+ import type * as WasiRandomInsecureSeed from './interfaces/wasi-random-insecure-seed.js'; // wasi:random/insecure-seed@0.2.6
7
62
  export interface ImportObject {
8
63
  'wasi:cli/environment@0.2.6': typeof WasiCliEnvironment,
9
64
  'wasi:cli/exit@0.2.6': typeof WasiCliExit,
10
65
  'wasi:cli/stderr@0.2.6': typeof WasiCliStderr,
11
66
  'wasi:io/error@0.2.6': typeof WasiIoError,
12
67
  'wasi:io/streams@0.2.6': typeof WasiIoStreams,
68
+ 'wasi:random/insecure-seed@0.2.6': typeof WasiRandomInsecureSeed,
13
69
  }
14
70
  export interface Root {
15
71
  containsAppOrHandler(source: string): boolean,
72
+ parseDistMetadata(content: Uint8Array): DistMetadata,
73
+ parseRecord(content: string): Array<RecordEntry>,
74
+ parseDirectUrl(content: string): DirectUrlInfo,
75
+ normalizePackageName(name: string): string,
16
76
  }
17
77
 
18
78
  /**