Skip to content
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

[PWA-355]: Add richer extension points and documentation #2298

Merged
merged 38 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6f61fa3
fix: resolve symlinked packages during configureWebpack
Apr 23, 2020
8a19226
chore(debugging): improve logging and debuggability of BuildBus
Apr 23, 2020
7ab6808
chore: reorganize target impls into lib folders, not root
Apr 23, 2020
c5114f0
feat(extensibility): codemod targets and peregrine talon targets
Apr 23, 2020
9486131
chore(ci): upgrade jest to v25 and adjust tests for it
Apr 23, 2020
d34cc2b
feat(extensibility): buildpack test helpers for extension devs
Apr 23, 2020
b30e790
docs: targets tutorial
Apr 28, 2020
2e813a6
fixup jest tolerate slow ci
Apr 29, 2020
75cba6c
Merge branch 'develop' into zetlen/talon-targets
May 1, 2020
9857a5f
fixup remove unnecessary alias hack
May 1, 2020
6068ed2
fixup lint error
May 1, 2020
a9ccfcf
Merge branch 'develop' of https://github.com/magento/pwa-studio into …
May 4, 2020
ef159e7
docs: add note on intercept filename
May 4, 2020
33abe89
docs: reorganize contributing section
May 4, 2020
4fe3789
Merge branch 'develop' into zetlen/talon-targets
May 6, 2020
be01e64
Merge branch 'develop' into zetlen/talon-targets
May 7, 2020
abf01e2
chore: upgrade to jest 26
May 7, 2020
a51e977
docs: more detail and test helpers
May 8, 2020
62eeffd
fix better var names
May 8, 2020
37c79c2
Merge branch 'develop' into zetlen/talon-targets
May 8, 2020
6629515
fixup run serviceworker through buildbus as well
May 8, 2020
6435361
Update packages/peregrine/lib/targets/peregrine-declare.js
May 11, 2020
68b95e3
fixup var names i missed
May 11, 2020
396f747
Merge branch 'develop' of https://github.com/magento/pwa-studio into …
May 11, 2020
8ee376d
Merge branch 'develop' into zetlen/talon-targets
May 13, 2020
df1277f
Merge branch 'develop' into zetlen/talon-targets
May 14, 2020
1e2f6a8
Merge branch 'develop' of https://github.com/magento/pwa-studio into …
May 14, 2020
a59fceb
Merge branch 'develop' of https://github.com/magento/pwa-studio into …
May 19, 2020
ec8fd6c
fixup update snapshots after merge
May 19, 2020
6020571
Merge branch 'develop' of https://github.com/magento/pwa-studio into …
May 20, 2020
bd6d470
Merge branch 'develop' into zetlen/talon-targets
May 20, 2020
c8e240b
Merge branch 'develop' into zetlen/talon-targets
dpatil-magento May 21, 2020
5f4d98e
fixup watch server regression
May 22, 2020
8a06315
Merge branch 'zetlen/talon-targets' of https://github.com/magento/pwa…
May 22, 2020
cc448c4
Merge branch 'develop' of https://github.com/magento/pwa-studio into …
Jun 1, 2020
fdb35df
Merge branch 'develop' into zetlen/talon-targets
dpatil-magento Jun 1, 2020
53173a8
fixup add local intercept back
Jun 1, 2020
186b3af
Merge branch 'zetlen/talon-targets' of https://github.com/magento/pwa…
Jun 1, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ test-results.json
yarn-error.log
# Packages that build partially transpiled ES modules put them here
docker/certs
.history
.history

