Skip to content

Commit

Permalink
feat: loader webpack plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
atanasster committed May 21, 2020
1 parent dea8230 commit 409fdec
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 88 deletions.
1 change: 1 addition & 0 deletions core/loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"license": "MIT",
"dependencies": {
"@component-controls/config": "^1.1.0",
"@component-controls/instrument": "^1.1.0",
"@component-controls/specification": "^1.1.0",
"@storybook/csf": "^0.0.1",
Expand Down
1 change: 1 addition & 0 deletions core/loader/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/plugin');
1 change: 1 addition & 0 deletions core/loader/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default config({
'./src/index.ts',
'./src/store.ts',
'./src/loader.ts',
'./src/plugin.ts',
'./src/runtimeLoader.ts',
],
});
1 change: 1 addition & 0 deletions core/loader/src/loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';

import { getOptions } from 'loader-utils';
import { loader } from 'webpack';
import {
Expand Down
71 changes: 71 additions & 0 deletions core/loader/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ReplaceSource } from 'webpack-sources';
import * as path from 'path';
import * as webpack from 'webpack';
import { createHash } from 'crypto';
import { store } from './store';

class LoaderPlugin {
public static pluginName = 'component-controls-loader-plugin';
private readonly compilationHash: string;

constructor() {
const hash = createHash('md5')
.update(new Date().getTime().toString())
.digest('hex');
this.compilationHash = `__${hash.substr(0, 6)}__`;
}

apply(compiler: webpack.Compiler) {
this.replaceRuntimeModule(compiler);
compiler.hooks.compilation.tap(LoaderPlugin.pluginName, compilation => {
compilation.hooks.optimizeChunkAssets.tap(
LoaderPlugin.pluginName,
chunks => {
chunks.forEach(chunk => {
chunk.files
.filter(fileName => fileName.endsWith('.js'))
.forEach(file => {
this.replaceSource(compilation, file);
});
});
},
);
});
}

private replaceRuntimeModule(compiler: webpack.Compiler) {
const nmrp = new webpack.NormalModuleReplacementPlugin(
/story-store-data\.js$/,
(resource: any) => {
if (resource.resource) {
resource.loaders.push({
loader: path.join(__dirname, 'runtimeLoader.js'),
options: JSON.stringify({ compilationHash: this.compilationHash }),
});
}
},
);
nmrp.apply(compiler);
}

private replaceSource(
compilation: webpack.compilation.Compilation,
file: string,
) {
const placeholder = `__COMPILATION_HASH__${this.compilationHash}`;
const source = compilation.assets[file];
const placeholderPos = source.source().indexOf(placeholder);
if (placeholderPos > -1) {
const newContent = JSON.stringify(store);
const newSource = new ReplaceSource(source, file);
newSource.replace(
placeholderPos,
placeholderPos + placeholder.length - 1,
newContent,
);
compilation.assets[file] = newSource;
}
}
}

module.exports = LoaderPlugin;
67 changes: 67 additions & 0 deletions core/loader/src/replaceSource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
export interface StoryPath {
absPath: string;
relPath: string;
}

export const replaceSource = (stories: StoryPath[], hashKey: string) => {
const imports = `
const imports = {};
${stories
.map(story => `imports['${story.absPath}'] = require(${story.relPath});`)
.join('\n')}
`;
const storeConst = `const store = ${hashKey};\n`;
let loadStories = `
for (let i = 0; i < store.stores.length; i+= 1) {
const s = store.stores[i];
const kinds = Object.keys(s.kinds);
for (let j=0; j < kinds.length; j += 1) {
const kind = s.kinds[kinds[j]];
if (imports.hasOwnProperty(kind.fileName)) {
const exports = imports[kind.fileName];
try {
Object.keys(exports).forEach(key => {
const exported = exports[key];
if (key === 'default') {
const { storySource, ...rest } = exported;
Object.assign(kind, rest);
} else {
const story = s.stories[key];
if (story) {
story.renderFn = exported;
if (exported.story) {
Object.assign(story, exported.story);
}
}
}
});
} catch (e) {
console.error(\`unable to load module \${kind.moduleId}\`, e);
}
}
}
}
`;
const exports = `module.exports = store;\n`;
const hmr = `
if (module.hot) {
${stories
.map(
story => `
module.hot.accept(${story.relPath}, function() {
console.log('HMR',${story.relPath});
})
`,
)
.join('\n')}
}
`;
const newContent = `
${imports}
${storeConst}
${loadStories}
${hmr}
${exports}
`;
return newContent;
};
90 changes: 19 additions & 71 deletions core/loader/src/runtimeLoader.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,23 @@
import loaderUtils from 'loader-utils';
import { StoriesStore } from '@component-controls/specification';
import { store } from './store';
import { getConfiguration, extractStories } from '@component-controls/config';
import { stringifyRequest } from 'loader-utils';
import { replaceSource, StoryPath } from './replaceSource';

