@hubspot/project-parsing-lib 0.16.0-beta.0 → 0.17.0-beta.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/package.json +6 -1
- package/src/exports/workspaces.d.ts +2 -2
- package/src/exports/workspaces.js +1 -1
- package/src/lang/copy.d.ts +3 -0
- package/src/lang/copy.js +3 -0
- package/src/lib/platformVersion.js +4 -0
- package/src/lib/workspaces.d.ts +48 -3
- package/src/lib/workspaces.js +188 -75
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/project-parsing-lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0-beta.0",
|
|
4
4
|
"description": "Parsing library for converting projects directory structures to their intermediate representation",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
"@inquirer/prompts": "^7.1.0",
|
|
112
112
|
"@types/node": "^24.9.0",
|
|
113
113
|
"@types/npm-packlist": "7.0.3",
|
|
114
|
+
"@types/npmcli__map-workspaces": "3.0.4",
|
|
114
115
|
"@types/semver": "^7.5.8",
|
|
115
116
|
"@typescript-eslint/eslint-plugin": "8.54.0",
|
|
116
117
|
"@typescript-eslint/parser": "8.54.0",
|
|
@@ -126,11 +127,15 @@
|
|
|
126
127
|
},
|
|
127
128
|
"dependencies": {
|
|
128
129
|
"@hubspot/local-dev-lib": "4.0.4",
|
|
130
|
+
"@npmcli/map-workspaces": "4.0.2",
|
|
129
131
|
"ajv": "8.18.0",
|
|
130
132
|
"ajv-formats": "3.0.1",
|
|
131
133
|
"glob": "13.0.0",
|
|
132
134
|
"npm-packlist": "8.0.1"
|
|
133
135
|
},
|
|
136
|
+
"resolutions": {
|
|
137
|
+
"ajv-formats/ajv": "8.18.0"
|
|
138
|
+
},
|
|
134
139
|
"scripts": {
|
|
135
140
|
"build": "tsx ./scripts/build.ts",
|
|
136
141
|
"lint": "echo 'Linting is disabled for Blazar'",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { findAndParsePackageJsonFiles, resolveWorkspaceDirectories, collectWorkspaceDirectories, collectFileDependencies, getPackableFiles, WorkspaceResolutionError, FileDependencyResolutionError, } from '../lib/workspaces.js';
|
|
2
|
-
export type { ParsedPackageJson, WorkspaceMapping, FileDependencyMapping, } from '../lib/workspaces.js';
|
|
1
|
+
export { findAndParsePackageJsonFiles, resolveWorkspaceDirectories, collectWorkspaceDirectories, collectFileDependencies, validateWorkspaceProtocolDependencies, parseLocalSpec, getPackableFiles, WorkspaceResolutionError, FileDependencyResolutionError, OrphanWorkspaceProtocolError, } from '../lib/workspaces.js';
|
|
2
|
+
export type { ParsedPackageJson, WorkspaceMapping, FileDependencyMapping, FileDependencyKind, LocalDependencyProtocol, } from '../lib/workspaces.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { findAndParsePackageJsonFiles, resolveWorkspaceDirectories, collectWorkspaceDirectories, collectFileDependencies, getPackableFiles, WorkspaceResolutionError, FileDependencyResolutionError, } from '../lib/workspaces.js';
|
|
1
|
+
export { findAndParsePackageJsonFiles, resolveWorkspaceDirectories, collectWorkspaceDirectories, collectFileDependencies, validateWorkspaceProtocolDependencies, parseLocalSpec, getPackableFiles, WorkspaceResolutionError, FileDependencyResolutionError, OrphanWorkspaceProtocolError, } from '../lib/workspaces.js';
|
package/src/lang/copy.d.ts
CHANGED
package/src/lang/copy.js
CHANGED
|
@@ -6,11 +6,15 @@ export function isSupportedPlatformVersion(platformVersion) {
|
|
|
6
6
|
if (!platformVersion || typeof platformVersion !== 'string') {
|
|
7
7
|
return false;
|
|
8
8
|
}
|
|
9
|
+
if (process.env.HUBSPOT_PROJECT_INTERNAL_TEST === 'true')
|
|
10
|
+
return true;
|
|
9
11
|
const supportedPlatformVersions = Object.values(PLATFORM_VERSIONS);
|
|
10
12
|
return supportedPlatformVersions.includes(platformVersion);
|
|
11
13
|
}
|
|
12
14
|
export function isLegacyProject(platformVersion) {
|
|
13
15
|
if (!platformVersion)
|
|
14
16
|
return false;
|
|
17
|
+
if (process.env.HUBSPOT_PROJECT_INTERNAL_TEST === 'true')
|
|
18
|
+
return false;
|
|
15
19
|
return [PLATFORM_VERSIONS.v2023_2, PLATFORM_VERSIONS.v2025_1].includes(platformVersion);
|
|
16
20
|
}
|
package/src/lib/workspaces.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export declare class FileDependencyResolutionError extends Error {
|
|
|
15
15
|
sourcePackageJson: string;
|
|
16
16
|
constructor(packageName: string, dependencyPath: string, sourcePackageJson: string, cause?: Error);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Error thrown when a `workspace:` protocol dependency does not match any known
|
|
20
|
+
* workspace member. npm itself does not support the `workspace:` protocol —
|
|
21
|
+
* only pnpm and yarn berry do — so the install fails with a confusing message
|
|
22
|
+
* server-side. Surface a clearer error up front.
|
|
23
|
+
*/
|
|
24
|
+
export declare class OrphanWorkspaceProtocolError extends Error {
|
|
25
|
+
packageName: string;
|
|
26
|
+
sourcePackageJson: string;
|
|
27
|
+
constructor(packageName: string, sourcePackageJson: string);
|
|
28
|
+
}
|
|
18
29
|
interface PackageJson {
|
|
19
30
|
name?: string;
|
|
20
31
|
workspaces?: string[] | {
|
|
@@ -32,18 +43,40 @@ export interface WorkspaceMapping {
|
|
|
32
43
|
workspaceDir: string;
|
|
33
44
|
sourcePackageJsonPath: string;
|
|
34
45
|
}
|
|
46
|
+
export type FileDependencyKind = 'directory' | 'tarball';
|
|
47
|
+
export type LocalDependencyProtocol = 'file' | 'link';
|
|
35
48
|
export interface FileDependencyMapping {
|
|
36
49
|
packageName: string;
|
|
37
50
|
localPath: string;
|
|
38
51
|
sourcePackageJsonPath: string;
|
|
52
|
+
kind: FileDependencyKind;
|
|
53
|
+
protocol: LocalDependencyProtocol;
|
|
39
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Parses a `file:` or `link:` dependency spec into its protocol and raw path.
|
|
57
|
+
* Handles all npm-supported shapes:
|
|
58
|
+
* - file:./relative/path
|
|
59
|
+
* - file:relative/path (no leading slash/dot)
|
|
60
|
+
* - file:/absolute/path
|
|
61
|
+
* - file://./relative — npm-style file URL with relative path
|
|
62
|
+
* - file:///absolute — file URL with absolute path
|
|
63
|
+
* - link:./relative/path (symlink semantics for directories only)
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseLocalSpec(spec: string): {
|
|
66
|
+
protocol: LocalDependencyProtocol;
|
|
67
|
+
rawPath: string;
|
|
68
|
+
} | null;
|
|
40
69
|
/**
|
|
41
70
|
* Finds and parses all package.json files in a directory.
|
|
42
71
|
* This is the single entry point for discovering package.json files and parsing their contents.
|
|
43
72
|
*/
|
|
44
73
|
export declare function findAndParsePackageJsonFiles(srcDir: string): Promise<ParsedPackageJson[]>;
|
|
45
74
|
/**
|
|
46
|
-
* Resolves workspace glob patterns to actual directories
|
|
75
|
+
* Resolves workspace glob patterns to actual directories.
|
|
76
|
+
*
|
|
77
|
+
* Uses @npmcli/map-workspaces (npm's reference impl) so that negation patterns
|
|
78
|
+
* like `["packages/*", "!packages/excluded"]` are honored — a hand-rolled
|
|
79
|
+
* per-pattern glob() silently accepted negations without actually excluding.
|
|
47
80
|
*/
|
|
48
81
|
export declare function resolveWorkspaceDirectories(baseDir: string, workspaceGlobs: string[]): Promise<string[]>;
|
|
49
82
|
/**
|
|
@@ -53,10 +86,22 @@ export declare function resolveWorkspaceDirectories(baseDir: string, workspaceGl
|
|
|
53
86
|
*/
|
|
54
87
|
export declare function collectWorkspaceDirectories(parsedPackageJsons: ParsedPackageJson[]): Promise<WorkspaceMapping[]>;
|
|
55
88
|
/**
|
|
56
|
-
* Collects all file
|
|
57
|
-
* Returns mappings that track the package name, resolved path,
|
|
89
|
+
* Collects all `file:` and `link:` dependencies that need to be uploaded.
|
|
90
|
+
* Returns mappings that track the package name, resolved path, source
|
|
91
|
+
* package.json, kind (directory vs tarball) and protocol (file vs link).
|
|
58
92
|
*/
|
|
59
93
|
export declare function collectFileDependencies(parsedPackageJsons: ParsedPackageJson[]): Promise<FileDependencyMapping[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Validates that every `workspace:` protocol dependency in the parsed package
|
|
96
|
+
* trees matches a known workspace member. Throws OrphanWorkspaceProtocolError
|
|
97
|
+
* for the first orphan found.
|
|
98
|
+
*
|
|
99
|
+
* npm itself does not implement the `workspace:` protocol; only pnpm and yarn
|
|
100
|
+
* berry do. If a project uses `workspace:*` without a matching member declared
|
|
101
|
+
* under `workspaces`, the install fails server-side with a confusing message —
|
|
102
|
+
* fail fast with a clearer one.
|
|
103
|
+
*/
|
|
104
|
+
export declare function validateWorkspaceProtocolDependencies(parsedPackageJsons: ParsedPackageJson[], workspaceMappings: WorkspaceMapping[]): void;
|
|
60
105
|
/**
|
|
61
106
|
* Returns the set of files that npm would include when publishing a package.
|
|
62
107
|
* Uses npm-packlist which respects the "files" field in package.json,
|
package/src/lib/workspaces.js
CHANGED
|
@@ -2,11 +2,13 @@ import fs from 'fs';
|
|
|
2
2
|
import fsPromises from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
import {
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import mapWorkspaces from '@npmcli/map-workspaces';
|
|
6
7
|
import packlist from 'npm-packlist';
|
|
7
8
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
8
9
|
import { walk } from '@hubspot/local-dev-lib/fs';
|
|
9
10
|
import { createMinimalTree } from './minimalArboristTree.js';
|
|
11
|
+
import { logMessages } from '../lang/copy.js';
|
|
10
12
|
/**
|
|
11
13
|
* Error thrown when a workspace directory cannot be resolved
|
|
12
14
|
*/
|
|
@@ -41,6 +43,28 @@ export class FileDependencyResolutionError extends Error {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Error thrown when a `workspace:` protocol dependency does not match any known
|
|
48
|
+
* workspace member. npm itself does not support the `workspace:` protocol —
|
|
49
|
+
* only pnpm and yarn berry do — so the install fails with a confusing message
|
|
50
|
+
* server-side. Surface a clearer error up front.
|
|
51
|
+
*/
|
|
52
|
+
export class OrphanWorkspaceProtocolError extends Error {
|
|
53
|
+
packageName;
|
|
54
|
+
sourcePackageJson;
|
|
55
|
+
constructor(packageName, sourcePackageJson) {
|
|
56
|
+
super(`Found \`workspace:\` protocol dependency "${packageName}" in ${sourcePackageJson} but no workspace member is named "${packageName}". ` +
|
|
57
|
+
`The \`workspace:\` protocol requires a matching workspace member declared under "workspaces" in a parent package.json. ` +
|
|
58
|
+
`If you're migrating from pnpm or yarn berry, replace with \`file:./<relative-path>\` or \`link:./<relative-path>\`, or add the package under "workspaces".`);
|
|
59
|
+
this.packageName = packageName;
|
|
60
|
+
this.sourcePackageJson = sourcePackageJson;
|
|
61
|
+
this.name = 'OrphanWorkspaceProtocolError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function hasTarballExtension(p) {
|
|
65
|
+
const { ext, name } = path.parse(p.toLowerCase());
|
|
66
|
+
return (ext === '.tgz' || ext === '.tar' || (ext === '.gz' && name.endsWith('.tar')));
|
|
67
|
+
}
|
|
44
68
|
/**
|
|
45
69
|
* Expands tilde (~) in paths to the user's home directory
|
|
46
70
|
*/
|
|
@@ -49,6 +73,33 @@ function expandTildePath(filePath) {
|
|
|
49
73
|
? path.join(os.homedir(), filePath.slice(1))
|
|
50
74
|
: filePath;
|
|
51
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Parses a `file:` or `link:` dependency spec into its protocol and raw path.
|
|
78
|
+
* Handles all npm-supported shapes:
|
|
79
|
+
* - file:./relative/path
|
|
80
|
+
* - file:relative/path (no leading slash/dot)
|
|
81
|
+
* - file:/absolute/path
|
|
82
|
+
* - file://./relative — npm-style file URL with relative path
|
|
83
|
+
* - file:///absolute — file URL with absolute path
|
|
84
|
+
* - link:./relative/path (symlink semantics for directories only)
|
|
85
|
+
*/
|
|
86
|
+
export function parseLocalSpec(spec) {
|
|
87
|
+
const match = /^(file|link):(.*)$/.exec(spec);
|
|
88
|
+
if (!match) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const protocol = match[1];
|
|
92
|
+
let rawPath = match[2];
|
|
93
|
+
if (rawPath.startsWith('//')) {
|
|
94
|
+
try {
|
|
95
|
+
rawPath = fileURLToPath(`file:${rawPath}`);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { protocol, rawPath };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { protocol, rawPath };
|
|
102
|
+
}
|
|
52
103
|
/**
|
|
53
104
|
* Finds and parses all package.json files in a directory.
|
|
54
105
|
* This is the single entry point for discovering package.json files and parsing their contents.
|
|
@@ -82,56 +133,58 @@ function extractWorkspaceGlobs(packageJson) {
|
|
|
82
133
|
if (!workspaces) {
|
|
83
134
|
return [];
|
|
84
135
|
}
|
|
85
|
-
// Handle array format: "workspaces": ["packages/*"]
|
|
86
136
|
if (Array.isArray(workspaces)) {
|
|
87
137
|
return workspaces;
|
|
88
138
|
}
|
|
89
|
-
// Handle object format: "workspaces": {"packages": ["packages/*"]}
|
|
90
139
|
if (workspaces.packages && Array.isArray(workspaces.packages)) {
|
|
91
140
|
return workspaces.packages;
|
|
92
141
|
}
|
|
93
142
|
return [];
|
|
94
143
|
}
|
|
95
144
|
/**
|
|
96
|
-
* Resolves workspace glob patterns to actual directories
|
|
145
|
+
* Resolves workspace glob patterns to actual directories.
|
|
146
|
+
*
|
|
147
|
+
* Uses @npmcli/map-workspaces (npm's reference impl) so that negation patterns
|
|
148
|
+
* like `["packages/*", "!packages/excluded"]` are honored — a hand-rolled
|
|
149
|
+
* per-pattern glob() silently accepted negations without actually excluding.
|
|
97
150
|
*/
|
|
98
151
|
export async function resolveWorkspaceDirectories(baseDir, workspaceGlobs) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
152
|
+
if (workspaceGlobs.length === 0) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const expandedGlobs = workspaceGlobs.map(expandTildePath);
|
|
156
|
+
let workspaceMap;
|
|
157
|
+
try {
|
|
158
|
+
workspaceMap = await mapWorkspaces({
|
|
159
|
+
pkg: { workspaces: expandedGlobs },
|
|
160
|
+
cwd: baseDir,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
logger.debug(`Failed to resolve workspace patterns in ${baseDir} via map-workspaces: ${e}`);
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const resolved = new Set();
|
|
168
|
+
for (const dir of workspaceMap.values()) {
|
|
103
169
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
await fsPromises.access(packageJsonPath, fs.constants.F_OK);
|
|
117
|
-
resolvedDirs.add(match);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
// Directory exists but doesn't contain package.json - skip silently
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
// Inaccessible directories may indicate permission issues or broken symlinks
|
|
126
|
-
logger.warn(`Cannot access directory ${match}: ${e}`);
|
|
127
|
-
}
|
|
170
|
+
const stats = await fsPromises.stat(dir);
|
|
171
|
+
if (!stats.isDirectory()) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const packageJsonPath = path.join(dir, 'package.json');
|
|
175
|
+
try {
|
|
176
|
+
await fsPromises.access(packageJsonPath, fs.constants.F_OK);
|
|
177
|
+
resolved.add(dir);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Directory exists but doesn't contain package.json — skip silently.
|
|
128
181
|
}
|
|
129
182
|
}
|
|
130
183
|
catch (e) {
|
|
131
|
-
logger.
|
|
184
|
+
logger.warn(logMessages.workspaces.cannotAccessDirectory(dir, e));
|
|
132
185
|
}
|
|
133
186
|
}
|
|
134
|
-
return Array.from(
|
|
187
|
+
return Array.from(resolved);
|
|
135
188
|
}
|
|
136
189
|
/**
|
|
137
190
|
* Collects all workspace directories that need to be uploaded.
|
|
@@ -142,9 +195,8 @@ export async function collectWorkspaceDirectories(parsedPackageJsons) {
|
|
|
142
195
|
const workspaceMappings = [];
|
|
143
196
|
// Track (sourcePackageJsonPath, workspaceDir) pairs to avoid duplicates within
|
|
144
197
|
// the same source, while still allowing the same workspace dir to appear in
|
|
145
|
-
// mappings from multiple different package.json files.
|
|
146
|
-
//
|
|
147
|
-
// regardless of whether another package.json already references the same dir.
|
|
198
|
+
// mappings from multiple different package.json files. Both keys use realpath
|
|
199
|
+
// form so case-only differences on case-insensitive filesystems collapse.
|
|
148
200
|
const visited = new Set();
|
|
149
201
|
for (const parsed of parsedPackageJsons) {
|
|
150
202
|
if (!parsed.content) {
|
|
@@ -156,8 +208,14 @@ export async function collectWorkspaceDirectories(parsedPackageJsons) {
|
|
|
156
208
|
}
|
|
157
209
|
logger.debug(`Found workspaces in ${parsed.path}: ${workspaceGlobs.join(', ')}`);
|
|
158
210
|
const resolved = await resolveWorkspaceDirectories(parsed.dir, workspaceGlobs);
|
|
211
|
+
let sourceRealPath;
|
|
212
|
+
try {
|
|
213
|
+
sourceRealPath = await fsPromises.realpath(parsed.path);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
sourceRealPath = parsed.path;
|
|
217
|
+
}
|
|
159
218
|
for (const dir of resolved) {
|
|
160
|
-
// Resolve symlinks to real path
|
|
161
219
|
let realDir;
|
|
162
220
|
try {
|
|
163
221
|
realDir = await fsPromises.realpath(dir);
|
|
@@ -165,19 +223,12 @@ export async function collectWorkspaceDirectories(parsedPackageJsons) {
|
|
|
165
223
|
catch (e) {
|
|
166
224
|
throw new WorkspaceResolutionError(dir, parsed.path, e instanceof Error ? e : undefined);
|
|
167
225
|
}
|
|
168
|
-
|
|
169
|
-
// may legitimately be referenced by multiple package.json files and each
|
|
170
|
-
// needs its own mapping so its workspaces field is rewritten correctly.
|
|
171
|
-
const pairKey = `${parsed.path}::${realDir}`;
|
|
226
|
+
const pairKey = `${sourceRealPath}::${realDir}`;
|
|
172
227
|
if (visited.has(pairKey)) {
|
|
173
228
|
logger.debug(`Skipping duplicate workspace mapping: ${realDir} from ${parsed.path}`);
|
|
174
229
|
continue;
|
|
175
230
|
}
|
|
176
231
|
visited.add(pairKey);
|
|
177
|
-
// Include all workspaces - both internal (inside srcDir) and external.
|
|
178
|
-
// The CLI determines the archive path:
|
|
179
|
-
// - Internal: preserved as-is (already included via srcDir walk)
|
|
180
|
-
// - External: _workspaces/<name>-<hash>
|
|
181
232
|
workspaceMappings.push({
|
|
182
233
|
workspaceDir: realDir,
|
|
183
234
|
sourcePackageJsonPath: parsed.path,
|
|
@@ -188,7 +239,7 @@ export async function collectWorkspaceDirectories(parsedPackageJsons) {
|
|
|
188
239
|
return workspaceMappings;
|
|
189
240
|
}
|
|
190
241
|
/**
|
|
191
|
-
* Extracts file: dependencies from parsed package.json content.
|
|
242
|
+
* Extracts file: and link: dependencies from parsed package.json content.
|
|
192
243
|
* Only scans production dependencies since devDependencies and optionalDependencies
|
|
193
244
|
* are not needed for the build pipeline.
|
|
194
245
|
*/
|
|
@@ -199,18 +250,24 @@ function extractFileDependencies(packageJson) {
|
|
|
199
250
|
return fileDeps;
|
|
200
251
|
}
|
|
201
252
|
for (const [packageName, version] of Object.entries(deps)) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
253
|
+
if (typeof version !== 'string') {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const parsed = parseLocalSpec(version);
|
|
257
|
+
if (parsed) {
|
|
258
|
+
fileDeps.push({
|
|
259
|
+
packageName,
|
|
260
|
+
protocol: parsed.protocol,
|
|
261
|
+
rawPath: parsed.rawPath,
|
|
262
|
+
});
|
|
207
263
|
}
|
|
208
264
|
}
|
|
209
265
|
return fileDeps;
|
|
210
266
|
}
|
|
211
267
|
/**
|
|
212
|
-
* Collects all file
|
|
213
|
-
* Returns mappings that track the package name, resolved path,
|
|
268
|
+
* Collects all `file:` and `link:` dependencies that need to be uploaded.
|
|
269
|
+
* Returns mappings that track the package name, resolved path, source
|
|
270
|
+
* package.json, kind (directory vs tarball) and protocol (file vs link).
|
|
214
271
|
*/
|
|
215
272
|
export async function collectFileDependencies(parsedPackageJsons) {
|
|
216
273
|
const fileDependencyMappings = [];
|
|
@@ -223,11 +280,14 @@ export async function collectFileDependencies(parsedPackageJsons) {
|
|
|
223
280
|
if (fileDeps.length === 0) {
|
|
224
281
|
continue;
|
|
225
282
|
}
|
|
226
|
-
logger.debug(`Found file: dependencies in ${parsed.path}: ${fileDeps.map(d => d.packageName).join(', ')}`);
|
|
227
|
-
for (const { packageName,
|
|
228
|
-
const expandedPath = expandTildePath(
|
|
283
|
+
logger.debug(`Found file:/link: dependencies in ${parsed.path}: ${fileDeps.map(d => d.packageName).join(', ')}`);
|
|
284
|
+
for (const { packageName, protocol, rawPath } of fileDeps) {
|
|
285
|
+
const expandedPath = expandTildePath(rawPath);
|
|
229
286
|
const absolutePath = path.resolve(parsed.dir, expandedPath);
|
|
230
|
-
|
|
287
|
+
const looksLikeTarball = hasTarballExtension(absolutePath);
|
|
288
|
+
if (looksLikeTarball && protocol === 'link') {
|
|
289
|
+
throw new FileDependencyResolutionError(packageName, absolutePath, parsed.path, new Error('`link:` protocol is for directories (symlink semantics); use `file:` for tarballs'));
|
|
290
|
+
}
|
|
231
291
|
let realPath;
|
|
232
292
|
try {
|
|
233
293
|
realPath = await fsPromises.realpath(absolutePath);
|
|
@@ -235,38 +295,93 @@ export async function collectFileDependencies(parsedPackageJsons) {
|
|
|
235
295
|
catch (e) {
|
|
236
296
|
throw new FileDependencyResolutionError(packageName, absolutePath, parsed.path, e instanceof Error ? e : undefined);
|
|
237
297
|
}
|
|
238
|
-
|
|
239
|
-
const stats = await fsPromises.stat(realPath);
|
|
240
|
-
if (!stats.isDirectory()) {
|
|
241
|
-
throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Path is not a directory'));
|
|
242
|
-
}
|
|
243
|
-
const depPackageJsonPath = path.join(realPath, 'package.json');
|
|
298
|
+
let stats;
|
|
244
299
|
try {
|
|
245
|
-
await fsPromises.
|
|
300
|
+
stats = await fsPromises.stat(realPath);
|
|
246
301
|
}
|
|
247
|
-
catch {
|
|
248
|
-
throw new FileDependencyResolutionError(packageName, realPath, parsed.path,
|
|
302
|
+
catch (e) {
|
|
303
|
+
throw new FileDependencyResolutionError(packageName, realPath, parsed.path, e instanceof Error ? e : undefined);
|
|
304
|
+
}
|
|
305
|
+
let kind;
|
|
306
|
+
if (looksLikeTarball) {
|
|
307
|
+
if (!stats.isFile()) {
|
|
308
|
+
throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Tarball path is not a regular file'));
|
|
309
|
+
}
|
|
310
|
+
kind = 'tarball';
|
|
311
|
+
}
|
|
312
|
+
else if (stats.isDirectory()) {
|
|
313
|
+
const depPackageJsonPath = path.join(realPath, 'package.json');
|
|
314
|
+
try {
|
|
315
|
+
await fsPromises.access(depPackageJsonPath, fs.constants.F_OK);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Directory does not contain package.json'));
|
|
319
|
+
}
|
|
320
|
+
kind = 'directory';
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Path is not a directory'));
|
|
249
324
|
}
|
|
250
|
-
// Skip if already visited (same path referenced multiple times)
|
|
251
325
|
if (visited.has(realPath)) {
|
|
252
326
|
logger.debug(`Skipping already visited file: dependency: ${realPath}`);
|
|
253
327
|
continue;
|
|
254
328
|
}
|
|
255
329
|
visited.add(realPath);
|
|
256
|
-
// Include all file: dependencies - both internal (inside srcDir) and external
|
|
257
|
-
// The CLI determines the archive path:
|
|
258
|
-
// - Internal: _workspaces/<relative-path>
|
|
259
|
-
// - External: _workspaces/external/<name>-<hash>
|
|
260
330
|
fileDependencyMappings.push({
|
|
261
331
|
packageName,
|
|
262
332
|
localPath: realPath,
|
|
263
333
|
sourcePackageJsonPath: parsed.path,
|
|
334
|
+
kind,
|
|
335
|
+
protocol,
|
|
264
336
|
});
|
|
265
|
-
logger.debug(`Resolved
|
|
337
|
+
logger.debug(`Resolved ${protocol}: dependency ${packageName} (${kind}): ${realPath}`);
|
|
266
338
|
}
|
|
267
339
|
}
|
|
268
340
|
return fileDependencyMappings;
|
|
269
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Validates that every `workspace:` protocol dependency in the parsed package
|
|
344
|
+
* trees matches a known workspace member. Throws OrphanWorkspaceProtocolError
|
|
345
|
+
* for the first orphan found.
|
|
346
|
+
*
|
|
347
|
+
* npm itself does not implement the `workspace:` protocol; only pnpm and yarn
|
|
348
|
+
* berry do. If a project uses `workspace:*` without a matching member declared
|
|
349
|
+
* under `workspaces`, the install fails server-side with a confusing message —
|
|
350
|
+
* fail fast with a clearer one.
|
|
351
|
+
*/
|
|
352
|
+
export function validateWorkspaceProtocolDependencies(parsedPackageJsons, workspaceMappings) {
|
|
353
|
+
const workspacePackageNames = new Set();
|
|
354
|
+
const workspaceDirs = new Set(workspaceMappings.map(m => m.workspaceDir));
|
|
355
|
+
for (const parsed of parsedPackageJsons) {
|
|
356
|
+
if (!parsed.content?.name) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
let realDir;
|
|
360
|
+
try {
|
|
361
|
+
realDir = fs.realpathSync(parsed.dir);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (workspaceDirs.has(realDir)) {
|
|
367
|
+
workspacePackageNames.add(parsed.content.name);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
for (const parsed of parsedPackageJsons) {
|
|
371
|
+
const deps = parsed.content?.dependencies;
|
|
372
|
+
if (!deps) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
for (const [packageName, version] of Object.entries(deps)) {
|
|
376
|
+
if (typeof version !== 'string' || !version.startsWith('workspace:')) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (!workspacePackageNames.has(packageName)) {
|
|
380
|
+
throw new OrphanWorkspaceProtocolError(packageName, parsed.path);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
270
385
|
/**
|
|
271
386
|
* Returns the set of files that npm would include when publishing a package.
|
|
272
387
|
* Uses npm-packlist which respects the "files" field in package.json,
|
|
@@ -277,8 +392,6 @@ export async function collectFileDependencies(parsedPackageJsons) {
|
|
|
277
392
|
export async function getPackableFiles(dir) {
|
|
278
393
|
try {
|
|
279
394
|
const tree = createMinimalTree(dir);
|
|
280
|
-
// Cast to Parameters<typeof packlist>[0] since npm-packlist only uses
|
|
281
|
-
// a subset of the Arborist Node properties at runtime
|
|
282
395
|
const files = await packlist(tree);
|
|
283
396
|
return new Set(files);
|
|
284
397
|
}
|