@esmx/core 3.0.0-rc.10

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 (47) hide show
  1. package/README.md +29 -0
  2. package/dist/app.d.ts +84 -0
  3. package/dist/app.mjs +34 -0
  4. package/dist/cli/cli.d.ts +2 -0
  5. package/dist/cli/cli.mjs +73 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.mjs +3 -0
  8. package/dist/gez.d.ts +703 -0
  9. package/dist/gez.mjs +842 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.mjs +19 -0
  12. package/dist/manifest-json.d.ts +48 -0
  13. package/dist/manifest-json.mjs +21 -0
  14. package/dist/module-config.d.ts +185 -0
  15. package/dist/module-config.mjs +79 -0
  16. package/dist/pack-config.d.ts +251 -0
  17. package/dist/pack-config.mjs +26 -0
  18. package/dist/render-context.d.ts +1212 -0
  19. package/dist/render-context.mjs +1010 -0
  20. package/dist/utils/cache.d.ts +49 -0
  21. package/dist/utils/cache.mjs +16 -0
  22. package/dist/utils/import-map.d.ts +8 -0
  23. package/dist/utils/import-map.mjs +32 -0
  24. package/dist/utils/middleware.d.ts +57 -0
  25. package/dist/utils/middleware.mjs +51 -0
  26. package/dist/utils/path-without-index.d.ts +1 -0
  27. package/dist/utils/path-without-index.mjs +9 -0
  28. package/dist/utils/resolve-path.d.ts +2 -0
  29. package/dist/utils/resolve-path.mjs +4 -0
  30. package/dist/utils/static-import-lexer.d.ts +26 -0
  31. package/dist/utils/static-import-lexer.mjs +43 -0
  32. package/package.json +103 -0
  33. package/src/app.ts +144 -0
  34. package/src/cli/cli.ts +99 -0
  35. package/src/cli/index.ts +5 -0
  36. package/src/gez.ts +1026 -0
  37. package/src/index.ts +38 -0
  38. package/src/manifest-json.ts +80 -0
  39. package/src/module-config.ts +282 -0
  40. package/src/pack-config.ts +301 -0
  41. package/src/render-context.ts +1326 -0
  42. package/src/utils/cache.ts +68 -0
  43. package/src/utils/import-map.ts +47 -0
  44. package/src/utils/middleware.ts +118 -0
  45. package/src/utils/path-without-index.ts +9 -0
  46. package/src/utils/resolve-path.ts +33 -0
  47. package/src/utils/static-import-lexer.ts +85 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 缓存处理函数的类型定义