## exception: commit node_modules folders in test fixtures
!**/__fixtures__/*/node_modules
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing the test helpers meant adding fixtures with node_modules directories.

9 changes: 5 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const testGlob = '/**/{src,lib,_buildpack}/**/__tests__/*.(test|spec).js';
// Reusable test configuration for Venia UI and storefront packages.
const testReactComponents = inPackage => ({
// Expose jsdom to tests.
browser: true,
moduleNameMapper: {
// Mock binary files to avoid excess RAM usage.
'\\.(jpg|jpeg|png)$':
Expand All @@ -52,7 +51,9 @@ const testReactComponents = inPackage => ({
// This mapping forces CSS Modules to return literal identies,
// so e.g. `classes.root` is always `"root"`.
'\\.css$': 'identity-obj-proxy',
'\\.svg$': 'identity-obj-proxy'
'\\.svg$': 'identity-obj-proxy',
'@magento/venia-drivers':
'<rootDir>/packages/venia-ui/lib/drivers/index.js'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of these small changes were for Jest 25 (see below)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually Jest 26 now

},
moduleFileExtensions: ['ee.js', 'ce.js', 'js', 'json', 'jsx', 'node'],
// Reproduce the Webpack resolution config that lets Venia import
Expand All @@ -73,7 +74,7 @@ const testReactComponents = inPackage => ({
// import `.graphql` files into JS.
'\\.(gql|graphql)$': 'jest-transform-graphql',
// Use the default babel-jest for everything else.
'\\.(js|css)$': 'babel-jest'
'\\.(jsx?|css)$': 'babel-jest'
},
// Normally babel-jest ignores node_modules and only transpiles the current
// package's source. The below setting forces babel-jest to transpile
Expand Down Expand Up @@ -256,7 +257,6 @@ const jestConfig = {
configureProject('pagebuilder', 'Pagebuilder', testReactComponents),
configureProject('peregrine', 'Peregrine', inPackage => ({
// Expose jsdom to tests.
browser: true,
setupFiles: [
// Shim DOM properties not supported by jsdom
inPackage('scripts/shim.js'),
Expand Down Expand Up @@ -318,6 +318,7 @@ const jestConfig = {
// Not node_modules
'!**/node_modules/**',
// Not __tests__, __helpers__, or __any_double_underscore_folders__
'!**/TestHelpers/**',
'!**/__[[:alpha:]]*__/**',
'!**/.*/__[[:alpha:]]*__/**',
// Not this file itself
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"storybook:venia": "yarn workspace @magento/venia-ui run storybook",
"test": "jest",
"test:ci": "jest --no-cache --max-workers=3 --json --outputFile=test-results.json",
"test:debug": "node --inspect-brk node_modules/.bin/jest --no-cache --no-coverage --runInBand",
"test:debug": "node --inspect-brk node_modules/.bin/jest --no-cache --no-coverage --runInBand --testTimeout 86400",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--testTimeout 86400

Default is 5000m - did we need to extend the timeout for certain tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, see below. The test helpers are slow, and I don't think it's easily fixable; we should probably talk about putting them in a separate integration test suite.

"test:dev": "jest --watch",
"validate-queries": "yarn venia run validate-queries",
"venia": "yarn workspace @magento/venia-concept",
Expand All @@ -48,7 +48,7 @@
},
"devDependencies": {
"@magento/eslint-config": "~1.5.0",
"@types/jest": "~24.0.18",
"@types/jest": "~25.2.1",
"caller-id": "~0.1.0",
"chalk": "~2.4.2",
"chokidar": "~2.1.2",
Expand All @@ -68,9 +68,9 @@
"first-run": "~2.0.0",
"graphql-tag": "~2.10.1",
"identity-obj-proxy": "~3.0.0",
"jest": "~24.3.1",
"jest": "~26.0.1",
"jest-fetch-mock": "~2.1.1",
"jest-junit": "~6.3.0",
"jest-junit": "~10.0.0",
"jest-transform-graphql": "~2.1.0",
"lodash.debounce": "~4.0.8",
"prettier": "~1.16.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,7 @@ test('renders a Block component with all props configured and Page Builder rich
};
const component = createTestInstance(<Block {...blockProps} />);

expect(
component.root.find(child => child.type.name === 'Row')
).toBeTruthy();
expect(component.root.find(child => child.type === MockRow)).toBeTruthy();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small change for Jest 25 quirk.

});

test('renders a Block component with HTML content', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ jest.mock('@magento/venia-drivers', () => ({
}));
useHistory.mockImplementation(() => history);

const mockWindowLocation = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jest 25's jsdom needs the location mocked differently.

assign: jest.fn()
};

let oldWindowLocation;
beforeEach(() => {
oldWindowLocation = window.location;
delete window.location;
window.location = mockWindowLocation;
mockWindowLocation.assign.mockClear();
});
afterEach(() => {
window.location = oldWindowLocation;
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is...gross. If I were writing the unit test for this effect, I would sooner give up than do this kind of mocking. Is there something earlier in the chain we can mock, if window.location is going to be this bad?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like it, but it's the only approach I've seen in extensive research on the subject. It's not that unusual, though. It belongs to a whole class of problems mocking globals in JSDOM.

test('renders a ButtonItem component', () => {
const component = createTestInstance(<ButtonItem />);

Expand Down Expand Up @@ -73,7 +88,6 @@ test('clicking button with external link goes to correct destination', () => {
};
const component = createTestInstance(<ButtonItem {...buttonItemProps} />);
const button = component.root.findByType(Button);
window.location.assign = jest.fn();
button.props.onClick();
expect(window.location.assign).toBeCalledWith('https://www.adobe.com');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module.exports = targets => {
.richContentRenderers.tap(richContentRenderers => {
richContentRenderers.add({
componentName: 'PageBuilder',
packageName: myName
importPath: myName
});
});
};
2 changes: 1 addition & 1 deletion packages/pagebuilder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
},
"pwa-studio": {
"targets": {
"intercept": "./intercept"
"intercept": "./lib/intercept"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rearranged code to make sure that everything is in ./lib, so that we don't accidentally fail to publish (hi @tjwiebell )

}
},
"sideEffects": false
Expand Down
83 changes: 83 additions & 0 deletions packages/peregrine/lib/targets/__tests__/peregrine-targets.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const {
buildModuleWith,
mockTargetProvider
} = require('@magento/pwa-buildpack');
const declare = require('../peregrine-declare');
const intercept = require('../peregrine-intercept');

test('declares a sync target talons and intercepts transformModules', () => {
const targets = mockTargetProvider(
'@magento/peregrine',
(_, dep) =>
({
'@magento/pwa-buildpack': {
specialFeatures: {
tap: jest.fn()
},
transformModules: {
tap: jest.fn()
}
}
}[dep])
);
declare(targets);
expect(targets.own.talons.tap).toBeDefined();
const hook = jest.fn();
// no implementation testing in declare phase
targets.own.talons.tap('test', hook);
targets.own.talons.call('woah');
expect(hook).toHaveBeenCalledWith('woah');

intercept(targets);
const buildpackTargets = targets.of('@magento/pwa-buildpack');
expect(buildpackTargets.transformModules.tap).toHaveBeenCalled();
});

test('enables third parties to wrap talons', async () => {
// sorry, buildModuleWith is slow. TODO: make it take less than a minute?
jest.setTimeout(60000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we discussed this worries me, how common is it for developers to run all tests during their development cycle? Is there a way we could make some of these tests only run on PR to save developers the minute for this single test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could! It's just not something we have set up currently. We could create a two-tiered test suite for Jest unit tests versus integration tests.

In normal workflow, a developer should be using jest --watch, which tests only files which have changed in the current Git repo state. That's been really slow in Jest for a while, though, so it's sometimes an unpopular choice.

Jest 26 really speeds it up, so I think that the full test suite might get a lot less common.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this is not great and could be improved. At the same time, I'm not sure how much we want to optimize for actual developers blindly running all tests. I think running all tests at once should be avoided, generally, when dealing with a monorepo. But it will happen sometimes, so it probably shouldn't take forever.

As Zetlen said, jest --watch is the recommended approach. I go a step further and input the exact tests I want to run and the exact directories from which I want to collect coverage, which is easy to do with yarn test or yarn jest.

const talonIntegratingDep = {
name: 'goose-app',
declare() {},
intercept(targets) {
targets.of('@magento/peregrine').talons.tap(talons => {
talons.ProductFullDetail.useProductFullDetail.wrapWith(
'src/usePFDIntercept'
);
talons.App.useApp.wrapWith('src/useAppIntercept');
talons.App.useApp.wrapWith('src/swedish');
});
}
};
const built = await buildModuleWith('src/index.js', {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is one of the test helpers for unit testing extensions. It does a full Webpack build and gives you a component runnable in Node, so you can test how your extension affected the built and generated code.

context: __dirname,
dependencies: [
{
name: '@magento/peregrine',
declare,
intercept
},
talonIntegratingDep
],
mockFiles: {
'src/index.js': `
import { useApp } from '@magento/peregrine/lib/talons/App/useApp';
import { useProductFullDetail } from '@magento/peregrine/lib/talons/ProductFullDetail/useProductFullDetail';
export default useApp() + useProductFullDetail()`,
'src/usePFDIntercept': `export default function usePFDIntercept(original) { return function usePFD() { return 'BEEP >o'; } };`,
'src/useAppIntercept': `export default function useAppIntercept(original) {
return function useApp() {
return 'o< HONK';
};
}
`,
'src/swedish': `export default function swedish(impl) {
return function() {
return impl().replace("O", "Ö")
}
}`
}
});

expect(built.run()).toBe('o< HÖNKBEEP >o');
});
48 changes: 48 additions & 0 deletions packages/peregrine/lib/targets/peregrine-declare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* These targets are available for interception to modules which depend on `@magento/peregrine`.
*
* Their implementations are found in `./peregrine-intercept.js`.
*
* @module Peregrine/Targets
*/
module.exports = targets => {
targets.declare({
/**
* @callback talonIntercept
* @param {Peregrine/Targets.TalonWrapperConfig} talons - Registry of talon namespaces, talons, and Sets of interceptors
* @returns {undefined} - Interceptors of `talons` should add to the
* sets on passed TalonWrapperConfig instance. Any returned value will
* be ignored.
*/

/**
* Collects requests to intercept and "wrap" individual Peregrine
* talons in decorator functions. Use it to add your own code to run
* when Peregrine talons are invoked, and/or to modify the behavior and
* output of those talons.
*
* This target is a convenience wrapper around the
* `@magento/pwa-buildpack` target `transformModules`. That target uses
sirugh marked this conversation as resolved.
Show resolved Hide resolved
* filenames, which are not guaranteed to be semantically versioned or
* to be readable APIs.
* This target publishes talons as functions to wrap, rather than as
* files to decorate.
*
* @type {tapable.SyncHook}
* @param {talonIntercept}
*
* @example <caption>Log whenever the `useApp()` hook runs.</caption>
* targets.of('@magento/peregrine').talons.tap(talons => {
* talons.App.useApp.wrapWith('./log-wrapper');
* })
* // log-wrapper.js:
* export default function wrapUseApp(original) {
* return function useApp(...args) {
* console.log('calling useApp with', ...args);
* return original(...args);
* }
* }
*/
talons: new targets.types.Sync(['talons'])
});
};
68 changes: 68 additions & 0 deletions packages/peregrine/lib/targets/peregrine-intercept.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @module Peregrine/Targets
*/
const path = require('path');

