New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

@graphox/babel-plugin

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@graphox/babel-plugin - npm Package Compare versions

Comparing version
0.4.4
to
0.5.0
+108
-0
auto-resolution.test.js

@@ -6,2 +6,3 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';

import os from 'os';
import { execFileSync } from 'child_process';
import plugin from './index.js';

@@ -187,2 +188,109 @@

});
it('rewrites graphql calls when outputDir is relative and cwd differs from package root', () => {
const rootDir = tmpDir;
const pkgDir = path.join(rootDir, 'packages', 'app');
const outputDir = './src/generated';
const absoluteOutputDir = path.join(pkgDir, 'src', 'generated');
fs.mkdirSync(absoluteOutputDir, { recursive: true });
fs.writeFileSync(
path.join(pkgDir, 'package.json'),
JSON.stringify({ name: 'app' }),
);
fs.writeFileSync(
path.join(absoluteOutputDir, 'manifest.json'),
JSON.stringify(manifestData),
);
const code =
"import { graphql } from './generated/graphql'; const q = graphql(`query { me { id } }`);";
const pluginPath = path.join(process.cwd(), 'index.js');
const presetPath = require.resolve('@babel/preset-typescript');
const filePath = path.join(pkgDir, 'src', 'test.ts');
const output = execFileSync(
process.execPath,
[
'-e',
`
const babel = require('@babel/core');
const plugin = require(${JSON.stringify(pluginPath)});
process.chdir(${JSON.stringify(rootDir)});
const result = babel.transformSync(${JSON.stringify(code)}, {
plugins: [[plugin, { outputDir: ${JSON.stringify(outputDir)} }]],
presets: [${JSON.stringify(presetPath)}],
filename: ${JSON.stringify(filePath)},
babelrc: false,
configFile: false,
});
process.stdout.write(result.code);
`,
],
{
cwd: process.cwd(),
encoding: 'utf8',
},
);
expect(output).toContain(
'import { MyQueryDocument } from "./generated/query.codegen";',
);
});
it('rewrites graphql calls when manifestPath is relative and cwd differs from package root', () => {
const rootDir = tmpDir;
const pkgDir = path.join(rootDir, 'packages', 'app');
const outputDir = './src/generated';
const absoluteOutputDir = path.join(pkgDir, 'src', 'generated');
const relativeManifestPath = './custom-manifest.json';
const absoluteManifestPath = path.join(pkgDir, 'custom-manifest.json');
fs.mkdirSync(absoluteOutputDir, { recursive: true });
fs.writeFileSync(
path.join(pkgDir, 'package.json'),
JSON.stringify({ name: 'app' }),
);
fs.writeFileSync(
absoluteManifestPath,
JSON.stringify(manifestData),
);
const code =
"import { graphql } from './generated/graphql'; const q = graphql(`query { me { id } }`);";
const pluginPath = path.join(process.cwd(), 'index.js');
const presetPath = require.resolve('@babel/preset-typescript');
const filePath = path.join(pkgDir, 'src', 'test.ts');
const output = execFileSync(
process.execPath,
[
'-e',
`
const babel = require('@babel/core');
const plugin = require(${JSON.stringify(pluginPath)});
process.chdir(${JSON.stringify(rootDir)});
const result = babel.transformSync(${JSON.stringify(code)}, {
plugins: [[plugin, {
outputDir: ${JSON.stringify(outputDir)},
manifestPath: ${JSON.stringify(relativeManifestPath)}
}]],
presets: [${JSON.stringify(presetPath)}],
filename: ${JSON.stringify(filePath)},
babelrc: false,
configFile: false,
});
process.stdout.write(result.code);
`,
],
{
cwd: process.cwd(),
encoding: 'utf8',
},
);
expect(output).toContain(
'import { MyQueryDocument } from "./generated/query.codegen";',
);
});

