Skip to content

Commit

Permalink
feature(init): adds extensions detection to one shot configs (#712)
Browse files Browse the repository at this point in the history
  • Loading branch information
sverweij authored Dec 27, 2022
1 parent 41461f4 commit 76508e3
Show file tree
Hide file tree
Showing 38 changed files with 307 additions and 31 deletions.
9 changes: 7 additions & 2 deletions .dependency-cruiser.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"^fs$",
"^path$",
"$1",
"^src/meta.js$"
"^src/meta.js$",
"^src/extract/transpile/meta.js$"
]
}
},
Expand Down Expand Up @@ -299,11 +300,15 @@
"extensions": [
".js",
// ".cjs",
// ".mjs",
".mjs",
// ".jsx",
// ".ts",
// ".cts",
// ".mts",
// ".tsx",
".d.ts"
// ".d.cts",
// ".d.mts",
// ".coffee",
// ".litcoffee",
// "cofee.md",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ yarn.lock

# integration test intermediate files
test/integration/*.testing-ground
test/extract/__mocks__/symlinked
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"depcruise:graph:view:diff": "node ./bin/dependency-cruise.js bin src test --prefix vscode://file/$(pwd)/ --config configs/.dependency-cruiser-unlimited.json --output-type dot --progress cli-feedback --reaches \"$(watskeburt develop)\" | dot -T svg | node ./bin/wrap-stream-in-html.js | browser",
"depcruise:report": "node ./bin/dependency-cruise.js src bin test configs types --output-type err-html --config configs/.dependency-cruiser-show-metrics-config.json --output-to dependency-violations.html",
"depcruise:report:view": "node ./bin/dependency-cruise.js src bin test configs types --output-type err-html --config configs/.dependency-cruiser-show-metrics-config.json --output-to - | browser",
"depcruise:focus": "node ./bin/dependency-cruise.js src bin test configs types tools --progress --config configs/.dependency-cruiser-show-metrics-config.json --output-type text --focus",
"depcruise:focus": "node ./bin/dependency-cruise.js src bin test configs types tools --progress --config --output-type text --focus",
"depcruise:reaches": "node ./bin/dependency-cruise.js src bin test configs types tools --progress --config configs/.dependency-cruiser-unlimited.json --output-type text --reaches",
"format": "prettier --loglevel warn --write \"src/**/*.js\" \"configs/**/*.js\" \"tools/**/*.mjs\" \"bin/*\" \"types/*.d.ts\" \"test/**/*.spec.{cjs,js}\" \"test/**/*.{spec,utl}.mjs\"",
"format:check": "prettier --loglevel warn --check \"src/**/*.js\" \"configs/**/*.js\" \"tools/**/*.mjs\" \"bin/*\" \"types/*.d.ts\" \"test/**/*.spec.{cjs,js}\" \"test/**/*.{spec,utl}.mjs\"",
Expand Down
24 changes: 23 additions & 1 deletion src/cli/init-config/build-config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
// @ts-check
const Handlebars = require("handlebars/runtime");

const { folderNameArrayToRE } = require("./utl");

/* eslint import/no-unassigned-import: 0 */
require("./config.js.template");

/**
* @param {string} pString
* @returns {string}
*/
function quote(pString) {
return `"${pString}"`;
}

/**
* @param {string[]=} pExtensions
* @returns {string}
*/
function extensionsToString(pExtensions) {
if (pExtensions) {
return `[${pExtensions.map(quote).join(", ")}]`;
}
return "";
}

/**
* Creates a .dependency-cruiser config with a set of basic validations
* to the current directory.
Expand All @@ -22,5 +41,8 @@ module.exports = function buildConfig(pNormalizedInitOptions) {
pNormalizedInitOptions.sourceLocation
),
testLocationRE: folderNameArrayToRE(pNormalizedInitOptions.testLocation),
resolutionExtensionsAsString: extensionsToString(
pNormalizedInitOptions.resolutionExtensions
),
});
};
6 changes: 5 additions & 1 deletion src/cli/init-config/config.js.template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ module.exports = {
If you have a 'conditionNames' attribute in your webpack config, that one will
have precedence over the one specified here.
*/
conditionNames: ["import", "require", "node", "default"]
conditionNames: ["import", "require", "node", "default"],
/*
The extensions, by default are the same as the ones dependency-cruiser
can access (run `npx depcruise --info` to see which ones that are in
Expand All @@ -408,7 +408,11 @@ module.exports = {
[".js", ".jsx"]). This can speed up the most expensive step in
dependency cruising (module resolution) quite a bit.
*/
{{#if specifyResolutionExtensions}}
extensions: {{{resolutionExtensionsAsString}}},
{{^}}
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"]
{{/if}}
},
reporterOptions: {
dot: {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/init-config/config.js.template.js

Large diffs are not rendered by default.

35 changes: 33 additions & 2 deletions src/cli/init-config/environment-helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check
const { readFileSync, readdirSync, accessSync, statSync, R_OK } = require("fs");
const { join } = require("path");
const has = require("lodash/has");
Expand All @@ -15,8 +16,8 @@ const BABEL_CONFIG_CANDIDATE_PATTERN = /^\.babelrc$|.*babel.*\.json/gi;
/**
* Read the package manifest ('package.json') and return it as a javascript object
*
* @param {string} pManifestFileName - the file name where the package manifest (package.json) lives
* @returns {any} - the contents of said manifest as a javascript object
* @param {import("fs").PathOrFileDescriptor} pManifestFileName - the file name where the package manifest (package.json) lives
* @returns {Record<string,any>} - the contents of said manifest as a javascript object
* @throws {ENOENT} when the manifest wasn't found
* @throws {SyntaxError} when the manifest's json is invalid
*/
Expand All @@ -40,6 +41,9 @@ function fileExists(pFile) {
return true;
}

/**
* @returns {boolean}
*/
function babelIsConfiguredInManifest() {
let lReturnValue = false;

Expand All @@ -51,6 +55,9 @@ function babelIsConfiguredInManifest() {
return lReturnValue;
}

/**
* @returns {boolean}
*/
function isTypeModule() {
let lReturnValue = false;

Expand All @@ -63,12 +70,21 @@ function isTypeModule() {
return lReturnValue;
}

/**
* @param {string} pFolderName
* @returns {string[]} Array of folder names
*/
function getFolderNames(pFolderName) {
return readdirSync(pFolderName, "utf8").filter((pFileName) =>
statSync(join(pFolderName, pFileName)).isDirectory()
);
}

/**
* @param {RegExp} pPattern
* @param {string=} pFolderName
* @returns {string[]}
*/
function getMatchingFileNames(pPattern, pFolderName = process.cwd()) {
return readdirSync(pFolderName, "utf8").filter(
(pFileName) =>
Expand All @@ -77,6 +93,10 @@ function getMatchingFileNames(pPattern, pFolderName = process.cwd()) {
);
}

/**
* @param {string[]} pFolderNames
* @returns {boolean}
*/
function isLikelyMonoRepo(pFolderNames = getFolderNames(process.cwd())) {
return pFolderNames.includes("packages");
}
Expand All @@ -95,13 +115,20 @@ function getFolderCandidates(pCandidateFolderArray) {
};
}

/**
* @param {string[]|string} pLocations
* @returns {string[]}
*/
function toSourceLocationArray(pLocations) {
if (!Array.isArray(pLocations)) {
return pLocations.split(",").map((pFolder) => pFolder.trim());
}
return pLocations;
}

/**
* @returns {string[]}
*/
function getManifestFilesWithABabelConfig() {
return babelIsConfiguredInManifest() ? ["package.json"] : [];
}
Expand Down Expand Up @@ -132,6 +159,10 @@ const getTestFolderCandidates = getFolderCandidates(LIKELY_TEST_FOLDERS);
const getMonoRepoPackagesCandidates = getFolderCandidates(
LIKELY_PACKAGES_FOLDERS
);

/**
* @returns {string}
*/
function getDefaultConfigFileName() {
return isTypeModule() ? ".dependency-cruiser.cjs" : DEFAULT_CONFIG_FILE_NAME;
}
Expand Down
114 changes: 114 additions & 0 deletions src/cli/init-config/find-extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// @ts-check
/* eslint-disable security/detect-object-injection */
const fs = require("fs");
const path = require("path");
const pathToPosix = require("../../utl/path-to-posix");
const getExtension = require("../../utl/get-extension.js");
const meta = require("../../extract/transpile/meta");

/**
* @param {string[]} pIgnorablePathElements
* @returns {(string) => boolean}
*/
function notIgnorable(pIgnorablePathElements) {
return (pPath) => {
return !pIgnorablePathElements.includes(pPath);
};
}

/**
* @param {string} pFullPathToFile
* @param {string} pBaseDirectory
* @returns {boolean}
*/
function fileIsDirectory(pFullPathToFile, pBaseDirectory) {
try {
const lStat = fs.statSync(path.join(pBaseDirectory, pFullPathToFile));
return lStat.isDirectory();
} catch (pError) {
return false;
}
}

/**
* @param {string} pDirectoryName
* @param {{baseDir: string; ignorablePathElements: string[]}} pOptions
* @returns {string[]}
*/
function listAllModules(pDirectoryName, { baseDir, ignorablePathElements }) {
return fs
.readdirSync(path.join(baseDir, pDirectoryName))
.filter(notIgnorable(ignorablePathElements))
.map((pFileName) => path.join(pDirectoryName, pFileName))
.map((pFullPathToFile) => ({
fullPathToFile: pFullPathToFile,
isDirectory: fileIsDirectory(pFullPathToFile, baseDir),
}))
.reduce(
/**
* @param {string[]} pSum
* @param {{fullPathToFile: string; isDirectory: boolean}} pCurrentValue
* @returns {string[]}
*/
(pSum, { fullPathToFile, isDirectory }) => {
if (isDirectory) {
return pSum.concat(
listAllModules(fullPathToFile, { baseDir, ignorablePathElements })
);
}
return pSum.concat(fullPathToFile);
},
[]
)
.map((pFullPathToFile) => pathToPosix(pFullPathToFile));
}

/**
* @param {Record<string,number>} pAll
* @param {string} pExtension
*/
function reduceToCounts(pAll, pExtension) {
if (pAll[pExtension]) {
pAll[pExtension] += 1;
} else {
pAll[pExtension] = 1;
}
return pAll;
}

function compareByCount(pCountsObject) {
return function compare(pLeft, pRight) {
return pCountsObject[pRight] - pCountsObject[pLeft];
};
}

/**
* @param {string[]} pDirectories
* @param {{baseDir?: string; ignorablePathElements?: string[], scannableExtensions?: string[]}=} pOptions
* @returns {string[]}
*/
module.exports = function findExtensions(pDirectories, pOptions) {
const lOptions = {
baseDir: process.cwd(),
ignorablePathElements: [
".git",
".husky",
".vscode",
"coverage",
"node_nodules",
"nyc",
],
scannableExtensions: meta.scannableExtensions,
...pOptions,
};

const lExtensionsWithCounts = pDirectories
.flatMap((pDirectory) =>
listAllModules(pDirectory, lOptions).map(getExtension).filter(Boolean)
)
.reduce(reduceToCounts, {});

return Object.keys(lExtensionsWithCounts)
.filter((pExtension) => lOptions.scannableExtensions.includes(pExtension))
.sort(compareByCount(lExtensionsWithCounts));
};
13 changes: 7 additions & 6 deletions src/cli/init-config/get-user-input.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check
const prompts = require("prompts");
const {
isLikelyMonoRepo,
Expand Down Expand Up @@ -34,9 +35,9 @@ const QUESTIONS = [
},
{
name: "sourceLocation",
type: (_, pAnswers) => (pAnswers.isMonoRepo ? "text" : false),
type: (_, pAnswers) => (pAnswers.isMonoRepo ? "list" : false),
message: "Mono repo it is! Where do your packages live?",
initial: getMonoRepoPackagesCandidates(),
initial: getMonoRepoPackagesCandidates().join(", "),
validate: validateLocation,
},
{
Expand All @@ -48,9 +49,9 @@ const QUESTIONS = [
},
{
name: "sourceLocation",
type: (_, pAnswers) => (pAnswers.isMonoRepo ? false : "text"),
type: (_, pAnswers) => (pAnswers.isMonoRepo ? false : "list"),
message: "Where do your source files live?",
initial: getSourceFolderCandidates(),
initial: getSourceFolderCandidates().join(", "),
validate: validateLocation,
},
{
Expand All @@ -67,9 +68,9 @@ const QUESTIONS = [
{
name: "testLocation",
type: (_, pAnswers) =>
pAnswers.hasTestsOutsideSource && !pAnswers.isMonoRepo ? "text" : false,
pAnswers.hasTestsOutsideSource && !pAnswers.isMonoRepo ? "list" : false,
message: "Where do your test files live?",
initial: getTestFolderCandidates(),
initial: getTestFolderCandidates().join(", "),
validate: validateLocation,
},
{
Expand Down
1 change: 1 addition & 0 deletions src/cli/init-config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function getOneShotConfig(pOneShotConfigId) {
webpackConfig: getWebpackConfigCandidates().shift(),
useBabelConfig: hasBabelConfigCandidates(),
babelConfig: getBabelConfigCandidates().shift(),
specifyResolutionExtensions: true,
};
/** @type {Record<import("./types").OneShotConfigIDType, import("./types").IPartialInitConfig>} */
const lOneShotConfigs = {
Expand Down
9 changes: 8 additions & 1 deletion src/cli/init-config/normalize-init-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ const {
hasTestsWithinSource,
toSourceLocationArray,
} = require("./environment-helpers");
const findExtensions = require("./find-extensions.js");

/**
*
* @param {import("./types").IPartialInitConfig} pInitOptions
* @return {import("./types").IPartialInitConfig}
*/
function populate(pInitOptions) {
return {
const lReturnValue = {
version,
date: new Date().toJSON(),
configType: "self-contained",
Expand All @@ -27,6 +28,12 @@ function populate(pInitOptions) {
pInitOptions.testLocation || getTestFolderCandidates()
),
};
if (lReturnValue.specifyResolutionExtensions) {
lReturnValue.resolutionExtensions = findExtensions(
lReturnValue.sourceLocation.concat(lReturnValue.testLocation)
);
}
return lReturnValue;
}

/**
Expand Down
Loading

0 comments on commit 76508e3

Please sign in to comment.