├── .codesandbox └── ci.json ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jest.config.ts ├── package.json ├── packages ├── create-project │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── index.js │ ├── package.json │ ├── template-app │ │ ├── README.md │ │ ├── _gitignore │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ │ ├── $.mdx │ │ │ ├── _theme.tsx │ │ │ ├── dir │ │ │ │ ├── page2$.tsx │ │ │ │ └── page3 │ │ │ │ │ ├── box.module.css │ │ │ │ │ ├── box.tsx │ │ │ │ │ └── index$.mdx │ │ │ ├── page1$.tsx │ │ │ └── users │ │ │ │ ├── [userId] │ │ │ │ ├── index$.tsx │ │ │ │ └── posts │ │ │ │ │ └── [postId]$.tsx │ │ │ │ └── index$.tsx │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── template-lib-monorepo │ │ ├── README.md │ │ ├── _gitignore │ │ ├── package.json │ │ ├── packages │ │ │ ├── button │ │ │ │ ├── README.md │ │ │ │ ├── demos │ │ │ │ │ ├── demo1.tsx │ │ │ │ │ └── demo2.tsx │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── declare.d.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── style.module.css │ │ │ │ │ └── tsconfig.json │ │ │ │ └── tsconfig.json │ │ │ ├── card │ │ │ │ ├── README.md │ │ │ │ ├── demos │ │ │ │ │ ├── demo1.tsx │ │ │ │ │ └── demo2.tsx │ │ │ │ ├── package.json │ │ │ │ ├── src │ │ │ │ │ ├── declare.d.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── style.module.css │ │ │ │ │ └── tsconfig.json │ │ │ │ └── tsconfig.json │ │ │ └── demos │ │ │ │ ├── index.html │ │ │ │ ├── package.json │ │ │ │ ├── pages │ │ │ │ ├── $.tsx │ │ │ │ ├── 404$.tsx │ │ │ │ └── _theme.tsx │ │ │ │ ├── tsconfig.json │ │ │ │ └── vite.demos.ts │ │ ├── script │ │ │ └── copy.js │ │ └── tsconfig.json │ └── template-lib │ │ ├── README.md │ │ ├── _gitignore │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ ├── $.mdx │ │ ├── 404$.tsx │ │ └── _theme.tsx │ │ ├── src │ │ ├── Button │ │ │ ├── README.md │ │ │ ├── demos │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.tsx │ │ │ └── style.module.css │ │ ├── Card │ │ │ ├── README.md │ │ │ ├── demos │ │ │ │ ├── demo1.tsx │ │ │ │ └── demo2.tsx │ │ │ ├── index.tsx │ │ │ └── style.module.css │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts ├── examples │ └── intro │ │ ├── components │ │ ├── Component.tsx │ │ ├── Layout.tsx │ │ └── Model.tsx │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ ├── _app.tsx │ │ ├── index.tsx │ │ └── ruby │ │ │ └── [slug].tsx │ │ ├── styles │ │ └── main.css │ │ ├── tsconfig.json │ │ ├── vitext.config.ts │ │ └── windi.config.ts ├── playground │ ├── basic │ │ ├── __tests__ │ │ │ ├── async.spec.ts │ │ │ ├── basic.spec.ts │ │ │ ├── hmr.spec.ts │ │ │ └── navigation.spec.ts │ │ ├── components │ │ │ └── Component.tsx │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ │ ├── all │ │ │ │ └── [[...all]].tsx │ │ │ ├── developer │ │ │ │ └── [...developerId].tsx │ │ │ ├── index.tsx │ │ │ ├── page1.tsx │ │ │ └── users │ │ │ │ ├── [userId].tsx │ │ │ │ └── index.tsx │ │ ├── styles │ │ │ └── main.css │ │ ├── vitext.config.ts │ │ └── yarn-error.log │ ├── css │ │ ├── __tests__ │ │ │ └── basic.spec.ts │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ └── index.tsx │ │ ├── postcss.config.js │ │ ├── styles │ │ │ ├── main.css │ │ │ └── main.module.css │ │ ├── tailwind.config.js │ │ ├── vitext.config.ts │ │ └── yarn-error.log │ ├── custom-document-app │ │ ├── __tests__ │ │ │ └── basic.spec.ts │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ └── index.tsx │ │ ├── vitext.config.ts │ │ └── yarn-error.log │ ├── package.json │ ├── shims.d.ts │ ├── ssg-basic │ │ ├── __tests__ │ │ │ └── basic.spec.ts │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ │ ├── [slug].tsx │ │ │ └── users │ │ │ │ └── [slug].tsx │ │ ├── vitext.config.ts │ │ └── yarn-error.log │ ├── ssr-basic │ │ ├── __tests__ │ │ │ └── basic.spec.ts │ │ ├── declare.d.ts │ │ ├── index.html │ │ ├── package.json │ │ ├── pages │ │ │ └── index.tsx │ │ ├── vitext.config.ts │ │ └── yarn-error.log │ ├── testEnv.d.ts │ ├── testUtils.ts │ └── tsconfig.json └── vitext │ ├── .gitignore │ ├── .npmignore │ ├── bin │ └── vitext.js │ ├── package.json │ ├── react-shim.js │ ├── rollup.config.js │ ├── src │ ├── client │ │ ├── declarations.d.ts │ │ ├── main.tsx │ │ └── tsconfig.json │ ├── node │ │ ├── build.ts │ │ ├── cli.ts │ │ ├── components │ │ │ ├── Head.tsx │ │ │ ├── _app.tsx │ │ │ └── _document.tsx │ │ ├── constants.ts │ │ ├── middlewares │ │ │ └── page.ts │ │ ├── plugin.ts │ │ ├── preview.ts │ │ ├── proxy.ts │ │ ├── route │ │ │ ├── export.ts │ │ │ ├── fetch.ts │ │ │ ├── pages.ts │ │ │ └── render.tsx │ │ ├── server.ts │ │ ├── tsconfig.json │ │ ├── types.ts │ │ └── utils.ts │ └── react │ │ ├── dynamic.tsx │ │ ├── index.tsx │ │ ├── loadable.d.ts │ │ └── loadable.js │ ├── tsconfig.json │ └── yarn-error.log ├── prettier.config.js ├── scripts ├── jestEnv.js ├── jestGlobalSetup.js ├── jestGlobalTeardown.js ├── jestPerTestSetup.ts └── release.js ├── tsconfig.json ├── workspace.code-workspace └── yarn.lock /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/vitext"], 3 | "sandboxes": ["vanilla"], 4 | "node": "14" 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const { defineConfig } = require('eslint-define-config'); 3 | 4 | module.exports = defineConfig({ 5 | root: true, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:node/recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | '@typescript-eslint/explicit-function-return-type': 'off', 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | eqeqeq: ['warn', 'always', { null: 'never' }], 20 | 'no-debugger': ['error'], 21 | 'no-empty': ['warn', { allowEmptyCatch: true }], 22 | 'no-process-exit': 'off', 23 | 'no-useless-escape': 'off', 24 | 'prefer-const': [ 25 | 'warn', 26 | { 27 | destructuring: 'all', 28 | }, 29 | ], 30 | 31 | 'node/no-missing-import': [ 32 | 'error', 33 | { 34 | allowModules: ['types', 'estree', 'testUtils', 'stylus'], 35 | tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'], 36 | }, 37 | ], 38 | 'node/no-missing-require': [ 39 | 'error', 40 | { 41 | // for try-catching yarn pnp 42 | allowModules: ['pnpapi'], 43 | tryExtensions: ['.ts', '.js', '.jsx', '.tsx', '.d.ts'], 44 | }, 45 | ], 46 | 'node/no-restricted-require': [ 47 | 'error', 48 | Object.keys( 49 | require('./packages/vitext/package.json').devDependencies 50 | ).map((d) => ({ 51 | name: d, 52 | message: 53 | `devDependencies can only be imported using ESM syntax so ` + 54 | `that they are included in the rollup bundle. If you are trying to ` + 55 | `lazy load a dependency, use (await import('dependency')).default instead.`, 56 | })), 57 | ], 58 | 'node/no-extraneous-import': [ 59 | 'error', 60 | { 61 | allowModules: ['vite', 'less', 'sass'], 62 | }, 63 | ], 64 | 'node/no-extraneous-require': [ 65 | 'error', 66 | { 67 | allowModules: ['vite'], 68 | }, 69 | ], 70 | 'node/no-deprecated-api': 'off', 71 | 'node/no-unpublished-import': 'off', 72 | 'node/no-unpublished-require': 'off', 73 | 'node/no-unsupported-features/es-syntax': 'off', 74 | 75 | '@typescript-eslint/ban-ts-comment': 'off', // TODO: we should turn this on in a new PR 76 | '@typescript-eslint/ban-types': 'off', // TODO: we should turn this on in a new PR 77 | '@typescript-eslint/no-empty-function': [ 78 | 'error', 79 | { allow: ['arrowFunctions'] }, 80 | ], 81 | '@typescript-eslint/no-empty-interface': 'off', 82 | '@typescript-eslint/no-explicit-any': 'off', // maybe we should turn this on in a new PR 83 | '@typescript-eslint/no-extra-semi': 'off', // conflicts with prettier 84 | '@typescript-eslint/no-inferrable-types': 'off', 85 | '@typescript-eslint/no-non-null-assertion': 'off', // maybe we should turn this on in a new PR 86 | '@typescript-eslint/no-unused-vars': 'off', // maybe we should turn this on in a new PR 87 | '@typescript-eslint/no-var-requires': 'off', 88 | }, 89 | overrides: [ 90 | { 91 | files: ['packages/vitext/src/node/**'], 92 | rules: { 93 | 'no-console': ['error'], 94 | }, 95 | }, 96 | { 97 | files: ['packages/playground/**'], 98 | rules: { 99 | 'node/no-extraneous-import': 'off', 100 | 'node/no-extraneous-require': 'off', 101 | }, 102 | }, 103 | { 104 | files: ['packages/create-vitext/template-*/**'], 105 | rules: { 106 | 'node/no-missing-import': 'off', 107 | }, 108 | }, 109 | { 110 | files: ['*.js'], 111 | rules: { 112 | '@typescript-eslint/explicit-module-boundary-types': 'off', 113 | }, 114 | }, 115 | { 116 | files: ['*.d.ts'], 117 | rules: { 118 | '@typescript-eslint/triple-slash-reference': 'off', 119 | }, 120 | }, 121 | ], 122 | }); 123 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | env: 4 | NODE_OPTIONS: --max-old-space-size=6144 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | node_version: [12, 14, 16] 19 | include: 20 | - os: macos-latest 21 | node_version: 14 22 | - os: windows-latest 23 | node_version: 14 24 | fail-fast: false 25 | 26 | name: "Build&Test: node-${{ matrix.node_version }}, ${{ matrix.os }}" 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v2 30 | 31 | - name: Set node version to ${{ matrix.node_version }} 32 | uses: actions/setup-node@v2 33 | with: 34 | node-version: ${{ matrix.node_version }} 35 | 36 | - name: Get yarn cache directory 37 | id: yarn-cache 38 | run: echo "::set-output name=dir::$(yarn cache dir)" 39 | 40 | - name: Set dependencies cache 41 | uses: actions/cache@v2 42 | with: 43 | path: ${{ steps.yarn-cache.outputs.dir }} 44 | key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('yarn.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('yarn.lock') }} 47 | ${{ runner.os }}-${{ matrix.node_version }}- 48 | 49 | - name: Versions 50 | run: yarn versions 51 | 52 | - name: Install dependencies 53 | run: yarn install --frozen-lockfile 54 | 55 | - name: Build vitext 56 | run: yarn build 57 | 58 | - name: Test serve 59 | run: yarn test-serve --runInBand 60 | 61 | - name: Test build 62 | run: yarn test-build --runInBand 63 | 64 | lint: 65 | runs-on: ubuntu-latest 66 | name: "Lint: node-14, ubuntu-latest" 67 | steps: 68 | - uses: actions/checkout@v2 69 | with: 70 | fetch-depth: 0 71 | 72 | - name: Set node version to 14 73 | uses: actions/setup-node@v2 74 | with: 75 | node-version: 14 76 | 77 | - name: Set dependencies cache 78 | uses: actions/cache@v2 79 | with: 80 | path: ~/.cache/yarn 81 | key: lint-dependencies-${{ hashFiles('yarn.lock') }} 82 | restore-keys: | 83 | lint-dependencies-${{ hashFiles('yarn.lock') }} 84 | lint-dependencies- 85 | 86 | - name: Prepare 87 | run: | 88 | yarn install --frozen-lockfile 89 | 90 | - name: Lint 91 | run: yarn lint 92 | 93 | - name: Checkout 94 | uses: actions/checkout@v2 95 | - name: Semantic Release 96 | uses: cycjimmy/semantic-release-action@v2 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 100 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release npm package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "14.x" 17 | - run: yarn install --frozen-lockfile 18 | - run: yarn build 19 | - run: yarn workspace vitext run semantic-release 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | dist 4 | tmp 5 | temp 6 | .vscode 7 | .yarn 8 | workspace.code-workspace 9 | TODO.md 10 | 11 | packages/vitext/react.d.ts 12 | packages/vitext/src/node/error.ts 13 | .vimspector.json 14 | core -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## pnpm 4 | 5 | This project uses pnpm to manage monorepo. You should run `pnpm i` after cloning this repo. 6 | 7 | ## Tests 8 | 9 | The test setup is copied from [vite](https://github.com/vitejs/vite/blob/f6b58a0f535b1c26f9c1dfda74c28c685402c3c9/jest.config.js#L1). Fixtures and test cases is under `packages/playground`. 10 | 11 | > There must be no more than one `.spec.ts` file for each fixture. Otherwise it will cause Error like `Error: EEXIST: file already exists, mkdir '/home/csr/vitext/temp/basic'`. There is some flaws with current test setup (which is copied from vite's repo). 12 | 13 | ## Run playgrounds 14 | 15 | ```sh 16 | cd packages/playground/basic/ # or other playgrounds 17 | npm run dev 18 | ``` 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 M. Bagher Abiat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vitext ⚡🚀 2 | 3 | [![Discord](https://img.shields.io/discord/815937377888632913.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/Rhg9cEghMF) 4 | 5 | > The Next.js like React framework for better User & Developer experience 6 | 7 | - 💡 Instant Server Start 8 | - 💥 Suspense support 9 | - ⚫ Next.js like API 10 | - 📦 Optimized Build 11 | - 💎 Build & Export on fly 12 | - 🚀 Lightning SSG/SSR 13 | - ⚡ Fast HMR 14 | - 🔑 Vite & Rollup Compatible 15 | 16 | https://user-images.githubusercontent.com/37929992/128530290-41165a31-29a5-4108-825b-843a09059deb.mp4 17 | ``` 18 | npm install vitext 19 | ``` 20 | 21 | Vitext (Vite + Next) is a lightning fast SSG/SSR tool that lets you develop better and quicker front-end apps. It consists of these major parts: 22 | 23 | ### 💡 Instant Server Start 24 | The development server uses native ES modules, So you're going to have your React app server-rendered and client rendered very fast, under a half a second for me. 25 | 26 | ### 💥 Suspense support 27 | Vitext supports React Suspense & Lazy out of the box. 28 | ```ts 29 | import { lazy, Suspense } from 'react'; 30 | 31 | const Component = lazy(() => import('../components/Component')); 32 | const Loading = () =>

Loading the Component

; 33 | 34 | const App = () => { 35 | return ( 36 | }> 37 | 38 | 39 | ); 40 | }; 41 | ``` 42 | 43 | ### ⚫ Next.js like API 44 | If you're coming from a Next.js background, everything will work the same way for you. Vitext has a similar API design to Next.js. 45 | ```ts 46 | // pages/Page/[id].jsx 47 | const Page = ({ id }) => { 48 | return
{id}
; 49 | }; 50 | 51 | // build time + request time (SSG/SSR/ISR) 52 | export function getProps({ req, res, query, params }) { 53 | // props for `Page` component 54 | return { props: { id: params.id } }; 55 | } 56 | 57 | // build time (SSG) 58 | export async function getPaths() { 59 | // an array of { params: ... }, which every `params` goes to `getProps` 60 | return { 61 | paths: [{ id: 1 }], 62 | }; 63 | } 64 | 65 | export default IndexPage; 66 | 67 | ``` 68 | > `getPaths` & `getProps` are optional. If `getPaths`' running got done, then every `paths` item is going to be passed to a `getProps` function, And when the user requests for the specific page, they're going to receive the exported html (SSG). But if `getPaths` wasn't done or there's no exported html page for the user's request, then the `getProps` is going to get called with the request url's params (SSR). 69 | ### 📦 Optimized Build 70 | Vitext uses Vite's building and bundling approach, So it bundles your code in a fast and optimized way. 71 | 72 | ### 💎 Build & Export on fly 73 | You don't need to wait for HTML exports of your app because Vitext exports pages to HTML simultaneously while serving your app, So no `next export`. 74 | 75 | ### 🚀 Lightning SSR/SSG 76 | ES modules, Fast compiles and Web workers empower the Vitext SSR/SSG strategy, so you'll have an astonishingly fast SSR/SSG. 77 | 78 | ### ⚡ Fast HMR 79 | Vitext uses [@vitejs/plugin-react-refresh](https://github.com/vitejs/vite/tree/main/packages/plugin-react-refresh) under the hood, So you have a fast HMR right here. 80 | 81 | ### 🔑 Vite & Rollup Compatible 82 | We can call Vitext a superset of Vite; It means that Vitext supports everything Vite supports with `vitext.config.js`. 83 | ```ts 84 | // exact Vite's config API 85 | export default { 86 | plugins: [...], 87 | optimizeDeps: {...}, 88 | ... 89 | }; 90 | ``` 91 | ## Examples 92 | You can checkout [packages/examples](https://github.com/Aslemammad/vitext/tree/master/packages/examples) directory to see examples that have been implemented using vitext. 93 | 94 | ## Contribution 95 | 96 | We're in the early stages now, So we need your help on Vitext; please try things out, recommend new features, and issue stuff. You can also check out the issues to see if you can work on some. 97 | 98 | ## License 99 | 100 | MIT 101 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | testMatch: process.env.VITE_TEST_BUILD 5 | ? ['**/playground/**/*.spec.[jt]s?(x)'] 6 | : ['**/*.spec.[jt]s?(x)'], 7 | transform: { 8 | '^.+\\.(t|j)sx?$': [ 9 | 'esbuild-jest', 10 | { 11 | sourcemap: true, 12 | loaders: { 13 | '.spec.ts': 'tsx', 14 | }, 15 | }, 16 | ], 17 | }, 18 | testTimeout: process.env.CI ? 30000 : 10000, 19 | globalSetup: './scripts/jestGlobalSetup.js', 20 | globalTeardown: './scripts/jestGlobalTeardown.js', 21 | testEnvironment: './scripts/jestEnv.js', 22 | setupFilesAfterEnv: ['./scripts/jestPerTestSetup.ts'], 23 | watchPathIgnorePatterns: ['/temp'], 24 | moduleNameMapper: { 25 | testUtils: '/packages/playground/testUtils.ts', 26 | }, 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "version": "0.0.2", 4 | "name": "vitext-monorepo", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">=12.0.0" 9 | }, 10 | "scripts": { 11 | "test": "run-s test-serve test-build", 12 | "test-serve": "cross-env VITEXT_TEST=1 jest", 13 | "test-build": "cross-env VITEXT_TEST=1 VITE_TEST_BUILD=1 jest", 14 | "lint": "eslint packages/*/{src,types}/**", 15 | "build": "yarn workspace vitext build", 16 | "postinstall": "" 17 | }, 18 | "devDependencies": { 19 | "@jest/types": "^26.6.2", 20 | "@trivago/prettier-plugin-sort-imports": "^2.0.2", 21 | "@types/jest": "^26.0.19", 22 | "@types/minimist": "^1.2.1", 23 | "@types/node": "^14.14.34", 24 | "@typescript-eslint/eslint-plugin": "^4.28.5", 25 | "@typescript-eslint/parser": "^4.28.5", 26 | "cross-env": "^7.0.3", 27 | "esbuild-jest": "^0.5.0", 28 | "eslint": "^7.32.0", 29 | "eslint-define-config": "^1.0.9", 30 | "eslint-plugin-node": "^11.1.0", 31 | "fs-extra": "^9.1.0", 32 | "jest": "^26.6.3", 33 | "lint-staged": "^11.0.0", 34 | "minimist": "^1.2.5", 35 | "npm-run-all": "^4.1.5", 36 | "playwright-chromium": "~1.9.2", 37 | "prettier": "^2.3.0", 38 | "sirv": "^1.0.10", 39 | "slash": "^3.0.0", 40 | "ts-jest": "^26.4.4", 41 | "ts-node": "^10.0.0", 42 | "typescript": "^4.3.4", 43 | "vite": "^2.3.6", 44 | "yorkie": "^2.0.0" 45 | }, 46 | "workspaces": { 47 | "packages": [ 48 | "packages/*", 49 | "packages/playground/*", 50 | "packages/examples/*" 51 | ] 52 | }, 53 | "gitHooks": { 54 | "pre-commit": "lint-staged" 55 | }, 56 | "lint-staged": { 57 | "*.js,*.jsx": [ 58 | "prettier --write" 59 | ], 60 | "*.ts,*.tsx": [ 61 | "eslint", 62 | "prettier --parser=typescript --write" 63 | ], 64 | "*.html": [ 65 | "prettier --write" 66 | ] 67 | }, 68 | "dependencies": {} 69 | } 70 | -------------------------------------------------------------------------------- /packages/create-project/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/create-project/.npmignore: -------------------------------------------------------------------------------- 1 | dist 2 | docs-dist 3 | lib 4 | node_modules 5 | -------------------------------------------------------------------------------- /packages/create-project/README.md: -------------------------------------------------------------------------------- 1 | # create-vite-pages 2 | 3 | Create a [vite-pages](https://github.com/vitejs/vitext) project. 4 | 5 | ``` 6 | npm init vite-pages [new-directory-name] --template [app|lib|lib-monorepo] 7 | ``` 8 | 9 | For example: 10 | 11 | ``` 12 | npm init vite-pages app-demo --template app 13 | npm init vite-pages library-demo --template lib 14 | npm init vite-pages library-monorepo-demo --template lib-monorepo 15 | ``` 16 | 17 | After project is initialized, you should read the `README.md` of it. 18 | -------------------------------------------------------------------------------- /packages/create-project/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node; 2 | 3 | const path = require('path') 4 | const fs = require('fs-extra') 5 | const argv = require('minimist')(process.argv.slice(2)) 6 | 7 | async function init() { 8 | const targetDir = argv._[0] || '.' 9 | const cwd = process.cwd() 10 | const root = path.join(cwd, targetDir) 11 | const renameFiles = { 12 | _gitignore: '.gitignore', 13 | } 14 | console.log(`Scaffolding project in ${root}...`) 15 | 16 | await fs.ensureDir(root) 17 | const existing = await fs.readdir(root) 18 | if (existing.length) { 19 | console.error(`Error: target directory is not empty.`) 20 | process.exit(1) 21 | } 22 | 23 | const templateDir = path.join( 24 | __dirname, 25 | `template-${argv.t || argv.template || 'app'}` 26 | ) 27 | const write = async (file, content) => { 28 | const targetPath = renameFiles[file] 29 | ? path.join(root, renameFiles[file]) 30 | : path.join(root, file) 31 | if (content) { 32 | await fs.writeFile(targetPath, content) 33 | } else { 34 | await fs.copy(path.join(templateDir, file), targetPath) 35 | } 36 | } 37 | 38 | const files = await fs.readdir(templateDir) 39 | for (const file of files.filter((f) => f !== 'package.json')) { 40 | await write(file) 41 | } 42 | 43 | const pkg = require(path.join(templateDir, `package.json`)) 44 | removeWorkspace(pkg) 45 | pkg.name = path.basename(root) 46 | await write('package.json', JSON.stringify(pkg, null, 2)) 47 | 48 | console.log(`\nDone. Now run:\n`) 49 | if (root !== cwd) { 50 | console.log(` cd ${path.relative(cwd, root)}`) 51 | } 52 | console.log(` npm install (or \`yarn\`)`) 53 | console.log(` npm run dev (or \`yarn dev\`)`) 54 | console.log() 55 | } 56 | 57 | init().catch((e) => { 58 | console.error(e) 59 | }) 60 | 61 | function removeWorkspace(pkg) { 62 | rm(pkg.dependencies) 63 | rm(pkg.devDependencies) 64 | function rm(deps) { 65 | if (!deps) return 66 | Object.keys(deps).forEach((k) => { 67 | if (deps[k].startsWith('workspace:')) { 68 | deps[k] = deps[k].slice('workspace:'.length) 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/create-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-vite-pages", 3 | "version": "3.0.4", 4 | "bin": { 5 | "create-vite-pages": "index.js", 6 | "cvp": "index.js" 7 | }, 8 | "dependencies": { 9 | "fs-extra": "^9.1.0", 10 | "minimist": "^1.2.5" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/create-project/template-app/README.md: -------------------------------------------------------------------------------- 1 | ## vite-pages app starter 2 | 3 | This is a demo project for [vitext](https://github.com/vitejs/vitext). 4 | This project demonstrate how to develop a React app using [vitext](https://github.com/vitejs/vitext) as framework. 5 | 6 | # How to use 7 | 8 | `npm install` or `yarn` 9 | 10 | `npm run dev` You can now play with the local develop envirenment. 11 | 12 | Edit `pages/page1$.tsx` or other souce files, the playground will inflect your change instantly. 13 | 14 | `npm run build` The app are built and served. 15 | 16 | `npm run ssr` The app are built into a static site (Static-Site Generation) and served. 17 | 18 | --- 19 | 20 | Checkout [vitext](https://github.com/vitejs/vitext) for more info. 21 | -------------------------------------------------------------------------------- /packages/create-project/template-app/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local -------------------------------------------------------------------------------- /packages/create-project/template-app/declare.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' 2 | -------------------------------------------------------------------------------- /packages/create-project/template-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 📘Vite Pages 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/create-project/template-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "1.0.2", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite serve", 7 | "build": "rm -rf dist && vite build --outDir dist && serve -s dist", 8 | "ssr": "rm -rf dist && vite-pages ssr && serve dist" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "react": "^17.0.1", 15 | "react-dom": "^17.0.1", 16 | "react-router-dom": "^5.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^14.14.37", 20 | "@types/react": "^17.0.3", 21 | "@types/react-router-dom": "^5.1.7", 22 | "@vitejs/plugin-react-refresh": "^1.3.1", 23 | "serve": "^11.3.2", 24 | "vite": "^2.0.5", 25 | "vite-plugin-mdx": "^3.3.1", 26 | "@mdx-js/mdx": "^1.6.22", 27 | "@mdx-js/react": "^1.6.22", 28 | "vitext": "latest", 29 | "vite-pages-theme-basic": "latest" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/$.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Index Page Title 3 | sort: 0 4 | --- 5 | 6 | import README from '../README.md' 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/_theme.tsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from 'vite-pages-theme-basic' 2 | 3 | export default createTheme({ 4 | topNavs: [ 5 | { text: 'index', path: '/' }, 6 | { text: 'Vite', href: 'https://github.com/vitejs/vite' }, 7 | { 8 | text: 'Vite Pages', 9 | href: 'https://github.com/vitejs/vitext', 10 | }, 11 | ], 12 | logo: 'Vite Pages', 13 | }) 14 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/dir/page2$.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title page2 title 3 | * @sort 2 4 | */ 5 | 6 | import React from 'react' 7 | 8 | const Page = () => { 9 | return

page 2. This is a page inside a dir.

10 | } 11 | 12 | export default Page 13 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/dir/page3/box.module.css: -------------------------------------------------------------------------------- 1 | .box { 2 | border: 1px solid red; 3 | } 4 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/dir/page3/box.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import s from './box.module.css' 3 | 4 | const Box = () => { 5 | return
React Box
6 | } 7 | 8 | export default Box 9 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/dir/page3/index$.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: page3 title 3 | sort: 3 4 | --- 5 | 6 | import Box from './box' 7 | 8 | # Markdown: Syntax 9 | 10 | This is page3. This is a complicated [mdx](https://mdxjs.com/) page. 11 | 12 | ## Use React Component 13 | 14 | 15 | 16 | ## Test code block with syntax highlighing 17 | 18 | ```tsx 19 | export function createTheme({ sideMenuData }: Option = {}): ICreateTheme { 20 | return (pages) => { 21 | // theme can create menu from pages data 22 | const actualMenuData = sideMenuData || defaultMenu(pages) 23 | return { 24 | loaded(pageData) { 25 | const Component = pageData.default 26 | return ( 27 | 28 | 29 | 30 | ) 31 | }, 32 | } 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/page1$.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title page1 title 3 | * @sort 1 4 | */ 5 | 6 | import React from 'react' 7 | 8 | const Page = () => { 9 | return

This is page1. This is a page defined with React component.

10 | } 11 | 12 | export default Page 13 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/users/[userId]/index$.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title dynamic route page 3 | */ 4 | 5 | import React from 'react' 6 | import { useParams } from 'react-router-dom' 7 | 8 | const IndexPage = () => { 9 | const { userId } = useParams<{ userId: string }>() 10 | return ( 11 |
12 |
User Main Page
13 |
userId: {userId}
14 |
15 | ) 16 | } 17 | 18 | export default IndexPage 19 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/users/[userId]/posts/[postId]$.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title dynamic route page 3 | */ 4 | 5 | import React from 'react' 6 | import { useParams } from 'react-router-dom' 7 | 8 | const IndexPage = () => { 9 | const { userId, postId } = useParams<{ userId: string; postId: string }>() 10 | return ( 11 |
12 |
User Post Page
13 |
userId: {userId}
14 |
postId: {postId}
15 |
16 | ) 17 | } 18 | 19 | export default IndexPage 20 | -------------------------------------------------------------------------------- /packages/create-project/template-app/pages/users/index$.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title Users 3 | */ 4 | 5 | import React from 'react' 6 | import { Link } from 'react-router-dom' 7 | 8 | const Page = () => { 9 | return ( 10 |
11 | Test dynamic routes:
12 | /users/aaa 13 |
14 | /users/bbb 15 |
16 | /users/aaa/posts/111 17 |
18 | /users/bbb/posts/222 19 |
20 | ) 21 | } 22 | 23 | export default Page 24 | -------------------------------------------------------------------------------- /packages/create-project/template-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "baseUrl": ".", 5 | "jsx": "react", 6 | "lib": ["ES2015"], 7 | "moduleResolution": "Node", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/create-project/template-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import * as path from 'path' 3 | import reactRefresh from '@vitejs/plugin-react-refresh' 4 | import mdx from 'vite-plugin-mdx' 5 | import pages from 'vitext' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | reactRefresh(), 10 | mdx(), 11 | pages({ 12 | pagesDir: path.join(__dirname, 'pages'), 13 | }), 14 | ], 15 | }) 16 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/README.md: -------------------------------------------------------------------------------- 1 | ## vite-pages library-monorepo starter 2 | 3 | This is a demo project for [vitext](https://github.com/vitejs/vitext). 4 | This project demonstrate how to develop libraries **in monorepo** using vite as your local develop envirenment. 5 | 6 | # How to use 7 | 8 | `yarn` (require yarn 1.x) 9 | 10 | `cd packages/demos` 11 | 12 | `yarn dev` You can play with demos of your packages in local develop envirenment. 13 | 14 | Edit `packages/button/src/index.tsx` or other souce files, the demos will inflect your change instantly. 15 | Edit `packages/button/demos/demo1.tsx` or other demo files, the demos will inflect your change instantly. 16 | 17 | `yarn build` The demos are built and served. 18 | 19 | `npm run ssr` The app are built into a static site (Static-Site Generation) and served. 20 | 21 | --- 22 | 23 | Checkout [vitext](https://github.com/vitejs/vitext) for more info. 24 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | docs-dist 4 | lib 5 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-monorepo-root", 3 | "version": "0.0.2", 4 | "private": true, 5 | "scripts": { 6 | "dev": "cd packages/demos && npm run dev" 7 | }, 8 | "workspaces": [ 9 | "packages/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/packages/button/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Button title 3 | --- 4 | 5 | # Button 6 | 7 | This is a **markdown** description of the `Button` component. 8 | 9 | This is a composed page with data from three files: 10 | 11 | - `README.md` 12 | - `demos/demo1.tsx` 13 | - `demos/demo2.tsx` 14 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/packages/button/demos/demo1.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title Button Demo1 Title 3 | * @description Button demo1 description 4 | */ 5 | 6 | import React from 'react' 7 | import Button from 'my-button' 8 | 9 | const Demo1 = () => { 10 | return 11 | } 12 | 13 | export default Demo1 14 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/packages/button/demos/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title Button Demo2 Title 3 | * @description Button demo2 description 4 | */ 5 | 6 | import React from 'react' 7 | import Button from 'my-button' 8 | 9 | const Demo2 = () => { 10 | return 11 | } 12 | 13 | export default Demo2 14 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/packages/button/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-button", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rm -rf ./lib && tsc -p src && node ../../script/copy.js" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/react": "^17.0.3", 13 | "react": "^17.0.1", 14 | "typescript": "^4.2.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/packages/button/src/declare.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' 2 | -------------------------------------------------------------------------------- /packages/create-project/template-lib-monorepo/packages/button/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import s from './style.module.css' 3 | 4 | interface Props 5 | extends React.DetailedHTMLProps< 6 | React.ButtonHTMLAttributes, 7 | HTMLButtonElement 8 | > {} 9 | 10 | const Button = ({ className, ...props }: Props) => { 11 | return ( 12 | 11 | } 12 | 13 | export default Demo1 14 | -------------------------------------------------------------------------------- /packages/create-project/template-lib/src/Button/demos/demo2.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @title Button Demo2 Title 3 | * @description Button demo2 description 4 | */ 5 | 6 | import React from 'react' 7 | import { Button } from 'my-lib' 8 | 9 | const Demo2 = () => { 10 | return 11 | } 12 | 13 | export default Demo2 14 | -------------------------------------------------------------------------------- /packages/create-project/template-lib/src/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import s from './style.module.css' 3 | 4 | interface Props 5 | extends React.DetailedHTMLProps< 6 | React.ButtonHTMLAttributes, 7 | HTMLButtonElement 8 | > {} 9 | 10 | const Button = ({ className, ...props }: Props) => { 11 | return ( 12 | 52 | return 53 | 59 | 60 | 61 |
62 | }> 63 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | ); 80 | }; 81 | 82 | export default IndexPage; 83 | -------------------------------------------------------------------------------- /packages/examples/intro/pages/ruby/[slug].tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls, Stage } from '@react-three/drei'; 2 | import { Canvas, PrimitiveProps } from '@react-three/fiber'; 3 | import { ComponentType, lazy, Suspense, useRef } from 'react'; 4 | 5 | const Model: ComponentType = lazy( 6 | () => import('../../components/Model') 7 | ); 8 | 9 | const Loading = () =>

Loading the Ruby 💎

; 10 | 11 | const Ruby = ({ id }) => { 12 | const ref = useRef(); 13 | 14 | return ( 15 | }> 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default function ({ slug }) { 34 | // const 35 | const Rubies = []; 36 | 37 | for (let i = 0; i < (Math.abs(Number(slug)) || 1); i++) { 38 | Rubies.push(); 39 | } 40 | return ( 41 | <> 42 |

Dynamicly loaded using React.lazy & Suspense

43 | 44 |
{...Rubies}
45 | 46 | ); 47 | } 48 | 49 | export function getProps({ params }) { 50 | return { props: params }; 51 | } 52 | -------------------------------------------------------------------------------- /packages/examples/intro/styles/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100vh; 3 | } 4 | #root { 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /packages/examples/intro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["."], 3 | "exclude": ["**/dist/**"], 4 | "compilerOptions": { 5 | "target": "es2019", 6 | "outDir": "dist", 7 | "allowJs": true, 8 | "module":"esnext", 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "baseUrl": ".", 12 | "jsx": "react-jsx", 13 | "lib": ["ESNext", "ES2020.String", "ES2020", "DOM"], 14 | "types": ["vitext/client", "jest", "node", "vitext"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/examples/intro/vitext.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig } from 'vite'; 2 | import WindiCSS from 'vite-plugin-windicss'; 3 | 4 | import pkg from './package.json'; 5 | 6 | export default { 7 | plugins: [ 8 | WindiCSS({ 9 | scan: { 10 | include: ['index.html'], 11 | dirs: ['pages', 'components'], 12 | }, 13 | }).map((p) => ({ ...p, enforce: 'pre' })), 14 | ], 15 | optimizeDeps: { 16 | include: Object.keys(pkg.dependencies).filter((id) => id !== 'vitext') as [ 17 | 18 | ], 19 | }, 20 | build: { 21 | watch: null, 22 | }, 23 | } as UserConfig; 24 | -------------------------------------------------------------------------------- /packages/examples/intro/windi.config.ts: -------------------------------------------------------------------------------- 1 | // windi.config.js 2 | import { defineConfig } from 'windicss/helpers'; 3 | 4 | export default defineConfig({ 5 | extract: { 6 | include: ['pages/**/*.{html,vue,jsx,tsx,svelte}', "components/**/*.{html,vue,jsx,tsx,svelte}"], 7 | }, 8 | theme: { 9 | extend: { 10 | colors: { 11 | black: '#181a1b', 12 | }, 13 | }, 14 | 15 | fontSize: { 16 | '4xs': ['0.375rem'], 17 | '3xs': ['0.5rem'], 18 | '2xs': ['0.625rem'], 19 | xs: ['0.75rem'], 20 | sm: ['0.875rem'], 21 | base: ['1rem'], 22 | lg: ['1.125rem'], 23 | xl: ['1.25rem'], 24 | '2xl': ['1.5rem'], 25 | '3xl': ['1.875rem'], 26 | '4xl': ['2.25rem'], 27 | '5xl': ['3rem'], 28 | '6xl': ['3.75rem'], 29 | '7xl': ['4.5rem'], 30 | '8xl': ['6rem'], 31 | '9xl': ['8rem'], 32 | '10xl': ['10rem'], 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /packages/playground/basic/__tests__/async.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated } from '../../testUtils'; 2 | 3 | test('dynamic component with server rendering', async () => { 4 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 5 | untilUpdated(() => page.textContent('#dynamic-server-test'), 'loaded'); 6 | untilUpdated(() => page.textContent('#dynamic-server-test'), 'loading'); 7 | untilUpdated(() => page.textContent('#dynamic-server-test'), 'loaded'); 8 | }); 9 | 10 | test('dynamic component with no server rendering', async () => { 11 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 12 | untilUpdated( 13 | () => page.textContent('#dynamic-no-server-test'), 14 | 'loading' 15 | ); 16 | untilUpdated( 17 | () => page.textContent('#dynamic-no-server-test'), 18 | 'loaded' 19 | ); 20 | }); 21 | 22 | test('suspense support', async () => { 23 | untilUpdated(() => page.textContent('#test'), 'IndexPage'); 24 | untilUpdated(() => page.textContent('#suspense-test'), 'loading'); 25 | untilUpdated(() => page.textContent('#suspense-test'), 'loaded'); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/playground/basic/__tests__/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated } from '../../testUtils'; 2 | 3 | test('should render pages', async () => { 4 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 5 | const element = await page.$('#test'); 6 | 7 | expect(await element.textContent()).toBe('IndexPage'); 8 | }); 9 | 10 | test('Helmet should work', async () => { 11 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 12 | const title = await page.title(); 13 | expect(title).toBe('Hello World'); 14 | }); 15 | 16 | test('ssr should work', async () => { 17 | await untilUpdated( 18 | () => page.textContent('#hydration-test'), 19 | 'server-rendered' 20 | ); 21 | }); 22 | 23 | test('Hydration should work', async () => { 24 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 25 | await untilUpdated(() => page.textContent('#hydration-test'), 'hydrated'); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/playground/basic/__tests__/hmr.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated, editFile, isBuild } from '../../testUtils'; 2 | 3 | globalThis.test = !isBuild ? test : test.skip; 4 | 5 | test('Editing a page', async () => { 6 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 7 | editFile('pages/index.tsx', (code) => 8 | code.replace( 9 | '
IndexPage
', 10 | '
IndexPage hmr
' 11 | ) 12 | ); 13 | await untilUpdated(() => page.textContent('#hmr-test-page'), 'IndexPage hmr'); 14 | }); 15 | 16 | test('Editing a component', async () => { 17 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 18 | editFile('components/Component.tsx', (code) => 19 | code.replace('<>loaded', '<>loaded hmr') 20 | ); 21 | await untilUpdated( 22 | () => page.textContent('#hmr-test-component'), 23 | 'loaded hmr' 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/playground/basic/__tests__/navigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated } from '../../testUtils'; 2 | 3 | test('should navigate correctly index', async () => { 4 | await page.goto(viteTestUrl + '/page1') 5 | await untilUpdated(() => page.textContent('#test'), 'Page1'); 6 | }); 7 | 8 | test('should navigate correctly [[...slug]]', async () => { 9 | await page.goto(viteTestUrl + '/all') 10 | await untilUpdated(() => page.textContent('#test'), 'all'); 11 | }); 12 | 13 | test('should navigate correctly [[...slug]] 2', async () => { 14 | await page.goto(viteTestUrl + '/all/test') 15 | await untilUpdated(() => page.textContent('#test'), 'all'); 16 | }); 17 | 18 | test('should navigate correctly [...slug]', async () => { 19 | await page.goto(viteTestUrl + '/developer/test') 20 | await untilUpdated(() => page.textContent('#test'), 'developerId'); 21 | }); 22 | 23 | test('should navigate correctly index 2', async () => { 24 | await page.goto(viteTestUrl + '/users') 25 | await untilUpdated(() => page.textContent('#test'), 'usersIndex'); 26 | }); 27 | 28 | test('should navigate correctly [slug]', async () => { 29 | await page.goto(viteTestUrl + '/users/test') 30 | await untilUpdated(() => page.textContent('#test'), 'userId'); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/playground/basic/components/Component.tsx: -------------------------------------------------------------------------------- 1 | const Component = () => { 2 | return <>loaded; 3 | }; 4 | 5 | export default Component; 6 | -------------------------------------------------------------------------------- /packages/playground/basic/declare.d.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | declare module '*.module.css'; 4 | 5 | declare module 'react' { 6 | interface SuspenseProps { 7 | children?: ReactNode | undefined; 8 | progressive?: boolean; 9 | server?: boolean; 10 | 11 | /** A fallback react tree to show when a Suspense child (like React.lazy) suspends */ 12 | fallback: NonNullable | null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/playground/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/playground/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "private": true, 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vitext dev", 8 | "build": "vitext build" 9 | }, 10 | "dependencies": { 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-router-dom": "^5.2.0", 14 | "vite": "^2.3.6", 15 | "vitext": "latest" 16 | }, 17 | "devDependencies": { 18 | "@babel/helper-compilation-targets": "^7.13.16", 19 | "@mdx-js/mdx": "^1.6.22", 20 | "@types/react": "^17.0.3", 21 | "@types/react-router-dom": "^5.1.7", 22 | "@vitejs/plugin-react-refresh": "^1.3.1", 23 | "react-async-ssr": "^0.7.2", 24 | "react-lazy-ssr": "^0.2.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/playground/basic/pages/all/[[...all]].tsx: -------------------------------------------------------------------------------- 1 | const Page = () => { 2 | return
all
; 3 | }; 4 | 5 | export default Page; 6 | -------------------------------------------------------------------------------- /packages/playground/basic/pages/developer/[...developerId].tsx: -------------------------------------------------------------------------------- 1 | const Page = () => { 2 | return
developerId
; 3 | }; 4 | 5 | export default Page; 6 | -------------------------------------------------------------------------------- /packages/playground/basic/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'vitext/dynamic'; 2 | import Head from 'vitext/head'; 3 | import * as React from 'react'; 4 | 5 | import Component from '../components/Component'; 6 | 7 | const timeout = (n: number) => new Promise((r) => setTimeout(r, n)); 8 | 9 | const DynamicComponent = dynamic(async () => { 10 | return (await import('../components/Component')).default; 11 | }); 12 | 13 | const DynamicComponentNoServer = dynamic( 14 | async () => { 15 | return (await import('../components/Component')).default; 16 | }, 17 | { server: false } 18 | ); 19 | const LazyComponent = React.lazy(async () => { 20 | return import('../components/Component'); 21 | }); 22 | 23 | const IndexPage = () => { 24 | const [isMounted, setIsMounted] = React.useState(false); 25 | const [count, setCount] = React.useState(0); 26 | React.useEffect(() => { 27 | timeout(200).then(() => setIsMounted(true)); 28 | }, []); 29 | 30 | return ( 31 | <> 32 | 33 | Hello World 34 | 35 |
IndexPage
36 |
IndexPage
37 |
38 | {isMounted ? 'hydrated' : 'server-rendered'} 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 | 52 | 53 |
54 |
{count}
55 | 58 | 59 | ); 60 | }; 61 | 62 | export default IndexPage; 63 | -------------------------------------------------------------------------------- /packages/playground/basic/pages/page1.tsx: -------------------------------------------------------------------------------- 1 | const Page1 = () => { 2 | return
Page1
; 3 | }; 4 | 5 | export default Page1; 6 | -------------------------------------------------------------------------------- /packages/playground/basic/pages/users/[userId].tsx: -------------------------------------------------------------------------------- 1 | const Page = () => { 2 | return
userId
; 3 | }; 4 | 5 | export default Page; 6 | -------------------------------------------------------------------------------- /packages/playground/basic/pages/users/index.tsx: -------------------------------------------------------------------------------- 1 | const Page = () => { 2 | return
usersIndex
; 3 | }; 4 | 5 | export default Page; 6 | -------------------------------------------------------------------------------- /packages/playground/basic/styles/main.css: -------------------------------------------------------------------------------- 1 | #root { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /packages/playground/basic/vitext.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig } from 'vite'; 2 | 3 | export default {} as UserConfig 4 | -------------------------------------------------------------------------------- /packages/playground/css/__tests__/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated, getBgc } from "../../testUtils"; 2 | 3 | test('should return color', async () => { 4 | await page.goto(viteTestUrl) 5 | untilUpdated(() => page.textContent('p'), 'hello'); 6 | 7 | expect(await getBgc('#bg-red')).toBe('rgb(248, 113, 113)'); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/playground/css/declare.d.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | declare module '*.module.css'; 4 | 5 | declare module 'react' { 6 | interface SuspenseProps { 7 | children?: ReactNode | undefined; 8 | progressive?: boolean; 9 | server?: boolean; 10 | 11 | /** A fallback react tree to show when a Suspense child (like React.lazy) suspends */ 12 | fallback: NonNullable | null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/playground/css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/playground/css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css", 3 | "private": true, 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vitext dev", 8 | "build": "vitext build" 9 | }, 10 | "dependencies": { 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-router-dom": "^5.2.0", 14 | "vite": "^2.3.6", 15 | "vitext": "latest" 16 | }, 17 | "devDependencies": { 18 | "@babel/helper-compilation-targets": "^7.13.16", 19 | "@mdx-js/mdx": "^1.6.22", 20 | "@types/react": "^17.0.3", 21 | "@types/react-router-dom": "^5.1.7", 22 | "@vitejs/plugin-react-refresh": "^1.3.1", 23 | "autoprefixer": "^10.3.1", 24 | "postcss": "^8.3.6", 25 | "react-async-ssr": "^0.7.2", 26 | "react-lazy-ssr": "^0.2.4", 27 | "tailwindcss": "^2.2.7", 28 | "vite-plugin-windicss": "^1.2.7", 29 | "windicss": "^3.1.6" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/playground/css/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/main.css' 2 | 3 | export default function App({ 4 | Component, 5 | props, 6 | }: { 7 | Component: React.ComponentType; 8 | props: React.PropsWithChildren; 9 | }) { 10 | return ( 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/playground/css/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import classes from '../styles/main.module.css' 2 | 3 | const IndexPage = () => { 4 | return ( 5 |
6 |

