Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(init): adds extensions detection to one shot configs #712

Merged
merged 1 commit into from Dec 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 7 additions & 2 deletions .dependency-cruiser.json
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
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
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
@@ -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
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
@@ -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
@@ -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
@@ -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
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
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