/**
*
* @class TalonWrapperConfig
* @hideconstructor
*/
function TalonWrapperConfig(addTransforms) {
const wrappable = (talonFile, exportName) => ({
wrapWith: wrapperModule =>
addTransforms({
type: 'source',
fileToTransform: path.join('./lib/talons/', talonFile),
transformModule:
'@magento/pwa-buildpack/lib/WebpackTools/loaders/wrap-esm-loader',
options: {
wrapperModule,
exportName
}
})
});
return {
/**
* @memberof TalonWrapperConfig
*
*/

ProductFullDetail: {
useProductFullDetail: wrappable(
'ProductFullDetail/useProductFullDetail',
'useProductFullDetail'
)
},
App: {
useApp: wrappable('App/useApp', 'useApp')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit too verbose. Why do we need so much repetition here around the component and talon names?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second argument is the name of the export to wrap. All of the talon files export their main talon functions as a named export, so that looks like our convention, but are we sticking to it?

If so, we can maybe automate some of what this class does, since its implementation is internal to Peregrine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why we export named talons vs just have them as the default but it may be too late to make such a sweeping change anyways. I'd prefer we not have to provide the export name. I had assumed we would just automate intercepts around all talons by default.

@jimbo may have an opinion about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sirugh Let's definitely wait for @jimbo to have input, but I think this can be a followup too. The point of the TalonInterceptorConfig is to be a facade for however we are organizing Peregrine talons. If we change it, we can modify the implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit too verbose. Why do we need so much repetition here around the component and talon names?

Yes, it's verbose and repetitive because it's explicit, which is a Good Thing here. We certainly wouldn't just tap into every export (*), and I don't think tapping into the default export would save us a whole lot of verbosity. I don't really share your concern here, I guess.

I'm not sure why we export named talons vs just have them as the default but it may be too late to make such a sweeping change anyways. I'd prefer we not have to provide the export name. I had assumed we would just automate intercepts around all talons by default.

When you and I wrote these talons, React hooks were new. I elected to make them named exports because I was uncertain that our developer users would grasp the magical requirement that hooks must be named useFoo in order for React to handle them correctly, so as a precaution, I wanted to discourage renaming talons on import.

As it turns out, we haven't really had an issue. The React community at large has adopted the useFoo convention without any problems or controversy (much less controversy than JSX, even), and our contributors and downstream users are using our hooks (and writing their own!) without any problems. So we could switch from named exports to default exports for talons now, but I don't think that amount of churn would be very justifiable.

Copy link
Contributor Author

@zetlen zetlen May 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a very bright idea in the first place. I thought it was general doubt about default exports, but the enforcing of a naming convention was very smart.

We can reduce complexity by switching to default exports. I don't think much churn would be required, because you can just put export useFoo as default at the bottom of every file, thus preserving the old and new APIs.

Wanna schedule that, and mark the verbosity in this file as tech debt? It's up to y'all.

Copy link
Contributor

@jimbo jimbo May 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can reduce complexity by switching to default exports. I don't think much churn would be required, because you can just put export useFoo as default at the bottom of every file, thus preserving the old and new APIs.

Wanna schedule that, and mark the verbosity in this file as tech debt? It's up to y'all.

Yes, actually. As you're suggesting, if we were to add a default export to the talons, and deprecate but not remove the named export, it wouldn't even require us to bump the major version.

export const useFoo = () => {}

export default useFoo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'll push that as a todo.

}
};
}

