| /** |
| * Copyright (c) HashiCorp, Inc. |
| * SPDX-License-Identifier: MPL-2.0 |
| */ |
| |
| import babylonParser from './jscodeshift-babylon-parser'; |
| |
| // use babylon parser with decorators-legacy plugin |
| export const parser = babylonParser; |
| |
| // checks for access of specified service on this |
| // injects service if not present and imports inject as service if needed |
| // example usage - npx jscodeshift -t ./scripts/codemods/inject-service.js ./app/**/*.js --service=store |
| // --service arg is required with name of service |
| // pass -d for dry run (no files transformed) |
| |
| export default function transformer({ source }, api, { service }) { |
| if (!service) { |
| throw new Error('Missing service arg. Pass --service=store for example to script'); |
| } |
| const j = api.jscodeshift; |
| const filterForService = (path) => { |
| return j(path.value).find(j.MemberExpression, { |
| object: { |
| type: 'ThisExpression', |
| }, |
| property: { |
| name: service, |
| }, |
| }).length; |
| }; |
| const recastOptions = { |
| reuseWhitespace: false, |
| wrapColumn: 110, |
| quote: 'single', |
| trailingComma: true, |
| }; |
| let didInjectService = false; |
| |
| // find class bodies and filter down to ones that access service |
| const classesAccessingService = j(source).find(j.ClassBody).filter(filterForService); |
| |
| if (classesAccessingService.length) { |
| // filter down to class bodies where service is not injected |
| const missingService = classesAccessingService.filter((path) => { |
| return !j(path.value) |
| .find(j.ClassProperty, { |
| key: { |
| name: service, |
| }, |
| }) |
| .filter((path) => { |
| // ensure service property belongs to service decorator |
| return path.value.decorators.find((path) => path.expression.name === 'service'); |
| }).length; |
| }); |
| |
| if (missingService.length) { |
| // inject service |
| const serviceInjection = j.classProperty(j.identifier(`@service ${service}`), null); |
| // adding a decorator this way will force injection down to a new line and then add a new line |
| // leaving in just in case it's needed |
| // serviceInjection.decorators = [j.decorator(j.identifier('service'))]; |
| |
| source = missingService |
| .forEach((path) => { |
| path.value.body.unshift(serviceInjection); |
| }) |
| .toSource(); |
| |
| didInjectService = true; |
| } |
| } |
| |
| // find .extend object expressions and filter down to ones that access this[service] |
| const objectsAccessingService = j(source) |
| .find(j.CallExpression, { |
| callee: { |
| type: 'MemberExpression', |
| property: { |
| name: 'extend', |
| }, |
| }, |
| }) |
| .filter(filterForService) |
| .find(j.ObjectExpression) |
| .filter((path) => { |
| // filter object expressions that belong to .extend |
| // otherwise service will also be injected in actions: { } block of component for example |
| const callee = path.parent.value.callee; |
| return callee && callee.property?.name === 'extend'; |
| }); |
| |
| if (objectsAccessingService.length) { |
| // filter down to objects where service is not injected |
| const missingService = objectsAccessingService.filter((path) => { |
| return !j(path.value).find(j.ObjectProperty, { |
| key: { |
| name: service, |
| }, |
| value: { |
| callee: { |
| name: 'service', |
| }, |
| }, |
| }).length; |
| }); |
| |
| if (missingService.length) { |
| // inject service |
| const serviceInjection = j.objectProperty( |
| j.identifier(service), |
| j.callExpression(j.identifier('service'), []) |
| ); |
| |
| source = missingService |
| .forEach((path) => { |
| path.value.properties.unshift(serviceInjection); |
| }) |
| .toSource(recastOptions); |
| |
| didInjectService = true; |
| } |
| } |
| |
| // if service was injected here check if inject has been imported |
| if (didInjectService) { |
| const needsImport = !j(source).find(j.ImportSpecifier, { |
| imported: { |
| name: 'inject', |
| }, |
| }).length; |
| |
| if (needsImport) { |
| const injectionImport = j.importDeclaration( |
| [j.importSpecifier(j.identifier('inject'), j.identifier('service'))], |
| j.literal('@ember/service') |
| ); |
| |
| const imports = j(source).find(j.ImportDeclaration); |
| source = imports |
| .at(imports.length - 1) |
| .insertAfter(injectionImport) |
| .toSource(recastOptions); |
| } |
| } |
| |
| return source; |
| } |