diff --git a/.npmrc b/.npmrc index dd0f90e08..ae59eca65 100644 --- a/.npmrc +++ b/.npmrc @@ -1,5 +1,5 @@ engine-strict=true -registry=https://registry.npmjs.org +registry=https://registry.npmmirror.com public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*stylelint* public-hoist-pattern[]=*postcss* diff --git a/.nvmrc b/.nvmrc index 9bcccb943..620c5e1e9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.13.1 +v22.9.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index 948dee957..264d57e1f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,6 @@ "source.fixAll": "explicit" }, "cSpell.words": [ - "fflate", "Importmap", "rspack" ] diff --git a/examples/docs/.gitignore b/examples/docs/.gitignore index 044373fb2..f988c31ea 100644 --- a/examples/docs/.gitignore +++ b/examples/docs/.gitignore @@ -23,3 +23,4 @@ dist-ssr *.sln *.sw? doc_build +api \ No newline at end of file diff --git a/examples/docs/package.json b/examples/docs/package.json index ec21eef61..14686ff8d 100644 --- a/examples/docs/package.json +++ b/examples/docs/package.json @@ -12,6 +12,7 @@ "rspress": "^1.32.0" }, "devDependencies": { + "typescript": "5.1.3", "@types/node": "^16" } } diff --git a/examples/docs/rspress.config.ts b/examples/docs/rspress.config.ts index bc4ae25df..fc8efe647 100644 --- a/examples/docs/rspress.config.ts +++ b/examples/docs/rspress.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ globalStyles: path.join(__dirname, 'src/styles/index.css'), title: 'Gez', description: - 'Gez 是一个基于 Rspack 构建的模块链接(Module Link) 解决方案,通过 importmap 将多服务的模块映射到具有强缓存,基于内容哈希的 URL 中。', + 'Gez 是一个基于 Rspack 构建的模块链接(Module Link) 解决方案,通过 importmap 将多服务模块映射到具有强缓存,基于内容哈希的 URL 中。', icon: '/logo.svg', base: '/gez/', logo: '/logo.svg', @@ -20,5 +20,6 @@ export default defineConfig({ content: 'https://github.com/dp-os/gez' } ] - } + }, + markdown: {} }); diff --git a/examples/docs/src/_meta.json b/examples/docs/src/_meta.json index 287be79ce..eab14ad5c 100644 --- a/examples/docs/src/_meta.json +++ b/examples/docs/src/_meta.json @@ -6,7 +6,7 @@ }, { "text": "API", - "link": "/api/gez", + "link": "/api/index", "activeMatch": "/api/" } ] diff --git a/examples/docs/src/api/_meta.json b/examples/docs/src/api/_meta.json deleted file mode 100644 index 85087305e..000000000 --- a/examples/docs/src/api/_meta.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "gez", - "render-context", - "package-json" -] \ No newline at end of file diff --git a/examples/docs/src/api/gez.mdx b/examples/docs/src/api/gez.mdx deleted file mode 100644 index 75954ceb3..000000000 --- a/examples/docs/src/api/gez.mdx +++ /dev/null @@ -1,303 +0,0 @@ -import { Badge } from '@theme'; - -# Gez - -## GezOptions -使用方式: -```ts title="src/entry.node.ts" -import type { GezOptions } from '@gez/core'; - -export default { - // 配置选项 -} satisfies GezOptions; - -``` - -### name -- **类型:** `string` -- **默认值:** `'gez'` -- **描述:** 服务的名称,全局唯一。 - -:::tip -如果你的网站,同一个域名下,使用 Gez 打包了多个项目,那么你需要配置一个 `name` 来区分不同的项目。 -::: - -### root -- **类型:** `string` -- **默认值:** `cwd()` -- **描述:** 项目根目录,默认为当前执行命令的目录。 - -:::warning -如果你没有充足的理由,你都不应该配置它。 -::: - -### isProd -- **类型:** `boolean` -- **默认值:** `process.env.NODE_ENV === 'production'` -- **描述:** 是否是生产环境。 - -:::warning -如果你没有充足的理由,你都不应该配置它。 -::: - -### isInstall -- **类型:** `boolean` -- **默认值:** `process.env.npm_config_production !== 'true'` -- **描述:** 安装生产依赖时,是否安装远程依赖。 - -:::warning -如果你没有充足的理由,你都不应该配置它。 -::: - -### basePathPlaceholder -- **类型:** `string | false` -- **默认值:** `'[[[___GEZ_DYNAMIC_BASE___]]]'` -- **描述:** 动态路径的变量占位符,深入了解请看[基本路径](/guide/essentials/base-path.mdx)说明。 - -::: tip -如果你的业务不存在动态路径的需求,可以设置 `false`,减少一次占位符替换,来提升渲染性能。 -::: - -### modules -模块链接配置。 -#### modules.exports -- **类型:** `string[]` -- **默认值:** `[]` -- **描述:** 对外模块导出。 - -```ts title="src/entry.node.ts" -export default { - modules: { - exports: [ - 'root:src/components/layout.vue', - 'root:src/utils/index.ts', - 'npm:vue', - 'npm:vue-router' - ] - } -} satisfies GezOptions; -``` - -::: tip -你可以将当前项目的模块或者当前项目的第三方依赖,对外导出,这样其它服务就可以使用了。 -::: - -#### modules.imports -- **类型:** `Record` -- **默认值:** `{}` -- **描述:** 配置远程依赖。 -```ts title="src/entry.node.ts" -export default { - modules: { - imports: { - 'ssr-base': ['root:../ssr-base/dist', 'https:///ssr-base/versions/latest.json'] - } - } -} satisfies GezOptions; -``` - -:::tip -- 第一个参数为本地的存储路径 -- 第二个参数是远程依赖的地址 -- 执行 `gez install` 命令可以下载远程依赖到本地的地址。 - -::: - -你也可以直接配置本地地址。 - -```ts title="src/entry.node.ts" -export default { - modules: { - imports: { - 'ssr-base': 'root:../ssr-base/dist' - } - } -} satisfies GezOptions; -``` - -#### modules.externals -- **类型:** `Record` -- **默认值:** `{}` -- **描述:** 外部依赖设置,你可以将当前服务的依赖,指向到其它导出的服务。 -```ts title="src/entry.node.ts" -export default { - name: 'ssr-main', - modules: { - externals: { - vue: 'ssr-base/npm/vue', - 'vue-router': 'ssr-base/npm/vue-router' - } - } -} satisfies GezOptions; -``` - -:::warning -需要先配置对应服务的 [modules.imports](#modulesimports),否则运行起来会报错,提示找不到模块。 -::: - -### createDevApp() -- **类型:** `(gez: Gez) => Promise` -- **默认值:** `isProd === false` -- **描述:** 创建开发应用,在执行 [dev](/guide/essentials/command.mdx#gez-dev)、[build](/guide/essentials/command.mdx#gez-build)、[preview](/guide/essentials/command.mdx#gez-preview) 命令时调用。 - -```ts title="src/entry.node.ts" - -export default { - async createDevApp(gez) { - return import('@gez/rspack').then((m) => - m.createRspackHtmlApp(gez, { - config(context) { - // 可以在这里修改 Rspack 编译的配置 - } - }) - ); - } -} satisfies GezOptions; -``` - -::: tip -- Rspack 配置请看[这里](./rspack.mdx) - -::: - -### createServer() -- **类型:** `(gez: Gez) => Promise` -- **默认值:** `undefined` -- **描述:** 创建服务器,执行 [dev](/guide/essentials/command.mdx#gez-dev)、[build](/guide/essentials/command.mdx#gez-build)、[preview](/guide/essentials/command.mdx#gez-preview) 命令时调用。 - -```ts title="src/entry.node.ts" -import http from 'node:http'; - -export default { - async createServer(gez) { - const server = http.createServer((req, res) => { - // 静态文件处理 - gez.middleware(req, res, async () => { - // 传入渲染的参数 - const render = await gez.render({ - params: { - url: req.url - } - }); - // 响应 HTML 内容 - res.end(render.html); - }); - }); - // 监听端口 - server.listen(3000, () => { - console.log('http://localhost:3000'); - }); - }, -} satisfies GezOptions; -``` -::: tip -你也可以使用其它的框架来创建服务器,例如:[Express](https://expressjs.com/)。 -::: - -### postCompileProdHook() -- **类型:** `(gez: Gez) => Promise` -- **默认值:** `undefined` -- **描述:** [gez build](/guide/essentials/command.mdx#gez-build) 构建完成后,以生产模式执行的钩子。 - -```ts title="src/entry.node.ts" -import path from 'node:path'; - -export default { - async postCompileProdHook(gez) { - const render = await gez.render({ - base: '/gez', - params: { url: '/' } - }); - gez.writeSync( - path.resolve(gez.getProjectPath('dist/client'), 'index.html'), - render.html - ); - } -} satisfies GezOptions; -``` - -:::tip -你可以使用这个钩子来生成静态网站。 -::: - -## Gez -创建的实例。 - -### name -- **类型:** `string` -- **描述:** 服务名称。 - -### root -- **类型:** `string` -- **描述:** 项目根目录。 - -### isProd -- **类型:** `boolean` -- **描述:** 是否是生产环境。 - -### isInstall -- **类型:** `boolean` -- **描述:** 是否安装远程依赖。 - -### basePath -- **类型:** `string` -- **描述:** 根据服务名称生成的静态资源基本路径。 - -### basePathPlaceholder -- **类型:** `string` -- **描述:** 基本路径变量占位符。 - -### varName -- **类型:** `string` -- **描述:** 根据服务名称生成的 JS 变量名称。 - -### command -- **类型:** `COMMAND` -- **描述:** 当前执行的命令。 - -### moduleConfig -- **类型:** `ParsedModuleConfig` -- **描述:** 根据传入的 [modules](#modules) 选项解析出来的对象。 - -::: tip -我们提供了一个 Rspack 的插件 `@gez/rspack-module-link-plugin`,`moduleConfig` 会传递为该插件的选项,除非你想要深入了解模块链接的实现,不然大多数时候你都不需要关心它。 -::: - -### init() -- **类型:** `init(command: COMMAND): Promise` -- **描述:** 初始化 Gez 实例。 - -### install() -- **类型:** `(): Promise` -- **描述:** 安装远程模块到本地。 - -### build() -- **类型:** `(): Promise` -- **描述:** 构建生产代码。 - -### release() -- **类型:** `(): Promise` -- **描述:** 生产代码构建完成后,可以构建版本代码,提供给其它服务使用。 - -### middleware() -- **类型:** `Middleware` -- **描述:** 生产代码构建完成后,可以构建版本代码,提供给其它服务使用。 - -### render() -- **类型:** `(options?: RenderContextOptions) => Promise` -- **描述:** 调用 `entry.server.ts` 导出的渲染函数。 -### getProjectPath() -- **类型:** `getProjectPath(projectPath: ProjectPath): string` -- **描述:** 获取项目文件的绝对路径。 - -### destroy() -- **类型:** `(): Promise` -- **描述:** 调用 `entry.server.ts` 导出的渲染函数。 - -### write -- **类型:** `(filepath: string, data: any): Promise` -- **描述:** 异步的写入一个文件。 - -### writeSync -- **类型:** `(filepath: string, data: any): void` -- **描述:** 同步的写入一个文件。 diff --git a/examples/docs/src/api/package-json.mdx b/examples/docs/src/api/package-json.mdx deleted file mode 100644 index 52f290a29..000000000 --- a/examples/docs/src/api/package-json.mdx +++ /dev/null @@ -1,67 +0,0 @@ -import { Badge } from '@theme'; - -# PackageJson -每次构建完成后,都会在 `client` 和 `server` 目录创建一个 `package.json` 文件用来描述当前服务的一些基本信息。 - -## 基本字段 - -### name -- **类型:** `string` -- **描述:** 服务的名字。 - -### version -- **类型:** `string` -- **描述:** 版本号,目前无实际意义。 - -### hash -- **类型:** `string` -- **描述:** Rspack 本次构建的版本号。 - -### type -- **类型:** `'module'` -- **描述:** 软件包的模块系统。 - -### exports -- **类型:** `Record` -- **描述:** 当前服务对外导出的模块。 - -### files -- **类型:** `string[]` -- **描述:** 构建产物的清单。 - -### chunks -- **类型:** `Record` -- **描述:** 构建产物的依赖信息。 - -## PackageJsonChunks -- 在服务端渲染过程中,收集了渲染的模块元信息。根据这些元信息来生成客户端首屏加载所需要的资源。 -- 更多了解请查看:[渲染上下文](/guide/essentials/render-context) -### js -- **类型:** `string` -- **描述:** 当前编译的 JS 文件。 - -### css -- **类型:** `string[]` -- **描述:** 当前 JS 抽离出来的 CSS 文件列表。 - -### resources -- **类型:** `string[]` -- **描述:** 除了 JS 和 CSS 之外的其它的文件。 - -### sizes -- **类型:** `PackageJsonChunkSizes` -- **描述:** 资源的大小。 - -## PackageJsonChunkSizes - -### js -- **类型:** `number` -- **描述:** JS 文件的大小。 - -### css -- **类型:** `number` -- **描述:** CSS 文件的大小。 - -### resource -- **类型:** `number` -- **描述:** 除了 JS 和 CSS 之外的其它的文件的大小。 \ No newline at end of file diff --git a/examples/docs/src/api/render-context.mdx b/examples/docs/src/api/render-context.mdx deleted file mode 100644 index 0f73ae870..000000000 --- a/examples/docs/src/api/render-context.mdx +++ /dev/null @@ -1,137 +0,0 @@ -import { Badge } from '@theme'; - -# RenderContext - -## 选项 -使用方式: -```ts title="src/entry.node.ts" -const rc = await gez.render({ - // 配置选项 -}); - -``` -### base -- **类型:** `string` -- **默认值:** `''` -- **描述:** 静态资产的公共路径,可以根据业务的上下文来动态设置不同的路径。 - -::: warning -如果 Gez 的 [basePathPlaceholder](./gez.mdx#basepathplaceholder) 选项设置为 `false`,该功能将无法使用。 -::: - -### entryName -- **类型:** `string` -- **默认值:** `'default'` -- **描述:** 渲染时,执行 `entry.server.ts` 文件导出函数的名称。 - -### params -- **类型:** `Record` -- **默认值:** `{}` -- **描述:** 自定义渲染参数。 - - -## 实例 - -使用方式: -```ts title="src/entry.server.ts" -import type { RenderContext } from '@gez/core'; - -export default async (rc: RenderContext) => { - // SSR 服务端渲染的值 - const html = ''; - // 提交依赖收集 - await rc.commit(); - // 响应 HTML - rc.html = ` - - - - ${rc.preload()} - Gez - ${rc.css()} - - - ${html} - ${rc.importmap()} - ${rc.moduleEntry()} - ${rc.modulePreload()} - - -`; -}; - -``` - -### gez -- **类型:** [Gez](./gez.mdx#gez-1) -- **描述:** Gez 的实例。 - -### redirect -- **类型:** `string | null` -- **描述:** 重定向地址。 - -### status -- **类型:** `number | null` -- **描述:** 响应的状态码。 - -### base -- **类型:** `string` -- **描述:** 参数传入的 `base`。 - -### params -- **类型:** `string` -- **描述:** 参数传入的 `params`。 - -### entryName -- **类型:** `string` -- **描述:** 参数传入的 `entryName`。 - -### importMetaSet -- **类型:** `Set` -- **描述:** 服务端渲染过程中,收集执行模块的元信息。 - -### files -- **类型:** `RenderFiles` -- **描述:** 根据 `importMetaSet` 收集的模块元信息,当成当前页面在客户端首次加载所需要的资源文件。 - - -### html -- **类型:** `string` -- **描述:** 当前请求响应的 html 内容。 - -### serialize() -- **类型:** `(input: any, options?: serialize.SerializeJSOptions): string` -- **描述:** 透传 [serialize-javascript](https://github.com/yahoo/serialize-javascript)。 - -### state() -- **类型:** `(varName: string, data: Record): string` -- **描述:** 在 window 对象,注入一个 JS 变量对象,data 必须是可以被序列化的。 - -### getPackagesJson() -- **类型:** `(): Promise` -- **描述:** 获取全部服务的远程包信息。 - -### commit() -- **类型:** `(): Promise` -- **描述:** 通过应用渲染完成后,提交模块依赖更新 `files` 对象。 - -### preload() -- **类型:** `(): string` -- **描述:** 生成 JS 和 CSS 文件的预加载代码。 - -### css() -- **类型:** `(): string` -- **描述:** 生成服务端首屏加载的 CSS 文件。 - -### importmap() -- **类型:** `(): string` -- **描述:** 生成 importmap 相关代码。 - -### moduleEntry() -- **类型:** `(): string` -- **描述:** 生成模块入口执行代码。 - -### modulePreload() -- **类型:** `(): string` -- **描述:** ESM 模块预加载代码。 - diff --git a/examples/docs/src/guide/essentials/command.mdx b/examples/docs/src/guide/essentials/command.mdx index e45c091d3..25223dcca 100644 --- a/examples/docs/src/guide/essentials/command.mdx +++ b/examples/docs/src/guide/essentials/command.mdx @@ -6,7 +6,7 @@ "dev": "gez dev", "build": "npm run build:ssr && npm run build:dts && npm run release", "build:ssr": "gez build", - "build:dts": "tsc --noEmit --outDir dist/server", + "build:dts": "tsc --noEmit --outDir dist/src", "release": "gez release", "preview": "gez preview", "start": "gez start", @@ -42,7 +42,7 @@ export default { ## gez release 当前服务如果有对外导出模块时使用。 - 执行 `gez build` 命令,构建生产产物。 -- 执行 `npm run build:dts` 命令,将类型输出到 `dist/server/src` 目录,本地开发时,可以得到类型提示。 +- 执行 `npm run build:dts` 命令,将类型输出到 `dist/server/` 目录,本地开发时,可以得到类型提示。 - 执行 `gez release` 命令,将 `dist/client` 和 `dist/server` 目录生成 zip 压缩文件,放到 `dist/client/versions` 目录中。 - 将 `dist` 目录的代码,部署到生产环境中。 - 其它服务调用 diff --git a/examples/docs/src/guide/essentials/config.mdx b/examples/docs/src/guide/essentials/config.mdx index 0d493f809..2b86ab01b 100644 --- a/examples/docs/src/guide/essentials/config.mdx +++ b/examples/docs/src/guide/essentials/config.mdx @@ -223,8 +223,8 @@ export default { params: { url: '/' } }); gez.writeSync( - path.resolve(gez.getProjectPath('dist/client'), 'index.html'), - render.html + gez.resolvePath('dist/client', url.substring(1), 'index.html'), + rc.html ); } } satisfies GezOptions; diff --git a/examples/docs/src/guide/essentials/csr.mdx b/examples/docs/src/guide/essentials/csr.mdx index bc04c6c33..f35237458 100644 --- a/examples/docs/src/guide/essentials/csr.mdx +++ b/examples/docs/src/guide/essentials/csr.mdx @@ -47,8 +47,8 @@ export default { } }); gez.writeSync( - path.resolve(gez.getProjectPath('dist/client'), 'index.html'), - render.html + gez.resolvePath('dist/client', url.substring(1), 'index.html'), + rc.html ); } } satisfies GezOptions; diff --git a/examples/docs/src/guide/start/getting-started.mdx b/examples/docs/src/guide/start/getting-started.mdx index f6213bdd4..638530ca4 100644 --- a/examples/docs/src/guide/start/getting-started.mdx +++ b/examples/docs/src/guide/start/getting-started.mdx @@ -36,7 +36,7 @@ npm init "dev": "gez dev", "build": "npm run build:ssr && npm run build:dts && npm run release", "build:ssr": "gez build", - "build:dts": "tsc --noEmit --outDir dist/server", + "build:dts": "tsc --noEmit --outDir dist/src", "release": "gez release", "preview": "gez preview", "start": "gez start", diff --git a/examples/docs/tsconfig.json b/examples/docs/tsconfig.json index 936218cee..cf1a9317a 100644 --- a/examples/docs/tsconfig.json +++ b/examples/docs/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "ES2020", - "lib": ["DOM", "ES2020"], + "target": "ESNext", + "lib": ["DOM", "ESNext"], "module": "ESNext", "jsx": "react-jsx", "noEmit": true, @@ -13,7 +13,7 @@ "useDefineForClassFields": true, "allowImportingTsExtensions": true }, - "include": ["docs", "theme", "rspress.config.ts"], + "include": ["docs", "theme", "rspress.config.ts", "../../packages/core/src"], "mdx": { "checkMdx": true } diff --git a/examples/ssr-html/package.json b/examples/ssr-html/package.json index bb7bb2a39..206533d36 100644 --- a/examples/ssr-html/package.json +++ b/examples/ssr-html/package.json @@ -6,22 +6,20 @@ "private": true, "scripts": { "dev": "gez dev", - "build": "npm run build:ssr && npm run build:dts && npm run release", + "build": "npm run build:dts && npm run build:ssr", "build:ssr": "gez build", - "build:dts": "tsc --declaration --emitDeclarationOnly --outDir dist/server/", - "release": "gez release", "preview": "gez preview", "start": "gez start", - "postinstall": "gez install" + "build:dts": "tsc --declaration --emitDeclarationOnly --outDir dist/types" }, "author": "", "license": "MIT", "description": "", "dependencies": { - "@gez/core": "workspace:3.0.0-alpha.5" + "@gez/core": "workspace:*" }, "devDependencies": { - "@gez/rspack": "workspace:3.0.0-alpha.5", + "@gez/rspack": "workspace:*", "@types/node": "22.8.6", "typescript": "^5.2.2" } diff --git a/examples/ssr-html/src/entry.node.ts b/examples/ssr-html/src/entry.node.ts index 6aa77a345..2a8df5dc1 100644 --- a/examples/ssr-html/src/entry.node.ts +++ b/examples/ssr-html/src/entry.node.ts @@ -1,11 +1,10 @@ import http from 'node:http'; -import path from 'node:path'; import type { GezOptions } from '@gez/core'; -import { name } from '../package.json'; export default { - // 设置应用的唯一名字,如果有多个项目,则名字不能重复 - name, + packs: { + enable: true + }, // 本地执行 dev 和 build 时会使用 async createDevApp(gez) { // 这里应使用动态模块。生产依赖不存在。 @@ -42,14 +41,10 @@ export default { for (const url of list) { const rc = await gez.render({ base: '/gez', - params: { url: url, htmlBase: `/gez/${name}` } + params: { url: url, htmlBase: `/gez/${gez.name}` } }); gez.writeSync( - path.resolve( - gez.getProjectPath('dist/client'), - url.substring(1), - 'index.html' - ), + gez.resolvePath('dist/client', url.substring(1), 'index.html'), rc.html ); } diff --git a/examples/ssr-html/tsconfig.json b/examples/ssr-html/tsconfig.json index f802357e0..ccc689e08 100644 --- a/examples/ssr-html/tsconfig.json +++ b/examples/ssr-html/tsconfig.json @@ -8,8 +8,8 @@ ], "target": "ESNext", "module": "ESNext", - "strict": true, "moduleResolution": "node", + "strict": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "paths": { diff --git a/examples/ssr-preact-htm/package.json b/examples/ssr-preact-htm/package.json index c2e2ff982..ddde2aefc 100644 --- a/examples/ssr-preact-htm/package.json +++ b/examples/ssr-preact-htm/package.json @@ -6,22 +6,20 @@ "private": true, "scripts": { "dev": "gez dev", - "build": "npm run build:ssr && npm run build:dts && npm run release", + "build": "npm run build:dts && npm run build:ssr", "build:ssr": "gez build", - "build:dts": "tsc --declaration --emitDeclarationOnly --outDir dist/server/", - "release": "gez release", "preview": "gez preview", "start": "gez start", - "postinstall": "gez install" + "build:dts": "tsc --declaration --emitDeclarationOnly --outDir dist/src" }, "author": "", "license": "MIT", "description": "", "dependencies": { - "@gez/core": "workspace:3.0.0-alpha.5" + "@gez/core": "workspace:*" }, "devDependencies": { - "@gez/rspack": "workspace:3.0.0-alpha.5", + "@gez/rspack": "workspace:*", "@types/node": "22.8.6", "htm": "^3.1.1", "preact": "^10.24.3", diff --git a/examples/ssr-preact-htm/src/entry.node.ts b/examples/ssr-preact-htm/src/entry.node.ts index 95433410b..dc5bb7cdd 100644 --- a/examples/ssr-preact-htm/src/entry.node.ts +++ b/examples/ssr-preact-htm/src/entry.node.ts @@ -1,11 +1,8 @@ import http from 'node:http'; import path from 'node:path'; import type { GezOptions } from '@gez/core'; -import { name } from '../package.json'; export default { - // 设置应用的唯一名字,如果有多个项目,则名字不能重复 - name, // 本地执行 dev 和 build 时会使用 async createDevApp(gez) { // 这里应使用动态模块。生产依赖不存在。 @@ -43,7 +40,7 @@ export default { params: { url: '/' } }); gez.writeSync( - path.resolve(gez.getProjectPath('dist/client'), 'index.html'), + gez.resolvePath('dist/client', 'index.html'), render.html ); } diff --git a/examples/ssr-preact-htm/tsconfig.json b/examples/ssr-preact-htm/tsconfig.json index e82278d58..2d862ee7b 100644 --- a/examples/ssr-preact-htm/tsconfig.json +++ b/examples/ssr-preact-htm/tsconfig.json @@ -8,8 +8,8 @@ ], "target": "ESNext", "module": "ESNext", - "strict": true, "moduleResolution": "node", + "strict": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "paths": { diff --git a/examples/ssr-vue2-host/package.json b/examples/ssr-vue2-host/package.json index dbfe101cb..320a34b8c 100644 --- a/examples/ssr-vue2-host/package.json +++ b/examples/ssr-vue2-host/package.json @@ -6,21 +6,20 @@ "license": "MIT", "scripts": { "dev": "gez dev", - "build": "npm run build:ssr && npm run build:dts && npm run release", + "build": "npm run build:dts && npm run build:ssr", "build:ssr": "gez build", - "build:dts": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/server/", - "release": "gez release", "preview": "gez preview", "start": "gez start", - "postinstall": "gez install" - }, + "build:dts": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/src" + }, "dependencies": { - "@gez/core": "workspace:^", + "@gez/core": "workspace:*", "express": "^4.19.2" }, "devDependencies": { - "@gez/rspack": "workspace:^", - "@gez/rspack-vue": "workspace:^", + "@types/ssr-vue2-remote": "workspace:ssr-vue2-remote@^", + "@gez/rspack": "workspace:*", + "@gez/rspack-vue": "workspace:*", "@types/express": "^4.17.21", "@types/node": "^20.6.3", "less": "^4.2.0", diff --git a/examples/ssr-vue2-host/src/entry.node.ts b/examples/ssr-vue2-host/src/entry.node.ts index 65c8100e7..55fc12293 100644 --- a/examples/ssr-vue2-host/src/entry.node.ts +++ b/examples/ssr-vue2-host/src/entry.node.ts @@ -1,10 +1,7 @@ -import path from 'node:path'; import type { GezOptions } from '@gez/core'; import express from 'express'; -import { name } from '../package.json'; export default { - name, async createDevApp(gez) { return import('@gez/rspack-vue').then((m) => m.createRspackVue2App(gez) @@ -38,7 +35,7 @@ export default { params: { url: '/' } }); gez.writeSync( - path.resolve(gez.getProjectPath('dist/client'), 'index.html'), + gez.resolvePath('dist/client', 'index.html'), render.html ); } diff --git a/examples/ssr-vue2-host/tsconfig.json b/examples/ssr-vue2-host/tsconfig.json index 6a290658a..4e40ec32d 100644 --- a/examples/ssr-vue2-host/tsconfig.json +++ b/examples/ssr-vue2-host/tsconfig.json @@ -8,8 +8,8 @@ ], "target": "ESNext", "module": "ESNext", - "strict": true, "moduleResolution": "node", + "strict": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "paths": { diff --git a/examples/ssr-vue2-remote/package.json b/examples/ssr-vue2-remote/package.json index 1dd295a26..d65c8ddc3 100644 --- a/examples/ssr-vue2-remote/package.json +++ b/examples/ssr-vue2-remote/package.json @@ -1,26 +1,24 @@ { "name": "ssr-vue2-remote", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "private": true, "type": "module", "scripts": { "dev": "gez dev", - "build": "npm run build:ssr && npm run build:dts && npm run release", + "build": "npm run build:dts && npm run build:ssr", "build:ssr": "gez build", - "build:dts": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/server/", - "release": "gez release", "preview": "gez preview", "start": "gez start", - "postinstall": "gez install" + "build:dts": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/src" }, "license": "MIT", "dependencies": { - "@gez/core": "workspace:^", + "@gez/core": "workspace:*", "express": "^4.19.2" }, "devDependencies": { - "@gez/rspack": "workspace:^", - "@gez/rspack-vue": "workspace:^", + "@gez/rspack": "workspace:*", + "@gez/rspack-vue": "workspace:*", "@types/express": "^4.17.21", "@types/node": "^20.6.3", "less": "^4.2.0", @@ -29,4 +27,4 @@ "vue-server-renderer": "2.7.16", "vue-tsc": "^2.1.6" } -} +} \ No newline at end of file diff --git a/examples/ssr-vue2-remote/src/create-app.ts b/examples/ssr-vue2-remote/src/create-app.ts index 1f1014df3..db76f4cee 100644 --- a/examples/ssr-vue2-remote/src/create-app.ts +++ b/examples/ssr-vue2-remote/src/create-app.ts @@ -1,4 +1,4 @@ -import './style/global.less'; +import './styles/global.less'; import Vue, { defineAsyncComponent } from 'vue'; const App = defineAsyncComponent(() => import('./app.vue')); diff --git a/examples/ssr-vue2-remote/src/entry.node.ts b/examples/ssr-vue2-remote/src/entry.node.ts index 8a8a8eff0..567bcd3ed 100644 --- a/examples/ssr-vue2-remote/src/entry.node.ts +++ b/examples/ssr-vue2-remote/src/entry.node.ts @@ -1,10 +1,8 @@ import path from 'node:path'; import type { GezOptions } from '@gez/core'; import express from 'express'; -import { name } from '../package.json'; export default { - name, async createDevApp(gez) { return import('@gez/rspack-vue').then((m) => m.createRspackVue2App(gez) @@ -36,7 +34,7 @@ export default { params: { url: '/' } }); gez.writeSync( - path.resolve(gez.getProjectPath('dist/client'), 'index.html'), + gez.resolvePath('dist/client', 'index.html'), render.html ); } diff --git a/examples/ssr-vue2-remote/src/style/global.less b/examples/ssr-vue2-remote/src/styles/global.less similarity index 100% rename from examples/ssr-vue2-remote/src/style/global.less rename to examples/ssr-vue2-remote/src/styles/global.less diff --git a/examples/ssr-vue2-remote/tsconfig.json b/examples/ssr-vue2-remote/tsconfig.json index 638294de4..9a34617a0 100644 --- a/examples/ssr-vue2-remote/tsconfig.json +++ b/examples/ssr-vue2-remote/tsconfig.json @@ -8,8 +8,8 @@ ], "target": "ESNext", "module": "ESNext", - "strict": true, "moduleResolution": "node", + "strict": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "paths": { diff --git a/examples/ssr-vue3/package.json b/examples/ssr-vue3/package.json index 03ec85b8c..67051eade 100644 --- a/examples/ssr-vue3/package.json +++ b/examples/ssr-vue3/package.json @@ -6,21 +6,19 @@ "license": "MIT", "scripts": { "dev": "gez dev", - "build": "npm run build:ssr && npm run build:dts && npm run release", + "build": "npm run build:dts && npm run build:ssr", "build:ssr": "gez build", - "build:dts": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/server/", - "release": "gez release", "preview": "gez preview", "start": "gez start", - "postinstall": "gez install" - }, + "build:dts": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/src" + }, "dependencies": { - "@gez/core": "workspace:^", + "@gez/core": "workspace:*", "express": "^4.19.2" }, "devDependencies": { - "@gez/rspack": "workspace:^", - "@gez/rspack-vue": "workspace:^", + "@gez/rspack": "workspace:*", + "@gez/rspack-vue": "workspace:*", "@types/express": "^4.17.21", "@types/node": "^20.6.3", "less": "^4.2.0", diff --git a/examples/ssr-vue3/src/entry.node.ts b/examples/ssr-vue3/src/entry.node.ts index 68574ad30..50a2a0682 100644 --- a/examples/ssr-vue3/src/entry.node.ts +++ b/examples/ssr-vue3/src/entry.node.ts @@ -1,10 +1,7 @@ -import path from 'node:path'; import type { GezOptions } from '@gez/core'; import express from 'express'; -import { name } from '../package.json'; export default { - name, async createDevApp(gez) { return import('@gez/rspack-vue').then((m) => m.createRspackVue3App(gez) @@ -32,7 +29,7 @@ export default { params: { url: '/' } }); gez.writeSync( - path.resolve(gez.getProjectPath('dist/client'), 'index.html'), + gez.resolvePath('dist/client', 'index.html'), render.html ); } diff --git a/examples/ssr-vue3/tsconfig.json b/examples/ssr-vue3/tsconfig.json index 7d5090f44..56dd408a4 100644 --- a/examples/ssr-vue3/tsconfig.json +++ b/examples/ssr-vue3/tsconfig.json @@ -8,8 +8,8 @@ ], "target": "ESNext", "module": "ESNext", - "strict": true, "moduleResolution": "node", + "strict": true, "skipLibCheck": true, "allowSyntheticDefaultImports": true, "paths": { diff --git a/gen.task.json b/gen.task.json index ecc5926ff..fb4a21d53 100644 --- a/gen.task.json +++ b/gen.task.json @@ -1,9 +1,11 @@ { "packages": [ - "packages/lint", "packages/class-state", "packages/core", + "packages/import", + "packages/lint", "packages/rspack", - "packages/rspack-vue2" + "packages/rspack-module-link-plugin", + "packages/rspack-vue" ] } diff --git a/package.json b/package.json index 27c9c403b..ee9f9ef01 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,6 @@ "release": "npm run build:packages && lerna publish --force-publish --exact", "deploy": "npm run build && ./deploy.sh" }, - "engines": { - "lerna": "~8.1.3", - "node": "~20.13.1", - "pnpm": "~8.15.8" - }, "devDependencies": { "lint-staged": "15.2.4", "husky": "8.0.3", @@ -31,5 +26,10 @@ ], "publishConfig": { "registry": "https://registry.npmjs.org" + }, + "engines": { + "lerna": ">=8.1.9", + "node": ">=22.9.0", + "pnpm": ">=9.13.2" } } diff --git a/packages/class-state/package.json b/packages/class-state/package.json index cd855ca1c..4e9c02e69 100644 --- a/packages/class-state/package.json +++ b/packages/class-state/package.json @@ -15,12 +15,12 @@ }, "devDependencies": { "@biomejs/biome": "1.9.2", - "@gez/lint": "3.0.0-alpha.5", - "@types/node": "20.12.12", + "@gez/lint": "0.0.9", + "@types/node": "22.9.0", "@vitest/coverage-v8": "1.6.0", "@vue/compiler-dom": "^3.5.6", - "stylelint": "16.5.0", - "typescript": "5.4.5", + "stylelint": "16.10.0", + "typescript": "5.6.3", "unbuild": "2.0.0", "vitest": "1.6.0", "vue": "^3.5.6" @@ -42,7 +42,8 @@ "src", "dist", "*.mjs", - "template" + "template", + "public" ], "gitHead": "a1175657723ffc3c0979cabe8ce4978268618270" } diff --git a/packages/core/cli.mjs b/packages/core/cli.mjs deleted file mode 100755 index c6a6bbf4a..000000000 --- a/packages/core/cli.mjs +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env node --experimental-vm-modules --experimental-import-meta-resolve -import { cli } from './dist/cli/index.mjs'; - -cli(); diff --git a/packages/core/package.json b/packages/core/package.json index cecbfc5f0..bed45b403 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,7 +10,7 @@ ], "license": "MIT", "bin": { - "gez": "./cli.mjs" + "gez": "./dist/cli/index.mjs" }, "engines": { "node": ">=20.13" @@ -24,25 +24,24 @@ "build": "unbuild" }, "dependencies": { - "@types/serialize-javascript": "^5.0.4", - "fflate": "^0.8.2", + "@gez/import": "workspace:*", "find": "^0.3.0", "send": "^1.1.0", + "@types/serialize-javascript": "^5.0.4", "serialize-javascript": "^6.0.2", - "symlink-dir": "^6.0.2", - "tsx": "4.19.1", "write": "^2.0.0" }, "devDependencies": { + "@types/tar-fs": "^2.0.4", "@biomejs/biome": "1.9.2", "@gez/lint": "0.0.9", "@types/find": "^0.2.4", - "@types/node": "20.12.12", + "@types/node": "22.9.0", "@types/send": "^0.17.4", "@types/write": "^2.0.4", "@vitest/coverage-v8": "1.6.0", - "stylelint": "16.5.0", - "typescript": "5.4.5", + "stylelint": "16.10.0", + "typescript": "5.6.3", "unbuild": "2.0.0", "vitest": "1.6.0" }, @@ -54,6 +53,11 @@ "import": "./dist/index.mjs", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" + }, + "./cli": { + "import": "./dist/cli/index.mjs", + "require": "./dist/cli/index.cjs", + "types": "./dist/cli/index.d.ts" } }, "main": "./dist/index.cjs", diff --git a/packages/core/src/cli/cli.ts b/packages/core/src/cli/cli.ts index 375623c6b..2b9e8277c 100644 --- a/packages/core/src/cli/cli.ts +++ b/packages/core/src/cli/cli.ts @@ -1,19 +1,15 @@ import path from 'node:path'; -import { COMMAND, Gez, type GezOptions, getProjectPath } from '../core'; +import { COMMAND, Gez, type GezOptions } from '../core/gez'; +import { resolvePath } from '../core/resolve-path'; export function cli() { const command = process.argv.slice(2)[0] || ''; switch (command) { - case COMMAND.install: - process.env.NODE_ENV = 'production'; - runDevApp(command); - break; case COMMAND.dev: runDevApp(command); break; case COMMAND.build: - case COMMAND.release: case COMMAND.preview: process.env.NODE_ENV = 'production'; runDevApp(command); @@ -29,9 +25,7 @@ export function cli() { } async function tsImport(file: string): Promise> { - // @ts-ignore - const result = await import('tsx/esm/api'); - return result.tsImport(path.resolve(file), import.meta.url); + return import(path.resolve(file)); } async function runDevApp(command: COMMAND) { @@ -46,10 +40,6 @@ async function runDevApp(command: COMMAND) { } }; switch (command) { - case COMMAND.install: - exit(await gez.install()); - exit(await gez.destroy()); - break; case COMMAND.dev: options?.createServer?.(gez); break; @@ -58,10 +48,6 @@ async function runDevApp(command: COMMAND) { exit(await gez.destroy()); exit(await postCompileProdHook(gez)); break; - case COMMAND.release: - exit(await gez.release()); - exit(await gez.destroy()); - break; case COMMAND.preview: exit(await gez.build()); exit(await gez.destroy()); @@ -76,7 +62,7 @@ async function runProdApp() { } async function getProdGez(): Promise { - const file = getProjectPath(path.resolve(), 'dist/node/entry.js'); + const file = resolvePath(path.resolve(), 'dist/node/src/entry.node.js'); return import(file).then(async (module) => { const options: GezOptions = module.default || {}; diff --git a/packages/core/src/cli/index.ts b/packages/core/src/cli/index.ts index bf3b7e5f5..22bccdd87 100644 --- a/packages/core/src/cli/index.ts +++ b/packages/core/src/cli/index.ts @@ -1 +1,5 @@ -export * from './cli'; +#!/usr/bin/env node --experimental-vm-modules --experimental-import-meta-resolve --experimental-strip-types + +import { cli } from './cli'; + +cli(); diff --git a/packages/core/src/core/app.ts b/packages/core/src/core/app.ts index d669a42fc..c403e5c6d 100644 --- a/packages/core/src/core/app.ts +++ b/packages/core/src/core/app.ts @@ -1,3 +1,5 @@ +import { pathToFileURL } from 'node:url'; +import { createLoaderImport } from '@gez/import'; import type { Gez } from './gez'; import { type Middleware, createMiddleware } from './middleware'; import { @@ -5,7 +7,6 @@ import { type RenderContextOptions, type ServerRenderHandle } from './render-context'; -import { compression, decompression } from './version'; export interface App { /** @@ -21,47 +22,42 @@ export interface App { /** * 执行构建 */ - build: () => Promise; - /** - * 生成远程的压缩包 - */ - release: () => Promise; + build?: () => Promise; /** * 销毁实例,释放内存 */ - destroy: () => Promise; - /** - * 安装依赖执行命令 - */ - install: () => Promise; + destroy?: () => Promise; } export async function createApp(gez: Gez): Promise { + const render = + gez.command === gez.COMMAND.start + ? createStartRender(gez) + : createErrorRender(gez); return { middleware: createMiddleware(gez), - async render(options?: RenderContextOptions) { - const rc = new RenderContext(gez, options); - const result = await import( - gez.getProjectPath('dist/server/entry.js') - ); - const serverRender: ServerRenderHandle = result[rc.entryName]; - if (typeof serverRender === 'function') { - await serverRender(rc); - } + render + }; +} + +function createStartRender(gez: Gez) { + const baseURL = pathToFileURL(gez.root) as URL; + const importMap = gez.getServerImportMap(); + const loaderImport = createLoaderImport(baseURL, importMap); - return rc; - }, - async build() { - return true; - }, - async release() { - return compression(gez); - }, - async destroy() { - return true; - }, - async install() { - return decompression(gez, 0); + return async (options?: RenderContextOptions): Promise => { + const rc = new RenderContext(gez, options); + const result = await loaderImport(`${gez.name}/src/entry.server`); + const serverRender: ServerRenderHandle = result[rc.entryName]; + if (typeof serverRender === 'function') { + await serverRender(rc); } + return rc; + }; +} + +function createErrorRender(gez: Gez) { + return (options?: RenderContextOptions) => { + throw new Error(`Custom rendering function not implemented`); }; } diff --git a/packages/core/src/core/gez.ts b/packages/core/src/core/gez.ts index 8f79a9d92..a81d4b24a 100644 --- a/packages/core/src/core/gez.ts +++ b/packages/core/src/core/gez.ts @@ -1,121 +1,101 @@ +import fs from 'node:fs'; import path from 'node:path'; import { cwd } from 'node:process'; +import type { ImportMap } from '@gez/import'; import write from 'write'; import { type App, createApp } from './app'; +import type { ManifestJson } from './manifest-json'; import { type ModuleConfig, type ParsedModuleConfig, parseModuleConfig } from './module-config'; -import { moduleLink } from './module-link'; -import { type ProjectPath, getProjectPath } from './project-path'; - +import { + type PackConfig, + type ParsedPackConfig, + parsePackConfig +} from './pack-config'; +import { type ProjectPath, resolvePath } from './resolve-path'; /** * 详细说明,请看文档:https://dp-os.github.io/gez/api/gez.html */ export interface GezOptions { - name?: string; - root?: string; - isProd?: boolean; - isInstall?: boolean; - basePathPlaceholder?: string | false; - modules?: ModuleConfig; - postCompileProdHook?: (gez: Gez) => Promise; - createDevApp?: (gez: Gez) => Promise; - createServer?: (gez: Gez) => Promise; -} - -export enum COMMAND { - dev = 'dev', - build = 'build', - release = 'release', - preview = 'preview', - install = 'install', - start = 'start' -} - -export interface PackageJsonChunks { - /** - * 当前编译的 JS 文件。 - */ - js: string; - /** - * 当前编译的 CSS 文件。 - */ - css: string[]; - /** - * 其它的资源文件。 - */ - resources: string[]; /** - * 构建产物的大小。 + * 项目根目录,默认为当前执行命令的目录。 */ - sizes: PackageJsonChunkSizes; -} - -export interface PackageJsonChunkSizes { - js: number; - css: number; - resource: number; -} - -export interface PackageJson { + root?: string; /** - * 服务名字,来自于:GezOptions.name + * 是否是生产环境。 */ - name: string; + isProd?: boolean; /** - * 版本号,默认为 1.0.0 + * 动态路径的变量占位符。 */ - version: string; + basePathPlaceholder?: string | false; /** - * 构建的版本号 + * 模块链接配置。 */ - hash: string; + modules?: ModuleConfig; /** - * 模块系统 + * 是否启用归档,等同于 npm pack。 */ - type: 'module'; + packs?: PackConfig; /** - * 对外导出的文件 + * 创建开发应用,在执行 dev、build、preview 命令时调用。 */ - exports: Record; + createDevApp?: (gez: Gez) => Promise; /** - * 构建的全部文件清单 + * 创建服务器,执行 dev、build、preview 命令时调用。 */ - files: string[]; + createServer?: (gez: Gez) => Promise; /** - * 编译的文件信息 - * 类型:Record<源文件, 编译信息> + * gez build 构建完成后,以生产模式执行的钩子。 */ - chunks: Record; + postCompileProdHook?: (gez: Gez) => Promise; } + +export enum COMMAND { + dev = 'dev', + build = 'build', + preview = 'preview', + start = 'start' +} + function noon(gez: Gez) {} export class Gez { private readonly _options: GezOptions; private _app: App | null = null; private _command: COMMAND | null = null; + /** + * 根据传入的 modules 选项解析出来的对象。 + */ readonly moduleConfig: ParsedModuleConfig; + readonly packConfig: ParsedPackConfig; public constructor(options: GezOptions = {}) { this._options = options; - this.moduleConfig = parseModuleConfig( - this.name, - this.root, - options.modules - ); + const name = this.readJsonSync( + path.resolve(this.root, 'package.json') + ).name; + this.moduleConfig = parseModuleConfig(name, this.root, options.modules); + this.packConfig = parsePackConfig(options.packs); } /** - * 服务的名称 + * 服务名称,来源于 package.json 文件的 name 字段。 */ public get name() { - return this._options.name ?? 'gez'; + return this.moduleConfig.name; } - /** - * 本地开发根目录 + * 根据 name 生成的 JS 变量名称。 + */ + public get varName() { + return '__' + this.name.replace(/[^a-zA-Z]/g, '_') + '__'; + } + /** + * 项目根目录。 */ public get root(): string { const { root = cwd() } = this._options; @@ -125,24 +105,14 @@ export class Gez { return path.resolve(cwd(), root); } /** - * 是否是生产环境 + * 是否是生产环境。 */ public get isProd(): boolean { return this._options?.isProd ?? process.env.NODE_ENV === 'production'; } - /** - * 是否安装生产依赖 - */ - get isInstall() { - return ( - this._options?.isInstall ?? - process.env.npm_config_production !== 'true' - ); - } /** - * 静态资源请求目录 - * 例如:/gez/ + * 根据服务名称生成的静态资源基本路径。 */ public get basePath() { return `/${this.name}/`; @@ -159,7 +129,7 @@ export class Gez { } /** - * 当前程序执行的命令 + * 当前执行的命令。 */ public get command(): COMMAND { const { _command } = this; @@ -168,6 +138,12 @@ export class Gez { } throw new Error(`'command' does not exist`); } + /** + * 全部命令的枚举对象。 + */ + public get COMMAND() { + return COMMAND; + } private get app() { const { _app } = this; @@ -184,91 +160,123 @@ export class Gez { } /** - * 安装代码方法,对 npm install 的补充 - * 目前用于远程模块的安装(包括类型文件) + * 初始化实例。 */ - public install(): Promise { - return this.app.install(); - } + public async init(command: COMMAND) { + if (this._command) { + throw new Error('Cannot be initialized repeatedly'); + } + const createDevApp = this._options.createDevApp || defaultCreateDevApp; + this._command = command; + const app: App = + // 只有 dev 和 build 时使用createDevApp + [COMMAND.dev, COMMAND.build].includes(command) + ? await createDevApp(this) + : await createApp(this); + this._app = app; + } + /** + * 销毁实例,释放内存。 + */ + public async destroy(): Promise { + const { _app } = this; + if (_app?.destroy) { + return _app.destroy(); + } + return true; + } /** - * 构建应用代码 + * 构建生产代码。 */ public async build(): Promise { const startTime = Date.now(); console.log('[gez]: build start'); - const successful = await this.app.build(); + const successful = await this.app?.build?.(); const endTime = Date.now(); console.log(`[gez]: build end, cost: ${endTime - startTime}ms`); - return successful; - } - - /** - * 生成应用代码压缩包 - */ - public release(): Promise { - return this.app.release(); + return successful ?? true; } - /** - * 静态资源中间件 + * 中间件。 */ public get middleware() { return this.app.middleware; } - /** - * 渲染函数 + * 调用 entry.server.ts 导出的渲染函数。 */ public get render() { return this.app.render; } - /** - * 销毁实例,释放内存 + * 解析项目路径。 */ - public async destroy(): Promise { - const { _app } = this; - if (_app) { - return _app.destroy(); - } - return true; + public resolvePath(projectPath: ProjectPath, ...args: string[]): string { + return resolvePath(this.root, projectPath, ...args); } - /** - * 当前服务,生成一个全局唯一的变量名称 + * 同步写入一个文件。 */ - public get varName() { - return '__' + this.name.replace(/[^a-zA-Z]/g, '_') + '__'; - } - - public async init(command: COMMAND) { - if (this._command) { - throw new Error('Cannot be initialized repeatedly'); - } - moduleLink(this.root, this.moduleConfig); - const createDevApp = this._options.createDevApp || defaultCreateDevApp; - - this._command = command; - const app: App = - // 只有 dev 和 build 时使用createDevApp - [COMMAND.dev, COMMAND.build].includes(command) - ? await createDevApp(this) - : await createApp(this); - this._app = app; - } - - public getProjectPath(projectPath: ProjectPath): string { - return getProjectPath(this.root, projectPath); - } public writeSync(filepath: string, data: any) { write.sync(filepath, data); } - public async write(filepath: string, data: any) { - await write(filepath, data); + /** + * 异步的读取一个 JSON 文件。 + */ + public readJsonSync(filename: string): any { + return JSON.parse(fs.readFileSync(filename, 'utf-8')); + } + /** + * 获取全部服务的清单文件。 + */ + public getManifestList(target: 'client' | 'server'): ManifestJson[] { + return this.moduleConfig.imports.map((item) => { + const filename = path.resolve( + item.localPath, + target, + 'manifest.json' + ); + try { + const text = fs.readFileSync(filename, 'utf-8'); + const data = JSON.parse(text); + data.name = item.name; + return data; + } catch (e) { + throw new Error( + `'${item.name}' service '${target}/manifest.json' file read error` + ); + } + }); + } + /** + * 获取服务端的 importmap 映射文件。 + */ + public getServerImportMap(): ImportMap { + const imports: Record = {}; + this.getManifestList('server').forEach((manifest) => { + const importItem = this.moduleConfig.imports.find((item) => { + return item.name === manifest.name; + }); + if (!importItem) { + throw new Error( + `'${manifest.name}' service did not find module config` + ); + } + Object.entries(manifest.exports).forEach(([name, value]) => { + imports[`${manifest.name}/${name.substring(2)}`] = path.resolve( + importItem.localPath, + 'server', + value + ); + }); + }); + return { + imports + }; } } diff --git a/packages/core/src/core/index.ts b/packages/core/src/core/index.ts index 19af95aa0..566fd0913 100644 --- a/packages/core/src/core/index.ts +++ b/packages/core/src/core/index.ts @@ -1,7 +1,29 @@ -export * from './gez'; -export * from './app'; -export * from './project-path'; -export * from './module-config'; -export * from './render-context'; -export * from './middleware'; -export * from './build-target'; +export { type GezOptions, Gez } from './gez'; +export { + type PathType, + type ModuleConfig, + type ParsedModuleConfig, + parseModuleConfig +} from './module-config'; +export { + type PackConfig, + type ParsedPackConfig, + parsePackConfig +} from './pack-config'; +export { type App, createApp } from './app'; +export { + type RenderContextOptions, + RenderContext, + type ServerRenderHandle, + type RenderFiles +} from './render-context'; +export { + type Middleware, + createMiddleware, + mergeMiddlewares +} from './middleware'; +export { + type ManifestJson, + type ManifestJsonChunkSizes, + type ManifestJsonChunks +} from './manifest-json'; diff --git a/packages/core/src/core/manifest-json.ts b/packages/core/src/core/manifest-json.ts new file mode 100644 index 000000000..c70f68818 --- /dev/null +++ b/packages/core/src/core/manifest-json.ts @@ -0,0 +1,56 @@ +export interface ManifestJsonChunks { + /** + * 当前编译的 JS 文件。 + */ + js: string; + /** + * 当前编译的 CSS 文件。 + */ + css: string[]; + /** + * 其它的资源文件。 + */ + resources: string[]; + /** + * 构建产物的大小。 + */ + sizes: ManifestJsonChunkSizes; +} + +export interface ManifestJsonChunkSizes { + js: number; + css: number; + resource: number; +} + +export interface ManifestJson { + /** + * 服务名字,来自于:GezOptions.name + */ + name: string; + /** + * 版本号,默认为 1.0.0 + */ + version: string; + /** + * 构建的版本号 + */ + hash: string; + /** + * 模块系统 + */ + type: 'module'; + /** + * 对外导出的文件 + */ + exports: Record; + /** + * 构建的全部文件清单 + */ + buildFiles: string[]; + /** + * 编译的文件信息 + * 类型:Record<源文件, 编译信息> + */ + chunks: Record; +} diff --git a/packages/core/src/core/module-config.ts b/packages/core/src/core/module-config.ts index bce0bb763..a0cd27cec 100644 --- a/packages/core/src/core/module-config.ts +++ b/packages/core/src/core/module-config.ts @@ -20,7 +20,7 @@ export interface ModuleConfig { /** * 导入的模块基本配置 */ - imports?: Record; + imports?: Record; /** * 设置项目的外部依赖 * 例如: @@ -89,11 +89,6 @@ export interface ParsedModuleConfig { * 用于读取依赖 和 存放远程下载的依赖 */ localPath: string; - /** - * 远程路径 - * 用于下载远程依赖 - */ - remoteUrl?: string; }[]; /** * 外部依赖 @@ -156,24 +151,11 @@ export function parseModuleConfig( }; const _imports = config.imports; Object.keys(config.imports).forEach((key) => { - const value = _imports[key]; - if (typeof value === 'string') { - imports.push({ - name: key, - localPath: getLocalPath(value) - }); - } else if (Array.isArray(value)) { - try { - const url = new URL(value[1]); - imports.push({ - name: key, - localPath: getLocalPath(value[0]), - remoteUrl: url.href - }); - } catch { - throw new TypeError(`'${key}' 'remoteUrl' parsing failed`); - } - } + const value = _imports[key].trim(); + imports.push({ + name: key, + localPath: getLocalPath(value) + }); }); } diff --git a/packages/core/src/core/module-link.ts b/packages/core/src/core/module-link.ts deleted file mode 100644 index e3461e222..000000000 --- a/packages/core/src/core/module-link.ts +++ /dev/null @@ -1,57 +0,0 @@ -import path from 'node:path'; -import symlinkDir from 'symlink-dir'; -import type { ParsedModuleConfig } from './module-config'; - -/** - * 模块的联合 - * @param root 模块的目录 - * @param moduleConfig 模块的配置 - * todo 下载时检查 version,无更新不下载 - */ -export function moduleLink(root: string, moduleConfig: ParsedModuleConfig) { - const localPathList = [ - root, - ...moduleConfig.imports.map(({ localPath }) => localPath) - ]; - const maxSamePath = getMaxSamePath(localPathList); - - moduleConfig.imports.forEach(({ localPath, name }) => { - const sourceDir = path.resolve(localPath, 'server'); - const targetDir = path.resolve(maxSamePath, 'node_modules', name); - symlinkDir.default.sync(sourceDir, targetDir, { - overwrite: true - }); - }); -} - -/** - * 获取路径列表中的最大相同路径 - * @param pathList - 路径列表 - * @returns 最大相同路径 - */ -function getMaxSamePath(pathList: string[]): string { - let maxSamePath = ''; - if (pathList.length === 0) { - return maxSamePath; - } - const charList = pathList[0].split('/'); - for (let i = 0; i < charList.length; i++) { - const char = charList[i]; - const targetPath = maxSamePath + char; - const isSame = pathList.every((fullPath) => { - if ( - fullPath === targetPath || - fullPath.startsWith(`${targetPath}/`) - ) { - return true; - } - return false; - }); - if (isSame) { - maxSamePath = `${maxSamePath}${char}/`; - } else { - break; - } - } - return maxSamePath; -} diff --git a/packages/core/src/core/pack-config.ts b/packages/core/src/core/pack-config.ts new file mode 100644 index 000000000..adfc00bf2 --- /dev/null +++ b/packages/core/src/core/pack-config.ts @@ -0,0 +1,84 @@ +import type { Gez } from './gez'; + +export interface PackConfig { + /** + * 是否启用归档 + */ + enable?: boolean; + /** + * 输出的文件 + */ + outputs?: string | string[] | boolean; + /** + * 发布的类型 + * 环境变量设置:process.env.RELEASE_TYPE + */ + releaseType?: + | 'major' + | 'premajor' + | 'minor' + | 'preminor' + | 'patch' + | 'prepatch' + | 'prerelease'; + packageJson?: ( + gez: Gez, + pkgJson: Record + ) => Promise>; + onBefore?: (gez: Gez, pkgJson: Record) => Promise; + onAfter?: ( + gez: Gez, + pkgJson: Record, + file: Buffer + ) => Promise; +} +export interface ParsedPackConfig { + enable: boolean; + outputs: string[]; + packageJson: ( + gez: Gez, + pkgJson: Record + ) => Promise>; + onBefore: (gez: Gez, pkgJson: Record) => Promise; + onAfter: ( + gez: Gez, + pkgJson: Record, + file: Buffer + ) => Promise; +} + +export function parsePackConfig(config: PackConfig = {}): ParsedPackConfig { + const outputs: string[] = []; + if (typeof config.outputs === 'string') { + outputs.push(config.outputs); + } else if (Array.isArray(config.outputs)) { + outputs.push(...config.outputs); + } else if (config.outputs !== false) { + outputs.push('dist/client/versions/latest.tgz'); + } + return { + enable: config.enable ?? false, + outputs, + async packageJson(gez, pkgJson) { + const { dependencies } = pkgJson; + if (config.packageJson) { + pkgJson = await config.packageJson(gez, pkgJson); + } + if (dependencies) { + Object.keys(dependencies).forEach((name) => { + const value = dependencies[name]; + if (value && /^(workspace|file):/.test(value)) { + dependencies[name] = undefined; + } + }); + } + return pkgJson; + }, + async onBefore(gez, pkgJson: Record) { + await config.onBefore?.(gez, pkgJson); + }, + async onAfter(gez, pkgJson, file) { + await config.onAfter?.(gez, pkgJson, file); + } + }; +} diff --git a/packages/core/src/core/project-path.ts b/packages/core/src/core/project-path.ts deleted file mode 100644 index 8710f01ba..000000000 --- a/packages/core/src/core/project-path.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from 'node:path'; - -export type ProjectPath = - | 'dist' - | 'dist/client' - | 'dist/client/package.json' - | 'dist/client/versions' - | 'dist/client/versions/latest.json' - | 'dist/server' - | 'dist/server/package.json' - | 'dist/server/entry.js' - | 'dist/node' - | 'dist/node/entry.js' - | 'src' - | 'src/entry.node.ts' - | 'src/entry.client.ts' - | 'src/entry.server.ts'; - -export function getProjectPath(root: string, projectPath: ProjectPath): string { - return path.resolve(root, projectPath); -} diff --git a/packages/core/src/core/render-context.ts b/packages/core/src/core/render-context.ts index 8694fab34..f9cc387d7 100644 --- a/packages/core/src/core/render-context.ts +++ b/packages/core/src/core/render-context.ts @@ -1,22 +1,20 @@ -import fs from 'node:fs/promises'; -import path from 'node:path'; import serialize from 'serialize-javascript'; -import type { Gez, PackageJson } from './gez'; +import type { Gez } from './gez'; /** * 渲染的参数 */ export interface RenderContextOptions { /** - * 静态资产的 base 地址,默认为空 + * 静态资产的公共路径,可以根据业务的上下文来动态设置不同的路径。 */ base?: string; /** - * 调用 src/entry.server.ts 文件导出的哪个函数渲染,默认为 default。 + * gez.render() 函数执行时,会调用 entry.server.ts 文件导出的名称。 */ entryName?: string; /** - * 自定义请求的参数 + * 传递给 RenderContext 对象的 params 字段。 */ params?: Record; } @@ -25,34 +23,39 @@ export interface RenderContextOptions { * 渲染上下文 */ export class RenderContext { + /** + * Gez 的实例。 + */ public gez: Gez; /** - * 设置重定向的地址 + * 重定向地址。 */ public redirect: string | null = null; /** - * 设置状态码 + * 响应的状态码。 */ public status: number | null = null; private _html = ''; /** - * 静态资源的基本地址 + * 参数传入的 base。 */ public readonly base: string; /** - * 请求的参数 + * 参数传入的 params。 */ public readonly params: Record; /** - * 调用 src/entry.server.ts 文件导出的哪个函数渲染,默认为 default。 + * 参数传入的 entryName。 */ public readonly entryName: string; /** - * 收集渲染过程中执行模块的元信息 + * 服务端渲染过程中,收集模块执行过程中的 import.meta 对象。 */ public importMetaSet = new Set(); - + /** + * importMetaSet 收集完成后,调用 rc.commit() 函数时,会更新这个对象的信息。 + */ public files: RenderFiles = { js: [], css: [], @@ -66,6 +69,9 @@ export class RenderContext { this.params = options.params ?? {}; this.entryName = options.entryName ?? 'default'; } + /** + * 响应的 html 内容。 + */ public get html() { return this._html; } @@ -81,32 +87,18 @@ export class RenderContext { public serialize(input: any, options?: serialize.SerializeJSOptions) { return serialize(input, options); } + /** + * 在 window 对象,注入一个 JS 变量对象,data 必须是可以被序列化的。 + */ public state(varName: string, data: Record): string { return ``; } /** - * 获取全部的远程包信息 - */ - public async getPackagesJson(): Promise { - return Promise.all( - this.gez.moduleConfig.imports.map(async (item) => { - const file = path.resolve( - item.localPath, - 'client/package.json' - ); - const result = await fs.readFile(file, 'utf-8'); - const json = JSON.parse(result) as PackageJson; - json.name = item.name; - return json; - }) - ); - } - /** - * 当 imports 依赖收集完毕后,需要提交变更 + * 同构应用渲染完成后,提交模块依赖更新 files 对象。 */ public async commit() { - const packages = await this.getPackagesJson(); - const chunkSet = new Set([`${this.gez.name}@src/entry.ts`]); + const { gez } = this; + const chunkSet = new Set([`${gez.name}@src/entry.client.ts`]); for (const item of this.importMetaSet) { if ('chunkName' in item && typeof item.chunkName === 'string') { chunkSet.add(item.chunkName); @@ -129,7 +121,7 @@ export class RenderContext { cb(); }; - packages.forEach((item) => { + this.gez.getManifestList('client').forEach((item) => { const base = `${this.base}/${item.name}/`; files.importmap.push(`${base}importmap.${item.hash}.final.js`); Object.entries(item.chunks).forEach(([filepath, info]) => { @@ -153,6 +145,9 @@ export class RenderContext { files.js.push(...files.importmap, ...files.modulepreload); this.files = files; } + /** + * 根据 files 生成 JS 和 CSS 文件的预加载代码。 + */ public preload() { const css = this.files.css .map((url) => { @@ -166,11 +161,17 @@ export class RenderContext { .join(''); return css + js; } + /** + * 根据 files 生成服务端首屏加载的 CSS。 + */ public css() { return this.files.css .map((url) => ``) .join(''); } + /** + * 根据 files 生成 importmap 相关代码。 + */ public importmap() { return ( this.files.importmap @@ -179,9 +180,15 @@ export class RenderContext { `` ); } + /** + * 根据 files 生成模块入口执行代码。 + */ public moduleEntry() { - return ``; + return ``; } + /** + * 根据 files 生成 ESM 模块预加载代码。 + */ public modulePreload() { return this.files.modulepreload .map((url) => ``) @@ -190,7 +197,7 @@ export class RenderContext { } /** - * 服务渲染的处理函数 + * 服务端渲染处理函数。 */ export type ServerRenderHandle = (render: RenderContext) => Promise; @@ -198,9 +205,24 @@ export type ServerRenderHandle = (render: RenderContext) => Promise; * 当前页面渲染的文件 */ export interface RenderFiles { - js: string[]; + /** + * CSS 文件列表。 + */ css: string[]; + /** + * ESM 模块列表。 + */ modulepreload: string[]; + /** + * importmap.js 文件列表。 + */ importmap: string[]; + /** + * 全部的 JS 文件列表,包含 modulepreload 和 importmap。 + */ + js: string[]; + /** + * 除了 JS 和 CSS 之外的其它文件列表。 + */ resources: string[]; } diff --git a/packages/core/src/core/resolve-path.ts b/packages/core/src/core/resolve-path.ts new file mode 100644 index 000000000..222637ca9 --- /dev/null +++ b/packages/core/src/core/resolve-path.ts @@ -0,0 +1,27 @@ +import path from 'node:path'; + +export type ProjectPath = + | './' + | 'dist' + | 'dist/package.json' + | 'dist/client' + | 'dist/client/manifest.json' + | 'dist/client/versions' + | 'dist/client/versions/latest.json' + | 'dist/server' + | 'dist/server/manifest.json' + | 'dist/node' + | 'dist/node/src/entry.node.js' + | 'src' + | 'src/entry.node.ts' + | 'src/entry.client.ts' + | 'src/entry.server.ts' + | 'package.json'; + +export function resolvePath( + root: string, + projectPath: ProjectPath, + ...args: string[] +): string { + return path.resolve(root, projectPath, ...args); +} diff --git a/packages/core/src/core/version/compression.ts b/packages/core/src/core/version/compression.ts deleted file mode 100644 index 2f66e5980..000000000 --- a/packages/core/src/core/version/compression.ts +++ /dev/null @@ -1,50 +0,0 @@ -import crypto from 'node:crypto'; -import path from 'node:path'; -import write from 'write'; -import type { Gez } from '../gez'; -import { getPkgHash } from './pkg'; -import { compressionDir } from './zip'; - -/** - * 压缩 - * @param sourceDir 压缩目录 - * @param outputFilename 压缩的文件名 - */ -export function compression(gez: Gez): boolean { - const outputDir = path.resolve( - gez.getProjectPath('dist/client'), - 'versions' - ); - const list = ['client', 'server']; - const versionJson: Record = {}; - list.forEach((name) => { - const root = path.resolve(gez.getProjectPath('dist'), name); - const packageFile = path.resolve(root, 'package.json'); - const hash = getPkgHash(packageFile); - if (!hash) { - console.error(`'${root}' hash does not exist, compression failed`); - return false; - } - const filename = `${hash}.zip`; - compressionDir(root, path.resolve(outputDir, filename)); - versionJson[name] = hash; - }); - - const versionJsonText = JSON.stringify(versionJson, null, 4); - const writeJson = (version: string) => { - const filename = path.resolve(outputDir, `${version}.json`); - write.sync(path.resolve(gez.root, filename), versionJsonText); - }; - writeJson('latest'); - writeJson(contentHash(JSON.stringify(versionJsonText))); - console.log( - `Compression completed, See detail in '${path.relative(process.cwd(), outputDir)}'` - ); - return true; -} - -function contentHash(text: string) { - const hash = crypto.createHash('md5'); - hash.update(text); - return hash.digest('hex'); -} diff --git a/packages/core/src/core/version/decompression.ts b/packages/core/src/core/version/decompression.ts deleted file mode 100644 index 3fae32457..000000000 --- a/packages/core/src/core/version/decompression.ts +++ /dev/null @@ -1,79 +0,0 @@ -import path from 'node:path'; -import type { Gez } from '../gez'; -import { getFile, getJsonFile } from './fetch'; -import { getPkgHash } from './pkg'; -import { decompressionDir } from './zip'; - -export async function decompression( - gez: Gez, - maxRetryCount = 3 -): Promise { - let currentImports = gez.moduleConfig.imports.filter((item) => { - const remoteUrl = item.remoteUrl; - if (!remoteUrl) { - return false; - } - return true; - }); - let currentRetryCount = 0; - let tryImports: typeof currentImports = []; - const next = async () => { - const item = currentImports.shift(); - if (!item) { - return; - } - const url = new URL(item.remoteUrl!); - const versionJson = await getJsonFile>(url.href); - if (!versionJson) { - tryImports.push(item); - return next(); - } - const result = await Promise.all( - Object.entries(versionJson).map(async ([key, newHash]) => { - const root = path.resolve(item.localPath, key); - const oldHash = getPkgHash(path.resolve(root, 'package.json')); - // 判断和本地版本号是否一致 - if (newHash === oldHash) { - return true; - } - const zip = await getFile(getZipUrl(url, newHash)); - // 下载失败 - if (!zip) { - return !!oldHash; - } - decompressionDir(root, await zip.arrayBuffer()); - return true; - }) - ); - if (result.includes(false)) { - tryImports.push(item); - return next(); - } - return next(); - }; - const start = async (): Promise => { - await next(); - // 没有错误需要处理 - if (!tryImports.length) { - return true; - } - // 处理下一次的请求 - currentImports = tryImports; - tryImports = []; - currentRetryCount++; - if (currentRetryCount > maxRetryCount) { - return false; - } - await new Promise((resolve) => { - setTimeout(resolve, 1000 * 10); - }); - return start(); - }; - return start(); -} - -function getZipUrl(url: URL, hash: string) { - const arr = url.href.split('/'); - arr[arr.length - 1] = `${hash}.zip`; - return arr.join('/'); -} diff --git a/packages/core/src/core/version/fetch.ts b/packages/core/src/core/version/fetch.ts deleted file mode 100644 index cfbdd1272..000000000 --- a/packages/core/src/core/version/fetch.ts +++ /dev/null @@ -1,28 +0,0 @@ -export async function getFile(url: string): Promise { - try { - const result = await fetch(url); - if (!result.ok || result.status !== 200) { - console.log(`fetch error: ${url}`); - return null; - } - return result; - } catch (e) { - console.error(e); - return null; - } -} - -export async function getJsonFile>( - url: string -): Promise { - const result = await getFile(url); - if (result !== null) { - try { - return JSON.parse(await result.text()); - } catch { - console.log(`JSON parsing failed: ${url}`); - return null; - } - } - return null; -} diff --git a/packages/core/src/core/version/index.ts b/packages/core/src/core/version/index.ts deleted file mode 100644 index 37daa92a5..000000000 --- a/packages/core/src/core/version/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './compression'; -export * from './decompression'; diff --git a/packages/core/src/core/version/pkg.ts b/packages/core/src/core/version/pkg.ts deleted file mode 100644 index e81b822e5..000000000 --- a/packages/core/src/core/version/pkg.ts +++ /dev/null @@ -1,14 +0,0 @@ -import fs from 'node:fs'; - -import type { PackageJson } from '../gez'; - -export function getPkgHash(filename: string): string | null { - try { - const json: PackageJson = JSON.parse( - fs.readFileSync(filename, 'utf-8') - ); - return json.hash || null; - } catch { - return null; - } -} diff --git a/packages/core/src/core/version/zip.ts b/packages/core/src/core/version/zip.ts deleted file mode 100644 index 013ea3943..000000000 --- a/packages/core/src/core/version/zip.ts +++ /dev/null @@ -1,29 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import * as fflate from 'fflate'; -import find from 'find'; -import write from 'write'; -/** - * 压缩指定目录 - * @param root 压缩的目录 - * @param outputFilename 输出的文件名 - */ -export function compressionDir(root: string, outputFilename: string) { - const data: Record = {}; - find.fileSync(root).forEach((filename: string) => { - data[path.relative(root, filename)] = fs.readFileSync(filename); - }); - const zipU8 = fflate.zipSync(data); - write.sync(outputFilename, zipU8); -} -/** - * 解压的目录 - * @param root 解压到的目录 - */ -export function decompressionDir(root: string, data: ArrayBuffer) { - const buffer = new Uint8Array(data); - const files: fflate.Unzipped = fflate.unzipSync(buffer); - Object.keys(files).forEach((name) => { - write.sync(path.resolve(root, name), files[name]); - }); -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a003582b4..4b0e04137 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,2 +1 @@ export * from './core'; -export * from './cli'; diff --git a/packages/import/package.json b/packages/import/package.json index 8ffd680b4..1464ff19f 100644 --- a/packages/import/package.json +++ b/packages/import/package.json @@ -10,14 +10,16 @@ "coverage": "vitest run --coverage --pass-with-no-tests", "build": "unbuild" }, + "dependencies": { + "@import-maps/resolve": "^2.0.0" + }, "devDependencies": { "@biomejs/biome": "1.9.2", - "@gez/core": "3.0.0-alpha.5", - "@gez/lint": "3.0.0-alpha.5", - "@types/node": "20.12.12", + "@gez/lint": "0.0.9", + "@types/node": "22.9.0", "@vitest/coverage-v8": "1.6.0", - "stylelint": "16.5.0", - "typescript": "5.4.5", + "stylelint": "16.10.0", + "typescript": "5.6.3", "unbuild": "2.0.0", "vitest": "1.6.0" }, @@ -38,7 +40,8 @@ "src", "dist", "*.mjs", - "template" + "template", + "public" ], "gitHead": "a1175657723ffc3c0979cabe8ce4978268618270" } diff --git a/packages/import/src/import-loader.ts b/packages/import/src/import-loader.ts new file mode 100644 index 000000000..9df8f5e0c --- /dev/null +++ b/packages/import/src/import-loader.ts @@ -0,0 +1,56 @@ +import module from 'node:module'; +import { fileURLToPath } from 'node:url'; +import IM from '@import-maps/resolve'; +import type { ImportMap } from './types'; + +interface Data { + baseURL: string; + importMap: ImportMap; +} + +let registered = ''; +/** + * 创建一个使用 loader 实现的 importmap 的 import 函数,只能创建一次,无热更新,适合生产使用。 + */ +export function createLoaderImport(baseURL: URL, importMap: ImportMap = {}) { + if (!registered) { + module.register(fileURLToPath(import.meta.url), { + parentURL: baseURL, + data: { + baseURL: baseURL.href, + importMap + } + }); + registered = JSON.stringify(importMap); + } else if (registered !== JSON.stringify(importMap)) { + throw new Error( + `'createLoaderImport()' can only be created once and cannot be created repeatedly` + ); + } + return (specifier: string): Promise> => { + return import(specifier); + }; +} + +// loader 线程时的处理逻辑 +let loaderBaseURL: URL = new URL('file:'); +let loaderParsedImportMap: IM.ParsedImportMap = {}; + +export function initialize(data: Data) { + loaderBaseURL = new URL(data.baseURL); + loaderParsedImportMap = IM.parse(data.importMap, loaderBaseURL); +} + +export function resolve( + specifier: string, + context: Record, + nextResolve: Function +) { + const scriptURL = new URL(context.parentURL); + const result = IM.resolve(specifier, loaderParsedImportMap, scriptURL); + + if (result.matched && result.resolvedImport) { + return nextResolve(result.resolvedImport.href); + } + return nextResolve(specifier, context); +} diff --git a/packages/import/src/import-vm.ts b/packages/import/src/import-vm.ts new file mode 100644 index 000000000..22ad563d3 --- /dev/null +++ b/packages/import/src/import-vm.ts @@ -0,0 +1,108 @@ +import fs from 'node:fs'; +import { isBuiltin } from 'node:module'; +import path from 'node:path'; +import vm from 'node:vm'; +import IM from '@import-maps/resolve'; +import type { ImportMap } from './types'; + +/** + * 创建一个使用 vm 实现的 importmap 的 import 函数,可以创建多次来实现热更新效果,适合开发使用。 + */ +export function createVmImport(baseURL: URL, importMap: ImportMap = {}) { + const parsedImportMap = IM.parse(importMap, baseURL); + async function moduleLinker( + specifier: string, + parent: string, + context: vm.Context, + cache = new Map>() + ) { + if (isBuiltin(specifier)) { + const nodeModule = await import(specifier); + const keys = Object.keys(nodeModule); + const module = new vm.SyntheticModule( + keys, + function evaluateCallback() { + keys.forEach((key) => { + this.setExport(key, nodeModule[key]); + }); + }, + { + identifier: specifier, + context: context + } + ); + await module.link(() => { + throw new TypeError(`Native modules should not be linked`); + }); + await module.evaluate(); + return module; + } + const result = IM.resolve(specifier, parsedImportMap, new URL(parent)); + + let filename: string; + if (result.matched && result.resolvedImport) { + filename = result.resolvedImport.href; + } else { + filename = import.meta.resolve(specifier, parent); + } + const url = new URL(filename); + const readFilename = url.pathname; + let module = cache.get(readFilename); + if (module) { + return module; + } + const dirname = path.dirname(filename); + const build = async (): Promise => { + const text = fs.readFileSync(readFilename, 'utf-8'); + const module = new vm.SourceTextModule(text, { + initializeImportMeta: (meta) => { + meta.filename = filename; + meta.dirname = dirname; + meta.resolve = ( + specifier: string, + parent: string | URL = url + ) => { + return import.meta.resolve(specifier, parent); + }; + meta.url = url.toString(); + }, + identifier: specifier, + context: context, + // @ts-ignore + importModuleDynamically: (specifier, referrer) => { + return moduleLinker( + specifier, + filename, + // @ts-ignore + referrer.context, + cache + ); + } + }); + await module.link((specifier: string, referrer) => { + return moduleLinker( + specifier, + filename, + referrer.context, + cache + ); + }); + await module.evaluate(); + return module; + }; + module = build(); + cache.set(readFilename, module); + return module; + } + return async ( + specifier: string, + parent: string, + sandbox?: vm.Context, + options?: vm.CreateContextOptions + ) => { + const context = vm.createContext(sandbox, options); + const module = await moduleLinker(specifier, parent, context); + + return module.namespace as Record; + }; +} diff --git a/packages/import/src/index.ts b/packages/import/src/index.ts index 864f139c8..d27652fb3 100644 --- a/packages/import/src/index.ts +++ b/packages/import/src/index.ts @@ -1,98 +1,3 @@ -import fs from 'node:fs'; -import { isBuiltin } from 'node:module'; -import path from 'node:path'; -import * as vm from 'node:vm'; - -/** - * 导入 ESM 模块,每次都是全新的上下文 - * 需要启用:node --experimental-vm-modules --experimental-import-meta-resolve - * @param specifier 模块的名称 - * @param parent 将使用模块的 import.meta.url 传入 - * @param sandbox 透传给 node:vm createContext(sandbox) - * @param options 透传给 node:vm createContext(sandbox, options) - * @returns - */ -export async function import$( - specifier: string, - parent: string, - sandbox?: vm.Context, - options?: vm.CreateContextOptions -) { - const context = vm.createContext(sandbox, options); - const module = await moduleLinker(specifier, parent, context); - - return module.namespace as Record; -} - -async function moduleLinker( - specifier: string, - parent: string, - context: vm.Context, - cache = new Map>() -) { - if (isBuiltin(specifier)) { - const nodeModule = await import(specifier); - const keys = Object.keys(nodeModule); - const module = new vm.SyntheticModule( - keys, - function evaluateCallback() { - keys.forEach((key) => { - this.setExport(key, nodeModule[key]); - }); - }, - { - identifier: specifier, - context: context - } - ); - await module.link(() => { - throw new TypeError(`Native modules should not be linked`); - }); - await module.evaluate(); - return module; - } - const filename = import.meta.resolve(specifier, parent); - const dirname = path.dirname(filename); - const url = new URL(import.meta.resolve(specifier, parent)); - const readFilename = url.pathname; - let module = cache.get(readFilename); - if (module) { - return module; - } - const build = async (): Promise => { - const text = fs.readFileSync(readFilename, 'utf-8'); - const module = new vm.SourceTextModule(text, { - initializeImportMeta: (meta) => { - meta.filename = filename; - meta.dirname = dirname; - meta.resolve = ( - specifier: string, - parent: string | URL = url - ) => { - return import.meta.resolve(specifier, parent); - }; - meta.url = url.toString(); - }, - identifier: specifier, - context: context, - // @ts-ignore - importModuleDynamically: (specifier, referrer) => { - return moduleLinker( - specifier, - filename, - // @ts-ignore - referrer.context, - cache - ); - } - }); - await module.link((specifier: string, referrer) => { - return moduleLinker(specifier, filename, referrer.context, cache); - }); - await module.evaluate(); - return module; - }; - module = build(); - cache.set(readFilename, module); - return module; -} +export { createVmImport } from './import-vm'; +export { createLoaderImport } from './import-loader'; +export { type ImportMap, type SpecifierMap, type ScopesMap } from './types'; diff --git a/packages/import/src/types.ts b/packages/import/src/types.ts new file mode 100644 index 000000000..a29f34b38 --- /dev/null +++ b/packages/import/src/types.ts @@ -0,0 +1,8 @@ +export type SpecifierMap = Record; + +export type ScopesMap = Record; + +export interface ImportMap { + imports?: SpecifierMap; + scopes?: ScopesMap; +} diff --git a/packages/lint/package.json b/packages/lint/package.json index bcd7be7e6..b95fec7bd 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -12,41 +12,24 @@ }, "devDependencies": { "@biomejs/biome": "1.9.2", - "@gez/lint": "workspace:^", - "@types/node": "20.12.12", + "@gez/lint": "0.0.9", + "@types/node": "22.9.0", "@vitest/coverage-v8": "1.6.0", - "stylelint": "16.5.0", - "typescript": "5.4.5", + "stylelint": "16.10.0", + "typescript": "5.6.3", "unbuild": "2.0.0", "vitest": "1.6.0" }, "peerDependencies": { - "eslint": ">=8.57.0", "stylelint": ">=16.5.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^6.21.0", - "@typescript-eslint/parser": "^6.21.0", - "eslint-config-prettier": "^9.1.0", - "eslint-config-standard": "^17.1.0", - "eslint-config-standard-with-typescript": "^43.0.1", - "eslint-import-resolver-custom-alias": "^1.3.2", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-n": "^16.6.2", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-promise": "^6.1.1", - "eslint-plugin-simple-import-sort": "^12.1.0", - "eslint-plugin-vue": "^9.26.0", - "postcss-html": "^1.7.0", - "prettier": "^3.2.5", "stylelint-config-html": "^1.1.0", - "stylelint-config-recess-order": "^5.0.1", + "stylelint-config-recess-order": "^5.1.1", "stylelint-config-recommended-less": "^3.0.1", "stylelint-config-recommended-vue": "^1.5.0", - "stylelint-config-standard": "^36.0.0", - "stylelint-order": "^6.0.4", - "vue-eslint-parser": "^9.4.2" + "stylelint-config-standard": "^36.0.1", + "stylelint-order": "^6.0.4" }, "version": "3.0.0-alpha.5", "type": "module", @@ -56,22 +39,6 @@ "import": "./dist/index.mjs", "require": "./dist/index.cjs", "types": "./dist/index.d.ts" - }, - "./css-base": { - "import": "./dist/css-base.mjs", - "require": "./dist/css-base.cjs" - }, - "./js-base": { - "import": "./dist/js-base.mjs", - "require": "./dist/js-base.cjs" - }, - "./js-vue2": { - "import": "./dist/js-vue2.mjs", - "require": "./dist/js-vue2.cjs" - }, - "./js-vue3": { - "import": "./dist/js-vue3.mjs", - "require": "./dist/js-vue3.cjs" } }, "main": "./dist/index.cjs", @@ -81,7 +48,8 @@ "src", "dist", "*.mjs", - "template" + "template", + "public" ], "gitHead": "a1175657723ffc3c0979cabe8ce4978268618270" } diff --git a/packages/lint/src/css-base.ts b/packages/lint/src/css-base.ts deleted file mode 100644 index 0b9cc677d..000000000 --- a/packages/lint/src/css-base.ts +++ /dev/null @@ -1,26 +0,0 @@ -export default { - extends: [ - 'stylelint-config-standard', - 'stylelint-config-recess-order', - 'stylelint-config-recommended-less', - 'stylelint-config-html', - 'stylelint-config-recommended-vue' - ], - rules: { - 'no-empty-source': null, - 'selector-pseudo-class-no-unknown': [ - true, - { - ignorePseudoClasses: ['deep', 'global'] - } - ], - 'declaration-block-no-shorthand-property-overrides': null, - 'media-query-no-invalid': null, - 'media-feature-range-notation': null, - 'selector-pseudo-element-no-unknown': null, - 'order/properties-order': [], - 'no-descending-specificity': null, - 'font-family-no-missing-generic-family-keyword': null, - 'selector-class-pattern': null - } -}; diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index 7bb7b0a1b..cb0ff5c3b 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -1,4 +1 @@ -export { default as CssBase } from './css-base'; -export { default as JsBase } from './js-base'; -export { default as JsVue2 } from './js-vue2'; -export { default as JsVue3 } from './js-vue3'; +export {}; diff --git a/packages/lint/src/js-base.ts b/packages/lint/src/js-base.ts deleted file mode 100644 index 0f807e8c0..000000000 --- a/packages/lint/src/js-base.ts +++ /dev/null @@ -1,72 +0,0 @@ -export default { - env: { - browser: true, - es2021: true, - node: true - }, - extends: [ - 'standard-with-typescript', - 'plugin:import/errors', - 'plugin:import/typescript', - 'plugin:prettier/recommended' - ], - plugins: ['simple-import-sort'], - overrides: [], - parser: '@typescript-eslint/parser', - parserOptions: { - project: true, - ecmaVersion: 'latest', - sourceType: 'module' - }, - rules: { - 'simple-import-sort/imports': 'error', - 'no-lone-blocks': 'off', - '@typescript-eslint/no-empty-interface': 'off', - 'no-template-curly-in-string': 'off', - '@typescript-eslint/no-extraneous-class': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/no-dynamic-delete': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-namespace': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-confusing-void-expression': 'error', - '@typescript-eslint/await-thenable': 'off', - '@typescript-eslint/no-misused-promises': 'off', - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/strict-boolean-expressions': 'off', - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/naming-convention': 'off', - '@typescript-eslint/prefer-nullish-coalescing': 'off', - '@typescript-eslint/unbound-method': 'off', - '@typescript-eslint/no-unsafe-argument': 'off', - 'no-return-await': 'error', - '@typescript-eslint/promise-function-async': 'off', - '@typescript-eslint/return-await': 'off', - '@typescript-eslint/class-literal-property-style': 'off', - 'prettier/prettier': [ - 'error', - { - tabWidth: 4, - useTabs: false, - semi: true, - trailingComma: 'none', - singleQuote: true, - bracketSpacing: true, - arrowParens: 'always' - } - ] - }, - settings: { - 'import/resolver': { - 'eslint-import-resolver-custom-alias': { - extensions: ['.js', 'jsx', '.vue', '.ts', 'tsx'] - }, - typescript: { - alwaysTryTypes: true - } - } - } -}; diff --git a/packages/lint/src/js-vue2.ts b/packages/lint/src/js-vue2.ts deleted file mode 100644 index 6bea204f4..000000000 --- a/packages/lint/src/js-vue2.ts +++ /dev/null @@ -1,36 +0,0 @@ -import base from './js-base'; - -export default { - ...base, - parser: 'vue-eslint-parser', - parserOptions: { - ...base.parserOptions, - extraFileExtensions: ['.vue'], - parser: { - ts: '@typescript-eslint/parser', - js: '@typescript-eslint/parser', - '