@modern-js/plugin-ssg 1.0.0-rc.9 → 1.1.1

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 (50) hide show
  1. package/.eslintrc.js +6 -0
  2. package/CHANGELOG.md +249 -0
  3. package/README.md +19 -21
  4. package/dist/js/modern/index.js +96 -58
  5. package/dist/js/modern/libs/{render.js → make.js} +22 -0
  6. package/dist/js/modern/libs/replace.js +3 -2
  7. package/dist/js/modern/libs/util.js +38 -22
  8. package/dist/js/modern/server/index.js +1 -1
  9. package/dist/js/modern/server/process.js +3 -2
  10. package/dist/js/modern/types.js +1 -0
  11. package/dist/js/node/index.js +94 -57
  12. package/dist/js/node/libs/{render.js → make.js} +28 -0
  13. package/dist/js/node/libs/replace.js +3 -2
  14. package/dist/js/node/libs/util.js +38 -29
  15. package/dist/js/node/server/index.js +2 -2
  16. package/dist/js/node/server/process.js +3 -2
  17. package/dist/types/libs/make.d.ts +5 -0
  18. package/dist/types/libs/replace.d.ts +1 -1
  19. package/dist/types/libs/util.d.ts +4 -8
  20. package/dist/types/types.d.ts +13 -17
  21. package/package.json +21 -9
  22. package/src/index.ts +84 -83
  23. package/src/libs/make.ts +45 -0
  24. package/src/libs/replace.ts +7 -4
  25. package/src/libs/util.ts +40 -28
  26. package/src/server/index.ts +2 -2
  27. package/src/server/process.ts +4 -2
  28. package/src/types.ts +26 -20
  29. package/tests/.eslintrc.js +6 -0
  30. package/tests/lib.test.ts +48 -169
  31. package/tests/util.test.ts +71 -32
  32. package/dist/js/modern/libs/createPage.js +0 -46
  33. package/dist/js/modern/libs/invoker.js +0 -55
  34. package/dist/js/modern/loader/index.js +0 -105
  35. package/dist/js/modern/manifest-op.js +0 -101
  36. package/dist/js/node/libs/createPage.js +0 -57
  37. package/dist/js/node/libs/invoker.js +0 -66
  38. package/dist/js/node/loader/index.js +0 -115
  39. package/dist/js/node/manifest-op.js +0 -124
  40. package/dist/types/libs/createPage.d.ts +0 -2
  41. package/dist/types/libs/invoker.d.ts +0 -5
  42. package/dist/types/libs/render.d.ts +0 -3
  43. package/dist/types/loader/index.d.ts +0 -4
  44. package/dist/types/manifest-op.d.ts +0 -18
  45. package/src/libs/createPage.ts +0 -42
  46. package/src/libs/invoker.ts +0 -55
  47. package/src/libs/render.ts +0 -16
  48. package/src/loader/index.ts +0 -99
  49. package/src/manifest-op.ts +0 -111
  50. package/tests/operate.test.ts +0 -39
package/src/index.ts CHANGED
@@ -1,71 +1,36 @@
1
1
  import path from 'path';
2
+ import { logger, PLUGIN_SCHEMAS } from '@modern-js/utils';
2
3
  import {
3
4
  createPlugin,
4
5
  useAppContext,
5
6
  useResolvedConfigContext,
6
7
  } from '@modern-js/core';
7
- import { BabelChain } from '@modern-js/babel-chain';
8
- import { INTERNAL_SRC_ALIAS, logger, PLUGIN_SCHEMAS } from '@modern-js/utils';
9
- import { LoaderManifest } from './manifest-op';
8
+ import { generatePath } from 'react-router-dom';
10
9
  import {
11
10
  AgreedRoute,
12
11
  AgreedRouteMap,
13
- CreatePageListener,
14
12
  EntryPoint,
15
13
  ExtendOutputConfig,
16
- HookContext,
14
+ SSG,
17
15
  SsgRoute,
18
16
  } from './types';
