@hubspot/cli 8.0.3-experimental.1 → 8.0.4-experimental.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/commands/project/migrate.js +2 -2
- package/lang/en.d.ts +5 -8
- package/lang/en.js +6 -9
- package/lib/getStartedV2Actions.d.ts +13 -0
- package/lib/getStartedV2Actions.js +53 -0
- package/lib/projects/__tests__/upload.test.js +0 -10
- package/lib/projects/platformVersion.d.ts +1 -1
- package/lib/projects/platformVersion.js +2 -1
- package/lib/projects/upload.js +0 -9
- package/mcp-server/tools/project/AddFeatureToProjectTool.d.ts +20 -3
- package/mcp-server/tools/project/AddFeatureToProjectTool.js +6 -10
- package/mcp-server/tools/project/CreateProjectTool.d.ts +24 -4
- package/mcp-server/tools/project/CreateProjectTool.js +5 -10
- package/mcp-server/tools/project/GetApiUsagePatternsByAppIdTool.js +5 -8
- package/mcp-server/tools/project/GetBuildLogsTool.d.ts +2 -2
- package/mcp-server/tools/project/GetBuildLogsTool.js +6 -7
- package/mcp-server/tools/project/GetBuildStatusTool.d.ts +1 -1
- package/mcp-server/tools/project/GetBuildStatusTool.js +3 -4
- package/mcp-server/tools/project/GuidedWalkthroughTool.d.ts +6 -1
- package/mcp-server/tools/project/GuidedWalkthroughTool.js +1 -6
- package/mcp-server/tools/project/__tests__/GetApiUsagePatternsByAppIdTool.test.js +0 -32
- package/mcp-server/tools/project/constants.d.ts +12 -1
- package/mcp-server/tools/project/constants.js +12 -16
- package/package.json +3 -3
- package/ui/components/getStarted/GetStartedFlow.js +79 -2
- package/ui/components/getStarted/reducer.d.ts +20 -0
- package/ui/components/getStarted/reducer.js +36 -0
- package/ui/components/getStarted/screens/InstallationScreen.d.ts +7 -0
- package/ui/components/getStarted/screens/InstallationScreen.js +16 -0
- package/ui/components/getStarted/screens/ProjectSetupScreen.js +2 -1
- package/ui/lib/constants.d.ts +1 -0
- package/ui/lib/constants.js +1 -0
- package/lib/projects/__tests__/workspaceArchive.test.d.ts +0 -1
- package/lib/projects/__tests__/workspaceArchive.test.js +0 -207
- package/lib/projects/workspaces.d.ts +0 -36
- package/lib/projects/workspaces.js +0 -224
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { sanitizeFileName, untildify } from '@hubspot/local-dev-lib/path';
|
|
3
3
|
import { useApp, useFocus, useInput } from 'ink';
|
|
4
|
+
import open from 'open';
|
|
4
5
|
import { useCallback, useEffect, useReducer } from 'react';
|
|
5
6
|
import { commands } from '../../../lang/en.js';
|
|
6
|
-
import { createProjectAction, trackGetStartedUsage, uploadAndDeployAction, } from '../../../lib/getStartedV2Actions.js';
|
|
7
|
+
import { createProjectAction, pollAppInstallation, trackGetStartedUsage, uploadAndDeployAction, } from '../../../lib/getStartedV2Actions.js';
|
|
8
|
+
import { validateProjectDirectory } from '../../../lib/prompts/projectNameAndDestPrompt.js';
|
|
7
9
|
import { uiAccountDescription } from '../../../lib/ui/index.js';
|
|
8
10
|
import { ACTION_STATUSES, GET_STARTED_FLOW_STEPS, } from '../../lib/constants.js';
|
|
9
11
|
import { flowReducer } from './reducer.js';
|
|
12
|
+
import { InstallationScreen } from './screens/InstallationScreen.js';
|
|
10
13
|
import { ProjectSetupScreen } from './screens/ProjectSetupScreen.js';
|
|
11
14
|
import { UploadScreen } from './screens/UploadScreen.js';
|
|
12
15
|
import { getProject } from './selectors.js';
|
|
@@ -44,6 +47,7 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
44
47
|
statuses: {
|
|
45
48
|
create: initialName ? ACTION_STATUSES.RUNNING : ACTION_STATUSES.IDLE,
|
|
46
49
|
upload: ACTION_STATUSES.IDLE,
|
|
50
|
+
installApp: ACTION_STATUSES.IDLE,
|
|
47
51
|
},
|
|
48
52
|
});
|
|
49
53
|
const [state, dispatch] = useReducer(flowReducer, getInitialState());
|
|
@@ -68,6 +72,16 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
68
72
|
dispatch({ type: 'SET_STEP', payload: GET_STARTED_FLOW_STEPS.DEST_INPUT });
|
|
69
73
|
}, []);
|
|
70
74
|
const handleDestSubmit = useCallback(async () => {
|
|
75
|
+
const validationResult = validateProjectDirectory(project.destination);
|
|
76
|
+
if (validationResult !== true) {
|
|
77
|
+
dispatch({
|
|
78
|
+
type: 'SET_DEST_ERROR',
|
|
79
|
+
payload: typeof validationResult === 'string'
|
|
80
|
+
? validationResult
|
|
81
|
+
: commands.getStarted.v2.unknownError,
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
71
85
|
dispatch({ type: 'SET_STEP', payload: GET_STARTED_FLOW_STEPS.CREATING });
|
|
72
86
|
try {
|
|
73
87
|
await createProjectAction({
|
|
@@ -108,6 +122,57 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
108
122
|
dispatch({ type: 'UPLOAD_ERROR', payload: errorMessage });
|
|
109
123
|
}
|
|
110
124
|
}, [derivedAccountId, project.destination]);
|
|
125
|
+
const handlePollInstallation = useCallback(async () => {
|
|
126
|
+
const uploadApp = project.uploadResult?.app;
|
|
127
|
+
const projectId = project.uploadResult?.projectId;
|
|
128
|
+
if (!projectId || !uploadApp?.uid) {
|
|
129
|
+
dispatch({
|
|
130
|
+
type: 'INSTALL_APP_ERROR',
|
|
131
|
+
payload: commands.getStarted.v2.unknownError,
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await pollAppInstallation({
|
|
137
|
+
accountId: derivedAccountId,
|
|
138
|
+
projectId,
|
|
139
|
+
appUid: uploadApp.uid,
|
|
140
|
+
requiredScopes: uploadApp.config?.auth?.requiredScopes,
|
|
141
|
+
optionalScopes: uploadApp.config?.auth?.optionalScopes,
|
|
142
|
+
onTimeout: () => {
|
|
143
|
+
dispatch({ type: 'SET_POLLING_TIMED_OUT', payload: true });
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
dispatch({ type: 'INSTALL_APP_DONE' });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
dispatch({
|
|
150
|
+
type: 'INSTALL_APP_ERROR',
|
|
151
|
+
payload: error instanceof Error
|
|
152
|
+
? error.message
|
|
153
|
+
: commands.getStarted.v2.unknownError,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}, [project.uploadResult, derivedAccountId]);
|
|
157
|
+
const handleBrowserOpen = useCallback(async (shouldOpen) => {
|
|
158
|
+
await trackGetStartedUsage({
|
|
159
|
+
step: 'open-install-page',
|
|
160
|
+
type: shouldOpen ? 'opened' : 'declined',
|
|
161
|
+
}, derivedAccountId);
|
|
162
|
+
if (shouldOpen && project.uploadResult?.installUrl) {
|
|
163
|
+
try {
|
|
164
|
+
await open(project.uploadResult.installUrl, { url: true });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
dispatch({
|
|
168
|
+
type: 'SET_BROWSER_FAILED_URL',
|
|
169
|
+
payload: project.uploadResult.installUrl,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
dispatch({ type: 'START_INSTALL_APP' });
|
|
174
|
+
await handlePollInstallation();
|
|
175
|
+
}, [project.uploadResult, derivedAccountId, handlePollInstallation]);
|
|
111
176
|
const handleNameChange = useCallback((value) => {
|
|
112
177
|
dispatch({ type: 'SET_PROJECT_NAME', payload: value });
|
|
113
178
|
}, []);
|
|
@@ -116,7 +181,8 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
116
181
|
}, []);
|
|
117
182
|
useInput((_, key) => {
|
|
118
183
|
const hasError = state.statuses.create === ACTION_STATUSES.ERROR ||
|
|
119
|
-
state.statuses.upload === ACTION_STATUSES.ERROR
|
|
184
|
+
state.statuses.upload === ACTION_STATUSES.ERROR ||
|
|
185
|
+
state.statuses.installApp === ACTION_STATUSES.ERROR;
|
|
120
186
|
if (hasError) {
|
|
121
187
|
exit();
|
|
122
188
|
return;
|
|
@@ -126,11 +192,22 @@ export function GetStartedFlow({ derivedAccountId, initialName, initialDest, })
|
|
|
126
192
|
if (state.step === GET_STARTED_FLOW_STEPS.COMPLETE) {
|
|
127
193
|
handleUploadStart();
|
|
128
194
|
}
|
|
195
|
+
else if (state.step === GET_STARTED_FLOW_STEPS.OPEN_APP_PROMPT) {
|
|
196
|
+
handleBrowserOpen(true);
|
|
197
|
+
}
|
|
198
|
+
else if (state.step === GET_STARTED_FLOW_STEPS.INSTALLING_APP &&
|
|
199
|
+
state.statuses.installApp === ACTION_STATUSES.DONE) {
|
|
200
|
+
// Ready for card setup - will be handled in PR3
|
|
201
|
+
exit();
|
|
202
|
+
}
|
|
129
203
|
});
|
|
130
204
|
if (state.step === GET_STARTED_FLOW_STEPS.UPLOADING ||
|
|
131
205
|
state.step === GET_STARTED_FLOW_STEPS.OPEN_APP_PROMPT) {
|
|
132
206
|
return _jsx(UploadScreen, { state: state, accountName: accountName });
|
|
133
207
|
}
|
|
208
|
+
if (state.step === GET_STARTED_FLOW_STEPS.INSTALLING_APP) {
|
|
209
|
+
return _jsx(InstallationScreen, { state: state, accountName: accountName });
|
|
210
|
+
}
|
|
134
211
|
// Show project setup screen for initial flow
|
|
135
212
|
return (_jsx(ProjectSetupScreen, { state: state, onSelectOption: handleSelect, onNameChange: handleNameChange, onNameSubmit: handleNameSubmit, onDestChange: handleDestChange, onDestSubmit: handleDestSubmit }));
|
|
136
213
|
}
|
|
@@ -14,6 +14,7 @@ export type AppState = {
|
|
|
14
14
|
export type ActionStatuses = {
|
|
15
15
|
create: ActionStatus;
|
|
16
16
|
upload: ActionStatus;
|
|
17
|
+
installApp: ActionStatus;
|
|
17
18
|
};
|
|
18
19
|
export type FlowState = {
|
|
19
20
|
step: FlowStep;
|
|
@@ -21,6 +22,9 @@ export type FlowState = {
|
|
|
21
22
|
app: AppState;
|
|
22
23
|
statuses: ActionStatuses;
|
|
23
24
|
error?: string;
|
|
25
|
+
destError?: string;
|
|
26
|
+
browserFailedUrl?: string;
|
|
27
|
+
pollingTimedOut?: boolean;
|
|
24
28
|
};
|
|
25
29
|
type FlowAction = {
|
|
26
30
|
type: 'SET_STEP';
|
|
@@ -37,6 +41,9 @@ type FlowAction = {
|
|
|
37
41
|
} | {
|
|
38
42
|
type: 'SET_ERROR';
|
|
39
43
|
payload: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'SET_DEST_ERROR';
|
|
46
|
+
payload: string;
|
|
40
47
|
} | {
|
|
41
48
|
type: 'CLEAR_ERROR';
|
|
42
49
|
} | {
|
|
@@ -54,6 +61,19 @@ type FlowAction = {
|
|
|
54
61
|
} | {
|
|
55
62
|
type: 'UPLOAD_ERROR';
|
|
56
63
|
payload: string;
|
|
64
|
+
} | {
|
|
65
|
+
type: 'START_INSTALL_APP';
|
|
66
|
+
} | {
|
|
67
|
+
type: 'INSTALL_APP_DONE';
|
|
68
|
+
} | {
|
|
69
|
+
type: 'INSTALL_APP_ERROR';
|
|
70
|
+
payload: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'SET_BROWSER_FAILED_URL';
|
|
73
|
+
payload: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'SET_POLLING_TIMED_OUT';
|
|
76
|
+
payload: boolean;
|
|
57
77
|
};
|
|
58
78
|
export declare function flowReducer(state: FlowState, action: FlowAction): FlowState;
|
|
59
79
|
export {};
|
|
@@ -17,6 +17,13 @@ export function flowReducer(state, action) {
|
|
|
17
17
|
return {
|
|
18
18
|
...state,
|
|
19
19
|
project: { ...state.project, destination: action.payload },
|
|
20
|
+
destError: undefined,
|
|
21
|
+
};
|
|
22
|
+
case 'SET_DEST_ERROR':
|
|
23
|
+
return {
|
|
24
|
+
...state,
|
|
25
|
+
step: GET_STARTED_FLOW_STEPS.DEST_INPUT,
|
|
26
|
+
destError: action.payload,
|
|
20
27
|
};
|
|
21
28
|
case 'SET_ERROR':
|
|
22
29
|
return { ...state, error: action.payload };
|
|
@@ -66,6 +73,35 @@ export function flowReducer(state, action) {
|
|
|
66
73
|
statuses: { ...state.statuses, upload: ACTION_STATUSES.ERROR },
|
|
67
74
|
error: action.payload,
|
|
68
75
|
};
|
|
76
|
+
case 'START_INSTALL_APP':
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
step: GET_STARTED_FLOW_STEPS.INSTALLING_APP,
|
|
80
|
+
statuses: { ...state.statuses, installApp: ACTION_STATUSES.RUNNING },
|
|
81
|
+
error: undefined,
|
|
82
|
+
pollingTimedOut: false,
|
|
83
|
+
};
|
|
84
|
+
case 'INSTALL_APP_DONE':
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
statuses: { ...state.statuses, installApp: ACTION_STATUSES.DONE },
|
|
88
|
+
};
|
|
89
|
+
case 'INSTALL_APP_ERROR':
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
statuses: { ...state.statuses, installApp: ACTION_STATUSES.ERROR },
|
|
93
|
+
error: action.payload,
|
|
94
|
+
};
|
|
95
|
+
case 'SET_BROWSER_FAILED_URL':
|
|
96
|
+
return {
|
|
97
|
+
...state,
|
|
98
|
+
browserFailedUrl: action.payload,
|
|
99
|
+
};
|
|
100
|
+
case 'SET_POLLING_TIMED_OUT':
|
|
101
|
+
return {
|
|
102
|
+
...state,
|
|
103
|
+
pollingTimedOut: action.payload,
|
|
104
|
+
};
|
|
69
105
|
default:
|
|
70
106
|
return state;
|
|
71
107
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FlowState } from '../reducer.js';
|
|
2
|
+
type InstallationScreenProps = {
|
|
3
|
+
state: FlowState;
|
|
4
|
+
accountName: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function InstallationScreen({ state, accountName, }: InstallationScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { commands } from '../../../../lang/en.js';
|
|
4
|
+
import { ActionSection } from '../../ActionSection.js';
|
|
5
|
+
import { BoxWithTitle } from '../../BoxWithTitle.js';
|
|
6
|
+
import { INK_COLORS } from '../../../styles.js';
|
|
7
|
+
import { getProject } from '../selectors.js';
|
|
8
|
+
import { ACTION_STATUSES, GET_STARTED_FLOW_STEPS, } from '../../../lib/constants.js';
|
|
9
|
+
export function InstallationScreen({ state, accountName, }) {
|
|
10
|
+
const project = getProject(state);
|
|
11
|
+
const titleText = commands.getStarted.v2.startTitle;
|
|
12
|
+
// If we get to the installation screen, the app is uploaded and we have the name
|
|
13
|
+
const appName = project.uploadResult?.app?.config.name;
|
|
14
|
+
return (_jsx(BoxWithTitle, { flexGrow: 1, title: "hs get-started", borderColor: INK_COLORS.HUBSPOT_ORANGE, titleBackgroundColor: INK_COLORS.HUBSPOT_ORANGE, children: _jsxs(Box, { flexDirection: "column", rowGap: 1, children: [_jsx(Text, { bold: true, children: titleText }), _jsx(Text, { children: commands.getStarted.v2.installInstructions }), _jsx(ActionSection, { status: state.statuses.installApp, statusText: commands.getStarted.v2.installingApp(appName, accountName) }), state.browserFailedUrl && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: INK_COLORS.WARNING_YELLOW, children: commands.getStarted.v2.browserFailedToOpen(state.browserFailedUrl) }) })), state.pollingTimedOut && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Text, { color: INK_COLORS.WARNING_YELLOW, children: commands.getStarted.v2.pollingTimeout(2) }) })), state.step === GET_STARTED_FLOW_STEPS.INSTALLING_APP &&
|
|
15
|
+
state.statuses.installApp === ACTION_STATUSES.DONE && (_jsx(Text, { children: commands.getStarted.v2.pressEnterToContinueSetup }))] }) }));
|
|
16
|
+
}
|
|
@@ -35,5 +35,6 @@ export function ProjectSetupScreen({ state, onSelectOption, onNameChange, onName
|
|
|
35
35
|
return (_jsx(BoxWithTitle, { flexGrow: 1, title: "hs get-started", borderColor: INK_COLORS.HUBSPOT_ORANGE, titleBackgroundColor: INK_COLORS.HUBSPOT_ORANGE, children: _jsxs(Box, { flexDirection: "column", rowGap: 1, children: [_jsx(Text, { bold: true, children: titleText }), state.step === GET_STARTED_FLOW_STEPS.SELECT ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: overviewText }), _jsx(Text, { children: projectsText }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: selectPrompt })] }), _jsx(SelectInput, { items: GET_STARTED_FLOW_OPTIONS, onSelect: onSelectOption })] })) : (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: `${selectPrompt}` }), _jsx(Text, { color: INK_COLORS.INFO_BLUE, children: state.app.selectedLabel })] })), _jsxs(ActionSection, { status: state.statuses.create, statusText: runningProjectCreateText, errorMessage: state.statuses.create === ACTION_STATUSES.ERROR
|
|
36
36
|
? `${state.error}\n\n${commands.getStarted.v2.pressKeyToExit}`
|
|
37
37
|
: undefined, children: [state.step !== GET_STARTED_FLOW_STEPS.SELECT && (_jsx(InputField, { flag: "name", prompt: "Enter your project name", value: project.name, isEditing: state.step === GET_STARTED_FLOW_STEPS.NAME_INPUT, onChange: onNameChange, onSubmit: onNameSubmit })), state.step !== GET_STARTED_FLOW_STEPS.SELECT &&
|
|
38
|
-
state.step !== GET_STARTED_FLOW_STEPS.NAME_INPUT && (_jsx(InputField, { flag: "dest", prompt: "Choose where to create the project", value: project.destination, isEditing: state.step === GET_STARTED_FLOW_STEPS.DEST_INPUT, onChange: onDestChange, onSubmit: onDestSubmit })
|
|
38
|
+
state.step !== GET_STARTED_FLOW_STEPS.NAME_INPUT && (_jsxs(_Fragment, { children: [_jsx(InputField, { flag: "dest", prompt: "Choose where to create the project", value: project.destination, isEditing: state.step === GET_STARTED_FLOW_STEPS.DEST_INPUT, onChange: onDestChange, onSubmit: onDestSubmit }), state.destError &&
|
|
39
|
+
state.step === GET_STARTED_FLOW_STEPS.DEST_INPUT && (_jsx(Text, { color: INK_COLORS.ALERT_RED, children: state.destError }))] }))] }), state.step === GET_STARTED_FLOW_STEPS.COMPLETE && (_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", columnGap: 1, children: [_jsx(Text, { color: INK_COLORS.HUBSPOT_TEAL, children: "?" }), _jsx(Text, { children: commands.getStarted.v2.pressEnterToContinueDeploy(state.app.selectedLabel) })] }))] }) }));
|
|
39
40
|
}
|
package/ui/lib/constants.d.ts
CHANGED
package/ui/lib/constants.js
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import { computeExternalArchivePath, shortHash, updatePackageJsonInArchive, } from '../workspaces.js';
|
|
5
|
-
describe('computeExternalArchivePath', () => {
|
|
6
|
-
it('places external workspace in _workspaces/ with basename-hash', () => {
|
|
7
|
-
const localPath = '/Users/test/company-libs/utils';
|
|
8
|
-
const result = computeExternalArchivePath(localPath);
|
|
9
|
-
const expectedHash = shortHash(path.resolve(localPath));
|
|
10
|
-
expect(result).toBe(path.join('_workspaces', `utils-${expectedHash}`));
|
|
11
|
-
});
|
|
12
|
-
it('does not include an external/ subdirectory', () => {
|
|
13
|
-
const result = computeExternalArchivePath('/Users/test/libs/utils');
|
|
14
|
-
expect(result).not.toContain('external');
|
|
15
|
-
expect(result.startsWith('_workspaces')).toBe(true);
|
|
16
|
-
});
|
|
17
|
-
it('produces different paths for different directories with same basename', () => {
|
|
18
|
-
const path1 = computeExternalArchivePath('/Users/test/project-a/utils');
|
|
19
|
-
const path2 = computeExternalArchivePath('/Users/test/project-b/utils');
|
|
20
|
-
expect(path1).not.toBe(path2);
|
|
21
|
-
expect(path1).toContain('utils-');
|
|
22
|
-
expect(path2).toContain('utils-');
|
|
23
|
-
});
|
|
24
|
-
it('is deterministic', () => {
|
|
25
|
-
const localPath = '/Users/test/libs/utils';
|
|
26
|
-
expect(computeExternalArchivePath(localPath)).toBe(computeExternalArchivePath(localPath));
|
|
27
|
-
});
|
|
28
|
-
it('produces paths matching _workspaces/<name>-[8 hex chars]', () => {
|
|
29
|
-
const result = computeExternalArchivePath('/Users/test/libs/utils');
|
|
30
|
-
// Normalize path separators for cross-platform compatibility
|
|
31
|
-
const normalized = result.replace(/\\/g, '/');
|
|
32
|
-
expect(normalized).toMatch(/_workspaces\/utils-[a-f0-9]{8}$/);
|
|
33
|
-
});
|
|
34
|
-
it('uses the last path segment as basename', () => {
|
|
35
|
-
const result = computeExternalArchivePath('/Users/test/libs/@company/shared-utils');
|
|
36
|
-
expect(result).toContain('shared-utils-');
|
|
37
|
-
const normalized = result.replace(/\\/g, '/');
|
|
38
|
-
expect(normalized).toMatch(/_workspaces\/shared-utils-[a-f0-9]{8}$/);
|
|
39
|
-
});
|
|
40
|
-
it('never produces paths with .. segments', () => {
|
|
41
|
-
const testCases = [
|
|
42
|
-
'/Users/other/libs/utils',
|
|
43
|
-
'/completely/different/path',
|
|
44
|
-
'/Users/test/other-project/shared',
|
|
45
|
-
];
|
|
46
|
-
testCases.forEach(localPath => {
|
|
47
|
-
expect(computeExternalArchivePath(localPath)).not.toContain('..');
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
describe('shortHash', () => {
|
|
52
|
-
it('produces 8-character hex string', () => {
|
|
53
|
-
const hash = shortHash('/some/path');
|
|
54
|
-
expect(hash).toMatch(/^[a-f0-9]{8}$/);
|
|
55
|
-
});
|
|
56
|
-
it('is deterministic', () => {
|
|
57
|
-
const input = '/Users/test/workspace';
|
|
58
|
-
expect(shortHash(input)).toBe(shortHash(input));
|
|
59
|
-
});
|
|
60
|
-
it('produces different hashes for different inputs', () => {
|
|
61
|
-
const hash1 = shortHash('/path/a');
|
|
62
|
-
const hash2 = shortHash('/path/b');
|
|
63
|
-
expect(hash1).not.toBe(hash2);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe('updatePackageJsonInArchive', () => {
|
|
67
|
-
const srcDir = '/project/src';
|
|
68
|
-
function createMockArchive() {
|
|
69
|
-
const appended = [];
|
|
70
|
-
const mock = {
|
|
71
|
-
append: (content, opts) => {
|
|
72
|
-
appended.push({ content, name: opts.name });
|
|
73
|
-
return mock;
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
return {
|
|
77
|
-
archive: mock,
|
|
78
|
-
getAppended: () => appended,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
it('writes external workspace entries as absolute archive paths', async () => {
|
|
82
|
-
const packageJsonPath = '/project/src/app/functions/package.json';
|
|
83
|
-
const originalPackageJson = {
|
|
84
|
-
name: 'my-app',
|
|
85
|
-
workspaces: ['../../packages/utils'],
|
|
86
|
-
dependencies: {},
|
|
87
|
-
};
|
|
88
|
-
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
89
|
-
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
|
|
90
|
-
const { archive, getAppended } = createMockArchive();
|
|
91
|
-
const packageWorkspaces = new Map();
|
|
92
|
-
packageWorkspaces.set(packageJsonPath, [
|
|
93
|
-
'/_workspaces/packages-utils-a1b2c3d4',
|
|
94
|
-
'/_workspaces/packages-core-e5f6a7b8',
|
|
95
|
-
]);
|
|
96
|
-
await updatePackageJsonInArchive(archive, srcDir, packageWorkspaces, new Map());
|
|
97
|
-
const appended = getAppended();
|
|
98
|
-
expect(appended).toHaveLength(1);
|
|
99
|
-
const written = JSON.parse(appended[0].content);
|
|
100
|
-
expect(written.workspaces).toEqual([
|
|
101
|
-
'/_workspaces/packages-utils-a1b2c3d4',
|
|
102
|
-
'/_workspaces/packages-core-e5f6a7b8',
|
|
103
|
-
]);
|
|
104
|
-
vi.restoreAllMocks();
|
|
105
|
-
});
|
|
106
|
-
it('preserves internal workspace entries as relative paths', async () => {
|
|
107
|
-
const packageJsonPath = '/project/src/app/functions/package.json';
|
|
108
|
-
const originalPackageJson = {
|
|
109
|
-
name: 'my-app',
|
|
110
|
-
workspaces: ['../packages/utils'],
|
|
111
|
-
};
|
|
112
|
-
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
113
|
-
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
|
|
114
|
-
const { archive, getAppended } = createMockArchive();
|
|
115
|
-
const packageWorkspaces = new Map();
|
|
116
|
-
packageWorkspaces.set(packageJsonPath, ['../packages/utils']);
|
|
117
|
-
await updatePackageJsonInArchive(archive, srcDir, packageWorkspaces, new Map());
|
|
118
|
-
const written = JSON.parse(getAppended()[0].content);
|
|
119
|
-
expect(written.workspaces).toEqual(['../packages/utils']);
|
|
120
|
-
vi.restoreAllMocks();
|
|
121
|
-
});
|
|
122
|
-
it('writes mixed internal and external workspace entries', async () => {
|
|
123
|
-
const packageJsonPath = '/project/src/app/functions/package.json';
|
|
124
|
-
const originalPackageJson = {
|
|
125
|
-
name: 'my-app',
|
|
126
|
-
workspaces: ['../packages/utils', '/_workspaces/logger-a1b2c3d4'],
|
|
127
|
-
};
|
|
128
|
-
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
129
|
-
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
|
|
130
|
-
const { archive, getAppended } = createMockArchive();
|
|
131
|
-
const packageWorkspaces = new Map();
|
|
132
|
-
packageWorkspaces.set(packageJsonPath, [
|
|
133
|
-
'../packages/utils',
|
|
134
|
-
'/_workspaces/logger-a1b2c3d4',
|
|
135
|
-
]);
|
|
136
|
-
await updatePackageJsonInArchive(archive, srcDir, packageWorkspaces, new Map());
|
|
137
|
-
const written = JSON.parse(getAppended()[0].content);
|
|
138
|
-
expect(written.workspaces).toEqual([
|
|
139
|
-
'../packages/utils',
|
|
140
|
-
'/_workspaces/logger-a1b2c3d4',
|
|
141
|
-
]);
|
|
142
|
-
vi.restoreAllMocks();
|
|
143
|
-
});
|
|
144
|
-
it('rewrites external file: dependencies as absolute archive paths', async () => {
|
|
145
|
-
const packageJsonPath = '/project/src/app/functions/package.json';
|
|
146
|
-
const originalPackageJson = {
|
|
147
|
-
name: 'my-app',
|
|
148
|
-
dependencies: {
|
|
149
|
-
'@company/logger': 'file:../../external/logger',
|
|
150
|
-
react: '^18.0.0',
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
154
|
-
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
|
|
155
|
-
const { archive, getAppended } = createMockArchive();
|
|
156
|
-
const packageFileDeps = new Map();
|
|
157
|
-
packageFileDeps.set(packageJsonPath, new Map([['@company/logger', '_workspaces/logger-a1b2c3d4']]));
|
|
158
|
-
await updatePackageJsonInArchive(archive, srcDir, new Map(), packageFileDeps);
|
|
159
|
-
const appended = getAppended();
|
|
160
|
-
expect(appended).toHaveLength(1);
|
|
161
|
-
const written = JSON.parse(appended[0].content);
|
|
162
|
-
expect(written.dependencies['@company/logger']).toBe('file:/_workspaces/logger-a1b2c3d4');
|
|
163
|
-
expect(written.dependencies['react']).toBe('^18.0.0');
|
|
164
|
-
vi.restoreAllMocks();
|
|
165
|
-
});
|
|
166
|
-
it('leaves internal file: dependencies untouched when not in packageFileDeps', async () => {
|
|
167
|
-
const originalPackageJson = {
|
|
168
|
-
name: 'my-app',
|
|
169
|
-
dependencies: {
|
|
170
|
-
'@internal/utils': 'file:../packages/utils',
|
|
171
|
-
react: '^18.0.0',
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
175
|
-
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(originalPackageJson));
|
|
176
|
-
const { archive, getAppended } = createMockArchive();
|
|
177
|
-
await updatePackageJsonInArchive(archive, srcDir, new Map(), new Map());
|
|
178
|
-
// Nothing to update, so no package.json should be appended
|
|
179
|
-
expect(getAppended()).toHaveLength(0);
|
|
180
|
-
vi.restoreAllMocks();
|
|
181
|
-
});
|
|
182
|
-
it('uses same absolute path regardless of package.json depth', async () => {
|
|
183
|
-
const shallowPath = '/project/src/package.json';
|
|
184
|
-
const deepPath = '/project/src/app/functions/nested/package.json';
|
|
185
|
-
const archivePath = '/_workspaces/utils-a1b2c3d4';
|
|
186
|
-
const makePackageJson = () => ({
|
|
187
|
-
name: 'test',
|
|
188
|
-
workspaces: ['placeholder'],
|
|
189
|
-
});
|
|
190
|
-
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
191
|
-
const { archive: archive1, getAppended: getAppended1 } = createMockArchive();
|
|
192
|
-
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(makePackageJson()));
|
|
193
|
-
const workspaces1 = new Map();
|
|
194
|
-
workspaces1.set(shallowPath, [archivePath]);
|
|
195
|
-
await updatePackageJsonInArchive(archive1, srcDir, workspaces1, new Map());
|
|
196
|
-
const { archive: archive2, getAppended: getAppended2 } = createMockArchive();
|
|
197
|
-
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(makePackageJson()));
|
|
198
|
-
const workspaces2 = new Map();
|
|
199
|
-
workspaces2.set(deepPath, [archivePath]);
|
|
200
|
-
await updatePackageJsonInArchive(archive2, srcDir, workspaces2, new Map());
|
|
201
|
-
const written1 = JSON.parse(getAppended1()[0].content);
|
|
202
|
-
const written2 = JSON.parse(getAppended2()[0].content);
|
|
203
|
-
expect(written1.workspaces).toEqual(['/_workspaces/utils-a1b2c3d4']);
|
|
204
|
-
expect(written2.workspaces).toEqual(['/_workspaces/utils-a1b2c3d4']);
|
|
205
|
-
vi.restoreAllMocks();
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import archiver from 'archiver';
|
|
2
|
-
import { WorkspaceMapping, FileDependencyMapping } from '@hubspot/project-parsing-lib/workspaces';
|
|
3
|
-
/**
|
|
4
|
-
* Result of archiving workspaces and file dependencies
|
|
5
|
-
*/
|
|
6
|
-
export type WorkspaceArchiveResult = {
|
|
7
|
-
packageWorkspaces: Map<string, string[]>;
|
|
8
|
-
packageFileDeps: Map<string, Map<string, string>>;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Generates a short hash of the input string for use in workspace paths.
|
|
12
|
-
* Uses SHA256 truncated to 8 hex characters (4 billion possibilities).
|
|
13
|
-
*/
|
|
14
|
-
export declare function shortHash(input: string): string;
|
|
15
|
-
/**
|
|
16
|
-
* Determines the archive path for an external workspace or file: dependency.
|
|
17
|
-
* Produces `_workspaces/<basename>-<hash>` with no subdirectory.
|
|
18
|
-
* The hash prevents collisions between different directories with the same basename.
|
|
19
|
-
*/
|
|
20
|
-
export declare function computeExternalArchivePath(absolutePath: string): string;
|
|
21
|
-
/**
|
|
22
|
-
* Updates package.json files in the archive to reflect new workspace and file: dependency paths.
|
|
23
|
-
*
|
|
24
|
-
* Workspace entries in packageWorkspaces are already in final form:
|
|
25
|
-
* - Internal workspaces: relative paths (e.g. "../packages/utils")
|
|
26
|
-
* - External workspaces: absolute paths (e.g. "/_workspaces/logger-abc")
|
|
27
|
-
*
|
|
28
|
-
* Only external file: dependencies appear in packageFileDeps; internal ones
|
|
29
|
-
* keep their original file: references and are left untouched.
|
|
30
|
-
*/
|
|
31
|
-
export declare function updatePackageJsonInArchive(archive: archiver.Archiver, srcDir: string, packageWorkspaces: Map<string, string[]>, packageFileDeps: Map<string, Map<string, string>>): Promise<void>;
|
|
32
|
-
/**
|
|
33
|
-
* Main orchestration function that handles archiving of workspaces and file dependencies.
|
|
34
|
-
* This is the clean integration point for upload.ts.
|
|
35
|
-
*/
|
|
36
|
-
export declare function archiveWorkspacesAndDependencies(archive: archiver.Archiver, srcDir: string, projectDir: string, workspaceMappings: WorkspaceMapping[], fileDependencyMappings: FileDependencyMapping[]): Promise<WorkspaceArchiveResult>;
|