Skip to content

Commit

Permalink
CLI: Addon postinstall hooks (#8700)
Browse files Browse the repository at this point in the history
CLI: Addon postinstall hooks
  • Loading branch information
shilman authored Nov 14, 2019
2 parents 2d18cba + a2006b9 commit cbc16f5
Show file tree
Hide file tree
Showing 23 changed files with 354 additions and 13 deletions.
2 changes: 2 additions & 0 deletions addons/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"angular/**/*",
"common/**/*",
"html/**/*",
"postinstall/**/*",
"react/**/*",
"vue/**/*",
"web-components/**/*",
Expand All @@ -47,6 +48,7 @@
"@storybook/addons": "5.3.0-alpha.44",
"@storybook/api": "5.3.0-alpha.44",
"@storybook/components": "5.3.0-alpha.44",
"@storybook/postinstall": "5.3.0-alpha.44",
"@storybook/router": "5.3.0-alpha.44",
"@storybook/source-loader": "5.3.0-alpha.44",
"@storybook/theming": "5.3.0-alpha.44",
Expand Down
39 changes: 39 additions & 0 deletions addons/docs/postinstall/presets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from 'fs';
import { presetsAddPreset, getFrameworks } from '@storybook/postinstall';
// eslint-disable-next-line import/no-extraneous-dependencies
import { logger } from '@storybook/node-logger';

export default function transformer(file, api) {
const packageJson = JSON.parse(fs.readFileSync('./package.json'));
const frameworks = getFrameworks(packageJson);

let err = null;
let framework = null;
let presetOptions = null;
if (frameworks.length !== 1) {
err = `${frameworks.length === 0 ? 'No' : 'Multiple'} frameworks found: ${frameworks}`;
logger.error(`${err}, please configure '@storybook/addon-docs' manually.`);
return file.source;
}

// eslint-disable-next-line prefer-destructuring
framework = frameworks[0];

const { dependencies, devDependencies } = packageJson;
if (
framework === 'react' &&
((dependencies && dependencies['react-scripts']) ||
(devDependencies && devDependencies['react-scripts']))
) {
presetOptions = {
configureJSX: true,
};
}

const j = api.jscodeshift;
const root = j(file.source);

presetsAddPreset(`@storybook/addon-docs/preset`, presetOptions, { root, api });

return root.toSource({ quote: 'single' });
}
1 change: 1 addition & 0 deletions lib/addons/src/typings.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
declare module 'global';
declare module '@hypnosphi/jscodeshift/dist/testUtils';
1 change: 1 addition & 0 deletions lib/cli/bin/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ if (process.argv[1].includes('getstorybook')) {
.command('add <addon>')
.description('Add an addon to your Storybook')
.option('-N --use-npm', 'Use NPM to build the Storybook server')
.option('-s --skip-postinstall', 'Skip package specific postinstall config modifications')
.action((addonName, options) => add(addonName, options));

program
Expand Down
52 changes: 39 additions & 13 deletions lib/cli/lib/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,42 @@ export const addStorybookAddonToFile = (addonName, addonsFile, isOfficialAddon)
];
};

