Skip to content

Commit

Permalink
feat(preset): support to analyze multi-level local deps for demo
Browse files Browse the repository at this point in the history
  • Loading branch information
PeachScript committed Apr 20, 2020
1 parent a026b96 commit 6c636ad
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 140 deletions.
121 changes: 121 additions & 0 deletions packages/preset-dumi/src/transformer/demo/dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import path from 'path';
import slash from 'slash';
import * as babel from '@babel/core';
import * as types from '@babel/types';
import traverse from '@babel/traverse';
import {
getModuleResolvePkg,
getModuleResolvePath,
getModuleResolveContent,
} from '../../utils/moduleResolver';
import { saveFileOnDepChange } from '../../utils/watcher';
import { getBabelOptions } from './options';

interface IDepAnalyzeResult {
dependencies: { [key: string]: string };
files: { [key: string]: { path: string; content: string } };
}

// local dependency extensions which will be collected
export const LOCAL_DEP_EXT = [
'.jsx',
'.tsx',
'.js',
'.ts',
'.json',
'.less',
'.css',
'.scss',
'.sass',
'.styl',
];

function analyzeDeps(
raw: babel.BabelFileResult['ast'] | string,
{
isTSX,
fileAbsPath,
entryAbsPath,
}: { isTSX: boolean; fileAbsPath: string; entryAbsPath?: string },
totalFiles?: IDepAnalyzeResult['files'],
): IDepAnalyzeResult {
// support to pass babel transform result directly
const ast = typeof raw === 'string' ? babel.transformSync(raw, getBabelOptions(isTSX)).ast : raw;
const files = totalFiles || {};
const dependencies = {};

// traverse all expression
traverse(ast, {
CallExpression(callPath) {
const callPathNode = callPath.node;

// tranverse all require statement
if (
types.isIdentifier(callPathNode.callee) &&
callPathNode.callee.name === 'require' &&
types.isStringLiteral(callPathNode.arguments[0]) &&
callPathNode.arguments[0].value !== 'react'
) {
const requireStr = callPathNode.arguments[0].value;
const resolvePath = getModuleResolvePath({
basePath: fileAbsPath,
sourcePath: requireStr,
});
const resolvePathParsed = path.parse(resolvePath);

if (resolvePath.includes('node_modules')) {
// save external deps
const pkg = getModuleResolvePkg({
basePath: fileAbsPath,
sourcePath: requireStr,
});

dependencies[pkg.name] = pkg.version;
} else if (
// only analysis for valid local file type
LOCAL_DEP_EXT.includes(resolvePathParsed.ext) &&
// do not collect entry file
resolvePath !== entryAbsPath
) {
// save local deps
const fileName = slash(path.relative(fileAbsPath, resolvePath)).replace(
/(\.\/|\..\/)/g,
'',
);

// to avoid circular-reference
if (fileName && !files[fileName]) {
files[fileName] = {
path: requireStr,
content: getModuleResolveContent({
basePath: fileAbsPath,
sourcePath: requireStr,
}),
};

// continue to collect deps for dep
const result = analyzeDeps(
files[fileName].content,
{
isTSX: /\.tsx?/.test(resolvePathParsed.ext),
fileAbsPath: resolvePath,
entryAbsPath: entryAbsPath || fileAbsPath,
},
files,
);

Object.assign(files, result.files);
Object.assign(dependencies, result.dependencies);

// trigger parent file change to update frontmatter when dep file change
saveFileOnDepChange(fileAbsPath, resolvePath);
}
}
}
},
});

return { files, dependencies };
}

export default analyzeDeps;
106 changes: 7 additions & 99 deletions packages/preset-dumi/src/transformer/demo/index.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,24 @@
import fs from 'fs';
import path from 'path';
import slash from 'slash2';
import * as babel from '@babel/core';
import * as types from '@babel/types';
import traverse from '@babel/traverse';
import generator from '@babel/generator';
import {
getModuleResolvePkg,
getModuleResolvePath,
getModuleResolveContent,
} from '../../utils/moduleResolver';
import ctx from '../../context';
import { getBabelOptions } from './options';