@@ -189,0 +297,0 @@ it('ignores tsconfig paths that do not point to output dir', () => {

+220
-24

@@ -62,4 +62,18 @@ const path = require('path');

const absoluteOutputDir = path.resolve(outputDir);
const currentFile = state.file.opts.filename;
const currentDir = currentFile ? path.dirname(currentFile) : process.cwd();
const tsconfigPath = findNearestFile(currentDir, 'tsconfig.json');
const pkgJsonPath = findNearestFile(currentDir, 'package.json');
const rootDir = (pkgJsonPath || tsconfigPath) ? path.dirname(pkgJsonPath || tsconfigPath) : process.cwd();
const absoluteOutputDir = path.isAbsolute(outputDir)
? outputDir
: path.resolve(rootDir, outputDir);
// Resolve manifestPath relative to rootDir if provided as relative
if (manifestPath && !path.isAbsolute(manifestPath)) {
manifestPath = path.resolve(rootDir, manifestPath);
}
// Default manifestPath if not provided

@@ -70,9 +84,5 @@ if (!manifestPath && !manifestData) {

const currentFile = state.file.opts.filename;
const currentDir = currentFile ? path.dirname(currentFile) : process.cwd();
// Auto-detect import paths from tsconfig.json and package.json
const importPathsSet = new Set(configuredImportPaths.map(toPosixPath));
const tsconfigPath = findNearestFile(currentDir, 'tsconfig.json');
if (tsconfigPath) {

@@ -85,3 +95,2 @@ const paths = resolveTsConfigPaths(tsconfigPath, absoluteOutputDir);

const pkgJsonPath = findNearestFile(currentDir, 'package.json');
if (pkgJsonPath) {

@@ -194,2 +203,111 @@ const imports = resolvePackageJsonImports(pkgJsonPath, absoluteOutputDir);

const getStaticString = (node) => {
if (t.isStringLiteral(node)) {
return node.value;
}
if (
t.isTemplateLiteral(node) &&
node.expressions.length === 0 &&
node.quasis.length === 1
) {
return node.quasis[0].value.cooked || node.quasis[0].value.raw;
}
return null;
};
const getRelativeImportPath = (entryPath) => {
const codegenAbsPath = path.join(absoluteOutputDir, entryPath);
let relPath = path.relative(path.dirname(currentFile), codegenAbsPath);
relPath = toPosixPath(relPath);
if (!relPath.startsWith('.') && !path.isAbsolute(relPath)) {
relPath = './' + relPath;
}
return relPath + extension;
};
const getDynamicImportInfo = (exprPath) => {
if (!exprPath?.node) {
return null;
}
if (exprPath.isAwaitExpression()) {
const importCallPath = exprPath.get('argument');
if (!importCallPath.isCallExpression()) {
return null;
}
if (!importCallPath.get('callee').isImport()) {
return null;
}
const [sourceArgPath] = importCallPath.get('arguments');
if (!sourceArgPath?.node) {
return null;
}
return { awaited: true, sourceArgPath };
}
if (!exprPath.isCallExpression() || !exprPath.get('callee').isImport()) {
return null;
}
const [sourceArgPath] = exprPath.get('arguments');
if (!sourceArgPath?.node) {
return null;
}
return { awaited: false, sourceArgPath };
};
const buildDynamicImportRewrite = (requests, scope) => {
const moduleNameByPath = new Map();
const uniquePaths = [];
for (const { sourcePath } of requests) {
if (!moduleNameByPath.has(sourcePath)) {
moduleNameByPath.set(sourcePath, scope.generateUid('graphoxModule'));
uniquePaths.push(sourcePath);
}
}
if (uniquePaths.length === 1) {
return t.callExpression(t.import(), [t.stringLiteral(uniquePaths[0])]);
}
const imports = uniquePaths.map((sourcePath) =>
t.callExpression(t.import(), [t.stringLiteral(sourcePath)])
);
const moduleParams = uniquePaths.map((sourcePath) =>
t.identifier(moduleNameByPath.get(sourcePath))
);
const properties = requests.map(({ importedName, sourcePath }) =>
t.objectProperty(
t.identifier(importedName),
t.memberExpression(
t.identifier(moduleNameByPath.get(sourcePath)),
t.identifier(importedName),
),
),
);
return t.callExpression(
t.memberExpression(
t.callExpression(
t.memberExpression(t.identifier('Promise'), t.identifier('all')),
[t.arrayExpression(imports)],
),
t.identifier('then'),
),
[
t.arrowFunctionExpression(
[t.arrayPattern(moduleParams)],
t.objectExpression(properties),
),
],
);
};
const graphqlIds = new Set();

@@ -266,10 +384,3 @@ // newImports stores: localName -> { sourcePath, importedName }

const entry = documentNameToEntry.get(importedName);
const codegenAbsPath = path.join(absoluteOutputDir, entry.path);
let relPath = path.relative(path.dirname(currentFile), codegenAbsPath);
relPath = toPosixPath(relPath);
if (!relPath.startsWith('.') && !path.isAbsolute(relPath)) {
relPath = './' + relPath;
}
// Append the emit extension
relPath += extension;
const relPath = getRelativeImportPath(entry.path);

@@ -339,10 +450,3 @@ // If the original import was aliased (e.g. import { D as MyD }),

const codegenAbsPath = path.join(absoluteOutputDir, entry.path);
let relPath = path.relative(path.dirname(currentFile), codegenAbsPath);
relPath = toPosixPath(relPath);
if (!relPath.startsWith('.') && !path.isAbsolute(relPath)) {
relPath = './' + relPath;
}
// Append the emit extension
relPath += extension;
const relPath = getRelativeImportPath(entry.path);

@@ -357,2 +461,70 @@ const uniqueLocalName = getLocalName(entry.name, callPath.scope);

// Third pass: rewrite supported dynamic import patterns from graphql.ts/index.ts.
programPath.traverse({
VariableDeclarator(varPath) {
const initPath = varPath.get('init');
const info = getDynamicImportInfo(initPath);
if (!info) {
return;
}
const source = getStaticString(info.sourceArgPath.node);
if (!source || !isOurGraphqlPath(source)) {
return;
}
const idPath = varPath.get('id');
if (!idPath.isObjectPattern()) {
throw varPath.buildCodeFrameError(
`@graphox/babel-plugin could not fully rewrite this dynamic import from "${source}". ` +
'Use object destructuring of named documents from the generated graphql entrypoint or split the import by document.',
);
}
const requests = [];
for (const propertyPath of idPath.get('properties')) {
if (!propertyPath.isObjectProperty() || propertyPath.node.computed) {
throw propertyPath.buildCodeFrameError(
`@graphox/babel-plugin could not fully rewrite this dynamic import from "${source}". ` +
'Use object destructuring of named documents from the generated graphql entrypoint or split the import by document.',
);
}
const key = propertyPath.node.key;
const importedName = t.isIdentifier(key)
? key.name
: t.isStringLiteral(key)
? key.value
: null;
if (!importedName || importedName === 'graphql' || importedName === 'gql') {
throw propertyPath.buildCodeFrameError(
`@graphox/babel-plugin could not fully rewrite this dynamic import from "${source}". ` +
'Use object destructuring of named documents from the generated graphql entrypoint or split the import by document.',
);
}
const entry = documentNameToEntry.get(importedName);
if (!entry) {
throw propertyPath.buildCodeFrameError(
`@graphox/babel-plugin could not rewrite "${importedName}" from "${source}". ` +
'Run Graphox codegen and ensure the manifest includes this document.',
);
}
requests.push({
importedName,
sourcePath: getRelativeImportPath(entry.path),
});
}
if (requests.length === 0) {
return;
}
const replacement = buildDynamicImportRewrite(requests, varPath.scope);
initPath.replaceWith(info.awaited ? t.awaitExpression(replacement) : replacement);
},
});
// Ensure we never remove the entrypoint import while runtime references still exist.

@@ -377,4 +549,28 @@ programPath.traverse({

// Third pass: remove ALL imports from graphql.ts/index.ts
// Reject runtime dynamic imports we could not rewrite before the entrypoint is cleared.
programPath.traverse({
CallExpression(callPath) {
if (!callPath.get('callee').isImport()) {
return;
}
const [sourceArgPath] = callPath.get('arguments');
if (!sourceArgPath?.node) {
return;
}
const source = getStaticString(sourceArgPath.node);
if (!source || !isOurGraphqlPath(source)) {
return;
}
throw callPath.buildCodeFrameError(
`@graphox/babel-plugin could not fully rewrite this dynamic import from "${source}". ` +
'Use object destructuring of named documents from the generated graphql entrypoint or split the import by document.',
);
},
});
// Fourth pass: remove ALL imports from graphql.ts/index.ts
programPath.traverse({
ImportDeclaration(importPath) {

@@ -381,0 +577,0 @@ const src = importPath.node.source.value;

@@ -212,2 +212,78 @@ import { describe, it, expect } from 'vitest';

it('rewrites dynamic import destructuring from graphql.js to the codegen module', () => {
const manifest = [
{
source: 'mutation CreatePlaybackClient { createPlaybackClient { id } }',
path: './CreatePlaybackClientMutation.codegen',
name: 'CreatePlaybackClientDocument',
},
];
const outputDir = path.resolve('/root/gen');
const filename = '/root/app/TokenManager.ts';
const output = transform(
`
async function load() {
const { CreatePlaybackClientDocument } = await import('../gen/graphql.js');
return CreatePlaybackClientDocument;
}
`,
{ manifestData: manifest, outputDir },
filename,
);
expect(output).toContain('CreatePlaybackClientDocument');
expect(output).toContain('await import("../gen/CreatePlaybackClientMutation.codegen")');
expect(output).not.toContain('graphql.js');
});
it('rewrites multi-document dynamic imports across codegen files', () => {
const manifest = [
{
source: 'query GetUser { user { id } }',
path: './user.codegen',
name: 'GetUserDocument',
},
{
source: 'query GetPost { post { id } }',
path: './post.codegen',
name: 'GetPostDocument',
},
];
const output = transform(
`
async function load() {
const { GetUserDocument, GetPostDocument } = await import('./gen/graphql.js');
return [GetUserDocument, GetPostDocument];
}
`,
{ manifestData: manifest, outputDir: './gen' },
'test.ts',
);
expect(output).toContain('Promise.all([import("./gen/user.codegen"), import("./gen/post.codegen")])');
expect(output).toMatch(/GetUserDocument:\s*_graphoxModule\d*\.GetUserDocument/);
expect(output).toMatch(/GetPostDocument:\s*_graphoxModule\d*\.GetPostDocument/);
expect(output).not.toContain('graphql.js');
});
it('throws on unsupported dynamic import namespaces from the graphql entrypoint', () => {
const manifest = [
{
source: 'query GetUser { user { id } }',
path: './user.codegen',
name: 'GetUserDocument',
},
];
const code = `
async function load() {
const docs = await import('./gen/graphql.js');
return docs.GetUserDocument;
}
`;
expect(() => transform(code, { manifestData: manifest, outputDir: './gen' })).toThrow(
/could not fully rewrite this dynamic import from "\.\/gen\/graphql\.js"/,
);
});
describe('emit extensions', () => {

@@ -214,0 +290,0 @@ it('appends .ts extension when emitExtensions is "ts"', () => {

+1
-1
{
"name": "@graphox/babel-plugin",
"version": "0.4.4",
"version": "0.5.0",
"description": "Babel plugin for Graphox codesplitting",

@@ -5,0 +5,0 @@ "main": "index.js",