@hubspot/project-parsing-lib 0.2.0-experimental.0 → 0.2.0-experimental.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/package.json +4 -11
- package/src/lib/constants.d.ts +2 -1
- package/src/lib/constants.js +12 -8
- package/src/lib/project.js +9 -2
- package/src/lib/transform.js +13 -3
- package/src/exports/workspaces.d.ts +0 -2
- package/src/exports/workspaces.js +0 -1
- package/src/lib/minimalArboristTree.d.ts +0 -118
- package/src/lib/minimalArboristTree.js +0 -32
- package/src/lib/workspaces.d.ts +0 -68
- package/src/lib/workspaces.js +0 -282
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubspot/project-parsing-lib",
|
|
3
|
-
"version": "0.2.0-experimental.
|
|
3
|
+
"version": "0.2.0-experimental.2",
|
|
4
4
|
"description": "Parsing library for converting projects directory structures to their intermediate representation",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -40,20 +40,15 @@
|
|
|
40
40
|
"./uid": {
|
|
41
41
|
"import": "./src/exports/uid.js",
|
|
42
42
|
"types": "./src/exports/uid.d.ts"
|
|
43
|
-
},
|
|
44
|
-
"./workspaces": {
|
|
45
|
-
"import": "./src/exports/workspaces.js",
|
|
46
|
-
"types": "./src/exports/workspaces.d.ts"
|
|
47
43
|
}
|
|
48
44
|
},
|
|
49
45
|
"devDependencies": {
|
|
50
46
|
"@hubspot/npm-scripts": "0.0.6",
|
|
51
47
|
"@inquirer/prompts": "^7.1.0",
|
|
52
48
|
"@types/node": "^24.9.0",
|
|
53
|
-
"@types/npm-packlist": "7.0.3",
|
|
54
49
|
"@types/semver": "^7.5.8",
|
|
55
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
56
|
-
"@typescript-eslint/parser": "8.
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "8.46.3",
|
|
51
|
+
"@typescript-eslint/parser": "8.46.3",
|
|
57
52
|
"eslint": "^9.38.0",
|
|
58
53
|
"eslint-plugin-import": "^2.31.0",
|
|
59
54
|
"husky": "^9.1.7",
|
|
@@ -67,9 +62,7 @@
|
|
|
67
62
|
"dependencies": {
|
|
68
63
|
"@hubspot/local-dev-lib": "4.0.4",
|
|
69
64
|
"ajv": "8.18.0",
|
|
70
|
-
"ajv-formats": "3.0.1"
|
|
71
|
-
"glob": "13.0.0",
|
|
72
|
-
"npm-packlist": "8.0.1"
|
|
65
|
+
"ajv-formats": "3.0.1"
|
|
73
66
|
},
|
|
74
67
|
"scripts": {
|
|
75
68
|
"build": "tsx ./scripts/build.ts",
|
package/src/lib/constants.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export declare const THEME_KEY = "theme";
|
|
|
4
4
|
export declare const CMS_ASSETS_KEY = "cms-assets";
|
|
5
5
|
export declare const APP_EVENTS_KEY = "app-event";
|
|
6
6
|
export declare const APP_FUNCTIONS_KEY = "app-function";
|
|
7
|
-
export declare const
|
|
7
|
+
export declare const PAGE_KEY = "page";
|
|
8
|
+
export declare const PAGES_KEY = "pages";
|
|
8
9
|
export declare const APP_OBJECT_KEY = "app-object";
|
|
9
10
|
export declare const APP_OBJECT_ASSOCIATION_KEY = "app-object-association";
|
|
10
11
|
export declare const CALLING_KEY = "calling";
|
package/src/lib/constants.js
CHANGED
|
@@ -7,7 +7,8 @@ export const CMS_ASSETS_KEY = 'cms-assets';
|
|
|
7
7
|
// Sub-Component types
|
|
8
8
|
export const APP_EVENTS_KEY = 'app-event';
|
|
9
9
|
export const APP_FUNCTIONS_KEY = 'app-function';
|
|
10
|
-
export const
|
|
10
|
+
export const PAGE_KEY = 'page';
|
|
11
|
+
export const PAGES_KEY = 'pages';
|
|
11
12
|
export const APP_OBJECT_KEY = 'app-object';
|
|
12
13
|
export const APP_OBJECT_ASSOCIATION_KEY = 'app-object-association';
|
|
13
14
|
export const CALLING_KEY = 'calling';
|
|
@@ -35,6 +36,13 @@ const SUB_COMPONENT_FIELDS = {
|
|
|
35
36
|
userFriendlyType: 'feature',
|
|
36
37
|
userFriendlyTypePlural: 'features',
|
|
37
38
|
};
|
|
39
|
+
const PAGE_COMPONENT = {
|
|
40
|
+
dir: 'pages',
|
|
41
|
+
parentComponent: APP_KEY,
|
|
42
|
+
...SUB_COMPONENT_FIELDS,
|
|
43
|
+
userFriendlyName: 'Page',
|
|
44
|
+
singularComponent: true,
|
|
45
|
+
};
|
|
38
46
|
export const Components = {
|
|
39
47
|
[APP_KEY]: {
|
|
40
48
|
dir: APP_KEY,
|
|
@@ -54,13 +62,8 @@ export const Components = {
|
|
|
54
62
|
...SUB_COMPONENT_FIELDS,
|
|
55
63
|
userFriendlyName: 'App Object Association',
|
|
56
64
|
},
|
|
57
|
-
[
|
|
58
|
-
|
|
59
|
-
parentComponent: APP_KEY,
|
|
60
|
-
...SUB_COMPONENT_FIELDS,
|
|
61
|
-
userFriendlyName: 'Page',
|
|
62
|
-
singularComponent: true,
|
|
63
|
-
},
|
|
65
|
+
[PAGE_KEY]: PAGE_COMPONENT,
|
|
66
|
+
[PAGES_KEY]: PAGE_COMPONENT,
|
|
64
67
|
[THEME_KEY]: {
|
|
65
68
|
dir: THEME_KEY,
|
|
66
69
|
userFriendlyName: 'Theme',
|
|
@@ -168,6 +171,7 @@ export const USER_FACING_TO_INTERNAL_TYPE = {
|
|
|
168
171
|
[SCIM_KEY]: 'SCIM_INTEGRATION',
|
|
169
172
|
[TELEMETRY_KEY]: 'TELEMETRY_CONFIG',
|
|
170
173
|
[CMS_ASSETS_KEY]: 'REACT_THEME',
|
|
174
|
+
[PAGES_KEY]: 'PAGE',
|
|
171
175
|
};
|
|
172
176
|
export const INTERNAL_TYPE_TO_USER_FACING = Object.fromEntries(Object.entries(USER_FACING_TO_INTERNAL_TYPE).map(([key, value]) => [
|
|
173
177
|
value,
|
package/src/lib/project.js
CHANGED
|
@@ -9,18 +9,25 @@ export async function getProjectMetadata(projectSrcDir) {
|
|
|
9
9
|
if (fs.existsSync(projectSrcDir)) {
|
|
10
10
|
metafiles = await locateHsMetaFiles(projectSrcDir, { silent: true });
|
|
11
11
|
}
|
|
12
|
+
const seenComponentPaths = new Map();
|
|
12
13
|
Object.keys(Components).forEach(componentType => {
|
|
13
14
|
const { parentComponent, singularComponent, dir } = Components[componentType];
|
|
14
15
|
const componentPath = path.join(projectSrcDir, parentComponent ? path.join(parentComponent, dir) : dir);
|
|
16
|
+
if (seenComponentPaths.has(componentPath)) {
|
|
17
|
+
components[componentType] = seenComponentPaths.get(componentPath);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
15
20
|
const metaFilesByType = metafiles
|
|
16
21
|
.filter(metafile => path.parse(metafile.file).dir === componentPath)
|
|
17
22
|
.map(metaFile => metaFile.file);
|
|
18
|
-
|
|
19
|
-
components[componentType] = {
|
|
23
|
+
const metadata = {
|
|
20
24
|
hsMetaFiles: metaFilesByType,
|
|
21
25
|
count: metaFilesByType.length,
|
|
22
26
|
maxCount: singularComponent ? 1 : Infinity,
|
|
23
27
|
};
|
|
28
|
+
hsMetaFiles.push(...metaFilesByType);
|
|
29
|
+
seenComponentPaths.set(componentPath, metadata);
|
|
30
|
+
components[componentType] = metadata;
|
|
24
31
|
});
|
|
25
32
|
return {
|
|
26
33
|
hsMetaFiles,
|
package/src/lib/transform.js
CHANGED
|
@@ -2,11 +2,11 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
4
4
|
import { applyHsProfileVariables, getHsProfileVariables } from './profiles.js';
|
|
5
|
-
import { APP_KEY, APP_OBJECT_KEY, Components, PROJECT_STRUCTURE, APP_FUNCTIONS_KEY, APP_FUNCTIONS_PACKAGE_KEY, USER_FACING_TO_INTERNAL_TYPE, INTERNAL_TYPE_TO_USER_FACING, PACKAGE_JSON, PACKAGE_LOCK_JSON, PROFILE_VARIABLE_TYPES, } from './constants.js';
|
|
5
|
+
import { APP_KEY, APP_OBJECT_KEY, CARDS_KEY, Components, PROJECT_STRUCTURE, APP_FUNCTIONS_KEY, APP_FUNCTIONS_PACKAGE_KEY, USER_FACING_TO_INTERNAL_TYPE, INTERNAL_TYPE_TO_USER_FACING, PACKAGE_JSON, PACKAGE_LOCK_JSON, PROFILE_VARIABLE_TYPES, } from './constants.js';
|
|
6
6
|
import { errorMessages } from '../lang/copy.js';
|
|
7
7
|
import { getJavaNumberType } from './utils.js';
|
|
8
8
|
import { convertPathToPosixPath } from './files.js';
|
|
9
|
-
function calculateComponentDeps(fileValidationResult, parentComponents, appObjects, appFunctionsPackageUid) {
|
|
9
|
+
function calculateComponentDeps(fileValidationResult, parentComponents, appObjects, appFunctions, appFunctionsPackageUid) {
|
|
10
10
|
let dependencies = {};
|
|
11
11
|
// If there are dependencies in the config file, pass them through
|
|
12
12
|
if (!fileValidationResult.content?.dependencies) {
|
|
@@ -32,6 +32,14 @@ function calculateComponentDeps(fileValidationResult, parentComponents, appObjec
|
|
|
32
32
|
if (type === APP_FUNCTIONS_KEY && appFunctionsPackageUid) {
|
|
33
33
|
dependencies.serverlessPackage = appFunctionsPackageUid;
|
|
34
34
|
}
|
|
35
|
+
if (type === CARDS_KEY) {
|
|
36
|
+
if (appFunctionsPackageUid) {
|
|
37
|
+
dependencies.serverlessPackage = appFunctionsPackageUid;
|
|
38
|
+
}
|
|
39
|
+
if (appFunctions.length > 0) {
|
|
40
|
+
dependencies.allAppFunctions = appFunctions;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
return { dependencies };
|
|
37
45
|
}
|
|
@@ -54,6 +62,7 @@ export function transform(fileParseResults, translationContext, hsProfileContent
|
|
|
54
62
|
const parentTypes = Object.keys(PROJECT_STRUCTURE);
|
|
55
63
|
const parentComponents = {};
|
|
56
64
|
const allAppObjects = [];
|
|
65
|
+
const allAppFunctions = [];
|
|
57
66
|
let appUid = '';
|
|
58
67
|
let appFunctionsDirectory;
|
|
59
68
|
// Apply the profile variable overrides to the config
|
|
@@ -73,6 +82,7 @@ export function transform(fileParseResults, translationContext, hsProfileContent
|
|
|
73
82
|
}
|
|
74
83
|
if (file.content?.type === APP_FUNCTIONS_KEY) {
|
|
75
84
|
appFunctionsDirectory = path.dirname(file.file);
|
|
85
|
+
allAppFunctions.push(file.content.uid);
|
|
76
86
|
}
|
|
77
87
|
});
|
|
78
88
|
const autoGeneratedComponents = [];
|
|
@@ -96,7 +106,7 @@ export function transform(fileParseResults, translationContext, hsProfileContent
|
|
|
96
106
|
};
|
|
97
107
|
}
|
|
98
108
|
const { config, uid, type } = currentFile.content;
|
|
99
|
-
const { dependencies, errors } = calculateComponentDeps(currentFile, parentComponents, allAppObjects, serverlessPackageUid);
|
|
109
|
+
const { dependencies, errors } = calculateComponentDeps(currentFile, parentComponents, allAppObjects, allAppFunctions, serverlessPackageUid);
|
|
100
110
|
if (errors) {
|
|
101
111
|
currentFile.errors?.push(...errors);
|
|
102
112
|
}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
export { findAndParsePackageJsonFiles, resolveWorkspaceDirectories, collectWorkspaceDirectories, collectFileDependencies, getPackableFiles, WorkspaceResolutionError, FileDependencyResolutionError, } from '../lib/workspaces.js';
|
|
2
|
-
export type { ParsedPackageJson, WorkspaceMapping, FileDependencyMapping, } from '../lib/workspaces.js';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { findAndParsePackageJsonFiles, resolveWorkspaceDirectories, collectWorkspaceDirectories, collectFileDependencies, getPackableFiles, WorkspaceResolutionError, FileDependencyResolutionError, } from '../lib/workspaces.js';
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MINIMAL ARBORIST TREE
|
|
3
|
-
*
|
|
4
|
-
* This module provides a lightweight alternative to @npmcli/arborist for use
|
|
5
|
-
* with npm-packlist. Instead of importing the full arborist package (~3.4MB
|
|
6
|
-
* with 35+ transitive dependencies), we create a minimal tree object that
|
|
7
|
-
* satisfies npm-packlist's runtime requirements.
|
|
8
|
-
*
|
|
9
|
-
* WHY THIS EXISTS:
|
|
10
|
-
* - @npmcli/arborist adds ~3.4MB to the bundle
|
|
11
|
-
* - npm-packlist only uses a small subset of the Arborist Node interface
|
|
12
|
-
* - For our use case (determining packable files in workspace directories),
|
|
13
|
-
* we don't need the full dependency resolution capabilities of arborist
|
|
14
|
-
*
|
|
15
|
-
* CONTEXT: HUBSPOT DEVELOPER PROJECTS UPLOAD FLOW
|
|
16
|
-
*
|
|
17
|
-
* This library is used to collect files from npm workspace packages for upload
|
|
18
|
-
* to HubSpot's build system. Important constraints:
|
|
19
|
-
*
|
|
20
|
-
* - node_modules/ directories are EXCLUDED from uploads
|
|
21
|
-
* - The build system runs `npm install` to fetch dependencies
|
|
22
|
-
* - Source files, package.json, and config files are included
|
|
23
|
-
*
|
|
24
|
-
* This means bundleDependencies (which includes files from node_modules/) would
|
|
25
|
-
* not work even if we used full arborist, because node_modules/ is excluded
|
|
26
|
-
* from the upload archive. Developers with private registry packages should use
|
|
27
|
-
* file: dependencies instead, which ARE supported.
|
|
28
|
-
*
|
|
29
|
-
* HOW NPM-PACKLIST USES THE TREE (as of npm-packlist@8.0.1):
|
|
30
|
-
*
|
|
31
|
-
* tree.path - Directory path (REQUIRED)
|
|
32
|
-
* tree.package - Parsed package.json content (REQUIRED)
|
|
33
|
-
* .files - Files array for inclusion
|
|
34
|
-
* .main - Main entry point (always included)
|
|
35
|
-
* .bin - Binary files (always included)
|
|
36
|
-
* .browser - Browser field (always included)
|
|
37
|
-
* .bundleDependencies - Only used when isProjectRoot=true
|
|
38
|
-
* .dependencies - Only used when isProjectRoot=true
|
|
39
|
-
* .optionalDependencies - Only used when isProjectRoot=true
|
|
40
|
-
* tree.workspaces - Map of workspace paths (OPTIONAL, guarded by if-check)
|
|
41
|
-
* tree.isProjectRoot - Boolean for bundled deps handling (OPTIONAL)
|
|
42
|
-
* tree.edgesOut - Dependency edges (only accessed when isProjectRoot=true)
|
|
43
|
-
*
|
|
44
|
-
* KNOWN LIMITATIONS:
|
|
45
|
-
*
|
|
46
|
-
* 1. BUNDLED DEPENDENCIES NOT SUPPORTED
|
|
47
|
-
* We set isProjectRoot=false, which skips bundled dependency handling.
|
|
48
|
-
* If a workspace package has "bundleDependencies" in package.json, those
|
|
49
|
-
* files will NOT be included in the packlist result.
|
|
50
|
-
*
|
|
51
|
-
* NOTE: This would not work anyway because bundleDependencies includes files
|
|
52
|
-
* from node_modules/, and node_modules/ is excluded from project uploads.
|
|
53
|
-
* Even with full arborist, bundled dependencies would be filtered out.
|
|
54
|
-
* Developers needing to include private/local packages should use file:
|
|
55
|
-
* dependencies instead (e.g., "my-pkg": "file:../my-pkg").
|
|
56
|
-
*
|
|
57
|
-
* 2. WORKSPACE-AWARE FILTERING DISABLED
|
|
58
|
-
* We set workspaces=null, so npm-packlist won't exclude workspace
|
|
59
|
-
* directories. This is fine for our use case since we process each
|
|
60
|
-
* workspace independently.
|
|
61
|
-
*
|
|
62
|
-
* 3. VERSION COUPLING
|
|
63
|
-
* This minimal interface is based on npm-packlist@8.0.1. Future versions
|
|
64
|
-
* may access additional properties, causing runtime errors. If packlist
|
|
65
|
-
* behavior changes unexpectedly, check for new property accesses:
|
|
66
|
-
*
|
|
67
|
-
* grep -E "this\.tree\." node_modules/npm-packlist/lib/index.js
|
|
68
|
-
*
|
|
69
|
-
* DEBUGGING:
|
|
70
|
-
*
|
|
71
|
-
* If getPackableFiles() returns unexpected results:
|
|
72
|
-
*
|
|
73
|
-
* 1. Check if npm-packlist version changed (package.json)
|
|
74
|
-
* 2. Compare tree property accesses (grep command above)
|
|
75
|
-
* 3. Test with full arborist to isolate the issue:
|
|
76
|
-
*
|
|
77
|
-
* import Arborist from '@npmcli/arborist';
|
|
78
|
-
* const arb = new Arborist({ path: dir });
|
|
79
|
-
* const tree = await arb.loadActual();
|
|
80
|
-
* const files = await packlist(tree);
|
|
81
|
-
*
|
|
82
|
-
* 4. If full arborist works but minimal tree doesn't, add the missing
|
|
83
|
-
* properties to MinimalArboristTree interface and createMinimalTree()
|
|
84
|
-
*/
|
|
85
|
-
/**
|
|
86
|
-
* Minimal subset of package.json fields used by npm-packlist
|
|
87
|
-
*/
|
|
88
|
-
interface PackageJsonForPacklist {
|
|
89
|
-
name?: string;
|
|
90
|
-
files?: string[];
|
|
91
|
-
main?: string;
|
|
92
|
-
bin?: string | Record<string, string>;
|
|
93
|
-
browser?: string | Record<string, string>;
|
|
94
|
-
bundleDependencies?: string[];
|
|
95
|
-
dependencies?: Record<string, string>;
|
|
96
|
-
optionalDependencies?: Record<string, string>;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Minimal tree interface that satisfies npm-packlist's runtime requirements.
|
|
100
|
-
* This is NOT the full Arborist Node interface - only the properties that
|
|
101
|
-
* npm-packlist actually accesses at runtime.
|
|
102
|
-
*/
|
|
103
|
-
export interface MinimalArboristTree {
|
|
104
|
-
path: string;
|
|
105
|
-
package: PackageJsonForPacklist;
|
|
106
|
-
workspaces: null;
|
|
107
|
-
isProjectRoot: false;
|
|
108
|
-
edgesOut: Map<string, unknown>;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Creates a minimal tree object that satisfies npm-packlist's requirements
|
|
112
|
-
* without importing the heavy @npmcli/arborist package.
|
|
113
|
-
*
|
|
114
|
-
* @param dir - Directory containing the package
|
|
115
|
-
* @returns Minimal tree object compatible with npm-packlist
|
|
116
|
-
*/
|
|
117
|
-
export declare function createMinimalTree(dir: string): MinimalArboristTree;
|
|
118
|
-
export {};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
/**
|
|
4
|
-
* Creates a minimal tree object that satisfies npm-packlist's requirements
|
|
5
|
-
* without importing the heavy @npmcli/arborist package.
|
|
6
|
-
*
|
|
7
|
-
* @param dir - Directory containing the package
|
|
8
|
-
* @returns Minimal tree object compatible with npm-packlist
|
|
9
|
-
*/
|
|
10
|
-
export function createMinimalTree(dir) {
|
|
11
|
-
let pkgJson = {};
|
|
12
|
-
try {
|
|
13
|
-
const content = fs.readFileSync(path.join(dir, 'package.json'), 'utf8');
|
|
14
|
-
pkgJson = JSON.parse(content);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// Use empty package.json if not found or invalid
|
|
18
|
-
// npm-packlist handles this gracefully
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
path: dir,
|
|
22
|
-
package: pkgJson,
|
|
23
|
-
// Set to null to skip workspace-aware filtering
|
|
24
|
-
// (we process each workspace independently)
|
|
25
|
-
workspaces: null,
|
|
26
|
-
// Set to false to skip bundled dependency handling
|
|
27
|
-
// (not needed for workspace file collection)
|
|
28
|
-
isProjectRoot: false,
|
|
29
|
-
// Empty map - only accessed when isProjectRoot is true
|
|
30
|
-
edgesOut: new Map(),
|
|
31
|
-
};
|
|
32
|
-
}
|
package/src/lib/workspaces.d.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error thrown when a workspace directory cannot be resolved
|
|
3
|
-
*/
|
|
4
|
-
export declare class WorkspaceResolutionError extends Error {
|
|
5
|
-
workspaceDir: string;
|
|
6
|
-
sourcePackageJson: string;
|
|
7
|
-
constructor(workspaceDir: string, sourcePackageJson: string, cause?: Error);
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Error thrown when a file: dependency cannot be resolved
|
|
11
|
-
*/
|
|
12
|
-
export declare class FileDependencyResolutionError extends Error {
|
|
13
|
-
packageName: string;
|
|
14
|
-
dependencyPath: string;
|
|
15
|
-
sourcePackageJson: string;
|
|
16
|
-
constructor(packageName: string, dependencyPath: string, sourcePackageJson: string, cause?: Error);
|
|
17
|
-
}
|
|
18
|
-
interface PackageJson {
|
|
19
|
-
name?: string;
|
|
20
|
-
workspaces?: string[] | {
|
|
21
|
-
packages?: string[];
|
|
22
|
-
nohoist?: string[];
|
|
23
|
-
};
|
|
24
|
-
dependencies?: Record<string, string>;
|
|
25
|
-
}
|
|
26
|
-
export interface ParsedPackageJson {
|
|
27
|
-
path: string;
|
|
28
|
-
dir: string;
|
|
29
|
-
content: PackageJson | null;
|
|
30
|
-
}
|
|
31
|
-
export interface WorkspaceMapping {
|
|
32
|
-
workspaceDir: string;
|
|
33
|
-
sourcePackageJsonPath: string;
|
|
34
|
-
}
|
|
35
|
-
export interface FileDependencyMapping {
|
|
36
|
-
packageName: string;
|
|
37
|
-
localPath: string;
|
|
38
|
-
sourcePackageJsonPath: string;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Finds and parses all package.json files in a directory.
|
|
42
|
-
* This is the single entry point for discovering package.json files and parsing their contents.
|
|
43
|
-
*/
|
|
44
|
-
export declare function findAndParsePackageJsonFiles(srcDir: string): Promise<ParsedPackageJson[]>;
|
|
45
|
-
/**
|
|
46
|
-
* Resolves workspace glob patterns to actual directories
|
|
47
|
-
*/
|
|
48
|
-
export declare function resolveWorkspaceDirectories(baseDir: string, workspaceGlobs: string[]): Promise<string[]>;
|
|
49
|
-
/**
|
|
50
|
-
* Collects all workspace directories that need to be uploaded.
|
|
51
|
-
* Handles edge cases like circular references, symlinks, and overlapping paths.
|
|
52
|
-
* Returns mappings that track which package.json defined each workspace.
|
|
53
|
-
*/
|
|
54
|
-
export declare function collectWorkspaceDirectories(parsedPackageJsons: ParsedPackageJson[]): Promise<WorkspaceMapping[]>;
|
|
55
|
-
/**
|
|
56
|
-
* Collects all file: dependencies that need to be uploaded.
|
|
57
|
-
* Returns mappings that track the package name, resolved path, and source package.json.
|
|
58
|
-
*/
|
|
59
|
-
export declare function collectFileDependencies(parsedPackageJsons: ParsedPackageJson[]): Promise<FileDependencyMapping[]>;
|
|
60
|
-
/**
|
|
61
|
-
* Returns the set of files that npm would include when publishing a package.
|
|
62
|
-
* Uses npm-packlist which respects the "files" field in package.json,
|
|
63
|
-
* .npmignore, and .gitignore rules.
|
|
64
|
-
*
|
|
65
|
-
* @throws Error if packlist fails to generate the file list
|
|
66
|
-
*/
|
|
67
|
-
export declare function getPackableFiles(dir: string): Promise<Set<string>>;
|
|
68
|
-
export {};
|
package/src/lib/workspaces.js
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import fsPromises from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import { glob } from 'glob';
|
|
6
|
-
import packlist from 'npm-packlist';
|
|
7
|
-
import { logger } from '@hubspot/local-dev-lib/logger';
|
|
8
|
-
import { walk } from '@hubspot/local-dev-lib/fs';
|
|
9
|
-
import { createMinimalTree } from './minimalArboristTree.js';
|
|
10
|
-
/**
|
|
11
|
-
* Error thrown when a workspace directory cannot be resolved
|
|
12
|
-
*/
|
|
13
|
-
export class WorkspaceResolutionError extends Error {
|
|
14
|
-
workspaceDir;
|
|
15
|
-
sourcePackageJson;
|
|
16
|
-
constructor(workspaceDir, sourcePackageJson, cause) {
|
|
17
|
-
super(`Failed to resolve workspace directory "${workspaceDir}" declared in ${sourcePackageJson}${cause ? `: ${cause.message}` : ''}`);
|
|
18
|
-
this.workspaceDir = workspaceDir;
|
|
19
|
-
this.sourcePackageJson = sourcePackageJson;
|
|
20
|
-
this.name = 'WorkspaceResolutionError';
|
|
21
|
-
if (cause) {
|
|
22
|
-
this.cause = cause;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Error thrown when a file: dependency cannot be resolved
|
|
28
|
-
*/
|
|
29
|
-
export class FileDependencyResolutionError extends Error {
|
|
30
|
-
packageName;
|
|
31
|
-
dependencyPath;
|
|
32
|
-
sourcePackageJson;
|
|
33
|
-
constructor(packageName, dependencyPath, sourcePackageJson, cause) {
|
|
34
|
-
super(`Failed to resolve file: dependency "${packageName}" at path "${dependencyPath}" declared in ${sourcePackageJson}${cause ? `: ${cause.message}` : ''}`);
|
|
35
|
-
this.packageName = packageName;
|
|
36
|
-
this.dependencyPath = dependencyPath;
|
|
37
|
-
this.sourcePackageJson = sourcePackageJson;
|
|
38
|
-
this.name = 'FileDependencyResolutionError';
|
|
39
|
-
if (cause) {
|
|
40
|
-
this.cause = cause;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Expands tilde (~) in paths to the user's home directory
|
|
46
|
-
*/
|
|
47
|
-
function expandTildePath(filePath) {
|
|
48
|
-
return filePath.startsWith('~')
|
|
49
|
-
? path.join(os.homedir(), filePath.slice(1))
|
|
50
|
-
: filePath;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Finds and parses all package.json files in a directory.
|
|
54
|
-
* This is the single entry point for discovering package.json files and parsing their contents.
|
|
55
|
-
*/
|
|
56
|
-
export async function findAndParsePackageJsonFiles(srcDir) {
|
|
57
|
-
const allFiles = await walk(srcDir, ['node_modules']);
|
|
58
|
-
const packageJsonPaths = allFiles.filter(file => path.basename(file) === 'package.json');
|
|
59
|
-
return packageJsonPaths.map(filePath => {
|
|
60
|
-
try {
|
|
61
|
-
return {
|
|
62
|
-
path: filePath,
|
|
63
|
-
dir: path.dirname(filePath),
|
|
64
|
-
content: JSON.parse(fs.readFileSync(filePath, 'utf8')),
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
catch (e) {
|
|
68
|
-
logger.debug(`Failed to parse package.json at ${filePath}: ${e}`);
|
|
69
|
-
return {
|
|
70
|
-
path: filePath,
|
|
71
|
-
dir: path.dirname(filePath),
|
|
72
|
-
content: null,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Extracts workspace patterns from parsed package.json content
|
|
79
|
-
*/
|
|
80
|
-
function extractWorkspaceGlobs(packageJson) {
|
|
81
|
-
const workspaces = packageJson.workspaces;
|
|
82
|
-
if (!workspaces) {
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
// Handle array format: "workspaces": ["packages/*"]
|
|
86
|
-
if (Array.isArray(workspaces)) {
|
|
87
|
-
return workspaces;
|
|
88
|
-
}
|
|
89
|
-
// Handle object format: "workspaces": {"packages": ["packages/*"]}
|
|
90
|
-
if (workspaces.packages && Array.isArray(workspaces.packages)) {
|
|
91
|
-
return workspaces.packages;
|
|
92
|
-
}
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Resolves workspace glob patterns to actual directories
|
|
97
|
-
*/
|
|
98
|
-
export async function resolveWorkspaceDirectories(baseDir, workspaceGlobs) {
|
|
99
|
-
const resolvedDirs = new Set();
|
|
100
|
-
for (const pattern of workspaceGlobs) {
|
|
101
|
-
const expandedPattern = expandTildePath(pattern);
|
|
102
|
-
const absolutePattern = path.resolve(baseDir, expandedPattern);
|
|
103
|
-
try {
|
|
104
|
-
// Use glob to find matching directories
|
|
105
|
-
const matches = await glob(absolutePattern, {
|
|
106
|
-
absolute: true,
|
|
107
|
-
withFileTypes: false,
|
|
108
|
-
});
|
|
109
|
-
// Filter to directories that contain package.json
|
|
110
|
-
for (const match of matches) {
|
|
111
|
-
try {
|
|
112
|
-
const stats = await fsPromises.stat(match);
|
|
113
|
-
if (stats.isDirectory()) {
|
|
114
|
-
const packageJsonPath = path.join(match, 'package.json');
|
|
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
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
logger.debug(`Failed to resolve workspace pattern "${pattern}": ${e}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return Array.from(resolvedDirs);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Collects all workspace directories that need to be uploaded.
|
|
138
|
-
* Handles edge cases like circular references, symlinks, and overlapping paths.
|
|
139
|
-
* Returns mappings that track which package.json defined each workspace.
|
|
140
|
-
*/
|
|
141
|
-
export async function collectWorkspaceDirectories(parsedPackageJsons) {
|
|
142
|
-
const workspaceMappings = [];
|
|
143
|
-
const visited = new Set();
|
|
144
|
-
for (const parsed of parsedPackageJsons) {
|
|
145
|
-
if (!parsed.content) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const workspaceGlobs = extractWorkspaceGlobs(parsed.content);
|
|
149
|
-
if (workspaceGlobs.length === 0) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
logger.debug(`Found workspaces in ${parsed.path}: ${workspaceGlobs.join(', ')}`);
|
|
153
|
-
const resolved = await resolveWorkspaceDirectories(parsed.dir, workspaceGlobs);
|
|
154
|
-
for (const dir of resolved) {
|
|
155
|
-
// Resolve symlinks to real path
|
|
156
|
-
let realDir;
|
|
157
|
-
try {
|
|
158
|
-
realDir = await fsPromises.realpath(dir);
|
|
159
|
-
}
|
|
160
|
-
catch (e) {
|
|
161
|
-
throw new WorkspaceResolutionError(dir, parsed.path, e instanceof Error ? e : undefined);
|
|
162
|
-
}
|
|
163
|
-
// Skip if already visited (circular reference)
|
|
164
|
-
if (visited.has(realDir)) {
|
|
165
|
-
logger.debug(`Skipping already visited workspace: ${realDir}`);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
visited.add(realDir);
|
|
169
|
-
// Include all workspaces - both internal (inside srcDir) and external
|
|
170
|
-
// The CLI determines the archive path:
|
|
171
|
-
// - Internal: _workspaces/<relative-path>
|
|
172
|
-
// - External: _workspaces/external/<name>-<hash>
|
|
173
|
-
workspaceMappings.push({
|
|
174
|
-
workspaceDir: realDir,
|
|
175
|
-
sourcePackageJsonPath: parsed.path,
|
|
176
|
-
});
|
|
177
|
-
logger.debug(`Resolved workspace: ${realDir}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return workspaceMappings;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Extracts file: dependencies from parsed package.json content.
|
|
184
|
-
* Only scans production dependencies since devDependencies and optionalDependencies
|
|
185
|
-
* are not needed for the build pipeline.
|
|
186
|
-
*/
|
|
187
|
-
function extractFileDependencies(packageJson) {
|
|
188
|
-
const fileDeps = [];
|
|
189
|
-
const deps = packageJson.dependencies;
|
|
190
|
-
if (!deps) {
|
|
191
|
-
return fileDeps;
|
|
192
|
-
}
|
|
193
|
-
for (const [packageName, version] of Object.entries(deps)) {
|
|
194
|
-
// Only handle file: dependencies. workspace: dependencies reference packages
|
|
195
|
-
// already collected via the workspaces field, so they don't need separate handling.
|
|
196
|
-
if (typeof version === 'string' && version.startsWith('file:')) {
|
|
197
|
-
const filePath = version.slice(5); // Remove 'file:' prefix
|
|
198
|
-
fileDeps.push({ packageName, filePath });
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return fileDeps;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Collects all file: dependencies that need to be uploaded.
|
|
205
|
-
* Returns mappings that track the package name, resolved path, and source package.json.
|
|
206
|
-
*/
|
|
207
|
-
export async function collectFileDependencies(parsedPackageJsons) {
|
|
208
|
-
const fileDependencyMappings = [];
|
|
209
|
-
const visited = new Set();
|
|
210
|
-
for (const parsed of parsedPackageJsons) {
|
|
211
|
-
if (!parsed.content) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const fileDeps = extractFileDependencies(parsed.content);
|
|
215
|
-
if (fileDeps.length === 0) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
logger.debug(`Found file: dependencies in ${parsed.path}: ${fileDeps.map(d => d.packageName).join(', ')}`);
|
|
219
|
-
for (const { packageName, filePath } of fileDeps) {
|
|
220
|
-
const expandedPath = expandTildePath(filePath);
|
|
221
|
-
const absolutePath = path.resolve(parsed.dir, expandedPath);
|
|
222
|
-
// Resolve symlinks to real path
|
|
223
|
-
let realPath;
|
|
224
|
-
try {
|
|
225
|
-
realPath = await fsPromises.realpath(absolutePath);
|
|
226
|
-
}
|
|
227
|
-
catch (e) {
|
|
228
|
-
throw new FileDependencyResolutionError(packageName, absolutePath, parsed.path, e instanceof Error ? e : undefined);
|
|
229
|
-
}
|
|
230
|
-
// Verify it's a directory with a package.json
|
|
231
|
-
const stats = await fsPromises.stat(realPath);
|
|
232
|
-
if (!stats.isDirectory()) {
|
|
233
|
-
throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Path is not a directory'));
|
|
234
|
-
}
|
|
235
|
-
const depPackageJsonPath = path.join(realPath, 'package.json');
|
|
236
|
-
try {
|
|
237
|
-
await fsPromises.access(depPackageJsonPath, fs.constants.F_OK);
|
|
238
|
-
}
|
|
239
|
-
catch {
|
|
240
|
-
throw new FileDependencyResolutionError(packageName, realPath, parsed.path, new Error('Directory does not contain package.json'));
|
|
241
|
-
}
|
|
242
|
-
// Skip if already visited (same path referenced multiple times)
|
|
243
|
-
if (visited.has(realPath)) {
|
|
244
|
-
logger.debug(`Skipping already visited file: dependency: ${realPath}`);
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
visited.add(realPath);
|
|
248
|
-
// Include all file: dependencies - both internal (inside srcDir) and external
|
|
249
|
-
// The CLI determines the archive path:
|
|
250
|
-
// - Internal: _workspaces/<relative-path>
|
|
251
|
-
// - External: _workspaces/external/<name>-<hash>
|
|
252
|
-
fileDependencyMappings.push({
|
|
253
|
-
packageName,
|
|
254
|
-
localPath: realPath,
|
|
255
|
-
sourcePackageJsonPath: parsed.path,
|
|
256
|
-
});
|
|
257
|
-
logger.debug(`Resolved file: dependency ${packageName}: ${realPath}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return fileDependencyMappings;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Returns the set of files that npm would include when publishing a package.
|
|
264
|
-
* Uses npm-packlist which respects the "files" field in package.json,
|
|
265
|
-
* .npmignore, and .gitignore rules.
|
|
266
|
-
*
|
|
267
|
-
* @throws Error if packlist fails to generate the file list
|
|
268
|
-
*/
|
|
269
|
-
export async function getPackableFiles(dir) {
|
|
270
|
-
try {
|
|
271
|
-
const tree = createMinimalTree(dir);
|
|
272
|
-
// Cast to Parameters<typeof packlist>[0] since npm-packlist only uses
|
|
273
|
-
// a subset of the Arborist Node properties at runtime
|
|
274
|
-
const files = await packlist(tree);
|
|
275
|
-
return new Set(files);
|
|
276
|
-
}
|
|
277
|
-
catch (e) {
|
|
278
|
-
const errorMessage = `Failed to get packlist for ${dir}: ${e instanceof Error ? e.message : String(e)}`;
|
|
279
|
-
logger.error(errorMessage);
|
|
280
|
-
throw new Error(errorMessage, { cause: e });
|
|
281
|
-
}
|
|
282
|
-
}
|