interface IDemoTransformResult {
content: string;
dependencies: { [key: string]: string };
files: { [key: string]: { path: string; content: string } };
ast: babel.BabelFileResult['ast'];
}

export const DEMO_COMPONENT_NAME = 'DumiDemo';
// locale dependency extensions which will be collected
export const LOCAL_DEP_EXT = [
'.jsx',
'.tsx',
'.js',
'.ts',
'.json',
'.less',
'.css',
'.scss',
'.sass',
'.styl',
];
export { default as getDepsForDemo } from './dependencies';

const fileWatchers: { [key: string]: fs.FSWatcher } = {};
export const DEMO_COMPONENT_NAME = 'DumiDemo';

/**
* transform code block statments to preview
*/
export default (
raw: string,
{ isTSX, fileAbsPath }: { isTSX?: boolean; fileAbsPath: string },
): IDemoTransformResult => {
const code = babel.transformSync(raw, {
presets: [
require.resolve('@babel/preset-react'),
require.resolve('@babel/preset-env'),
...(ctx.umi?.config?.extraBabelPresets || []),
],
plugins: [
require.resolve('@babel/plugin-proposal-class-properties'),
[require.resolve('@babel/plugin-transform-modules-commonjs'), { strict: true }],
...(isTSX ? [[require.resolve('@babel/plugin-transform-typescript'), { isTSX: true }]] : []),
...(ctx.umi?.config?.extraBabelPlugins || []),
],
ast: true,
babelrc: false,
configFile: false,
});
export default (raw: string, { isTSX }: { isTSX?: boolean } = {}): IDemoTransformResult => {
const code = babel.transformSync(raw, getBabelOptions(isTSX));
const body = code.ast.program.body as types.Statement[];
const dependencies: IDemoTransformResult['dependencies'] = {};
const files: IDemoTransformResult['files'] = {};
let reactVar: string;
let returnStatement: types.ReturnStatement;

Expand All @@ -83,55 +41,6 @@ export default (
reactVar = callPathNode.declarations[0].id.name;
}
},
CallExpression(callPath) {
const callPathNode = callPath.node;

// tranverse all require statement
if (
types.isIdentifier(callPathNode.callee) &&
callPathNode.callee.name === 'require' &&
types.isStringLiteral(callPathNode.arguments[0]) &&
callPathNode.arguments[0].value !== 'react'
) {
const requireStr = callPathNode.arguments[0].value;
const resolvePath = getModuleResolvePath({
basePath: fileAbsPath,
sourcePath: requireStr,
});
const resolvePathParsed = path.parse(resolvePath);

if (resolvePath.includes('node_modules')) {
// save external deps
const pkg = getModuleResolvePkg({
basePath: fileAbsPath,
sourcePath: requireStr,
});

dependencies[pkg.name] = pkg.version;
} else if (LOCAL_DEP_EXT.includes(resolvePathParsed.ext)) {
// save local deps
files[slash(path.relative(fileAbsPath, resolvePath)).replace(/(\.\/|\..\/)/g, '')] = {
path: requireStr,
content: getModuleResolveContent({
basePath: fileAbsPath,
sourcePath: requireStr,
}),
};

// watch deps change
if (process.env.NODE_ENV === 'development') {
if (fileWatchers[resolvePath]) {
fileWatchers[resolvePath].close();
}

fileWatchers[resolvePath] = fs.watch(resolvePath, () => {
// trigger parent file change to update frontmatter when dep file change
fs.writeFileSync(fileAbsPath, fs.readFileSync(fileAbsPath));
});
}
}
}
},
AssignmentExpression(callPath) {
const callPathNode = callPath.node;

Expand Down Expand Up @@ -191,8 +100,7 @@ export default (
);

return {
ast: code.ast,
content: generator(types.program([demoFunction]), {}, raw).code,
dependencies,
files,
};
};
18 changes: 18 additions & 0 deletions packages/preset-dumi/src/transformer/demo/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ctx from '../../context';

export const getBabelOptions = (isTSX: boolean) => ({
presets: [
require.resolve('@babel/preset-react'),
require.resolve('@babel/preset-env'),
...(ctx.umi?.config?.extraBabelPresets || []),
],
plugins: [
require.resolve('@babel/plugin-proposal-class-properties'),
[require.resolve('@babel/plugin-transform-modules-commonjs'), { strict: true }],
...(isTSX ? [[require.resolve('@babel/plugin-transform-typescript'), { isTSX: true }]] : []),
...(ctx.umi?.config?.extraBabelPlugins || []),
],
ast: true,
babelrc: false,
configFile: false,
});
9 changes: 5 additions & 4 deletions packages/preset-dumi/src/transformer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import slash from 'slash2';
import extractComments from 'esprima-extract-comments';
import remark from './remark';
import html from './html';
import demo from './demo';
import demo, { getDepsForDemo } from './demo';
import FileCache from '../utils/cache';

const FRONT_COMMENT_EXP = /^\n*\/\*[^]+?\s*\*\/\n*/;
Expand Down Expand Up @@ -131,12 +131,13 @@ export default {
/**
* transform code block (j|t)sx demo to js
*/
demo(raw: string, opts: { isTsx: boolean; fileAbsPath: string }): TransformResult {
const { content, ...config } = demo(raw, opts);
demo(raw: string, opts: { isTSX: boolean; fileAbsPath: string }): TransformResult {
const { content, ast } = demo(raw, { isTSX: opts.isTSX });
const deps = getDepsForDemo(ast, opts);

return {
content,
config,
config: deps,
};
},
};
29 changes: 11 additions & 18 deletions packages/preset-dumi/src/transformer/remark/previewer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Node } from 'unist';
import visit from 'unist-util-visit';
import demoTransformer, { DEMO_COMPONENT_NAME } from '../demo';
import demoTransformer, { DEMO_COMPONENT_NAME, getDepsForDemo } from '../demo';
import transformer from '../index';

function visitor(node, i, parent) {
if (node.tagName === 'div' && node.properties?.type === 'previewer') {
const source = node.properties?.source || {};
const yaml = node.properties?.meta || {};
const raw = source.tsx || source.jsx;
const isTSX = Boolean(source.tsx);
let transformCode = raw;
let dependencies;
let files;

// transform markdown for previewer desc field
Object.keys(yaml).forEach(key => {
Expand All @@ -26,26 +25,20 @@ import React, { useEffect } from 'react';
import Demo from '${node.properties.filePath}';
export default () => <Demo />;`;

// collect deps from source code if it is external demo
const transformResult = demoTransformer(raw, {
isTSX: Boolean(source.tsx),
fileAbsPath: node.properties.filePath,
});

dependencies = transformResult.dependencies;
files = transformResult.files;
}

// transform demo source code
const { content: code, ...demoTransformResult } = demoTransformer(transformCode, {
const { content: code } = demoTransformer(transformCode, {
isTSX: Boolean(source.tsx),
fileAbsPath: this.data('fileAbsPath'),
});

// fallback to demo code dependencies
dependencies = dependencies || demoTransformResult.dependencies;
files = files || demoTransformResult.files;
const { dependencies, files } = getDepsForDemo(raw, {
isTSX,
fileAbsPath:
// for external demo
node.properties.filePath ||
// for embed demo
this.data('fileAbsPath'),
});

// save code into data then declare them on the top page component
this.vFile.data.demos = (this.vFile.data.demos || []).concat(
Expand Down
16 changes: 0 additions & 16 deletions packages/preset-dumi/src/transformer/test/demo-deps.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ describe('demo: missing react', () => {
const FILE_PATH = path.join(__dirname, '../fixtures/raw/demo-missing-react.tsx');

it('override react to null for throw error', () => {
const result = demo(fs.readFileSync(FILE_PATH).toString(), {
fileAbsPath: FILE_PATH,
});
const result = demo(fs.readFileSync(FILE_PATH).toString());

// compare transform content
expect(result.content).toContain('var React;');
Expand Down

0 comments on commit 6c636ad

Please sign in to comment.