diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..297061b --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + presets: ['stage-1', 'es2015'] +} diff --git a/.gitignore b/.gitignore index 782c2cb..4b56bdc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ dist/ node_modules/ npm-debug.log coverage/ +package-lock.json \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..792bb9d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,34 @@ +*.min.js +**/node_modules/** +flow-typed + +# webfont demo styles +**/specimen_files + +# built sites +benchmarks/**/public +e2e-tests/**/public +examples/**/public +integration-tests/**/public +www/public + +# cache-dirs +**/.cache + +# ignore built packages +packages/**/*.js +!packages/gatsby/cache-dir/**/*.js +!packages/*/src/**/*.js +packages/gatsby/cache-dir/commonjs/**/*.js + +# fixtures +**/__testfixtures__/** +**/__tests__/fixtures/** + +infrastructure + +# coverage +coverage + +# forestry files +.forestry/**/* diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..da515b4 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,19 @@ +module.exports = { + endOfLine: 'lf', + semi: true, + singleQuote: true, + useTabs: true, + trailingComma: 'es5', + overrides: [ + { + // This file uses semicolons. It's needed here because `documentation` + // package (used to parse jsdoc and provide content for API reference pages) + // behaviour is inconsistent when not using semicolons after + // object declarations. + files: ['**/api-node-helpers-docs.js'], + options: { + semi: true, + }, + }, + ], +} diff --git a/package.json b/package.json index 6872e05..fb415af 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ ], "scripts": { "build": "npm run lint && npm run test && rm -rf dist && babel src --out-dir dist", - "test": "babel-node node_modules/.bin/isparta cover --report text-summary --report lcov node_modules/mocha/bin/_mocha -- --recursive", + "test": "node node_modules/isparta/bin/isparta cover --report text-summary --report lcov node_modules/mocha/bin/_mocha -- --recursive", "lint": "eslint src/ test/", "ci": "npm run build && cat coverage/lcov.info | node_modules/.bin/coveralls", "patch": "release patch", @@ -32,7 +32,8 @@ "major": "release major" }, "dependencies": { - "eslint-plugin-sort-class-members": "^1.0.1" + "eslint-plugin-sort-class-members": "^1.0.1", + "prettier": "^1.18.2" }, "peerDependencies": { "eslint": ">=0.8.0" @@ -40,9 +41,11 @@ "devDependencies": { "@bryanrsmith/eslint-config-standard": "^2.1.3", "babel-cli": "^6.9.0", + "babel-core": "^6.26.3", "babel-eslint": "^6.0.4", "babel-preset-es2015": "^6.9.0", "babel-preset-stage-1": "^6.5.0", + "babel-register": "^6.26.0", "coveralls": "^2.11.9", "eslint": "^2.13.0", "isparta": "^4.0.0", @@ -52,11 +55,5 @@ "engines": { "node": ">=4.0.0" }, - "license": "MIT", - "babel": { - "presets": [ - "stage-1", - "es2015" - ] - } + "license": "MIT" } diff --git a/src/index.js b/src/index.js index 0bb9eea..1b7d130 100644 --- a/src/index.js +++ b/src/index.js @@ -2,8 +2,10 @@ import injectRule from './rules/inject-matches-ctor'; import injectTypeRule from './rules/inject-type'; import noConventionsRule from './rules/no-conventions'; import noConsoleLogRule from './rules/no-console-log'; +import platformModulename from './rules/platform-modulename'; import storeUnsubscribeRule from './rules/store-unsubscribe'; import sortClassMembers, { defaultOrder } from './rules/sort-class-members'; +import webpackEntryPoint from './rules/webpack-entry-point'; module.exports = { rules: { @@ -11,8 +13,10 @@ module.exports = { 'inject-type': injectTypeRule, 'no-conventions': noConventionsRule, 'no-console-log': noConsoleLogRule, + 'platform-modulename': platformModulename, 'store-unsubscribe': storeUnsubscribeRule, 'sort-class-members': sortClassMembers, + 'webpack-entry-point': webpackEntryPoint, }, configs: { recommended: { @@ -21,10 +25,12 @@ module.exports = { 'inject-type': 0, 'no-conventions': 0, 'no-console-log': 0, + 'platform-modulename': 0, 'store-unsubscribe': 2, 'sort-class-members': [ 2, { order: defaultOrder, }], + 'webpack-entry-point': 0, }, }, }, diff --git a/src/rules/eslint-types.js b/src/rules/eslint-types.js new file mode 100644 index 0000000..1ee726c --- /dev/null +++ b/src/rules/eslint-types.js @@ -0,0 +1,20 @@ +module.exports = { + types: { + ArrayExpression: 'ArrayExpression', + ArrowFunctionExpression: 'ArrowFunctionExpression', + AssignmentExpression: 'AssignmentExpression', + BlockStatement: 'BlockStatement', + CallExpression: 'CallExpression', + ExpressionStatement: 'ExpressionStatement', + FunctionDeclaration: 'FunctionDeclaration', + FunctionExpression: 'FunctionExpression', + Identifier: 'Identifier', + Literal: 'Literal', + MemberExpression: 'MemberExpression', + MethodDefinition: 'MethodDefinition', + ObjectExpression: 'ObjectExpression', + Program: 'Program', + Property: 'Property', + ReturnStatement: 'ReturnStatement', + }, +}; diff --git a/src/rules/platform-modulename.js b/src/rules/platform-modulename.js new file mode 100644 index 0000000..5a510b8 --- /dev/null +++ b/src/rules/platform-modulename.js @@ -0,0 +1,346 @@ +/* + Ensure that module usage is wrapped in `PLATFORM.moduleName()`. +*/ + +const { types } = require('./eslint-types'); + +const identity = x => x; + +const getName = node => { + if (node.type === types.Identifier) { + return node.name; + } + + if (node.type === types.MemberExpression) { + return `${getName(node.property)}`; + } + + return `getName() unknown Node type ${node.type}`; +}; +const nameOfNode = node => { + if (node.type === types.MethodDefinition) { + return node.key.name; + } + + return undefined; +}; + +const lookupInParentHierarchy = (node, type, name) => { + let current = node.parent; + while (current.type !== types.Program) { + if (current.type === type && nameOfNode(current) === name) { + return current; + } + current = current.parent; + } + + return undefined; +}; + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Enforce wrapping of module name in PLATFORM.moduleName()', + category: 'aurelia', + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + debug: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + }, + create: context => { + let aureliaParameter; + let isDebugEnabled = false; + + if (context.options && context.options.length >= 1) { + isDebugEnabled = context.options[0].debug; + } + + const logDebug = (...args) => { + if (isDebugEnabled) { + console.log('DEBUG:', ...args); + } + }; + + const wrapInPlatformModuleName = node => fixer => + fixer.replaceText(node, `PLATFORM.moduleName('${node.value}')`); + + const captureAureliaConfigure = exportNamedDeclaration => { + const functionDeclaration = exportNamedDeclaration.declaration; + if (functionDeclaration.type !== types.FunctionDeclaration) { + return; + } + + if ( + !functionDeclaration.id || + functionDeclaration.id.name !== 'configure' + ) { + return; + } + + const params = functionDeclaration.params; + if (params.length !== 1) { + logDebug( + 'captureAureliaConfigure():', + 'Ignoring "configure" function with incorrect params', + functionDeclaration + ); + + return; + } + + aureliaParameter = params[0]; + }; + + const calleeObjectIsAurelia = callee => { + if (callee.type !== types.MemberExpression) { + logDebug('calleeObjectIsAurelia():', 'Ignoring callee', callee); + + return false; + } + + if (!aureliaParameter) { + logDebug( + 'calleeObjectIsAurelia():', + 'Ignoring calleeObjectIsAurelia check, aureliaParameter is not set', + callee + ); + + return false; + } + + if (aureliaParameter.name !== callee.object.name) { + logDebug( + 'calleeObjectIsAurelia():', + 'Ignoring callee as does not use aurelia', + aureliaParameter, + callee + ); + + return false; + } + return true; + }; + + const calleeObjectIsAureliaUse = callee => { + // Check for calls like aurelia.use.(); + if (callee.type !== types.MemberExpression) { + logDebug( + 'calleeObjectIsAureliaUse():', + 'Ignoring callee as wrong type', + callee + ); + + return false; + } + + const object = callee.object; // MemberExpression of call + const objectProperty = object.property; + + if (!objectProperty) { + logDebug( + 'calleeObjectIsAureliaUse():', + 'Ignoring callee as object has no property', + callee + ); + + return false; + } + + if ( + objectProperty.type !== types.Identifier || + objectProperty.name !== 'use' + ) { + logDebug( + 'calleeObjectIsAureliaUse():', + 'Ignoring property as not .use', + objectProperty + ); + + return false; + } + + if (!calleeObjectIsAurelia(object)) { + return false; + } + + return true; + }; + + const nodeIsCallToPlatformModuleName = node => + node && + node.type === types.CallExpression && + node.callee.object.name === 'PLATFORM' && + node.callee.property.name === 'moduleName'; + + const reportMustWrapModules = callName => node => + context.report({ + node, + message: `${callName} must wrap modules with 'PLATFORM.moduleName()'`, + fix: wrapInPlatformModuleName(node), + }); + + const transformRouteToModuleIdNode = routeNode => { + if (routeNode.type !== types.ObjectExpression) { + return undefined; + } + const moduleIdProperty = routeNode.properties.filter( + property => property.key.name === 'moduleId' + )[0]; + if (!moduleIdProperty) { + return undefined; + } + return moduleIdProperty.value; + }; + + const checkArgumentsWrappedInPlatformModuleName = ( + callExpression, + { transformation } = { transformation: identity } + ) => { + const arg1 = callExpression.arguments[0]; + // Treat: single arg call and array arg call the same + const args = arg1.type === types.ArrayExpression ? arg1.elements : [arg1]; // eslint-disable-line array-bracket-spacing + + const callName = getName(callExpression.callee); + args + .map(transformation) + .filter(arg => arg && !nodeIsCallToPlatformModuleName(arg)) + .map(reportMustWrapModules(callName)); + }; + + const checkRouterConfig = node => { + // https://aurelia.io/docs/api/router/interface/ConfiguresRouter/method/configureRouter + const callee = node.callee; + const calleeObject = callee.object; + + if (!calleeObject) { + logDebug( + 'checkRouterConfig():', + 'Ignoring missing callee.object', + callee + ); + return; + } + + const containedWithinMethodDefinition = lookupInParentHierarchy( + node, + types.MethodDefinition, + 'configureRouter' + ); + if (!containedWithinMethodDefinition) { + logDebug( + 'checkRouterConfig():', + 'Ignoring as not contained within MethodDefinition', + node + ); + return; + } + + const methodValue = containedWithinMethodDefinition.value; + if (methodValue.type !== types.FunctionExpression) { + logDebug( + 'checkRouterConfig():', + 'Ignoring as contained within MethodDefinition does not have FunctionExpression as value', + containedWithinMethodDefinition + ); + return; + } + + const params = methodValue.params; + if (!params && params.length !== 2) { + logDebug( + 'checkRouterConfig():', + 'Ignoring as MethodDefinition as FunctionExpression does not have correct signature', + containedWithinMethodDefinition + ); + return; + } + + if (calleeObject.name !== params[0].name) { + logDebug( + 'checkRouterConfig():', + 'Ignoring as call of .map as not on Router', + containedWithinMethodDefinition + ); + return; + } + + // https://aurelia.io/docs/api/router/class/RouterConfiguration/method/map + // https://aurelia.io/docs/api/router/interface/RouteConfig + // Either a single RouteConfig or an array of them. + checkArgumentsWrappedInPlatformModuleName(node, { + transformation: transformRouteToModuleIdNode, + }); + }; + + const usesPlatformModuleName = node => { + const callee = node.callee; + + if ( + callee.property && + callee.property.name === 'globalResources' && + node.arguments.length === 1 && + calleeObjectIsAureliaUse(callee) + ) { + checkArgumentsWrappedInPlatformModuleName(node); + return; + } + + if ( + callee.property && + callee.property.name === 'setRoot' && + node.arguments.length === 1 && + calleeObjectIsAurelia(callee) + ) { + checkArgumentsWrappedInPlatformModuleName(node); + + return; + } + + if ( + callee.property && + callee.property.name === 'feature' && + node.arguments.length === 1 && + calleeObjectIsAureliaUse(callee) + ) { + checkArgumentsWrappedInPlatformModuleName(node); + return; + } + + if ( + callee.property && + callee.property.name === 'plugin' && + node.arguments.length === 1 && + calleeObjectIsAureliaUse(callee) + ) { + checkArgumentsWrappedInPlatformModuleName(node); + return; + } + + if ( + callee.property && + callee.property.name === 'map' && + node.arguments.length === 1 + ) { + checkRouterConfig(node); + return; + } + + return; + }; + + return { + ExportNamedDeclaration: captureAureliaConfigure, + CallExpression: usesPlatformModuleName, + }; + }, +}; diff --git a/src/rules/webpack-entry-point.js b/src/rules/webpack-entry-point.js new file mode 100644 index 0000000..00f5796 --- /dev/null +++ b/src/rules/webpack-entry-point.js @@ -0,0 +1,223 @@ +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: + "Expected entry.app to be ['aurelia-bootstrapper'] but found {{ value }}", + data: { + value: context.getSourceCode().getText(value), + }, + }); + return; + } + } +}; + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'TODO', + category: 'aurelia', + fixable: 'code', + }, + }, + create: context => ({ + Identifier: webpackEntryPointIsAureliaBootrap(context), + }), +}; diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..52fdec4 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ + --compilers js:babel-register \ No newline at end of file diff --git a/test/rules/platform-modulename-feature.spec.js b/test/rules/platform-modulename-feature.spec.js new file mode 100644 index 0000000..e2fa663 --- /dev/null +++ b/test/rules/platform-modulename-feature.spec.js @@ -0,0 +1,42 @@ +// Tests for https://aurelia.io/docs/build-systems/webpack/a-basic-example#introduction + +import eslint from 'eslint'; +import rule from '../../src/rules/platform-modulename'; + +const ruleTester = new eslint.RuleTester({ parser: 'babel-eslint' }); + +const shouldEnableDebug = false; + +// Use https://astexplorer.net/ and `espree` tokens to transform your `code` +// into an AST for use here. +ruleTester.run('platform-modulename', rule, { + valid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.feature(PLATFORM.moduleName('./my-awesome-feature')) // OK +} +`, + }, + ], + + invalid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.feature('./my-awesome-feature') // WRONG +} +`, + errors: [ + { + message: "feature must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 3, + column: 23, + }, + ], + }, + ], +}); diff --git a/test/rules/platform-modulename-featureModule.spec.js b/test/rules/platform-modulename-featureModule.spec.js new file mode 100644 index 0000000..e0a7cbd --- /dev/null +++ b/test/rules/platform-modulename-featureModule.spec.js @@ -0,0 +1,25 @@ +// Tests for https://aurelia.io/docs/build-systems/webpack/a-basic-example#introduction + +import eslint from 'eslint'; +import rule from '../../src/rules/platform-modulename'; + +const ruleTester = new eslint.RuleTester({ parser: 'babel-eslint' }); + +const shouldEnableDebug = false; + +// Use https://astexplorer.net/ and `espree` tokens to transform your `code` +// into an AST for use here. +ruleTester.run('platform-modulename', rule, { + valid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` // A Feature Module index.js + export function configure(config) { + // Doesn't use PLATFORM.module() + config.globalResources(['./my-component', './my-component-2', 'my-component-3', 'etc.']); + } +`, + }, + ], + invalid: [], +}); diff --git a/test/rules/platform-modulename-plugin.spec.js b/test/rules/platform-modulename-plugin.spec.js new file mode 100644 index 0000000..348d9ea --- /dev/null +++ b/test/rules/platform-modulename-plugin.spec.js @@ -0,0 +1,42 @@ +// Tests for https://aurelia.io/docs/build-systems/webpack/a-basic-example#introduction + +import eslint from 'eslint'; +import rule from '../../src/rules/platform-modulename'; + +const ruleTester = new eslint.RuleTester({ parser: 'babel-eslint' }); + +const shouldEnableDebug = false; + +// Use https://astexplorer.net/ and `espree` tokens to transform your `code` +// into an AST for use here. +ruleTester.run('platform-modulename', rule, { + valid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.plugin(PLATFORM.moduleName('some-awesome-plugin')) // OK +} +`, + }, + ], + + invalid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.plugin('some-awesome-plugin') // WRONG +} +`, + errors: [ + { + message: "plugin must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 3, + column: 22, + }, + ], + }, + ], +}); diff --git a/test/rules/platform-modulename-router-config.spec.js b/test/rules/platform-modulename-router-config.spec.js new file mode 100644 index 0000000..eced176 --- /dev/null +++ b/test/rules/platform-modulename-router-config.spec.js @@ -0,0 +1,60 @@ +// Tests for https://aurelia.io/docs/build-systems/webpack/a-basic-example#introduction + +import eslint from 'eslint'; +import rule from '../../src/rules/platform-modulename'; + +const ruleTester = new eslint.RuleTester({ parser: 'babel-eslint' }); + +const shouldEnableDebug = false; + +// Use https://astexplorer.net/ and `espree` tokens to transform your `code` +// into an AST for use here. +ruleTester.run('platform-modulename', rule, { + valid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export class MyViewModel { + + configureRouter(config, router) { + config.map([ + { + route: '', + moduleId: PLATFORM.moduleName('pages/home') // OK + } + ]) + } + +} +`, + }, + ], + + invalid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export class MyViewModel { + + configureRouter(config, router) { + config.map([ + { + route: '', + moduleId: 'pages/home' // WRONG + } + ]) + } + +} +`, + errors: [ + { + message: "map must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 8, + column: 19, + }, + ], + }, + ], +}); diff --git a/test/rules/platform-modulename-setRoot.spec.js b/test/rules/platform-modulename-setRoot.spec.js new file mode 100644 index 0000000..b16c508 --- /dev/null +++ b/test/rules/platform-modulename-setRoot.spec.js @@ -0,0 +1,57 @@ +// Tests for https://aurelia.io/docs/build-systems/webpack/a-basic-example#introduction + +import eslint from 'eslint'; +import rule from '../../src/rules/platform-modulename'; + +const ruleTester = new eslint.RuleTester({ parser: 'babel-eslint' }); + +const shouldEnableDebug = false; + +// Use https://astexplorer.net/ and `espree` tokens to transform your `code` +// into an AST for use here. +ruleTester.run('platform-modulename', rule, { + valid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.setRoot(PLATFORM.moduleName('app')) // OK +} +`, + }, + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + // ignored as not calling on aurelia parameter + notAurelia.setRoot('app') // OK +} +`, + }, + { + options: [{ debug: shouldEnableDebug }], + code: ` + // ignored as not calling from within configure + aurelia.setRoot('app') // OK +`, + }, + ], + invalid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.setRoot('app') // WRONG +} +`, + errors: [ + { + message: "setRoot must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 3, + column: 19, + }, + ], + }, + ], +}); diff --git a/test/rules/platform-modulename-useGlobalResources.spec.js b/test/rules/platform-modulename-useGlobalResources.spec.js new file mode 100644 index 0000000..14fb284 --- /dev/null +++ b/test/rules/platform-modulename-useGlobalResources.spec.js @@ -0,0 +1,173 @@ +// Tests for https://aurelia.io/docs/build-systems/webpack/a-basic-example#introduction + +import eslint from 'eslint'; +import rule from '../../src/rules/platform-modulename'; + +const ruleTester = new eslint.RuleTester({ parser: 'babel-eslint' }); + +const shouldEnableDebug = false; + +// Use https://astexplorer.net/ and `espree` tokens to transform your `code` +// into an AST for use here. +ruleTester.run('platform-modulename', rule, { + valid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.globalResources( // single element + PLATFORM.moduleName('./my-custom-element') // OK + ) +} +`, + }, + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.globalResources([ // array of one + PLATFORM.moduleName('./my-custom-element') // OK + ]) +} +`, + }, + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(au) { + au.use.globalResources([ + PLATFORM.moduleName('./my-custom-element1'), // OK + PLATFORM.moduleName('./my-custom-element2'), // OK + PLATFORM.moduleName('./my-custom-element3'), // OK + ]) +} +`, + }, + ], + invalid: [ + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.globalResources( + './my-custom-element' // WRONG + ) +} +`, + errors: [ + { + message: + "globalResources must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 4, + column: 5, + }, + ], + output: ` +export function configure(aurelia) { + aurelia.use.globalResources( + PLATFORM.moduleName('./my-custom-element') // WRONG + ) +} +`, + }, + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.globalResources([ + './my-custom-element' // WRONG + ]) +} +`, + errors: [ + { + message: + "globalResources must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 4, + column: 5, + }, + ], + output: ` +export function configure(aurelia) { + aurelia.use.globalResources([ + PLATFORM.moduleName('./my-custom-element') // WRONG + ]) +} +`, + }, + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(au) { + au.use.globalResources([ + PLATFORM.moduleName('./my-custom-element1'), // OK + './my-custom-element2', // WRONG + PLATFORM.moduleName('./my-custom-element3'), // OK + ]) +} +`, + errors: [ + { + message: + "globalResources must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 5, + column: 5, + }, + ], + output: ` +export function configure(au) { + au.use.globalResources([ + PLATFORM.moduleName('./my-custom-element1'), // OK + PLATFORM.moduleName('./my-custom-element2'), // WRONG + PLATFORM.moduleName('./my-custom-element3'), // OK + ]) +} +`, + }, + { + options: [{ debug: shouldEnableDebug }], + code: ` +export function configure(aurelia) { + aurelia.use.globalResources([ + './my-custom-element1', // WRONG + './my-custom-element2', // WRONG + './my-custom-element3', // WRONG + ]) +}`, + errors: [ + { + message: + "globalResources must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 4, + column: 5, + }, + { + message: + "globalResources must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 5, + column: 5, + }, + { + message: + "globalResources must wrap modules with 'PLATFORM.moduleName()'", + type: 'Literal', + line: 6, + column: 5, + }, + ], + output: ` +export function configure(aurelia) { + aurelia.use.globalResources([ + PLATFORM.moduleName('./my-custom-element1'), // WRONG + PLATFORM.moduleName('./my-custom-element2'), // WRONG + PLATFORM.moduleName('./my-custom-element3'), // WRONG + ]) +}`, + }, + ], +}); diff --git a/test/rules/webpack-entry-point.spec.js b/test/rules/webpack-entry-point.spec.js new file mode 100644 index 0000000..9b3df3b --- /dev/null +++ b/test/rules/webpack-entry-point.spec.js @@ -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: + "Expected entry.app to be ['aurelia-bootstrapper'] but found ['not-aurelia-bootstrapper']", + type: 'Identifier', + }, + ], + }, + { + ...embedFilenameInCode({ + filename: '/some/dir/webpack.config.js', + code: moduleExportsAsArrowFunction({ app: 'not-aurelia-bootstrapper' }), + }), + errors: [ + { + message: + "Expected entry.app to be ['aurelia-bootstrapper'] but found ['not-aurelia-bootstrapper']", + type: 'Identifier', + }, + ], + }, + ], +});