From 03495018220a64aca0af1f75e791bc9cc9ef410c Mon Sep 17 00:00:00 2001 From: aabhinavvvvvvv Date: Fri, 20 Mar 2026 03:13:02 +0000 Subject: [PATCH 1/4] feat: add JavaScript/JSX asset handler (#362) --- .../CustomNodes/Code/CodeNode.js | 3 + .../DependencyGraph/DependencyGraphEChart.js | 3 + app/constants/assets-config.js | 8 + app/images/js.svg | 4 + app/preload.js | 4 +- app/services/assets/handlers/javascript.js | 182 ++++++++++++++ app/utils/workflow.js | 5 + docs/Assets.md | 20 ++ test/services/assets/asset.spec.js | 18 ++ .../assets/handlers/javascript.spec.js | 226 ++++++++++++++++++ test/utils/workflow.spec.js | 41 ++++ 11 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 app/images/js.svg create mode 100644 app/services/assets/handlers/javascript.js create mode 100644 test/services/assets/handlers/javascript.spec.js diff --git a/app/components/Workflow/DependencyGraph/CustomNodes/Code/CodeNode.js b/app/components/Workflow/DependencyGraph/CustomNodes/Code/CodeNode.js index f879b5b8..f985ec53 100644 --- a/app/components/Workflow/DependencyGraph/CustomNodes/Code/CodeNode.js +++ b/app/components/Workflow/DependencyGraph/CustomNodes/Code/CodeNode.js @@ -9,6 +9,7 @@ const ICON_TYPES = { R: `${ICON_PATH}r.svg`, SAS: `${ICON_PATH}sas.svg`, STATA: `${ICON_PATH}stata.png`, + JAVASCRIPT: `${ICON_PATH}js.svg`, GENERIC: `${ICON_PATH}generic.svg`, LIBRARY: `${ICON_PATH}library.svg`, DATA: `${ICON_PATH}data.svg`, @@ -31,6 +32,8 @@ function CodeNode({ node, renderType }) { iconUrl = ICON_TYPES.SAS; } else if (node.assetType === 'stata') { iconUrl = ICON_TYPES.STATA; + } else if (node.assetType === 'javascript') { + iconUrl = ICON_TYPES.JAVASCRIPT; } else if (node.assetType === 'dependency') { iconUrl = ICON_TYPES.LIBRARY; } else if (node.assetType === 'sql') { diff --git a/app/components/Workflow/DependencyGraph/DependencyGraphEChart.js b/app/components/Workflow/DependencyGraph/DependencyGraphEChart.js index 5f9fa788..e56b1f71 100644 --- a/app/components/Workflow/DependencyGraph/DependencyGraphEChart.js +++ b/app/components/Workflow/DependencyGraph/DependencyGraphEChart.js @@ -15,6 +15,7 @@ const ICON_TYPES = { R: `${ICON_PATH}r.svg`, SAS: `${ICON_PATH}sas.svg`, STATA: `${ICON_PATH}stata.png`, + JAVASCRIPT: `${ICON_PATH}js.svg`, GENERIC: `${ICON_PATH}generic.svg`, LIBRARY: `${ICON_PATH}library.svg`, DATA: `${ICON_PATH}data.svg`, @@ -38,6 +39,8 @@ function getIcon(node) { iconUrl = ICON_TYPES.SAS; } else if (node.value === 'stata') { iconUrl = ICON_TYPES.STATA; + } else if (node.value === 'javascript') { + iconUrl = ICON_TYPES.JAVASCRIPT; } else if (node.value === 'dependency') { iconUrl = ICON_TYPES.LIBRARY; } else if (node.value === 'sql') { diff --git a/app/constants/assets-config.js b/app/constants/assets-config.js index e9c7c8fc..150ded15 100644 --- a/app/constants/assets-config.js +++ b/app/constants/assets-config.js @@ -57,10 +57,18 @@ module.exports = { { name: 'SQL', extensions: ['sql'], + categories: ['code'], + }, + { name: 'Go', extensions: ['go'], categories: ['code'], }, + { + name: 'JavaScript', + extensions: ['js', 'jsx', 'mjs', 'cjs'], + categories: ['code'], + }, { name: 'Text Data File', extensions: ['csv', 'tsv'], diff --git a/app/images/js.svg b/app/images/js.svg new file mode 100644 index 00000000..f3a120d4 --- /dev/null +++ b/app/images/js.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/preload.js b/app/preload.js index 75758033..0b7d7ec6 100644 --- a/app/preload.js +++ b/app/preload.js @@ -15,8 +15,9 @@ import AssetUtil from './utils/asset'; import ProjectUtil from './utils/project'; import JavaHandler from './services/assets/handlers/java'; import GoHandler from './services/assets/handlers/go'; -import Constants from './constants/constants'; import SQLHandler from './services/assets/handlers/sql'; +import JavaScriptHandler from './services/assets/handlers/javascript'; +import Constants from './constants/constants'; const projectService = new ProjectService(); const projectListService = new ProjectListService(); @@ -58,6 +59,7 @@ contextBridge.exposeInMainWorld('workerElectronBridge', { new JavaHandler(), new SQLHandler(), new GoHandler(), + new JavaScriptHandler(), ]); response.assets = service.scan(project.path); // Returns absolute paths diff --git a/app/services/assets/handlers/javascript.js b/app/services/assets/handlers/javascript.js new file mode 100644 index 00000000..be87fe2c --- /dev/null +++ b/app/services/assets/handlers/javascript.js @@ -0,0 +1,182 @@ +import BaseCodeHandler from './baseCode'; +import Constants from '../../../constants/constants'; + +// JavaScript file extensions that we will scan. +// All lookups should be lowercase - we will do lowercase conversion before comparison. +const FILE_EXTENSION_LIST = ['js', 'jsx', 'mjs', 'cjs']; + +export default class JavaScriptHandler extends BaseCodeHandler { + static id = 'StatWrap.JavaScriptHandler'; + + constructor() { + super(JavaScriptHandler.id, FILE_EXTENSION_LIST); + } + + id() { + return JavaScriptHandler.id; + } + + getLibraryId(moduleName, importName) { + let id = ''; + if (moduleName && importName) { + id = `${moduleName}.${importName}`; + } else if (moduleName) { + id = moduleName; + } else if (importName) { + id = importName; + } else { + id = '(unknown)'; + } + return id; + } + + getInputs(uri, text) { + const inputs = []; + if (!text || text.trim() === '') { + return inputs; + } + + const processedPaths = new Set(); + + // fs.readFileSync('file') and fs.readFile('file', ...) + const readMatches = [ + ...text.matchAll(/fs\.(?:readFileSync|readFile)\s*\(\s*(['"]{1,}\s*?[\s\S]+?['"]{1,})[\s\S]*?\)/gim), + ]; + for (let index = 0; index < readMatches.length; index++) { + const match = readMatches[index]; + const filePath = match[1].trim(); + if (!processedPaths.has(filePath)) { + inputs.push({ + id: `readFile - ${filePath}`, + type: Constants.DependencyType.DATA, + path: filePath, + }); + processedPaths.add(filePath); + } + } + + // fs.createReadStream('file') + const streamMatches = [ + ...text.matchAll(/fs\.createReadStream\s*\(\s*(['"]{1,}\s*?[\s\S]+?['"]{1,})[\s\S]*?\)/gim), + ]; + for (let index = 0; index < streamMatches.length; index++) { + const match = streamMatches[index]; + const filePath = match[1].trim(); + if (!processedPaths.has(filePath)) { + inputs.push({ + id: `createReadStream - ${filePath}`, + type: Constants.DependencyType.DATA, + path: filePath, + }); + processedPaths.add(filePath); + } + } + + return inputs; + } + + getOutputs(uri, text) { + const outputs = []; + if (!text || text.trim() === '') { + return outputs; + } + + const processedPaths = new Set(); + + // fs.writeFileSync('file') and fs.writeFile('file', ...) + const writeMatches = [ + ...text.matchAll(/fs\.(?:writeFileSync|writeFile|appendFile|appendFileSync)\s*\(\s*(['"]{1,}\s*?[\s\S]+?['"]{1,})[\s\S]*?\)/gim), + ]; + for (let index = 0; index < writeMatches.length; index++) { + const match = writeMatches[index]; + const filePath = match[1].trim(); + if (!processedPaths.has(filePath)) { + outputs.push({ + id: `writeFile - ${filePath}`, + type: Constants.DependencyType.DATA, + path: filePath, + }); + processedPaths.add(filePath); + } + } + + // fs.createWriteStream('file') + const streamMatches = [ + ...text.matchAll(/fs\.createWriteStream\s*\(\s*(['"]{1,}\s*?[\s\S]+?['"]{1,})[\s\S]*?\)/gim), + ]; + for (let index = 0; index < streamMatches.length; index++) { + const match = streamMatches[index]; + const filePath = match[1].trim(); + if (!processedPaths.has(filePath)) { + outputs.push({ + id: `createWriteStream - ${filePath}`, + type: Constants.DependencyType.DATA, + path: filePath, + }); + processedPaths.add(filePath); + } + } + + return outputs; + } + + getLibraries(uri, text) { + const libraries = []; + if (!text || text.trim() === '') { + return libraries; + } + + // ES module imports: import x from 'module', import { x } from 'module', import * as x from 'module' + const esImportMatches = [ + ...text.matchAll(/^[^/]*?import\s+(?:(\*\s+as\s+\w+|\{[^}]*\}|\w+))\s+from\s+(['"]{1,}[\s\S]+?['"]{1,})/gim), + ]; + for (let index = 0; index < esImportMatches.length; index++) { + const match = esImportMatches[index]; + const importName = match[1].trim(); + const moduleName = match[2].trim().replace(/['"]/g, ''); + libraries.push({ + id: this.getLibraryId(moduleName, importName), + module: moduleName, + import: importName, + alias: null, + }); + } + + // Side-effect imports: import 'module' + const sideEffectMatches = [ + ...text.matchAll(/^[^/]*?import\s+(['"]{1,}[\s\S]+?['"]{1,})/gim), + ]; + for (let index = 0; index < sideEffectMatches.length; index++) { + const match = sideEffectMatches[index]; + const moduleName = match[1].trim().replace(/['"]/g, ''); + libraries.push({ + id: moduleName, + module: moduleName, + import: null, + alias: null, + }); + } + + // CommonJS require: require('module') or const x = require('module') + const requireMatches = [ + ...text.matchAll(/(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*require\s*\(\s*(['"]{1,}[\s\S]+?['"]{1,})\s*\)/gim), + ...text.matchAll(/require\s*\(\s*(['"]{1,}[\s\S]+?['"]{1,})\s*\)/gim), + ]; + const seenModules = new Set(libraries.map((l) => l.module)); + for (let index = 0; index < requireMatches.length; index++) { + const match = requireMatches[index]; + const moduleName = match[1].trim().replace(/['"]/g, ''); + if (!seenModules.has(moduleName)) { + libraries.push({ + id: moduleName, + module: moduleName, + import: null, + alias: null, + }); + seenModules.add(moduleName); + } + } + + return libraries; + } +} diff --git a/app/utils/workflow.js b/app/utils/workflow.js index 4a486f0e..71c71b84 100644 --- a/app/utils/workflow.js +++ b/app/utils/workflow.js @@ -7,6 +7,7 @@ import Constants from '../constants/constants'; import JavaHandler from '../services/assets/handlers/java'; import SQLHandler from '../services/assets/handlers/sql'; import GoHandler from '../services/assets/handlers/go'; +import JavaScriptHandler from '../services/assets/handlers/javascript'; import path from 'path'; export default class WorkflowUtil { @@ -58,6 +59,8 @@ export default class WorkflowUtil { assetType = 'sql'; } else if (AssetUtil.getHandlerMetadata(GoHandler.id, asset.metadata)) { assetType = 'go'; + } else if (AssetUtil.getHandlerMetadata(JavaScriptHandler.id, asset.metadata)) { + assetType = 'javascript'; } return assetType; } @@ -350,6 +353,7 @@ export default class WorkflowUtil { WorkflowUtil._getMetadataDependencies(asset, JavaHandler.id, libraries, inputs, outputs); WorkflowUtil._getMetadataDependencies(asset, SQLHandler.id, libraries, inputs, outputs); WorkflowUtil._getMetadataDependencies(asset, GoHandler.id, libraries, inputs, outputs); + WorkflowUtil._getMetadataDependencies(asset, JavaScriptHandler.id, libraries, inputs, outputs); return libraries .map((e) => { @@ -415,6 +419,7 @@ export default class WorkflowUtil { WorkflowUtil._getMetadataDependencies(asset, JavaHandler.id, libraries, [], []); WorkflowUtil._getMetadataDependencies(asset, SQLHandler.id, libraries, [], []); WorkflowUtil._getMetadataDependencies(asset, GoHandler.id, libraries, [], []); + WorkflowUtil._getMetadataDependencies(asset, JavaScriptHandler.id, libraries, [], []); return libraries; } diff --git a/docs/Assets.md b/docs/Assets.md index 88499a45..7e160dbb 100644 --- a/docs/Assets.md +++ b/docs/Assets.md @@ -179,6 +179,26 @@ We collect 3 categories of metadata for Stata code files: External programs and plugins, and references to Do files to run via another script. +### JavaScript Code Files + +We collect 3 categories of metadata for JavaScript code files (`.js`, `.jsx`, `.mjs`, `.cjs`): + +**Inputs** + +| Category | Example Functions / Patterns | Data Type | +| ------------------ | ------------------------------------------------------- | --------- | +| Node.js file reads | `fs.readFileSync`, `fs.readFile`, `fs.createReadStream` | `data` | + +**Outputs** + +| Category | Example Functions / Patterns | Data Type | +| ------------------- | ------------------------------------------------------------------------------------------------ | --------- | +| Node.js file writes | `fs.writeFileSync`, `fs.writeFile`, `fs.appendFile`, `fs.appendFileSync`, `fs.createWriteStream` | `data` | + +**Libraries** + +ES module imports (`import x from 'module'`, `import { x } from 'module'`, `import * as x from 'module'`, `import 'module'`) and CommonJS requires (`require('module')`). + ## Asset Groups By default StatWrap mimics the traditional hierarchical file system view. However, we realize that not all assets will be within a single file system (or may not even be files / folders). Also, we want to allow users to establish other groups of assets that make sense to them. Asset Groups will be a way for users to do this, and StatWrap will store these within the [Project](Projects.md) metadata. diff --git a/test/services/assets/asset.spec.js b/test/services/assets/asset.spec.js index ca2059b8..d76e648a 100644 --- a/test/services/assets/asset.spec.js +++ b/test/services/assets/asset.spec.js @@ -319,6 +319,24 @@ describe('services', () => { expect(service.assetContentTypes('folder.do', stat)).toStrictEqual(['other']); }); + it('should identify JavaScript code files as code', () => { + const stat = new fs.Stats(); + stat.isFile.mockReturnValue(true); + const service = new AssetService(); + expect(service.assetContentTypes('test.js', stat)).toStrictEqual(['code']); + expect(service.assetContentTypes('test.jsx', stat)).toStrictEqual(['code']); + expect(service.assetContentTypes('test.mjs', stat)).toStrictEqual(['code']); + expect(service.assetContentTypes('test.cjs', stat)).toStrictEqual(['code']); + expect(service.assetContentTypes('test.JS', stat)).toStrictEqual(['code']); + expect(service.assetContentTypes('test.JSX', stat)).toStrictEqual(['code']); + // False leads... + expect(service.assetContentTypes('test.ts', stat)).toStrictEqual(['other']); + expect(service.assetContentTypes('test.js.bak', stat)).toStrictEqual(['other']); + expect(service.assetContentTypes('.js', stat)).toStrictEqual(['other']); + stat.isFile.mockReturnValue(false); + expect(service.assetContentTypes('folder.js', stat)).toStrictEqual(['other']); + }); + it('should identify Stata data files as data', () => { const stat = new fs.Stats(); stat.isFile.mockReturnValue(true); diff --git a/test/services/assets/handlers/javascript.spec.js b/test/services/assets/handlers/javascript.spec.js new file mode 100644 index 00000000..c310ab72 --- /dev/null +++ b/test/services/assets/handlers/javascript.spec.js @@ -0,0 +1,226 @@ +import fs from 'fs'; +import JavaScriptHandler from '../../../../app/services/assets/handlers/javascript'; +import Constants from '../../../../app/constants/constants'; + +jest.mock('fs'); + +describe('services', () => { + describe('JavaScriptHandler', () => { + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + + describe('id', () => { + it('should return an id that matches the class name plus StatWrap pseudo-namespace', () => { + expect(new JavaScriptHandler().id()).toEqual(`StatWrap.${JavaScriptHandler.name}`); + }); + }); + + describe('includeFile', () => { + it('should include JS files and exclude others', () => { + const handler = new JavaScriptHandler(); + expect(handler.includeFile('/path/to/app.js')).toBeTruthy(); + expect(handler.includeFile('/path/to/component.jsx')).toBeTruthy(); + expect(handler.includeFile('/path/to/module.mjs')).toBeTruthy(); + expect(handler.includeFile('/path/to/module.cjs')).toBeTruthy(); + expect(handler.includeFile('/path/to/app.ts')).toBeFalsy(); + expect(handler.includeFile('/path/to/app.py')).toBeFalsy(); + expect(handler.includeFile(null)).toBeFalsy(); + }); + }); + + describe('getLibraries', () => { + it('should return empty array for empty text', () => { + expect(new JavaScriptHandler().getLibraries('test.uri', '')).toEqual([]); + expect(new JavaScriptHandler().getLibraries('test.uri', null)).toEqual([]); + }); + + it('should detect default ES module imports', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + "import React from 'react';" + ); + expect(libraries.length).toEqual(1); + expect(libraries[0]).toMatchObject({ + module: 'react', + import: 'React', + }); + }); + + it('should detect named ES module imports', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + "import { readFileSync } from 'fs';" + ); + expect(libraries.length).toEqual(1); + expect(libraries[0]).toMatchObject({ + module: 'fs', + import: '{ readFileSync }', + }); + }); + + it('should detect namespace ES module imports', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + "import * as path from 'path';" + ); + expect(libraries.length).toEqual(1); + expect(libraries[0]).toMatchObject({ + module: 'path', + }); + }); + + it('should detect CommonJS require', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + "const fs = require('fs');" + ); + expect(libraries.length).toEqual(1); + expect(libraries[0]).toMatchObject({ + module: 'fs', + }); + }); + + it('should detect destructured require', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + "const { join, resolve } = require('path');" + ); + expect(libraries.length).toEqual(1); + expect(libraries[0]).toMatchObject({ + module: 'path', + }); + }); + }); + + describe('getInputs', () => { + it('should return empty array for empty text', () => { + expect(new JavaScriptHandler().getInputs('test.uri', '')).toEqual([]); + expect(new JavaScriptHandler().getInputs('test.uri', null)).toEqual([]); + }); + + it('should detect fs.readFileSync', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + "const data = fs.readFileSync('input.csv', 'utf8');" + ); + expect(inputs.length).toEqual(1); + expect(inputs[0]).toMatchObject({ + id: "readFile - 'input.csv'", + type: Constants.DependencyType.DATA, + path: "'input.csv'", + }); + }); + + it('should detect fs.readFile', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + "fs.readFile('data.json', callback);" + ); + expect(inputs.length).toEqual(1); + expect(inputs[0]).toMatchObject({ + type: Constants.DependencyType.DATA, + }); + }); + + it('should detect fs.createReadStream', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + "const stream = fs.createReadStream('input.csv');" + ); + expect(inputs.length).toEqual(1); + expect(inputs[0]).toMatchObject({ + id: "createReadStream - 'input.csv'", + type: Constants.DependencyType.DATA, + }); + }); + + it('should not duplicate the same path', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + "fs.readFileSync('data.csv');\nfs.readFileSync('data.csv');" + ); + expect(inputs.length).toEqual(1); + }); + }); + + describe('getOutputs', () => { + it('should return empty array for empty text', () => { + expect(new JavaScriptHandler().getOutputs('test.uri', '')).toEqual([]); + expect(new JavaScriptHandler().getOutputs('test.uri', null)).toEqual([]); + }); + + it('should detect fs.writeFileSync', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "fs.writeFileSync('output.csv', data);" + ); + expect(outputs.length).toEqual(1); + expect(outputs[0]).toMatchObject({ + id: "writeFile - 'output.csv'", + type: Constants.DependencyType.DATA, + path: "'output.csv'", + }); + }); + + it('should detect fs.writeFile', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "fs.writeFile('result.json', data, callback);" + ); + expect(outputs.length).toEqual(1); + expect(outputs[0]).toMatchObject({ + type: Constants.DependencyType.DATA, + }); + }); + + it('should detect fs.appendFile and fs.appendFileSync', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "fs.appendFile('log.txt', entry, callback);\nfs.appendFileSync('log2.txt', entry);" + ); + expect(outputs.length).toEqual(2); + }); + + it('should detect fs.createWriteStream', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "const stream = fs.createWriteStream('output.csv');" + ); + expect(outputs.length).toEqual(1); + expect(outputs[0]).toMatchObject({ + id: "createWriteStream - 'output.csv'", + type: Constants.DependencyType.DATA, + }); + }); + + it('should not duplicate the same path', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "fs.writeFileSync('out.csv', data);\nfs.writeFileSync('out.csv', data);" + ); + expect(outputs.length).toEqual(1); + }); + }); + + describe('scan', () => { + it('should return metadata for a valid JS file', () => { + fs.readFileSync.mockReturnValue( + "const fs = require('fs');\nconst data = fs.readFileSync('input.csv', 'utf8');" + ); + const testAsset = { + uri: '/path/to/script.js', + type: 'file', + metadata: [], + }; + const response = new JavaScriptHandler().scan(testAsset); + expect(response.metadata[0]).toMatchObject({ + id: 'StatWrap.JavaScriptHandler', + libraries: [{ module: 'fs' }], + inputs: [{ type: Constants.DependencyType.DATA }], + }); + }); + }); + }); +}); diff --git a/test/utils/workflow.spec.js b/test/utils/workflow.spec.js index 652e4e90..f68c617e 100644 --- a/test/utils/workflow.spec.js +++ b/test/utils/workflow.spec.js @@ -81,6 +81,44 @@ describe('utils', () => { expect(dependencies[2].dependencies.length).toEqual(1); expect(dependencies[3].dependencies.length).toEqual(1); }); + + it('should include JavaScript metadata dependencies in the workflow list', () => { + const asset = { + uri: '/test/js', + metadata: [ + { + id: 'StatWrap.JavaScriptHandler', + libraries: [ + { + id: 'react', + module: 'react', + import: 'React', + alias: null, + }, + ], + inputs: [ + { + id: 'readFile - "data/input.csv"', + type: Constants.DependencyType.DATA, + path: '"data/input.csv"', + }, + ], + outputs: [ + { + id: 'writeFile - "data/output.csv"', + type: Constants.DependencyType.DATA, + path: '"data/output.csv"', + }, + ], + }, + ], + children: [], + }; + const dependencies = WorkflowUtil.getAllDependencies(asset); + expect(dependencies.length).toEqual(1); + expect(dependencies[0].assetType).toEqual('javascript'); + expect(dependencies[0].dependencies.length).toEqual(3); + }); }); describe('getAllDependenciesAsGraph', () => { @@ -568,6 +606,9 @@ describe('utils', () => { expect(WorkflowUtil.getAssetType({ metadata: [{ id: 'StatWrap.PythonHandler' }] })).toEqual( 'python', ); + expect(WorkflowUtil.getAssetType({ metadata: [{ id: 'StatWrap.JavaScriptHandler' }] })).toEqual( + 'javascript', + ); }); it('should return a default value for unkown types', () => { From 0a03ccd80db92e330eadd8ef5ab66473dac3f2b4 Mon Sep 17 00:00:00 2001 From: aabhinavvvvvvv Date: Tue, 31 Mar 2026 17:41:08 +0000 Subject: [PATCH 2/4] test: add double-quote and path variation tests for JavaScriptHandler --- .../assets/handlers/javascript.spec.js | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/services/assets/handlers/javascript.spec.js b/test/services/assets/handlers/javascript.spec.js index c310ab72..745977c2 100644 --- a/test/services/assets/handlers/javascript.spec.js +++ b/test/services/assets/handlers/javascript.spec.js @@ -92,6 +92,29 @@ describe('services', () => { module: 'path', }); }); + + it('should detect ES module imports using double quotes', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + 'import { readFile, writeFile } from "fs/promises";' + ); + expect(libraries.length).toEqual(1); + expect(libraries[0]).toMatchObject({ + module: 'fs/promises', + import: '{ readFile, writeFile }', + }); + }); + + it('should detect CommonJS require using double quotes', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + 'const path = require("path");' + ); + expect(libraries.length).toEqual(1); + expect(libraries[0]).toMatchObject({ + module: 'path', + }); + }); }); describe('getInputs', () => { @@ -143,6 +166,52 @@ describe('services', () => { ); expect(inputs.length).toEqual(1); }); + + it('should detect paths using double quotes', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + 'const data = fs.readFileSync("input.csv", "utf8");' + ); + expect(inputs.length).toEqual(1); + expect(inputs[0]).toMatchObject({ + id: 'readFile - "input.csv"', + type: Constants.DependencyType.DATA, + path: '"input.csv"', + }); + }); + + it('should detect fully qualified Linux/Mac paths', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + "fs.readFileSync('/home/user/data/test.csv', 'utf8');" + ); + expect(inputs.length).toEqual(1); + expect(inputs[0]).toMatchObject({ + type: Constants.DependencyType.DATA, + path: "'/home/user/data/test.csv'", + }); + }); + + it('should detect fully qualified Windows paths', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + "fs.readFileSync('C:\\\\Users\\\\user\\\\data\\\\test.csv', 'utf8');" + ); + expect(inputs.length).toEqual(1); + expect(inputs[0]).toMatchObject({ + type: Constants.DependencyType.DATA, + }); + }); + + it('should detect relative paths', () => { + const inputs = new JavaScriptHandler().getInputs( + 'test.uri', + "fs.readFileSync('data/test.csv');\nfs.readFileSync('../data/test.csv');" + ); + expect(inputs.length).toEqual(2); + expect(inputs[0]).toMatchObject({ path: "'data/test.csv'" }); + expect(inputs[1]).toMatchObject({ path: "'../data/test.csv'" }); + }); }); describe('getOutputs', () => { @@ -202,6 +271,52 @@ describe('services', () => { ); expect(outputs.length).toEqual(1); }); + + it('should detect paths using double quotes', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + 'fs.writeFileSync("output.csv", data);' + ); + expect(outputs.length).toEqual(1); + expect(outputs[0]).toMatchObject({ + id: 'writeFile - "output.csv"', + type: Constants.DependencyType.DATA, + path: '"output.csv"', + }); + }); + + it('should detect fully qualified Linux/Mac paths', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "fs.writeFileSync('/home/user/results/output.csv', data);" + ); + expect(outputs.length).toEqual(1); + expect(outputs[0]).toMatchObject({ + type: Constants.DependencyType.DATA, + path: "'/home/user/results/output.csv'", + }); + }); + + it('should detect fully qualified Windows paths', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "fs.writeFileSync('C:\\\\Users\\\\user\\\\results\\\\output.csv', data);" + ); + expect(outputs.length).toEqual(1); + expect(outputs[0]).toMatchObject({ + type: Constants.DependencyType.DATA, + }); + }); + + it('should detect relative paths', () => { + const outputs = new JavaScriptHandler().getOutputs( + 'test.uri', + "fs.writeFileSync('results/output.csv', data);\nfs.writeFileSync('../results/output.csv', data);" + ); + expect(outputs.length).toEqual(2); + expect(outputs[0]).toMatchObject({ path: "'results/output.csv'" }); + expect(outputs[1]).toMatchObject({ path: "'../results/output.csv'" }); + }); }); describe('scan', () => { From 3f2bcf6d20bf4d7a432c7f624e696f39fd24200d Mon Sep 17 00:00:00 2001 From: aabhinavvvvvvv Date: Tue, 31 Mar 2026 23:17:07 +0530 Subject: [PATCH 3/4] test: add test verifying commented-out imports are ignored --- test/services/assets/handlers/javascript.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/services/assets/handlers/javascript.spec.js b/test/services/assets/handlers/javascript.spec.js index 745977c2..287d2931 100644 --- a/test/services/assets/handlers/javascript.spec.js +++ b/test/services/assets/handlers/javascript.spec.js @@ -115,6 +115,14 @@ describe('services', () => { module: 'path', }); }); + + it('should not detect commented-out imports', () => { + const libraries = new JavaScriptHandler().getLibraries( + 'test.uri', + "// import React from 'react';\n// const fs = require('fs');" + ); + expect(libraries.length).toEqual(0); + }); }); describe('getInputs', () => { From 2faecb024628d61050f4d0b938b4c9dd7f5d499e Mon Sep 17 00:00:00 2001 From: aabhinavvvvvvv Date: Wed, 1 Apr 2026 00:17:52 +0000 Subject: [PATCH 4/4] fix: ignore commented-out require statements in getLibraries --- app/services/assets/handlers/javascript.js | 4 ++-- test/services/assets/handlers/javascript.spec.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/assets/handlers/javascript.js b/app/services/assets/handlers/javascript.js index be87fe2c..34568441 100644 --- a/app/services/assets/handlers/javascript.js +++ b/app/services/assets/handlers/javascript.js @@ -159,8 +159,8 @@ export default class JavaScriptHandler extends BaseCodeHandler { // CommonJS require: require('module') or const x = require('module') const requireMatches = [ - ...text.matchAll(/(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*require\s*\(\s*(['"]{1,}[\s\S]+?['"]{1,})\s*\)/gim), - ...text.matchAll(/require\s*\(\s*(['"]{1,}[\s\S]+?['"]{1,})\s*\)/gim), + ...text.matchAll(/^[^/]*?(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*require\s*\(\s*(['"]{1,}[\s\S]+?['"]{1,})\s*\)/gim), + ...text.matchAll(/^[^/]*?require\s*\(\s*(['"]{1,}[\s\S]+?['"]{1,})\s*\)/gim), ]; const seenModules = new Set(libraries.map((l) => l.module)); for (let index = 0; index < requireMatches.length; index++) { diff --git a/test/services/assets/handlers/javascript.spec.js b/test/services/assets/handlers/javascript.spec.js index 287d2931..df6518c1 100644 --- a/test/services/assets/handlers/javascript.spec.js +++ b/test/services/assets/handlers/javascript.spec.js @@ -203,7 +203,7 @@ describe('services', () => { it('should detect fully qualified Windows paths', () => { const inputs = new JavaScriptHandler().getInputs( 'test.uri', - "fs.readFileSync('C:\\\\Users\\\\user\\\\data\\\\test.csv', 'utf8');" + "fs.readFileSync('C:\\Users\\user\\data\\test.csv', 'utf8');" ); expect(inputs.length).toEqual(1); expect(inputs[0]).toMatchObject({ @@ -308,7 +308,7 @@ describe('services', () => { it('should detect fully qualified Windows paths', () => { const outputs = new JavaScriptHandler().getOutputs( 'test.uri', - "fs.writeFileSync('C:\\\\Users\\\\user\\\\results\\\\output.csv', data);" + "fs.writeFileSync('C:\\Users\\user\\results\\output.csv', data);" ); expect(outputs.length).toEqual(1); expect(outputs[0]).toMatchObject({