3
+ *
4
+ * @template T - 缓存数据的类型
5
+ * @param name - 缓存项的唯一标识符
6
+ * @param fetch - 获取数据的异步函数
7
+ * @returns 返回缓存的数据或新获取的数据
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const cache = createCache(true);
12
+ *
13
+ * // 第一次调用会执行 fetch 函数
14
+ * const data1 = await cache('key', async () => {
15
+ * return await fetchSomeData();
16
+ * });
17
+ *
18
+ * // 第二次调用会直接返回缓存的结果
19
+ * const data2 = await cache('key', async () => {
20
+ * return await fetchSomeData();
21
+ * });
22
+ * ```
23
+ */
24
+ export type CacheHandle = <T>(
25
+ name: string,
26
+ fetch: () => Promise<T>
27
+ ) => Promise<T>;
28
+
29
+ /**
30
+ * 创建一个缓存处理函数
31
+ *
32
+ * @param enable - 是否启用缓存功能
33
+ * @returns 返回一个缓存处理函数
34
+ *
35
+ * @description
36
+ * 当 enable 为 true 时,会创建一个带有内存缓存的处理函数,相同的 name 只会执行一次 fetch。
37
+ * 当 enable 为 false 时,每次调用都会执行 fetch 函数,不会缓存结果。
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // 创建一个启用缓存的处理函数
42
+ * const cacheEnabled = createCache(true);
43
+ *
44
+ * // 创建一个禁用缓存的处理函数
45
+ * const cacheDisabled = createCache(false);
46
+ *
47
+ * // 使用缓存处理函数
48
+ * const result = await cacheEnabled('userProfile', async () => {
49
+ * return await fetchUserProfile(userId);
50
+ * });
51
+ * ```
52
+ */
53
+ export function createCache(enable: boolean) {
54
+ if (enable) {
55
+ const map = new Map<string, any>();
56
+ return async <T>(name: string, fetch: () => Promise<T>): Promise<T> => {
57
+ if (map.has(name)) {
58
+ return map.get(name);
59
+ }
60
+ const result = await fetch();
61
+ map.set(name, result);
62
+ return result;
63
+ };
64
+ }
65
+ return <T>(name: string, fetch: () => Promise<T>): Promise<T> => {
66
+ return fetch();
67
+ };
68
+ }
@@ -0,0 +1,47 @@
1
+ import path from 'node:path';
2
+
3
+ import { pathWithoutIndex } from './path-without-index';
4
+
5
+ import type { ImportMap, SpecifierMap } from '@esmx/import';
6
+ import type { RuntimeTarget } from '../esmx';
7
+ import type { ManifestJson } from '../manifest-json';
8
+ import type { ParsedModuleConfig } from '../module-config';
9
+
10
+ /**
11
+ * 获取导入映射对象
12
+ */
13
+ export async function getImportMap(
14
+ target: RuntimeTarget,
15
+ manifests: readonly ManifestJson[],
16
+ moduleConfig: ParsedModuleConfig
17
+ ): Promise<ImportMap> {
18
+ const imports: SpecifierMap = {};
19
+ if (target === 'client') {
20
+ for (const manifest of manifests) {
21
+ for (const [name, value] of Object.entries(manifest.exports)) {
22
+ imports[`${manifest.name}/${name}`] =
23
+ `/${manifest.name}/${value}`;
24
+ }
25
+ }
26
+ } else {
27
+ for (const manifest of manifests) {
28
+ const link = moduleConfig.links.find(
29
+ (item) => item.name === manifest.name
30
+ );
31
+ if (!link) {
32
+ throw new Error(
33
+ `'${manifest.name}' service did not find module config`
34
+ );
35
+ }
36
+ for (const [name, value] of Object.entries(manifest.exports)) {
37
+ imports[`${manifest.name}/${name}`] = path.resolve(
38
+ link.root,
39
+ 'server',
40
+ value
41
+ );
42
+ }
43
+ }
44
+ }
45
+ pathWithoutIndex(imports);
46
+ return { imports };
47
+ }
@@ -0,0 +1,118 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import path from 'node:path';
3
+ import send from 'send';
4
+ import type { Esmx } from '../esmx';
5
+
6
+ /**
7
+ * 中间件函数类型定义
8
+ *
9
+ * @description
10
+ * 中间件是一个函数,用于处理 HTTP 请求。它接收请求对象、响应对象和下一个中间件函数作为参数。
11
+ * 中间件可以执行以下操作:
12
+ * - 修改请求和响应对象
13
+ * - 结束请求-响应循环
14
+ * - 调用下一个中间件
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // 创建一个简单的日志中间件
19
+ * const loggerMiddleware: Middleware = (req, res, next) => {
20
+ * console.log(`${req.method} ${req.url}`);
21
+ * next();
22
+ * };
23
+ * ```
24
+ */
25
+ export type Middleware = (
26
+ req: IncomingMessage,
27
+ res: ServerResponse,
28
+ next: Function
29
+ ) => void;
30
+
31
+ const reFinal = /\.final\.[a-zA-Z0-9]+$/;
32
+ /**
33
+ * 判断文件路径是否是一个符合 esmx 规范的不可变文件
34
+ * @param path 文件路径
35
+ */
36
+ export function isImmutableFile(filename: string) {
37
+ return reFinal.test(filename);
38
+ }
39
+
40
+ /**
41
+ * 创建 Esmx 应用的中间件
42
+ *
43
+ * @param esmx - Esmx 实例
44
+ * @returns 返回一个处理静态资源的中间件
45
+ *
46
+ * @description
47
+ * 该函数创建一个中间件,用于处理模块的静态资源请求。它会:
48
+ * - 根据模块配置创建对应的静态资源中间件
49
+ * - 处理资源的缓存控制
50
+ * - 支持不可变文件的长期缓存
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { Esmx, createMiddleware } from '@esmx/core';
55
+ *
56
+ * const esmx = new Esmx();
57
+ * const middleware = createMiddleware(esmx);
58
+ *
59
+ * // 在 HTTP 服务器中使用
60
+ * server.use(middleware);
61
+ * ```
62
+ */
63
+ export function createMiddleware(esmx: Esmx): Middleware {
64
+ const middlewares = esmx.moduleConfig.links.map((item): Middleware => {
65
+ const base = `/${item.name}/`;
66
+ const baseUrl = new URL(`file:`);
67
+ const root = path.resolve(item.root, 'client');
68
+ // const reFinal = /\.final\.[a-zA-Z0-9]+$/;
69
+ return (req, res, next) => {
70
+ const url = req.url ?? '/';
71
+ const { pathname } = new URL(req.url ?? '/', baseUrl);
72
+
73
+ if (!url.startsWith(base) || req.method !== 'GET') {
74
+ next();
75
+ return;
76
+ }
77
+
78
+ send(req, pathname.substring(base.length - 1), {
79
+ root
80
+ })
81
+ .on('headers', () => {
82
+ if (isImmutableFile(pathname)) {
83
+ res.setHeader(
84
+ 'cache-control',
85
+ 'public, max-age=31536000, immutable'
86
+ );
87
+ } else {
88
+ res.setHeader('cache-control', 'no-cache');
89
+ }
90
+ })
91
+ .pipe(res);
92
+ };
93
+ });
94
+ return mergeMiddlewares(middlewares);
95
+ }
96
+
97
+ /**
98
+ * 将多个中间件,合并成一个中间件执行
99
+ * @param middlewares 中间件列表
100
+ * @returns
101
+ */
102
+ export function mergeMiddlewares(middlewares: Middleware[]): Middleware {
103
+ return (req, res, next) => {
104
+ let index = 0;
105
+ function dispatch() {
106
+ if (index < middlewares.length) {
107
+ middlewares[index](req, res, () => {
108
+ index++;
109
+ dispatch();
110
+ });
111
+ return;
112
+ } else {
113
+ next();
114
+ }
115
+ }
116
+ dispatch();
117
+ };
118
+ }
@@ -0,0 +1,9 @@
1
+ export function pathWithoutIndex(i: Record<string, string>) {
2
+ const s = '/index';
3
+ Object.entries(i).forEach(([k, v]) => {
4
+ if (k.endsWith(s)) {
5
+ k = k.substring(0, k.length - s.length);
6
+ i[k] = v;
7
+ }
8
+ });
9
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+
3
+ export type ProjectPath =
4
+ | './'
5
+ | 'dist'
6
+ | 'dist/index.js'
7
+ | 'dist/package.json'
8
+ | 'dist/client'
9
+ | 'dist/client/manifest.json'
10
+ | 'dist/client/css'
11
+ | 'dist/client/images'
12
+ | 'dist/client/media'
13
+ | 'dist/client/fonts'
14
+ | 'dist/client/workers'
15
+ | 'dist/client/importmap'
16
+ | 'dist/client/versions/latest.tgz'
17
+ | 'dist/server'
18
+ | 'dist/server/manifest.json'
19
+ | 'dist/node'
20
+ | 'dist/node/src/entry.node.js'
21
+ | 'src'
22
+ | 'src/entry.node.ts'
23
+ | 'src/entry.client.ts'
24
+ | 'src/entry.server.ts'
25
+ | 'package.json';
26
+
27
+ export function resolvePath(
28
+ root: string,
29
+ projectPath: ProjectPath,
30
+ ...args: string[]
31
+ ): string {
32
+ return path.resolve(root, projectPath, ...args);
33
+ }
@@ -0,0 +1,85 @@
1
+ import type fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ import * as esmLexer from 'es-module-lexer';
6
+
7
+ /**
8
+ * 从 JS 代码中获取静态 import 的模块名列表。也许不能并发多个调用,没实验过。
9
+ * @param code js 代码
10
+ * @returns `Promise<string[]>` 静态 import 的模块名列表
11
+ */
12
+ export async function getImportsFromJsCode(code: string) {
13
+ await esmLexer.init;
14
+ const [imports] = esmLexer.parse(code);
15
+ // 静态导入 && 拥有模块名
16
+ return imports
17
+ .filter((item) => item.t === 1 && item.n)
18
+ .map((item) => item.n as string);
19
+ }
20
+
21
+ /**
22
+ * 从 JS 文件中获取静态 import 的模块名列表。
23
+ * @param filepath js 文件路径
24
+ * @returns `Promise<string[]>` 静态 import 的模块名列表
25
+ */
26
+ export async function getImportsFromJsFile(
27
+ filepath: fs.PathLike | fs.promises.FileHandle
28
+ ) {
29
+ const source = await fsp.readFile(filepath, 'utf-8');
30
+ return getImportsFromJsCode(source);
31
+ }
32
+
33
+ import type { ImportMap, SpecifierMap } from '@esmx/import';
34
+ import type { ParsedModuleConfig } from '../module-config';
35
+
36
+ export type ImportPreloadInfo = SpecifierMap;
37
+ /**
38
+ * 获取导入的预加载信息。
39
+ * @param specifier 模块名
40
+ * @param importMap 导入映射对象
41
+ * @param moduleConfig 模块配置
42
+ * @returns
43
+ * - `Promise<{ [specifier: string]: ImportPreloadPathString }>` 模块名和文件路径的映射对象
44
+ * - `null` specifier 不存在
45
+ */
46
+ export async function getImportPreloadInfo(
47
+ specifier: string,
48
+ importMap: ImportMap,
49
+ moduleConfig: ParsedModuleConfig
50
+ ) {
51
+ const importInfo = importMap.imports;
52
+ if (!importInfo || !(specifier in importInfo)) {
53
+ return null;
54
+ }
55
+
56
+ const ans: ImportPreloadInfo = {};
57
+
58
+ const needHandles: string[][] = [[specifier]];
59
+ // 词法分析是耗时操作,因此处理的文件越少越快,换句话说就是深度越浅越快,因此这里使用广度优先搜索
60
+ while (needHandles.length) {
61
+ const needHandle: string[] = [];
62
+ for (const specifier of needHandles.shift()!) {
63
+ let filepath = importInfo[specifier];
64
+ const splitRes = filepath.split('/');
65
+ if (splitRes[0] === '') splitRes.shift();
66
+ const name = splitRes.shift() + '';
67
+ const link = moduleConfig.links.find((item) => item.name === name);
68
+ if (!link) {
69
+ continue;
70
+ }
71
+ filepath = path.join(link.root, 'client', ...splitRes);
72
+ const imports = await getImportsFromJsFile(filepath);
73
+ imports.forEach((specifier) => {
74
+ // 如果模块名在 importMap 中存在,且没处理过
75
+ if (specifier in importInfo && !ans[specifier]) {
76
+ ans[specifier] = importInfo[specifier];
77
+ needHandle.push(specifier);
78
+ }
79
+ });
80
+ }
81
+ needHandle.length && needHandles.push(needHandle);
82
+ }
83
+
84
+ return ans;
85
+ }