Skip to content

Commit

Permalink
feat: add presets option
Browse files Browse the repository at this point in the history
  • Loading branch information
liximomo authored Aug 25, 2020
1 parent a84062e commit 912d9a0
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 90 deletions.
61 changes: 33 additions & 28 deletions packages/shuvi/src/api/__tests__/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { getApi, Api } from '../api';
import { getApi } from '../api';
import { PluginApi } from '../pluginApi';
import { resolvePlugin } from './utils';
import { IApiConfig, IPaths } from '@shuvi/types';
import path from 'path';
import { resolvePreset } from './utils';

describe('api', () => {
let gApi: Api;
beforeAll(async () => {
gApi = await getApi({
mode: 'development',
config: {}
});
});

test('should has "production" be default mode', async () => {
const prodApi = await getApi({ config: {} });
expect(prodApi.mode).toBe('production');
Expand All @@ -28,19 +20,6 @@ describe('api', () => {
expect(pluginApi!.paths).toBe(api.paths);
});

test('should modify config', async () => {
let pluginApi: PluginApi;
const api = await getApi({
config: {
plugins: [resolvePlugin('modify-config'), api => (pluginApi = api)]
}
});
const plugins = (pluginApi! as any).__plugins;
expect(plugins.length).toBe(1);
expect(plugins[0].name).toBe('modify-config');
expect(api.config.publicPath).toBe('/bar');
});

test('should access config and paths', async () => {
let config: IApiConfig;
let paths: IPaths;
Expand All @@ -62,12 +41,38 @@ describe('api', () => {
});
});

test('getPluginApi', () => {
const pluginApi = gApi.getPluginApi();
describe('presets', () => {
test('should work', async () => {
const api = await getApi({
config: { presets: [resolvePreset('a-b-preset')] }
});
const plugins = (api as any)._presetPlugins;
expect(plugins.length).toBe(2);
expect(plugins[0].id).toMatch(/plugin-a/);
expect(plugins[1].id).toMatch(/plugin-b/);
});

test('should work with nested preset', async () => {
const api = await getApi({
config: { presets: [resolvePreset('nest-preset-preset')] }
});
const plugins = (api as any)._presetPlugins;
expect(plugins.length).toBe(3);
expect(plugins[0].id).toMatch(/plugin-a/);
expect(plugins[1].id).toMatch(/plugin-b/);
expect(plugins[2].id).toMatch(/plugin-c/);
});
});

test('getPluginApi', async () => {
let pluginApi!: PluginApi;
const api = await getApi({
config: { plugins: [api => (pluginApi = api)] }
});

expect(pluginApi.mode).toBe(gApi.mode);
expect(pluginApi.paths).toBe(gApi.paths);
expect(pluginApi.config).toBe(gApi.config);
expect(pluginApi.mode).toBe(api.mode);
expect(pluginApi.paths).toBe(api.paths);
expect(pluginApi.config).toBe(api.config);

[
'tap',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
module.exports = class Plugin {
constructor(options) {
this.options = options;
this.name = 'modify-config';
}

modifyConfig(config) {
config.publicPath = '/bar';
return config;
this.name = 'a';
}

apply(api) {
Expand Down
11 changes: 11 additions & 0 deletions packages/shuvi/src/api/__tests__/fixtures/plugins/plugin-b.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = class Plugin {
constructor(options) {
this.options = options;
this.name = 'b';
}

apply(api) {
api.__plugins = api.__plugins || [];
api.__plugins.push(this);
}
};
11 changes: 11 additions & 0 deletions packages/shuvi/src/api/__tests__/fixtures/plugins/plugin-c.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = class Plugin {
constructor(options) {
this.options = options;
this.name = 'c';
}

apply(api) {
api.__plugins = api.__plugins || [];
api.__plugins.push(this);
}
};
12 changes: 12 additions & 0 deletions packages/shuvi/src/api/__tests__/fixtures/presets/a-b-preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const path = require('path');

function resolvePlugin(name) {
return path.join(__dirname, '..', 'plugins', name);
}

module.exports = (api, options) => {
return {
presets: [],
plugins: [resolvePlugin('plugin-a'), resolvePlugin('plugin-b')]
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const path = require('path');

function resolvePlugin(name) {
return path.join(__dirname, '..', 'plugins', name);
}

function resolvePreset(name) {
return path.join(__dirname, '..', 'presets', name);
}

module.exports = (api, options) => {
return {
presets: [resolvePreset('a-b-preset')],
plugins: [resolvePlugin('plugin-c')]
};
};
12 changes: 12 additions & 0 deletions packages/shuvi/src/api/__tests__/fixtures/presets/simple-preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = (api, options) => {
api.__presets = api.__presets || [];
api.__presets.push({
name: 'simple-preset',
options
});

return {
presets: [],
plugins: []
};
};
29 changes: 27 additions & 2 deletions packages/shuvi/src/api/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import path from 'path';
import { IPluginConfig } from '@shuvi/types';
import { resolvePlugins } from '../plugin';
import { IPluginConfig, IPresetConfig } from '@shuvi/types';
import { resolvePlugins, resolvePresets } from '../plugin';

function callPlugins(context: any, ...plugins: IPluginConfig[]) {
resolvePlugins(plugins, {
dir: path.join(__dirname, 'fixtures/plugins')
}).forEach(p => p.get().apply(context));
}

function callPresets(context: any, ...presets: IPresetConfig[]) {
resolvePresets(presets, {
dir: path.join(__dirname, 'fixtures/presets')
}).forEach(p => p.get()(context));
}

describe('plugin', () => {
test('should accept class module as a plugin', () => {
const api = {};
Expand Down Expand Up @@ -70,3 +76,22 @@ describe('plugin', () => {
});
});
});

describe('preset', () => {
test('should accept function module as a plugin', () => {
const api = {};
callPresets(api, './simple-preset');
const presets = (api as any).__presets;
expect(presets.length).toBe(1);
expect(presets[0].name).toBe('simple-preset');
});

test('should accept module and option', () => {
const api = {};
callPresets(api, ['./simple-preset', { test: 1 }]);
const presets = (api as any).__presets;
expect(presets.length).toBe(1);
expect(presets[0].name).toBe('simple-preset');
expect(presets[0].options).toMatchObject({ test: 1 });
});
});
4 changes: 4 additions & 0 deletions packages/shuvi/src/api/__tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ import path from 'path';
export function resolvePlugin(name: string) {
return path.join(__dirname, 'fixtures', 'plugins', name);
}

export function resolvePreset(name: string) {
return path.join(__dirname, 'fixtures', 'presets', name);
}
119 changes: 68 additions & 51 deletions packages/shuvi/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import {
import { App, IRouteConfig, IFile } from '@shuvi/core';
import { joinPath } from '@shuvi/utils/lib/string';
import { deepmerge } from '@shuvi/utils/lib/deepmerge';
import invariant from '@shuvi/utils/lib/invariant';
import { Hookable } from '@shuvi/hooks';
import { setRuntimeConfig } from '../lib/runtimeConfig';
import { serializeRoutes, normalizeRoutes } from '../lib/routes';
import { PUBLIC_PATH, ROUTE_RESOURCE_QUERYSTRING } from '../constants';
import { runtime } from '../runtime';
import { defaultConfig, IConfig, loadConfig } from '../config';
import { IResources, IBuiltResource, IPlugin } from './types';
import { IResources, IBuiltResource, IPlugin, IPreset } from './types';
import { Server } from '../server';
import { setupApp } from './setupApp';
import { initCoreResource } from './initCoreResource';
import { resolvePlugins } from './plugin';
import { resolvePlugins, resolvePresets } from './plugin';
import { createPluginApi, PluginApi } from './pluginApi';
import { getPaths } from './paths';

Expand All @@ -44,7 +45,9 @@ class Api extends Hookable implements IApi {
private _server!: Server;
private _resources: IResources = {} as IResources;
private _routes: IRouteConfig[] = [];
private _presetPlugins: IPlugin[] = [];
private _plugins!: IPlugin[];
private _presets!: IPreset[];
private _pluginApi!: PluginApi;

constructor({ cwd, mode, config, configFile }: IApiOPtions) {
Expand Down Expand Up @@ -83,46 +86,7 @@ class Api extends Hookable implements IApi {
overrides: this._userConfig
});

const config: IApiConfig = deepmerge(defaultConfig, configFromFile);

// init plugins
this._plugins = resolvePlugins(config.plugins || [], {
dir: config.rootDir
});

let runPlugins = function runNext(
next?: Function,
cur: Function = () => void 0
) {
if (next) {
return (n: Function) =>
runNext(n, () => {
cur();
next();
});
}
return cur();
};
for (const plugin of this._plugins) {
const pluginInst = plugin.get();
if (typeof pluginInst.modifyConfig === 'function') {
this.tap<APIHooks.IHookGetConfig>('getConfig', {
name: 'pluginModifyConfig',
fn(config) {
return pluginInst.modifyConfig!(config);
}
});
}
runPlugins = runPlugins(() => {
pluginInst.apply(this.getPluginApi());
});
}

// prepare all properties befofre run plugins, so plugin can use all api of Api
this._config = await this.callHook<APIHooks.IHookGetConfig>({
name: 'getConfig',
initialValue: config
});
this._config = deepmerge(defaultConfig, configFromFile);
// do not allow to modify config
Object.freeze(this._config);
this._paths = getPaths({
Expand All @@ -133,12 +97,12 @@ class Api extends Hookable implements IApi {
// do not allow to modify paths
Object.freeze(this._paths);

runPlugins();
this._initPresetsAndPlugins();

initCoreResource(this);
// TODO?: move into application
if (typeof this.config.runtimeConfig === 'object') {
setRuntimeConfig(this.config.runtimeConfig);
if (typeof this._config.runtimeConfig === 'object') {
setRuntimeConfig(this._config.runtimeConfig);
}
}

Expand Down Expand Up @@ -307,20 +271,73 @@ class Api extends Hookable implements IApi {
return joinPath(this.paths.publicDir, ...paths);
}

getPluginApi(): PluginApi {
async destory() {
if (this._server) {
await this._server.close();
}
this._app.stopBuild(this.paths.appDir);
await this.callHook<APIHooks.IHookDestory>('destory');
}

private _getPluginApi(): PluginApi {
if (!this._pluginApi) {
this._pluginApi = createPluginApi(this);
}

return this._pluginApi;
}

async destory() {
if (this._server) {
await this._server.close();
private _initPresetsAndPlugins() {
const config = this._config;
// init presets
this._presets = resolvePresets(config.presets || [], {
dir: config.rootDir
});
for (const preset of this._presets) {
this._initPreset(preset);
}

// init plugins
this._plugins = resolvePlugins(this._config.plugins || [], {
dir: this._config.rootDir
});
const allPlugins = this._presetPlugins.concat(this._plugins);
for (const plugin of allPlugins) {
plugin.get().apply(this._getPluginApi());
}
}

private _initPreset(preset: IPreset) {
const { id, get: getPreset } = preset;
const { presets, plugins } = getPreset()(this._getPluginApi());

if (presets) {
invariant(
Array.isArray(presets),
`presets returned from preset ${id} must be Array.`
);

const resolvedPresets = resolvePresets(presets, {
dir: this._config.rootDir
});

for (const preset of resolvedPresets) {
this._initPreset(preset);
}
}

if (plugins) {
invariant(
Array.isArray(plugins),
`presets returned from preset ${id} must be Array.`
);

this._presetPlugins.push(
...resolvePlugins(plugins, {
dir: this._config.rootDir
})
);
}
this._app.stopBuild(this.paths.appDir);
await this.callHook<APIHooks.IHookDestory>('destory');
}
}

Expand Down
Loading

0 comments on commit 912d9a0

Please sign in to comment.