const registerAddon = (addonName, isOfficialAddon) => {
const registerDone = commandLog(`Registering the ${addonName} Storybook addon`);
const addonsFilePath = path.resolve('.storybook/addons.js');
const addonsFile = fs
.readFileSync(addonsFilePath)
.toString()
.split('\n');
fs.writeFileSync(
addonsFilePath,
addStorybookAddonToFile(addonName, addonsFile, isOfficialAddon).join('\n')
);
registerDone();
const LEGACY_CONFIGS = ['addons', 'config', 'presets'];

const postinstallAddon = async (addonName, isOfficialAddon) => {
let skipMsg = null;
if (!isOfficialAddon) {
skipMsg = 'unofficial addon';
} else if (!fs.existsSync('.storybook')) {
skipMsg = 'no .storybook config';
} else {
skipMsg = 'no codmods found';
LEGACY_CONFIGS.forEach(config => {
try {
const codemod = require.resolve(
`${getPackageName(addonName, isOfficialAddon)}/postinstall/${config}.js`
);
commandLog(`Running postinstall script for ${addonName}`)();
let configFile = path.join('.storybook', `${config}.ts`);
if (!fs.existsSync(configFile)) {
configFile = path.join('.storybook', `${config}.js`);
if (!fs.existsSync(configFile)) {
fs.writeFileSync(configFile, '', 'utf8');
}
}
spawnSync('npx', ['jscodeshift', '-t', codemod, configFile], {
stdio: 'inherit',
});
skipMsg = null;
} catch (err) {
// resolve failed, skip
}
});
}

if (skipMsg) {
commandLog(`Skipping postinstall for ${addonName}, ${skipMsg}`)();
}
};

export default async function add(addonName, options) {
Expand All @@ -119,5 +143,7 @@ export default async function add(addonName, options) {
}
addonCheckDone();
installAddon(addonName, npmOptions, isOfficialAddon);
registerAddon(addonName, isOfficialAddon);
if (!options.skipPostinstall) {
await postinstallAddon(addonName, isOfficialAddon);
}
}
20 changes: 20 additions & 0 deletions lib/postinstall/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Storybook Postinstall Utilties

A minimal utility library for addons to update project configurations after the addon is installed via the [Storybook CLI](https://github.com/storybookjs/storybook/tree/master/lib/cli), e.g. `sb add docs`.

Each postinstall is written as a [jscodeshift](https://github.com/facebook/jscodeshift) codemod, with the naming convention `addon-name/postinstall/<file>.js` where `file` is one of { `config`, `addons`, `presets` }.

If these files are present in the addon, the CLI will run them on the existing file in the user's project (or create a new empty file if one doesn't exist). This library exists to make it really easy to make common modifications without having to muck with jscodeshift internals.

## Adding a preset

To add a preset to `presets.js`, simply create a file `postinstall/presets.js` in your addon:

```js
improt { presetsAddPreset } = require('@storybook/postinstall');
export default function transformer(file, api) {
const root = api.jscodeshift(file.source);
presetsAddPreset(`@storybook/addon-docs/preset`, { some: 'options' }, { root, api });
return root.toSource();
};
```
39 changes: 39 additions & 0 deletions lib/postinstall/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@storybook/postinstall",
"version": "5.3.0-alpha.44",
"description": "",
"keywords": [
"storybook"
],
"homepage": "https://github.com/storybookjs/storybook/tree/master/lib/postinstall",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "lib/postinstall"
},
"license": "MIT",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"core-js": "^3.0.1"
},
"devDependencies": {
"@hypnosphi/jscodeshift": "^0.6.4",
"jest-specific-snapshot": "^2.0.0"
},
"publishConfig": {
"access": "public"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = ['foo'];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`presets-add-preset-options transforms correctly using "basic.input.js" data 1`] = `
"module.exports = ['foo', {
name: 'test',
options: {\\"a\\":[1,2,3],\\"b\\":{\\"foo\\":\\"bar\\"},\\"c\\":\\"baz\\"}
}];"
`;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`presets-add-preset-options transforms correctly using "empty.input.js" data 1`] = `
"module.exports = [{
name: 'test',
options: {\\"a\\":[1,2,3],\\"b\\":{\\"foo\\":\\"bar\\"},\\"c\\":\\"baz\\"}
}];"
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = ['foo'];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`presets-add-preset transforms correctly using "basic.input.js" data 1`] = `"module.exports = ['foo', 'test'];"`;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`presets-add-preset transforms correctly using "empty.input.js" data 1`] = `"module.exports = ['test'];"`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { addPreset } from '../presets';

export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);

const options = {
a: [1, 2, 3],
b: { foo: 'bar' },
c: 'baz',
};

addPreset('test', options, { root, api });

return root.toSource({ quote: 'single' });
}
10 changes: 10 additions & 0 deletions lib/postinstall/src/__testtransforms__/presets-add-preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { addPreset } from '../presets';

export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);

addPreset('test', null, { root, api });

return root.toSource({ quote: 'single' });
}
32 changes: 32 additions & 0 deletions lib/postinstall/src/codemods.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import path from 'path';
import fs from 'fs';
import 'jest-specific-snapshot';
// TODO move back to original 'jscodeshift' package as soon as https://github.com/facebook/jscodeshift/pull/297 is released
import { applyTransform } from '@hypnosphi/jscodeshift/dist/testUtils';

jest.mock('@storybook/node-logger');

const inputRegExp = /\.input\.js$/;

const fixturesDir = path.resolve(__dirname, './__testfixtures__');
fs.readdirSync(fixturesDir).forEach(transformName => {
const transformFixturesDir = path.join(fixturesDir, transformName);
// eslint-disable-next-line jest/valid-describe
describe(transformName, () =>
fs
.readdirSync(transformFixturesDir)
.filter(fileName => inputRegExp.test(fileName))
.forEach(fileName => {
const inputPath = path.join(transformFixturesDir, fileName);
it(`transforms correctly using "${fileName}" data`, () =>
expect(
applyTransform(
// eslint-disable-next-line global-require,import/no-dynamic-require
require(path.join(__dirname, '__testtransforms__', transformName)),
null,
{ path: inputPath, source: fs.readFileSync(inputPath, 'utf8') }
)
).toMatchSpecificSnapshot(inputPath.replace(inputRegExp, '.output.snapshot')));
})
);
});
41 changes: 41 additions & 0 deletions lib/postinstall/src/frameworks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getFrameworks } from './frameworks';

const REACT = {
'@storybook/react': '5.2.5',
};

const VUE = {
'@storybook/vue': '5.2.5',
};

const NONE = {
'@storybook/addons': '5.2.5',
lodash: '^4.17.15',
};

describe('getFrameworks', () => {
it('single framework', () => {
const frameworks = getFrameworks({
dependencies: NONE,
devDependencies: REACT,
});
expect(frameworks).toEqual(['react']);
});
it('multi-framework', () => {
const frameworks = getFrameworks({
dependencies: VUE,
devDependencies: REACT,
});
expect(frameworks.sort()).toEqual(['react', 'vue']);
});
it('no deps', () => {
const frameworks = getFrameworks({});
expect(frameworks).toEqual([]);
});
it('no framework', () => {
const frameworks = getFrameworks({
dependencies: NONE,
});
expect(frameworks).toEqual([]);
});
});
30 changes: 30 additions & 0 deletions lib/postinstall/src/frameworks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type Deps = Record<string, string>;
interface PackageJson {
dependencies?: Deps;
devDependencies?: Deps;
}

const FRAMEWORKS = [
'angular',
'ember',
'html',
'marko',
'mithril',
'polymer',
'preact',
'rax',
'react',
'react-native',
'riot',
'svelte',
'vue',
'web-components',
];

export const getFrameworks = ({ dependencies, devDependencies }: PackageJson): string[] => {
const allDeps: Deps = {};
Object.assign(allDeps, dependencies || {});
Object.assign(allDeps, devDependencies || {});

return FRAMEWORKS.filter(f => !!allDeps[`@storybook/${f}`]);
};
2 changes: 2 additions & 0 deletions lib/postinstall/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { addPreset as presetsAddPreset } from './presets';
export * from './frameworks';
Loading

1 comment on commit cbc16f5

@vercel
Copy link

@vercel vercel bot commented on cbc16f5 Nov 14, 2019

Choose a reason for hiding this comment

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

Please sign in to comment.