19
17
  import {
20
18
  formatOutput,
21
- getOutput,
22
- getSSGRenderLevel,
23
- getUrlPrefix,
24
- parsedSSGConfig,
19
+ isDynamicUrl,
25
20
  readJSONSpec,
26
- replaceWithAlias,
21
+ standardOptions,
27
22
  writeJSONSpec,
28
23
  } from './libs/util';
29
- import { invoker } from './libs/invoker';
30
24
  import { createServer } from './server';
31
25
  import { writeHtmlFile } from './libs/output';
32
26
  import { replaceRoute } from './libs/replace';
33
-
34
- const listStaticFiles = (
35
- pwd: string,
36
- entriesDir: string,
37
- useSSG: string | boolean,
38
- ) => {
39
- const absEntriesDir = path.join(pwd, entriesDir);
40
-
41
- const staticRenderLevel = getSSGRenderLevel(useSSG);
42
-
43
- const staticFiles = new LoaderManifest().get(
44
- absEntriesDir,
45
- staticRenderLevel,
46
- );
47
-
48
- // 将绝对路径转换成 alias,因为获取到的约定路由也是使用别名的
49
- const staticAlias = staticFiles.map(filepath =>
50
- replaceWithAlias(path.join(pwd, 'src'), filepath, INTERNAL_SRC_ALIAS),
51
- );
52
- return staticAlias;
53
- };
27
+ import { makeRoute } from './libs/make';
54
28
 
