Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
CPP: `${ICON_PATH}cpp.svg`,
GENERIC: `${ICON_PATH}generic.svg`,
LIBRARY: `${ICON_PATH}library.svg`,
Expand All @@ -35,6 +36,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 === 'cpp') {
iconUrl = ICON_TYPES.CPP;
} else if (node.assetType === 'rust') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
CPP: `${ICON_PATH}cpp.svg`,
JAVA: `${ICON_PATH}java.svg`,
GENERIC: `${ICON_PATH}generic.svg`,
Expand Down Expand Up @@ -43,6 +44,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 === 'cpp') {
iconUrl = ICON_TYPES.CPP;
} else if (node.value === 'java') {
Expand Down
5 changes: 5 additions & 0 deletions app/constants/assets-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ module.exports = {
extensions: ['go'],
categories: ['code'],
},
{
name: 'JavaScript',
extensions: ['js', 'jsx', 'mjs', 'cjs'],
categories: ['code'],
},
{
name: 'C++',
extensions: ['cc', 'cpp', 'cxx', 'c++', 'h', 'hh', 'hpp', 'hxx', 'ipp', 'tpp', 'inl'],
Expand Down
4 changes: 4 additions & 0 deletions app/images/js.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions app/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ import PythonHandler from './services/assets/handlers/python';
import RHandler from './services/assets/handlers/r';
import SASHandler from './services/assets/handlers/sas';
import StataHandler from './services/assets/handlers/stata';
import AssetUtil from './utils/asset';
import ProjectUtil from './utils/project';
import JavaHandler from './services/assets/handlers/java';
import RustHandler from './services/assets/handlers/rust';
import GoHandler from './services/assets/handlers/go';
import SQLHandler from './services/assets/handlers/sql';
import JavaScriptHandler from './services/assets/handlers/javascript';
import CppHandler from './services/assets/handlers/cpp';
import CHandler from './services/assets/handlers/c';
import Constants from './constants/constants';
import SQLHandler from './services/assets/handlers/sql';
import DartHandler from './services/assets/handlers/dart';
import AssetUtil from './utils/asset';
import ProjectUtil from './utils/project';
import Constants from './constants/constants';

const projectService = new ProjectService();
const projectListService = new ProjectListService();
Expand Down Expand Up @@ -63,6 +64,7 @@ contextBridge.exposeInMainWorld('workerElectronBridge', {
new RustHandler(),
new SQLHandler(),
new GoHandler(),
new JavaScriptHandler(),
new CppHandler(),
new CHandler(),
new DartHandler(),
Expand Down
182 changes: 182 additions & 0 deletions app/services/assets/handlers/javascript.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions app/utils/workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import JavaHandler from '../services/assets/handlers/java';
import RustHandler from '../services/assets/handlers/rust';
import SQLHandler from '../services/assets/handlers/sql';
import GoHandler from '../services/assets/handlers/go';
import JavaScriptHandler from '../services/assets/handlers/javascript';
import CppHandler from '../services/assets/handlers/cpp';
import CHandler from '../services/assets/handlers/c';
import DartHandler from '../services/assets/handlers/dart';
Expand Down Expand Up @@ -66,6 +67,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';
} else if (AssetUtil.getHandlerMetadata(CHandler.id, asset.metadata)) {
assetType = 'c';
} else if (AssetUtil.getHandlerMetadata(DartHandler.id, asset.metadata)) {
Expand Down Expand Up @@ -371,6 +374,7 @@ export default class WorkflowUtil {
WorkflowUtil._getMetadataDependencies(asset, RustHandler.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);
WorkflowUtil._getMetadataDependencies(asset, CppHandler.id, libraries, inputs, outputs);
WorkflowUtil._getMetadataDependencies(asset, CHandler.id, libraries, inputs, outputs);

Expand Down Expand Up @@ -440,6 +444,7 @@ export default class WorkflowUtil {
WorkflowUtil._getMetadataDependencies(asset, RustHandler.id, libraries, [], []);
WorkflowUtil._getMetadataDependencies(asset, SQLHandler.id, libraries, [], []);
WorkflowUtil._getMetadataDependencies(asset, GoHandler.id, libraries, [], []);
WorkflowUtil._getMetadataDependencies(asset, JavaScriptHandler.id, libraries, [], []);
WorkflowUtil._getMetadataDependencies(asset, CppHandler.id, libraries, [], []);
WorkflowUtil._getMetadataDependencies(asset, CHandler.id, libraries, [], []);
WorkflowUtil._getMetadataDependencies(asset, DartHandler.id, libraries, [], []);
Expand Down
20 changes: 20 additions & 0 deletions docs/Assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions test/services/assets/asset.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading