Socket
Socket
Sign inDemoInstall

@ts-bridge/cli

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ts-bridge/cli - npm Package Compare versions

Comparing version 0.5.0 to 0.5.1

11

CHANGELOG.md

@@ -10,2 +10,10 @@ # Changelog

## [0.5.1]
### Fixed
- Transform dynamic imports ([#53](https://github.com/ts-bridge/ts-bridge/pull/53))
- This fixes a bug where dynamic imports were not transformed correctly,
resulting in unresolved imports in some cases.
## [0.5.0]

@@ -139,3 +147,4 @@

[Unreleased]: https://github.com/ts-bridge/ts-bridge/compare/@ts-bridge/cli@0.5.0...HEAD
[Unreleased]: https://github.com/ts-bridge/ts-bridge/compare/@ts-bridge/cli@0.5.1...HEAD
[0.5.1]: https://github.com/ts-bridge/ts-bridge/compare/@ts-bridge/cli@0.5.0...@ts-bridge/cli@0.5.1
[0.5.0]: https://github.com/ts-bridge/ts-bridge/compare/@ts-bridge/cli@0.4.4...@ts-bridge/cli@0.5.0

@@ -142,0 +151,0 @@ [0.4.4]: https://github.com/ts-bridge/ts-bridge/compare/@ts-bridge/cli@0.4.3...@ts-bridge/cli@0.4.4

6

dist/build.js

@@ -11,3 +11,3 @@ import chalk from 'chalk';

import { executeSteps } from './steps.js';
import { getTypeImportExportTransformer, getExportExtensionTransformer, getImportExtensionTransformer, getRequireExtensionTransformer, } from './transformers.js';
import { getDynamicImportExtensionTransformer, getTypeImportExportTransformer, getExportExtensionTransformer, getImportExtensionTransformer, getRequireExtensionTransformer, } from './transformers.js';
import { getDefinedArray } from './utils.js';

@@ -343,3 +343,3 @@ const { createProgram, getPreEmitDiagnostics, ModuleResolutionKind } = typescript;

};
const { diagnostics } = program.emit(undefined, getWriteFileFunction(type, system), undefined, undefined, {
const { diagnostics } = program.emit(undefined, getWriteFileFunction(type, program.getCompilerOptions(), system, verbose), undefined, undefined, {
before: [

@@ -349,2 +349,3 @@ getLoggingTransformer(verbose),

getImportExtensionTransformer(extension, options),
getDynamicImportExtensionTransformer(extension, options),
getExportExtensionTransformer(extension, options),

@@ -356,2 +357,3 @@ getTypeImportExportTransformer(options),

getImportExtensionTransformer(extension, options),
getDynamicImportExtensionTransformer(extension, options),
getExportExtensionTransformer(extension, options),

@@ -358,0 +360,0 @@ ],

@@ -1,2 +0,2 @@

import type { System, WriteFileCallback } from 'typescript';
import type { CompilerOptions, System, WriteFileCallback } from 'typescript';
import type { BuildType } from './build-type.js';

@@ -26,6 +26,8 @@ /**

* @param type - The build type to use.
* @param compilerOptions - The compiler options to use.
* @param system - The system to use for file operations.
* @param verbose - Whether to log verbose output.
* @returns The function that writes files to the file system.
*/
export declare function getWriteFileFunction(type: BuildType, system: System): WriteFileCallback;
export declare function getWriteFileFunction(type: BuildType, compilerOptions: CompilerOptions, system: System, verbose?: boolean): WriteFileCallback;
/**

@@ -32,0 +34,0 @@ * Read a JSON file and return its content as a parsed value.

import { rmSync } from 'fs';
import { sep, resolve, normalize } from 'path';
import { sep, resolve, normalize, relative, join } from 'path';
import typescript from 'typescript';

@@ -46,2 +46,15 @@ import { getBuildTypeOptions } from './build-type.js';

/**
* Given the output file path, map it back to its source file path.
*
* @param outputFilePath - The path to the output file.
* @param rootDir - The root directory where the source files are located.
* @param outDir - The output directory where the compiled files are placed.
* @returns The source file path corresponding to the output file.
*/
function getSourceFilePath(outputFilePath, rootDir, outDir) {
const relativePath = relative(outDir, outputFilePath);
const sourceFilePath = join(rootDir, relativePath);
return sourceFilePath.replace(/\.jsx?$/u, '.ts');
}
/**
* Get a function that writes files to the file system, after transforming them.

@@ -53,10 +66,13 @@ *

* @param type - The build type to use.
* @param compilerOptions - The compiler options to use.
* @param system - The system to use for file operations.
* @param verbose - Whether to log verbose output.
* @returns The function that writes files to the file system.
*/
export function getWriteFileFunction(type, system) {
export function getWriteFileFunction(type, compilerOptions, system, verbose = false) {
const { extension, declarationExtension } = getBuildTypeOptions(type);
return (fileName, content, writeByteOrderMark) => {
const fileNameWithExtension = getNewFileName(fileName, extension, declarationExtension);
const updatedContent = transformFile(fileName, content, extension, declarationExtension);
const sourceFilePath = getSourceFilePath(fileName, compilerOptions.rootDir ?? '.', compilerOptions.outDir ?? './dist');
const updatedContent = transformFile(fileName, sourceFilePath, content, extension, declarationExtension, system, verbose);
system.writeFile(fileNameWithExtension, updatedContent, writeByteOrderMark);

@@ -63,0 +79,0 @@ };

@@ -5,4 +5,12 @@ import { getVirtualEnvironment, noOp } from '@ts-bridge/test-utils';

import { resolve } from 'path';
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { getCanonicalFileName, getNewFileName, getWriteFileFunction, readJsonFile, removeDirectory, } from './file-system.js';
// TODO: Change these tests to use the real file system, to avoid the need for
// mocking the resolver.
vi.mock('@ts-bridge/resolver', () => ({
resolve: vi.fn().mockImplementation(() => ({
format: 'commonjs',
path: '/fake.js',
})),
}));
describe('removeDirectory', () => {

@@ -55,3 +63,3 @@ const TEMPORARY_DIRECTORY = tmpdir();

});
const writeFile = getWriteFileFunction('module', system);
const writeFile = getWriteFileFunction('module', {}, system);
writeFile('/foo.ts', 'console.log("Hello, world!");', false);

@@ -66,3 +74,3 @@ expect(system.fileExists('/foo.ts')).toBe(true);

});
const writeFile = getWriteFileFunction('module', system);
const writeFile = getWriteFileFunction('module', {}, system);
writeFile('/foo.js', 'console.log("Hello, world!");', false);

@@ -77,3 +85,3 @@ expect(system.fileExists('/foo.mjs')).toBe(true);

});
const writeFile = getWriteFileFunction('module', system);
const writeFile = getWriteFileFunction('module', {}, system);
writeFile('/foo.d.ts', 'console.log("Hello, world!");', false);

@@ -88,3 +96,3 @@ expect(system.fileExists('/foo.d.mts')).toBe(true);

});
const writeFile = getWriteFileFunction('module', system);
const writeFile = getWriteFileFunction('module', {}, system);
writeFile('/foo.d.ts.map', JSON.stringify({ file: '/index.js' }), false);

@@ -100,3 +108,3 @@ expect(system.fileExists('/foo.d.mts.map')).toBe(true);

});
const writeFile = getWriteFileFunction('module', {
const writeFile = getWriteFileFunction('module', {}, {
...system,

@@ -110,2 +118,45 @@ // The virtual file system does not have a `createDirectory` method, so we

});
it('transforms dynamic imports in declaration files before writing them to the file system', () => {
const code = `
/**
* This function results in a case where TypeScript emits the declaration file
* with a dynamic import.
*
* @returns A class that extends \`Foo\`.
*/
export declare function bar(): {
new (): {
getFoo(): import("./dummy").Value;
};
};
//# sourceMappingURL=declaration.d.ts.map
`;
const { system } = getVirtualEnvironment({
files: {
'/index.ts': '// no-op',
},
});
const writeFile = getWriteFileFunction('module', {
rootDir: '/',
outDir: '/dist',
}, system);
writeFile('/foo.d.ts', code, false);
expect(system.fileExists('/foo.d.mts')).toBe(true);
expect(system.readFile('/foo.d.mts')).toMatchInlineSnapshot(`
"
/**
* This function results in a case where TypeScript emits the declaration file
* with a dynamic import.
*
* @returns A class that extends \`Foo\`.
*/
export declare function bar(): {
new (): {
getFoo(): import("./dummy.mjs").Value;
};
};
//# sourceMappingURL=declaration.d.ts.map
"
`);
});
});

@@ -112,0 +163,0 @@ describe('readJsonFile', () => {

@@ -72,2 +72,10 @@ import type { FileSystemInterface, FileFormat } from '@ts-bridge/resolver';

/**
* Replace the extension of a path.
*
* @param path - The path to replace the extension of.
* @param extension - The new extension.
* @returns The path with the new extension.
*/
export declare function replaceExtension(path: string, extension: string): string;
/**
* Get the path to a module.

@@ -74,0 +82,0 @@ *

@@ -101,2 +101,12 @@ import { resolve } from '@ts-bridge/resolver';

/**
* Replace the extension of a path.
*
* @param path - The path to replace the extension of.
* @param extension - The new extension.
* @returns The path with the new extension.
*/
export function replaceExtension(path, extension) {
return path.replace(SOURCE_EXTENSIONS_REGEX, extension);
}
/**
* Get the path to a module.

@@ -121,3 +131,3 @@ *

if (isRelative(packageSpecifier)) {
return resolution.specifier.replace(SOURCE_EXTENSIONS_REGEX, extension);
return replaceExtension(resolution.specifier, extension);
}

@@ -124,0 +134,0 @@ return resolution.specifier;

@@ -37,2 +37,23 @@ import type { FileFormat } from '@ts-bridge/resolver';

/**
* Get a transformer that updates the dynamic import extensions to append the
* given extension.
*
* For example, the following import declaration:
* ```js
* import('./foo.js');
* ```
*
* will be transformed to (assuming the extension is `.mjs`):
* ```js
* import('./foo.mjs');
* ```
*
* @param extension - The extension to append to import paths.
* @param options - The transformer options.
* @param options.system - The compiler system to use.
* @param options.verbose - Whether to enable verbose logging.
* @returns The transformer function.
*/
export declare function getDynamicImportExtensionTransformer(extension: string, { system, verbose }: TransformerOptions): (context: TransformationContext) => CustomTransformer;
/**
* Get a transformer that updates the `require` calls to use the given

@@ -282,12 +303,27 @@ * extension.

/**
* Transform the dynamic imports in the declaration file to use the new file
* extension.
*
* @param content - The content of the declaration file.
* @param extension - The new file extension.
* @param parentUrl - The URL of the parent module.
* @param system - The TypeScript system.
* @param verbose - Whether to show verbose output.
* @returns The transformed content.
*/
export declare function transformDeclarationImports(content: string, extension: string, parentUrl: string, system: System, verbose: boolean): string;
/**
* Transform the file content to update the source map path or the source file
* extension.
*
* @param fileName - The name of the source file.
* @param fileName - The name of the file.
* @param sourceFileName - The name of the source file.
* @param content - The content of the source file.
* @param extension - The new file extension.
* @param declarationExtension - The new file extension for declaration files.
* @param system - The TypeScript system.
* @param verbose - Whether to show verbose output.
* @returns The transformed content.
*/
export declare function transformFile(fileName: string, content: string, extension: string, declarationExtension: string): string;
export declare function transformFile(fileName: string, sourceFileName: string, content: string, extension: string, declarationExtension: string, system: System, verbose: boolean): string;
//# sourceMappingURL=transformers.d.ts.map

@@ -59,2 +59,54 @@ import typescript from 'typescript';

/**
* Get a transformer that updates the dynamic import extensions to append the
* given extension.
*
* For example, the following import declaration:
* ```js
* import('./foo.js');
* ```
*
* will be transformed to (assuming the extension is `.mjs`):
* ```js
* import('./foo.mjs');
* ```
*
* @param extension - The extension to append to import paths.
* @param options - The transformer options.
* @param options.system - The compiler system to use.
* @param options.verbose - Whether to enable verbose logging.
* @returns The transformer function.
*/
export function getDynamicImportExtensionTransformer(extension, { system, verbose }) {
return (context) => {
// This returns a custom transformer instead of a transformer factory, as
// this transformer is used for declaration files, which requires support
// for bundle transformations (even though we don't use it here).
return {
transformSourceFile(sourceFile) {
const visitor = (node) => {
if (node.parent &&
isStringLiteral(node) &&
isCallExpression(node.parent) &&
node.parent.expression.kind === SyntaxKind.ImportKeyword) {
const importPath = getModulePath({
packageSpecifier: node.text,
parentUrl: sourceFile.fileName,
extension,
system,
verbose,
});
return factory.createStringLiteral(importPath);
}
return visitEachChild(node, visitor, context);
};
return visitNode(sourceFile, visitor);
},
/* istanbul ignore next 3 */
transformBundle(bundle) {
return bundle;
},
};
};
}
/**
* Get a transformer that updates the `require` calls to use the given

@@ -573,12 +625,80 @@ * extension.

/**
* Transform the source map URL to match the new file extension of the source
* file.
*
* @param content - The content of the source file.
* @param extension - The new file extension.
* @param declarationExtension - The new file extension for declaration files.
* @returns The transformed content.
*/
function transformSourceMapUrl(content, extension, declarationExtension) {
// This is a bit hacky, but TypeScript doesn't provide a way to transform
// the source map comment in the source file.
return content
.replace(/^\/\/# sourceMappingURL=(.*)\.js\.map$/mu, `//# sourceMappingURL=$1${extension}.map`)
.replace(/^\/\/# sourceMappingURL=(.*)\.d\.ts\.map$/mu, `//# sourceMappingURL=$1${declarationExtension}.map`);
}
/**
* The regular expression to match dynamic imports. The aim of `ts-bridge` is to
* avoid regular expressions as much as possible, but this is a special case
* where we need to match dynamic imports created by the TypeScript compiler,
* after AST transformation.
*
* This regular expression matches dynamic imports, but only if they are not
* part of a larger identifier. This is to avoid matching `import.meta.url`,
* `foo.import`, etc.
*
* The following imports will be matched:
*
* ```ts
* import('./foo.js')
* import('./foo/bar.js').SomeClass
* ```
*
* The following imports will not be matched:
*
* ```ts
* import.meta.url
* foo.import('./foo.js')
* _import('./foo.js')
* ```
*/
const DYNAMIC_IMPORT_REGEX = /(?<![\w.])import\(['"](\..+?)['"]\)/gu;
/**
* Transform the dynamic imports in the declaration file to use the new file
* extension.
*
* @param content - The content of the declaration file.
* @param extension - The new file extension.
* @param parentUrl - The URL of the parent module.
* @param system - The TypeScript system.
* @param verbose - Whether to show verbose output.
* @returns The transformed content.
*/
export function transformDeclarationImports(content, extension, parentUrl, system, verbose) {
return content.replace(DYNAMIC_IMPORT_REGEX, (match, path) => {
const importPath = getModulePath({
packageSpecifier: path,
parentUrl,
extension,
system,
verbose,
});
return match.replace(path, importPath);
});
}
/**
* Transform the file content to update the source map path or the source file
* extension.
*
* @param fileName - The name of the source file.
* @param fileName - The name of the file.
* @param sourceFileName - The name of the source file.
* @param content - The content of the source file.
* @param extension - The new file extension.
* @param declarationExtension - The new file extension for declaration files.
* @param system - The TypeScript system.
* @param verbose - Whether to show verbose output.
* @returns The transformed content.
*/
export function transformFile(fileName, content, extension, declarationExtension) {
export function transformFile(fileName, sourceFileName, content, extension, declarationExtension, system, verbose) {
if (fileName.endsWith('.d.ts.map')) {

@@ -590,7 +710,7 @@ return transformSourceMap(content, declarationExtension);

}
// This is a bit hacky, but TypeScript doesn't provide a way to transform
// the source map comment in the source file.
return content
.replace(/^\/\/# sourceMappingURL=(.*)\.js\.map$/mu, `//# sourceMappingURL=$1${extension}.map`)
.replace(/^\/\/# sourceMappingURL=(.*)\.d\.ts\.map$/mu, `//# sourceMappingURL=$1${declarationExtension}.map`);
const updatedContent = transformSourceMapUrl(content, extension, declarationExtension);
if (fileName.endsWith('.d.ts')) {
return transformDeclarationImports(updatedContent, extension, sourceFileName, system, verbose);
}
return updatedContent;
}
import { getFixture } from '@ts-bridge/test-utils';
import { basename } from 'path';
import { basename, dirname, resolve } from 'path';
import { createProgram, sys } from 'typescript';
import { fileURLToPath } from 'url';
import { beforeAll, describe, expect, it } from 'vitest';
import { getBuildTypeOptions } from './build-type.js';
import { getTypeScriptConfig } from './config.js';
import { getDefaultImportTransformer, getExportExtensionTransformer, getGlobalsTransformer, getImportAttributeTransformer, getImportExtensionTransformer, getImportMetaTransformer, getNamedImportTransformer, getRemoveImportAttributeTransformer, getRequireExtensionTransformer, getRequireTransformer, getTargetTransformer, getTypeImportExportTransformer, } from './transformers.js';
import { getDefaultImportTransformer, getDynamicImportExtensionTransformer, getExportExtensionTransformer, getGlobalsTransformer, getImportAttributeTransformer, getImportExtensionTransformer, getImportMetaTransformer, getNamedImportTransformer, getRemoveImportAttributeTransformer, getRequireExtensionTransformer, getRequireTransformer, getTargetTransformer, getTypeImportExportTransformer, transformDeclarationImports, } from './transformers.js';
const BASE_DIRECTORY = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'test-utils', 'test', 'fixtures', 'import-resolver');
const PARENT_URL = resolve(BASE_DIRECTORY, 'src', 'index.ts');
/**

@@ -16,5 +19,7 @@ * Compile a string of (TypeScript) code to JavaScript, and return the compiled

* @param options.transformer - The transformer to use.
* @param options.declarationsTransformers - The transformers to use for
* declaration files.
* @returns The compiled code.
*/
function compile({ program, format, transformer }) {
function compile({ program, format, transformer, declarationsTransformers = [], }) {
const { target } = getBuildTypeOptions(format);

@@ -26,2 +31,3 @@ const files = {};

before: [transformer, getTargetTransformer(target)],
afterDeclarations: declarationsTransformers,
});

@@ -42,4 +48,4 @@ return files;

});
const fn = (format, transformer) => {
return compile({ program, format, transformer });
const fn = (format, transformer, declarationsTransformers = []) => {
return compile({ program, format, transformer, declarationsTransformers });
};

@@ -171,2 +177,116 @@ fn.typeChecker = program.getTypeChecker();

});
describe('getDynamicImportExtensionTransformer', () => {
describe('when targeting `module`', () => {
let files;
beforeAll(() => {
const compiler = createCompiler(getFixture('dynamic-imports'));
const transformer = getDynamicImportExtensionTransformer('.mjs', {
typeChecker: compiler.typeChecker,
system: sys,
});
files = compiler('module', transformer, [transformer]);
});
it('adds the `.mjs` extension to the import statement', async () => {
expect(files['add.js']).toMatchInlineSnapshot(`
""use strict";
import("./dummy.mjs");
"
`);
});
it('rewrites the import to `index.mjs` when importing from a directory', async () => {
expect(files['import-folder.js']).toMatchInlineSnapshot(`
""use strict";
import("./folder/index.mjs");
"
`);
});
it('overrides an existing extension', async () => {
expect(files['override.js']).toMatchInlineSnapshot(`
""use strict";
import("./dummy.mjs");
"
`);
});
it('resolves external imports with paths', async () => {
expect(files['external.js']).toMatchInlineSnapshot(`
""use strict";
import("semver");
import("semver/preload.js");
"
`);
});
it('does not add an extension if the module fails to resolve', async () => {
expect(files['unresolved.js']).toMatchInlineSnapshot(`
""use strict";
// @ts-expect-error - Unresolved module.
import("./unresolved-module");
"
`);
});
it('does not add an extension if the module specifier is invalid', async () => {
expect(files['invalid.js']).toMatchInlineSnapshot(`
""use strict";
// @ts-expect-error - Invalid module specifier.
import(0);
"
`);
});
});
describe('when targeting `commonjs`', () => {
let files;
beforeAll(() => {
const compiler = createCompiler(getFixture('dynamic-imports'));
const transformer = getDynamicImportExtensionTransformer('.cjs', {
typeChecker: compiler.typeChecker,
system: sys,
});
files = compiler('commonjs', transformer, [transformer]);
});
it('adds the `.cjs` extension to the import statement', async () => {
expect(files['add.js']).toMatchInlineSnapshot(`
""use strict";
import("./dummy.cjs");
"
`);
});
it('rewrites the import to `index.cjs` when importing from a directory', async () => {
expect(files['import-folder.js']).toMatchInlineSnapshot(`
""use strict";
import("./folder/index.cjs");
"
`);
});
it('overrides an existing extension', async () => {
expect(files['override.js']).toMatchInlineSnapshot(`
""use strict";
import("./dummy.cjs");
"
`);
});
it('resolves external imports with paths', async () => {
expect(files['external.js']).toMatchInlineSnapshot(`
""use strict";
import("semver");
import("semver/preload.js");
"
`);
});
it('does not add an extension if the module fails to resolve', async () => {
expect(files['unresolved.js']).toMatchInlineSnapshot(`
""use strict";
// @ts-expect-error - Unresolved module.
import("./unresolved-module");
"
`);
});
it('does not add an extension if the module specifier is invalid', async () => {
expect(files['invalid.js']).toMatchInlineSnapshot(`
""use strict";
// @ts-expect-error - Invalid module specifier.
import(0);
"
`);
});
});
});
describe('getRequireExtensionTransformer', () => {

@@ -798,1 +918,66 @@ describe('when targeting `module`', () => {

});
describe('transformDeclarationImports', () => {
it.each([
`import { foo } from './dummy';`,
`import type { foo } from './dummy';`,
`foo.import('./dummy');`,
`import('dummy');`,
`require('./dummy');`,
`import.meta.url;`,
`import.meta.resolve('dummy');`,
])('does not alter the import statement `%s`', (code) => {
expect(transformDeclarationImports(code, '.mjs', PARENT_URL, sys, false)).toBe(code);
});
it('adds an extension to a relative dynamic import', () => {
expect(transformDeclarationImports(`import('./dummy');`, '.mjs', PARENT_URL, sys, false)).toBe(`import('./dummy.mjs');`);
});
it('adds an extension to a relative dynamic import of a folder', () => {
expect(transformDeclarationImports(`import('./folder');`, '.mjs', PARENT_URL, sys, false)).toBe(`import('./folder/index.mjs');`);
});
it('adds an extension to multiple relative dynamic imports', () => {
const code = `
/**
* This function results in a case where TypeScript emits the declaration file
* with a dynamic import.
*
* @returns A class that extends \`Foo\`.
*/
export declare function bar(): {
new (): {
getFoo(): import("./dummy").Value;
getBar(): import("./folder").Value;
};
};
//# sourceMappingURL=declaration.d.ts.map
`;
expect(transformDeclarationImports(code, '.mjs', PARENT_URL, sys, false))
.toMatchInlineSnapshot(`
"
/**
* This function results in a case where TypeScript emits the declaration file
* with a dynamic import.
*
* @returns A class that extends \`Foo\`.
*/
export declare function bar(): {
new (): {
getFoo(): import("./dummy.mjs").Value;
getBar(): import("./folder/index.mjs").Value;
};
};
//# sourceMappingURL=declaration.d.ts.map
"
`);
});
it('adds an extension to multiple relative dynamic imports on one line', () => {
const code = `
import("./dummy").Value; import("./folder").Value;
`;
expect(transformDeclarationImports(code, '.mjs', PARENT_URL, sys, false))
.toMatchInlineSnapshot(`
"
import("./dummy.mjs").Value; import("./folder/index.mjs").Value;
"
`);
});
});
{
"name": "@ts-bridge/cli",
"version": "0.5.0",
"version": "0.5.1",
"description": "Bridge the gap between ES modules and CommonJS modules with an easy-to-use alternative to `tsc`.",

@@ -5,0 +5,0 @@ "keywords": [

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc