diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfa8c2e4d..1039cd78d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,12 @@ on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [14.x] + node-version: [14.x, 16.x] + os: [ubuntu-latest, windows-latest] + fail-fast: false steps: - uses: actions/checkout@v3 with: diff --git a/.husky/pre-commit b/.husky/pre-commit index cb5024d8f..36af21989 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npm run lint:diff +npx lint-staged diff --git a/constants.ts b/constants.ts deleted file mode 100644 index 089a034f2..000000000 --- a/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const ICE_PKG_PACKAGES = [ - 'rax-compat', - 'jsx-runtime', -]; diff --git a/examples/basic-project/ice.config.mts b/examples/basic-project/ice.config.mts index ba3e6d26c..809efa536 100644 --- a/examples/basic-project/ice.config.mts +++ b/examples/basic-project/ice.config.mts @@ -12,10 +12,7 @@ export default defineConfig({ 'process.env.HAHA': JSON.stringify(true), }, transform: (code, id) => { - if (id.includes('src/pages')) { - // console.log('transform page:', id); - } - return code; + return id.includes('src/pages') && id.endsWith('.js') ? code : null; }, webpack: (webpackConfig) => { if (process.env.NODE_ENV !== 'test') { diff --git a/examples/basic-project/package.json b/examples/basic-project/package.json index 6d047b656..4f79c1abc 100644 --- a/examples/basic-project/package.json +++ b/examples/basic-project/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "scripts": { "start": "ice start", - "build": "ice build" + "build": "ice build", + "build:splitChunks": "ice build --config splitChunks.config.mts" }, "description": "", "author": "", @@ -21,8 +22,6 @@ "devDependencies": { "@types/react": "^18.0.0", "@types/react-dom": "^18.0.2", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9", "speed-measure-webpack-plugin": "^1.5.0", "webpack": "^5.73.0" } diff --git a/examples/basic-project/splitChunks.config.mts b/examples/basic-project/splitChunks.config.mts new file mode 100644 index 000000000..a8c0f5e33 --- /dev/null +++ b/examples/basic-project/splitChunks.config.mts @@ -0,0 +1,6 @@ +import { defineConfig } from '@ice/app'; + +export default defineConfig({ + splitChunks: false, + minify: false, +}); diff --git a/examples/basic-project/src/pages/about.tsx b/examples/basic-project/src/pages/about.tsx index 0c46fcb0f..dce5b82a5 100644 --- a/examples/basic-project/src/pages/about.tsx +++ b/examples/basic-project/src/pages/about.tsx @@ -1,4 +1,4 @@ -import { Link, useData, useConfig } from 'ice'; +import { Link, useData, useConfig, history } from 'ice'; import { isWeb } from '@uni/env'; // @ts-expect-error import url from './ice.png'; @@ -12,6 +12,7 @@ export default function About() { const config = useConfig(); console.log('render About', 'data', data, 'config', config); + console.log('history in component', history); return ( <> @@ -19,7 +20,7 @@ export default function About() { home new -
isWeb: { isWeb ? 'true' : 'false' }
+
isWeb: {isWeb ? 'true' : 'false'}
); } diff --git a/examples/csr-project/package.json b/examples/csr-project/package.json index aec43b654..d77a50cfd 100644 --- a/examples/csr-project/package.json +++ b/examples/csr-project/package.json @@ -19,8 +19,6 @@ "devDependencies": { "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9", "speed-measure-webpack-plugin": "^1.5.0", "webpack": "^5.73.0" } diff --git a/examples/hash-router/package.json b/examples/hash-router/package.json index ea1f252b9..416b1dbb8 100644 --- a/examples/hash-router/package.json +++ b/examples/hash-router/package.json @@ -13,7 +13,6 @@ }, "devDependencies": { "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "regenerator-runtime": "^0.13.9" + "@types/react-dom": "^18.0.0" } } \ No newline at end of file diff --git a/examples/rax-project/ice.config.mts b/examples/rax-project/ice.config.mts index de6cbd325..95a7ba884 100644 --- a/examples/rax-project/ice.config.mts +++ b/examples/rax-project/ice.config.mts @@ -2,8 +2,6 @@ import { defineConfig } from '@ice/app'; import compatRax from '@ice/plugin-rax-compat'; export default defineConfig({ - ssr: false, - ssg: false, publicPath: '/', plugins: [compatRax()], }); diff --git a/examples/rax-project/package.json b/examples/rax-project/package.json index 98d2eb2b7..adb81eb63 100644 --- a/examples/rax-project/package.json +++ b/examples/rax-project/package.json @@ -23,8 +23,6 @@ "devDependencies": { "@types/react": "^18.0.0", "@types/react-dom": "^18.0.2", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9", "webpack": "^5.73.0" } } diff --git a/examples/routes-generate/package.json b/examples/routes-generate/package.json index e77e7f169..c91e04f2d 100644 --- a/examples/routes-generate/package.json +++ b/examples/routes-generate/package.json @@ -16,8 +16,6 @@ }, "devDependencies": { "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9" + "@types/react-dom": "^18.0.0" } } \ No newline at end of file diff --git a/examples/single-route/ice.config.mts b/examples/single-route/ice.config.mts index e91d7cc3e..2038c4c35 100644 --- a/examples/single-route/ice.config.mts +++ b/examples/single-route/ice.config.mts @@ -2,6 +2,7 @@ import { defineConfig } from '@ice/app'; export default defineConfig({ publicPath: '/', - removeHistoryDeadCode: true, - sourceMap: true, + optimization: { + router: false, + } }); \ No newline at end of file diff --git a/examples/single-route/optimization.config.mts b/examples/single-route/optimization.config.mts new file mode 100644 index 000000000..deb4235b4 --- /dev/null +++ b/examples/single-route/optimization.config.mts @@ -0,0 +1,8 @@ +import { defineConfig } from '@ice/app'; + +export default defineConfig({ + publicPath: '/', + optimization: { + router: true, + } +}); \ No newline at end of file diff --git a/examples/single-route/package.json b/examples/single-route/package.json index da8ea68b0..f813e5717 100644 --- a/examples/single-route/package.json +++ b/examples/single-route/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "scripts": { "start": "ice start", - "build": "ice build" + "build": "ice build", + "build:optimization": "ice build --config optimization.config.mts" }, "description": "", "author": "", @@ -15,9 +16,7 @@ "react-dom": "^18.0.0" }, "devDependencies": { - "browserslist": "^4.19.3", "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "webpack-bundle-analyzer": "^4.5.0" + "@types/react-dom": "^18.0.0" } } diff --git a/examples/with-antd-mobile/package.json b/examples/with-antd-mobile/package.json index 7021da8ef..900e72d27 100644 --- a/examples/with-antd-mobile/package.json +++ b/examples/with-antd-mobile/package.json @@ -19,8 +19,6 @@ }, "devDependencies": { "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.2", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9" + "@types/react-dom": "^18.0.2" } } \ No newline at end of file diff --git a/examples/with-antd/package.json b/examples/with-antd/package.json index 6a946af4e..be7de568d 100644 --- a/examples/with-antd/package.json +++ b/examples/with-antd/package.json @@ -20,8 +20,6 @@ }, "devDependencies": { "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.2", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9" + "@types/react-dom": "^18.0.2" } } diff --git a/examples/with-fusion/package.json b/examples/with-fusion/package.json index 18bc2d41d..7eed0b8ef 100644 --- a/examples/with-fusion/package.json +++ b/examples/with-fusion/package.json @@ -18,7 +18,6 @@ }, "devDependencies": { "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.2", - "regenerator-runtime": "^0.13.9" + "@types/react-dom": "^18.0.2" } } diff --git a/examples/with-fusion/src/pages/index.tsx b/examples/with-fusion/src/pages/index.tsx index 8561086f7..354ffcc85 100644 --- a/examples/with-fusion/src/pages/index.tsx +++ b/examples/with-fusion/src/pages/index.tsx @@ -1,4 +1,5 @@ import { Button } from '@alifd/next'; +import '@alifd/next/dist/next.css'; export default function Home() { return ( diff --git a/examples/with-pha/package.json b/examples/with-pha/package.json index 090630352..c4af75689 100644 --- a/examples/with-pha/package.json +++ b/examples/with-pha/package.json @@ -18,8 +18,6 @@ "devDependencies": { "@types/react": "^18.0.0", "@types/react-dom": "^18.0.2", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9", "webpack": "^5.73.0" } } diff --git a/examples/with-store/package.json b/examples/with-store/package.json index 5dd61cb02..ff07de011 100644 --- a/examples/with-store/package.json +++ b/examples/with-store/package.json @@ -14,8 +14,6 @@ "devDependencies": { "@ice/plugin-store": "workspace:*", "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "browserslist": "^4.19.3", - "regenerator-runtime": "^0.13.9" + "@types/react-dom": "^18.0.0" } } \ No newline at end of file diff --git a/examples/with-store/src/models/user.ts b/examples/with-store/src/models/user.ts index e162baf09..041b699b2 100644 --- a/examples/with-store/src/models/user.ts +++ b/examples/with-store/src/models/user.ts @@ -1,4 +1,4 @@ -import { createModel } from '@ice/plugin-store/esm/runtime'; +import { createModel } from 'ice'; export default createModel({ state: { diff --git a/examples/with-store/src/pages/blog/models/info.ts b/examples/with-store/src/pages/blog/models/info.ts index c99ee5b20..60924a244 100644 --- a/examples/with-store/src/pages/blog/models/info.ts +++ b/examples/with-store/src/pages/blog/models/info.ts @@ -1,4 +1,4 @@ -import { createModel } from '@ice/plugin-store/esm/runtime'; +import { createModel } from 'ice'; export default createModel({ state: { diff --git a/examples/with-store/src/pages/blog/store.ts b/examples/with-store/src/pages/blog/store.ts index 4375df669..4a1f21686 100644 --- a/examples/with-store/src/pages/blog/store.ts +++ b/examples/with-store/src/pages/blog/store.ts @@ -1,4 +1,4 @@ -import { createStore } from '@ice/plugin-store/esm/runtime'; +import { createStore } from 'ice'; import info from './models/info'; export default createStore({ info }); diff --git a/examples/with-store/src/pages/models/counter.ts b/examples/with-store/src/pages/models/counter.ts index ed62a5603..d47da9565 100644 --- a/examples/with-store/src/pages/models/counter.ts +++ b/examples/with-store/src/pages/models/counter.ts @@ -1,4 +1,4 @@ -import { createModel } from '@ice/plugin-store/esm/runtime'; +import { createModel } from 'ice'; export default createModel({ state: { diff --git a/examples/with-store/src/pages/store.ts b/examples/with-store/src/pages/store.ts index 4a59915c7..fd399d4f1 100644 --- a/examples/with-store/src/pages/store.ts +++ b/examples/with-store/src/pages/store.ts @@ -1,4 +1,4 @@ -import { createStore } from '@ice/plugin-store/esm/runtime'; +import { createStore } from 'ice'; import counter from './models/counter'; const store = createStore({ counter }); diff --git a/examples/with-store/src/store.ts b/examples/with-store/src/store.ts index 5fa480991..34eafd1ac 100644 --- a/examples/with-store/src/store.ts +++ b/examples/with-store/src/store.ts @@ -1,4 +1,4 @@ -import { createStore } from '@ice/plugin-store/esm/runtime'; +import { createStore } from 'ice'; import user from './models/user'; export default createStore({ user }); diff --git a/package.json b/package.json index eb34bd51a..80a172ce8 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { + "name": "ice-monorepo", "private": true, - "workspaces": [ - "packages/*" - ], - "version": "0.0.1", "description": "A universal framework based on React", + "author": "ice-admin@alibaba-inc.com", + "license": "MIT", + "homepage": "https://v3.ice.work", + "repository": "ice-lab/ice-next", + "bugs": "https://github.com/ice-lab/ice-next/issues", "scripts": { "prepare": "husky install", "setup": "rm -rf node_modules packages/*/node_modules && pnpm install && pnpm prebundle && pnpm build", @@ -16,20 +18,13 @@ "dependency:check": "esmo ./scripts/dependencyCheck.ts", "version": "esmo ./scripts/tagVersion.ts", "version:check": "esmo ./scripts/versionCheck.ts", - "lint:diff": "esmo ./scripts/lintDiff.ts", "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./", - "lint:fix": "npm run lint -- --fix", "publish:alpha": "PUBLISH_TYPE=alpha esmo ./scripts/publishPackageWithDistTag.ts", "publish:beta": "PUBLISH_TYPE=beta esmo ./scripts/publishPackageWithDistTag.ts", "publish:release": "PUBLISH_TYPE=release VERSION_PREFIX=rc esmo ./scripts/publishPackageWithDistTag.ts", "cov": "vitest run --coverage", "test": "vitest" }, - "author": "ice-admin@alibaba-inc.com", - "license": "MIT", - "repository": "ice-lab/ice-next", - "bugs": "https://github.com/ice-lab/ice-next/issues", - "homepage": "https://v3.ice.work", "devDependencies": { "@applint/spec": "^1.2.3", "@commitlint/cli": "^16.3.0", @@ -56,6 +51,7 @@ "husky": "^7.0.4", "ice-npm-utils": "^3.0.2", "jsdom": "^20.0.0", + "lint-staged": "^13.0.3", "prettier": "^2.7.1", "puppeteer": "^13.7.0", "react": "^18.2.0", @@ -66,5 +62,8 @@ "typescript": "^4.7.4", "vitest": "^0.15.2" }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": "eslint --cache --fix" + }, "packageManager": "pnpm@7.2.1" } diff --git a/packages/bundles/scripts/tasks.ts b/packages/bundles/scripts/tasks.ts index b0de7a0f8..628e9b548 100644 --- a/packages/bundles/scripts/tasks.ts +++ b/packages/bundles/scripts/tasks.ts @@ -77,8 +77,8 @@ const tasks = [ { pkgName: 'terser-webpack-plugin', matchCopyFiles: (data: { resolvePath: string; resolveId: string }): boolean => { - const { resolvePath, resolveId } = data; - return resolvePath.endsWith('./utils') && resolveId.endsWith('terser-webpack-plugin/dist/index.js'); + const { resolvePath } = data; + return resolvePath.endsWith('./utils') || resolvePath.endsWith('.json'); }, }, { diff --git a/packages/ice/package.json b/packages/ice/package.json index b8fecdf7a..828061fe0 100644 --- a/packages/ice/package.json +++ b/packages/ice/package.json @@ -41,7 +41,7 @@ "acorn": "^8.7.1", "address": "^1.1.2", "body-parser": "^1.20.0", - "build-scripts": "^2.0.0-23", + "build-scripts": "^2.0.0-24", "chalk": "^4.0.0", "commander": "^9.0.0", "consola": "^2.15.3", @@ -63,6 +63,7 @@ "open": "^8.4.0", "path-to-regexp": "^6.2.0", "react-router": "^6.3.0", + "regenerator-runtime": "^0.13.9", "resolve.exports": "^1.1.0", "sass": "^1.49.9", "semver": "^7.3.5", @@ -71,8 +72,8 @@ "webpack-dev-server": "^4.7.4" }, "devDependencies": { - "@types/babel__traverse": "^7.17.1", "@types/babel__generator": "^7.6.4", + "@types/babel__traverse": "^7.17.1", "@types/cross-spawn": "^6.0.2", "@types/ejs": "^3.1.0", "@types/estree": "^0.0.51", @@ -83,7 +84,7 @@ "@types/temp": "^0.9.1", "chokidar": "^3.5.3", "react": "^18.0.0", - "unplugin": "^0.8.0", + "unplugin": "^0.9.5", "webpack": "^5.73.0" }, "peerDependencies": { diff --git a/packages/ice/src/commands/start.ts b/packages/ice/src/commands/start.ts index ec943f999..2de7f3dc9 100644 --- a/packages/ice/src/commands/start.ts +++ b/packages/ice/src/commands/start.ts @@ -78,7 +78,7 @@ const start = async ( preBundle: format === 'esm' && (ssr || ssg), swc: { // Remove components and getData when document only. - removeExportExprs: false ? ['default', 'getData', 'getServerData', 'getStaticData'] : [], + removeExportExprs: (!ssg && !ssr) ? ['default', 'getData', 'getServerData', 'getStaticData'] : [], keepPlatform: 'node', }, }, diff --git a/packages/ice/src/config.ts b/packages/ice/src/config.ts index 1b8679ada..816dd3f40 100644 --- a/packages/ice/src/config.ts +++ b/packages/ice/src/config.ts @@ -279,8 +279,8 @@ const userConfig = [ }, }, { - name: 'removeHistoryDeadCode', - validation: 'boolean', + name: 'optimization', + validation: 'object', }, { name: 'mock', @@ -311,6 +311,14 @@ const userConfig = [ } }, }, + { + name: 'splitChunks', + validation: 'boolean', + defaultValue: true, + setConfig: (config: Config, splitChunks: UserConfig['splitChunks']) => { + config.splitChunks = splitChunks; + }, + }, ]; const cliOption = [ diff --git a/packages/ice/src/constant.ts b/packages/ice/src/constant.ts index bd8e59305..8feb54785 100644 --- a/packages/ice/src/constant.ts +++ b/packages/ice/src/constant.ts @@ -6,7 +6,7 @@ export const ASSETS_MANIFEST = path.join(RUNTIME_TMP_DIR, 'assets-manifest.json' export const SERVER_ENTRY = path.join(RUNTIME_TMP_DIR, 'entry.server.ts'); export const DATA_LOADER_ENTRY = path.join(RUNTIME_TMP_DIR, 'data-loader.ts'); export const SERVER_OUTPUT_DIR = 'server'; -export const CACHE_DIR = path.join('node_modules', RUNTIME_TMP_DIR); +export const CACHE_DIR = path.join('node_modules', '.cache'); export const BUILDIN_ESM_DEPS = [ '@ice/runtime', ]; diff --git a/packages/ice/src/createService.ts b/packages/ice/src/createService.ts index 6e8e7baf5..faa344347 100644 --- a/packages/ice/src/createService.ts +++ b/packages/ice/src/createService.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; import { Context } from 'build-scripts'; import consola from 'consola'; import type { CommandArgs, CommandName } from 'build-scripts'; @@ -27,6 +28,7 @@ import { getAppExportConfig, getRouteExportConfig } from './service/config.js'; import renderExportsTemplate from './utils/renderExportsTemplate.js'; import { getFileExports } from './service/analyze.js'; +const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); interface CreateServiceOptions { @@ -97,9 +99,6 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt ['userConfig', 'cliOption'].forEach((configType) => ctx.registerConfig(configType, config[configType])); let taskConfigs = await ctx.setup(); - // merge task config with built-in config - taskConfigs = mergeTaskConfig(taskConfigs, { port: commandArgs.port }); - const webTaskConfig = taskConfigs.find(({ name }) => name === 'web'); // get userConfig after setup because of userConfig maybe modified by plugins const { userConfig } = ctx; @@ -112,6 +111,22 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt const hasExportAppData = (await getFileExports({ rootDir, file: 'src/app' })).includes('getAppData'); const csr = !userConfig.ssr && !userConfig.ssg; + const disableRouter = userConfig?.optimization?.router && routesInfo.routesCount <= 1; + let taskAlias = {}; + if (disableRouter) { + consola.info('[ice]', 'optimization.router is enabled and only have one route, ice build will remove react-router and history which is unnecessary.'); + taskAlias['@ice/runtime/router'] = path.join(require.resolve('@ice/runtime'), '../single-router.js'); + } + // merge task config with built-in config + taskConfigs = mergeTaskConfig(taskConfigs, { + port: commandArgs.port, + alias: { + // Get absolute path of `regenerator-runtime`, so it's unnecessary to add it to project dependencies + 'regenerator-runtime': require.resolve('regenerator-runtime'), + }, + }); + const webTaskConfig = taskConfigs.find(({ name }) => name === 'web'); + // add render data generator.setRenderData({ ...routesInfo, @@ -124,6 +139,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt }); dataCache.set('routes', JSON.stringify(routesInfo)); dataCache.set('hasExportAppData', hasExportAppData ? 'true' : ''); + // Render exports files if route component export getData / getConfig. renderExportsTemplate({ ...routesInfo, @@ -176,10 +192,6 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt consola.debug(err); } - const disableRouter = userConfig.removeHistoryDeadCode && routesInfo.routesCount <= 1; - if (disableRouter) { - consola.info('[ice] removeHistoryDeadCode is enabled and only have one route, ice build will remove history and react-router dead code.'); - } updateRuntimeEnv(appConfig, { disableRouter }); return { @@ -198,7 +210,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt reCompileRouteConfig, dataCache, appConfig, - devPath: (routePaths[0] || '').replace(/^\//, ''), + devPath: (routePaths[0] || '').replace(/^[\/\\]/, ''), spinner: buildSpinner, }); } else if (command === 'build') { diff --git a/packages/ice/src/esbuild/transformImport.ts b/packages/ice/src/esbuild/transformImport.ts index 13464a3ea..b593cc906 100644 --- a/packages/ice/src/esbuild/transformImport.ts +++ b/packages/ice/src/esbuild/transformImport.ts @@ -8,13 +8,14 @@ import type { ImportSpecifier } from '@ice/bundles/compiled/es-module-lexer/inde import type { Node } from 'estree'; import type { UnpluginOptions } from 'unplugin'; import type { DepsMetaData } from '../service/preBundleCJSDeps.js'; +import formatPath from '../utils/formatPath.js'; const { init, parse } = esModuleLexer; type ImportNameSpecifier = { importedName: string; localName: string }; // Redirect original dependency to the pre-bundle dependency(cjs) which is handled by preBundleCJSDeps function. -const transformImportPlugin = (metadata: DepsMetaData): UnpluginOptions => { +const transformImportPlugin = (metadata: DepsMetaData, serverDir: string): UnpluginOptions => { const { deps } = metadata; const redirectDepIds = []; return { @@ -54,7 +55,7 @@ const transformImportPlugin = (metadata: DepsMetaData): UnpluginOptions => { } const importExp = source.slice(expStart, expEnd); - const filePath = deps[specifier].file; + const filePath = formatPath(path.relative(formatPath(serverDir), formatPath(deps[specifier].file))); redirectDepIds.push(filePath); const rewritten = transformCjsImport( importExp, diff --git a/packages/ice/src/middlewares/ssr/renderMiddleware.ts b/packages/ice/src/middlewares/ssr/renderMiddleware.ts index 11068801f..b66e280e7 100644 --- a/packages/ice/src/middlewares/ssr/renderMiddleware.ts +++ b/packages/ice/src/middlewares/ssr/renderMiddleware.ts @@ -9,6 +9,7 @@ import type { ExtendsPluginAPI } from '@ice/types/esm/plugin.js'; import type { TaskConfig } from 'build-scripts'; import type { Config } from '@ice/types'; import getRouterBasename from '../../utils/getRouterBasename.js'; +import dynamicImport from '../../utils/dynamicImport.js'; const require = createRequire(import.meta.url); @@ -27,7 +28,8 @@ export default function createRenderMiddleware(options: Options): Middleware { const routes = JSON.parse(fse.readFileSync(routeManifestPath, 'utf-8')); const basename = getRouterBasename(taskConfig, (await getAppConfig()).default); const matches = matchRoutes(routes, req.path, basename); - if (matches.length) { + // When documentOnly is true, it means that the app is CSR and it should return the html. + if (matches.length || documentOnly) { // Wait for the server compilation to finish const { serverEntry, error } = await serverCompileTask.get(); if (error) { @@ -37,9 +39,7 @@ export default function createRenderMiddleware(options: Options): Middleware { let serverModule; try { delete require.cache[serverEntry]; - // timestamp for disable import cache - const serverEntryWithVersion = `${serverEntry}?version=${new Date().getTime()}`; - serverModule = await import(serverEntryWithVersion); + serverModule = await dynamicImport(serverEntry, true); } catch (err) { // make error clearly, notice typeof err === 'string' consola.error(`import ${serverEntry} error: ${err}`); diff --git a/packages/ice/src/routes.ts b/packages/ice/src/routes.ts index 501bdef06..f1102a264 100644 --- a/packages/ice/src/routes.ts +++ b/packages/ice/src/routes.ts @@ -3,6 +3,7 @@ import { formatNestedRouteManifest, generateRouteManifest } from '@ice/route-man import type { NestedRouteManifest } from '@ice/route-manifest'; import type { UserConfig } from '@ice/types'; import { getFileExports } from './service/analyze.js'; +import formatPath from './utils/formatPath.js'; export async function generateRoutesInfo(rootDir: string, routesConfig: UserConfig['routes'] = {}) { const routeManifest = generateRouteManifest(rootDir, routesConfig.ignoreFiles, routesConfig.defineRoutes); @@ -12,7 +13,7 @@ export async function generateRoutesInfo(rootDir: string, routesConfig: UserConf // add exports filed for route manifest routeItem.exports = await getFileExports({ rootDir, - file: path.join('./src/pages', routeItem.file), + file: formatPath(path.join('./src/pages', routeItem.file)), }); }); await Promise.all(analyzeTasks); @@ -59,8 +60,8 @@ function recurseRoutesStr(nestRouteManifest: NestedRouteManifest[], depth = 0) { const componentPath = id.startsWith('__') ? file : `@/pages/${file}`.replace(new RegExp(`${path.extname(file)}$`), ''); const routeProperties: string[] = [ - `path: '${routePath || ''}',`, - `load: () => import(/* webpackChunkName: "${componentName}" */ '${componentPath}'),`, + `path: '${formatPath(routePath || '')}',`, + `load: () => import(/* webpackChunkName: "${componentName}" */ '${formatPath(componentPath)}'),`, `componentName: '${componentName}',`, `index: ${index},`, `id: '${id}',`, @@ -99,19 +100,18 @@ function generateRouteConfig( function importConfig(routes: NestedRouteManifest[], parentPath: string) { return routes.reduce((prev, route) => { const { children, file, id, exports } = route; - if (exports.indexOf(exportKey) === -1) { - return prev; - } - - const fileExtname = path.extname(file); - const componentFile = file.replace(new RegExp(`${fileExtname}$`), ''); - const componentPath = path.isAbsolute(componentFile) ? componentFile : `@/pages/${componentFile}`; - - const loaderName = `${exportKey}_${id}`.replace(/[-/]/g, '_'); const routePath = route.path || (route.index ? 'index' : '/'); - const fullPath = path.join(parentPath, routePath); - imports.push([id, loaderName, fullPath]); - let str = `import { ${exportKey} as ${loaderName} } from '${componentPath}';\n`; + let str = ''; + if (exports.includes(exportKey)) { + const fileExtname = path.extname(file); + const componentFile = file.replace(new RegExp(`${fileExtname}$`), ''); + const componentPath = path.isAbsolute(componentFile) ? componentFile : `@/pages/${componentFile}`; + + const loaderName = `${exportKey}_${id}`.replace(/[-/]/g, '_'); + const fullPath = path.join(parentPath, routePath); + imports.push([id, loaderName, fullPath]); + str = `import { ${exportKey} as ${loaderName} } from '${componentPath}';\n`; + } if (children) { str += importConfig(children, routePath); diff --git a/packages/ice/src/service/analyze.ts b/packages/ice/src/service/analyze.ts index 63016bcab..f4b7b03f8 100644 --- a/packages/ice/src/service/analyze.ts +++ b/packages/ice/src/service/analyze.ts @@ -6,10 +6,15 @@ import moduleLexer from '@ice/bundles/compiled/es-module-lexer/index.js'; import { transform, build } from 'esbuild'; import type { Loader, Plugin } from 'esbuild'; import consola from 'consola'; +import type { TaskConfig } from 'build-scripts'; +import type { Config } from '@ice/types'; import { getCache, setCache } from '../utils/persistentCache.js'; import { getFileHash } from '../utils/hash.js'; import scanPlugin from '../esbuild/scan.js'; import type { DepScanData } from '../esbuild/scan.js'; +import formatPath from '../utils/formatPath.js'; + +type Alias = TaskConfig['config']['alias']; interface Options { parallel?: number; @@ -17,10 +22,6 @@ interface Options { alias?: Alias; } -export interface Alias { - [x: string]: string | false; -} - const { init, parse } = moduleLexer; function addLastSlash(filePath: string) { @@ -68,7 +69,7 @@ export function getImportPath( let aliasedPath = resolveId(id, alias) || ''; if (!path.isAbsolute(aliasedPath)) { if (aliasedPath.startsWith('.')) { - aliasedPath = path.resolve(path.dirname(importer), aliasedPath); + aliasedPath = formatPath(path.resolve(path.dirname(importer), aliasedPath)); } else { // node_modules dependencies aliasedPath = ''; diff --git a/packages/ice/src/service/config.ts b/packages/ice/src/service/config.ts index 646860b31..760959d37 100644 --- a/packages/ice/src/service/config.ts +++ b/packages/ice/src/service/config.ts @@ -4,6 +4,8 @@ import type { ServerCompiler } from '@ice/types/esm/plugin.js'; import removeTopLevelCode from '../esbuild/removeTopLevelCode.js'; import { getCache, setCache } from '../utils/persistentCache.js'; import { getFileHash } from '../utils/hash.js'; +import dynamicImport from '../utils/dynamicImport.js'; +import formatPath from '../utils/formatPath.js'; import { RUNTIME_TMP_DIR } from '../constant.js'; type GetOutfile = (entry: string, exportNames: string[]) => string; @@ -18,7 +20,7 @@ interface CompileConfig { class Config { private compileTasks: Record>; - private compiler: (keepExports: string[]) => Promise; + private compiler: (keepExports: string[]) => Promise; private compileConfig: CompileConfig; private lastOptions: string[]; private getOutfile: GetOutfile; @@ -31,7 +33,7 @@ class Config { this.lastOptions = []; this.status = 'PENDING'; this.getOutfile = compileConfig.getOutfile || - (() => path.join(rootDir, 'node_modules', `${path.basename(entry)}.mjs`)); + (() => formatPath(path.join(rootDir, 'node_modules', `${path.basename(entry)}.mjs`))); } public setCompiler(esbuildCompiler: ServerCompiler): void { @@ -39,15 +41,17 @@ class Config { const { entry, transformInclude } = this.compileConfig; const outfile = this.getOutfile(entry, keepExports); this.status = 'PENDING'; - await esbuildCompiler({ + const { error } = await esbuildCompiler({ entryPoints: [entry], format: 'esm', inject: [], outfile, plugins: [removeTopLevelCode(keepExports, transformInclude)], }); - this.status = 'RESOLVED'; - return `${outfile}?version=${new Date().getTime()}`; + if (!error) { + this.status = 'RESOLVED'; + return outfile; + } }; } @@ -67,7 +71,7 @@ class Config { const outfile = this.getOutfile(this.compileConfig.entry, keepExports); const cached = await this.compileConfig?.needRecompile(outfile, keepExports); if (cached && typeof cached === 'string') { - targetFile = this.status === 'RESOLVED' ? `${cached}?version=${new Date().getTime()}` + targetFile = this.status === 'RESOLVED' ? cached : (await this.compileTasks[taskKey]); } else if (!cached) { this.reCompile(taskKey); @@ -79,14 +83,14 @@ class Config { if (!targetFile) { targetFile = await this.compileTasks[taskKey]; } - return await import(targetFile); + if (targetFile) return await dynamicImport(targetFile, true); }; } export const getAppExportConfig = (rootDir: string) => { const appEntry = path.join(rootDir, 'src/app'); const getOutfile = (entry: string, keepExports: string[]) => - path.join(rootDir, 'node_modules', `${keepExports.join('_')}_${path.basename(entry)}.mjs`); + formatPath(path.join(rootDir, 'node_modules', `${keepExports.join('_')}_${path.basename(entry)}.mjs`)); const appExportConfig = new Config({ entry: appEntry, rootDir, @@ -95,7 +99,7 @@ export const getAppExportConfig = (rootDir: string) => { getOutfile, needRecompile: async (entry, keepExports) => { let cached = null; - const cachedKey = `app_${keepExports.join('_')}`; + const cachedKey = `app_${keepExports.join('_')}_${process.env.__ICE_VERSION__}`; try { cached = await getCache(rootDir, cachedKey); } catch (err) {} @@ -109,7 +113,7 @@ export const getAppExportConfig = (rootDir: string) => { }); const getAppConfig = async (exportNames?: string[]) => { - return await appExportConfig.getConfig(exportNames || ['default', 'defineAppConfig']); + return (await appExportConfig.getConfig(exportNames || ['default', 'defineAppConfig'])) || {}; }; return { @@ -129,7 +133,7 @@ export const getRouteExportConfig = (rootDir: string) => { transformInclude: (id) => id.includes('src/pages'), needRecompile: async (entry) => { let cached = false; - const cachedKey = 'route_config_file'; + const cachedKey = `route_config_file_${process.env.__ICE_VERSION__}`; try { cached = await getCache(rootDir, cachedKey); } catch (err) {} @@ -147,7 +151,7 @@ export const getRouteExportConfig = (rootDir: string) => { if (!fs.existsSync(routeConfigFile)) { return undefined; } - const routeConfig = (await routeExportConfig.getConfig(['getConfig'])).default; + const routeConfig = (await routeExportConfig.getConfig(['getConfig']) || {}).default; return specifyRoutId ? routeConfig[specifyRoutId] : routeConfig; }; return { diff --git a/packages/ice/src/service/preBundleCJSDeps.ts b/packages/ice/src/service/preBundleCJSDeps.ts index fb76b1f7c..d30216dfa 100644 --- a/packages/ice/src/service/preBundleCJSDeps.ts +++ b/packages/ice/src/service/preBundleCJSDeps.ts @@ -7,10 +7,12 @@ import type { Plugin } from 'esbuild'; import { resolve as resolveExports } from 'resolve.exports'; import moduleLexer from '@ice/bundles/compiled/es-module-lexer/index.js'; import type { Config } from '@ice/types'; +import type { TaskConfig } from 'build-scripts'; import flattenId from '../utils/flattenId.js'; import formatPath from '../utils/formatPath.js'; import { BUILDIN_CJS_DEPS, BUILDIN_ESM_DEPS } from '../constant.js'; import type { DepScanData } from '../esbuild/scan.js'; +import aliasPlugin from '../esbuild/alias.js'; interface DepInfo { file: string; @@ -30,6 +32,7 @@ interface PreBundleDepsOptions { depsInfo: Record; cacheDir: string; taskConfig: Config; + alias: TaskConfig['config']['alias']; plugins?: Plugin[]; } @@ -37,7 +40,7 @@ interface PreBundleDepsOptions { * Pre bundle dependencies from esm to cjs. */ export default async function preBundleCJSDeps(options: PreBundleDepsOptions): Promise { - const { depsInfo, cacheDir, taskConfig, plugins = [] } = options; + const { depsInfo, cacheDir, taskConfig, plugins = [], alias } = options; const metadata = createDepsMetadata(depsInfo, taskConfig); if (!Object.keys(depsInfo)) { @@ -64,7 +67,7 @@ export default async function preBundleCJSDeps(options: PreBundleDepsOptions): P await moduleLexer.init; for (const depId in depsInfo) { - const packageEntry = resolvePackageEntry(depId, depsInfo[depId].pkgPath); + const packageEntry = resolvePackageEntry(depId, depsInfo[depId].pkgPath, alias); const flatId = flattenId(depId); flatIdDeps[flatId] = packageEntry; @@ -87,7 +90,10 @@ export default async function preBundleCJSDeps(options: PreBundleDepsOptions): P platform: 'node', loader: { '.js': 'jsx' }, ignoreAnnotations: true, - plugins, + plugins: [ + aliasPlugin({ alias, format: 'cjs', externalDependencies: false }), + ...plugins, + ], external: [...BUILDIN_CJS_DEPS, ...BUILDIN_ESM_DEPS], }); } catch (error) { @@ -102,15 +108,20 @@ export default async function preBundleCJSDeps(options: PreBundleDepsOptions): P }; } -function resolvePackageEntry(depId: string, pkgPath: string) { +function resolvePackageEntry(depId: string, pkgPath: string, alias: TaskConfig['config']['alias']) { const pkgJSON = fse.readJSONSync(pkgPath); const pkgDir = path.dirname(pkgPath); + const aliasKey = Object.keys(alias).find(key => depId === key || depId.startsWith(`${depId}/`)); + // alias: { rax: 'rax-compat' } + // rax -> . + // rax/element -> ./element + const entry = aliasKey ? depId.replace(new RegExp(`^${aliasKey}`), '.') : depId; // resolve exports cjs field - let entryPoint = resolveExports(pkgJSON, depId, { require: true }) || ''; - if (!entryPoint) { - entryPoint = pkgJSON['main'] || 'index.js'; + let resolvedEntryPoint = resolveExports(pkgJSON, entry, { require: true }) || ''; + if (!resolvedEntryPoint) { + resolvedEntryPoint = pkgJSON['main'] || 'index.js'; } - const entryPointPath = path.join(pkgDir, entryPoint); + const entryPointPath = path.join(pkgDir, resolvedEntryPoint); return entryPointPath; } diff --git a/packages/ice/src/service/serverCompiler.ts b/packages/ice/src/service/serverCompiler.ts index bb54b3ae4..fd0d15dcd 100644 --- a/packages/ice/src/service/serverCompiler.ts +++ b/packages/ice/src/service/serverCompiler.ts @@ -140,7 +140,10 @@ export function createServerCompiler(options: Options) { plugins: [ ...transformPlugins, // Plugin transformImportPlugin need after transformPlugins in case of it has onLoad lifecycle. - dev && preBundle && transformImportPlugin(depsMetadata), + dev && preBundle && transformImportPlugin( + depsMetadata, + path.join(rootDir, task.config.outputDir, SERVER_OUTPUT_DIR), + ), ].filter(Boolean), }), ].filter(Boolean), @@ -188,10 +191,10 @@ interface CreateDepsMetadataOptions { */ async function createDepsMetadata({ rootDir, task, plugins }: CreateDepsMetadataOptions) { const serverEntry = getServerEntry(rootDir, task.config?.server?.entry); - + const alias = (task.config?.alias || {}) as TaskConfig['config']['alias']; const deps = await scanImports([serverEntry], { rootDir, - alias: (task.config?.alias || {}) as Record, + alias, plugins, }); @@ -212,6 +215,7 @@ async function createDepsMetadata({ rootDir, task, plugins }: CreateDepsMetadata depsInfo: preBundleDepsInfo, cacheDir, taskConfig: task.config, + alias, plugins, }); diff --git a/packages/ice/src/service/webpackCompiler.ts b/packages/ice/src/service/webpackCompiler.ts index 9a0e151b4..9998acdbc 100644 --- a/packages/ice/src/service/webpackCompiler.ts +++ b/packages/ice/src/service/webpackCompiler.ts @@ -95,20 +95,22 @@ async function webpackCompiler(options: { consola.warn(messages.warnings.join('\n')); } if (command === 'start') { + const appConfig = (await hooksAPI.getAppConfig()).default; + const hashChar = appConfig?.router?.type === 'hash' ? '#/' : ''; if (isSuccessful && isFirstCompile) { let logoutMessage = '\n'; logoutMessage += chalk.green(' Starting the development server at:'); if (process.env.CLOUDIDE_ENV) { - logoutMessage += `\n - IDE server: https://${process.env.WORKSPACE_UUID}-${commandArgs.port}.${process.env.WORKSPACE_HOST}${devPath}`; + logoutMessage += `\n - IDE server: https://${process.env.WORKSPACE_UUID}-${commandArgs.port}.${process.env.WORKSPACE_HOST}${hashChar}${devPath}`; } else { logoutMessage += `\n - - Local : ${chalk.underline.white(`${urls.localUrlForBrowser}${devPath}`)} - - Network: ${chalk.underline.white(`${urls.lanUrlForTerminal}${devPath}`)}`; + - Local : ${chalk.underline.white(`${urls.localUrlForBrowser}${hashChar}${devPath}`)} + - Network: ${chalk.underline.white(`${urls.lanUrlForTerminal}${hashChar}${devPath}`)}`; } consola.log(`${logoutMessage}\n`); if (commandArgs.open) { - openBrowser(`${urls.localUrlForBrowser}${devPath}`); + openBrowser(`${urls.localUrlForBrowser}${hashChar}${devPath}`); } } // compiler.hooks.done is AsyncSeriesHook which does not support async function diff --git a/packages/ice/src/utils/babelPluginRemoveCode.ts b/packages/ice/src/utils/babelPluginRemoveCode.ts index 64f59f679..e928a4665 100644 --- a/packages/ice/src/utils/babelPluginRemoveCode.ts +++ b/packages/ice/src/utils/babelPluginRemoveCode.ts @@ -92,8 +92,10 @@ const removeTopLevelCode = (keepExports: string[] = []) => { ExpressionStatement: { enter(nodePath: NodePath) { // Remove top level call expression. - if (nodePath.parentPath.isProgram() && t.isCallExpression(nodePath.node.expression)) { - nodePath.remove(); + if (nodePath.parentPath.isProgram()) { + if (t.isCallExpression(nodePath.node.expression) || t.isAssignmentExpression(nodePath.node.expression)) { + nodePath.remove(); + } } }, }, diff --git a/packages/ice/src/utils/dynamicImport.ts b/packages/ice/src/utils/dynamicImport.ts new file mode 100644 index 000000000..e0d8e3048 --- /dev/null +++ b/packages/ice/src/utils/dynamicImport.ts @@ -0,0 +1,19 @@ +import { pathToFileURL } from 'url'; +import formatPath from './formatPath.js'; + +export default async function dynamicImport(filePath: string, timestamp?: boolean) { + const isWin32 = process.platform === 'win32'; + let importPath = formatPath(filePath); + + if (isWin32) { + // Compatible with win32 path which starts with unsupported url scheme such as `D:/xx/xx/index.mjs` + const importUrl = pathToFileURL(importPath); + if (timestamp) { + importUrl.search = `version=${new Date().getTime()}`; + } + importPath = importUrl.toString(); + } else if (timestamp) { + importPath += `?version=${new Date().getTime()}`; + } + return await import(importPath); +} \ No newline at end of file diff --git a/packages/ice/src/utils/generateHTML.ts b/packages/ice/src/utils/generateHTML.ts index 8a7e5731b..6df6860bf 100644 --- a/packages/ice/src/utils/generateHTML.ts +++ b/packages/ice/src/utils/generateHTML.ts @@ -5,6 +5,7 @@ import consola from 'consola'; import type { ServerContext, RenderMode } from '@ice/runtime'; import { ROUTER_MANIFEST } from '../constant.js'; import getRoutePaths from './getRoutePaths.js'; +import dynamicImport from './dynamicImport.js'; interface Options { rootDir: string; @@ -26,7 +27,7 @@ export default async function generateHTML(options: Options) { let serverEntry; try { - serverEntry = await import(entry); + serverEntry = await dynamicImport(entry); } catch (err) { // make error clearly, notice typeof err === 'string' throw new Error(`import ${entry} error: ${err}`); @@ -52,8 +53,8 @@ export default async function generateHTML(options: Options) { routePath, serverOnlyBasename: '/', }); - - const fileName = routePath === '/' ? 'index.html' : `${routePath}.html`; + // Win32 do not support file name with + const fileName = routePath === '/' ? 'index.html' : `${routePath.replace(/\/:/g, '/$')}.html`; if (fse.existsSync(path.join(rootDir, 'public', fileName))) { consola.warn(`${fileName} is overwrite by framework, rename file name if it is necessary`); } diff --git a/packages/ice/src/utils/getRoutePaths.ts b/packages/ice/src/utils/getRoutePaths.ts index fa403a536..af0bbe94f 100644 --- a/packages/ice/src/utils/getRoutePaths.ts +++ b/packages/ice/src/utils/getRoutePaths.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import type { RouteObject } from 'react-router'; +import formatPath from './formatPath.js'; /** * get all route path @@ -13,7 +14,7 @@ function getRoutePaths(routes: RouteObject[], parentPath = ''): string[] { if (route.children) { pathList = pathList.concat(getRoutePaths(route.children, route.path)); } else { - pathList.push(path.join('/', parentPath, route.path || '')); + pathList.push(formatPath(path.join('/', parentPath, route.path || ''))); } }); diff --git a/packages/ice/src/utils/getRouterBasename.ts b/packages/ice/src/utils/getRouterBasename.ts index 32482fdfa..716e8bb5e 100644 --- a/packages/ice/src/utils/getRouterBasename.ts +++ b/packages/ice/src/utils/getRouterBasename.ts @@ -3,7 +3,7 @@ import type { Config } from '@ice/types'; import type { TaskConfig } from 'build-scripts'; const getRouterBasename = (taskConfig: TaskConfig, appConfig: AppConfig) => { - return taskConfig?.config?.basename || appConfig?.router?.basename; + return appConfig?.router?.basename ?? taskConfig?.config?.basename ?? ''; }; export default getRouterBasename; diff --git a/packages/ice/src/utils/getRuntimeModules.ts b/packages/ice/src/utils/getRuntimeModules.ts index 45a8d3359..f9c3c3f91 100644 --- a/packages/ice/src/utils/getRuntimeModules.ts +++ b/packages/ice/src/utils/getRuntimeModules.ts @@ -4,6 +4,7 @@ import findUp from 'find-up'; import consola from 'consola'; import type { PluginInfo } from 'build-scripts'; import type { ExtendsPluginAPI } from '@ice/types/esm/plugin.js'; +import formatPath from './formatPath.js'; export interface RuntimeModule { staticModule: boolean; @@ -27,7 +28,8 @@ function getRuntimeModules(plugins: Array>) { const pkgInfo = fse.readJSONSync(pkgPath); return { staticModule: !!pkgInfo?.pluginConfig?.staticModule, - path: runtime, + // Format path for win32 while runtime path will be render to template. + path: formatPath(runtime), name: pkgInfo.name as string, }; } catch (error) { diff --git a/packages/ice/src/utils/hash.ts b/packages/ice/src/utils/hash.ts index 0837e4143..55b0a121a 100644 --- a/packages/ice/src/utils/hash.ts +++ b/packages/ice/src/utils/hash.ts @@ -2,9 +2,10 @@ import * as path from 'path'; import * as fs from 'fs'; import { createHash } from 'crypto'; import fg from 'fast-glob'; +import formatPath from './formatPath.js'; export async function getFileHash(file: string): Promise { - let filePath = file; + let filePath = formatPath(file); if (!path.extname(filePath)) { const patterns = [`${filePath}.{js,ts,jsx,tsx}`]; filePath = fg.sync(patterns)[0]; diff --git a/packages/ice/templates/core/entry.client.ts.ejs b/packages/ice/templates/core/entry.client.ts.ejs index 8e1c90285..14f5fb191 100644 --- a/packages/ice/templates/core/entry.client.ts.ejs +++ b/packages/ice/templates/core/entry.client.ts.ejs @@ -6,7 +6,7 @@ import Document from '@/document'; const getRouterBasename = () => { const appConfig = getAppConfig(app); - return '<%- basename %>' || appConfig?.router?.basename || ''; + return appConfig?.router?.basename ?? '<%- basename %>' ?? ''; } runClientApp({ diff --git a/packages/ice/templates/core/entry.server.ts.ejs b/packages/ice/templates/core/entry.server.ts.ejs index 0b8cedcb9..86ea77594 100644 --- a/packages/ice/templates/core/entry.server.ts.ejs +++ b/packages/ice/templates/core/entry.server.ts.ejs @@ -9,7 +9,7 @@ import routes from './routes'; const getRouterBasename = () => { const appConfig = runtime.getAppConfig(app); - return '<%- basename %>' || appConfig?.router?.basename || ''; + return appConfig?.router?.basename ?? '<%- basename %>' ?? ''; } const setRuntimeEnv = (renderMode) => { diff --git a/packages/ice/templates/core/index.ts.ejs b/packages/ice/templates/core/index.ts.ejs index 8dc432a46..e161f248b 100644 --- a/packages/ice/templates/core/index.ts.ejs +++ b/packages/ice/templates/core/index.ts.ejs @@ -1,39 +1,15 @@ import { - Link as OriginLink, - Outlet as OriginOutlet, - useParams as useOriginParams, - useSearchParams as useOriginSearchParams, - useLocation as useOriginLocation, - LinkSingle, - OutletSingle, - useParamsSingle, - useSearchParamsSingle, - useLocationSingle, -} from '@ice/runtime'; - + Link, + Outlet, + useParams, + useSearchParams, + useLocation, + useNavigate, +} from '@ice/runtime/router'; <% if (globalStyle) {-%> import '<%= globalStyle %>' <% } -%> -let Link: typeof OriginLink; -let Outlet: typeof OriginOutlet; -let useParams: typeof useOriginParams; -let useSearchParams: typeof useOriginSearchParams; -let useLocation: typeof useOriginLocation; -if (process.env.ICE_CORE_ROUTER === 'true') { - Link = OriginLink; - Outlet = OriginOutlet; - useParams = useOriginParams; - useSearchParams = useOriginSearchParams; - useLocation = useOriginLocation; -} else { - Link = LinkSingle as any; - Outlet = OutletSingle; - useParams = useParamsSingle as typeof useParams; - useSearchParams = useSearchParamsSingle as any; - useLocation = useLocationSingle as typeof useLocation; -} - export { Link, Outlet, @@ -51,15 +27,17 @@ export { Title, Links, Scripts, + Data, Main, + history, } from '@ice/runtime'; <%- framework.imports %> -export { <% if (framework.exports) { -%> +export { <%- framework.exports %> -<% } -%> }; +<% } -%> export * from './types'; diff --git a/packages/ice/tests/fixtures/removeCode/expression.ts b/packages/ice/tests/fixtures/removeCode/expression.ts new file mode 100644 index 000000000..5c56a1367 --- /dev/null +++ b/packages/ice/tests/fixtures/removeCode/expression.ts @@ -0,0 +1,2 @@ +const a = {}; +a.test = 1; diff --git a/packages/ice/tests/preAnalyze.test.ts b/packages/ice/tests/preAnalyze.test.ts index 82862f1d1..b640ff22b 100644 --- a/packages/ice/tests/preAnalyze.test.ts +++ b/packages/ice/tests/preAnalyze.test.ts @@ -42,7 +42,8 @@ describe('resolveId', () => { describe('getImportPath', () => { it('import from relative path', () => { const filePath = getImportPath('./page.js', '/rootDir/test.ts', {}); - expect(filePath).toBe('/rootDir/page.js'); + // Compatible with test case in win32. + expect(filePath.replace(/^[A-Za-z]:/, '')).toBe('/rootDir/page.js'); }); it('import from alias', () => { const filePath = getImportPath('ice', '/rootDir/test.ts', { ice: '/rootDir/component.tsx'}); diff --git a/packages/ice/tests/preBundleCJSDeps.test.ts b/packages/ice/tests/preBundleCJSDeps.test.ts index 611e0db37..65a81db94 100644 --- a/packages/ice/tests/preBundleCJSDeps.test.ts +++ b/packages/ice/tests/preBundleCJSDeps.test.ts @@ -8,13 +8,14 @@ import { scanImports } from '../src/service/analyze'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const alias = { '@': path.join(__dirname, './fixtures/scan') }; const rootDir = path.join(__dirname, './fixtures/scan'); -const cacheDir = path.join(rootDir, '.ice'); +const cacheDir = path.join(rootDir, '.cache'); it('prebundle cjs deps', async () => { const deps = await scanImports([path.join(__dirname, './fixtures/scan/app.ts')], { alias, rootDir }); await preBundleCJSDeps({ depsInfo: deps, cacheDir, + alias, taskConfig: { mode: 'production' } }); diff --git a/packages/ice/tests/removeTopLevelCode.test.ts b/packages/ice/tests/removeTopLevelCode.test.ts index 49726f871..5ed015e20 100644 --- a/packages/ice/tests/removeTopLevelCode.test.ts +++ b/packages/ice/tests/removeTopLevelCode.test.ts @@ -85,4 +85,11 @@ describe('remove top level code', () => { const content = generate(ast).code; expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(`const a = 1;export default a;`); }); + + it('remove expression statement', () => { + const ast = parse(fs.readFileSync(path.join(__dirname, './fixtures/removeCode/expression.ts'), 'utf-8'), parserOptions); + traverse(ast, removeTopLevelCodePlugin(['default'])); + const content = generate(ast).code; + expect(content.replace(/\n/g, '').replace(/\s+/g, ' ')).toBe(''); + }); }) \ No newline at end of file diff --git a/packages/ice/tests/scan.test.ts b/packages/ice/tests/scan.test.ts index f721013e5..fbc07d5de 100644 --- a/packages/ice/tests/scan.test.ts +++ b/packages/ice/tests/scan.test.ts @@ -2,6 +2,7 @@ import { expect, it, describe } from 'vitest'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { scanImports } from '../src/service/analyze'; +import formatPath from '../src/utils/formatPath'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -12,17 +13,17 @@ describe('scan import', () => { it('basic scan', async () => { const deps = await scanImports([path.join(__dirname, './fixtures/scan/app.ts')], { alias, rootDir }); expect(deps['@ice/runtime'].name).toEqual('@ice/runtime'); - expect(/(@ice\/)?runtime\/package\.json/.test(deps['@ice/runtime'].pkgPath!)).toBeTruthy(); + expect(/(@ice\/)?runtime\/package\.json/.test(formatPath(deps['@ice/runtime'].pkgPath!))).toBeTruthy(); expect(deps['@ice/runtime/client'].name).toEqual('@ice/runtime/client'); - expect(/(@ice\/)?runtime\/package\.json/.test(deps['@ice/runtime/client'].pkgPath!)).toBeTruthy(); + expect(/(@ice\/)?runtime\/package\.json/.test(formatPath(deps['@ice/runtime/client'].pkgPath!))).toBeTruthy(); expect(deps.react.name).toEqual('react'); - expect(/react\/package\.json/.test(deps['react'].pkgPath!)).toBeTruthy(); + expect(/react\/package\.json/.test(formatPath(deps['react'].pkgPath!))).toBeTruthy(); }); it('scan with exclude', async () => { const deps = await scanImports([path.join(__dirname, './fixtures/scan/app.ts')], { alias, rootDir, exclude: ['@ice/runtime'] }); expect(deps.react.name).toEqual('react'); - expect(/react\/package\.json/.test(deps['react'].pkgPath!)).toBeTruthy(); + expect(/react\/package\.json/.test(formatPath(deps['react'].pkgPath!))).toBeTruthy(); expect(deps['@ice/runtime']).toBeUndefined(); }); diff --git a/packages/ice/tests/transformImport.test.ts b/packages/ice/tests/transformImport.test.ts index 553cfcbeb..ad57033f5 100644 --- a/packages/ice/tests/transformImport.test.ts +++ b/packages/ice/tests/transformImport.test.ts @@ -12,7 +12,7 @@ import { createUnplugin } from 'unplugin'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const alias = { '@': path.join(__dirname, './fixtures/scan') }; const rootDir = path.join(__dirname, './fixtures/scan'); -const cacheDir = path.join(rootDir, '.ice'); +const cacheDir = path.join(rootDir, '.cache'); const appEntry = path.join(__dirname, './fixtures/scan/app.ts'); const outdir = path.join(rootDir, 'build'); @@ -21,9 +21,10 @@ it('transform module import', async () => { const { metadata } = await preBundleCJSDeps({ depsInfo: deps, cacheDir, + alias, taskConfig: { mode: 'production' } }); - const transformImportPlugin = createUnplugin(() => transformImport(metadata)).esbuild; + const transformImportPlugin = createUnplugin(() => transformImport(metadata, path.join(outdir, 'server'))).esbuild; await esbuild.build({ entryPoints: [appEntry], outdir, @@ -33,8 +34,8 @@ it('transform module import', async () => { ], }); const buildContent = await fse.readFile(path.join(outdir, 'app.js')); - expect(buildContent.includes(path.join(rootDir, '.ice/deps/@ice_runtime_client.js'))).toBeTruthy(); - expect(buildContent.includes(path.join(rootDir, '.ice/deps/@ice_runtime.js'))).toBeTruthy(); + expect(buildContent.includes('../../.cache/deps/@ice_runtime_client.js')).toBeTruthy(); + expect(buildContent.includes('../../.cache/deps/@ice_runtime.js')).toBeTruthy(); }); afterAll(async () => { diff --git a/packages/jsx-runtime/build.config.ts b/packages/jsx-runtime/build.config.mts similarity index 100% rename from packages/jsx-runtime/build.config.ts rename to packages/jsx-runtime/build.config.mts diff --git a/packages/jsx-runtime/package.json b/packages/jsx-runtime/package.json index 031386fee..cfdd3e0c5 100644 --- a/packages/jsx-runtime/package.json +++ b/packages/jsx-runtime/package.json @@ -38,7 +38,7 @@ "style-unit": "^3.0.4" }, "devDependencies": { - "@ice/pkg": "^1.0.0", + "@ice/pkg": "1.1.2-0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "react": "^18.0.0", diff --git a/packages/plugin-fusion/src/index.ts b/packages/plugin-fusion/src/index.ts index c478f5a7a..473403734 100644 --- a/packages/plugin-fusion/src/index.ts +++ b/packages/plugin-fusion/src/index.ts @@ -27,6 +27,29 @@ function getVariablesPath({ return filePath; } +function importIcon(iconPath: string, cssPrefix: string) { + let entryFile = ''; + return { + name: 'transform-import-icon', + enforce: 'pre', + async transform(code: string, id: string, options: { isServer: boolean }) { + const { isServer } = options; + // Only import icon scss in client + if (!isServer) { + // Icon just import once. + if (!entryFile && !id.match(/node_modules/) && id.match(/[js|jsx|ts|tsx]$/)) { + entryFile = id; + } + if (id === entryFile) { + return `import '${iconPath}';\n${code}`; + } else if (id === iconPath) { + return `$css-prefix: '${cssPrefix}';\n${code}`; + } + } + }, + }; +} + const plugin: Plugin = (options = {}) => ({ name: '@ice/plugin-fusion', setup: ({ onGetConfig }) => { @@ -41,6 +64,15 @@ const plugin: Plugin = (options = {}) => ({ } if (theme || themePackage) { onGetConfig((config) => { + // Try to get icon.scss if exists. + const iconFile = getVariablesPath({ + packageName: themePackage, + filename: 'icons.scss', + silent: true, + }); + if (iconFile) { + config.transformPlugins = [...(config.transformPlugins || []), importIcon(iconFile, theme['css-prefix'] || 'next-')]; + } // Modify webpack config of scss rule for fusion theme. config.configureWebpack ??= []; config.configureWebpack.push((webpackConfig) => { @@ -57,29 +89,20 @@ const plugin: Plugin = (options = {}) => ({ return false; }); if (sassLoader) { - let additionalContent = ''; + const additionalContent = []; if (themePackage) { const themeFile = getVariablesPath({ packageName: themePackage, }); if (themeFile) { - additionalContent += `@import '${themePackage}/variables.scss'`; - } - // Try to get icon.scss if exists. - const iconFile = getVariablesPath({ - packageName: themePackage, - filename: 'icons.scss', - silent: true, - }); - if (iconFile) { - additionalContent += `@import '${themePackage}/icons.scss'`; + additionalContent.push(`@import '${themePackage}/variables.scss';`); } } let themeConfig = []; Object.keys(theme || {}).forEach((key) => { themeConfig.push(`$${key}: ${theme[key]};`); }); - additionalContent += themeConfig.join('\n'); + additionalContent.push(themeConfig.join('\n')); const loaderOptions = sassLoader.options || {}; sassLoader.options = { @@ -88,7 +111,7 @@ const plugin: Plugin = (options = {}) => ({ const originalContent = typeof loaderOptions.additionalData === 'function' ? loaderOptions.additionalData(content, loaderContext) : `${loaderOptions.additionalData || ''}${content}`; - return `${additionalContent}\n${originalContent}`; + return `${additionalContent.join('\n')}\n${originalContent}`; }, }; } diff --git a/packages/plugin-store/package.json b/packages/plugin-store/package.json index 3eb801350..b7a0b0ef9 100644 --- a/packages/plugin-store/package.json +++ b/packages/plugin-store/package.json @@ -15,10 +15,10 @@ "import": "./esm/runtime.js", "default": "./esm/runtime.js" }, - "./esm/runtime": { - "types": "./esm/runtime.d.ts", - "import": "./esm/runtime.js", - "default": "./esm/runtime.js" + "./api": { + "types": "./esm/api.d.ts", + "import": "./esm/api.js", + "default": "./esm/api.js" } }, "main": "./esm/index.js", diff --git a/packages/plugin-store/src/api.ts b/packages/plugin-store/src/api.ts new file mode 100644 index 000000000..215dfc296 --- /dev/null +++ b/packages/plugin-store/src/api.ts @@ -0,0 +1 @@ +export { createStore, createModel } from '@ice/store'; diff --git a/packages/plugin-store/src/index.ts b/packages/plugin-store/src/index.ts index 0e0edf62b..16efbb420 100644 --- a/packages/plugin-store/src/index.ts +++ b/packages/plugin-store/src/index.ts @@ -13,7 +13,7 @@ const ignoreStoreFilePatterns = ['**/models/**', storeFilePattern]; const plugin: Plugin = (options) => ({ name: '@ice/plugin-store', - setup: ({ onGetConfig, modifyUserConfig, context: { rootDir, userConfig } }) => { + setup: ({ onGetConfig, modifyUserConfig, generator, context: { rootDir, userConfig } }) => { const { disableResetPageState = false } = options || {}; const srcDir = path.join(rootDir, 'src'); const pageDir = path.join(srcDir, 'pages'); @@ -38,6 +38,13 @@ const plugin: Plugin = (options) => ({ ]; return config; }); + + // Export store api: createStore, createModel from `.ice/index.ts`. + generator.addExport({ + specifier: ['createStore', 'createModel'], + source: '@ice/plugin-store/api', + type: false, + }); }, runtime: path.join(path.dirname(fileURLToPath(import.meta.url)), 'runtime.js'), }); @@ -47,7 +54,7 @@ function exportStoreProviderPlugin({ pageDir, disableResetPageState }: { pageDir name: 'export-store-provider', enforce: 'post', transformInclude: (id) => { - return id.startsWith(pageDir) && !micromatch.isMatch(id, ignoreStoreFilePatterns); + return id.startsWith(pageDir.split(path.sep).join('/')) && !micromatch.isMatch(id, ignoreStoreFilePatterns); }, transform: async (source, id) => { const pageStorePath = getPageStorePath(id); diff --git a/packages/plugin-store/src/runtime.tsx b/packages/plugin-store/src/runtime.tsx index 9a9718d8f..e70dfb596 100644 --- a/packages/plugin-store/src/runtime.tsx +++ b/packages/plugin-store/src/runtime.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import type { RuntimePlugin, AppProvider, RouteWrapper } from '@ice/types'; -import { createStore, createModel } from '@ice/store'; import { PAGE_STORE_INITIAL_STATES, PAGE_STORE_PROVIDER } from './constants.js'; import appStore from '$store'; @@ -35,5 +34,4 @@ const runtime: RuntimePlugin = async ({ addWrapper, addProvider, useAppContext } addWrapper(StoreProviderWrapper, true); }; -export { createStore, createModel }; export default runtime; diff --git a/packages/rax-compat/build.config.ts b/packages/rax-compat/build.config.mts similarity index 100% rename from packages/rax-compat/build.config.ts rename to packages/rax-compat/build.config.mts diff --git a/packages/rax-compat/package.json b/packages/rax-compat/package.json index 0573ee0ca..f997da7d1 100644 --- a/packages/rax-compat/package.json +++ b/packages/rax-compat/package.json @@ -50,7 +50,7 @@ "create-react-class": "^15.7.0" }, "devDependencies": { - "@ice/pkg": "^1.0.0", + "@ice/pkg": "1.1.2-0", "@types/rax": "^1.0.8", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", diff --git a/packages/rax-compat/src/create-element.ts b/packages/rax-compat/src/create-element.ts index 43eb2d843..640d9747e 100644 --- a/packages/rax-compat/src/create-element.ts +++ b/packages/rax-compat/src/create-element.ts @@ -6,7 +6,7 @@ import type { RefObject, SyntheticEvent, } from 'react'; -import { createElement as _createElement, useEffect, useCallback, useRef, useState } from 'react'; +import { createElement as _createElement, useEffect, useCallback, useRef, useState, forwardRef as _forwardRef } from 'react'; import { cached, convertUnit } from 'style-unit'; import { observerElement } from './visibility'; import { isFunction, isObject, isNumber } from './type'; @@ -29,7 +29,7 @@ import { isFunction, isObject, isNumber } from './type'; const NON_DIMENSIONAL_REG = /opa|ntw|ne[ch]|ex(?:s|g|n|p|$)|^ord|zoo|grid|orp|ows|mnc|^columns$|bs|erim|onit/i; function createInputCompat(type: string) { - function InputCompat(props: any) { + function InputCompat(props: any, ref: RefObject) { const { value, onInput, ...rest } = props; const [v, setV] = useState(value); const onChange = useCallback((event: SyntheticEvent) => { @@ -39,14 +39,24 @@ function createInputCompat(type: string) { onInput && onInput(event.nativeEvent); }, [onInput]); + // Compat maxlength in rax-textinput, because maxlength is invalid props in web,it will be set attributes to element + // and react will Throw a warning in DEV. + // https://github.com/raxjs/rax-components/issues/459 + // https://github.com/raxjs/rax-components/blob/master/packages/rax-textinput/src/index.tsx#L142 + if (rest.maxlength) { + rest.maxLength = rest.maxlength; + delete rest.maxlength; + } + return _createElement(type, { ...rest, value: v, onChange, + ref, }); } - return InputCompat; + return _forwardRef(InputCompat); } /** @@ -81,12 +91,12 @@ export function createElement

{ const { current } = ref; - if (current != null) { - if (isFunction(handler)) { - observerElement(current as HTMLElement); - current.addEventListener(eventName, handler); - } + // Rax components will set custom ref by useImperativeHandle. + // So We should get eventTarget by _nativeNode. + // https://github.com/raxjs/rax-components/blob/master/packages/rax-textinput/src/index.tsx#L151 + if (current && isFunction(handler)) { + const eventTarget = current._nativeNode || current; + observerElement(eventTarget as HTMLElement); + eventTarget.addEventListener(eventName, handler); } return () => { const { current } = ref; if (current) { - current.removeEventListener(eventName, handler); + const eventTarget = current._nativeNode || current; + eventTarget.removeEventListener(eventName, handler); } }; }, [ref]); diff --git a/packages/rax-compat/tests/createElement.test.tsx b/packages/rax-compat/tests/createElement.test.tsx index 5b9ea82d3..0ba0e3afb 100644 --- a/packages/rax-compat/tests/createElement.test.tsx +++ b/packages/rax-compat/tests/createElement.test.tsx @@ -76,4 +76,19 @@ describe('createElement', () => { const node = wrapper.queryByTestId('valueTest'); expect(node.value).toBe(str); }); + + it('input set empty string to maxlength not be 0', () => { + const str = 'hello world'; + const wrapper = render(createElement( + 'input', + { + 'data-testid': 'maxlengthTest', + value: str, + maxlength: '' + }, + )); + + const node = wrapper.queryByTestId('valueTest'); + expect(node?.getAttribute('maxlength')).toBe(null); + }); }); diff --git a/packages/route-manifest/src/index.ts b/packages/route-manifest/src/index.ts index da8d2c051..d111057aa 100644 --- a/packages/route-manifest/src/index.ts +++ b/packages/route-manifest/src/index.ts @@ -2,7 +2,7 @@ import fs from 'fs'; import path from 'path'; import minimatch from 'minimatch'; -import { createRouteId, defineRoutes } from './routes.js'; +import { createRouteId, defineRoutes, normalizeSlashes } from './routes.js'; import type { RouteManifest, DefineRouteFunction, NestedRouteManifest, ConfigRoute } from './routes.js'; export type { @@ -122,7 +122,7 @@ function defineConventionalRoutes( // in order to escape the child route path is absolute path routeId.slice(parentRoutePath.length + (parentRoutePath ? 1 : 0)), ); - const routeFilePath = path.join('src', 'pages', files[routeId]); + const routeFilePath = normalizeSlashes(path.join('src', 'pages', files[routeId])); if (RegExp(`[^${validRouteChar.join(',')}]`).test(routePath)) { throw new Error(`invalid character in '${routeFilePath}'. Only support char: ${validRouteChar.join(', ')}`); } @@ -231,7 +231,7 @@ function visitFiles( if (stat.isDirectory()) { visitFiles(file, visitor, baseDir); } else if (stat.isFile()) { - visitor(path.relative(baseDir, file)); + visitor(normalizeSlashes(path.relative(baseDir, file))); } } } diff --git a/packages/runtime/package.json b/packages/runtime/package.json index c1f30d6a2..156a2e8a2 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -11,7 +11,9 @@ "./server": "./esm/index.server.js", "./jsx-runtime": "./esm/jsx-runtime.js", "./jsx-dev-runtime": "./esm/jsx-dev-runtime.js", - "./matchRoutes": "./esm/matchRoutes.js" + "./matchRoutes": "./esm/matchRoutes.js", + "./router": "./esm/router.js", + "./single-router": "./esm/single-router.js" }, "files": [ "esm", diff --git a/packages/runtime/src/AppRouter.tsx b/packages/runtime/src/AppRouter.tsx index 66cad0d3f..e360f4a72 100644 --- a/packages/runtime/src/AppRouter.tsx +++ b/packages/runtime/src/AppRouter.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import type { RouteObject } from 'react-router-dom'; import { Router, useRoutes } from 'react-router-dom'; -import { RouterSingle, useRoutesSingle } from './utils/history-single.js'; +import { Router as RouterSingle, useRoutes as useRoutesSingle } from './single-router.js'; import type { AppRouterProps } from './types.js'; const AppRouter: React.ComponentType = (props) => { diff --git a/packages/runtime/src/Document.tsx b/packages/runtime/src/Document.tsx index 5336a20ae..4228caf99 100644 --- a/packages/runtime/src/Document.tsx +++ b/packages/runtime/src/Document.tsx @@ -73,6 +73,10 @@ export function Scripts(props) { // Page assets need to be load before entry assets, so when call dynamic import won't cause duplicate js chunk loaded. const scripts = pageAssets.concat(entryAssets).filter(path => path.indexOf('.js') > -1); + if (assetsManifest.dataLoader) { + scripts.unshift(`${assetsManifest.publicPath}${assetsManifest.dataLoader}`); + } + const matchedIds = matches.map(match => match.route.id); const routePath = getCurrentRoutePath(matches); @@ -94,7 +98,10 @@ export function Scripts(props) { * disable hydration warning for CSR. * initial app data may not equal CSR result. */} -