@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubspot/project-parsing-lib",
3
- "version": "0.2.0-experimental.0",
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.54.0",
56
- "@typescript-eslint/parser": "8.54.0",
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",
@@ -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 PAGES_KEY = "page";
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";
@@ -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 PAGES_KEY = 'page';
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
- [PAGES_KEY]: {
58
- dir: 'pages',
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,
@@ -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
- hsMetaFiles.push(...metaFilesByType);
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,
@@ -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
- }
@@ -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 {};
@@ -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
- }