@modern-js/core 1.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +32 -0
  4. package/bin/modern-js.js +18 -0
  5. package/dist/js/modern/config/defaults.js +102 -0
  6. package/dist/js/modern/config/index.js +104 -0
  7. package/dist/js/modern/config/mergeConfig.js +20 -0
  8. package/dist/js/modern/config/schema/deploy.js +34 -0
  9. package/dist/js/modern/config/schema/index.js +104 -0
  10. package/dist/js/modern/config/schema/output.js +147 -0
  11. package/dist/js/modern/config/schema/server.js +167 -0
  12. package/dist/js/modern/config/schema/source.js +59 -0
  13. package/dist/js/modern/config/schema/tools.js +33 -0
  14. package/dist/js/modern/context.js +25 -0
  15. package/dist/js/modern/index.js +137 -0
  16. package/dist/js/modern/initWatcher.js +51 -0
  17. package/dist/js/modern/loadEnv.js +12 -0
  18. package/dist/js/modern/loadPlugins.js +66 -0
  19. package/dist/js/modern/utils/commander.js +19 -0
  20. package/dist/js/modern/utils/repeatKeyWarning.js +18 -0
  21. package/dist/js/node/config/defaults.js +109 -0
  22. package/dist/js/node/config/index.js +142 -0
  23. package/dist/js/node/config/mergeConfig.js +32 -0
  24. package/dist/js/node/config/schema/deploy.js +43 -0
  25. package/dist/js/node/config/schema/index.js +126 -0
  26. package/dist/js/node/config/schema/output.js +156 -0
  27. package/dist/js/node/config/schema/server.js +176 -0
  28. package/dist/js/node/config/schema/source.js +68 -0
  29. package/dist/js/node/config/schema/tools.js +40 -0
  30. package/dist/js/node/context.js +52 -0
  31. package/dist/js/node/index.js +212 -0
  32. package/dist/js/node/initWatcher.js +72 -0
  33. package/dist/js/node/loadEnv.js +28 -0
  34. package/dist/js/node/loadPlugins.js +76 -0
  35. package/dist/js/node/utils/commander.js +35 -0
  36. package/dist/js/node/utils/repeatKeyWarning.js +31 -0
  37. package/dist/types/config/defaults.d.ts +27 -0
  38. package/dist/types/config/index.d.ts +125 -0
  39. package/dist/types/config/mergeConfig.d.ts +29 -0
  40. package/dist/types/config/schema/deploy.d.ts +33 -0
  41. package/dist/types/config/schema/index.d.ts +474 -0
  42. package/dist/types/config/schema/output.d.ts +146 -0
  43. package/dist/types/config/schema/server.d.ts +179 -0
  44. package/dist/types/config/schema/source.d.ts +58 -0
  45. package/dist/types/config/schema/tools.d.ts +33 -0
  46. package/dist/types/context.d.ts +20 -0
  47. package/dist/types/index.d.ts +86 -0
  48. package/dist/types/initWatcher.d.ts +4 -0
  49. package/dist/types/loadEnv.d.ts +1 -0
  50. package/dist/types/loadPlugins.d.ts +16 -0
  51. package/dist/types/utils/commander.d.ts +7 -0
  52. package/dist/types/utils/repeatKeyWarning.d.ts +3 -0
  53. package/modern.config.js +13 -0
  54. package/package.json +73 -0
  55. package/src/config/defaults.ts +104 -0
  56. package/src/config/index.ts +296 -0
  57. package/src/config/mergeConfig.ts +68 -0
  58. package/src/config/schema/deploy.ts +23 -0
  59. package/src/config/schema/index.ts +111 -0
  60. package/src/config/schema/output.ts +66 -0
  61. package/src/config/schema/server.ts +105 -0
  62. package/src/config/schema/source.ts +34 -0
  63. package/src/config/schema/tools.ts +15 -0
  64. package/src/context.ts +46 -0
  65. package/src/index.ts +240 -0
  66. package/src/initWatcher.ts +81 -0
  67. package/src/loadEnv.ts +21 -0
  68. package/src/loadPlugins.ts +81 -0
  69. package/src/types.d.ts +0 -0
  70. package/src/utils/commander.ts +22 -0
  71. package/src/utils/repeatKeyWarning.ts +29 -0
  72. package/tests/fixtures/load-plugin/not-found/package.json +3 -0
  73. package/tests/fixtures/load-plugin/not-found/test-plugin-a.js +1 -0
  74. package/tests/fixtures/load-plugin/user-plugins/package.json +3 -0
  75. package/tests/fixtures/load-plugin/user-plugins/test-plugin-a.js +1 -0
  76. package/tests/fixtures/load-plugin/user-plugins/test-plugin-b.js +3 -0
  77. package/tests/loadEnv.test.ts +100 -0
  78. package/tests/loadPlugin.test.ts +29 -0
  79. package/tests/mergeConfig.test.ts +78 -0
  80. package/tests/repeatKeyWarning.test.ts +68 -0
  81. package/tests/schema.test.ts +109 -0
  82. package/tests/tsconfig.json +13 -0
  83. package/tsconfig.json +14 -0
