Skip to content

Commit

Permalink
Rename top level require functions in CommonJS [fix]
Browse files Browse the repository at this point in the history
Fixes #567.
  • Loading branch information
overlookmotel committed Dec 23, 2023
1 parent c354cef commit 74abd31
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 3 deletions.
12 changes: 11 additions & 1 deletion lib/init/getScopeId.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

'use strict';

const {defineProperties, getOwnPropertyDescriptors} = Object;
const {defineProperty, defineProperties, getOwnPropertyDescriptors} = Object;

// Exports

Expand All @@ -23,6 +23,7 @@ let nextScopeId = 1;

// Add additional static methods
getScopeId.toRest = toRest;
getScopeId.renameRequireAlias = renameRequireAlias;

/**
* Convert object to array.
Expand All @@ -33,3 +34,12 @@ getScopeId.toRest = toRest;
function toRest(obj) {
return defineProperties([], getOwnPropertyDescriptors(obj));
}

/**
* Set name property of a function to 'require'.
* @param {Function} req - Function
* @returns {Function} - Input function
*/
function renameRequireAlias(req) {
return defineProperty(req, 'name', {value: 'require'});
}
3 changes: 2 additions & 1 deletion lib/instrument/internalVars.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ module.exports = {
createTempVarNode,
createFnInfoVarNode,
checkInternalVarNameClash,
renameInternalVars
renameInternalVars,
addToInternalVars
};

/*
Expand Down
28 changes: 28 additions & 0 deletions lib/instrument/modify.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ function modifyAst(ast, filename, isCommonJs, isStrict, sources, evalState) {
trackerVarNode: undefined,
getScopeIdVarNode: undefined,
getSourcesNode: undefined,
requireAliasTempVarNode: undefined,
functions: [],
fileContainsFunctionsOrEval: false,
secondPass: (fn, ...params) => secondPassQueue.push({fn, params})
Expand Down Expand Up @@ -197,6 +198,7 @@ function modifySecondPass(ast, secondPassQueue, isEvalCode, sources, state) {
state.getSourcesNode = createFnInfoVarNode(0, state);
processQueue(secondPassQueue, state);
insertBlockVarsIntoBlockStatement(state.programBlock, programNode, state);
insertRequireAlias(programNode, state);
insertFunctionInfoFunctions(programNode, isEvalCode, sources, state);
}

Expand Down Expand Up @@ -295,6 +297,32 @@ function insertFunctionInfoFunctions(programNode, isEvalCode, sources, state) {
}
}

/**
* Insert alias reassignment for `require`.
* If CommonJS code containing top-level function declaration(s) called 'require',
* they've been renamed to a temporary name earlier in instrumentation.
* Insert `var` statement at top of file to restore the binding.
* @param {Object} programNode - Program AST node
* @param {Object} state - State object
* @returns {undefined}
*/
function insertRequireAlias(programNode, state) {
if (!state.requireAliasTempVarNode) return;

// `var require = livepack_getScopeId.renameRequireAlias(livepack_temp_3);`
programNode.body.unshift(
t.variableDeclaration('var', [
t.variableDeclarator(
t.identifier('require'),
t.callExpression(
t.memberExpression(state.getScopeIdVarNode, t.identifier('renameRequireAlias')),
[state.requireAliasTempVarNode]
)
)
])
);
}

/**
* Get current AST node, to get location where instrumentation failed and threw error.
* Uses current function node and trail to get node. If no node at that trail, get parent node.
Expand Down
28 changes: 27 additions & 1 deletion lib/instrument/visitors/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const Statement = require('./statement.js'),
createNewTargetBinding, getOrCreateExternalVar
} = require('../blocks.js'),
{insertTrackerCodeIntoFunction} = require('../tracking.js'),
{createFnInfoVarNode} = require('../internalVars.js'),
{createFnInfoVarNode, createTempVarNode, addToInternalVars} = require('../internalVars.js'),
{insertComment, hasUseStrictDirective, stringLiteralWithSingleQuotes} = require('../utils.js'),
{combineArraysWithDedup} = require('../../shared/functions.js'),
{
Expand Down Expand Up @@ -109,6 +109,12 @@ function FunctionDeclaration(node, state, parent, key) {

// Insert tracker comment
insertFunctionDeclarationOrExpressionTrackerComment(fn, node, state);

// If top-level function declaration called 'require' in CommonJS, rename it in 2nd pass
if (
fnName === 'require' && block === state.programBlock
&& state.programBlock.parent.bindings.has('require')
) state.secondPass(renameRequireFunctionDeclaration, node, state);
}

/**
Expand Down Expand Up @@ -691,6 +697,26 @@ function insertArrowFunctionTrackerComment(fn, node, state) {
);
}

/**
* Rename top-level function declaration in CommonJS file called `require` to a temp name.
* Otherwise, this prevents loading Livepack's `init` code.
* At end of instrumentation, a `var require =` statement will be added at top of file
* to restore the binding.
* @param {Object} fnNode - Function declaration AST node
* @param {Object} state - State object
* @returns {undefined}
*/
function renameRequireFunctionDeclaration(fnNode, state) {
// Use same temp var name for all `require` functions in this file
const tempVarNode = state.requireAliasTempVarNode
|| (state.requireAliasTempVarNode = createTempVarNode(state));

// Set `name` of existing identifier, rather than replacing with `tempVarNode`
// to preserve any attached comments (including tracker comment)
fnNode.id.name = tempVarNode.name;
addToInternalVars(fnNode.id, state);
}