55
29
  export default createPlugin(
56
30
  (() => {
57
31
  const agreedRouteMap: AgreedRouteMap = {};
58
32
 
59
33
  return {
60
- config() {
61
- return {
62
- tools: {
63
- babel(config: any, { chain }: { chain: BabelChain }) {
64
- chain.plugin('./loader').use(require.resolve('./loader'));
65
- },
66
- },
67
- };
68
- },
69
34
  validateSchema() {
70
35
  return PLUGIN_SCHEMAS['@modern-js/plugin-ssg'];
71
36
  },
@@ -88,25 +53,19 @@ export default createPlugin(
88
53
  // eslint-disable-next-line react-hooks/rules-of-hooks
89
54
  const appContext = useAppContext();
90
55
 
91
- const { appDirectory } = appContext;
92
- const {
93
- output,
94
- server: { baseUrl },
95
- source: { entriesDir },
96
- } = resolvedConfig;
56
+ const { appDirectory, entrypoints } = appContext;
57
+ const { output } = resolvedConfig;
97
58
  const { ssg, path: outputPath } = output as typeof output &
98
59
  ExtendOutputConfig;
99
60
 
100
- const ssgOptions = Array.isArray(ssg) ? ssg.pop() : ssg;
61
+ const ssgOptions: SSG = Array.isArray(ssg) ? ssg.pop() : ssg;
101
62
  // no ssg configuration, skip ssg render.
102
63
  if (!ssgOptions) {
103
64
  return;
104
65
  }
105
66
 
106
- const { useSSG, userHook } = parsedSSGConfig(ssgOptions);
107
67
  const buildDir = path.join(appDirectory, outputPath as string);
108
68
  const routes = readJSONSpec(buildDir);
109
- const staticAlias = listStaticFiles(appDirectory, entriesDir!, useSSG);
110
69
 
111
70
  // filter all routes not web
112
71
  const pageRoutes = routes.filter(route => !route.isApi);
@@ -117,42 +76,83 @@ export default createPlugin(
117
76
  return;
118
77
  }
119
78
 
79
+ const intermediateOptions = standardOptions(ssgOptions, entrypoints);
80
+
81
+ if (!intermediateOptions) {
82
+ return;
83
+ }
84
+
120
85
  const ssgRoutes: SsgRoute[] = [];
86
+ // each route will try to match the configuration
87
+ pageRoutes.forEach(pageRoute => {
88
+ const { entryName, entryPath } = pageRoute;
89
+ const agreedRoutes = agreedRouteMap[entryName];
90
+ let entryOptions = intermediateOptions[entryName];
91
+
92
+ if (!agreedRoutes) {
93
+ // default behavior for non-agreed route
94
+ if (!entryOptions) {
95
+ return;
96
+ }
121
97
 
122
- // callback of context.createPage, to format output, collect page route
123
- const listener: CreatePageListener = (
124
- route: SsgRoute,
125
- agreed?: boolean,
126
- ) => {
127
- const urlPrefix = getUrlPrefix(route, baseUrl!);
128
- const ssgOutput = getOutput(route, urlPrefix, agreed);
129
- route.output = formatOutput(route.entryPath, ssgOutput);
130
- ssgRoutes.push(route);
131
- };
132
-
133
- // check if every allowed agreed route was collected
134
- const autoAddAgreed = (
135
- context: HookContext & {
136
- component: string;
137
- },
138
- ) => {
139
- // if not exist in allowed list, return false
140
- if (!staticAlias.includes(context.component)) {
141
- return false;
98
+ // only add entry route if entryOptions is true
99
+ if (entryOptions === true) {
100
+ ssgRoutes.push({ ...pageRoute, output: entryPath });
101
+ } else if (entryOptions.routes?.length > 0) {
102
+ // if entryOptions is object and has routes options
103
+ // add every route in options
104
+ const { routes: enrtyRoutes, headers } = entryOptions;
105
+ enrtyRoutes.forEach(route => {
106
+ ssgRoutes.push(makeRoute(pageRoute, route, headers));
107
+ });
108
+ }
109
+ } else {
110
+ // Unless entryOptions is set to false
111
+ // the default behavior is to add all file-based routes
112
+ if (entryOptions === false) {
113
+ return;
114
+ }
115
+
116
+ if (!entryOptions || entryOptions === true) {
117
+ entryOptions = { preventDefault: [], routes: [], headers: {} };
118
+ }
119
+
120
+ const {
121
+ preventDefault = [],
122
+ routes: userRoutes = [],
123
+ headers,
124
+ } = entryOptions;
125
+ // if the user sets the routes, then only add them
126
+ if (userRoutes.length > 0) {
127
+ userRoutes.forEach(route => {
128
+ if (typeof route === 'string') {
129
+ ssgRoutes.push(makeRoute(pageRoute, route, headers));
130
+ } else if (Array.isArray(route.params)) {
131
+ route.params.forEach(param => {
132
+ ssgRoutes.push(
133
+ makeRoute(
134
+ pageRoute,
135
+ { ...route, url: generatePath(route.url, param) },
136
+ headers,
137
+ ),
138
+ );
139
+ });
140
+ } else {
141
+ ssgRoutes.push(makeRoute(pageRoute, route, headers));
142
+ }
143
+ });
144
+ } else {
145
+ // otherwith add all except dynamic routes
146
+ agreedRoutes
147
+ .filter(route => !preventDefault.includes(route.path))
148
+ .forEach(route => {
149
+ if (!isDynamicUrl(route.path)) {
150
+ ssgRoutes.push(makeRoute(pageRoute, route.path, headers));
151
+ }
152
+ });
153
+ }
142
154
  }
143
- // if allowed, return collection state
144
- return !ssgRoutes.some(
145
- ssgRoute => ssgRoute.urlPath === context.route.path,
146
- );
147
- };
148
-
149
- await invoker(
150
- pageRoutes,
151
- agreedRouteMap,
152
- userHook,
153
- autoAddAgreed,
154
- listener,
155
- );
155
+ });
156
156
 
157
157
  if (ssgRoutes.length === 0) {
158
158
  return;
@@ -178,6 +178,7 @@ export default createPlugin(
178
178
  );
179
179
  }
180
180
  ssgRoute.isSSR = false;
181
+ ssgRoute.output = formatOutput(ssgRoute.output);
181
182
  });
182
183
 
183
184
  const htmlAry = await createServer(
@@ -0,0 +1,45 @@
1
+ import path from 'path';
2
+ import { ModernRoute } from '@modern-js/server';
3
+ import normalize from 'normalize-path';
4
+ import { compile } from '../server/prerender';
5
+ import { RouteOptions, SsgRoute } from '../types';
6
+
7
+ export function makeRender(
8
+ ssgRoutes: SsgRoute[],
9
+ render: ReturnType<typeof compile>,
10
+ port: number,
11
+ ): Promise<string>[] {
12
+ return ssgRoutes.map((ssgRoute: SsgRoute) =>
13
+ render({
14
+ url: ssgRoute.urlPath,
15
+ headers: { host: `localhost:${port}`, ...ssgRoute.headers },
16
+ connection: {},
17
+ }),
18
+ );
19
+ }
20
+
21
+ export function makeRoute(
22
+ baseRoute: ModernRoute,
23
+ route: string | RouteOptions,
24
+ headers: Record<string, any> = {},
25
+ ): SsgRoute {
26
+ const { urlPath, entryPath } = baseRoute;
27
+
28
+ if (typeof route === 'string') {
29
+ return {
30
+ ...baseRoute,
31
+ urlPath: normalize(`${urlPath}${route}`),
32
+ headers,
33
+ output: path.join(entryPath, `..${route}`),
34
+ };
35
+ } else {
36
+ return {
37
+ ...baseRoute,
38
+ urlPath: normalize(`${urlPath}${route.url}`),
39
+ headers: { ...headers, ...route.headers },
40
+ output: route.output
41
+ ? path.normalize(route.output)
42
+ : path.join(entryPath, `..${route.url}`),
43
+ };
44
+ }
45
+ }
@@ -2,7 +2,7 @@ import normalize from 'normalize-path';
2
2
  import { ModernRoute } from '@modern-js/server';
3
3
  import { SsgRoute } from '../types';
4
4
 
5
- export function exist(route: SsgRoute, pageRoutes: ModernRoute[]): number {
5
+ export function exist(route: ModernRoute, pageRoutes: ModernRoute[]): number {
6
6
  return pageRoutes.slice().findIndex(pageRoute => {
7
7
  const urlEqual = normalize(pageRoute.urlPath) === normalize(route.urlPath);
8
8
  const entryEqual = pageRoute.entryName === route.entryName;
@@ -16,12 +16,15 @@ export function exist(route: SsgRoute, pageRoutes: ModernRoute[]): number {
16
16
  export function replaceRoute(ssgRoutes: SsgRoute[], pageRoutes: ModernRoute[]) {
17
17
  // remove redundant fields and replace rendered entryPath
18
18
  const cleanSsgRoutes = ssgRoutes.map(ssgRoute => {
19
- const { output, ...cleanSsgRoute } = ssgRoute;
20
- return Object.assign(cleanSsgRoute, output ? { entryPath: output } : {});
19
+ const { output, headers, ...cleanSsgRoute } = ssgRoute;
20
+ return Object.assign(
21
+ cleanSsgRoute,
22
+ output ? { entryPath: output } : {},
23
+ ) as ModernRoute;
21
24
  });
22
25
 
23
26
  // all routes that need to be added and replaced
24
- const freshRoutes: SsgRoute[] = [];
27
+ const freshRoutes: ModernRoute[] = [];
25
28
  cleanSsgRoutes.forEach(ssgRoute => {
26
29
  const index = exist(ssgRoute, pageRoutes);
27
30
 
package/src/libs/util.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import path from 'path';
2
+ import { ROUTE_SPEC_FILE, fs, isSingleEntry } from '@modern-js/utils';
2
3
  import { ModernRoute } from '@modern-js/server';
3
- import { ROUTE_SPEC_FILE, fs } from '@modern-js/utils';
4
- import { SSGConfig, SsgRoute } from '../types';
5
- import { MODE } from '@/manifest-op';
6
-
7
- export function formatOutput(base: string, filename: string) {
8
- const file = path.extname(filename) ? filename : `${filename}/index.html`;
9
- const dirname = path.dirname(base);
10
- return path.join(dirname, file);
4
+ import { EntryPoint, MultiEntryOptions, SSG, SsgRoute } from '../types';
5
+
6
+ export function formatOutput(filename: string) {
7
+ const outputPath = path.extname(filename)
8
+ ? filename
9
+ : `${filename}/index.html`;
10
+ return outputPath;
11
11
  }
12
12
 
13
13
  export function formatPath(str: string) {
@@ -89,27 +89,39 @@ export const writeJSONSpec = (dir: string, routes: ModernRoute[]) => {
89
89
  fs.writeJSONSync(routeJSONPath, { routes }, { spaces: 2 });
90
90
  };
91
91
 
92
- export const getSSGRenderLevel = (key: boolean | string) => {
93
- const level = typeof key === 'boolean' ? MODE.LOOSE : MODE[key.toUpperCase()];
94
- // currently only MODE.STRICT and MODE.LOOSE are supported
95
- if (!level || level > 2 || level < 1) {
96
- throw new Error(
97
- `[SSG Render Fail] SSG 不支持当前 Mode,useSSG: ${key.toString()}, Level: ${level}`,
98
- );
99
- }
100
-
101
- return level;
102
- };
103
-
104
- export const parsedSSGConfig = (ssg: SSGConfig) => {
105
- const useSSG = typeof ssg === 'string' ? ssg : true;
106
- // eslint-disable-next-line @typescript-eslint/no-empty-function
107
- const userHook = typeof ssg === 'function' ? ssg : () => {};
108
- return { useSSG, userHook };
109
- };
110
-
111
92
  export const replaceWithAlias = (
112
93
  base: string,
113
94
  filePath: string,
114
95
  alias: string,
115
- ) => path.join(alias, path.relative(base, filePath));
96
+ ) => path.posix.join(alias, path.posix.relative(base, filePath));
97
+
98
+ export const standardOptions = (ssgOptions: SSG, entrypoints: EntryPoint[]) => {
99
+ if (ssgOptions === false) {
100
+ return false;
101
+ }
102
+
103
+ if (ssgOptions === true) {
104
+ return entrypoints.reduce((opt, entry) => {
105
+ opt[entry.entryName] = ssgOptions;
106
+ return opt;
107
+ }, {} as MultiEntryOptions);
108
+ } else if (typeof ssgOptions === 'object') {
109
+ const isSingle = isSingleEntry(entrypoints);
110
+
111
+ if (isSingle && typeof (ssgOptions as any).main === 'undefined') {
112
+ return { main: ssgOptions } as MultiEntryOptions;
113
+ } else {
114
+ return ssgOptions as MultiEntryOptions;
115
+ }
116
+ } else if (typeof ssgOptions === 'function') {
117
+ const intermediateOptions: MultiEntryOptions = {};
118
+ for (const entrypoint of entrypoints) {
119
+ const { entryName } = entrypoint;
120
+ // Todo may be async function
121
+ intermediateOptions[entryName] = ssgOptions(entryName);
122
+ }
123
+ return intermediateOptions;
124
+ }
125
+
126
+ return false;
127
+ };
@@ -1,7 +1,7 @@
1
1
  import childProcess from 'child_process';
2
2
  import path from 'path';
3
- import { NormalizedConfig, useAppContext } from '@modern-js/core';
4
3
  import { logger, SERVER_BUNDLE_DIRECTORY } from '@modern-js/utils';
4
+ import { NormalizedConfig, useAppContext } from '@modern-js/core';
5
5
  import { ModernRoute } from '@modern-js/server';
6
6
  import { SsgRoute } from '../types';
7
7
  import { CLOSE_SIGN } from './consts';
@@ -15,7 +15,7 @@ export const createServer = (
15
15
  new Promise((resolve, reject) => {
16
16
  // this side of the shallow copy of a route for subsequent render processing, to prevent the modification of the current field
17
17
  // manually enable the server-side rendering configuration for all routes that require SSG
18
- const backup = ssgRoutes.map(ssgRoute => ({
18
+ const backup: ModernRoute[] = ssgRoutes.map(ssgRoute => ({
19
19
  ...ssgRoute,
20
20
  isSSR: true,
21
21
  bundle: `${SERVER_BUNDLE_DIRECTORY}/${ssgRoute.entryName}.js`,
@@ -2,9 +2,10 @@ import Server, { ModernRoute } from '@modern-js/server';
2
2
  import portfinder from 'portfinder';
3
3
  import { NormalizedConfig } from '@modern-js/core';
4
4
  import { compatRequire } from '@modern-js/utils';
5
- import { makeRender } from '../libs/render';
5
+ import { makeRender } from '../libs/make';
6
6
  import { compile as createRender } from './prerender';
7
7
  import { CLOSE_SIGN } from './consts';
8
+ import { SsgRoute } from '@/types';
8
9
 
9
10
  type Then<T> = T extends PromiseLike<infer U> ? U : T;
10
11
 
@@ -67,11 +68,12 @@ process.on('message', async (chunk: string) => {
67
68
  // get server handler, render to ssr
68
69
  const render = createRender(modernServer!.getRequestHandler());
69
70
  const renderPromiseAry = makeRender(
70
- routes.filter(route => !route.isApi),
71
+ routes.filter(route => !route.isApi) as SsgRoute[],
71
72
  render,
72
73
  port,
73
74
  );
74
75
 
76
+ // eslint-disable-next-line promise/no-promise-in-callback
75
77
  const htmlAry = await Promise.all(renderPromiseAry);
76
78
  htmlAry.forEach((html: string) => {
77
79
  process.send!(html);
package/src/types.ts CHANGED
@@ -16,30 +16,36 @@ export type AgreedRouteMap = {
16
16
  [propNames: string]: AgreedRoute[];
17
17
  };
18
18
 
19
- export type FreshPageConfig = {
20
- url?: string;
21
- output?: string;
22
- params?: Record<string, string | number>;
19
+ export type SsgRoute = ModernRoute & {
20
+ output: string;
23
21
  headers?: Record<string, string>;
24
22
  };
25
- export type UserInterfaceRoute = ModernRoute & {
26
- path: string;
27
- agreed?: boolean;
28
- };
29
23
 
30
- export type CreatePageParam = FreshPageConfig | FreshPageConfig[];
31
- export type CreatePageListener = (route: SsgRoute, agreed?: boolean) => void;
24
+ export type RouteOptions =
25
+ | string
26
+ | {
27
+ url: string;
28
+ output?: string;
29
+ params?: Record<string, any>[];
30
+ headers?: Record<string, any>;
31
+ };
32
32
 
33
- export type SsgRoute = ModernRoute & {
34
- output?: string;
35
- headers?: Record<string, string>;
36
- };
37
- export type HookContext = {
38
- createPage: (config?: CreatePageParam) => any;
39
- route: UserInterfaceRoute;
40
- };
33
+ export type SingleEntryOptions =
34
+ | boolean
35
+ | {
36
+ preventDefault?: string[];
37
+ headers?: Record<string, any>;
38
+ routes: RouteOptions[];
39
+ };
40
+
41
+ export type MultiEntryOptions = Record<string, SingleEntryOptions>;
42
+
43
+ export type SSG =
44
+ | boolean
45
+ | SingleEntryOptions
46
+ | MultiEntryOptions
47
+ | ((entryName: string) => SingleEntryOptions);
41
48
 
42
- export type SSGConfig = string | ((context: any) => void);
43
49
  export type ExtendOutputConfig = {
44
- ssg: SSGConfig;
50
+ ssg: SSG;
45
51
  };
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ extends: ['@modern-js'],
3
+ parserOptions: {
4
+ project: require.resolve('./tsconfig.json'),
5
+ },
6
+ };