@@ -0,0 +1,104 @@
1
+ import { OutputConfig, ServerConfig, SourceConfig } from '.';
2
+
3
+ const sourceDefaults: SourceConfig = {
4
+ entries: undefined,
5
+ disableDefaultEntries: false,
6
+ entriesDir: './src',
7
+ configDir: './config',
8
+ apiDir: './api',
9
+ envVars: [],
10
+ globalVars: undefined,
11
+ alias: undefined,
12
+ moduleScopes: undefined,
13
+ include: [],
14
+ };
15
+
16
+ const outputDefaults: OutputConfig = {
17
+ assetPrefix: '/',
18
+ htmlPath: 'html',
19
+ jsPath: 'static/js',
20
+ cssPath: 'static/css',
21
+ mediaPath: 'static/media',
22
+ path: 'dist',
23
+ title: '',
24
+ titleByEntries: undefined,
25
+ meta: {
26
+ charset: { charset: 'utf-8' },
27
+ viewport:
28
+ 'width=device-width, initial-scale=1.0, shrink-to-fit=no, viewport-fit=cover, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no',
29
+ 'http-equiv': { 'http-equiv': 'x-ua-compatible', content: 'ie=edge' },
30
+ renderer: 'webkit',
31
+ layoutmode: 'standard',
32
+ imagemode: 'force',
33
+ 'wap-font-scale': 'no',
34
+ 'format-detection': 'telephone=no',
35
+ },
36
+ metaByEntries: undefined,
37
+ inject: 'head',
38
+ injectByEntries: undefined,
39
+ mountId: 'root',
40
+ favicon: '',
41
+ faviconByEntries: undefined,
42
+ copy: undefined,
43
+ scriptExt: undefined,
44
+ disableHtmlFolder: false,
45
+ disableCssModuleExtension: false,
46
+ disableCssExtract: false,
47
+ enableCssModuleTSDeclaration: false,
48
+ disableMinimize: false,
49
+ enableInlineStyles: false,
50
+ enableInlineScripts: false,
51
+ disableSourceMap: false,
52
+ disableInlineRuntimeChunk: false,
53
+ disableAssetsCache: false,
54
+ enableLatestDecorators: false,
55
+ polyfill: 'entry',
56
+ dataUriLimit: 10000,
57
+ templateParameters: {},
58
+ templateParametersByEntries: undefined,
59
+ cssModuleLocalIdentName: '[name]__[local]--[hash:base64:5]',
60
+ enableModernMode: false,
61
+ federation: undefined,
62
+ disableNodePolyfill: false,
63
+ enableTsLoader: false,
64
+ };
65
+
66
+ const serverDefaults: ServerConfig = {
67
+ routes: undefined,
68
+ publicRoutes: undefined,
69
+ ssr: undefined,
70
+ ssrByEntries: undefined,
71
+ baseUrl: '/',
72
+ port: 8080,
73
+ };
74
+
75
+ const devDefaults = { assetPrefix: false };
76
+
77
+ const deployDefaults = {
78
+ microFrontend: {
79
+ enableHtmlEntry: false
80
+ },
81
+ domain: '',
82
+ domainByEntries: undefined,
83
+ };
84
+
85
+ const toolsDefaults = {
86
+ webpack: undefined,
87
+ babel: undefined,
88
+ postcss: undefined,
89
+ autoprefixer: undefined,
90
+ lodash: undefined,
91
+ devServer: undefined,
92
+ tsLoader: undefined,
93
+ terser: undefined,
94
+ minifyCss: undefined,
95
+ };
96
+
97
+ export const defaults = {
98
+ source: sourceDefaults,
99
+ output: outputDefaults,
100
+ server: serverDefaults,
101
+ dev: devDefaults,
102
+ deploy: deployDefaults,
103
+ tools: toolsDefaults,
104
+ };
@@ -0,0 +1,296 @@
1
+ import { loadConfig } from '@modern-js/load-config';
2
+ import Ajv, { ErrorObject } from 'ajv';
3
+ import ajvKeywords from 'ajv-keywords';
4
+ import logger from 'signale';
5
+ import {
6
+ createDebugger,
7
+ getPort,
8
+ isDev,
9
+ MetaOptions,
10
+ PLUGIN_SCHEMAS,
11
+ chalk,
12
+ } from '@modern-js/utils';
13
+ import mergeWith from 'lodash.mergewith';
14
+ import betterAjvErrors from 'better-ajv-errors';
15
+ import { codeFrameColumns } from '@babel/code-frame';
16
+ import { PluginConfig } from '../loadPlugins';
17
+ import { repeatKeyWarning } from '../utils/repeatKeyWarning';
18
+ import { defaults } from './defaults';
19
+ import { mergeConfig, NormalizedConfig } from './mergeConfig';
20
+ import { patchSchema, PluginValidateSchema } from './schema';
21
+
22
+ const debug = createDebugger('resolve-config');
23
+
24
+ export { defaults as defaultsConfig };
25
+
26
+ export interface SourceConfig {
27
+ entries?: Record<
28
+ string,
29
+ | string
30
+ | {
31
+ entry: string;
32
+ enableFileSystemRoutes?: boolean;
33
+ disableMount?: boolean;
34
+ }
35
+ >;
36
+ disableDefaultEntries?: boolean;
37
+ entriesDir?: string;
38
+ configDir?: string;
39
+ apiDir?: string;
40
+ envVars?: Array<string>;
41
+ globalVars?: Record<string, string>;
42
+ alias?:
43
+ | Record<string, string>
44
+ | ((aliases: Record<string, string>) => Record<string, unknown>);
45
+ moduleScopes?:
46
+ | Array<string | RegExp>
47
+ | ((scopes: Array<string | RegExp>) => Array<string | RegExp>);
48
+ include?: Array<string | RegExp>;
49
+ }
50
+
51
+ export interface OutputConfig {
52
+ assetPrefix?: string;
53
+ htmlPath?: string;
54
+ jsPath?: string;
55
+ cssPath?: string;
56
+ mediaPath?: string;
57
+ path?: string;
58
+ title?: string;
59
+ titleByEntries?: Record<string, string>;
60
+ meta?: MetaOptions;
61
+ metaByEntries?: Record<string, MetaOptions>;
62
+ inject?: 'body' | 'head' | boolean;
63
+ injectByEntries?: Record<string, 'body' | 'head' | boolean>;
64
+ mountId?: string;
65
+ favicon?: string;
66
+ faviconByEntries?: Record<string, string | undefined>;
67
+ copy?: Record<string, unknown>;
68
+ scriptExt?: Record<string, unknown>;
69
+ disableHtmlFolder?: boolean;
70
+ disableCssModuleExtension?: boolean;
71
+ disableCssExtract?: boolean;
72
+ enableCssModuleTSDeclaration?: boolean;
73
+ disableMinimize?: boolean;
74
+ enableInlineStyles?: boolean;
75
+ enableInlineScripts?: boolean;
76
+ disableSourceMap?: boolean;
77
+ disableInlineRuntimeChunk?: boolean;
78
+ disableAssetsCache?: boolean;
79
+ enableLatestDecorators?: boolean;
80
+ polyfill?: 'off' | 'usage' | 'entry' | 'ua';
81
+ dataUriLimit?: number;
82
+ templateParameters?: Record<string, unknown>;
83
+ templateParametersByEntries?: Record<
84
+ string,
85
+ Record<string, unknown> | undefined
86
+ >;
87
+ cssModuleLocalIdentName?: string;
88
+ enableModernMode?: boolean;
89
+ federation?: boolean;
90
+ disableNodePolyfill?: boolean;
91
+ enableTsLoader?: boolean;
92
+ }
93
+
94
+ export interface ServerConfig {
95
+ routes?: Record<
96
+ string,
97
+ | string
98
+ | {
99
+ route: string | string[];
100
+ disableSpa?: boolean;
101
+ }
102
+ >;
103
+ publicRoutes?: { [filepath: string]: string };
104
+ ssr?: boolean | Record<string, unknown>;
105
+ ssrByEntries?: Record<string, boolean | Record<string, unknown>>;
106
+ baseUrl?: string | Array<string>;
107
+ port?: number;
108
+ logger?: Record<string, string>;
109
+ measure?: Record<string, string>;
110
+ }
111
+
112
+ export interface DevConfig {
113
+ assetPrefix?: string | boolean;
114
+ }
115
+
116
+ export interface DeployConfig {
117
+ microFrontend?: {
118
+ enableHtmlEntry?: boolean;
119
+ };
120
+ domain?: string | Array<string>;
121
+ domainByEntries?: Record<string, string | Array<string>>;
122
+ }
123
+
124
+ type ConfigFunction =
125
+ | Record<string, unknown>
126
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
127
+ | ((config: Record<string, unknown>) => Record<string, unknown> | void);
128
+ export interface ToolsConfig {
129
+ webpack?: ConfigFunction;
130
+ babel?: ConfigFunction;
131
+ autoprefixer?: ConfigFunction;
132
+ postcss?: ConfigFunction;
133
+ lodash?: ConfigFunction;
134
+ devServer?: Record<string, unknown>;
135
+ tsLoader?: ConfigFunction;
136
+ terser?: ConfigFunction;
137
+ minifyCss?: ConfigFunction;
138
+ esbuild?: Record<string, unknown>;
139
+ }
140
+
141
+ export type RuntimeConfig = Record<string, any>;
142
+
143
+ export interface RuntimeByEntriesConfig {
144
+ [name: string]: RuntimeConfig;
145
+ }
146
+
147
+ export interface UserConfig {
148
+ source?: SourceConfig;
149
+ output?: OutputConfig;
150
+ server?: ServerConfig;
151
+ dev?: DevConfig;
152
+ deploy?: DeployConfig;
153
+ tools?: ToolsConfig;
154
+ plugins?: PluginConfig;
155
+ runtime?: RuntimeConfig;
156
+ runtimeByEntries?: RuntimeByEntriesConfig;
157
+ }
158
+
159
+ export type ConfigParam =
160
+ | UserConfig
161
+ | Promise<UserConfig>
162
+ | ((env: any) => UserConfig | Promise<UserConfig>);
163
+
164
+ export interface LoadedConfig {
165
+ config: UserConfig;
166
+ filePath: string | false;
167
+ dependencies: string[];
168
+ pkgConfig: UserConfig;
169
+ jsConfig: UserConfig;
170
+ }
171
+
172
+ export const defineConfig = (config: ConfigParam): ConfigParam => config;
173
+
174
+ export const loadUserConfig = async (
175
+ appDirectory: string,
176
+ filePath?: string,
177
+ ): Promise<LoadedConfig> => {
178
+ const loaded = loadConfig<ConfigParam>(appDirectory, filePath);
179
+
180
+ const config = !loaded
181
+ ? {}
182
+ : await (typeof loaded.config === 'function'
183
+ ? loaded.config(0)
184
+ : loaded.config);
185
+
186
+ return {
187
+ config: mergeWith({}, config || {}, loaded?.pkgConfig || {}),
188
+ jsConfig: config || {},
189
+ pkgConfig: (loaded?.pkgConfig || {}) as UserConfig,
190
+ filePath: loaded?.path,
191
+ dependencies: loaded?.dependencies || [],
192
+ };
193
+ };
194
+
195
+ const showAdditionalPropertiesError = (error: ErrorObject) => {
196
+ if (
197
+ error.keyword === 'additionalProperties' &&
198
+ error.instancePath &&
199
+ error.params.additionalProperty
200
+ ) {
201
+ const target = `${error.instancePath.substr(1)}.${
202
+ error.params.additionalProperty
203
+ }`;
204
+
205
+ const name = Object.keys(PLUGIN_SCHEMAS).find(key =>
206
+ (PLUGIN_SCHEMAS as Record<string, any>)[key].some(
207
+ (schemaItem: any) => schemaItem.target === target,
208
+ ),
209
+ );
210
+
211
+ if (name) {
212
+ logger.warn(
213
+ `The configuration of ${chalk.bold(
214
+ target,
215
+ )} is provided by plugin ${chalk.bold(name)}. Please use ${chalk.bold(
216
+ 'yarn new',
217
+ )} to enable the corresponding capability.\n`,
218
+ );
219
+ }
220
+ }
221
+ };
222
+
223
+ /* eslint-disable max-statements, max-params */
224
+ export const resolveConfig = async (
225
+ loaded: LoadedConfig,
226
+ configs: UserConfig[],
227
+ schemas: PluginValidateSchema[],
228
+ isRestart: boolean,
229
+ argv: string[],
230
+ ): Promise<NormalizedConfig> => {
231
+ const { config: userConfig, jsConfig, pkgConfig } = loaded;
232
+
233
+ const ajv = new Ajv({ $data: true, strict: false });
234
+
235
+ ajvKeywords(ajv);
236
+
237
+ const validateSchema = patchSchema(schemas);
238
+
239
+ const validate = ajv.compile(validateSchema);
240
+
241
+ repeatKeyWarning(validateSchema, jsConfig, pkgConfig);
242
+
243
+ // validate user config.
244
+ const valid = validate(userConfig);
245
+
246
+ if (!valid && validate.errors?.length) {
247
+ showAdditionalPropertiesError(validate?.errors[0]);
248
+ const errors = betterAjvErrors(
249
+ validateSchema,
250
+ userConfig,
251
+ validate.errors?.map(e => ({
252
+ ...e,
253
+ dataPath: e.instancePath,
254
+ })),
255
+ {
256
+ format: 'js',
257
+ indent: 2,
258
+ },
259
+ );
260
+
261
+ logger.log(
262
+ codeFrameColumns(
263
+ JSON.stringify(userConfig, null, 2),
264
+ {
265
+ start: errors?.[0].start as any,
266
+ end: errors?.[0].end as any,
267
+ },
268
+ {
269
+ highlightCode: true,
270
+ message: errors?.[0].error,
271
+ },
272
+ ),
273
+ );
274
+ throw new Error(`Validate configuration error`);
275
+ }
276
+
277
+ // validate config from plugins.
278
+ for (const config of configs) {
279
+ if (!validate(config)) {
280
+ logger.error(validate.errors);
281
+ throw new Error(`Validate configuration error.`);
282
+ }
283
+ }
284
+ const resolved = mergeConfig([defaults, ...configs, userConfig]);
285
+
286
+ resolved._raw = loaded.config;
287
+
288
+ if (isDev() && argv[0] === 'dev' && !isRestart) {
289
+ resolved.server.port = await getPort(resolved.server.port!);
290
+ }
291
+
292
+ debug('resolved %o', resolved);
293
+
294
+ return resolved;
295
+ };
296
+ /* eslint-enable max-statements, max-params */
@@ -0,0 +1,68 @@
1
+ import mergeWith from 'lodash.mergewith';
2
+ import { isFunction } from '@modern-js/utils';
3
+ import { UserConfig, SourceConfig, ToolsConfig } from '.';
4
+
5
+ export interface NormalizedSourceConfig
6
+ extends Omit<SourceConfig, 'alias' | 'moduleScopes'> {
7
+ alias: SourceConfig['alias'] | Array<SourceConfig['alias']>;
8
+ moduleScopes:
9
+ | SourceConfig['moduleScopes']
10
+ | Array<SourceConfig['moduleScopes']>;
11
+ }
12
+
13
+ export interface NormalizedToolsConfig
14
+ extends Omit<
15
+ ToolsConfig,
16
+ | 'webpack'
17
+ | 'babel'
18
+ | 'postcss'
19
+ | 'autoprefixer'
20
+ | 'lodash'
21
+ | 'tsLoader'
22
+ | 'terser'
23
+ | 'minifyCss'
24
+ | 'esbuild'
25
+ > {
26
+ webpack: ToolsConfig['webpack'] | Array<NonNullable<ToolsConfig['webpack']>>;
27
+ babel: ToolsConfig['babel'] | Array<NonNullable<ToolsConfig['babel']>>;
28
+ postcss: ToolsConfig['postcss'] | Array<NonNullable<ToolsConfig['postcss']>>;
29
+ autoprefixer:
30
+ | ToolsConfig['autoprefixer']
31
+ | Array<NonNullable<ToolsConfig['autoprefixer']>>;
32
+ lodash: ToolsConfig['lodash'] | Array<ToolsConfig['lodash']>;
33
+ tsLoader:
34
+ | ToolsConfig['tsLoader']
35
+ | Array<NonNullable<ToolsConfig['tsLoader']>>;
36
+ terser: ToolsConfig['terser'] | Array<NonNullable<ToolsConfig['terser']>>;
37
+ minifyCss:
38
+ | ToolsConfig['minifyCss']
39
+ | Array<NonNullable<ToolsConfig['minifyCss']>>;
40
+ esbuild: ToolsConfig['esbuild'] | Array<NonNullable<ToolsConfig['esbuild']>>;
41
+ }
42
+ export interface NormalizedConfig
43
+ extends Omit<Required<UserConfig>, 'source' | 'tools'> {
44
+ source: NormalizedSourceConfig;
45
+ tools: NormalizedToolsConfig;
46
+ _raw: UserConfig;
47
+ }
48
+
49
+ /**
50
+ * merge configuration from modern.config.js and plugins.
51
+ *
52
+ * @param configs - Configuration from modern.config.ts or plugin's config hook.
53
+ * @returns - normalized user config.
54
+ */
55
+ export const mergeConfig = (
56
+ configs: Array<UserConfig | NormalizedConfig>,
57
+ ): NormalizedConfig =>
58
+ mergeWith({}, ...configs, (target: any, source: any) => {
59
+ if (Array.isArray(target) && Array.isArray(source)) {
60
+ return [...target, ...source];
61
+ }
62
+ if (isFunction(source)) {
63
+ return Array.isArray(target)
64
+ ? [...target, source]
65
+ : [target, source].filter(Boolean);
66
+ }
67
+ return undefined;
68
+ });
@@ -0,0 +1,23 @@
1
+ import { ENTRY_NAME_PATTERN } from '@modern-js/utils';
2
+
3
+ export const deploy = {
4
+ type: 'object',
5
+ properties: {
6
+ microFrontend: {
7
+ type: 'object',
8
+ dependencies: {
9
+ enableHtmlEntry: { properties: { enableLegacy: { enum: [false] } } },
10
+ },
11
+ properties: {
12
+ enableHtmlEntry: { type: 'boolean' },
13
+ },
14
+ },
15
+ domain: { type: ['array', 'string'] },
16
+ domainByEntries: {
17
+ type: 'object',
18
+ patternProperties: {
19
+ [ENTRY_NAME_PATTERN]: { type: ['array', 'string'] },
20
+ },
21
+ },
22
+ },
23
+ };
@@ -0,0 +1,111 @@
1
+ import { JSONSchemaType } from 'ajv';
2
+ import { isObject, createDebugger } from '@modern-js/utils';
3
+ import cloneDeep from 'lodash.clonedeep';
4
+ import { source } from './source';
5
+ import { output } from './output';
6
+ import { server } from './server';
7
+ import { deploy } from './deploy';
8
+ import { tools } from './tools';
9
+
10
+ const debug = createDebugger('validate-schema');
11
+
12
+ const plugins = {
13
+ type: 'array',
14
+ additionalProperties: false,
15
+ };
16
+
17
+ const dev = {
18
+ type: 'object',
19
+ properties: { assetPrefix: { type: ['boolean', 'string'] } },
20
+ additionalProperties: false,
21
+ };
22
+ export interface PluginValidateSchema {
23
+ target: string;
24
+ schema: JSONSchemaType<any>;
25
+ }
26
+
27
+ export const patchSchema = (
28
+ pluginSchemas: Array<PluginValidateSchema | PluginValidateSchema[]>,
29
+ ) => {
30
+ const finalSchema = cloneDeep({
31
+ type: 'object',
32
+ additionalProperties: false,
33
+ properties: {
34
+ source,
35
+ output,
36
+ server,
37
+ deploy,
38
+ plugins,
39
+ dev,
40
+ tools,
41
+ },
42
+ });
43
+
44
+ const findTargetNode = (props: string[]) => {
45
+ let node = finalSchema.properties;
46
+
47
+ for (const prop of props) {
48
+ node = node[prop as keyof typeof node] as any;
49
+ if (!node || !isObject(node)) {
50
+ throw new Error(`add schema ${props.join('.')} error`);
51
+ }
52
+ (node as any).properties = (node as any).hasOwnProperty('properties')
53
+ ? (node as any).properties
54
+ : {};
55
+
56
+ node = (node as any).properties;
57
+ }
58
+ return node;
59
+ };
60
+
61
+ const finalPluginSchemas: PluginValidateSchema[] = [];
62
+ pluginSchemas.forEach(item => {
63
+ if (Array.isArray(item)) {
64
+ finalPluginSchemas.push(...item);
65
+ } else {
66
+ finalPluginSchemas.push(item);
67
+ }
68
+ });
69
+ for (const { target, schema } of finalPluginSchemas) {
70
+ if (!target) {
71
+ throw new Error(`should return target property in plugin schema.`);
72
+ }
73
+ const props = target.split('.');
74
+
75
+ const mountProperty = props.pop();
76
+
77
+ const targetNode = findTargetNode(props);
78
+
79
+ if (targetNode.hasOwnProperty(mountProperty!)) {
80
+ throw new Error(`${target} already exists in current validate schema`);
81
+ }
82
+
83
+ (targetNode as any)[mountProperty as string] = cloneDeep(schema);
84
+ }
85
+
86
+ debug(`final validate schema: %o`, finalSchema);
87
+
88
+ return finalSchema;
89
+ };
90
+
91
+ export const traverseSchema = (schema: ReturnType<typeof patchSchema>) => {
92
+ const keys: string[] = [];
93
+
94
+ const traverse = (
95
+ { properties }: { properties: any },
96
+ old: string[] = [],
97
+ ) => {
98
+ for (const key of Object.keys(properties)) {
99
+ const current = [...old, key];
100
+ if (properties[key].type === 'object' && properties[key].properties) {
101
+ traverse(properties[key], current);
102
+ } else {
103
+ keys.push(current.join('.'));
104
+ }
105
+ }
106
+ };
107
+
108
+ traverse(schema);
109
+
110
+ return keys;
111
+ };
@@ -0,0 +1,66 @@
1
+ import { ENTRY_NAME_PATTERN } from '@modern-js/utils';
2
+
3
+ export const output = {
4
+ type: 'object',
5
+ additionalProperties: false,
6
+ properties: {
7
+ assetPrefix: { type: 'string' },
8
+ path: { type: 'string' },
9
+ jsPath: { type: 'string' },
10
+ cssPath: { type: 'string' },
11
+ htmlPath: { type: 'string' },
12
+ mediaPath: { type: 'string' },
13
+ mountId: { type: 'string' },
14
+ favicon: { type: 'string' },
15
+ faviconByEntries: {
16
+ type: 'object',
17
+ patternProperties: { [ENTRY_NAME_PATTERN]: { type: 'string' } },
18
+ },
19
+ title: { type: 'string' },
20
+ titleByEntries: {
21
+ type: 'object',
22
+ patternProperties: { [ENTRY_NAME_PATTERN]: { type: 'string' } },
23
+ },
24
+ meta: { type: 'object' },
25
+ metaByEntries: {
26
+ type: 'object',
27
+ patternProperties: { [ENTRY_NAME_PATTERN]: { type: 'object' } },
28
+ },
29
+ inject: { enum: [true, 'head', 'body', false] },
30
+ injectByEntries: {
31
+ type: 'object',
32
+ patternProperties: {
33
+ [ENTRY_NAME_PATTERN]: { enum: [true, 'head', 'body', false] },
34
+ },
35
+ },
36
+ copy: { type: 'array' },
37
+ scriptExt: { type: 'object' },
38
+ disableHtmlFolder: { type: 'boolean' },
39
+ disableCssModuleExtension: { type: 'boolean' },
40
+ disableCssExtract: { type: 'boolean' },
41
+ enableCssModuleTSDeclaration: { type: 'boolean' },
42
+ disableMinimize: { type: 'boolean' },
43
+ enableInlineStyles: { type: 'boolean' },
44
+ enableInlineScripts: { type: 'boolean' },
45
+ disableSourceMap: { type: 'boolean' },
46
+ disableInlineRuntimeChunk: { type: 'boolean' },
47
+ disableAssetsCache: { type: 'boolean' },
48
+ enableLatestDecorators: { type: 'boolean' },
49
+ enableUsageBuiltIns: { type: 'boolean' },
50
+ enableTsLoader: { type: 'boolean' },
51
+ dataUriLimit: { type: 'number' },
52
+ templateParameters: { type: 'object' },
53
+ templateParametersByEntries: {
54
+ type: 'object',
55
+ patternProperties: { [ENTRY_NAME_PATTERN]: { type: 'object' } },
56
+ },
57
+ polyfill: {
58
+ type: 'string',
59
+ enum: ['usage', 'entry', 'off', 'ua'],
60
+ },
61
+ cssModuleLocalIdentName: { type: 'string' },
62
+ federation: { type: 'object' },
63
+ disableNodePolyfill: { type: 'boolean' },
64
+ enableModernMode: { type: 'boolean' },
65
+ },
66
+ };