/**
* Insert tracker comment.
* @param {number} fnId - Function ID
Expand Down
155 changes: 155 additions & 0 deletions test/commonjs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,161 @@ describe('`require`', () => {
);
});
});

describe('local functions called `require` do not break instrumentation', () => {
describe('1 function', () => {
itSerializes('sloppy mode', {
in: `
module.exports = require;
const ext = {x: 1};
function require() {
return ext;
}
`,
strictEnv: false,
out: '(a=>function require(){return a})({x:1})',
validate(fn) {
expect(fn).toBeFunction();
expect(fn.name).toBe('require');
expect(fn()).toEqual({x: 1});
}
});

itSerializes('strict mode', {
in: `
'use strict';
module.exports = require;
const ext = {x: 1};
function require() {
return ext;
}
`,
out: '(a=>function require(){return a})({x:1})',
validate(fn) {
expect(fn).toBeFunction();
expect(fn.name).toBe('require');
expect(fn()).toEqual({x: 1});
}
});
});

describe('multiple functions', () => {
itSerializes('sloppy mode', {
in: `
module.exports = require;
const extA = {x: 1},
extB = {y: 2},
extC = {z: 3};
function require() {
return extA;
}
function require() {
return extB;
}
function require() {
return extC;
}
`,
strictEnv: false,
out: '(a=>function require(){return a})({z:3})',
validate(fn) {
expect(fn).toBeFunction();
expect(fn.name).toBe('require');
expect(fn()).toEqual({z: 3});
}
});

itSerializes('strict mode', {
in: `
'use strict';
module.exports = require;
const extA = {x: 1},
extB = {y: 2};
function require() {
return extA;
}
function require() {
return extB;
}
`,
out: '(a=>function require(){return a})({y:2})',
validate(fn) {
expect(fn).toBeFunction();
expect(fn.name).toBe('require');
expect(fn()).toEqual({y: 2});
}
});
});

itSerializes('function prefixed with label', {
in: `
module.exports = require;
const ext = {x: 1};
foo: function require() {
return ext;
}
`,
strictEnv: false,
out: '(a=>function require(){return a})({x:1})',
validate(fn) {
expect(fn).toBeFunction();
expect(fn.name).toBe('require');
expect(fn()).toEqual({x: 1});
}
});

describe('function in nested block is not hoisted', () => {
itSerializes('when no top level function', {
in: {
'index.js': `
let inner;
{
inner = require;
function require() {
return 456;
}
}
module.exports = [require('./other.js'), inner];
`,
'other.js': 'module.exports = 123;'
},
out: '[123,function require(){return 456}]',
strictEnv: false,
validate([num, fn]) {
expect(num).toBe(123);
expect(fn).toBeFunction();
expect(fn.name).toBe('require');
expect(fn()).toBe(456);
}
});

itSerializes('when also top level function', {
in: `
let inner;
{
inner = require;
function require() {
return 456;
}
}
function require() {
return 123;
}
module.exports = [require, inner];
`,
out: '[function require(){return 123},function require(){return 456}]',
strictEnv: false,
validate([fn, inner]) {
expect(fn).toBeFunction();
expect(fn.name).toBe('require');
expect(fn()).toBe(123);
expect(inner).toBeFunction();
expect(inner.name).toBe('require');
expect(inner()).toBe(456);
}
});
});
});
});

describe('`require.resolve`', () => {
Expand Down

0 comments on commit 74abd31

Please sign in to comment.