Skip to content

Commit d8e3ab2

Browse files
committed
feat: Add auto-import dev dependencies with comprehensive TypeScript support
- Added auto_import_dev_dependencies configuration option - Implemented smart dependency filtering with regex exclusion patterns - Enhanced package.json parsing with robust error handling - Added comprehensive validation and conflict detection - Included extensive test coverage for all new functionality - Updated config template with new auto-import options
1 parent 8424da2 commit d8e3ab2

16 files changed

+1212
-9
lines changed

bin/commands/runs.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ module.exports = function run(args, rawArgs) {
143143
// set the no-wrap
144144
utils.setNoWrap(bsConfig, args);
145145

146+
// process auto-import dev dependencies
147+
utils.processAutoImportDependencies(bsConfig.run_settings);
148+
146149
// add cypress dependency if missing
147150
utils.setCypressNpmDependency(bsConfig);
148151

bin/helpers/constants.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,28 @@ const validationMessages = {
196196
"You have specified '--record' flag but you've not provided the '--record-key' and we could not find any value in 'CYPRESS_RECORD_KEY' environment variable. Your record functionality on cypress.io dashboard might not work as it needs the key and projectId",
197197
NODE_VERSION_PARSING_ERROR:
198198
"We weren't able to successfully parse the specified nodeVersion. We will be using the default nodeVersion to run your tests.",
199+
AUTO_IMPORT_CONFLICT_ERROR:
200+
"Cannot use both 'auto_import_dev_dependencies' and manual npm dependency configuration. Please either set 'auto_import_dev_dependencies' to false or remove manual 'npm_dependencies', 'win_npm_dependencies', and 'mac_npm_dependencies' configurations.",
201+
AUTO_IMPORT_INVALID_TYPE:
202+
"'auto_import_dev_dependencies' must be a boolean value (true or false).",
203+
PACKAGE_JSON_NOT_FOUND:
204+
"package.json not found in project directory. Cannot auto-import devDependencies.",
205+
PACKAGE_JSON_PERMISSION_DENIED:
206+
"Cannot read package.json due to permission issues. Please check file permissions.",
207+
PACKAGE_JSON_MALFORMED:
208+
"package.json contains invalid JSON syntax. Please fix the JSON format.",
209+
PACKAGE_JSON_NOT_OBJECT:
210+
"package.json must contain a JSON object, not an array or other type.",
211+
DEVDEPS_INVALID_FORMAT:
212+
"devDependencies field in package.json must be an object, not an array or other type.",
213+
EXCLUDE_DEPS_INVALID_TYPE:
214+
"'exclude_dependencies' must be an array of strings.",
215+
EXCLUDE_DEPS_INVALID_PATTERNS:
216+
"'exclude_dependencies' must contain only string values representing regex patterns.",
217+
INVALID_REGEX_PATTERN:
218+
"Invalid regex pattern found in 'exclude_dependencies': {pattern}. Please provide valid regex patterns.",
219+
DEPENDENCIES_PARAM_INVALID:
220+
"Dependencies parameter must be an object.",
199221
};
200222

201223
const cliMessages = {

bin/helpers/packageInstaller.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ const setupPackageFolder = (runSettings, directoryPath) => {
3232
}
3333

3434
// Combine win and mac specific dependencies if present
35-
if (typeof runSettings.npm_dependencies === 'object') {
35+
const combinedDependencies = combineMacWinNpmDependencies(runSettings);
36+
if (combinedDependencies && Object.keys(combinedDependencies).length > 0) {
3637
Object.assign(packageJSON, {
37-
devDependencies: runSettings.npm_dependencies,
38+
devDependencies: combinedDependencies,
3839
});
3940
}
4041

bin/helpers/utils.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,3 +1804,181 @@ exports.decodeJWTToken = (token) => {
18041804
return undefined;
18051805
}
18061806
}
1807+
1808+
exports.validateAutoImportConflict = (runSettings) => {
1809+
const constants = require('./constants');
1810+
1811+
// Validate auto_import_dev_dependencies type
1812+
if (runSettings.auto_import_dev_dependencies !== undefined &&
1813+
typeof runSettings.auto_import_dev_dependencies !== 'boolean') {
1814+
throw new Error(constants.validationMessages.AUTO_IMPORT_INVALID_TYPE);
1815+
}
1816+
1817+
// Skip validation if auto_import_dev_dependencies is not enabled
1818+
if (!runSettings.auto_import_dev_dependencies) {
1819+
return;
1820+
}
1821+
1822+
// Check if any manual npm dependency configurations have values
1823+
const hasNpmDeps = runSettings.npm_dependencies &&
1824+
typeof runSettings.npm_dependencies === 'object' &&
1825+
Object.keys(runSettings.npm_dependencies).length > 0;
1826+
1827+
const hasWinDeps = runSettings.win_npm_dependencies &&
1828+
typeof runSettings.win_npm_dependencies === 'object' &&
1829+
Object.keys(runSettings.win_npm_dependencies).length > 0;
1830+
1831+
const hasMacDeps = runSettings.mac_npm_dependencies &&
1832+
typeof runSettings.mac_npm_dependencies === 'object' &&
1833+
Object.keys(runSettings.mac_npm_dependencies).length > 0;
1834+
1835+
if (hasNpmDeps || hasWinDeps || hasMacDeps) {
1836+
throw new Error(constants.validationMessages.AUTO_IMPORT_CONFLICT_ERROR);
1837+
}
1838+
};
1839+
1840+
exports.readPackageJsonDevDependencies = (projectDir) => {
1841+
const fs = require('fs');
1842+
const path = require('path');
1843+
const constants = require('./constants');
1844+
1845+
const packageJsonPath = path.join(projectDir, 'package.json');
1846+
1847+
try {
1848+
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
1849+
1850+
// Remove BOM if present
1851+
const cleanedContent = packageJsonContent.replace(/^\ufeff/, '');
1852+
1853+
if (!cleanedContent.trim()) {
1854+
throw new Error(constants.validationMessages.PACKAGE_JSON_MALFORMED);
1855+
}
1856+
1857+
let packageJson;
1858+
try {
1859+
packageJson = JSON.parse(cleanedContent);
1860+
} catch (parseError) {
1861+
throw new Error(constants.validationMessages.PACKAGE_JSON_MALFORMED);
1862+
}
1863+
1864+
if (typeof packageJson !== 'object' || packageJson === null || Array.isArray(packageJson)) {
1865+
throw new Error(constants.validationMessages.PACKAGE_JSON_NOT_OBJECT);
1866+
}
1867+
1868+
// Handle missing devDependencies field
1869+
if (packageJson.devDependencies === undefined) {
1870+
return {};
1871+
}
1872+
1873+
// Validate devDependencies format
1874+
if (typeof packageJson.devDependencies !== 'object' ||
1875+
packageJson.devDependencies === null ||
1876+
Array.isArray(packageJson.devDependencies)) {
1877+
throw new Error(constants.validationMessages.DEVDEPS_INVALID_FORMAT);
1878+
}
1879+
1880+
return packageJson.devDependencies;
1881+
1882+
} catch (error) {
1883+
if (error.code === 'ENOENT') {
1884+
throw new Error(constants.validationMessages.PACKAGE_JSON_NOT_FOUND);
1885+
} else if (error.code === 'EACCES') {
1886+
throw new Error(constants.validationMessages.PACKAGE_JSON_PERMISSION_DENIED);
1887+
} else if (error.message.includes(constants.validationMessages.PACKAGE_JSON_MALFORMED) ||
1888+
error.message.includes(constants.validationMessages.PACKAGE_JSON_NOT_OBJECT) ||
1889+
error.message.includes(constants.validationMessages.DEVDEPS_INVALID_FORMAT)) {
1890+
throw error;
1891+
} else {
1892+
throw new Error(`Cannot read package.json: ${error.message}`);
1893+
}
1894+
}
1895+
};
1896+
1897+
exports.filterDependenciesWithRegex = (dependencies, excludePatterns) => {
1898+
const constants = require('./constants');
1899+
1900+
// Validate dependencies parameter
1901+
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
1902+
throw new Error(constants.validationMessages.DEPENDENCIES_PARAM_INVALID);
1903+
}
1904+
1905+
// Return all dependencies if no exclusion patterns
1906+
if (!excludePatterns) {
1907+
return dependencies;
1908+
}
1909+
1910+
// Validate excludePatterns parameter
1911+
if (!Array.isArray(excludePatterns)) {
1912+
throw new Error(constants.validationMessages.EXCLUDE_DEPS_INVALID_TYPE);
1913+
}
1914+
1915+
// Validate all patterns are strings
1916+
for (const pattern of excludePatterns) {
1917+
if (typeof pattern !== 'string') {
1918+
throw new Error(constants.validationMessages.EXCLUDE_DEPS_INVALID_PATTERNS);
1919+
}
1920+
}
1921+
1922+
// If no patterns, return all dependencies
1923+
if (excludePatterns.length === 0) {
1924+
return dependencies;
1925+
}
1926+
1927+
const filteredDependencies = {};
1928+
1929+
for (const [packageName, version] of Object.entries(dependencies)) {
1930+
let shouldExclude = false;
1931+
1932+
for (const pattern of excludePatterns) {
1933+
// Skip empty patterns
1934+
if (!pattern) {
1935+
continue;
1936+
}
1937+
1938+
try {
1939+
const regex = new RegExp(pattern);
1940+
if (regex.test(packageName)) {
1941+
shouldExclude = true;
1942+
break;
1943+
}
1944+
} catch (regexError) {
1945+
const errorMsg = constants.validationMessages.INVALID_REGEX_PATTERN.replace('{pattern}', pattern);
1946+
throw new Error(errorMsg);
1947+
}
1948+
}
1949+
1950+
if (!shouldExclude) {
1951+
filteredDependencies[packageName] = version;
1952+
}
1953+
}
1954+
1955+
return filteredDependencies;
1956+
};
1957+
1958+
exports.processAutoImportDependencies = (runSettings) => {
1959+
// Always run validation first
1960+
exports.validateAutoImportConflict(runSettings);
1961+
1962+
// Skip processing if auto_import_dev_dependencies is not enabled
1963+
if (!runSettings.auto_import_dev_dependencies) {
1964+
return;
1965+
}
1966+
1967+
// Determine project directory using battle-tested logic
1968+
let projectDir;
1969+
if (runSettings.home_directory) {
1970+
projectDir = runSettings.home_directory;
1971+
} else {
1972+
const path = require('path');
1973+
projectDir = path.dirname(runSettings.cypressConfigFilePath);
1974+
}
1975+
1976+
// Read devDependencies from package.json
1977+
const devDependencies = exports.readPackageJsonDevDependencies(projectDir);
1978+
1979+
// Apply exclusion filters
1980+
const filteredDependencies = exports.filterDependenciesWithRegex(devDependencies, runSettings.exclude_dependencies);
1981+
1982+
// Set the npm_dependencies in runSettings
1983+
runSettings.npm_dependencies = filteredDependencies;
1984+
};

bin/templates/configTemplate.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ module.exports = function () {
5959
"parallels": "Number of parallels you want to run",
6060
"npm_dependencies": {
6161
},
62+
"auto_import_dev_dependencies": false,
63+
"exclude_dependencies": [],
6264
"package_config_options": {
6365
},
6466
"headless": true

test/unit/bin/commands/runs.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ describe("runs", () => {
130130
setDebugModeStub = sandbox.stub();
131131
setTimezoneStub = sandbox.stub();
132132
setCypressNpmDependencyStub = sandbox.stub();
133+
processAutoImportDependenciesStub = sandbox.stub();
133134
});
134135

135136
afterEach(() => {
@@ -179,7 +180,8 @@ describe("runs", () => {
179180
setBuildTags: setBuildTagsStub,
180181
setNetworkLogs: setNetworkLogsStub,
181182
setTimezone: setTimezoneStub,
182-
setCypressNpmDependency: setCypressNpmDependencyStub
183+
setCypressNpmDependency: setCypressNpmDependencyStub,
184+
processAutoImportDependencies: processAutoImportDependenciesStub
183185
},
184186
'../helpers/capabilityHelper': {
185187
validate: capabilityValidatorStub
@@ -303,6 +305,7 @@ describe("runs", () => {
303305
setTimezoneStub = sandbox.stub();
304306
setCypressNpmDependencyStub = sandbox.stub();
305307
packageSetupAndInstallerStub = sandbox.stub();
308+
processAutoImportDependenciesStub = sandbox.stub();
306309
});
307310

308311
afterEach(() => {
@@ -356,7 +359,8 @@ describe("runs", () => {
356359
setNetworkLogs: setNetworkLogsStub,
357360
setInteractiveCapability: setInteractiveCapabilityStub,
358361
setTimezone: setTimezoneStub,
359-
setCypressNpmDependency: setCypressNpmDependencyStub
362+
setCypressNpmDependency: setCypressNpmDependencyStub,
363+
processAutoImportDependencies: processAutoImportDependenciesStub
360364
},
361365
'../helpers/capabilityHelper': {
362366
validate: capabilityValidatorStub,
@@ -510,6 +514,7 @@ describe("runs", () => {
510514
setCypressNpmDependencyStub = sandbox.stub();
511515
packageSetupAndInstallerStub = sandbox.stub();
512516
fetchFolderSizeStub = sandbox.stub();
517+
processAutoImportDependenciesStub = sandbox.stub();
513518
});
514519

515520
afterEach(() => {
@@ -565,7 +570,8 @@ describe("runs", () => {
565570
setInteractiveCapability: setInteractiveCapabilityStub,
566571
setTimezone: setTimezoneStub,
567572
setCypressNpmDependency: setCypressNpmDependencyStub,
568-
fetchFolderSize: fetchFolderSizeStub
573+
fetchFolderSize: fetchFolderSizeStub,
574+
processAutoImportDependencies: processAutoImportDependenciesStub
569575
},
570576
'../helpers/capabilityHelper': {
571577
validate: capabilityValidatorStub,
@@ -734,6 +740,7 @@ describe("runs", () => {
734740
setCypressNpmDependencyStub = sandbox.stub();
735741
packageSetupAndInstallerStub = sandbox.stub();
736742
fetchFolderSizeStub = sandbox.stub();
743+
processAutoImportDependenciesStub = sandbox.stub();
737744
});
738745

739746
afterEach(() => {
@@ -792,7 +799,8 @@ describe("runs", () => {
792799
setInteractiveCapability: setInteractiveCapabilityStub,
793800
setTimezone: setTimezoneStub,
794801
setCypressNpmDependency: setCypressNpmDependencyStub,
795-
fetchFolderSize: fetchFolderSizeStub
802+
fetchFolderSize: fetchFolderSizeStub,
803+
processAutoImportDependencies: processAutoImportDependenciesStub
796804
},
797805
'../helpers/capabilityHelper': {
798806
validate: capabilityValidatorStub,
@@ -954,7 +962,8 @@ describe("runs", () => {
954962
setInteractiveCapability: setInteractiveCapabilityStub,
955963
setTimezone: setTimezoneStub,
956964
setCypressNpmDependency: setCypressNpmDependencyStub,
957-
fetchFolderSize: fetchFolderSizeStub
965+
fetchFolderSize: fetchFolderSizeStub,
966+
processAutoImportDependencies: processAutoImportDependenciesStub
958967
},
959968
'../helpers/capabilityHelper': {
960969
validate: capabilityValidatorStub,
@@ -1147,6 +1156,7 @@ describe("runs", () => {
11471156
setCypressNpmDependencyStub = sandbox.stub();
11481157
packageSetupAndInstallerStub = sandbox.stub();
11491158
fetchFolderSizeStub = sandbox.stub();
1159+
processAutoImportDependenciesStub = sandbox.stub();
11501160
});
11511161

11521162
afterEach(() => {
@@ -1213,7 +1223,8 @@ describe("runs", () => {
12131223
setInteractiveCapability: setInteractiveCapabilityStub,
12141224
setTimezone: setTimezoneStub,
12151225
setCypressNpmDependency: setCypressNpmDependencyStub,
1216-
fetchFolderSize: fetchFolderSizeStub
1226+
fetchFolderSize: fetchFolderSizeStub,
1227+
processAutoImportDependencies: processAutoImportDependenciesStub
12171228
},
12181229
'../helpers/capabilityHelper': {
12191230
validate: capabilityValidatorStub,
@@ -1407,7 +1418,8 @@ describe("runs", () => {
14071418
setInteractiveCapability: setInteractiveCapabilityStub,
14081419
setTimezone: setTimezoneStub,
14091420
setCypressNpmDependency: setCypressNpmDependencyStub,
1410-
fetchFolderSize: fetchFolderSizeStub
1421+
fetchFolderSize: fetchFolderSizeStub,
1422+
processAutoImportDependencies: processAutoImportDependenciesStub
14111423
},
14121424
'../helpers/capabilityHelper': {
14131425
validate: capabilityValidatorStub,

0 commit comments

Comments
 (0)