Skip to content

Feat rule webpack entry point #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions src/rules/eslint-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = {
types: {
ArrayExpression: 'ArrayExpression',
ArrowFunctionExpression: 'ArrowFunctionExpression',
AssignmentExpression: 'AssignmentExpression',
BlockStatement: 'BlockStatement',
CallExpression: 'CallExpression',
ExpressionStatement: 'ExpressionStatement',
FunctionExpression: 'FunctionExpression',
Identifier: 'Identifier',
MemberExpression: 'MemberExpression',
ObjectExpression: 'ObjectExpression',
Property: 'Property',
ReturnStatement: 'ReturnStatement',
},
};
227 changes: 227 additions & 0 deletions src/rules/webpack-entry-point.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
const path = require('path');
const { types } = require('./eslint-types');
/*
Ensure that the `webpack.config.js` file correctly specifies the entry point
as `aurelia-bootstrapper`.

It expects the export to look something like this (exporting either a function or object)

module.exports = function () {
return {
entry: {
app: ['aurelia-bootstrapper']
},
}
};
*/

const webpackConfigFileName = 'webpack.config.js';

const isAssignedToModuleExports = context => {
const ancestors = context.getAncestors();

const appProperty = ancestors.pop();
if (
!appProperty ||
appProperty.type !== types.Property ||
appProperty.key.name !== 'app'
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring appProperty', appProperty);

return false;
}

const entryObjectExpression = ancestors.pop();
if (
!entryObjectExpression ||
entryObjectExpression.type !== types.ObjectExpression
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring entryObjectExpression', entryObjectExpression);
return false;
}

const entryProperty = ancestors.pop();
if (
!entryProperty ||
entryProperty.type !== types.Property ||
entryProperty.key.name !== 'entry'
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring entryProperty', entryProperty);
return false;
}

const objectExpression = ancestors.pop();
if (!objectExpression || objectExpression.type !== types.ObjectExpression) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring objectExpression', objectExpression);
return false;
}

// TODO Handle direct assignment rather than functions
// TODO Handle arrow functions with body
const returnStatementOrArrowFunctionExpression = ancestors.pop();
if (!returnStatementOrArrowFunctionExpression) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log(
'Ignoring returnStatementOrArrowFunctionExpression',
returnStatementOrArrowFunctionExpression
);
return false;
}

if (returnStatementOrArrowFunctionExpression.type === types.ReturnStatement) {
const blockStatement = ancestors.pop();
if (!blockStatement || blockStatement.type !== types.BlockStatement) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring blockStatement', blockStatement);
return false;
}

const functionExpression = ancestors.pop();
if (
!functionExpression ||
functionExpression.type !== types.FunctionExpression
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring functionExpression', functionExpression);
return false;
}
} else if (
returnStatementOrArrowFunctionExpression.type ===
types.ArrowFunctionExpression
) {
// No additional tokens to consume
} else {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log(
'Ignoring unknown type returnStatementOrArrowFunctionExpression',
returnStatementOrArrowFunctionExpression
);
}

const assignmentExpression = ancestors.pop();
if (
!assignmentExpression ||
assignmentExpression.type !== types.AssignmentExpression ||
assignmentExpression.operator !== '='
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring assignmentExpression', assignmentExpression);
return false;
}

const expressionStatement = ancestors.pop();
if (
!expressionStatement ||
expressionStatement.type !== types.ExpressionStatement
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring expressionStatement', expressionStatement);
return false;
}

const leftHandSideOfExpressionStatement = expressionStatement.expression.left;
if (
!leftHandSideOfExpressionStatement ||
leftHandSideOfExpressionStatement.type !== types.MemberExpression
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log(
'Ignoring leftHandSideOfExpressionStatement',
leftHandSideOfExpressionStatement
);
return false;
}

const object = leftHandSideOfExpressionStatement.object;
if (!object || object.type !== types.Identifier || object.name !== 'module') {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring object', object);
return false;
}

const property = leftHandSideOfExpressionStatement.property;
if (
!property ||
property.type !== types.Identifier ||
property.name !== 'exports'
) {
// TODO: Remove Console
// eslint-disable-next-line no-console
console.log('Ignoring property', property);
return false;
}

return true;
};

const webpackEntryPointIsAureliaBootrap = context => node => {
const basename = path.basename(context.getFilename());

// Only webpack.config.js is checked
if (basename !== webpackConfigFileName) {
return;
}

if (node.name === 'app') {
if (!isAssignedToModuleExports(context)) {
return;
}

const parent = node.parent;
const value = parent.value;
if (value.type !== types.ArrayExpression) {
context.report({
node,
message: 'entry.app must be an array of strings',
});
return;
}
const elements = value.elements;

if (elements.length !== 1 || elements[0].value !== 'aurelia-bootstrapper') {
context.report({
node,
message:
"entry.app must be ['aurelia-bootstrapper']: found {{ value }}",
data: {
value: context.getSourceCode().getText(value),
},
});
return;
}
}
};

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'TODO',
category: 'aurelia',
fixable: 'code',
},
},
create: context => {
// state here

return {
Identifier: webpackEntryPointIsAureliaBootrap(context),
};
},
};
75 changes: 75 additions & 0 deletions test/rules/webpack-entry-point.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import eslint from 'eslint';
import rule from '../../src/rules/webpack-entry-point';

const ruleTester = new eslint.RuleTester({ parser: 'babel-eslint' });

const moduleExportsAsFunction = ({ app }) => `
module.exports = function () {
return {
entry: {
${app ? `app: ['${app}']` : ''}
},
}
};
`;

const moduleExportsAsArrowFunction = ({ app }) => `
module.exports = ({ production, server, extractCss, coverage, analyze, karma } = {}) => ({
entry: {
${app ? `app: ['${app}']` : ''}
},
});
`;

const embedFilenameInCode = ({ filename, code }) => ({
filename,
code: `// filename=${filename}\n${code}`,
});

// Use https://astexplorer.net/ and `espree` tokens to transform your `code`
// into an AST for use here.
ruleTester.run('webpack-entry-point', rule, {
valid: [
{
// Only webpack.config.js is checked for valid entry.app values
...embedFilenameInCode({
filename: '/some/dir/not.webpack.config.js',
code: moduleExportsAsFunction({ app: 'not-aurelia-bootstrapper' }),
}),
},
{
...embedFilenameInCode({
filename: '/some/dir/webpack.config.js',
code: moduleExportsAsArrowFunction({ app: 'aurelia-bootstrapper' }),
}),
},
],
invalid: [
{
...embedFilenameInCode({
filename: '/some/dir/webpack.config.js',
code: moduleExportsAsFunction({ app: 'not-aurelia-bootstrapper' }),
}),
errors: [
{
message:
"entry.app must be ['aurelia-bootstrapper']: found ['not-aurelia-bootstrapper']",
type: 'Identifier',
},
],
},
{
...embedFilenameInCode({
filename: '/some/dir/webpack.config.js',
code: moduleExportsAsArrowFunction({ app: 'not-aurelia-bootstrapper' }),
}),
errors: [
{
message:
"entry.app must be ['aurelia-bootstrapper']: found ['not-aurelia-bootstrapper']",
type: 'Identifier',
},
],
},
],
});