Skip to content

Commit

Permalink
feat: alias root-level code by path
Browse files Browse the repository at this point in the history
  • Loading branch information
gabidobo authored and andreimarinescu committed Sep 21, 2022
1 parent beee58a commit fbf4405
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 25 deletions.
22 changes: 17 additions & 5 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ export default _default;
/** Initialize Sandworm. */
declare function init({
loadSourceMaps,
devMode: devModeOption,
devMode,
verbose,
skipTracking: skipTrackingOption,
skipTracking,
trackingIP,
trackingPort,
ignoreExtensions: ignoreExtensionsOption,
trustedModules: additionalTrustedModules,
permissions: permissionsOption,
ignoreExtensions,
trustedModules,
permissions,
aliases,
onAccessDenied,
}?: {
/**
* Set this to true to automatically load the sourcemap declared in the caller js file.
Expand Down Expand Up @@ -44,6 +46,10 @@ declare function init({
trustedModules?: any[] = [];
/** Module permissions to enforce if dev mode is false. */
permissions?: Permission[] = [];
/** An array describing the optional aliases to apply to root-level sources based on their path. */
aliases: Alias[] = [];
/** An optional callback to be triggered on access errors, before throwing */
onAccessDenied: function;
}): Promise<void>;
/** Specifies a set of permissions to grant a module or a class of modules. */
declare interface Permission {
Expand All @@ -52,6 +58,12 @@ declare interface Permission {
/** An array of string permissions to grant the specified module(s), formatted as `family.method`. */
permissions: string[];
}
declare interface Alias {
/** A path component shared between all source code files that should be matched by the alias. */
path: string;
/** The alias name to apply. */
name: string;
}
/** In dev mode, returns the current call history. In production mode, returns an empty array. */
declare function getHistory(): any[];
/** In dev mode, clears the current call history. In production mode, does nothing. */
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,25 @@ Sandworm interprets scripts loaded via the `<script>` tag as individual modules.

Sandworm can also catch activity coming from local, user-installed browser extensions. To enable this, set the `ignoreExtensions` config option to `false`. By default (`ignoreExtensions: true`), any invoke that has a browser extension anywhere in the call path will be passed through.

#### Aliases

Root code can be segmented into multiple "virtual" modules based on the file path by defining aliases. This can be useful, for example, when running tests, to separate core code from testing infrastructure code:

```javascript
// Say we want to run unit tests for https://github.com/expressjs/express
require("sandworm").init({
devMode: true,
trustedModules: ['mocha'],
// This will make the express core source code register as `express` instead of `root`
// Unit test code will still be labeled `root`
aliases: [{path: 'express/lib', name: 'express'}],
});
```

To configure aliases, set the `aliases` config option to an array of objects having:
* a string `path` attribute, representing a path component shared between all source code files that should be matched by the alias;
* a string `name` attribute, representing the alias name to apply.

### Configuration Options

| Option | Default | Description |
Expand All @@ -258,6 +277,7 @@ Sandworm can also catch activity coming from local, user-installed browser exten
| `trustedModules` | `[]` | Utility or platform modules that Sandworm should remove from a caller path. |
| `permissions` | `[]` | Module permissions to enforce if dev mode is false. |
| `onAccessDenied` | `undefined` | A function that will be invoked right before throwing on access denied. The error itself will be passed as the first arg. |
| `aliases` | `[]` | An array of alias definitions - see [aliases](#aliases). |

### Using With Bundlers & SourceMaps

Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
addSourceMap,
addTrustedModules,
getCurrentModuleInfo,
setAliases,
setAllowsAll,
setPermissions,
} from './module';
Expand Down Expand Up @@ -49,6 +50,7 @@ const init = ({
trustedModules: additionalTrustedModules = [],
permissions: permissionsOption = [],
onAccessDenied,
aliases = [],
} = {}) => {
try {
if (isInitialized()) {
Expand Down Expand Up @@ -85,6 +87,7 @@ const init = ({

setIgnoreExtensions(ignoreExtensionsOption);
setAccessDeniedCallback(onAccessDenied);
setAliases(aliases);

let library = [];

Expand Down
62 changes: 43 additions & 19 deletions src/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const cachedPermissions = {};
const defaultPermissions = {module: 'root', permissions: true};
let permissions = [defaultPermissions];
let trustedModules = ['sandworm', 'react-dom', 'scheduler'];
let aliases = [];

const sourcemaps = {};

Expand All @@ -16,6 +17,14 @@ export const addTrustedModules = (additionalTrustedModules) => {
trustedModules = [...trustedModules, ...additionalTrustedModules];
};

export const setAliases = (newAliases = []) => {
if (!Array.isArray(newAliases)) {
return;
}

aliases = [...newAliases.filter((item) => item && item.path && item.name)];
};

export const setPermissions = (newPermissions = []) => {
if (!Array.isArray(newPermissions)) {
return;
Expand Down Expand Up @@ -67,42 +76,57 @@ export const mapStackItemToSource = (item) => {
};
};

export const getNodeModuleName = (location) => {
const components = location.split('/');
const nodeModulesIndex = components.findIndex((v) => v === 'node_modules');
let moduleName = components[nodeModulesIndex + 1];
// Names starting with `@` are organizations, so it's good to get
// a bit more context by also grabbing the next path component
if (moduleName.startsWith('@')) {
const submodule = components[nodeModulesIndex + 2];
if (submodule) {
moduleName = `${moduleName}/${submodule}`;
}
}

return moduleName;
};

export const getModuleNameFromLocation = (location, allowURLs) => {
// Infer the module name
let moduleName = 'root';

if (!location || typeof location !== 'string') {
return undefined;
moduleName = undefined;
}

// Label locations coming from inside Node
if (location.startsWith('node:')) {
else if (location.startsWith('node:')) {
// locations like node:internal/modules/cjs/loader should map to node:internal
// node:fs should map to node:fs
return location.split('/')[0];
[moduleName] = location.split('/');
}

// Label packages
if (location.includes('node_modules')) {
const components = location.split('/');
const nodeModulesIndex = components.findIndex((v) => v === 'node_modules');
let moduleName = components[nodeModulesIndex + 1];
// Names starting with `@` are organizations, so it's good to get
// a bit more context by also grabbing the next path component
if (moduleName.startsWith('@')) {
const submodule = components[nodeModulesIndex + 2];
if (submodule) {
moduleName = `${moduleName}/${submodule}`;
}
}
return moduleName;
else if (location.includes('node_modules')) {
moduleName = getNodeModuleName(location);
}

// Treat URLs as separate modules
// These are usually scripts loaded from external sources, like directly from a CDN
if (allowURLs && location.includes('://')) {
return location;
else if (allowURLs && location.includes('://')) {
moduleName = location;
}

// Alias sources with paths containing specific search strings
else {
const alias = aliases.find(({path}) => location.includes(path));
if (alias) {
moduleName = alias.name;
}
}

return 'root';
return moduleName;
};

/** Reduce mod1>mod1>mod2>mod2>mod2 to mod1>mod2 */
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getModulePermissions,
isModuleAllowedToExecute,
mapStackItemToSource,
setAliases,
setAllowsAll,
setPermissions,
} from '../../src/module';
Expand Down Expand Up @@ -291,6 +292,21 @@ describe('module', () => {
expect(getModuleNameFromLocation('node:https')).toBe('node:https');
expect(getModuleNameFromLocation('node:internal/modules/cjs/loader')).toBe('node:internal');
});

test('should apply alias', () => {
setAliases([{path: 'tests/node', name: 'test'}]);
expect(
getModuleNameFromLocation('/Users/jason/code/sandworm/tests/node/prod/stack.test.js'),
).toBe('test');
setAliases([]);
});

test('should ignore invalid alias config', () => {
setAliases(5);
expect(
getModuleNameFromLocation('/Users/jason/code/sandworm/tests/node/prod/stack.test.js'),
).toBe('root');
});
});

describe('getCurrentModuleInfo', () => {
Expand Down

0 comments on commit fbf4405

Please sign in to comment.