hello

7 |
8 | ); 9 | }; 10 | 11 | export default IndexPage; 12 | -------------------------------------------------------------------------------- /packages/playground/css/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/playground/css/styles/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/playground/css/styles/main.module.css: -------------------------------------------------------------------------------- 1 | .red { 2 | margin-top: 3rem; 3 | font-size: 15rem; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /packages/playground/css/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: process.env.NODE_ENV === 'production' ? '' : 'jit', 3 | purge: ['./index.html', './pages/**/*.{js,ts,jsx,tsx}'], 4 | darkMode: false, // or 'media' or 'class' 5 | theme: { 6 | extend: {}, 7 | }, 8 | variants: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/playground/css/vitext.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { UserConfig } from 'vite'; 3 | import WindiCSS from 'vite-plugin-windicss'; 4 | 5 | export default { 6 | plugins: [ 7 | { 8 | ...WindiCSS({ 9 | scan: { 10 | include: ['**/*.{jsx,tsx}'], 11 | exclude: ['node_modules', '.git'], 12 | }, 13 | }), 14 | enforce: 'pre', 15 | }, 16 | ], 17 | } as UserConfig; 18 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/__tests__/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated } from '../../testUtils'; 2 | 3 | test("Page shouldn't have window.__DATA", async () => { 4 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 5 | expect(await page.evaluate('window.__DATA')).toBe(undefined); 6 | }); 7 | 8 | test('App should inject meta element', async () => { 9 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 10 | const element = await page.$('meta[name="description"]'); 11 | expect(await element.getAttribute('content')).toBe('Test'); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/declare.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' 2 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-document-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vitext dev", 8 | "build": "rm -rf dist && vite build --outDir dist && serve -s dist" 9 | }, 10 | "dependencies": { 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-router-dom": "^5.2.0", 14 | "vite": "^2.3.6", 15 | "vitext": "latest" 16 | }, 17 | "devDependencies": { 18 | "@babel/helper-compilation-targets": "^7.13.16", 19 | "@mdx-js/mdx": "^1.6.22", 20 | "@types/react": "^17.0.3", 21 | "@types/react-router-dom": "^5.1.7", 22 | "@vitejs/plugin-react-refresh": "^1.3.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Head } from 'vitext/head'; 2 | 3 | export default function App({ 4 | Component, 5 | props, 6 | }: { 7 | Component: React.ComponentType; 8 | props: React.PropsWithChildren; 9 | }) { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Document as BaseDocument, Main, Script } from 'vitext/document'; 2 | 3 | export default class extends BaseDocument { 4 | render() { 5 | return ( 6 | <> 7 |
8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Helmet } from 'react-helmet-async'; 3 | 4 | const IndexPage = () => { 5 | const [isMounted, setIsMounted] = useState(false); 6 | useEffect(() => { 7 | setIsMounted(true); 8 | }, []); 9 | return ( 10 | <> 11 | 12 | Hello World 13 | 14 |
IndexPage 2
15 |
16 | {isMounted ? 'hydrated' : 'server-rendered'} 17 |
18 | 19 | ); 20 | }; 21 | 22 | export default IndexPage; 23 | -------------------------------------------------------------------------------- /packages/playground/custom-document-app/vitext.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig } from 'vite'; 2 | 3 | export default {} as UserConfig 4 | -------------------------------------------------------------------------------- /packages/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vitext-playground", 3 | "version": "1.0.0", 4 | "private": true, 5 | "devDependencies": {} 6 | } 7 | -------------------------------------------------------------------------------- /packages/playground/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'css-color-names' { 2 | const colors: Record; 3 | export default colors; 4 | } 5 | -------------------------------------------------------------------------------- /packages/playground/ssg-basic/__tests__/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated, isBuild, readFile } from '../../testUtils'; 2 | 3 | const timeout = (num: number) => new Promise((res) => setTimeout(res, num)); 4 | 5 | test('paths & params should work', async () => { 6 | await page.goto(viteTestUrl + '/1'); 7 | await untilUpdated(() => page.textContent('#test'), '1'); 8 | 9 | await page.goto(viteTestUrl + '/2'); 10 | await untilUpdated(() => page.textContent('#test'), '2'); 11 | }); 12 | 13 | if (isBuild) { 14 | test('route exporting should work', async () => { 15 | await timeout(2000); 16 | await untilUpdated(() => page.textContent('#test-export'), 'exporting'); 17 | await untilUpdated(() => page.textContent('#test'), '1'); 18 | await page.goto(viteTestUrl + '/users/2'); 19 | await untilUpdated(() => page.textContent('#test'), '2'); 20 | await untilUpdated(() => page.textContent('#test-export'), 'exporting'); 21 | }); 22 | 23 | test('route html should work', async () => { 24 | await timeout(2000); 25 | const manifest = JSON.parse(readFile('dist/out/users/[slug]/manifest.json')) 26 | 27 | const firstHTML = readFile('dist/out/users/[slug]/0.html') 28 | expect(firstHTML.includes(manifest[0].slug)).toBe(true) 29 | expect(firstHTML.includes('exporting')).toBe(true) 30 | 31 | const secondHTML = readFile('dist/out/users/[slug]/1.html') 32 | expect(secondHTML.includes(manifest[1].slug)).toBe(true) 33 | expect(secondHTML.includes('exporting')).toBe(true) 34 | }); 35 | 36 | test('route pre-exporting should work', async () => { 37 | await page.goto(viteTestUrl + '/users/1'); 38 | await untilUpdated(() => page.textContent('#test-export'), 'not-exporting'); 39 | }); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /packages/playground/ssg-basic/declare.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' 2 | -------------------------------------------------------------------------------- /packages/playground/ssg-basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/playground/ssg-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssg-basic", 3 | "private": true, 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "chokidar \"../../vitext/*.js\" -t -c \"vitext dev\"", 8 | "build": "rm -rf dist && vite build --outDir dist && serve -s dist" 9 | }, 10 | "dependencies": { 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-router-dom": "^5.2.0", 14 | "vite": "^2.3.6", 15 | "vitext": "latest" 16 | }, 17 | "devDependencies": { 18 | "@babel/helper-compilation-targets": "^7.13.16", 19 | "@mdx-js/mdx": "^1.6.22", 20 | "@types/react": "^17.0.3", 21 | "@types/react-router-dom": "^5.1.7", 22 | "@vitejs/plugin-react-refresh": "^1.3.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/playground/ssg-basic/pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | const IndexPage = ({ slug }) => { 2 | return
{slug}
; 3 | }; 4 | 5 | export function getProps({ params }) { 6 | return { props: params }; 7 | } 8 | 9 | export async function getPaths() { 10 | return { 11 | paths: [ 12 | { 13 | params: { slug: 1 }, 14 | }, 15 | { 16 | params: { slug: 2 }, 17 | }, 18 | ], 19 | }; 20 | } 21 | 22 | export default IndexPage; 23 | -------------------------------------------------------------------------------- /packages/playground/ssg-basic/pages/users/[slug].tsx: -------------------------------------------------------------------------------- 1 | const timeout = (num: number) => new Promise((res) => setTimeout(res, num)); 2 | 3 | const User = ({ slug, isExporting }) => { 4 | return ( 5 | <> 6 |
{slug}
7 |
{isExporting ? 'exporting' : 'not-exporting'}
8 | 9 | ); 10 | }; 11 | 12 | export function getProps({ params, isExporting }) { 13 | return { props: { ...params, isExporting } }; 14 | } 15 | 16 | export async function getPaths() { 17 | await timeout(1000); 18 | return { 19 | paths: [ 20 | { 21 | params: { slug: 1 }, 22 | }, 23 | { 24 | params: { slug: 2 }, 25 | }, 26 | ], 27 | }; 28 | } 29 | 30 | export default User; 31 | -------------------------------------------------------------------------------- /packages/playground/ssg-basic/vitext.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig } from 'vite'; 2 | 3 | export default {} as UserConfig 4 | -------------------------------------------------------------------------------- /packages/playground/ssr-basic/__tests__/basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { untilUpdated } from '../../testUtils'; 2 | 3 | test('should render pages with ssr props', async () => { 4 | await untilUpdated(() => page.textContent('#test'), 'IndexPage'); 5 | const element = await page.$('#test'); 6 | 7 | expect(await element.textContent()).toBe('IndexPage'); 8 | }); 9 | 10 | -------------------------------------------------------------------------------- /packages/playground/ssr-basic/declare.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' 2 | -------------------------------------------------------------------------------- /packages/playground/ssr-basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/playground/ssr-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr-basic", 3 | "private": true, 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "chokidar \"../../vitext/*.js\" -t -c \"vitext dev\"", 8 | "build": "rm -rf dist && vite build --outDir dist && serve -s dist" 9 | }, 10 | "dependencies": { 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-router-dom": "^5.2.0", 14 | "vite": "^2.3.6", 15 | "vitext": "latest" 16 | }, 17 | "devDependencies": { 18 | "@babel/helper-compilation-targets": "^7.13.16", 19 | "@mdx-js/mdx": "^1.6.22", 20 | "@types/react": "^17.0.3", 21 | "@types/react-router-dom": "^5.1.7", 22 | "@vitejs/plugin-react-refresh": "^1.3.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/playground/ssr-basic/pages/index.tsx: -------------------------------------------------------------------------------- 1 | const IndexPage = ({ text }) => { 2 | return
{text}
; 3 | }; 4 | 5 | export function getProps() { 6 | return { props: { text: 'IndexPage' } }; 7 | } 8 | 9 | export default IndexPage; 10 | -------------------------------------------------------------------------------- /packages/playground/ssr-basic/vitext.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig } from 'vite'; 2 | 3 | export default {} as UserConfig 4 | -------------------------------------------------------------------------------- /packages/playground/testEnv.d.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from 'playwright-chromium'; 2 | 3 | declare global { 4 | // injected by the custom jest env in scripts/jestEnv.js 5 | const page: Page; 6 | // injected in scripts/jestPerTestSetup.ts 7 | const browserLogs: string[]; 8 | const viteTestUrl: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/playground/testUtils.ts: -------------------------------------------------------------------------------- 1 | // test utils used in e2e tests for playgrounds. 2 | // this can be directly imported in any playground tests as 'testUtils', e.g. 3 | // `import { getColor } from 'testUtils'` 4 | import colors from 'css-color-names'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import type { ElementHandle } from 'playwright-chromium'; 8 | import slash from 'slash'; 9 | 10 | export const isBuild = !!process.env.VITE_TEST_BUILD; 11 | 12 | const testPath = expect.getState().testPath; 13 | const testName = slash(testPath).match(/playground\/([\w-]+)\//)?.[1]; 14 | export const testDir = path.resolve(__dirname, '../../temp', testName); 15 | 16 | const hexToNameMap: Record = {}; 17 | Object.keys(colors).forEach((color) => { 18 | hexToNameMap[colors[color]] = color; 19 | }); 20 | 21 | function componentToHex(c: number): string { 22 | var hex = c.toString(16); 23 | return hex.length == 1 ? '0' + hex : hex; 24 | } 25 | 26 | function rgbToHex(rgb: string): string { 27 | const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); 28 | if (match) { 29 | const [_, rs, gs, bs] = match; 30 | return ( 31 | '#' + 32 | componentToHex(parseInt(rs, 10)) + 33 | componentToHex(parseInt(gs, 10)) + 34 | componentToHex(parseInt(bs, 10)) 35 | ); 36 | } else { 37 | return '#000000'; 38 | } 39 | } 40 | 41 | const timeout = (n: number) => new Promise((r) => setTimeout(r, n)); 42 | 43 | async function toEl(el: string | ElementHandle): Promise { 44 | if (typeof el === 'string') { 45 | return await page.$(el); 46 | } 47 | return el; 48 | } 49 | 50 | export async function getColor(el: string | ElementHandle) { 51 | el = await toEl(el); 52 | const rgb = await el.evaluate((el) => getComputedStyle(el as Element).color); 53 | return hexToNameMap[rgbToHex(rgb)] || rgb; 54 | } 55 | 56 | export async function getBg(el: string | ElementHandle) { 57 | el = await toEl(el); 58 | return el.evaluate((el) => getComputedStyle(el as Element).backgroundImage); 59 | } 60 | 61 | export async function getBgc(el: string | ElementHandle) { 62 | el = await toEl(el); 63 | return el.evaluate((el) => getComputedStyle(el as Element).backgroundColor); 64 | } 65 | export function readFile(filename: string) { 66 | return fs.readFileSync(path.resolve(testDir, filename), 'utf-8'); 67 | } 68 | 69 | export function editFile(filename: string, replacer: (str: string) => string) { 70 | if (isBuild) return; 71 | filename = path.resolve(testDir, filename); 72 | const content = fs.readFileSync(filename, 'utf-8'); 73 | const modified = replacer(content); 74 | fs.writeFileSync(filename, modified); 75 | } 76 | 77 | export function addFile(filename: string, content: string) { 78 | fs.writeFileSync(path.resolve(testDir, filename), content); 79 | } 80 | 81 | export function removeFile(filename: string) { 82 | fs.unlinkSync(path.resolve(testDir, filename)); 83 | } 84 | 85 | export function listAssets(base = '') { 86 | const assetsDir = path.join(testDir, 'dist', base, 'assets'); 87 | return fs.readdirSync(assetsDir); 88 | } 89 | 90 | export function findAssetFile(match: string | RegExp, base = '') { 91 | const assetsDir = path.join(testDir, 'dist', base, 'assets'); 92 | const files = fs.readdirSync(assetsDir); 93 | const file = files.find((file) => { 94 | return file.match(match); 95 | }); 96 | return file ? fs.readFileSync(path.resolve(assetsDir, file), 'utf-8') : ''; 97 | } 98 | 99 | export function readManifest(base = '') { 100 | return JSON.parse( 101 | fs.readFileSync(path.join(testDir, 'dist', base, 'manifest.json'), 'utf-8') 102 | ); 103 | } 104 | 105 | /** 106 | * Poll a getter until the value it returns includes the expected value. 107 | */ 108 | export async function untilUpdated( 109 | poll: () => string | Promise, 110 | expected: string, 111 | runInBuild = false 112 | ) { 113 | if (isBuild && !runInBuild) return; 114 | const maxTries = process.env.CI ? 25 : 15; 115 | for (let tries = 0; tries < maxTries; tries++) { 116 | const actual = (await poll()) || ''; 117 | if (actual.indexOf(expected) > -1 || tries === maxTries - 1) { 118 | expect(actual).toMatch(expected); 119 | break; 120 | } else { 121 | await timeout(50); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /packages/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["."], 3 | "exclude": ["**/dist/**"], 4 | "compilerOptions": { 5 | "target": "esnext", 6 | "outDir": "dist", 7 | "allowJs": true, 8 | "module":"esnext", 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "baseUrl": ".", 12 | "jsx": "react-jsx", 13 | "lib": ["ESNext", "ES2020.String", "ES2020", "DOM"], 14 | "types": ["vitext/client", "jest", "node", "vitext"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vitext/.gitignore: -------------------------------------------------------------------------------- 1 | document.js 2 | document.d.ts 3 | app.js 4 | app.node.js 5 | app.d.ts 6 | dist 7 | react.js 8 | react.node.cjs 9 | react.node.js 10 | dynamic.js 11 | dynamic.d.ts 12 | head.d.ts 13 | head.js 14 | yarn-error.log 15 | ./client 16 | .rollup.cache 17 | -------------------------------------------------------------------------------- /packages/vitext/.npmignore: -------------------------------------------------------------------------------- 1 | .rollup.cache 2 | src 3 | rollup.config.js 4 | rollup.config.vite.js 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/vitext/bin/vitext.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // require('../cli'); 4 | // require('../dist/node/cli.mjs'); 5 | import('../dist/node/cli.mjs'); 6 | -------------------------------------------------------------------------------- /packages/vitext/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vitext", 3 | "version": "0.0.2", 4 | "type": "module", 5 | "bin": { 6 | "vitext": "./bin/vitext.js" 7 | }, 8 | "files": [ 9 | "**" 10 | ], 11 | "engines": { 12 | "node": ">=12.0.0" 13 | }, 14 | "keywords": [ 15 | "vitext", 16 | "vite", 17 | "ssg", 18 | "ssr", 19 | "react" 20 | ], 21 | "sideEffects": false, 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/aslemammad/vitext.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/aslemammad/vitext/issues" 29 | }, 30 | "homepage": "https://github.com/aslemammad/vitext", 31 | "scripts": { 32 | "build": "cross-env-shell NODE_ENV=production \"rollup -c rollup.config.js\"", 33 | "dev": "cross-env-shell NODE_ENV=development \"rollup -c rollup.config.js -w\"", 34 | "semantic-release": "semantic-release" 35 | }, 36 | "peerDependencies": { 37 | "react": ">=16.8.6", 38 | "react-dom": ">=16.8.6" 39 | }, 40 | "devDependencies": { 41 | "@rollup/plugin-alias": "^3.1.5", 42 | "@rollup/plugin-commonjs": "^20.0.0", 43 | "@rollup/plugin-json": "^4.1.0", 44 | "@rollup/plugin-node-resolve": "^13.0.0", 45 | "@rollup/plugin-replace": "^3.0.0", 46 | "@rollup/plugin-typescript": "^8.2.5", 47 | "@types/compression": "^1.7.1", 48 | "@types/cors": "^2.8.12", 49 | "@types/deep-equal": "^1.0.1", 50 | "@types/enhanced-resolve": "^3.0.6", 51 | "@types/fs-extra": "^9.0.8", 52 | "@types/http-proxy": "^1.17.7", 53 | "@types/minimist": "^1.2.0", 54 | "@types/react": "^17.0.1", 55 | "@types/react-dom": "^17.0.1", 56 | "@types/react-helmet": "^6.1.1", 57 | "@types/react-loadable": "^5.5.6", 58 | "@types/react-router-dom": "^5.1.7", 59 | "@types/use-subscription": "^1.0.0", 60 | "@vitejs/plugin-react-refresh": "^1.3.5", 61 | "async-mutex": "^0.3.1", 62 | "cac": "^6.7.3", 63 | "chalk": "^4.1.0", 64 | "chokidar": "^3.5.1", 65 | "chokidar-cli": "^2.1.0", 66 | "compression": "^1.7.4", 67 | "concurrently": "^6.0.0", 68 | "connect": "^3.7.0", 69 | "cors": "^2.8.5", 70 | "deep-equal": "^2.0.5", 71 | "es-module-lexer": "^0.7.1", 72 | "fast-glob": "^3.2.5", 73 | "fs-extra": "^9.1.0", 74 | "global": "^4.4.0", 75 | "http-proxy": "^1.18.1", 76 | "jsonfile": "^6.1.0", 77 | "magic-string": "^0.25.7", 78 | "mitt": "^2.1.0", 79 | "postcss": "^8.3.6", 80 | "prop-types": "^15.7.2", 81 | "querystring": "^0.2.1", 82 | "react": "^17.0.2", 83 | "react-dom": "^17.0.2", 84 | "react-helmet-async": "^1.0.9", 85 | "read-pkg-up": "^7.0.1", 86 | "resolve": "^1.20.0", 87 | "rollup": "^2.38.5", 88 | "rollup-plugin-delete": "^2.0.0", 89 | "rollup-plugin-dts": "^3.0.2", 90 | "rollup-plugin-postcss": "^4.0.0", 91 | "rollup-plugin-typescript2": "^0.30.0", 92 | "selfsigned": "^1.10.11", 93 | "semantic-release": "^17.4.4", 94 | "terser": "^5.7.1", 95 | "typescript": "^4.3.3", 96 | "use-subscription": "^1.5.1", 97 | "vite-plugin-inspect": "^0.2.2" 98 | }, 99 | "dependencies": { 100 | "esbuild": "^0.12.9", 101 | "postcss": "^8.3.6", 102 | "react-helmet-async": "^1.0.9", 103 | "react-refresh": "^0.10.0", 104 | "resolve": "^1.20.0", 105 | "rollup": "^2.38.5", 106 | "use-subscription": "^1.5.1", 107 | "vite": "^2.4.2", 108 | "vite-plugin-inspect": "^0.2.2" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /packages/vitext/react-shim.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export { React }; 4 | -------------------------------------------------------------------------------- /packages/vitext/rollup.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import json from '@rollup/plugin-json'; 4 | import nodeResolve from '@rollup/plugin-node-resolve'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import MagicString from 'magic-string'; 7 | import path from 'path'; 8 | import dts from 'rollup-plugin-dts'; 9 | 10 | import * as pkg from './package.json'; 11 | 12 | const externalDeps = [ 13 | '/@vitext/_app', 14 | 'react', 15 | 'react/index', 16 | 'react-dom', 17 | 'react-dom/server', 18 | 'react-dom/server.js', 19 | 'react-helmet-async', 20 | 'react-helmet-async/lib/index.js', 21 | 'react-helmet-async/lib/index.modern.js', 22 | 'use-subscription', 23 | 'vitext/app.js', 24 | 'vitext/document.js', 25 | ]; 26 | 27 | function external(id) { 28 | if (externalDeps.includes(id)) return true; 29 | if (pkg.peerDependencies[id] || pkg.dependencies[id]) return true; 30 | return id.startsWith('/@vitext') || !id.startsWith('.'); 31 | } 32 | /** 33 | * @type { import('rollup').RollupOptions } 34 | */ 35 | const sharedNodeOptions = { 36 | treeshake: { 37 | moduleSideEffects: 'no-external', 38 | propertyReadSideEffects: false, 39 | tryCatchDeoptimization: false, 40 | }, 41 | output: { 42 | dir: path.resolve(__dirname, 'dist'), 43 | entryFileNames: `node/[name].mjs`, 44 | chunkFileNames: 'node/chunks/dep-[hash].mjs', 45 | // exports: 'named', 46 | exports: 'auto', 47 | format: 'esm', 48 | externalLiveBindings: false, 49 | freeze: false, 50 | sourcemap: false 51 | }, 52 | external, 53 | onwarn(warning, warn) { 54 | // node-resolve complains a lot about this but seems to still work? 55 | if (warning.message.includes('Package subpath')) { 56 | return; 57 | } 58 | // we use the eval('require') trick to deal with optional deps 59 | if (warning.message.includes('Use of eval')) { 60 | return; 61 | } 62 | if (warning.message.includes('Circular dependency')) { 63 | return; 64 | } 65 | warn(warning); 66 | }, 67 | }; 68 | 69 | const requireInject = ` 70 | import { createRequire } from 'module'; 71 | const require = createRequire(import.meta.url);\n 72 | `; 73 | 74 | /** 75 | * @type { () => import('rollup').Plugin } 76 | */ 77 | function createRequire() { 78 | return { 79 | name: 'createRequire', 80 | transform(code) { 81 | if (code.includes('require')) { 82 | const s = new MagicString(code); 83 | s.prepend(requireInject); 84 | return { code: s.toString(), map: s.generateMap() }; 85 | } 86 | return; 87 | }, 88 | }; 89 | } 90 | 91 | /** 92 | * 93 | * @param {boolean} isProduction 94 | * @returns {import('rollup').RollupOptions} 95 | */ 96 | const createNodeConfig = (isProduction) => { 97 | /** 98 | * @type { import('rollup').RollupOptions } 99 | */ 100 | const nodeConfig = { 101 | ...sharedNodeOptions, 102 | preserveEntrySignatures: 'allow-extension', 103 | input: { 104 | cli: path.resolve(__dirname, 'src/node/cli.ts'), 105 | }, 106 | external: [ 107 | 'fsevents', 108 | ...externalDeps, 109 | ...Object.keys(require('./package.json').dependencies), 110 | ...Object.keys(require('./package.json').peerDependencies), 111 | ...(isProduction 112 | ? [] 113 | : Object.keys(require('./package.json').devDependencies)), 114 | ], 115 | plugins: [ 116 | typescript({ 117 | target: 'es2019', 118 | include: ['src/node/**/*.ts', 'src/node/**/*.tsx'], 119 | esModuleInterop: true, 120 | tsconfig: path.resolve(__dirname, 'src/node/tsconfig.json'), 121 | sourceMap: false, 122 | }), 123 | nodeResolve({ preferBuiltins: true }), 124 | // Some deps have try...catch require of optional deps, but rollup will 125 | // generate code that force require them upfront for side effects. 126 | // Shim them with eval() so rollup can skip these calls. 127 | commonjs({ 128 | // requireReturnsDefault: true, 129 | extensions: ['.js'], 130 | // Optional peer deps of ws. Native deps that are mostly for performance. 131 | // Since ws is not that perf critical for us, just ignore these deps. 132 | ignore: ['bufferutil', 'utf-8-validate'], 133 | // esmExternals:false 134 | }), 135 | json(), 136 | createRequire(), 137 | ], 138 | }; 139 | 140 | return nodeConfig; 141 | }; 142 | 143 | /** 144 | * 145 | * @param {boolean} isProduction 146 | * @param {boolean} types 147 | * @returns {import('rollup').RollupOptions} 148 | */ 149 | const createFilesConfig = (isProduction, types) => { 150 | /** 151 | * @type { import('rollup').RollupOptions } 152 | */ 153 | const filesConfig = { 154 | input: { 155 | app: path.resolve(__dirname, 'src/node/components/_app.tsx'), 156 | document: path.resolve(__dirname, 'src/node/components/_document.tsx'), 157 | head: path.resolve(__dirname, 'src/node/components/Head.tsx'), 158 | dynamic: path.resolve(__dirname, 'src/react/dynamic.tsx'), 159 | }, 160 | plugins: [ 161 | // @ts-ignore 162 | types 163 | ? {} 164 | : typescript({ 165 | target: 'es2018', 166 | types: ['vite/client'], 167 | jsx: 'react', 168 | sourceMap: false, 169 | module: 'es2020', 170 | }), 171 | // @ts-ignore 172 | types ? dts() : {}, 173 | ], 174 | external, 175 | output: { 176 | dir: path.resolve(__dirname), 177 | 178 | format: 'esm', 179 | sourcemap: false, 180 | }, 181 | }; 182 | return filesConfig; 183 | }; 184 | 185 | /** 186 | * 187 | * @param {boolean} isProduction 188 | * @param {boolean} cjs 189 | * @returns {import('rollup').RollupOptions} 190 | */ 191 | const createReactConfig = (isProduction, cjs) => { 192 | /** 193 | * @type { import('rollup').RollupOptions } 194 | */ 195 | const filesConfig = { 196 | input: { 197 | [cjs ? 'react.node' : 'react']: path.resolve( 198 | __dirname, 199 | 'src/react/index.tsx' 200 | ), 201 | }, 202 | plugins: [ 203 | // @ts-ignore 204 | cjs 205 | ? typescript({ 206 | target: 'es2018', 207 | types: ['vite/client'], 208 | jsx: 'react', 209 | sourceMap: false, 210 | module: 'commonjs', 211 | }) 212 | : typescript({ 213 | target: 'es2018', 214 | types: ['vite/client'], 215 | jsx: 'react', 216 | sourceMap: false, 217 | module: 'es2020', 218 | }), 219 | // @ts-ignore 220 | ], 221 | external, 222 | output: { 223 | dir: path.resolve(__dirname), 224 | entryFileNames: `[name].${cjs ? 'cjs' : 'js'}`, 225 | format: cjs ? 'cjs' : 'esm', 226 | sourcemap: false, 227 | }, 228 | }; 229 | 230 | return filesConfig; 231 | }; 232 | 233 | const createClientConfig = (isProduction) => { 234 | /** 235 | * @type { import('rollup').RollupOptions } 236 | */ 237 | const clientConfig = { 238 | input: path.resolve(__dirname, 'src/client/main.tsx'), 239 | plugins: [ 240 | typescript({ 241 | target: 'es2018', 242 | types: ['vite/client'], 243 | jsx: 'react', 244 | sourceMap: false, 245 | }), 246 | ], 247 | external, 248 | output: { 249 | file: path.resolve(__dirname, 'dist/client/main.js'), 250 | format: 'esm', 251 | sourcemap: false, 252 | }, 253 | }; 254 | 255 | return clientConfig; 256 | }; 257 | 258 | export default (commandLineArgs) => { 259 | const isDev = commandLineArgs.watch; 260 | const isProduction = !isDev; 261 | 262 | return [ 263 | createNodeConfig(isProduction), 264 | createFilesConfig(isProduction, false), 265 | createFilesConfig(isProduction, true), 266 | createClientConfig(isProduction), 267 | createReactConfig(isProduction, false), 268 | createReactConfig(isProduction, true), 269 | ]; 270 | }; 271 | -------------------------------------------------------------------------------- /packages/vitext/src/client/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '/@vitext/*'; 2 | 3 | interface Window extends Window { 4 | __DATA: { 5 | pageClientPath: string; 6 | props: Record 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /packages/vitext/src/client/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | // @ts-ignore 4 | import { HelmetProvider } from 'react-helmet-async/lib/index.modern.js'; 5 | 6 | const root = document.getElementById('root'); 7 | const initialData = document.getElementById('__DATA')?.textContent; 8 | window.__DATA = initialData ? JSON.parse(initialData!) : undefined; 9 | 10 | (async function () { 11 | if (!window.__DATA) { 12 | return; 13 | } 14 | 15 | // @ts-ignore 16 | const App = (await import('./@vitext/_app')).default; 17 | 18 | async function render() { 19 | const props = window.__DATA.props[window.__DATA.pageClientPath]; 20 | 21 | const Component = ( 22 | await import(`./@vitext/hack-import${window.__DATA.pageClientPath}.js`) 23 | ).default; 24 | 25 | const element = ( 26 | 27 | 28 | 29 | ); 30 | 31 | if (import.meta.env.DEV) { 32 | ReactDOM.render(element, root); 33 | } else { 34 | ReactDOM.hydrate(element, root); 35 | } 36 | } 37 | 38 | render(); 39 | })(); 40 | -------------------------------------------------------------------------------- /packages/vitext/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "declaration": true, 7 | "noImplicitOverride": true, 8 | "noUnusedLocals": true, 9 | "esModuleInterop": true, 10 | "types": ["vite/client"], 11 | "jsx": "react", 12 | "module": "esnext", 13 | "outDir": "./" 14 | }, 15 | "exclude": [] 16 | } 17 | -------------------------------------------------------------------------------- /packages/vitext/src/node/build.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { 3 | GetManualChunk, 4 | GetModuleInfo, 5 | OutputAsset, 6 | OutputBundle, 7 | } from 'rollup'; 8 | import type { Plugin} from 'vite'; 9 | 10 | import { cssLangRE, directRequestRE, getEntryPoints } from './utils'; 11 | 12 | export function build(): Plugin { 13 | return { 14 | name: 'vitext:build', 15 | apply: 'build', 16 | enforce: 'post', 17 | config: async (config) => { 18 | const pages = await getEntryPoints(config); 19 | const entries: Record = {}; 20 | pages.forEach( 21 | (p) => 22 | (entries[p.substr(0, p.lastIndexOf('.')).replace('./', '')] = 23 | path.join(config.root!, p)) 24 | ); 25 | 26 | return { 27 | mode: 'production', 28 | optimizeDeps: { 29 | keepNames: undefined, 30 | }, 31 | build: { 32 | outDir: path.join(config.root!, 'dist'), 33 | manifest: true, 34 | brotliSize: true, 35 | ssr: true, 36 | minify: true, 37 | rollupOptions: { 38 | input: entries, 39 | preserveEntrySignatures: 'allow-extension', 40 | output: { 41 | format: 'es', 42 | exports: 'named', 43 | manualChunks: createMoveToVendorChunkFn(), 44 | }, 45 | }, 46 | polyfillDynamicImport: false, 47 | }, 48 | }; 49 | }, 50 | }; 51 | } 52 | 53 | const assetsBundle: OutputBundle = {}; 54 | 55 | export function getAssets(): Plugin { 56 | return { 57 | name: 'vitext:get-assets', 58 | apply: 'build', 59 | enforce: 'pre', 60 | generateBundle(_, bundle) { 61 | const wipAssets: { 62 | [fileName: string]: OutputAsset; 63 | } = {}; 64 | 65 | for (const file in bundle) { 66 | if (bundle[file].type === 'asset') { 67 | const asset = bundle[file] as OutputAsset; 68 | const source = asset.source.toString(); 69 | wipAssets[asset.name!] = wipAssets[asset.name!] || asset; 70 | // inlined css (export ...) are the referenced files, but with incorrect source 71 | if (source.startsWith('export ')) { 72 | wipAssets[asset.name!].fileName = asset.fileName; 73 | } else { 74 | // plain css files source is correct 75 | wipAssets[asset.name!].source = asset.source; 76 | } 77 | } 78 | } 79 | for (const file in wipAssets) { 80 | const asset = wipAssets[file]; 81 | assetsBundle[asset.fileName] = asset; 82 | } 83 | }, 84 | }; 85 | } 86 | 87 | export function writeAssets(): Plugin { 88 | return { 89 | name: 'vitext:write-assets', 90 | apply: 'build', 91 | enforce: 'post', 92 | generateBundle(_, bundle) { 93 | Object.assign(bundle, assetsBundle); 94 | }, 95 | }; 96 | } 97 | 98 | export const isCSSRequest = (request: string): boolean => 99 | cssLangRE.test(request) && !directRequestRE.test(request); 100 | 101 | function createMoveToVendorChunkFn(): GetManualChunk { 102 | const cache = new Map(); 103 | const dynamicImportsCache = new Set(); 104 | 105 | return (id, { getModuleInfo }) => { 106 | const moduleInfo = getModuleInfo(id); 107 | 108 | moduleInfo?.dynamicallyImportedIds.forEach((id) => 109 | dynamicImportsCache.add(id) 110 | ); 111 | 112 | if (dynamicImportsCache.has(id)) { 113 | return path.basename(id, path.extname(id)); 114 | } 115 | 116 | if ( 117 | id.includes('node_modules') && 118 | !isCSSRequest(id) && 119 | staticImportedByEntry(id, getModuleInfo, cache) 120 | ) { 121 | return 'vendor'; 122 | } 123 | }; 124 | } 125 | 126 | function staticImportedByEntry( 127 | id: string, 128 | getModuleInfo: GetModuleInfo, 129 | cache: Map, 130 | importStack: string[] = [] 131 | ): boolean { 132 | if (cache.has(id)) { 133 | return cache.get(id) as boolean; 134 | } 135 | if (importStack.includes(id)) { 136 | // circular deps! 137 | cache.set(id, false); 138 | return false; 139 | } 140 | const mod = getModuleInfo(id); 141 | if (!mod) { 142 | cache.set(id, false); 143 | return false; 144 | } 145 | 146 | if (mod.isEntry) { 147 | cache.set(id, true); 148 | return true; 149 | } 150 | const someImporterIs = mod.importers.some((importer) => 151 | staticImportedByEntry( 152 | importer, 153 | getModuleInfo, 154 | cache, 155 | importStack.concat(id) 156 | ) 157 | ); 158 | cache.set(id, someImporterIs); 159 | return someImporterIs; 160 | } 161 | -------------------------------------------------------------------------------- /packages/vitext/src/node/cli.ts: -------------------------------------------------------------------------------- 1 | import cac from 'cac'; 2 | import chalk from 'chalk'; 3 | import Vite from 'vite'; 4 | 5 | import * as utils from './utils'; 6 | 7 | // eslint-disable-next-line 8 | console.log(chalk.cyan(`vitext v${require('vitext/package.json').version}`)); 9 | 10 | const cli = cac('vitext'); 11 | 12 | // global options 13 | interface GlobalCLIOptions { 14 | '--'?: string[]; 15 | debug?: boolean | string; 16 | d?: boolean | string; 17 | filter?: string; 18 | f?: string; 19 | config?: string; 20 | c?: boolean | string; 21 | root?: string; 22 | base?: string; 23 | r?: string; 24 | mode?: string; 25 | m?: string; 26 | logLevel?: Vite.LogLevel; 27 | l?: Vite.LogLevel; 28 | clearScreen?: boolean; 29 | } 30 | 31 | /** 32 | * removing global flags before passing as command specific sub-configs 33 | */ 34 | function cleanOptions(options: GlobalCLIOptions) { 35 | const ret = { ...options }; 36 | delete ret['--']; 37 | delete ret.debug; 38 | delete ret.d; 39 | delete ret.filter; 40 | delete ret.f; 41 | delete ret.config; 42 | delete ret.c; 43 | delete ret.root; 44 | delete ret.base; 45 | delete ret.r; 46 | delete ret.mode; 47 | delete ret.m; 48 | delete ret.logLevel; 49 | delete ret.l; 50 | delete ret.clearScreen; 51 | return ret; 52 | } 53 | 54 | cli 55 | .option('-c, --config ', `[string] use specified config file`) 56 | .option('-r, --root ', `[string] use specified root directory`) 57 | .option('--base ', `[string] public base path (default: /)`) 58 | .option('-l, --logLevel ', `[string] info | warn | error | silent`) 59 | .option('--clearScreen', `[boolean] allow/disable clear screen when logging`) 60 | .option('-d, --debug [feat]', `[string | boolean] show debug logs`) 61 | .option('-f, --filter ', `[string] filter debug logs`); 62 | 63 | // dev 64 | cli 65 | .command('[root]') // default command 66 | .alias('serve') 67 | .alias('dev') 68 | .option('--host [host]', `[string] specify hostname`) 69 | .option('--port ', `[number] specify port`) 70 | .option('--https', `[boolean] use TLS + HTTP/2`) 71 | .option('--open [path]', `[boolean | string] open browser on startup`) 72 | .option('--cors', `[boolean] enable CORS`) 73 | .option('--strictPort', `[boolean] exit if specified port is already in use`) 74 | .option('-m, --mode ', `[string] set env mode`) 75 | .option( 76 | '--force', 77 | `[boolean] force the optimizer to ignore the cache and re-bundle` 78 | ) 79 | .action( 80 | async ( 81 | root: string = process.cwd(), 82 | options: Vite.ServerOptions & GlobalCLIOptions 83 | ) => { 84 | const { createServer } = await import('./server'); 85 | try { 86 | process.env['NODE_ENV'] = options.mode || 'development'; 87 | const server = await createServer({ 88 | root, 89 | base: options.base, 90 | mode: options.mode || 'development', 91 | configFile: options.config, 92 | logLevel: options.logLevel, 93 | clearScreen: options.clearScreen, 94 | server: cleanOptions(options) as Vite.ServerOptions, 95 | }); 96 | server.listen(); 97 | } catch (e) { 98 | Vite.createLogger(options.logLevel).error( 99 | chalk.red(`error when starting dev server:\n${e.stack}`) 100 | ); 101 | process.exit(1); 102 | } 103 | } 104 | ); 105 | 106 | // build 107 | cli 108 | .command('build [root]') 109 | .option('--target ', `[string] transpile target (default: 'modules')`) 110 | .option('--outDir ', `[string] output directory (default: dist)`) 111 | .option( 112 | '--assetsDir ', 113 | `[string] directory under outDir to place assets in (default: _assets)` 114 | ) 115 | .option( 116 | '--assetsInlineLimit ', 117 | `[number] static asset base64 inline threshold in bytes (default: 4096)` 118 | ) 119 | .option( 120 | '--ssr [entry]', 121 | `[string] build specified entry for server-side rendering` 122 | ) 123 | .option( 124 | '--sourcemap', 125 | `[boolean] output source maps for build (default: false)` 126 | ) 127 | .option( 128 | '--minify [minifier]', 129 | `[boolean | "terser" | "esbuild"] enable/disable minification, ` + 130 | `or specify minifier to use (default: terser)` 131 | ) 132 | .option('--manifest', `[boolean] emit build manifest json`) 133 | .option('--ssrManifest', `[boolean] emit ssr manifest json`) 134 | .option( 135 | '--emptyOutDir', 136 | `[boolean] force empty outDir when it's outside of root` 137 | ) 138 | .option('-m, --mode ', `[string] set env mode`) 139 | .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`) 140 | .action( 141 | async ( 142 | root: string = process.cwd(), 143 | options: Vite.BuildOptions & GlobalCLIOptions 144 | ) => { 145 | const buildOptions = cleanOptions(options) as Vite.BuildOptions; 146 | 147 | process.env['NODE_ENV'] = options.mode || 'production'; 148 | 149 | try { 150 | const config = (await utils.resolveInlineConfig( 151 | { 152 | root, 153 | base: options.base, 154 | mode: options.mode || 'production', 155 | configFile: options.config, 156 | logLevel: options.logLevel, 157 | clearScreen: options.clearScreen, 158 | build: buildOptions, 159 | }, 160 | 'build' 161 | )) as Vite.InlineConfig; 162 | // await optimizeDeps(config as unknown as ResolvedConfig, true, true); 163 | await Vite.build(config); 164 | } catch (e) { 165 | Vite.createLogger(options.logLevel).error( 166 | chalk.red(`error during build:\n${e.stack}`) 167 | ); 168 | process.exit(1); 169 | } 170 | } 171 | ); 172 | 173 | // optimize 174 | cli 175 | .command('optimize [root]') 176 | .option( 177 | '--force', 178 | `[boolean] force the optimizer to ignore the cache and re-bundle` 179 | ) 180 | .action( 181 | async ( 182 | root: string = process.cwd(), 183 | options: { force?: boolean } & GlobalCLIOptions 184 | ) => { 185 | try { 186 | const config = (await utils.resolveInlineConfig( 187 | { 188 | root, 189 | base: options.base, 190 | logLevel: options.logLevel, 191 | }, 192 | 'build' 193 | )) as Vite.ResolvedConfig; 194 | await Vite.optimizeDeps(config, options.force, true); 195 | } catch (e) { 196 | Vite.createLogger(options.logLevel).error( 197 | chalk.red(`error when optimizing deps:\n${e.stack}`) 198 | ); 199 | process.exit(1); 200 | } 201 | } 202 | ); 203 | 204 | cli 205 | .command('preview [root]') 206 | .alias('start') 207 | .option('--host [host]', `[string] specify hostname`) 208 | .option('--port ', `[number] specify port`) 209 | .option('--https', `[boolean] use TLS + HTTP/2`) 210 | .option('--open [path]', `[boolean | string] open browser on startup`) 211 | .option('--strictPort', `[boolean] exit if specified port is already in use`) 212 | .action( 213 | async ( 214 | root: string = process.cwd(), 215 | options: { 216 | host?: string; 217 | port?: number; 218 | https?: boolean; 219 | open?: boolean | string; 220 | strictPort?: boolean; 221 | } & GlobalCLIOptions 222 | ) => { 223 | process.env['NODE_ENV'] = options.mode || 'production'; 224 | const { preview } = await import('./preview'); 225 | try { 226 | const config = (await utils.resolveInlineConfig( 227 | { 228 | root, 229 | mode: options.mode || 'production', 230 | base: options.base, 231 | logLevel: options.logLevel, 232 | server: { 233 | open: options.open, 234 | strictPort: options.strictPort, 235 | https: options.https, 236 | }, 237 | }, 238 | 'serve' 239 | )) as Vite.ResolvedConfig; 240 | 241 | await preview( 242 | config, 243 | cleanOptions(options) as { 244 | host?: string; 245 | port?: number; 246 | } 247 | ); 248 | } catch (e) { 249 | Vite.createLogger(options.logLevel).error( 250 | chalk.red(`error when starting preview server:\n${e.stack}`) 251 | ); 252 | process.exit(1); 253 | } 254 | } 255 | ); 256 | 257 | cli.help(); 258 | // eslint-disable-next-line 259 | cli.version(require('vitext/package.json').version); 260 | 261 | cli.parse(); 262 | -------------------------------------------------------------------------------- /packages/vitext/src/node/components/Head.tsx: -------------------------------------------------------------------------------- 1 | import { Helmet as HelmetType } from 'react-helmet-async'; 2 | 3 | // @ts-ignore 4 | import Helmet from 'react-helmet-async/lib/index.js'; 5 | 6 | const Head = Helmet.Helmet as HelmetType 7 | export { Head, Head as default } 8 | -------------------------------------------------------------------------------- /packages/vitext/src/node/components/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function App({ 4 | Component, 5 | props, 6 | }: { 7 | Component: React.ComponentType; 8 | props: React.PropsWithChildren; 9 | }) { 10 | return ; 11 | } 12 | export type AppType = typeof App; 13 | -------------------------------------------------------------------------------- /packages/vitext/src/node/components/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // @ts-ignore 3 | import Helmet from 'react-helmet-async/lib/index.js'; 4 | 5 | type DocumentProps = { 6 | Component: React.ComponentType; 7 | pageClientPath: string; 8 | props: any; // fetched data 9 | }; 10 | const DocumentContext = React.createContext(null as any); 11 | 12 | export class Document extends React.Component { 13 | static renderDocument( 14 | DocumentComponent: typeof Document, 15 | props: DocumentProps 16 | ) { 17 | const helmetContext = {} as { helmet: Helmet.HelmetData }; 18 | return { 19 | Page: ( 20 | 21 | 22 | 23 | 24 | 25 | ), 26 | helmetContext, 27 | }; 28 | } 29 | 30 | render() { 31 | return ( 32 | <> 33 |
34 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /packages/vitext/src/node/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | export const CLIENT_PATH = path.join(__dirname, '../client') 4 | -------------------------------------------------------------------------------- /packages/vitext/src/node/middlewares/page.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as path from 'path'; 3 | import { parse as parseQs } from 'querystring'; 4 | import type { ConfigEnv, Connect, Manifest, ViteDevServer } from 'vite'; 5 | 6 | import { loadExportedPage } from '../route/export'; 7 | import { fetchData } from '../route/fetch'; 8 | import { resolvePagePath } from '../route/pages'; 9 | import { renderToHTML } from '../route/render'; 10 | import type { Await, Entries } from '../types'; 11 | import { loadPage, resolveCustomComponents } from '../utils'; 12 | 13 | export async function createPageMiddleware({ 14 | server, 15 | env, 16 | entries, 17 | pagesModuleId, 18 | template, 19 | manifest, 20 | }: { 21 | server: ViteDevServer; 22 | env: ConfigEnv; 23 | pagesModuleId: string; 24 | template: string; 25 | entries: Entries; 26 | clearEntries: Entries; 27 | manifest: Manifest; 28 | }): Promise { 29 | let customComponents: Await>; 30 | 31 | return async function pageMiddleware(req, res, next) { 32 | const [pathname, queryString] = (req.originalUrl || '').split('?')!; 33 | const page = resolvePagePath(pathname, entries); 34 | 35 | customComponents = await resolveCustomComponents({ 36 | entries, 37 | server, 38 | }); 39 | 40 | if (!page) { 41 | return next(); 42 | } 43 | 44 | try { 45 | let html: string | undefined; 46 | if (env.mode === 'production') { 47 | html = await loadExportedPage({ 48 | root: server.config.root!, 49 | pageName: page.pageEntry.pageName, 50 | params: page.params, 51 | }); 52 | } 53 | 54 | if (!html) { 55 | const transformedTemplate = 56 | env.mode === 'development' 57 | ? await server.transformIndexHtml( 58 | req.url!, 59 | template, 60 | req.originalUrl 61 | ) 62 | : template; 63 | 64 | const pageFile = await loadPage({ 65 | entries, 66 | server, 67 | page: page.pageEntry, 68 | }); 69 | 70 | page.query = parseQs(queryString); 71 | 72 | const data = await fetchData({ 73 | req, 74 | res, 75 | pageFile, 76 | page, 77 | env, 78 | isExporting: false, 79 | }); 80 | 81 | html = await renderToHTML({ 82 | server, 83 | entries, 84 | manifest, 85 | pageEntry: page.pageEntry, 86 | pagesModuleId, 87 | props: data?.props, 88 | template: transformedTemplate, 89 | Component: pageFile.default, 90 | Document: customComponents.Document!, 91 | App: customComponents.App!, 92 | }); 93 | } 94 | 95 | res.statusCode = 200; 96 | res.setHeader('Content-Type', 'text/html'); 97 | res.end(html); 98 | } catch (e) { 99 | server.ssrFixStacktrace(e); 100 | server.config.logger.error(chalk.red(e)); 101 | next(e); 102 | } 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /packages/vitext/src/node/plugin.ts: -------------------------------------------------------------------------------- 1 | import { init, parse } from 'es-module-lexer'; 2 | import Esbuild from 'esbuild'; 3 | import * as fs from 'fs'; 4 | import MagicString from 'magic-string'; 5 | import * as path from 'path'; 6 | import type { 7 | ConfigEnv, 8 | Manifest, 9 | Plugin, 10 | ResolvedConfig, 11 | UserConfig, 12 | ViteDevServer, 13 | } from 'vite'; 14 | 15 | import { build, getAssets, writeAssets } from './build'; 16 | import { createPageMiddleware } from './middlewares/page'; 17 | import { exportPage } from './route/export'; 18 | import { getEntries, PageType } from './route/pages'; 19 | import { Entries } from './types'; 20 | import { 21 | getEntryPoints, 22 | jsLangsRE, 23 | removeImportQuery, 24 | resolveCustomComponents, 25 | resolveHackImport, 26 | } from './utils'; 27 | 28 | const modulePrefix = '/@vitext/'; 29 | 30 | const appEntryId = modulePrefix + 'index.js'; 31 | const pagesModuleId = modulePrefix + 'pages/'; 32 | const currentPageModuleId = modulePrefix + 'current-page'; 33 | 34 | export default function pluginFactory(): Plugin { 35 | let resolvedConfig: ResolvedConfig | UserConfig; 36 | const currentPage: PageType = {} as PageType; 37 | const manifest: Manifest = {}; 38 | let resolvedEnv: ConfigEnv; 39 | 40 | let server: ViteDevServer; 41 | let entries: Entries; 42 | let clearEntries: Entries; 43 | 44 | return { 45 | name: 'vitext', 46 | async config(userConfig, env) { 47 | resolvedEnv = env; 48 | if (env.command !== 'build') { 49 | const manifestPath = path.join( 50 | userConfig.root!, 51 | userConfig.build!.outDir!, 52 | 'manifest.json' 53 | ); 54 | 55 | Object.assign( 56 | manifest, 57 | env.mode === 'production' && env.command === 'serve' 58 | ? JSON.parse(await fs.promises.readFile(manifestPath, 'utf-8')) 59 | : {} 60 | ); 61 | const entryPoints = 62 | env.mode === 'development' 63 | ? await getEntryPoints(userConfig) 64 | : Object.keys(manifest).filter((key) => key.startsWith('pages/')); 65 | 66 | entries = getEntries(entryPoints, env.mode, manifest); 67 | 68 | clearEntries = entries.filter( 69 | (page) => 70 | !( 71 | page.pageName.includes('_document') || 72 | page.pageName.includes('_app') 73 | ) 74 | ); 75 | } 76 | 77 | if (env.command === 'build') { 78 | resolvedConfig = userConfig; 79 | } 80 | 81 | return { 82 | ssr: { 83 | target: 'webworker', 84 | external: [ 85 | 'prop-types', 86 | 'react-helmet-async', 87 | 'use-subscription', 88 | 'vitext/react.node.cjs', 89 | 'vitext/app.node', 90 | ], 91 | }, 92 | optimizeDeps: { 93 | include: [ 94 | 'react', 95 | 'react/index', 96 | 'react-dom', 97 | 'use-subscription', 98 | 'vitext/react', 99 | 'vitext/document', 100 | 'vitext/app', 101 | 'vitext/head', 102 | 'vitext/dynamic', 103 | 'react-helmet-async', 104 | 'react-helmet-async/lib/index.modern.js' 105 | ], 106 | }, 107 | esbuild: { 108 | legalComments: 'inline', 109 | jsxInject: `import * as React from 'react'`, 110 | }, 111 | build: { 112 | base: undefined, 113 | }, 114 | }; 115 | }, 116 | configResolved(config) { 117 | resolvedConfig = config; 118 | }, 119 | async configureServer(_server) { 120 | server = _server; 121 | 122 | const template = await fs.promises.readFile( 123 | path.join(server.config.root, 'index.html'), 124 | 'utf-8' 125 | ); 126 | 127 | const pageMiddleware = await createPageMiddleware({ 128 | server, 129 | entries, 130 | clearEntries, 131 | pagesModuleId, 132 | template, 133 | manifest, 134 | env: resolvedEnv, 135 | }); 136 | 137 | return async () => { 138 | server.middlewares.use(pageMiddleware); 139 | const customComponents = await resolveCustomComponents({ 140 | entries, 141 | server, 142 | }); 143 | 144 | if (resolvedEnv.mode === 'production') { 145 | clearEntries.forEach((entry) => 146 | exportPage({ 147 | manifest, 148 | server, 149 | entries, 150 | template, 151 | pagesModuleId, 152 | page: entry, 153 | App: customComponents.App, 154 | Document: customComponents.Document, 155 | }) 156 | ); 157 | } 158 | }; 159 | }, 160 | resolveId(id) { 161 | if (id.startsWith('.' + modulePrefix)) id = id.slice(1); 162 | 163 | if (id.includes(modulePrefix + '_app')) { 164 | return modulePrefix + '_app' 165 | } 166 | 167 | return id; 168 | }, 169 | 170 | async load(id) { 171 | if (id === currentPageModuleId) { 172 | id = 173 | pagesModuleId + 174 | (currentPage.pageEntry.pageName !== '/' 175 | ? currentPage.pageEntry.pageName 176 | : ''); 177 | } 178 | 179 | if (id === appEntryId) return `import "vitext/dist/client/main.js";`; 180 | 181 | if (id.startsWith(modulePrefix + '_app')) { 182 | const page = entries.find(({ pageName }) => pageName === '/_app'); 183 | if (page) { 184 | const absolutePagePath = path.resolve( 185 | resolvedConfig.root!, 186 | page!.absolutePagePath 187 | ); 188 | return `export { default } from "${absolutePagePath}"`; 189 | } 190 | return `export { App as default } from "vitext/app"`; 191 | } 192 | 193 | id = resolveHackImport(id); 194 | 195 | if (id.startsWith(pagesModuleId)) { 196 | // strip ?import 197 | id = removeImportQuery(id); 198 | 199 | let plainPageName = 200 | id.slice(pagesModuleId.length) + (id === pagesModuleId ? '/' : ''); 201 | if (!plainPageName.startsWith('/')) { 202 | plainPageName = '/' + plainPageName; 203 | } 204 | const page = clearEntries.find( 205 | ({ pageName }) => pageName === plainPageName 206 | ); 207 | if (!page) { 208 | return; 209 | } 210 | 211 | const absolutePagePath = path.resolve( 212 | resolvedConfig.root!, 213 | page!.absolutePagePath 214 | ); 215 | 216 | return `export { default } from "${absolutePagePath}"`; 217 | } 218 | }, 219 | }; 220 | } 221 | 222 | export function dependencyInjector(): Plugin { 223 | return { 224 | name: 'vitext:dependency-injector', 225 | enforce: 'pre', 226 | async transform(code, id, ssr) { 227 | if (!ssr) { 228 | return code; 229 | } 230 | const [file] = id.split('?'); 231 | if (!jsLangsRE.test(id)) return code; 232 | id = file; 233 | 234 | let ext = path.extname(id).slice(1); 235 | if (ext === 'mjs' || ext === 'cjs') ext = 'js'; 236 | 237 | await init; 238 | const source = ( 239 | await Esbuild.transform(code, { loader: ext as Esbuild.Loader, jsx: 'transform' }) 240 | ).code; 241 | 242 | const imports = parse(source)[0]; 243 | const s = new MagicString(source); 244 | for (let index = 0; index < imports.length; index++) { 245 | const { s: start, e: end } = imports[index]; 246 | const url = source.slice(start, end); 247 | s.overwrite(start, end, url === 'react' ? 'vitext/react.node.cjs' : url); 248 | } 249 | 250 | return { 251 | code: s.toString(), 252 | map: s.generateMap(), 253 | }; 254 | }, 255 | }; 256 | } 257 | export function createVitextPlugin(): Plugin[] { 258 | return [ 259 | pluginFactory(), 260 | dependencyInjector(), 261 | build(), 262 | getAssets(), 263 | writeAssets(), 264 | ]; 265 | } 266 | -------------------------------------------------------------------------------- /packages/vitext/src/node/preview.ts: -------------------------------------------------------------------------------- 1 | import compression from 'compression'; 2 | import corsMiddleware from 'cors'; 3 | import fs from 'fs'; 4 | import { Server as HttpServer } from 'http'; 5 | import { ServerOptions as HttpsServerOptions } from 'https'; 6 | import path from 'path'; 7 | import Vite from 'vite'; 8 | 9 | import { proxyMiddleware } from './proxy'; 10 | import { createServer } from './server'; 11 | import { isObject } from './utils'; 12 | 13 | const fsp = fs.promises 14 | 15 | export function readFileIfExists(value?: string | Buffer | any[]) { 16 | if (typeof value === 'string') { 17 | try { 18 | return fs.readFileSync(path.resolve(value as string)); 19 | } catch (e) { 20 | return value; 21 | } 22 | } 23 | return value; 24 | } 25 | 26 | /** 27 | * https://github.com/webpack/webpack-dev-server/blob/master/lib/utils/createCertificate.js 28 | * 29 | * Copyright JS Foundation and other contributors 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file at 32 | * https://github.com/webpack/webpack-dev-server/blob/master/LICENSE 33 | */ 34 | async function createCertificate() { 35 | // @ts-ignore 36 | const { generate } = await import('selfsigned'); 37 | 38 | const pems = generate(null, { 39 | algorithm: 'sha256', 40 | days: 30, 41 | keySize: 2048, 42 | extensions: [ 43 | // { 44 | // name: 'basicConstraints', 45 | // cA: true, 46 | // }, 47 | { 48 | name: 'keyUsage', 49 | keyCertSign: true, 50 | digitalSignature: true, 51 | nonRepudiation: true, 52 | keyEncipherment: true, 53 | dataEncipherment: true, 54 | }, 55 | { 56 | name: 'extKeyUsage', 57 | serverAuth: true, 58 | clientAuth: true, 59 | codeSigning: true, 60 | timeStamping: true, 61 | }, 62 | { 63 | name: 'subjectAltName', 64 | altNames: [ 65 | { 66 | // type 2 is DNS 67 | type: 2, 68 | value: 'localhost', 69 | }, 70 | { 71 | type: 2, 72 | value: 'localhost.localdomain', 73 | }, 74 | { 75 | type: 2, 76 | value: 'lvh.me', 77 | }, 78 | { 79 | type: 2, 80 | value: '*.lvh.me', 81 | }, 82 | { 83 | type: 2, 84 | value: '[::1]', 85 | }, 86 | { 87 | // type 7 is IP 88 | type: 7, 89 | ip: '127.0.0.1', 90 | }, 91 | { 92 | type: 7, 93 | ip: 'fe80::1', 94 | }, 95 | ], 96 | }, 97 | ], 98 | }); 99 | return pems.private + pems.cert; 100 | } 101 | async function getCertificate(config: Vite.ResolvedConfig) { 102 | if (!config.cacheDir) return await createCertificate(); 103 | 104 | const cachePath = path.join(config.cacheDir, '_cert.pem'); 105 | 106 | try { 107 | const [stat, content] = await Promise.all([ 108 | fsp.stat(cachePath), 109 | fsp.readFile(cachePath, 'utf8'), 110 | ]); 111 | 112 | if (Date.now() - stat.ctime.valueOf() > 30 * 24 * 60 * 60 * 1000) { 113 | throw new Error('cache is outdated.'); 114 | } 115 | 116 | return content; 117 | } catch { 118 | const content = await createCertificate(); 119 | fsp 120 | .mkdir(config.cacheDir, { recursive: true }) 121 | .then(() => fsp.writeFile(cachePath, content)) 122 | .catch(() => {}); 123 | return content; 124 | } 125 | } 126 | export async function resolveHttpsConfig( 127 | config: Vite.ResolvedConfig 128 | ): Promise { 129 | if (!config.server.https) return undefined; 130 | 131 | const httpsOption = isObject(config.server.https) ? config.server.https : {}; 132 | 133 | const { ca, cert, key, pfx } = httpsOption; 134 | Object.assign(httpsOption, { 135 | ca: readFileIfExists(ca), 136 | cert: readFileIfExists(cert), 137 | key: readFileIfExists(key), 138 | pfx: readFileIfExists(pfx), 139 | }); 140 | if (!httpsOption.key || !httpsOption.cert) { 141 | httpsOption.cert = httpsOption.key = await getCertificate(config); 142 | } 143 | return httpsOption; 144 | } 145 | export async function resolveHttpServer( 146 | { proxy }: Vite.ServerOptions, 147 | app: Vite.Connect.Server, 148 | httpsOptions?: HttpsServerOptions 149 | ): Promise { 150 | if (!httpsOptions) { 151 | return require('http').createServer(app); 152 | } 153 | 154 | if (proxy) { 155 | // #484 fallback to http1 when proxy is needed. 156 | return require('https').createServer(httpsOptions, app); 157 | } else { 158 | return require('http2').createSecureServer( 159 | { 160 | ...httpsOptions, 161 | allowHTTP1: true, 162 | }, 163 | app 164 | ); 165 | } 166 | } 167 | export async function httpServerStart( 168 | httpServer: HttpServer, 169 | serverOptions: { 170 | port: number; 171 | strictPort: boolean | undefined; 172 | host: string | undefined; 173 | logger: Vite.Logger; 174 | } 175 | ): Promise { 176 | return new Promise((resolve, reject) => { 177 | let { port, strictPort, host, logger } = serverOptions; 178 | 179 | const onError = (e: Error & { code?: string }) => { 180 | if (e.code === 'EADDRINUSE') { 181 | if (strictPort) { 182 | httpServer.removeListener('error', onError); 183 | reject(new Error(`Port ${port} is already in use`)); 184 | } else { 185 | logger.info(`Port ${port} is in use, trying another one...`); 186 | httpServer.listen(++port, host); 187 | } 188 | } else { 189 | httpServer.removeListener('error', onError); 190 | reject(e); 191 | } 192 | }; 193 | 194 | httpServer.on('error', onError); 195 | 196 | httpServer.listen(port, host, () => { 197 | httpServer.removeListener('error', onError); 198 | resolve(port); 199 | }); 200 | }); 201 | } 202 | 203 | export async function preview( 204 | config: Vite.ResolvedConfig, 205 | serverOptions: { host?: string; port?: number } = {} 206 | ): Promise { 207 | const vitext = await createServer({ 208 | ...config, 209 | server: { ...config.server, ...serverOptions, middlewareMode: null }, 210 | } as any); 211 | 212 | // cors 213 | const { cors } = config.server; 214 | if (cors !== false) { 215 | vitext.middlewares.use( 216 | // @ts-ignore 217 | corsMiddleware(typeof cors === 'boolean' ? {} : cors) 218 | ); 219 | } 220 | 221 | // proxy 222 | if (config.server.proxy) { 223 | vitext.middlewares.use(proxyMiddleware(vitext.httpServer, config)); 224 | } 225 | 226 | // @ts-ignore 227 | vitext.middlewares.use(compression()); 228 | 229 | return await vitext.listen(); 230 | } 231 | -------------------------------------------------------------------------------- /packages/vitext/src/node/proxy.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http' 2 | import httpProxy from 'http-proxy' 3 | import chalk from 'chalk' 4 | import { Connect, HttpProxy, ResolvedConfig } from 'vite' 5 | import { isObject } from './utils' 6 | 7 | const HMR_HEADER = 'vite-hmr' 8 | export interface ProxyOptions extends HttpProxy.ServerOptions { 9 | /** 10 | * rewrite path 11 | */ 12 | rewrite?: (path: string) => string 13 | /** 14 | * configure the proxy server (e.g. listen to events) 15 | */ 16 | configure?: (proxy: HttpProxy.Server, options: ProxyOptions) => void 17 | /** 18 | * webpack-dev-server style bypass function 19 | */ 20 | bypass?: ( 21 | req: http.IncomingMessage, 22 | res: http.ServerResponse, 23 | options: ProxyOptions 24 | ) => void | null | undefined | false | string 25 | } 26 | 27 | export function proxyMiddleware( 28 | httpServer: http.Server | null, 29 | config: ResolvedConfig 30 | ): Connect.NextHandleFunction { 31 | const options = config.server.proxy! 32 | 33 | // lazy require only when proxy is used 34 | const proxies: Record = {} 35 | 36 | Object.keys(options).forEach((context) => { 37 | let opts = options[context] 38 | if (typeof opts === 'string') { 39 | opts = { target: opts, changeOrigin: true } as ProxyOptions 40 | } 41 | const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server 42 | 43 | proxy.on('error', (err) => { 44 | config.logger.error(`${chalk.red(`http proxy error:`)}\n${err.stack}`, { 45 | timestamp: true 46 | }) 47 | }) 48 | 49 | if (opts.configure) { 50 | opts.configure(proxy, opts) 51 | } 52 | // clone before saving because http-proxy mutates the options 53 | proxies[context] = [proxy, { ...opts }] 54 | }) 55 | 56 | if (httpServer) { 57 | httpServer.on('upgrade', (req, socket, head) => { 58 | const url = req.url! 59 | for (const context in proxies) { 60 | if (url.startsWith(context)) { 61 | const [proxy, opts] = proxies[context] 62 | if ( 63 | (opts.ws || opts.target?.toString().startsWith('ws:')) && 64 | req.headers['sec-websocket-protocol'] !== HMR_HEADER 65 | ) { 66 | if (opts.rewrite) { 67 | req.url = opts.rewrite(url) 68 | } 69 | proxy.ws(req, socket, head) 70 | } 71 | } 72 | } 73 | }) 74 | } 75 | 76 | // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...` 77 | return function viteProxyMiddleware(req, res, next) { 78 | const url = req.url! 79 | for (const context in proxies) { 80 | if ( 81 | (context.startsWith('^') && new RegExp(context).test(url)) || 82 | url.startsWith(context) 83 | ) { 84 | const [proxy, opts] = proxies[context] 85 | const options: HttpProxy.ServerOptions = {} 86 | 87 | if (opts.bypass) { 88 | const bypassResult = opts.bypass(req, res, opts) 89 | if (typeof bypassResult === 'string') { 90 | req.url = bypassResult 91 | return next() 92 | } else if (isObject(bypassResult)) { 93 | Object.assign(options, bypassResult) 94 | return next() 95 | } else if (bypassResult === false) { 96 | return res.end(404) 97 | } 98 | } 99 | 100 | if (opts.rewrite) { 101 | req.url = opts.rewrite(req.url!) 102 | } 103 | proxy.web(req, res, options) 104 | return 105 | } 106 | } 107 | next() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/vitext/src/node/route/export.ts: -------------------------------------------------------------------------------- 1 | import { Mutex, MutexInterface } from 'async-mutex'; 2 | import chalk from 'chalk'; 3 | import isEqual from 'deep-equal'; 4 | import * as fs from 'fs-extra'; 5 | import * as path from 'path'; 6 | import { ParsedUrlQuery } from 'querystring'; 7 | import { Manifest, ViteDevServer } from 'vite'; 8 | 9 | import { AppType } from '../components/_app'; 10 | import { DocumentType } from '../components/_document'; 11 | import type { Entries, GetPathsResult, GetPropsResult } from '../types'; 12 | import { loadPage } from '../utils'; 13 | import { fetchPaths, fetchProps } from './fetch'; 14 | import { getRouteMatcher } from './pages'; 15 | import { renderToHTML } from './render'; 16 | 17 | type Params = ReturnType>; 18 | 19 | const cache: Map = new Map(); 20 | 21 | export async function loadExportedPage({ 22 | root, 23 | pageName, 24 | params, 25 | }: { 26 | root: string; 27 | pageName: string; 28 | params: Params; 29 | }) { 30 | const manifestJSON: Params[] = cache.get(pageName) || []; 31 | try { 32 | const id = manifestJSON.findIndex((p) => 33 | isEqual(params, p, { strict: false }) 34 | ); 35 | 36 | if (id === -1) return; 37 | 38 | const htmlAddress = path.join(root, 'dist/out', pageName, `${id}.html`); 39 | 40 | await fs.promises.access(htmlAddress); 41 | return await fs.promises.readFile(htmlAddress, 'utf-8'); 42 | } catch { 43 | return; 44 | } 45 | } 46 | export async function exportPage({ 47 | server, 48 | entries, 49 | page, 50 | template, 51 | pagesModuleId, 52 | Document, 53 | App, 54 | manifest, 55 | }: { 56 | server: ViteDevServer; 57 | entries: Entries; 58 | page: Entries[number]; 59 | template: string; 60 | pagesModuleId: string; 61 | Document: DocumentType; 62 | App: AppType; 63 | manifest: Manifest; 64 | }) { 65 | const mutex = new Mutex(); 66 | try { 67 | const { 68 | default: Component, 69 | getPaths, 70 | getProps, 71 | } = await loadPage({ server, entries, page }); 72 | 73 | if (!getPaths && getProps) { 74 | return; 75 | } 76 | 77 | if (getPaths && !getProps) { 78 | throw new Error('[vitext] Page contains `getPaths`, but not `getProps`'); 79 | } 80 | 81 | let paths: GetPathsResult['paths'] | undefined; 82 | if (getPaths && getProps) { 83 | paths = (await fetchPaths({ getPaths: getPaths })).paths; 84 | } 85 | 86 | const resultsArray: ( 87 | | Promise> 88 | | GetPropsResult 89 | )[] = paths 90 | ? paths.map(({ params }) => 91 | fetchProps({ getProps: getProps!, params, isExporting: true }) 92 | ) 93 | : [{ props: {} }]; 94 | 95 | const dir = path.join(server.config.root!, 'dist/out', page.pageName); 96 | 97 | const exportManifest = resultsArray.map(async (resultPromise, index) => { 98 | const result = await resultPromise; 99 | const params = paths ? paths[index].params : undefined; 100 | 101 | return { 102 | params, 103 | html: await renderToHTML({ 104 | server, 105 | entries, 106 | template, 107 | pagesModuleId, 108 | Component, 109 | Document, 110 | App, 111 | pageEntry: page, 112 | props: result.props, 113 | manifest, 114 | }), 115 | }; 116 | }); 117 | const manifestAddress = path.join(dir, 'manifest.json'); 118 | 119 | await fs.mkdirp(path.dirname(manifestAddress)); 120 | await fs.promises.writeFile(manifestAddress, JSON.stringify([])); 121 | 122 | exportManifest.forEach(async (filePromise, id) => { 123 | let release: MutexInterface.Releaser | undefined; 124 | try { 125 | const file = await filePromise; 126 | const htmlFile = path.join(dir, `${id}.html`); 127 | 128 | await fs.promises.writeFile(htmlFile, file.html); 129 | 130 | release = await mutex.acquire(); 131 | const manifestFileContent = await fs.promises.readFile( 132 | manifestAddress, 133 | 'utf-8' 134 | ); 135 | 136 | const manifestJSON: any[] = JSON.parse(manifestFileContent); 137 | 138 | manifestJSON.push(file.params); 139 | 140 | await fs.promises.writeFile( 141 | manifestAddress, 142 | JSON.stringify(manifestJSON) 143 | ); 144 | cache.set(page.pageName, manifestJSON); 145 | } catch (error) { 146 | server.config.logger.error( 147 | chalk.red(`[vitext] writing to file failed. error:\n`), 148 | error 149 | ); 150 | 151 | server.config.logger.error( 152 | chalk.red( 153 | `exporting ${page.pageName} failed. error:\n${ 154 | error.stack || error.message 155 | }` 156 | ), 157 | { 158 | timestamp: true, 159 | } 160 | ); 161 | } finally { 162 | if (release) release(); 163 | } 164 | }); 165 | server.config.logger.info( 166 | chalk.green(`${page.pageName} exported successfully`), 167 | { 168 | timestamp: true, 169 | } 170 | ); 171 | } catch (error) { 172 | server.config.logger.error( 173 | chalk.red( 174 | `exporting ${page.pageName} failed. error:\n${ 175 | error.stack || error.message 176 | }` 177 | ), 178 | { 179 | timestamp: true, 180 | } 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /packages/vitext/src/node/route/fetch.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'deep-equal'; 2 | import type { ServerResponse } from 'http'; 3 | import * as querystring from 'querystring'; 4 | import type { ConfigEnv, Connect } from 'vite'; 5 | 6 | import type { GetPaths, GetProps, PageFileType } from '../types'; 7 | import type { PageType } from './pages'; 8 | 9 | export async function fetchData({ 10 | env, 11 | req, 12 | res, 13 | pageFile, 14 | page, 15 | isExporting, 16 | }: { 17 | env: ConfigEnv; 18 | req?: Connect.IncomingMessage; 19 | res?: ServerResponse; 20 | pageFile: PageFileType; 21 | page: PageType; 22 | isExporting: boolean; 23 | }) { 24 | const query = querystring.parse(req?.originalUrl); 25 | 26 | let params: querystring.ParsedUrlQuery | undefined = page.params; 27 | 28 | // non-dynamic pages should not have getPaths 29 | if ( 30 | env.mode === 'development' && 31 | 'getPaths' in pageFile && 32 | page.pageEntry.pageName.includes('[') 33 | ) { 34 | const { paths } = await fetchPaths({ getPaths: pageFile.getPaths! }); 35 | 36 | params = paths.find((p) => 37 | isEqual(page.params, p.params, { strict: false }) 38 | )?.params; 39 | } 40 | 41 | if ('getProps' in pageFile) { 42 | const getPropsResult = await fetchProps({ 43 | req, 44 | res, 45 | query, 46 | params, 47 | getProps: pageFile.getProps!, 48 | isExporting, 49 | }); 50 | 51 | if (!isExporting && getPropsResult.notFound) { 52 | res!.statusCode = 404; 53 | return; 54 | } 55 | 56 | if (getPropsResult.revalidate) { 57 | // TODO 58 | } 59 | return getPropsResult; 60 | } 61 | return; 62 | } 63 | 64 | export function fetchProps({ 65 | req, 66 | res, 67 | query, 68 | getProps, 69 | params, 70 | isExporting, 71 | }: { 72 | req?: Connect.IncomingMessage; 73 | res?: ServerResponse; 74 | query?: querystring.ParsedUrlQuery; 75 | getProps: GetProps; 76 | params?: querystring.ParsedUrlQuery; 77 | isExporting: boolean; 78 | }) { 79 | return getProps({ req, res, query: query || {}, params, isExporting }); 80 | } 81 | 82 | export function fetchPaths({ getPaths }: { getPaths: GetPaths }) { 83 | return getPaths(); 84 | } 85 | -------------------------------------------------------------------------------- /packages/vitext/src/node/route/pages.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import type { ParsedUrlQuery } from 'querystring'; 3 | import { Manifest } from 'vite'; 4 | 5 | import { Entries } from '../types'; 6 | 7 | export const DYNAMIC_PAGE = new RegExp('\\[(\\w+)\\]', 'g'); 8 | const publicPaths = ['/favicon.ico', '/__vite_ping']; 9 | 10 | export type PageType = ReturnType & {}; 11 | 12 | export function resolvePagePath(pagePath: string, entries: Entries) { 13 | if (publicPaths.includes(pagePath)) { 14 | return; 15 | } 16 | 17 | const pagesMap = entries.map((pageEntry) => { 18 | const route = getRouteRegex(pageEntry.pageName); 19 | 20 | return { 21 | pageEntry, 22 | route, 23 | matcher: getRouteMatcher(route), 24 | params: {} as ReturnType>, 25 | query: {} as ParsedUrlQuery, 26 | }; 27 | }); 28 | 29 | const page = pagesMap.find((p) => p.route.re.test(pagePath)); 30 | 31 | if (!page) return; 32 | if (!Object.keys(page.route.groups).length) return page; 33 | 34 | page.params = page.matcher(pagePath); 35 | 36 | return page; 37 | } 38 | 39 | export function getEntries( 40 | pageManifest: string[], 41 | mode: string, 42 | manifest: Manifest 43 | ) { 44 | const prefix = mode === 'development' ? './pages' : 'pages'; 45 | 46 | const entries: { 47 | absolutePagePath: string; 48 | pageName: string; 49 | manifestAddress?: string; 50 | }[] = []; 51 | pageManifest.forEach((page) => { 52 | if (/pages\/api\//.test(page)) return; 53 | 54 | const pageWithoutBase = page.slice(prefix.length, page.length - 1); 55 | let pageName = '/' + pageWithoutBase.match(/\/(.+)\.(js|jsx|ts|tsx)$/)![1]; 56 | 57 | if (pageName.endsWith('/index')) { 58 | pageName = pageName.replace(/\/index$/, '/'); 59 | } 60 | 61 | entries.push({ 62 | absolutePagePath: 63 | mode === 'development' ? page : path.join('dist', manifest[page].file), 64 | pageName: pageName, 65 | manifestAddress: mode === 'development' ? undefined : page, 66 | }); 67 | }); 68 | return entries; 69 | } 70 | 71 | export interface Group { 72 | pos: number; 73 | repeat: boolean; 74 | optional: boolean; 75 | } 76 | 77 | // this isn't importing the escape-string-regex module 78 | // to reduce bytes 79 | function escapeRegex(str: string) { 80 | return str.replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&'); 81 | } 82 | 83 | function parseParameter(param: string) { 84 | const optional = param.startsWith('[') && param.endsWith(']'); 85 | if (optional) { 86 | param = param.slice(1, -1); 87 | } 88 | const repeat = param.startsWith('...'); 89 | if (repeat) { 90 | param = param.slice(3); 91 | } 92 | return { key: param, repeat, optional }; 93 | } 94 | 95 | // from next.js 96 | export function getRouteRegex(normalizedRoute: string): { 97 | re: RegExp; 98 | namedRegex?: string; 99 | routeKeys?: { [named: string]: string }; 100 | groups: { [groupName: string]: Group }; 101 | } { 102 | const segments = (normalizedRoute.replace(/\/$/, '') || '/') 103 | .slice(1) 104 | .split('/'); 105 | const groups: { [groupName: string]: Group } = {}; 106 | let groupIndex = 1; 107 | const parameterizedRoute = segments 108 | .map((segment) => { 109 | if (segment.startsWith('[') && segment.endsWith(']')) { 110 | const { key, optional, repeat } = parseParameter(segment.slice(1, -1)); 111 | groups[key] = { pos: groupIndex++, repeat, optional }; 112 | return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)'; 113 | } else { 114 | return `/${escapeRegex(segment)}`; 115 | } 116 | }) 117 | .join(''); 118 | 119 | let routeKeyCharCode = 97; 120 | let routeKeyCharLength = 1; 121 | 122 | // builds a minimal routeKey using only a-z and minimal number of characters 123 | const getSafeRouteKey = () => { 124 | let routeKey = ''; 125 | 126 | for (let i = 0; i < routeKeyCharLength; i++) { 127 | routeKey += String.fromCharCode(routeKeyCharCode); 128 | routeKeyCharCode++; 129 | 130 | if (routeKeyCharCode > 122) { 131 | routeKeyCharLength++; 132 | routeKeyCharCode = 97; 133 | } 134 | } 135 | return routeKey; 136 | }; 137 | 138 | const routeKeys: { [named: string]: string } = {}; 139 | 140 | const namedParameterizedRoute = segments 141 | .map((segment) => { 142 | if (segment.startsWith('[') && segment.endsWith(']')) { 143 | const { key, optional, repeat } = parseParameter(segment.slice(1, -1)); 144 | // replace any non-word characters since they can break 145 | // the named regex 146 | let cleanedKey = key.replace(/\W/g, ''); 147 | let invalidKey = false; 148 | 149 | // check if the key is still invalid and fallback to using a known 150 | // safe key 151 | if (cleanedKey.length === 0 || cleanedKey.length > 30) { 152 | invalidKey = true; 153 | } 154 | if (!isNaN(parseInt(cleanedKey.substr(0, 1)))) { 155 | invalidKey = true; 156 | } 157 | 158 | if (invalidKey) { 159 | cleanedKey = getSafeRouteKey(); 160 | } 161 | 162 | routeKeys[cleanedKey] = key; 163 | return repeat 164 | ? optional 165 | ? `(?:/(?<${cleanedKey}>.+?))?` 166 | : `/(?<${cleanedKey}>.+?)` 167 | : `/(?<${cleanedKey}>[^/]+?)`; 168 | } else { 169 | return `/${escapeRegex(segment)}`; 170 | } 171 | }) 172 | .join(''); 173 | 174 | return { 175 | re: new RegExp(`^${parameterizedRoute}(?:/)?$`), 176 | groups, 177 | routeKeys, 178 | namedRegex: `^${namedParameterizedRoute}(?:/)?$`, 179 | }; 180 | } 181 | 182 | export function getRouteMatcher(routeRegex: ReturnType) { 183 | const { re, groups } = routeRegex; 184 | return (pathname: string | null | undefined) => { 185 | const routeMatch = re.exec(pathname!); 186 | if (!routeMatch) { 187 | return {}; 188 | } 189 | 190 | const decode = (param: string) => { 191 | try { 192 | return decodeURIComponent(param); 193 | } catch (_) { 194 | throw new Error('failed to decode param'); 195 | } 196 | }; 197 | const params: { [paramName: string]: string | string[] } = {}; 198 | 199 | Object.keys(groups).forEach((slugName: string) => { 200 | const g = groups[slugName]; 201 | const m = routeMatch[g.pos]; 202 | if (m !== undefined) { 203 | params[slugName] = ~m.indexOf('/') 204 | ? m.split('/').map((entry) => decode(entry)) 205 | : g.repeat 206 | ? [decode(m)] 207 | : decode(m); 208 | } 209 | }); 210 | return params; 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /packages/vitext/src/node/route/render.tsx: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import React from 'react'; 3 | import ReactDOMServer from 'react-dom/server.js'; 4 | import { Manifest, ModuleNode, ViteDevServer } from 'vite'; 5 | 6 | import Loadable from '../../react/loadable'; 7 | import type { AppType } from '../components/_app'; 8 | import type { DocumentType } from '../components/_document'; 9 | import { Entries, GetPropsResult } from '../types'; 10 | 11 | function collectCss( 12 | mod: ModuleNode | undefined, 13 | preloadUrls: Set, 14 | visitedModules: Set 15 | ): void { 16 | if (!mod) return; 17 | if (!mod.url) return; 18 | if (visitedModules.has(mod.url)) return; 19 | visitedModules.add(mod.url); 20 | 21 | if (mod.url.endsWith('.css')) { 22 | preloadUrls.add(mod.url); 23 | } 24 | mod.importedModules.forEach((dep) => { 25 | collectCss(dep, preloadUrls, visitedModules); 26 | }); 27 | } 28 | 29 | function setToCss(preloadUrls: Set) { 30 | return [...preloadUrls] 31 | .map((url) => ``) 32 | .join('\n'); 33 | } 34 | 35 | export async function renderToHTML({ 36 | entries, 37 | server, 38 | pageEntry, 39 | props, 40 | template, 41 | pagesModuleId, 42 | Component, 43 | Document, 44 | App, 45 | manifest, 46 | }: { 47 | entries: Entries; 48 | server: ViteDevServer; 49 | pageEntry: Entries[number]; 50 | props: GetPropsResult['props']; 51 | template: string; 52 | pagesModuleId: string; 53 | Component: React.ComponentType; 54 | Document: DocumentType; 55 | App: AppType; 56 | manifest: Manifest; 57 | }): Promise { 58 | const WrappedPage = () => ; 59 | 60 | const { helmetContext, Page } = Document.renderDocument(Document, { 61 | props, 62 | Component: WrappedPage, 63 | pageClientPath: 64 | pagesModuleId + (pageEntry.pageName !== '/' ? pageEntry.pageName : ''), 65 | }); 66 | 67 | await Loadable.preloadAll(); 68 | const componentHTML = ReactDOMServer.renderToString(Page); 69 | 70 | const preloadUrls = new Set(); 71 | const visitedModules = new Set(); 72 | const file = path.join(server.config.root, pageEntry.absolutePagePath); 73 | const appEntry = entries.find((entry) => entry.pageName.includes('_app')); 74 | const appFile = appEntry?.absolutePagePath; 75 | 76 | if (Object.entries(manifest || {}).length) { 77 | if (appEntry) { 78 | manifest[appEntry.manifestAddress!].css?.forEach((css) => 79 | preloadUrls.add(path.join(server.config.root, 'dist', css)) 80 | ); 81 | } 82 | 83 | manifest[pageEntry.manifestAddress!].css?.forEach((css) => 84 | preloadUrls.add(path.join(server.config.root, 'dist', css)) 85 | ); 86 | } 87 | 88 | if (server.config.mode === 'development') { 89 | if (appFile) { 90 | collectCss( 91 | await server.moduleGraph.getModuleByUrl(appFile), 92 | preloadUrls, 93 | visitedModules 94 | ); 95 | } 96 | collectCss( 97 | await server.moduleGraph.getModuleByUrl(file), 98 | preloadUrls, 99 | visitedModules 100 | ); 101 | } 102 | 103 | const stylesString = setToCss(preloadUrls); 104 | 105 | const headHtml = ` 106 | ${helmetContext.helmet.title.toString()} 107 | ${helmetContext.helmet.meta.toString()} 108 | ${helmetContext.helmet.link.toString()} 109 | ${helmetContext.helmet.noscript.toString()} 110 | ${helmetContext.helmet.script.toString()} 111 | ${helmetContext.helmet.style.toString()} 112 | ${stylesString} 113 | `; 114 | 115 | const html = template 116 | .replace('', componentHTML) 117 | .replace('', headHtml + '') 118 | .replace(' 2 | /// 3 | import { IncomingMessage, ServerResponse } from 'http'; 4 | import { ParsedUrlQuery } from 'querystring'; 5 | import React from 'react'; 6 | 7 | import { getEntries } from './route/pages'; 8 | 9 | export type Entries = ReturnType; 10 | 11 | export type GetPathsResult

= { 12 | paths: Array<{ params: P }>; 13 | }; 14 | 15 | export type GetPaths

= () => 16 | | Promise> 17 | | GetPathsResult

; 18 | 19 | export type GetPropsContext = { 20 | req?: IncomingMessage; 21 | res?: ServerResponse; 22 | params?: Q; 23 | query: ParsedUrlQuery; 24 | isExporting: boolean; 25 | }; 26 | 27 | type JustProps

= { props: P; revalidate?: number | boolean }; 28 | type NotFound = { notFound?: true }; 29 | 30 | export type GetPropsResult

= JustProps

& NotFound; 31 | 32 | export type GetProps< 33 | P extends { [key: string]: any } = { [key: string]: any }, 34 | Q extends ParsedUrlQuery = ParsedUrlQuery 35 | > = (context: GetPropsContext) => Promise>; 36 | 37 | export type InferGetPropsType = T extends GetProps 38 | ? P 39 | : T extends ( 40 | context?: GetPropsContext 41 | ) => Promise> 42 | ? P 43 | : never; 44 | 45 | export interface PageFileType { 46 | default: React.ComponentType; 47 | getProps?: GetProps; 48 | getPaths?: GetPaths; 49 | } 50 | 51 | export type Await = T extends { 52 | then(onfulfilled?: (value: infer U) => unknown): unknown; 53 | } ? U : T; 54 | -------------------------------------------------------------------------------- /packages/vitext/src/node/utils.ts: -------------------------------------------------------------------------------- 1 | // Copied from flareact 2 | import reactRefresh from '@vitejs/plugin-react-refresh'; 3 | import glob from 'fast-glob'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import React from 'react'; 7 | import Vite from 'vite'; 8 | import { App as BaseApp, AppType } from 'vitext/app.js'; 9 | import { Document as BaseDocument, DocumentType } from 'vitext/document.js'; 10 | 11 | import { createVitextPlugin } from './plugin'; 12 | import { DYNAMIC_PAGE, getEntries } from './route/pages'; 13 | import { Entries, PageFileType } from './types'; 14 | 15 | export function isObject(value: unknown): value is Record { 16 | return Object.prototype.toString.call(value) === '[object Object]'; 17 | } 18 | 19 | export interface Hostname { 20 | // undefined sets the default behaviour of server.listen 21 | host: string | undefined; 22 | // resolve to localhost when possible 23 | name: string; 24 | } 25 | 26 | export function resolveHostname( 27 | optionsHost: string | boolean | undefined 28 | ): Hostname { 29 | let host: string | undefined; 30 | if ( 31 | optionsHost === undefined || 32 | optionsHost === false || 33 | optionsHost === 'localhost' 34 | ) { 35 | // Use a secure default 36 | host = '127.0.0.1'; 37 | } else if (optionsHost === true) { 38 | // If passed --host in the CLI without arguments 39 | host = undefined; // undefined typically means 0.0.0.0 or :: (listen on all IPs) 40 | } else { 41 | host = optionsHost; 42 | } 43 | 44 | // Set host name to localhost when possible, unless the user explicitly asked for '127.0.0.1' 45 | const name = 46 | (optionsHost !== '127.0.0.1' && host === '127.0.0.1') || 47 | host === '0.0.0.0' || 48 | host === '::' || 49 | host === undefined 50 | ? 'localhost' 51 | : host; 52 | 53 | return { host, name }; 54 | } 55 | export function extractDynamicParams(source: string, path: string) { 56 | let test: RegExp | string = source; 57 | const parts = []; 58 | const params: Record = {}; 59 | 60 | for (const match of source.matchAll(/\[(\w+)\]/g)) { 61 | parts.push(match[1]); 62 | 63 | test = test.replace(DYNAMIC_PAGE, () => '([\\w_-]+)'); 64 | } 65 | 66 | test = new RegExp(test, 'g'); 67 | 68 | const matches = path.matchAll(test); 69 | 70 | for (const match of matches) { 71 | parts.forEach((part, idx) => (params[part] = match[idx + 1])); 72 | } 73 | 74 | return params; 75 | } 76 | 77 | // This utility is based on https://github.com/zertosh/htmlescape 78 | // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE 79 | 80 | const ESCAPE_LOOKUP: Record = { 81 | '&': '\\u0026', 82 | '>': '\\u003e', 83 | '<': '\\u003c', 84 | '\u2028': '\\u2028', 85 | '\u2029': '\\u2029', 86 | }; 87 | 88 | const ESCAPE_REGEX = /[&><\u2028\u2029]/g; 89 | 90 | export function htmlEscapeJsonString(str: string) { 91 | return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]); 92 | } 93 | 94 | const importQueryRE = /(\?|&)import=?(?:&|$)/; 95 | const trailingSeparatorRE = /[\?&]$/; 96 | 97 | export function removeImportQuery(url: string): string { 98 | return url.replace(importQueryRE, '$1').replace(trailingSeparatorRE, ''); 99 | } 100 | 101 | type ComponentFileType = { default: AppType | DocumentType } & Record< 102 | string, 103 | any 104 | >; 105 | 106 | type PromisedComponentFileType = Promise | ComponentFileType; 107 | 108 | export async function resolveCustomComponents({ 109 | entries, 110 | server, 111 | }: { 112 | entries: ReturnType; 113 | server: Vite.ViteDevServer; 114 | }) { 115 | const customApp = entries.find((page) => page.pageName === '/_app'); 116 | const customDocument = entries.find((page) => page.pageName === '/_document'); 117 | 118 | let AppFile: PromisedComponentFileType = { default: BaseApp }; 119 | if (customApp) { 120 | AppFile = server.ssrLoadModule( 121 | customApp!.absolutePagePath 122 | ) as PromisedComponentFileType; 123 | } 124 | 125 | let DocumentFile: PromisedComponentFileType = { default: BaseDocument }; 126 | if (customDocument) { 127 | DocumentFile = server.ssrLoadModule( 128 | customDocument!.absolutePagePath 129 | ) as PromisedComponentFileType; 130 | } 131 | 132 | const [{ default: Document }, { default: App }] = await Promise.all([ 133 | DocumentFile, 134 | AppFile, 135 | ]); 136 | return { Document, App } as { 137 | Document: typeof BaseDocument; 138 | App: typeof BaseApp; 139 | }; 140 | } 141 | 142 | /* 143 | * /@fs/..../@vitext/hack-import/...js to /@vitext/hack-import/... 144 | */ 145 | export function resolveHackImport(id: string) { 146 | const str = '/@vitext/hack-import'; 147 | const portionIndex = id.search(str); 148 | const strLength = str.length; 149 | if (portionIndex < 0) return id; 150 | return id.slice(portionIndex + strLength, id.length - 3); 151 | } 152 | 153 | export async function getEntryPoints( 154 | config: Vite.UserConfig | Vite.ViteDevServer['config'] 155 | ) { 156 | return await glob('./pages/**/*.+(js|jsx|ts|tsx)', { 157 | cwd: config.root, 158 | }); 159 | } 160 | 161 | const returnConfigFiles = (root: string) => 162 | ['vitext.config.js', 'vitext.config.ts'].map((file) => 163 | path.resolve(root, file) 164 | ); 165 | 166 | export async function resolveInlineConfig( 167 | options: Vite.InlineConfig & Vite.UserConfig & { root: string }, 168 | command: 'build' | 'serve' 169 | ): Promise { 170 | const configFile: string = 171 | returnConfigFiles(options.root).find((file) => fs.existsSync(file)) || 172 | './vitext.config.js'; 173 | 174 | const config = await Vite.resolveConfig({ ...options, configFile }, command); 175 | 176 | if (command === 'build') { 177 | // @ts-ignore vite#issues#4016#4096 178 | config.plugins = config.plugins.filter( 179 | (p) => p.name !== 'vite:import-analysis' 180 | ); 181 | } 182 | 183 | return { 184 | ...config, 185 | assetsInclude: options.assetsInclude, 186 | configFile: configFile, 187 | plugins: [ 188 | { 189 | ...reactRefresh({ 190 | exclude: [/vitext\/dynamic\.js/, /vitext\/app\.js/], 191 | }), 192 | enforce: 'post', 193 | }, 194 | ...createVitextPlugin(), 195 | ...config.plugins, 196 | ], 197 | }; 198 | } 199 | 200 | export async function loadPage({ 201 | server, 202 | entries, 203 | page, 204 | }: { 205 | server: Vite.ViteDevServer; 206 | entries: Entries; 207 | page: Entries[number]; 208 | }) { 209 | const absolutePagePath = entries.find( 210 | (p) => p.pageName === page.pageName 211 | )!.absolutePagePath; 212 | 213 | return server.ssrLoadModule( 214 | path.join(server.config.root || '', absolutePagePath) 215 | ) as Promise; 216 | } 217 | 218 | export const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)`; 219 | export const jsLangs = `\\.(js|ts|jsx|tsx)($|\\?)`; 220 | export const jsLangsRE = new RegExp(jsLangs); 221 | export const cssLangRE = new RegExp(cssLangs); 222 | export const cssModuleRE = new RegExp(`\\.module${cssLangs}`); 223 | export const directRequestRE = /(\?|&)direct\b/; 224 | export const commonjsProxyRE = /\?commonjs-proxy/; 225 | -------------------------------------------------------------------------------- /packages/vitext/src/react/dynamic.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 Vercel, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | // Modified to be compatible with vitext 25 | import React from 'react'; 26 | // eslint-disable-next-line 27 | import Loadable, { CommonOptions } from 'vitext/src/react/loadable'; 28 | 29 | const isServerSide = typeof window === 'undefined'; 30 | 31 | export type Loader

= () => Promise

; 32 | 33 | export type LoadableOptions

= Omit & { 34 | loader?: Loader

; 35 | loading?: ({ 36 | error, 37 | isLoading, 38 | pastDelay, 39 | }: { 40 | error?: Error | null; 41 | isLoading?: boolean; 42 | pastDelay?: boolean; 43 | retry?: () => void; 44 | timedOut?: boolean; 45 | }) => JSX.Element | null; 46 | }; 47 | 48 | export type DynamicOptions

= LoadableOptions

; 49 | 50 | export type LoadableFn

= (opts: LoadableOptions

) => P; 51 | 52 | export type LoadableComponent

= P; 53 | 54 | type Props

= P & { 55 | fallback: string | LoadableOptions['loading'] | JSX.Element; 56 | }; 57 | function createDynamicComponent

( 58 | loadableOptions: LoadableOptions>, 59 | opts: { server: boolean } 60 | ): React.ComponentType> { 61 | const loadableFn: LoadableFn> = Loadable; 62 | const ResultComponent: React.ComponentType = loadableFn(loadableOptions); 63 | 64 | // Todo please clean this, that's total trash, to get ready for the release 65 | const init = (ResultComponent as any).render.init as () => void; 66 | if (isServerSide && !opts.server) { 67 | (globalThis as any).ALL_INITIALIZERS = ( 68 | (globalThis as any).ALL_INITIALIZERS as (() => void)[] 69 | ).filter((func) => func !== init); 70 | } 71 | 72 | const DynamicComponent: React.ComponentType> = ({ 73 | ...props 74 | }: P & { 75 | fallback: string | LoadableOptions['loading'] | JSX.Element; 76 | }) => { 77 | loadableOptions.loading = 78 | typeof props.fallback === 'function' 79 | ? props.fallback 80 | : () => <>{props.fallback}; 81 | 82 | const Loading = loadableOptions.loading!; 83 | // This will only be rendered on the server side 84 | if (isServerSide && !opts.server) { 85 | return ( 86 | 87 | ); 88 | } 89 | return ; 90 | }; 91 | 92 | return DynamicComponent; 93 | } 94 | 95 | export default function dynamic

( 96 | loader: Loader>, 97 | opts: { server: boolean } = { server: true } 98 | ): React.ComponentType> { 99 | const loadableOptions: LoadableOptions> = { 100 | delay: 400, 101 | }; 102 | 103 | loadableOptions.loader = loader; 104 | 105 | return createDynamicComponent(loadableOptions, opts); 106 | } 107 | -------------------------------------------------------------------------------- /packages/vitext/src/react/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react/index'; 2 | 3 | type Props = { 4 | progressive?: boolean; 5 | server?: boolean; 6 | }; 7 | 8 | const SuspenseServer: React.ComponentType = ({ 9 | fallback, 10 | }) => { 11 | return <>{fallback} 12 | }; 13 | 14 | const isServerSide = typeof window === 'undefined'; 15 | 16 | const injectedReact: typeof React = { 17 | ...React, 18 | Suspense: isServerSide ? SuspenseServer as any : React.Suspense, 19 | }; 20 | 21 | const { 22 | lazy, 23 | Children, 24 | Component, 25 | Fragment, 26 | Profiler, 27 | PureComponent, 28 | StrictMode, 29 | cloneElement, 30 | createContext, 31 | createElement, 32 | createFactory, 33 | createRef, 34 | forwardRef, 35 | isValidElement, 36 | memo, 37 | useCallback, 38 | useContext, 39 | useDebugValue, 40 | useEffect, 41 | useImperativeHandle, 42 | useLayoutEffect, 43 | useMemo, 44 | useReducer, 45 | useRef, 46 | useState, 47 | version, 48 | Suspense 49 | } = injectedReact; 50 | 51 | export { 52 | Children, 53 | Component, 54 | Fragment, 55 | Profiler, 56 | PureComponent, 57 | StrictMode, 58 | cloneElement, 59 | createContext, 60 | createElement, 61 | createFactory, 62 | createRef, 63 | forwardRef, 64 | isValidElement, 65 | memo, 66 | useCallback, 67 | useContext, 68 | useDebugValue, 69 | useEffect, 70 | useImperativeHandle, 71 | useLayoutEffect, 72 | useMemo, 73 | useReducer, 74 | useRef, 75 | useState, 76 | version, 77 | lazy, 78 | Suspense, 79 | }; 80 | 81 | export default injectedReact 82 | 83 | -------------------------------------------------------------------------------- /packages/vitext/src/react/loadable.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | import React from 'react'; 3 | 4 | declare namespace LoadableExport { 5 | interface LoadingComponentProps { 6 | isLoading: boolean; 7 | pastDelay: boolean; 8 | timedOut: boolean; 9 | error: any; 10 | retry: () => void; 11 | } 12 | interface CommonOptions { 13 | /** 14 | * React component displayed after delay until loader() succeeds. Also responsible for displaying errors. 15 | * 16 | * If you don't want to render anything you can pass a function that returns null 17 | * (this is considered a valid React component). 18 | */ 19 | loading: React.ComponentType; 20 | /** 21 | * Defaults to 200, in milliseconds. 22 | * 23 | * Only show the loading component if the loader() has taken this long to succeed or error. 24 | */ 25 | delay?: number | false | null | undefined; 26 | /** 27 | * Disabled by default. 28 | * 29 | * After the specified time in milliseconds passes, the component's `timedOut` prop will be set to true. 30 | */ 31 | timeout?: number | false | null | undefined; 32 | 33 | /** 34 | * Optional array of module paths that `Loadable.Capture`'s `report` function will be applied on during 35 | * server-side rendering. This helps the server know which modules were imported/used during SSR. 36 | * ```ts 37 | * Loadable({ 38 | * loader: () => import('./my-component'), 39 | * modules: ['./my-component'], 40 | * }); 41 | * ``` 42 | */ 43 | modules?: string[] | undefined; 44 | 45 | /** 46 | * An optional function which returns an array of Webpack module ids which you can get 47 | * with require.resolveWeak. This is used by the client (inside `Loadable.preloadReady`) to 48 | * guarantee each webpack module is preloaded before the first client render. 49 | * ```ts 50 | * Loadable({ 51 | * loader: () => import('./Foo'), 52 | * webpack: () => [require.resolveWeak('./Foo')], 53 | * }); 54 | * ``` 55 | */ 56 | webpack?: (() => Array) | undefined; 57 | } 58 | interface ILoadable { 59 |

>(opts: any): React.ComponentType

; 60 | Map

>( 61 | opts: any 62 | ): React.ComponentType

; 63 | preloadAll(): Promise; 64 | preloadReady(): Promise; 65 | } 66 | } 67 | 68 | // eslint-disable-next-line no-redeclare 69 | declare const LoadableExport: LoadableExport.ILoadable; 70 | 71 | export = LoadableExport; 72 | -------------------------------------------------------------------------------- /packages/vitext/src/react/loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | @copyright (c) 2017-present James Kyle 3 | MIT License 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 19 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE 20 | */ 21 | // https://github.com/jamiebuilds/react-loadable/blob/v5.5.0/src/index.js 22 | // Modified to be compatible with webpack 4 / Next.js 23 | // @ts-check 24 | import React from 'react'; 25 | import useSubscriptionDefault from 'use-subscription'; 26 | 27 | export const LoadableContext = React.createContext(null); 28 | 29 | if (!(globalThis.ALL_INITIALIZERS && globalThis.READY_INITIALIZERS)) { 30 | globalThis.ALL_INITIALIZERS = globalThis.ALL_INITIALIZERS ?? []; 31 | globalThis.READY_INITIALIZERS = globalThis.READY_INITIALIZERS ?? []; 32 | } 33 | const READY_INITIALIZERS = globalThis.READY_INITIALIZERS; 34 | let initialized = false; 35 | 36 | function load(loader) { 37 | const promise = loader(); 38 | 39 | const state = { 40 | loading: true, 41 | loaded: null, 42 | error: null, 43 | }; 44 | 45 | state.promise = promise 46 | .then((loaded) => { 47 | state.loading = false; 48 | state.loaded = loaded; 49 | return loaded; 50 | }) 51 | .catch((err) => { 52 | state.loading = false; 53 | state.error = err; 54 | throw err; 55 | }); 56 | 57 | return state; 58 | } 59 | 60 | function resolve(obj) { 61 | return obj && obj.__esModule ? obj.default : obj; 62 | } 63 | 64 | function createLoadableComponent(loadFn, options) { 65 | const defaultOpts = { 66 | delay: 200, 67 | loader: null, 68 | loading: null, 69 | timeout: null, 70 | webpack: null, 71 | modules: null, 72 | }; 73 | 74 | Object.keys((key) => (options[key] = options[key] ?? defaultOpts[key])); 75 | 76 | const opts = options; 77 | // let opts = Object.assign( 78 | // { 79 | // delay: 200, 80 | // loader: null, 81 | // loading: null, 82 | // timeout: null, 83 | // webpack: null, 84 | // modules: null, 85 | // }, 86 | // options 87 | // ); 88 | 89 | let subscription = null; 90 | 91 | function init() { 92 | if (!subscription) { 93 | const sub = new LoadableSubscription(loadFn, opts); 94 | subscription = { 95 | getCurrentValue: sub.getCurrentValue.bind(sub), 96 | subscribe: sub.subscribe.bind(sub), 97 | retry: sub.retry.bind(sub), 98 | promise: sub.promise.bind(sub), 99 | }; 100 | } 101 | return subscription.promise(); 102 | } 103 | 104 | // Server only 105 | if (typeof window === 'undefined') { 106 | globalThis.ALL_INITIALIZERS.push(init); 107 | } 108 | 109 | // Client only 110 | if ( 111 | !initialized && 112 | typeof window !== 'undefined' 113 | // typeof opts.webpack === 'function' && 114 | // typeof require.resolveWeak === 'function' 115 | ) { 116 | // const moduleIds = opts.webpack(); 117 | READY_INITIALIZERS.push((ids) => { 118 | return init(); 119 | }); 120 | } 121 | 122 | const LoadableComponent = (props, ref) => { 123 | init(); 124 | 125 | const context = React.useContext(LoadableContext); 126 | const state = useSubscriptionDefault.useSubscription(subscription); 127 | 128 | React.useImperativeHandle( 129 | ref, 130 | () => ({ 131 | retry: subscription.retry, 132 | }), 133 | [] 134 | ); 135 | 136 | if (context && Array.isArray(opts.modules)) { 137 | opts.modules.forEach((moduleName) => { 138 | context(moduleName); 139 | }); 140 | } 141 | 142 | return React.useMemo(() => { 143 | if (state.loading || state.error) { 144 | return React.createElement(opts.loading, { 145 | isLoading: state.loading, 146 | pastDelay: state.pastDelay, 147 | timedOut: state.timedOut, 148 | error: state.error, 149 | retry: subscription.retry, 150 | }); 151 | } else if (state.loaded) { 152 | return React.createElement(resolve(state.loaded), props); 153 | } else { 154 | return null; 155 | } 156 | }, [props, state]); 157 | }; 158 | 159 | LoadableComponent.preload = () => init(); 160 | LoadableComponent.init = init; 161 | 162 | return React.forwardRef(LoadableComponent); 163 | } 164 | 165 | class LoadableSubscription { 166 | constructor(loadFn, opts) { 167 | this._loadFn = loadFn; 168 | this._opts = opts; 169 | this._callbacks = new Set(); 170 | this._delay = null; 171 | this._timeout = null; 172 | 173 | this.retry(); 174 | } 175 | 176 | promise() { 177 | return this._res.promise; 178 | } 179 | 180 | retry() { 181 | this._clearTimeouts(); 182 | this._res = this._loadFn(this._opts.loader); 183 | 184 | this._state = { 185 | pastDelay: false, 186 | timedOut: false, 187 | }; 188 | 189 | const { _res: res, _opts: opts } = this; 190 | 191 | if (res.loading) { 192 | if (typeof opts.delay === 'number') { 193 | if (opts.delay === 0) { 194 | this._state.pastDelay = true; 195 | } else { 196 | this._delay = setTimeout(() => { 197 | this._update({ 198 | pastDelay: true, 199 | }); 200 | }, opts.delay); 201 | } 202 | } 203 | 204 | if (typeof opts.timeout === 'number') { 205 | this._timeout = setTimeout(() => { 206 | this._update({ timedOut: true }); 207 | }, opts.timeout); 208 | } 209 | } 210 | 211 | this._res.promise 212 | .then(() => { 213 | this._update({}); 214 | this._clearTimeouts(); 215 | }) 216 | .catch((_err) => { 217 | this._update({}); 218 | this._clearTimeouts(); 219 | }); 220 | this._update({}); 221 | } 222 | 223 | _update(partial) { 224 | this._state = { 225 | ...this._state, 226 | error: this._res.error, 227 | loaded: this._res.loaded, 228 | loading: this._res.loading, 229 | ...partial, 230 | }; 231 | this._callbacks.forEach((callback) => callback()); 232 | } 233 | 234 | _clearTimeouts() { 235 | clearTimeout(this._delay); 236 | clearTimeout(this._timeout); 237 | } 238 | 239 | getCurrentValue() { 240 | return this._state; 241 | } 242 | 243 | subscribe(callback) { 244 | this._callbacks.add(callback); 245 | return () => { 246 | this._callbacks.delete(callback); 247 | }; 248 | } 249 | } 250 | 251 | function Loadable(opts) { 252 | return createLoadableComponent(load, opts); 253 | } 254 | 255 | function flushInitializers(initializers, ids) { 256 | const promises = []; 257 | 258 | while (initializers.length) { 259 | const init = initializers.pop(); 260 | promises.push(init(ids)); 261 | } 262 | 263 | return Promise.all(promises).then(() => { 264 | if (initializers.length) { 265 | return flushInitializers(initializers, ids); 266 | } 267 | }); 268 | } 269 | 270 | Loadable.preloadAll = () => { 271 | return new Promise((resolveInitializers, reject) => { 272 | flushInitializers(globalThis.ALL_INITIALIZERS).then( 273 | resolveInitializers, 274 | reject 275 | ); 276 | }); 277 | }; 278 | 279 | Loadable.preloadReady = (ids = []) => { 280 | return new Promise((resolvePreload) => { 281 | const res = () => { 282 | initialized = true; 283 | return resolvePreload(); 284 | }; 285 | // We always will resolve, errors should be handled within loading UIs. 286 | flushInitializers(READY_INITIALIZERS, ids).then(res, res); 287 | }); 288 | }; 289 | 290 | export default Loadable; 291 | -------------------------------------------------------------------------------- /packages/vitext/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleResolution": "Node", 5 | "esModuleInterop": true, 6 | "lib": ["ESNext", "DOM"], 7 | "rootDir": "./", 8 | "resolveJsonModule": true, 9 | "jsx": "react-jsx" 10 | }, 11 | "exclude": [] 12 | } 13 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | semi: true, 4 | singleQuote: true, 5 | importOrderSeparation: true, 6 | importOrder: ['^@core/(.*)$', '^@server/(.*)$', '^@ui/(.*)$', '^[./]'], 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/jestEnv.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const NodeEnvironment = require('jest-environment-node'); 5 | const { chromium } = require('playwright-chromium'); 6 | 7 | const DIR = path.join(os.tmpdir(), 'jest_playwright_global_setup'); 8 | 9 | module.exports = class PlaywrightEnvironment extends NodeEnvironment { 10 | constructor(config, context) { 11 | super(config); 12 | this.testPath = context.testPath; 13 | } 14 | 15 | async setup() { 16 | await super.setup(); 17 | const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf-8'); 18 | if (!wsEndpoint) { 19 | throw new Error('wsEndpoint not found'); 20 | } 21 | 22 | // skip browser setup for non-playground tests 23 | if (!this.testPath.includes('playground')) { 24 | return; 25 | } 26 | 27 | const browser = (this.browser = await chromium.connect({ 28 | wsEndpoint, 29 | })); 30 | this.global.page = await browser.newPage(); 31 | 32 | // suppress @vue/compiler-sfc warning 33 | const console = this.global.console; 34 | const warn = console.warn; 35 | console.warn = (msg, ...args) => { 36 | if (!msg.includes('@vue/compiler-sfc')) { 37 | warn.call(console, msg, ...args); 38 | } 39 | }; 40 | } 41 | 42 | async teardown() { 43 | if (this.browser) { 44 | await this.browser.close(); 45 | } 46 | await super.teardown(); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /scripts/jestGlobalSetup.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const { chromium } = require('playwright-chromium'); 5 | 6 | const DIR = path.join(os.tmpdir(), 'jest_playwright_global_setup'); 7 | 8 | module.exports = async () => { 9 | await fs.remove(path.resolve(__dirname, '../temp')); 10 | const browserServer = await chromium.launchServer({ 11 | headless: !process.env.VITE_DEBUG_SERVE, 12 | args: process.env.CI 13 | ? ['--no-sandbox', '--disable-setuid-sandbox'] 14 | : undefined, 15 | }); 16 | 17 | global.__BROWSER_SERVER__ = browserServer; 18 | 19 | await fs.mkdirp(DIR); 20 | await fs.writeFile(path.join(DIR, 'wsEndpoint'), browserServer.wsEndpoint()); 21 | await fs.remove(path.resolve(__dirname, '../temp')); 22 | }; 23 | -------------------------------------------------------------------------------- /scripts/jestGlobalTeardown.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | 4 | module.exports = async () => { 5 | await global.__BROWSER_SERVER__.close() 6 | if (!process.env.VITE_PRESERVE_BUILD_ARTIFACTS) { 7 | await fs.remove(path.resolve(__dirname, '../temp')) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/jestPerTestSetup.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import * as http from 'http'; 3 | import * as path from 'path'; 4 | import { resolve } from 'path'; 5 | import { ConsoleMessage, Page } from 'playwright-chromium'; 6 | import slash from 'slash'; 7 | import { 8 | ViteDevServer, 9 | UserConfig, 10 | build, 11 | InlineConfig, 12 | ResolvedConfig, 13 | } from 'vite'; 14 | 15 | import { preview } from '../packages/vitext/src/node/preview'; 16 | import { createServer } from '../packages/vitext/src/node/server'; 17 | import { resolveInlineConfig } from '../packages/vitext/src/node/utils'; 18 | 19 | // injected by the test env 20 | declare global { 21 | namespace NodeJS { 22 | interface Global { 23 | page?: Page; 24 | viteTestUrl?: string; 25 | } 26 | } 27 | } 28 | 29 | const isBuildTest = !!process.env.VITE_TEST_BUILD; 30 | 31 | let server: ViteDevServer; 32 | let tempDir: string; 33 | let err: Error; 34 | 35 | const logs = ((global as any).browserLogs = []); 36 | const onConsole = (msg: ConsoleMessage) => { 37 | // @ts-ignore 38 | logs.push(msg.text()); 39 | }; 40 | 41 | beforeAll(async () => { 42 | const page = global.page; 43 | if (!page) { 44 | return; 45 | } 46 | try { 47 | page.on('console', onConsole); 48 | 49 | const testPath = expect.getState().testPath; 50 | const testName = slash(testPath).match(/playground\/([\w-]+)\//)?.[1]; 51 | 52 | // if this is a test placed under playground/xxx/__tests__ 53 | // start a vite server in that directory. 54 | if (testName) { 55 | const playgroundRoot = resolve(__dirname, '../packages/playground'); 56 | const srcDir = resolve(playgroundRoot, testName); 57 | tempDir = resolve(__dirname, '../temp', testName); 58 | // tempDir = path.relative( 59 | // __dirname, 60 | // resolve(__dirname, '../temp', testName) 61 | // ); 62 | try { 63 | fs.unlinkSync(tempDir); 64 | } catch {} 65 | 66 | fs.copySync(srcDir, tempDir, { 67 | dereference: true, 68 | errorOnExist: false, 69 | overwrite: true, 70 | filter(file) { 71 | file = slash(file); 72 | return ( 73 | !file.includes('__tests__') && 74 | !file.includes('node_modules') && 75 | !file.match(/dist(\/|$)/) 76 | ); 77 | }, 78 | }); 79 | modifyPackageName(path.join(tempDir, './package.json')); 80 | 81 | const options: UserConfig & { root: string } = { 82 | root: tempDir, 83 | logLevel: 'error', 84 | server: { 85 | watch: { 86 | // During tests we edit the files too fast and sometimes chokidar 87 | // misses change events, so enforce polling for consistency 88 | usePolling: true, 89 | interval: 100, 90 | }, 91 | hmr: !isBuildTest, 92 | }, 93 | build: { 94 | // skip transpilation and dynamic import polyfills during tests to 95 | // make it faster 96 | target: 'esnext', 97 | }, 98 | }; 99 | process.env.VITE_INLINE = 'inline-serve'; 100 | process.env['NODE_ENV'] = 'development'; 101 | if (isBuildTest) { 102 | process.env['NODE_ENV'] = 'production'; 103 | let config = await resolveInlineConfig( 104 | { ...options, mode: 'production' }, 105 | 'build' 106 | ); 107 | await build(config as InlineConfig); 108 | 109 | config = (await resolveInlineConfig( 110 | { ...options, mode: 'production' }, 111 | 'serve' 112 | )) as ResolvedConfig; 113 | 114 | server = await preview(config, {}); 115 | } else { 116 | server = await createServer({ 117 | ...options, 118 | mode: isBuildTest ? 'production' : 'development', 119 | }); 120 | server = await server.listen(); 121 | } 122 | 123 | const base = server.config.base === '/' ? '' : server.config.base; 124 | const url = 125 | (global.viteTestUrl = `http://localhost:${server.config.server.port}${base}`); 126 | await page.goto(url); 127 | } 128 | } catch (e) { 129 | // jest doesn't exit if our setup has error here 130 | // https://github.com/facebook/jest/issues/2713 131 | err = e; 132 | console.log(err); 133 | } 134 | }, 30000); 135 | 136 | afterAll(async () => { 137 | global.page && global.page.off('console', onConsole); 138 | if (server) { 139 | await server.close(); 140 | } 141 | if (err) { 142 | throw err; 143 | } 144 | }); 145 | 146 | function modifyPackageName(path: string) { 147 | const data: string = fs.readFileSync(path, 'utf-8'); 148 | const parsedData = JSON.parse(data); 149 | parsedData.name = parsedData.name + '-test'; 150 | fs.writeFileSync(path, JSON.stringify(parsedData), 'utf-8'); 151 | } 152 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * modified from https://github.com/vuejs/vue-next/blob/master/scripts/release.js 5 | */ 6 | const execa = require('execa') 7 | const path = require('path') 8 | const fs = require('fs') 9 | const args = require('minimist')(process.argv.slice(2)) 10 | const semver = require('semver') 11 | const chalk = require('chalk') 12 | const { prompt } = require('enquirer') 13 | 14 | const pkgDir = process.cwd() 15 | const pkgPath = path.resolve(pkgDir, 'package.json') 16 | /** 17 | * @type {{ name: string, version: string }} 18 | */ 19 | const pkg = require(pkgPath) 20 | const pkgName = pkg.name.replace(/^@vitejs\//, '') 21 | const currentVersion = pkg.version 22 | /** 23 | * @type {boolean} 24 | */ 25 | const isDryRun = args.dry 26 | /** 27 | * @type {boolean} 28 | */ 29 | const skipBuild = args.skipBuild 30 | 31 | /** 32 | * @type {import('semver').ReleaseType[]} 33 | */ 34 | const versionIncrements = [ 35 | 'patch', 36 | 'minor', 37 | 'major', 38 | 'prepatch', 39 | 'preminor', 40 | 'premajor', 41 | 'prerelease' 42 | ] 43 | 44 | /** 45 | * @param {import('semver').ReleaseType} i 46 | */ 47 | const inc = (i) => semver.inc(currentVersion, i) 48 | 49 | /** 50 | * @param {string} bin 51 | * @param {string[]} args 52 | * @param {object} opts 53 | */ 54 | const run = (bin, args, opts = {}) => 55 | execa(bin, args, { stdio: 'inherit', ...opts }) 56 | 57 | /** 58 | * @param {string} bin 59 | * @param {string[]} args 60 | * @param {object} opts 61 | */ 62 | const dryRun = (bin, args, opts = {}) => 63 | console.log(chalk.blue(`[dryrun] ${bin} ${args.join(' ')}`), opts) 64 | 65 | const runIfNotDry = isDryRun ? dryRun : run 66 | 67 | /** 68 | * @param {string} msg 69 | */ 70 | const step = (msg) => console.log(chalk.cyan(msg)) 71 | 72 | async function main() { 73 | let targetVersion = args._[0] 74 | 75 | if (!targetVersion) { 76 | // no explicit version, offer suggestions 77 | /** 78 | * @type {{ release: string }} 79 | */ 80 | const { release } = await prompt({ 81 | type: 'select', 82 | name: 'release', 83 | message: 'Select release type', 84 | choices: versionIncrements 85 | .map((i) => `${i} (${inc(i)})`) 86 | .concat(['custom']) 87 | }) 88 | 89 | if (release === 'custom') { 90 | /** 91 | * @type {{ version: string }} 92 | */ 93 | const res = await prompt({ 94 | type: 'input', 95 | name: 'version', 96 | message: 'Input custom version', 97 | initial: currentVersion 98 | }) 99 | targetVersion = res.version 100 | } else { 101 | targetVersion = release.match(/\((.*)\)/)[1] 102 | } 103 | } 104 | 105 | if (!semver.valid(targetVersion)) { 106 | throw new Error(`invalid target version: ${targetVersion}`) 107 | } 108 | 109 | const tag = 110 | pkgName === 'vite' ? `v${targetVersion}` : `${pkgName}@${targetVersion}` 111 | 112 | /** 113 | * @type {{ yes: boolean }} 114 | */ 115 | const { yes } = await prompt({ 116 | type: 'confirm', 117 | name: 'yes', 118 | message: `Releasing ${tag}. Confirm?` 119 | }) 120 | 121 | if (!yes) { 122 | return 123 | } 124 | 125 | step('\nUpdating package version...') 126 | updateVersion(targetVersion) 127 | 128 | step('\nBuilding package...') 129 | if (!skipBuild && !isDryRun) { 130 | await run('yarn', ['build']) 131 | } else { 132 | console.log(`(skipped)`) 133 | } 134 | 135 | step('\nGenerating changelog...') 136 | await run('yarn', ['changelog']) 137 | 138 | const { stdout } = await run('git', ['diff'], { stdio: 'pipe' }) 139 | if (stdout) { 140 | step('\nCommitting changes...') 141 | await runIfNotDry('git', ['add', '-A']) 142 | await runIfNotDry('git', ['commit', '-m', `release: ${tag}`]) 143 | } else { 144 | console.log('No changes to commit.') 145 | } 146 | 147 | step('\nPublishing package...') 148 | await publishPackage(targetVersion, runIfNotDry) 149 | 150 | step('\nPushing to GitHub...') 151 | await runIfNotDry('git', ['tag', tag]) 152 | await runIfNotDry('git', ['push', 'origin', `refs/tags/${tag}`]) 153 | await runIfNotDry('git', ['push']) 154 | 155 | if (isDryRun) { 156 | console.log(`\nDry run finished - run git diff to see package changes.`) 157 | } 158 | 159 | console.log() 160 | } 161 | 162 | /** 163 | * @param {string} version 164 | */ 165 | function updateVersion(version) { 166 | const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) 167 | pkg.version = version 168 | fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') 169 | } 170 | 171 | /** 172 | * @param {string} version 173 | * @param {Function} runIfNotDry 174 | */ 175 | async function publishPackage(version, runIfNotDry) { 176 | const publicArgs = [ 177 | 'publish', 178 | '--no-git-tag-version', 179 | '--new-version', 180 | version, 181 | '--access', 182 | 'public' 183 | ] 184 | if (args.tag) { 185 | publicArgs.push(`--tag`, args.tag) 186 | } 187 | try { 188 | await runIfNotDry('yarn', publicArgs, { 189 | stdio: 'pipe' 190 | }) 191 | console.log(chalk.green(`Successfully published ${pkgName}@${version}`)) 192 | } catch (e) { 193 | if (e.stderr.match(/previously published/)) { 194 | console.log(chalk.red(`Skipping already published: ${pkgName}`)) 195 | } else { 196 | throw e 197 | } 198 | } 199 | } 200 | 201 | main().catch((err) => { 202 | console.error(err) 203 | }) 204 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "moduleResolution": "node", 5 | "strict": true, 6 | "noUnusedLocals": false, 7 | "esModuleInterop": true, 8 | "jsx": "react", 9 | "module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 10 | "sourceMap": true /* Generates corresponding '.map' file. */, 11 | "skipLibCheck": true /* Skip type checking of declaration files. */, 12 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 13 | "preserveWatchOutput": true, 14 | "removeComments": false, 15 | "resolveJsonModule": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "packages/playground/basic" 8 | } 9 | ], 10 | "settings": { 11 | "typescript.tsdk": "vitext/node_modules/typescript/lib", 12 | "typescript.enablePromptUseWorkspaceTsdk": true 13 | } 14 | } --------------------------------------------------------------------------------