module.exports = function() {
const storyFiles = store.stores.reduce(
(acc: string[], s: Pick<StoriesStore, 'stories' | 'kinds'>) => {
return [
...acc,
...Object.keys(s.kinds).map(key =>
//@ts-ignore
loaderUtils.stringifyRequest(this, s.kinds[key].fileName),
),
];
},
[],
module.exports = function(content: string) {
//@ts-ignore
const params = JSON.parse(this.query.slice(1));
//@ts-ignore
const config = getConfiguration(this.rootContext);
const stories: StoryPath[] = (config ? extractStories(config) || [] : []).map(
fileName => ({
absPath: fileName,
//@ts-ignore
relPath: stringifyRequest(this, fileName),
}),
);

const imports = `
const imports = {};
${storyFiles
.map((fileName, fileIdx) => `imports['i_${fileIdx}'] = require(${fileName});`)
.join('\n')}
`;
const storeConst = `const store = ${JSON.stringify(store)};\n`;
let loadStories = `
let storyIdx = 0;
for (let i = 0; i < store.stores.length; i+= 1) {
const s = store.stores[i];
const kinds = Object.keys(s.kinds);
for (let j=0; j < kinds.length; j += 1) {
const exports = imports[\`i_\${storyIdx}\`];
storyIdx += 1;
const kind = s.kinds[kinds[j]];
try {
Object.keys(exports).forEach(key => {
const exported = exports[key];
if (key === 'default') {
const { storySource, ...rest } = exported;
Object.assign(kind, rest);
} else {
const story = s.stories[key];
if (story) {
story.renderFn = exported;
if (exported.story) {
Object.assign(story, exported.story);
}
}
}
});
} catch (e) {
console.error(\`unable to load module \${kind.moduleId}\`, e);
}
}
}
`;
const exports = `module.exports = store;\n`;
const hmr = `
if (module.hot) {
module.hot.accept([${storyFiles.join(', ')}], function() {
console.log('Accepting the updated modules');
})
}
`;
const newContent = `
${imports}
${storeConst}
${loadStories}
${hmr}
${exports}
`;

return newContent;
content = replaceSource(
stories,
`__COMPILATION_HASH__${params.compilationHash}`,
);
return content;
};
15 changes: 2 additions & 13 deletions integrations/storybook/src/preset.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
const { mergeWebpackConfig } = require('@component-controls/webpack-configs');
const LoaderPlugin = require('@component-controls/loader/plugin');
import { PresetOptions, defaultRules } from './types';

module.exports = {
Expand Down Expand Up @@ -52,19 +53,7 @@ module.exports = {
);
return {
...mergedConfig,
module: {
...mergedConfig.module,
rules: [
{
test: /story-store-data\.js$/,
loader: require.resolve(
'@component-controls/loader/dist/runtimeLoader.js',
),
enforce: 'post',
},
...mergedConfig.module.rules,
],
},
plugins: [...mergedConfig.plugins, new LoaderPlugin()],
};
},
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"scripts": {
"bootstrap": "lerna bootstrap",
"clean": "lerna clean",
"clean": "lerna clean && rm -rf node_modules",
"docs": "lerna run docs && ts-node -O '{\"module\": \"commonjs\"}' ./scripts/docs.ts",
"packages": "yarn fix && yarn lint && yarn build && yarn test",
"fix": "lerna run --parallel fix && echo",
Expand Down
9 changes: 6 additions & 3 deletions ui/blocks/src/context/block/BlockDataContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getComponentName,
PackageInfo,
} from '@component-controls/specification';
import { CURRENT_STORY } from '../../utils';

export interface BlockDataContextProps {
/**
Expand Down Expand Up @@ -69,10 +70,12 @@ export const BlockDataContextProvider: React.FC<BlockDataContextInoutProps> = ({
}) => {
const store: StoriesStore | undefined = storeProvider.getStore();

const getStoryData = (id: string = storyId) => {
const getStoryData = (id?: string) => {
if (store) {
const story: Story | undefined =
store.stories && id ? store.stories[id] : undefined;
const actualId = !id || id === CURRENT_STORY ? storyId : id;
const story: Story | undefined = store.stories
? store.stories[actualId]
: undefined;
const kind = story && story.kind ? store.kinds[story.kind] : undefined;
const storyComponent: any =
story && kind ? story.component || kind.component : undefined;
Expand Down

0 comments on commit 409fdec

Please sign in to comment.