diff --git a/cypher/External_Dependencies/Label_external_types_and_annotations.cypher b/cypher/External_Dependencies/Label_external_types_and_annotations.cypher index 13dbc6ddf..72ff76478 100644 --- a/cypher/External_Dependencies/Label_external_types_and_annotations.cypher +++ b/cypher/External_Dependencies/Label_external_types_and_annotations.cypher @@ -1,6 +1,6 @@ // Label external types and external annotations. Requires 'Label_base_java_types', 'Label_buildin_java_types' and 'Label_resolved_duplicate_types' of 'Types' directory. - MATCH (type:Type&!PrimitiveType&!Void&!JavaType&!ResolvedDuplicateType) + MATCH (type:Type&!PrimitiveType&!Void&!JavaType&!ResolvedDuplicateType&!TS) WITH type ,type.byteCodeVersion IS NULL AS isExternalType ,exists((type)<-[:OF_TYPE]-()<-[:ANNOTATED_BY]-()) AS isAnnotation diff --git a/cypher/Typescript_Enrichment/Add_RESOLVES_TO_relationship_for_matching_modules.cypher b/cypher/Typescript_Enrichment/Add_RESOLVES_TO_relationship_for_matching_modules.cypher index fe44b5910..8a85e88c5 100644 --- a/cypher/Typescript_Enrichment/Add_RESOLVES_TO_relationship_for_matching_modules.cypher +++ b/cypher/Typescript_Enrichment/Add_RESOLVES_TO_relationship_for_matching_modules.cypher @@ -3,21 +3,66 @@ // Inspired by https://github.com/jQAssistant/jqassistant/blob/4cd7face5d6d2953449d8e6ff5b484f00ffbdc2f/plugin/java/src/main/resources/META-INF/jqassistant-rules/java-classpath.xml#L5 // Related to https://github.com/jqassistant-plugin/jqassistant-typescript-plugin/issues/35 -MATCH (module:TS:Module) -WHERE module.globalFqn IS NOT NULL +MATCH (module:TS:Module)<-[:CONTAINS]-(package:TS:Project) +WHERE module.globalFqn IS NOT NULL + AND EXISTS { (module)-[:EXPORTS]->(:TS) } // only when module exports something MATCH (externalModule:TS:ExternalModule) WHERE module.globalFqn IS NOT NULL - AND ((module.globalFqn = externalModule.globalFqn) - OR (module.module = externalModule.module) - OR ( externalModule.name = module.name - AND externalModule.moduleName = module.moduleName - AND externalModule.namespace = module.namespace - AND externalModule.extensionExtended = module.extensionExtended - AND externalModule.globalFqn ENDS WITH module.localModulePath - ) - ) - AND module <> externalModule + AND externalModule <> module + AND externalModule.name = module.name // Base requirement: Same module name + AND EXISTS { (externalModule)-[:EXPORTS]->(:TS:ExternalDeclaration)<-[]-(used:TS) } // only when external declarations are used + WITH *, size(externalModule.extensionExtended) AS externalExtensionSize + WITH *, CASE externalModule.extensionExtended + WHEN ENDS WITH '.js' THEN left(externalModule.extensionExtended, externalExtensionSize - 3) + module.extension + WHEN ENDS WITH 'd.ts' THEN left(externalModule.extensionExtended, externalExtensionSize - 4) + module.extension + ELSE externalModule.extensionExtended + END AS normalizedExternalExtension + WITH * + // Find internal and external modules with identical "globalFqn" + ,(module.globalFqn = externalModule.globalFqn) AS equalGlobalFqn + // Find internal and external modules with identical "module" + ,(module.module = externalModule.module) AS equalModule + // Find matching internal and external modules within the same package and namespace + ,( externalModule.namespace > '' + AND externalModule.namespace = module.namespace + AND externalModule.packageName = package.name + AND normalizedExternalExtension = module.extensionExtended + AND externalModule.globalFqn ENDS WITH module.localModulePath + ) AS equalNameAndNamespace + // Find matching internal and external module without a namespace with matching local module path + ,( module.namespace = '' + AND externalModule.namespace = '' + AND normalizedExternalExtension = module.extensionExtended + AND externalModule.globalFqn ENDS WITH + replace(module.localModulePath, '/' + module.moduleName, '/' + externalModule.moduleName) + ) AS equalNameWithoutNamespace + // Find matching module name, npm package name and npm package namespace + ,( externalModule.namespace = module.namespace + AND externalModule.packageName = module.packageName + AND externalModule.packageName > '' + AND normalizedExternalExtension = module.extensionExtended + ) AS equalNameAndNpmPackage +WHERE equalGlobalFqn + OR equalModule + OR equalNameAndNamespace + OR equalNameWithoutNamespace + OR equalNameAndNpmPackage CALL { WITH module, externalModule MERGE (externalModule)-[:RESOLVES_TO]->(module) } IN TRANSACTIONS -RETURN count(*) AS resolvedModules \ No newline at end of file +RETURN CASE WHEN equalGlobalFqn THEN 'equalGlobalFqn' + WHEN equalModule THEN 'equalModule' + WHEN equalNameWithoutNamespace THEN 'equalNameWithoutNamespace' + WHEN equalNameAndNamespace THEN 'equalNameAndNamespace' + WHEN equalNameAndNpmPackage THEN 'equalNameAndNpmPackage' + END AS equality + ,count(*) AS resolvedModules + ,collect(externalModule.globalFqn + ' -> ' + module.globalFqn)[0..4] AS examples +// Debugging +// RETURN module.globalFqn ,externalModule.globalFqn +// ,module.name ,externalModule.name +// ,module.moduleName ,externalModule.moduleName +// ,module.namespace ,externalModule.namespace +// ,module.extensionExtended, externalModule.extensionExtended +// ,module.localModulePath +// ,split(module.module, '/')[-2] AS moduleDirectory \ No newline at end of file diff --git a/cypher/Typescript_Enrichment/Add_module_properties.cypher b/cypher/Typescript_Enrichment/Add_module_properties.cypher index efac4a6da..ac52753c0 100644 --- a/cypher/Typescript_Enrichment/Add_module_properties.cypher +++ b/cypher/Typescript_Enrichment/Add_module_properties.cypher @@ -17,7 +17,8 @@ OPTIONAL MATCH (class:TS:Class)-[:DECLARES]->(ts) ,nullif(reverse(split(reverse(moduleName), '.')[0]), moduleName) AS moduleNameExtension ,coalesce('@' + nullif(namespaceName, ''), '') AS namespaceNameWithAtPrefixed ,replace(symbolName, coalesce(optionalClassName + '.', ''), '') AS symbolNameWithoutClassName - SET ts.namespace = namespaceNameWithAtPrefixed + ,coalesce(split(split(ts.globalFqn, nullif(namespaceName, '') + '/')[1], '/')[0], '') AS packageName + SET ts.namespace = coalesce(nullif(namespaceNameWithAtPrefixed, ''), ts.namespace, '') ,ts.module = modulePathNameWithoutIndexAndDefault ,ts.moduleName = moduleName ,ts.name = coalesce(symbolNameWithoutClassName, indexAndExtensionOmittedName) @@ -26,6 +27,7 @@ OPTIONAL MATCH (class:TS:Class)-[:DECLARES]->(ts) ,ts.isNodeModule = isNodeModule ,ts.isUnresolvedImport = isUnresolvedImport ,ts.isExternalImport = isNodeModule OR isUnresolvedImport + ,ts.packageName = packageName RETURN count(*) AS updatedModules // For debugging // RETURN namespaceNameWithAtPrefixed AS namespace diff --git a/cypher/Typescript_Enrichment/Add_name_to_property_on_projects.cypher b/cypher/Typescript_Enrichment/Add_name_to_property_on_projects.cypher index d354e19a2..af3683a50 100644 --- a/cypher/Typescript_Enrichment/Add_name_to_property_on_projects.cypher +++ b/cypher/Typescript_Enrichment/Add_name_to_property_on_projects.cypher @@ -3,11 +3,13 @@ MATCH (project:TS:Project)-[:HAS_ROOT]->(root:Directory) OPTIONAL MATCH (project)-[:HAS_CONFIG]->(config:File)<-[:CONTAINS]-(config_dir:Directory) WITH * - ,reverse(split(reverse(root.absoluteFileName), '/')[0]) AS projectNameFromRoot - ,reverse(split(reverse(config_dir.absoluteFileName), '/')[0]) AS projectNameFromConfig + ,reverse(split(reverse(root.absoluteFileName), '/')[0]) AS projectNameFromRoot + ,reverse(split(reverse(config_dir.absoluteFileName), '/')[0]) AS projectNameFromConfig + ,nullif(substring(replace(config.name, 'tsconfig', ''), 1), '') AS projectAddOnFromConfig WITH * ,projectNameFromRoot + '/' + - nullif(projectNameFromConfig, projectNameFromRoot) AS projectNameWithDifferentConfigIfPresent + nullif(projectNameFromConfig, projectNameFromRoot) + + coalesce(' (' + projectAddOnFromConfig + ')', '') AS projectNameWithDifferentConfigIfPresent WITH * ,coalesce(projectNameWithDifferentConfigIfPresent, projectNameFromRoot) AS projectName SET project.name = projectName @@ -15,7 +17,7 @@ RETURN count(*) AS numberOfNamesProjects // For debugging //RETURN projectNameFromRoot // ,projectNameFromConfig -// ,projectNameWithDifferentConfig +// ,projectNameWithDifferentConfigIfPresent // ,projectName // ,project, root, config //LIMIT 10 \ No newline at end of file diff --git a/cypher/Typescript_Enrichment/Add_namespace_property_on_nodes_from_linked_npm_packages.cypher b/cypher/Typescript_Enrichment/Add_namespace_property_on_nodes_from_linked_npm_packages.cypher new file mode 100644 index 000000000..1b288b3c7 --- /dev/null +++ b/cypher/Typescript_Enrichment/Add_namespace_property_on_nodes_from_linked_npm_packages.cypher @@ -0,0 +1,16 @@ +// Add namespace property to Typescript nodes if a npm a package is linked. Requires Link_projects_to_npm_packages. + +MATCH (element:TS)<-[:CONTAINS]-(project:TS:Project)-[:HAS_NPM_PACKAGE]->(package:NPM:Package) +WHERE element.globalFqn IS NOT NULL + WITH * + ,coalesce(nullif(split(package.name, '/')[0], package.name), '') AS npmPackageNamespace + ,coalesce(split(package.name, '/')[1], package.name) AS packageName + SET element.namespace = coalesce(nullif(element.namespace, ''), npmPackageNamespace) + SET element.packageName = coalesce(nullif(element.packageName, ''), packageName) +RETURN labels(element)[0..4] AS nodeLabels, count(*) AS numberOfWrittenNamespaceProperties +// Debugging +//RETURN element.globalFqn, element.moduleName, element.namespace +// ,package.name, package.fileName +// ,npmPackageNamespace +// ,packageName +//LIMIT 10 \ No newline at end of file diff --git a/cypher/Typescript_Enrichment/Link_npm_dependencies_to_npm_packages.cypher b/cypher/Typescript_Enrichment/Link_npm_dependencies_to_npm_packages.cypher new file mode 100644 index 000000000..080b0fb93 --- /dev/null +++ b/cypher/Typescript_Enrichment/Link_npm_dependencies_to_npm_packages.cypher @@ -0,0 +1,15 @@ +// Link npm dependencies to the npm package that describe them if it exists + +MATCH (npm_dependency:NPM:Dependency) +MATCH (npm_package:NPM:Package) +WHERE npm_package.name = npm_dependency.name + AND npm_package <> npm_dependency + CALL { WITH npm_package, npm_dependency + MERGE (npm_dependency)-[:IS_DESCRIBED_IN_NPM_PACKAGE]->(npm_package) + } IN TRANSACTIONS +RETURN count(*) AS numberOfWrittenRelationships + ,count(DISTINCT npm_dependency) AS numberOfDistinctNpmDependencies + ,count(DISTINCT npm_package) AS numberOfDistinctNpmPackages +// Debugging +// RETURN npm_dependency, npm_package +// LIMIT 1 \ No newline at end of file diff --git a/cypher/Typescript_Enrichment/Link_projects_to_npm_packages.cypher b/cypher/Typescript_Enrichment/Link_projects_to_npm_packages.cypher index 1c6ac30f0..3b93b7948 100644 --- a/cypher/Typescript_Enrichment/Link_projects_to_npm_packages.cypher +++ b/cypher/Typescript_Enrichment/Link_projects_to_npm_packages.cypher @@ -24,7 +24,7 @@ MATCH (npmPackage:NPM:Package) MERGE (project)-[:HAS_NPM_PACKAGE]->(npmPackage) // Set the "relativeFileDirectory" on the npm package to the relative directory // that contains the package.json file - SET npmPackage.relativeFileDirectory = relativeNpmPackageDirectory + SET npmPackage.relativeFileDirectory = ltrim(relativeNpmPackageDirectory, '/') ,project.version = npmPackage.version RETURN count(*) AS numberOfCreatedNpmPackageRelationships // Detailed results for debugging diff --git a/scripts/prepareAnalysis.sh b/scripts/prepareAnalysis.sh index 11b9ad028..38d1d1034 100644 --- a/scripts/prepareAnalysis.sh +++ b/scripts/prepareAnalysis.sh @@ -72,15 +72,11 @@ execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Mark_test_modules.cypher" execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_name_to_property_on_projects.cypher" execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_name_to_property_on_scan_nodes.cypher" -# Preparation - Enrich Graph for Typescript by adding relationships between Modules with the same globalFqn -execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_RESOLVES_TO_relationship_for_matching_modules.cypher" -execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_RESOLVES_TO_relationship_for_matching_declarations.cypher" -execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_DEPENDS_ON_relationship_to_resolved_modules.cypher" - # Preparation - Cleanup Graph for Typescript by removing duplicate relationships execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Remove_duplicate_CONTAINS_relations_between_files.cypher" # Preparation - Enrich Graph for Typescript by adding relationships between corresponding TS:Project and NPM:Package nodes +execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Link_npm_dependencies_to_npm_packages.cypher" execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Link_projects_to_npm_packages.cypher" dataVerificationResult=$( execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Verify_projects_linked_to_npm_packages.cypher" "${@}") if is_csv_column_greater_zero "${dataVerificationResult}" "unresolvedProjectsCount"; then @@ -91,6 +87,12 @@ if is_csv_column_greater_zero "${dataVerificationResult}" "unresolvedProjectsCou # Since this is now only a warning, execution will be continued. fi execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Link_external_modules_to_corresponding_npm_dependency.cypher" +execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_namespace_property_on_nodes_from_linked_npm_packages.cypher" + +# Preparation - Enrich Graph for Typescript by adding relationships between Modules with the same globalFqn +execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_RESOLVES_TO_relationship_for_matching_modules.cypher" +execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_RESOLVES_TO_relationship_for_matching_declarations.cypher" +execute_cypher "${TYPESCRIPT_CYPHER_DIR}/Add_DEPENDS_ON_relationship_to_resolved_modules.cypher" # Preparation - Add weights to Java Package DEPENDS_ON relationships execute_cypher_summarized "${DEPENDS_ON_CYPHER_DIR}/Add_weight_property_for_Java_Interface_Dependencies_to_Package_DEPENDS_ON_Relationship.cypher" diff --git a/scripts/scanTypescript.sh b/scripts/scanTypescript.sh index 009049106..7fbd81ab2 100755 --- a/scripts/scanTypescript.sh +++ b/scripts/scanTypescript.sh @@ -23,12 +23,21 @@ echo "scanTypescript: TYPESCRIPT_SCAN_HEAP_MEMORY=${TYPESCRIPT_SCAN_HEAP_MEMORY} SCRIPTS_DIR=${SCRIPTS_DIR:-$( CDPATH=. cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P )} # Repository directory containing the shell scripts echo "scanTypescript: SCRIPTS_DIR=${SCRIPTS_DIR}" >&2 -# Dry run for internal testing (not intended to be accessible from the outside) -TYPESCRIPT_SCAN_DRY_RUN=false +# ------ Switches for internal testing and debugging ------------ +# - Dry run for internal testing (for now not intended to be accessible from the outside) +TYPESCRIPT_SCAN_DRY_RUN=false # Default = false + if [ "${TYPESCRIPT_SCAN_DRY_RUN}" = true ] ; then echo "scanTypescript: -> DRY RUN <- Scanning will only be logged, not executed." >&2 fi +# - Change detection for internal testing (for now not intended to be accessible from the outside) +TYPESCRIPT_SCAN_CHANGE_DETECTION=true # Default = true +if [ "${TYPESCRIPT_SCAN_CHANGE_DETECTION}" = false ] ; then + echo "scanTypescript: -> CHANGE_DETECTION OFF <- Scanning will also be done for unchanged sources." >&2 +fi +# --------------------------------------------------------------- + if [ ! -d "./${SOURCE_DIRECTORY}" ] ; then echo "scanTypescript: Source directory '${SOURCE_DIRECTORY}' doesn't exist. The scan will therefore be skipped." >&2 return 0 @@ -62,6 +71,8 @@ find_directories_with_package_json_file() { scan_directory() { local source_directory_name; source_directory_name=$(basename "${1}"); local progress_information; progress_information="${2}" + local COLOR_DARK_GREY='\033[0;30m' + local COLOR_DEFAULT='\033[0m' echo "" >&2 # Output an empty line to have a clearer separation between each scan @@ -70,7 +81,9 @@ scan_directory() { # Note: For later troubleshooting, the output is also copied to a dedicated log file using "tee". # Note: Don't worry about the hardcoded version number. It will be updated by Renovate using a custom Manager. # Note: NODE_OPTIONS --max-old-space-size=4096 increases the memory for scanning larger projects - NODE_OPTIONS="${NODE_OPTIONS} --max-old-space-size=${TYPESCRIPT_SCAN_HEAP_MEMORY}" npx --yes @jqassistant/ts-lce@1.3.1 "${1}" --extension React 2>&1 | tee "${LOG_DIRECTORY}/jqassistant-typescript-scan-${directory_name}.log" >&2 + echo -e "${COLOR_DARK_GREY}" + NODE_OPTIONS="${NODE_OPTIONS} --max-old-space-size=${TYPESCRIPT_SCAN_HEAP_MEMORY}" npx --yes @jqassistant/ts-lce@1.3.0 "${1}" --extension React 2>&1 | tee "${LOG_DIRECTORY}/jqassistant-typescript-scan-${source_directory_name}.log" >&2 + echo -e "${COLOR_DEFAULT}" else echo "scanTypescript: Skipping scan of ${source_directory_name} (${progress_information}) -----------------" >&2 fi @@ -101,47 +114,66 @@ is_valid_scan_result() { fi } -# Scan and analyze Artifacts when they were changed -changeDetectionHashFilePath="./${SOURCE_DIRECTORY}/typescriptFileChangeDetectionHashFile.txt" -changeDetectionReturnCode=$( source "${SCRIPTS_DIR}/detectChangedFiles.sh" --readonly --hashfile "${changeDetectionHashFilePath}" --paths "./${SOURCE_DIRECTORY}") +is_change_detected() { + # Scan and analyze Typescript sources only when they had been changed + local source_directory_name; source_directory_name=$(basename "${source_directory}"); + changeDetectionHashFilePath="./${SOURCE_DIRECTORY}/typescriptScanChangeDetection-${source_directory_name}.sha" + changeDetectionReturnCode=$( source "${SCRIPTS_DIR}/detectChangedFiles.sh" --readonly --hashfile "${changeDetectionHashFilePath}" --paths "${source_directory}") -if [ "${changeDetectionReturnCode}" == "0" ]; then - echo "scanTypescript: Files unchanged. Scan skipped." -fi - -if [ "${changeDetectionReturnCode}" != "0" ] || [ "${TYPESCRIPT_SCAN_DRY_RUN}" = true ]; then - echo "scanTypescript: Detected change (${changeDetectionReturnCode}). Scanning Typescript source using @jqassistant/ts-lce." - - mkdir -p "./runtime/logs" - LOG_DIRECTORY="$(pwd)/runtime/logs" - echo "scanTypescript: LOG_DIRECTORY=${LOG_DIRECTORY}" >&2 - - source_directories=$( find -L "./${SOURCE_DIRECTORY}" -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -r -I {} echo {} ) - total_source_directories=$(echo "${source_directories}" | wc -l | awk '{print $1}') - processed_source_directories=0 - - for source_directory in ${source_directories}; do - processed_source_directories=$((processed_source_directories + 1)) - progress_info_source_dirs="${processed_source_directories}/${total_source_directories}" - if scan_directory "${source_directory}" "${progress_info_source_dirs}" && is_valid_scan_result "${source_directory}"; then - continue # successful scan, proceed to next one. - fi - - echo "scanTypescript: Info: Unsuccessful source directory scan. Trying to scan all contained packages individually." >&2 - contained_package_directories=$( find_directories_with_package_json_file "${source_directory}" ) - echo "scanTypescript: contained_package_directories:" >&2 - echo "${contained_package_directories}" >&2 - total_package_directories=$(echo "${contained_package_directories}" | wc -l | awk '{print $1}') - processed_package_directories=0 - - for contained_package_directory in ${contained_package_directories}; do - processed_package_directories=$((processed_package_directories + 1)) - progress_info_package_dirs="${progress_info_source_dirs}: ${processed_package_directories}/${total_package_directories}" - scan_directory "${contained_package_directory}" "${progress_info_package_dirs}" - done - done + if [ "${changeDetectionReturnCode}" == "0" ] && [ "${TYPESCRIPT_SCAN_CHANGE_DETECTION}" = true ]; then + true + else + false + fi +} +write_change_detection_file() { + # The dry-run shouldn't write anything. Therefore, writing the change detection file is skipped regardless of TYPESCRIPT_SCAN_CHANGE_DETECTION. if [ "${TYPESCRIPT_SCAN_DRY_RUN}" = false ] ; then - changeDetectionReturnCode=$( source "${SCRIPTS_DIR}/detectChangedFiles.sh" --hashfile "${changeDetectionHashFilePath}" --paths "./${SOURCE_DIRECTORY}") + changeDetectionReturnCode=$( source "${SCRIPTS_DIR}/detectChangedFiles.sh" --hashfile "${changeDetectionHashFilePath}" --paths "${source_directory}") + fi +} + +mkdir -p "./runtime/logs" +LOG_DIRECTORY="$(pwd)/runtime/logs" +echo "scanTypescript: LOG_DIRECTORY=${LOG_DIRECTORY}" >&2 + +source_directories=$( find -L "./${SOURCE_DIRECTORY}" -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -r -I {} echo {} ) +total_source_directories=$(echo "${source_directories}" | wc -l | awk '{print $1}') +processed_source_directories=0 + +for source_directory in ${source_directories}; do + if is_change_detected; then + echo "scanTypescript: Files in ${source_directory} unchanged. Scan skipped." + continue # skipping scan since it had already be done according to change detection. fi -fi \ No newline at end of file + + #Debugging log for change detection. "scan_directory" already logs scanning and the source directory. + #echo "scanTypescript: Detected change (${changeDetectionReturnCode}) in ${source_directory}. Scanning Typescript source using @jqassistant/ts-lce." + + processed_source_directories=$((processed_source_directories + 1)) + progress_info_source_dirs="${processed_source_directories}/${total_source_directories}" + + if [ -f "${source_directory}/tsconfig.json" ] \ + && scan_directory "${source_directory}" "${progress_info_source_dirs}" \ + && is_valid_scan_result "${source_directory}" + then + write_change_detection_file + continue # successfully scanned a standard Typescript project (with tsconfig.json file). proceed with next one. + fi + + echo "scanTypescript: Info: Unsuccessful or skipped source directory scan. Scan all contained packages individually." >&2 + contained_package_directories=$( find_directories_with_package_json_file "${source_directory}" ) + echo "scanTypescript: contained_package_directories:" >&2 + echo "${contained_package_directories}" >&2 + total_package_directories=$(echo "${contained_package_directories}" | wc -l | awk '{print $1}') + processed_package_directories=0 + + for contained_package_directory in ${contained_package_directories}; do + processed_package_directories=$((processed_package_directories + 1)) + progress_info_package_dirs="${progress_info_source_dirs}: ${processed_package_directories}/${total_package_directories}" + scan_directory "${contained_package_directory}" "${progress_info_package_dirs}" + done + + write_change_detection_file +done