= {
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/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/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 | setCount((prevCount) => prevCount + 1)}>
56 | increase
57 |
58 | >
59 | );
60 | };
61 |
62 | export default IndexPage;
63 |
--------------------------------------------------------------------------------
/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/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/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/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/create-project/template-lib/vite.config.ts:
--------------------------------------------------------------------------------
1 | import type { UserConfig } 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, { DefaultPageStrategy } from 'vitext'
6 |
7 | module.exports = {
8 | jsx: 'react',
9 | plugins: [
10 | reactRefresh(),
11 | mdx(),
12 | pages({
13 | pagesDir: path.join(__dirname, 'pages'),
14 | pageStrategy: new DefaultPageStrategy({
15 | extraFindPages: async (pagesDir, helpers) => {
16 | const demosBasePath = path.join(__dirname, 'src')
17 | // find all component demos
18 | helpers.watchFiles(
19 | demosBasePath,
20 | '*/demos/**/*.{[tj]sx,md?(x)}',
21 | async function fileHandler(file, api) {
22 | const { relative, path: absolute } = file
23 | const match = relative.match(/(.*)\/demos\/(.*)\.([tj]sx|mdx?)$/)
24 | if (!match) throw new Error('unexpected file: ' + absolute)
25 | const [_, componentName, demoPath] = match
26 | const pageId = `/${componentName}`
27 | const runtimeDataPaths = api.getRuntimeData(pageId)
28 | runtimeDataPaths[demoPath] = absolute
29 | const staticData = api.getStaticData(pageId)
30 | staticData[demoPath] = await helpers.extractStaticData(file)
31 | if (!staticData.title) staticData.title = `${componentName} Title`
32 | }
33 | )
34 |
35 | // find all component README
36 | helpers.watchFiles(
37 | demosBasePath,
38 | '*/README.md?(x)',
39 | async function fileHandler(file, api) {
40 | const { relative, path: absolute } = file
41 | const match = relative.match(/(.*)\/README\.mdx?$/)
42 | if (!match) throw new Error('unexpected file: ' + absolute)
43 | const [_, componentName] = match
44 | const pageId = `/${componentName}`
45 | const runtimeDataPaths = api.getRuntimeData(pageId)
46 | runtimeDataPaths['README'] = absolute
47 | const staticData = api.getStaticData(pageId)
48 | staticData['README'] = await helpers.extractStaticData(file)
49 | // make sure the title data is bound to this file
50 | staticData.title = undefined
51 | staticData.title =
52 | staticData['README'].title ?? `${componentName} Title`
53 | }
54 | )
55 | },
56 | }),
57 | }),
58 | ],
59 | resolve: {
60 | alias: {
61 | 'my-lib': '/src',
62 | },
63 | },
64 | } as UserConfig
65 |
--------------------------------------------------------------------------------
/packages/examples/intro/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { OrbitControls, Stage } from '@react-three/drei';
2 | import { Canvas } from '@react-three/fiber';
3 | import { lazy, Suspense, useRef, useState } from 'react';
4 |
5 | const Model = lazy(() => import('../components/Model'));
6 |
7 | const Loading = () => Loading the Ruby 💎
;
8 |
9 | const IndexPage = () => {
10 | const ref = useRef();
11 | const [number, setNumber] = useState(0);
12 | return (
13 |
14 |
⚡🚀
15 |
16 |
22 | Vitext
23 |
24 |
25 |
26 |
The Next.js like React framework for Speed
27 |
28 |
29 | 💡 Instant Server Start
30 | 💥 Suspense support
31 | ⚫ Next.js like API
32 | 📦 Optimized Build
33 | 💎 Build & Export on fly
34 | 🚀 Lightning SSG/SSR
35 | 🔑 Vite & Rollup Compatible
36 |
37 |
38 |
42 | How many rubies do you want?
43 |
44 |
{number}
45 |
46 |
setNumber((prevNum) => prevNum + 1)}
49 | >
50 | increase
51 |
52 |
return
53 |
setNumber((prevNum) => prevNum - 1)}
56 | >
57 | decrease
58 |
59 |
60 |
61 |
62 | }>
63 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default IndexPage;
83 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/packages/create-project/template-lib-monorepo/packages/demos/vite.demos.ts:
--------------------------------------------------------------------------------
1 | import type { UserConfig } from 'vite'
2 | import * as path from 'path'
3 |
4 | import reactRefresh from '@vitejs/plugin-react-refresh'
5 | import mdx from 'vite-plugin-mdx'
6 | import pages, { DefaultPageStrategy } from 'vitext'
7 |
8 | module.exports = {
9 | jsx: 'react',
10 | plugins: [
11 | reactRefresh(),
12 | mdx(),
13 | pages({
14 | pagesDir: path.join(__dirname, 'pages'),
15 | pageStrategy: new DefaultPageStrategy({
16 | extraFindPages: async (pagesDir, helpers) => {
17 | const demosBasePath = path.join(__dirname, '../')
18 | // find all component demos
19 | helpers.watchFiles(
20 | demosBasePath,
21 | '*/demos/**/*.{[tj]sx,md?(x)}',
22 | async function fileHandler(file, api) {
23 | const { relative, path: absolute } = file
24 | const match = relative.match(/(.*)\/demos\/(.*)\.([tj]sx|mdx?)$/)
25 | if (!match) throw new Error('unexpected file: ' + absolute)
26 | const [_, componentName, demoPath] = match
27 | const pageId = `/${componentName}`
28 | const runtimeDataPaths = api.getRuntimeData(pageId)
29 | runtimeDataPaths[demoPath] = absolute
30 | const staticData = api.getStaticData(pageId)
31 | staticData[demoPath] = await helpers.extractStaticData(file)
32 | if (!staticData.title) staticData.title = `${componentName} Title`
33 | }
34 | )
35 |
36 | // find all component README
37 | helpers.watchFiles(
38 | demosBasePath,
39 | '*/README.md?(x)',
40 | async function fileHandler(file, api) {
41 | const { relative, path: absolute } = file
42 | const match = relative.match(/(.*)\/README\.mdx?$/)
43 | if (!match) throw new Error('unexpected file: ' + absolute)
44 | const [_, componentName] = match
45 | const pageId = `/${componentName}`
46 | const runtimeDataPaths = api.getRuntimeData(pageId)
47 | runtimeDataPaths['README'] = absolute
48 | const staticData = api.getStaticData(pageId)
49 | staticData['README'] = await helpers.extractStaticData(file)
50 | // make sure the title data is bound to this file
51 | staticData.title = undefined
52 | staticData.title =
53 | staticData['README'].title ?? `${componentName} Title`
54 | }
55 | )
56 | },
57 | }),
58 | }),
59 | ],
60 | resolve: {
61 | alias: {
62 | 'my-button': path.resolve(__dirname, '../button/src'),
63 | 'my-card': path.resolve(__dirname, '../card/src'),
64 | },
65 | },
66 | ssr: {
67 | // should not external them in ssr build,
68 | // otherwise the ssr bundle will contains `require("my-button")`
69 | // which will result in error
70 | noExternal: ['my-button', 'my-card'],
71 | },
72 | minify: false,
73 | } as UserConfig
74 |
--------------------------------------------------------------------------------
/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/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/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/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/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(' 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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------