module.exports = targets => {
const builtins = targets.of('@magento/pwa-buildpack');

builtins.specialFeatures.tap(featuresByModule => {
featuresByModule['@magento/peregrine'] = {
cssModules: true,
esModules: true,
graphQlQueries: true
};
});

/**
* Tap the low-level Buildpack target for wrapping _any_ frontend module.
* Wrap the config object in a TalonWrapperConfig, which presents
* higher-level targets for named and namespaced talons, instead of the
* file paths directly.
* Pass that higher-level config through `talons` interceptors, so they can
* add wrappers for the talon modules without tapping the `transformModules`
* config themselves.
*/
builtins.transformModules.tap(addTransform => {
const talonWrapperConfig = new TalonWrapperConfig(addTransform);

targets.own.talons.call(talonWrapperConfig);
});
};
6 changes: 6 additions & 0 deletions packages/peregrine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
"node": ">=10.x",
"yarn": ">=1.12.0"
},
"pwa-studio": {
"targets": {
"declare": "./lib/targets/peregrine-declare",
"intercept": "./lib/targets/peregrine-intercept"
}
},
"module": "lib/index.js",
"jsnext:main": "lib/index.js",
"es2015": "lib/index.js",
Expand Down
7 changes: 7 additions & 0 deletions packages/pwa-buildpack/__mocks__/devcert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
configuredDomains: jest.fn().mockReturnValue([]),
certificateFor: jest.fn().mockReturnValue({
key: 'fakekey',
cert: 'fakecert'
})
};
8 changes: 8 additions & 0 deletions packages/pwa-buildpack/__mocks__/word-wrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// don't actually word wrap, it messes with tests since the autodetect is
// system-dependent
const wordWrap = jest.requireActual('word-wrap');
module.exports = (str, opts) =>
wordWrap(str, {
...opts,
width: 1000
});
11 changes: 11 additions & 0 deletions packages/pwa-buildpack/envVarDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@
"example": "sandbox_8yrzsvtm_s2bg8fs563crhqzk"
}
]
},
{
"name": "BuildBus and targets",
"variables": [
{
"name": "BUILDBUS_DEPS_ADDITIONAL",
"type": "str",
"desc": "A list of resolvable NPM modules that BuildBus will scan for targets, in addition to those declared in project `dependencies` and `devDependencies`.",
"default": ""
}
]
}
],
"changes": [
Expand Down