zuby 1.0.46 → 1.0.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/index.js +4 -0
- package/commands/init.d.ts +1 -1
- package/commands/init.js +69 -34
- package/config.d.ts +4 -0
- package/config.js +12 -0
- package/constants.d.ts +4 -0
- package/constants.js +5 -0
- package/context/index.d.ts +2 -0
- package/context/index.js +3 -0
- package/context/types.d.ts +5 -0
- package/package.json +1 -1
- package/pageContext/index.d.ts +11 -0
- package/pageContext/index.js +14 -0
- package/plugins/contextPlugin/index.d.ts +2 -0
- package/plugins/contextPlugin/index.js +27 -4
- package/plugins/preloadPlugin/index.d.ts +7 -0
- package/plugins/preloadPlugin/index.js +27 -0
- package/plugins/prerenderPlugin/index.js +2 -2
- package/preload/index.d.ts +16 -0
- package/preload/index.js +94 -0
- package/server/index.js +23 -2
- package/server/zubyServer.js +6 -1
- package/templates/index.d.ts +4 -0
- package/templates/index.js +8 -0
- package/templates/types.d.ts +5 -0
- package/templates/types.js +4 -0
- package/types.d.ts +38 -3
package/commands/index.js
CHANGED
|
@@ -32,6 +32,10 @@ program
|
|
|
32
32
|
program
|
|
33
33
|
.command('init')
|
|
34
34
|
.description('Initializes a new Zuby project')
|
|
35
|
+
.option('-p, --project-path <path>', 'The relative path to the project directory')
|
|
36
|
+
.option('-j, --jsx-provider-name <name>', 'The name of the JSX provider to use (react, preact)')
|
|
37
|
+
.option('-e, --example-name <name>', 'The name of the example project to use (basic)')
|
|
38
|
+
.option('-t, --use-typescript', 'Add this flag to use TypeScript instead of JavaScript')
|
|
35
39
|
.action(async (options) => init(options));
|
|
36
40
|
program
|
|
37
41
|
.command('info')
|
package/commands/init.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InitCommandOptions } from '../types.js';
|
|
2
|
-
export default function init(
|
|
2
|
+
export default function init({ projectPath, jsxProviderName, exampleName, useTypescript, }: InitCommandOptions): Promise<void>;
|
|
3
3
|
export declare function initExample(example: Example, options: InitOptions): Promise<void>;
|
|
4
4
|
export declare function getExamples(): Example[];
|
|
5
5
|
export interface Example {
|
package/commands/init.js
CHANGED
|
@@ -11,7 +11,7 @@ import { getZubyPackageConfig } from '../packageConfig.js';
|
|
|
11
11
|
import { globSync } from 'glob';
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = dirname(__filename);
|
|
14
|
-
export default async function init(
|
|
14
|
+
export default async function init({ projectPath, jsxProviderName, exampleName, useTypescript, }) {
|
|
15
15
|
const logger = createLogger();
|
|
16
16
|
logger.clearScreen('info');
|
|
17
17
|
logger?.info(`${getTitle(`Let's get started with Zuby`)}\r\n`);
|
|
@@ -19,44 +19,79 @@ export default async function init(options) {
|
|
|
19
19
|
const examples = getExamples();
|
|
20
20
|
const defaultProjectName = 'my-zuby-app';
|
|
21
21
|
const defaultExample = examples.find(example => example.name === 'basic');
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
const jsxProviderNames = ['preact', 'react'];
|
|
23
|
+
const exampleNames = examples.map(example => example.name);
|
|
24
|
+
const validateProjectPath = (projectPath) => {
|
|
25
|
+
if (existsSync(projectPath)) {
|
|
26
|
+
return 'Project with this name already exists. Please choose a different one.';
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
const validateJsxProviderName = (name) => {
|
|
31
|
+
if (!jsxProviderNames.includes(name)) {
|
|
32
|
+
return `Invalid JSX provider name. Please choose one of the following: ${jsxProviderNames.join(',')}.`;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
const validateExampleName = (name) => {
|
|
37
|
+
if (!exampleNames.includes(name)) {
|
|
38
|
+
return `Invalid example project name. Please choose one of the following: ${exampleNames.join(',')}.`;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
if (!projectPath || validateProjectPath(projectPath) !== true) {
|
|
43
|
+
({ projectPath } = await inquirer.prompt([
|
|
44
|
+
{
|
|
45
|
+
type: 'input',
|
|
46
|
+
name: 'projectPath',
|
|
47
|
+
message: 'Where would you like to create your new project?',
|
|
48
|
+
default: `./${defaultProjectName}`,
|
|
49
|
+
prefix: `${chalk.yellowBright.bgWhite(' name ')} ❯ `,
|
|
50
|
+
validate: validateProjectPath,
|
|
51
|
+
},
|
|
52
|
+
]));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
logger.info(`Project path: ${projectPath}`);
|
|
56
|
+
}
|
|
57
|
+
if (!jsxProviderName || validateJsxProviderName(jsxProviderName) !== true) {
|
|
58
|
+
({ jsxProviderName } = await inquirer.prompt([
|
|
59
|
+
{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'jsxProviderName',
|
|
62
|
+
message: 'In which JSX framework would you like to write your components?',
|
|
63
|
+
choices: jsxProviderNames,
|
|
64
|
+
default: jsxProviderNames?.[0],
|
|
65
|
+
prefix: `${chalk.yellowBright.bgWhite(' jsx ')} ❯ `,
|
|
66
|
+
validate: validateJsxProviderName,
|
|
67
|
+
},
|
|
68
|
+
]));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
logger.info(`Jsx Provider: ${jsxProviderName}`);
|
|
72
|
+
}
|
|
73
|
+
if (!exampleName || validateExampleName(exampleName) !== true) {
|
|
74
|
+
({ exampleName } = await inquirer.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: 'list',
|
|
77
|
+
name: 'exampleName',
|
|
78
|
+
message: 'Which Zuby example project would you like to use?',
|
|
79
|
+
choices: exampleNames,
|
|
80
|
+
default: defaultExample?.name,
|
|
81
|
+
prefix: `${chalk.yellowBright.bgWhite(' example ')} ❯ `,
|
|
82
|
+
validate: validateExampleName,
|
|
34
83
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
choices: ['preact', 'react'],
|
|
41
|
-
default: 'preact',
|
|
42
|
-
prefix: `${chalk.yellowBright.bgWhite(' jsx ')} ❯ `,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
type: 'list',
|
|
46
|
-
name: 'exampleName',
|
|
47
|
-
message: 'Which Zuby example project would you like to use?',
|
|
48
|
-
choices: examples.map(example => example.name),
|
|
49
|
-
default: defaultExample?.name,
|
|
50
|
-
prefix: `${chalk.yellowBright.bgWhite(' example ')} ❯ `,
|
|
51
|
-
},
|
|
52
|
-
]);
|
|
53
|
-
let useTypescript = false;
|
|
84
|
+
]));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.info(`Example project: ${exampleName}`);
|
|
88
|
+
}
|
|
54
89
|
const selectedExample = examples.find(example => example.name === exampleName);
|
|
55
90
|
if (!selectedExample) {
|
|
56
91
|
logger.error(`Example ${exampleName} was not found`);
|
|
57
92
|
process.exit(1);
|
|
58
93
|
}
|
|
59
|
-
if (selectedExample?.isTs) {
|
|
94
|
+
if (useTypescript === undefined && selectedExample?.isTs) {
|
|
60
95
|
({ useTypescript } = await inquirer.prompt([
|
|
61
96
|
{
|
|
62
97
|
type: 'confirm',
|
|
@@ -67,7 +102,7 @@ export default async function init(options) {
|
|
|
67
102
|
},
|
|
68
103
|
]));
|
|
69
104
|
}
|
|
70
|
-
const projectName = projectPath
|
|
105
|
+
const projectName = projectPath?.split('/').pop() || defaultProjectName;
|
|
71
106
|
const zubyVersion = getZubyPackageConfig().version;
|
|
72
107
|
await initExample(selectedExample, {
|
|
73
108
|
projectName,
|
package/config.d.ts
CHANGED
|
@@ -48,3 +48,7 @@ export type ExecutePluginsParams = Omit<ZubyHookParams, 'command' | 'logger' | '
|
|
|
48
48
|
* @param plugins
|
|
49
49
|
*/
|
|
50
50
|
export declare const normalizePlugins: (plugins: (ZubyPlugin | ZubyPlugin[] | VitePluginOption | VitePluginOption[])[]) => Promise<(ZubyPlugin | VitePlugin)[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Returns random build ID.
|
|
53
|
+
*/
|
|
54
|
+
export declare const generateDefaultBuildId: () => string;
|
package/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PLUGIN_HOOKS, } from './types.js';
|
|
2
2
|
import { BUILD_CHUNKS_MANIFEST, ZUBY_CONFIG_FILE } from './constants.js';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
4
5
|
import { bundleRequire } from 'bundle-require';
|
|
5
6
|
import { createLogger } from './logger/index.js';
|
|
6
7
|
// Plugins
|
|
@@ -10,6 +11,7 @@ import chunkNamingPlugin from './plugins/chunkNamingPlugin/index.js';
|
|
|
10
11
|
import manifestPlugin from './plugins/manifestPlugin/index.js';
|
|
11
12
|
import prerenderPlugin from './plugins/prerenderPlugin/index.js';
|
|
12
13
|
import standaloneBuildPlugin from './plugins/dependenciesPlugin/index.js';
|
|
14
|
+
import preloadPlugin from './plugins/preloadPlugin/index.js';
|
|
13
15
|
let zubyInternalConfig;
|
|
14
16
|
/**
|
|
15
17
|
* Returns the path to the ZubyConfig file.
|
|
@@ -102,6 +104,8 @@ export const mergeDefaultConfig = async (config) => {
|
|
|
102
104
|
config.minifyCSS = config.minifyCSS ?? true;
|
|
103
105
|
config.minifyHTML = config.minifyHTML ?? true;
|
|
104
106
|
config.minifyJS = config.minifyJS ?? true;
|
|
107
|
+
// Build ID generator
|
|
108
|
+
config.generateBuildId = config.generateBuildId ?? generateDefaultBuildId;
|
|
105
109
|
// Add logger
|
|
106
110
|
config.customLogger =
|
|
107
111
|
config.customLogger ??
|
|
@@ -133,6 +137,7 @@ export const mergeDefaultConfig = async (config) => {
|
|
|
133
137
|
return {
|
|
134
138
|
...config,
|
|
135
139
|
templateExtensions: ['js', 'jsx', 'ts', 'tsx'],
|
|
140
|
+
buildId: await config.generateBuildId(),
|
|
136
141
|
};
|
|
137
142
|
};
|
|
138
143
|
/**
|
|
@@ -148,6 +153,7 @@ export const getBuiltInPlugins = () => {
|
|
|
148
153
|
manifestPlugin(),
|
|
149
154
|
prerenderPlugin(),
|
|
150
155
|
standaloneBuildPlugin(),
|
|
156
|
+
preloadPlugin(),
|
|
151
157
|
];
|
|
152
158
|
};
|
|
153
159
|
/**
|
|
@@ -204,3 +210,9 @@ export const normalizePlugins = async (plugins) => {
|
|
|
204
210
|
// Remove false, undefined, null values
|
|
205
211
|
.filter(plugin => !!plugin);
|
|
206
212
|
};
|
|
213
|
+
/**
|
|
214
|
+
* Returns random build ID.
|
|
215
|
+
*/
|
|
216
|
+
export const generateDefaultBuildId = () => {
|
|
217
|
+
return randomBytes(8).toString('hex');
|
|
218
|
+
};
|
package/constants.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare const ZUBY_CONFIG_FILE = "zuby.config.mjs";
|
|
|
2
2
|
export declare const BUILD_CHUNKS_MANIFEST = "chunks-manifest.json";
|
|
3
3
|
export declare const CLIENT_CHUNKS_MANIFEST = "client-chunks-manifest.json";
|
|
4
4
|
export declare const SERVER_CHUNKS_MANIFEST = "server-chunks-manifest.json";
|
|
5
|
+
export declare const PREALOD_MANIFEST = "preload-manifest.json";
|
|
5
6
|
export declare const PAGES_MANIFEST = "pages-manifest.json";
|
|
6
7
|
export declare const HTTP_HEADERS: {
|
|
7
8
|
Age: string;
|
|
@@ -24,3 +25,6 @@ export declare const HTTP_HEADERS: {
|
|
|
24
25
|
XZubyCacheTTL: string;
|
|
25
26
|
XZubyCache: string;
|
|
26
27
|
};
|
|
28
|
+
export declare const ZUBY_USER_AGENTS: {
|
|
29
|
+
Prerenderer: string;
|
|
30
|
+
};
|
package/constants.js
CHANGED
|
@@ -2,6 +2,7 @@ export const ZUBY_CONFIG_FILE = 'zuby.config.mjs';
|
|
|
2
2
|
export const BUILD_CHUNKS_MANIFEST = 'chunks-manifest.json';
|
|
3
3
|
export const CLIENT_CHUNKS_MANIFEST = 'client-chunks-manifest.json';
|
|
4
4
|
export const SERVER_CHUNKS_MANIFEST = 'server-chunks-manifest.json';
|
|
5
|
+
export const PREALOD_MANIFEST = 'preload-manifest.json';
|
|
5
6
|
export const PAGES_MANIFEST = 'pages-manifest.json';
|
|
6
7
|
export const HTTP_HEADERS = {
|
|
7
8
|
// Standard HTTP headers
|
|
@@ -27,3 +28,7 @@ export const HTTP_HEADERS = {
|
|
|
27
28
|
XZubyCacheTTL: 'X-Zuby-Cache-TTL',
|
|
28
29
|
XZubyCache: 'X-Zuby-Cache',
|
|
29
30
|
};
|
|
31
|
+
export const ZUBY_USER_AGENTS = {
|
|
32
|
+
// Zuby user agents
|
|
33
|
+
Prerenderer: 'zuby-prerender',
|
|
34
|
+
};
|
package/context/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export declare class ZubyContext {
|
|
|
9
9
|
layouts?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
10
10
|
innerLayouts?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
11
11
|
handlers?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
12
|
+
loaders?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
12
13
|
} | undefined;
|
|
13
14
|
get site(): string | undefined;
|
|
14
15
|
get generator(): string | undefined;
|
|
@@ -19,5 +20,6 @@ export declare class ZubyContext {
|
|
|
19
20
|
locales: string[];
|
|
20
21
|
defaultLocale: string;
|
|
21
22
|
} | undefined;
|
|
23
|
+
get buildId(): string | undefined;
|
|
22
24
|
}
|
|
23
25
|
export declare const getContext: () => ZubyContext;
|
package/context/index.js
CHANGED
package/context/types.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ZubyRawContext {
|
|
|
11
11
|
layouts?: LazyTemplate[];
|
|
12
12
|
innerLayouts?: LazyTemplate[];
|
|
13
13
|
handlers?: LazyTemplate[];
|
|
14
|
+
loaders?: LazyTemplate[];
|
|
14
15
|
};
|
|
15
16
|
/**
|
|
16
17
|
* The render module from JsxProvider.
|
|
@@ -34,6 +35,10 @@ export interface ZubyRawContext {
|
|
|
34
35
|
* @example 1.0.0
|
|
35
36
|
*/
|
|
36
37
|
version?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The build ID of the site.
|
|
40
|
+
*/
|
|
41
|
+
buildId?: string;
|
|
37
42
|
/**
|
|
38
43
|
* The internalization config from ZubyConfig.
|
|
39
44
|
* @example {
|
package/package.json
CHANGED
package/pageContext/index.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export declare class ZubyPageContext {
|
|
|
82
82
|
layouts?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
83
83
|
innerLayouts?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
84
84
|
handlers?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
85
|
+
loaders?: import("../templates/types.js").LazyTemplate[] | undefined;
|
|
85
86
|
} | undefined;
|
|
86
87
|
/**
|
|
87
88
|
* The object with props that should be passed to the page component.
|
|
@@ -166,4 +167,14 @@ export declare class ZubyPageContext {
|
|
|
166
167
|
* @example localizePath('/products/1', 'en') => /products/1
|
|
167
168
|
*/
|
|
168
169
|
localizePath(path: string, locale?: string | undefined): string;
|
|
170
|
+
/**
|
|
171
|
+
* Returns true if the current request
|
|
172
|
+
* was made by the Zuby.js pre-render build step.
|
|
173
|
+
*/
|
|
174
|
+
get isPrerendering(): boolean;
|
|
175
|
+
/**
|
|
176
|
+
* The current build ID of the site.
|
|
177
|
+
* @example ecdf1a94cc9b4f4c
|
|
178
|
+
*/
|
|
179
|
+
get buildId(): string | undefined;
|
|
169
180
|
}
|
package/pageContext/index.js
CHANGED
|
@@ -218,4 +218,18 @@ export class ZubyPageContext {
|
|
|
218
218
|
return path;
|
|
219
219
|
return `/${locale}/${path}`.replace(/\/+/g, '/');
|
|
220
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Returns true if the current request
|
|
223
|
+
* was made by the Zuby.js pre-render build step.
|
|
224
|
+
*/
|
|
225
|
+
get isPrerendering() {
|
|
226
|
+
return this._request?.headers?.get('user-agent') === 'zuby-prerender';
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The current build ID of the site.
|
|
230
|
+
* @example ecdf1a94cc9b4f4c
|
|
231
|
+
*/
|
|
232
|
+
get buildId() {
|
|
233
|
+
return this._zubyContext.buildId;
|
|
234
|
+
}
|
|
221
235
|
}
|
|
@@ -4,4 +4,6 @@ export default function index(): VitePlugin;
|
|
|
4
4
|
export declare function generateCompileTimeContextCode(ssr: boolean): Promise<string>;
|
|
5
5
|
export declare function generateTemplatesCode(ssr: boolean): Promise<string>;
|
|
6
6
|
export declare function generateTemplateCode(template: Template): Promise<string>;
|
|
7
|
+
export declare function generateImportCode(template: Template): string;
|
|
7
8
|
export declare function generateRenderCode(ssr: boolean): Promise<string>;
|
|
9
|
+
export declare function getStaticImportsCode(): string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { getZubyInternalConfig } from '../../config.js';
|
|
2
|
+
import { SYNC_TEMPLATES } from '../../templates/types.js';
|
|
2
3
|
import { relative } from 'path';
|
|
3
4
|
import { normalizePath } from '../../utils/pathUtils.js';
|
|
4
5
|
import { getZubyPackageConfig } from '../../packageConfig.js';
|
|
5
|
-
import { getApps, getErrors, getHandlers, getInnerLayouts, getLayouts, getPages, getTemplates, } from '../../templates/index.js';
|
|
6
|
+
import { getApps, getErrors, getHandlers, getInnerLayouts, getLayouts, getLoaders, getPages, getTemplates, } from '../../templates/index.js';
|
|
6
7
|
let viteConfig;
|
|
8
|
+
let staticImports = [];
|
|
7
9
|
export default function index() {
|
|
8
10
|
return {
|
|
9
11
|
name: 'zuby-context-plugin',
|
|
@@ -17,12 +19,13 @@ export default function index() {
|
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
21
|
const contextCode = await generateCompileTimeContextCode(ssr);
|
|
20
|
-
|
|
22
|
+
const staticImportsCode = getStaticImportsCode();
|
|
23
|
+
return staticImportsCode + contextCode + code;
|
|
21
24
|
},
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
27
|
export async function generateCompileTimeContextCode(ssr) {
|
|
25
|
-
const { site, i18n } = await getZubyInternalConfig();
|
|
28
|
+
const { site, i18n, buildId } = await getZubyInternalConfig();
|
|
26
29
|
const { version } = await getZubyPackageConfig();
|
|
27
30
|
return `globalThis.ZubyRawContext = {
|
|
28
31
|
...(globalThis.ZubyRawContext || {}),
|
|
@@ -31,6 +34,7 @@ export async function generateCompileTimeContextCode(ssr) {
|
|
|
31
34
|
site: '${site || ''}',
|
|
32
35
|
generator: 'Zuby.js ${version}',
|
|
33
36
|
version: '${version}',
|
|
37
|
+
buildId: '${buildId}',
|
|
34
38
|
i18n: ${JSON.stringify(i18n)},
|
|
35
39
|
};`;
|
|
36
40
|
}
|
|
@@ -42,6 +46,7 @@ export async function generateTemplatesCode(ssr) {
|
|
|
42
46
|
const innerLayouts = await getInnerLayouts(templates);
|
|
43
47
|
const errors = await getErrors(templates);
|
|
44
48
|
const handlers = await getHandlers(templates);
|
|
49
|
+
const loaders = await getLoaders(templates);
|
|
45
50
|
const pagesCode = await Promise.all(pages.map(generateTemplateCode) || []);
|
|
46
51
|
const appsCode = await Promise.all(apps.map(generateTemplateCode) || []);
|
|
47
52
|
const errorsCode = await Promise.all(errors.map(generateTemplateCode) || []);
|
|
@@ -50,6 +55,7 @@ export async function generateTemplatesCode(ssr) {
|
|
|
50
55
|
? await Promise.all(innerLayouts.map(generateTemplateCode) || [])
|
|
51
56
|
: [];
|
|
52
57
|
const handlersCode = ssr ? await Promise.all(handlers.map(generateTemplateCode) || []) : [];
|
|
58
|
+
const loadersCode = await Promise.all(loaders.map(generateTemplateCode) || []);
|
|
53
59
|
return `{
|
|
54
60
|
pages: [${pagesCode.join(',')}],
|
|
55
61
|
apps: [${appsCode.join(',')}],
|
|
@@ -57,6 +63,7 @@ export async function generateTemplatesCode(ssr) {
|
|
|
57
63
|
layouts: [${layoutsCode.join(',')}],
|
|
58
64
|
innerLayouts: [${innerLayoutsCode.join(',')}],
|
|
59
65
|
handlers: [${handlersCode.join(',')}],
|
|
66
|
+
loaders: [${loadersCode.join(',')}],
|
|
60
67
|
}`;
|
|
61
68
|
}
|
|
62
69
|
export async function generateTemplateCode(template) {
|
|
@@ -67,12 +74,28 @@ export async function generateTemplateCode(template) {
|
|
|
67
74
|
pathParams: ${JSON.stringify(template.pathParams)},
|
|
68
75
|
pathType: "${template.pathType}",
|
|
69
76
|
templateType: "${template.templateType}",
|
|
70
|
-
component: () =>
|
|
77
|
+
component: () => ${generateImportCode(template)},
|
|
71
78
|
}`;
|
|
72
79
|
}
|
|
80
|
+
export function generateImportCode(template) {
|
|
81
|
+
// Sync templates are imported statically
|
|
82
|
+
if (Object.values(SYNC_TEMPLATES).includes(template.templateType)) {
|
|
83
|
+
const key = `__zuby_static_import_${staticImports.length}`;
|
|
84
|
+
staticImports.push({
|
|
85
|
+
key,
|
|
86
|
+
path: template.filename,
|
|
87
|
+
});
|
|
88
|
+
return key;
|
|
89
|
+
}
|
|
90
|
+
// Async templates are imported dynamically
|
|
91
|
+
return `import("${template.filename}")`;
|
|
92
|
+
}
|
|
73
93
|
export async function generateRenderCode(ssr) {
|
|
74
94
|
const { jsx } = await getZubyInternalConfig();
|
|
75
95
|
if (!ssr)
|
|
76
96
|
return '{}';
|
|
77
97
|
return `await import("${jsx.renderFile}")`;
|
|
78
98
|
}
|
|
99
|
+
export function getStaticImportsCode() {
|
|
100
|
+
return staticImports.map(({ key, path }) => `import ${key} from "${path}";`).join('\n');
|
|
101
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import { normalizePath } from '../../utils/pathUtils.js';
|
|
4
|
+
import { getPages } from '../../templates/index.js';
|
|
5
|
+
import { PREALOD_MANIFEST } from '../../constants.js';
|
|
6
|
+
/**
|
|
7
|
+
* This is internal plugin
|
|
8
|
+
* that generates preload manifest file
|
|
9
|
+
* that is used for preloading assets.
|
|
10
|
+
*/
|
|
11
|
+
export default function preloadPlugin() {
|
|
12
|
+
return {
|
|
13
|
+
name: 'zuby-preload-plugin',
|
|
14
|
+
hooks: {
|
|
15
|
+
'zuby:build:done': async ({ config, clientChunksManifest, templates }) => {
|
|
16
|
+
const { srcDir, outDir } = config;
|
|
17
|
+
const preloadManifest = {};
|
|
18
|
+
const pages = await getPages(templates);
|
|
19
|
+
pages.forEach(page => {
|
|
20
|
+
const filename = normalizePath(relative(srcDir, page.filename));
|
|
21
|
+
preloadManifest[filename] = clientChunksManifest[filename] || [];
|
|
22
|
+
});
|
|
23
|
+
writeFileSync(normalizePath(join(outDir, 'client', PREALOD_MANIFEST)), JSON.stringify(preloadManifest, null, 2));
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -9,7 +9,7 @@ import { OUTPUTS } from '../../types.js';
|
|
|
9
9
|
import { PATH_TYPES } from '../../templates/types.js';
|
|
10
10
|
import { substitutePathParams } from '../../templates/pathUtils.js';
|
|
11
11
|
import { normalizePath } from '../../utils/pathUtils.js';
|
|
12
|
-
import { HTTP_HEADERS } from '../../constants.js';
|
|
12
|
+
import { HTTP_HEADERS, ZUBY_USER_AGENTS } from '../../constants.js';
|
|
13
13
|
/**
|
|
14
14
|
* This is internal plugin
|
|
15
15
|
* that pre-renders the pages during the build.
|
|
@@ -77,7 +77,7 @@ export default function prerenderPlugin() {
|
|
|
77
77
|
const reqBaseUrl = `http://${site || 'localhost'}`;
|
|
78
78
|
const reqOptions = {
|
|
79
79
|
headers: {
|
|
80
|
-
[HTTP_HEADERS.UserAgent]:
|
|
80
|
+
[HTTP_HEADERS.UserAgent]: ZUBY_USER_AGENTS.Prerenderer,
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
83
|
// Assign all discovered prerender paths to the config for other plugins.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PreloadEntry {
|
|
2
|
+
href: string;
|
|
3
|
+
as: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Preloads given link with the lowest priority
|
|
7
|
+
* in the future when network is idle and DOM loaded.
|
|
8
|
+
* @example preload("/api/products/1", "json")
|
|
9
|
+
*/
|
|
10
|
+
export declare function preload(href: string, as?: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* Preloads all required assets for matching page.
|
|
13
|
+
* Such as scripts, styles, props, etc.
|
|
14
|
+
* @example preloadPage("/products/1")
|
|
15
|
+
*/
|
|
16
|
+
export declare function preloadPage(href: string, onHandle?: () => void | Promise<void>): void;
|
package/preload/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { getContext } from '../context/index.js';
|
|
2
|
+
import { PREALOD_MANIFEST } from '../constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* The set of links that were already preloaded.
|
|
5
|
+
*/
|
|
6
|
+
const preloadLinks = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Preload manifest that lists all assets
|
|
9
|
+
* that should be preloaded for each page.
|
|
10
|
+
* It is generated by preloadPlugin.
|
|
11
|
+
* @example {
|
|
12
|
+
* "pages/index.tsx": [
|
|
13
|
+
* "/chunks/chunk-NGLgOfVh.js",
|
|
14
|
+
* "/chunks/chunk-8kxF91T_.css"
|
|
15
|
+
* ],
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
let preloadManifest;
|
|
19
|
+
/**
|
|
20
|
+
* Preloads given link with the lowest priority
|
|
21
|
+
* in the future when network is idle and DOM loaded.
|
|
22
|
+
* @example preload("/api/products/1", "json")
|
|
23
|
+
*/
|
|
24
|
+
export function preload(href, as = 'fetch') {
|
|
25
|
+
// Do nothing on server
|
|
26
|
+
if (typeof window === 'undefined')
|
|
27
|
+
return;
|
|
28
|
+
// Preload link only once
|
|
29
|
+
if (preloadLinks.has(href))
|
|
30
|
+
return;
|
|
31
|
+
preloadLinks.add(href);
|
|
32
|
+
// Detect asset type by extension
|
|
33
|
+
as = href.match(/\.css(\?.*)?$/) ? 'style' : as;
|
|
34
|
+
as = href.match(/\.js(\?.*)?$/) ? 'script' : as;
|
|
35
|
+
// Preload link in the future when network is idle
|
|
36
|
+
// and DOM content was loaded.
|
|
37
|
+
window.requestIdleCallback(() => addPreloadEntry({
|
|
38
|
+
href,
|
|
39
|
+
as,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Preloads all required assets for matching page.
|
|
44
|
+
* Such as scripts, styles, props, etc.
|
|
45
|
+
* @example preloadPage("/products/1")
|
|
46
|
+
*/
|
|
47
|
+
export function preloadPage(href, onHandle = () => { }) {
|
|
48
|
+
// Do nothing on server
|
|
49
|
+
if (typeof window === 'undefined')
|
|
50
|
+
return;
|
|
51
|
+
const context = getContext();
|
|
52
|
+
const pages = context.templates?.pages || [];
|
|
53
|
+
const page = pages.find(({ pathRegex }) => {
|
|
54
|
+
return pathRegex.test(href);
|
|
55
|
+
});
|
|
56
|
+
// Preload link itself
|
|
57
|
+
// if matching page was not found
|
|
58
|
+
if (!page) {
|
|
59
|
+
preload(href);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
window.requestIdleCallback(async () => {
|
|
63
|
+
const preloadManifest = await getPreloadManifest();
|
|
64
|
+
// Preload assets such as scripts and styles
|
|
65
|
+
const preloadAssets = preloadManifest?.[page.filename] || [];
|
|
66
|
+
preloadAssets.forEach(href => preload(href));
|
|
67
|
+
onHandle();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Appends preload link into head element of page.
|
|
72
|
+
* For example:
|
|
73
|
+
* <link rel="preload" href="/api/products/1" as="json"/>
|
|
74
|
+
*/
|
|
75
|
+
function addPreloadEntry({ href, as }) {
|
|
76
|
+
const preloadLink = document.createElement('link');
|
|
77
|
+
preloadLink.href = href;
|
|
78
|
+
preloadLink.as = as;
|
|
79
|
+
preloadLink.rel = 'preload';
|
|
80
|
+
document.head.appendChild(preloadLink);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns preload manifest the way
|
|
84
|
+
* that ensures it is loaded only once.
|
|
85
|
+
*/
|
|
86
|
+
async function getPreloadManifest() {
|
|
87
|
+
return (preloadManifest =
|
|
88
|
+
preloadManifest ||
|
|
89
|
+
(async () => {
|
|
90
|
+
const { buildId } = getContext();
|
|
91
|
+
const res = await fetch(`/${PREALOD_MANIFEST}?${buildId}`);
|
|
92
|
+
return res.json();
|
|
93
|
+
})());
|
|
94
|
+
}
|
package/server/index.js
CHANGED
|
@@ -875,7 +875,7 @@ function normalizePath(path2) {
|
|
|
875
875
|
}
|
|
876
876
|
|
|
877
877
|
// src/server/zubyServer.ts
|
|
878
|
-
import { resolve as resolve3 } from "path";
|
|
878
|
+
import { resolve as resolve3, basename as basename2 } from "path";
|
|
879
879
|
import { createReadStream, existsSync as existsSync2, statSync } from "fs";
|
|
880
880
|
|
|
881
881
|
// src/server/mimeTypes.ts
|
|
@@ -2088,6 +2088,9 @@ var ZubyContext = class {
|
|
|
2088
2088
|
get i18n() {
|
|
2089
2089
|
return this.rawContext.i18n;
|
|
2090
2090
|
}
|
|
2091
|
+
get buildId() {
|
|
2092
|
+
return this.rawContext.buildId;
|
|
2093
|
+
}
|
|
2091
2094
|
};
|
|
2092
2095
|
var getRawContext = () => {
|
|
2093
2096
|
return globalThis.ZubyRawContext;
|
|
@@ -2319,6 +2322,20 @@ var ZubyPageContext = class {
|
|
|
2319
2322
|
return path2;
|
|
2320
2323
|
return `/${locale}/${path2}`.replace(/\/+/g, "/");
|
|
2321
2324
|
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Returns true if the current request
|
|
2327
|
+
* was made by the Zuby.js pre-render build step.
|
|
2328
|
+
*/
|
|
2329
|
+
get isPrerendering() {
|
|
2330
|
+
return this._request?.headers?.get("user-agent") === "zuby-prerender";
|
|
2331
|
+
}
|
|
2332
|
+
/**
|
|
2333
|
+
* The current build ID of the site.
|
|
2334
|
+
* @example ecdf1a94cc9b4f4c
|
|
2335
|
+
*/
|
|
2336
|
+
get buildId() {
|
|
2337
|
+
return this._zubyContext.buildId;
|
|
2338
|
+
}
|
|
2322
2339
|
};
|
|
2323
2340
|
|
|
2324
2341
|
// src/server/zubyRenderer.ts
|
|
@@ -8507,7 +8524,8 @@ var BASE_TEMPLATES = {
|
|
|
8507
8524
|
innerLayout: "innerLayout",
|
|
8508
8525
|
app: "app",
|
|
8509
8526
|
error: "error",
|
|
8510
|
-
entry: "entry"
|
|
8527
|
+
entry: "entry",
|
|
8528
|
+
loader: "loader"
|
|
8511
8529
|
};
|
|
8512
8530
|
var TEMPLATES = {
|
|
8513
8531
|
...BASE_TEMPLATES,
|
|
@@ -8812,6 +8830,9 @@ var ZubyServer = class {
|
|
|
8812
8830
|
"Content-Length": fileStats.size.toString(),
|
|
8813
8831
|
"Last-Modified": fileStats.mtime.toUTCString()
|
|
8814
8832
|
};
|
|
8833
|
+
if (basename2(file).match(/^(chunk|entry)-/)) {
|
|
8834
|
+
headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
|
8835
|
+
}
|
|
8815
8836
|
let streamOptions = {
|
|
8816
8837
|
start: 0,
|
|
8817
8838
|
end: fileStats.size - 1
|
package/server/zubyServer.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createServer as createHttpsServer } from 'node:https';
|
|
|
2
2
|
import { createServer as createHttpServer } from 'node:http';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { normalizePath } from '../utils/pathUtils.js';
|
|
5
|
-
import { resolve } from 'path';
|
|
5
|
+
import { resolve, basename } from 'path';
|
|
6
6
|
import { createReadStream, existsSync, statSync } from 'fs';
|
|
7
7
|
import { mimeTypes } from './mimeTypes.js';
|
|
8
8
|
import { createLogger } from '../logger/index.js';
|
|
@@ -76,6 +76,11 @@ export default class ZubyServer {
|
|
|
76
76
|
'Content-Length': fileStats.size.toString(),
|
|
77
77
|
'Last-Modified': fileStats.mtime.toUTCString(),
|
|
78
78
|
};
|
|
79
|
+
// Chunks and entry can be cached forever,
|
|
80
|
+
// because they have a hash in their filename
|
|
81
|
+
if (basename(file).match(/^(chunk|entry)-/)) {
|
|
82
|
+
headers['Cache-Control'] = 'public, max-age=31536000, immutable';
|
|
83
|
+
}
|
|
79
84
|
let streamOptions = {
|
|
80
85
|
start: 0,
|
|
81
86
|
end: fileStats.size - 1,
|
package/templates/index.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export declare function getInnerLayouts(templates?: Template[]): Promise<Templat
|
|
|
23
23
|
* Returns the array of app templates.
|
|
24
24
|
*/
|
|
25
25
|
export declare function getApps(templates?: Template[]): Promise<Template[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the array of Loader templates.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getLoaders(templates?: Template[]): Promise<Template[]>;
|
|
26
30
|
/**
|
|
27
31
|
* Returns the array of page templates.
|
|
28
32
|
*/
|
package/templates/index.js
CHANGED
|
@@ -55,6 +55,14 @@ export async function getApps(templates) {
|
|
|
55
55
|
const apps = await getTemplatesOfType(TEMPLATES.app, templates);
|
|
56
56
|
return [...apps, getDefaultTemplate(jsx.appTemplateFile, TEMPLATES.app)];
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns the array of Loader templates.
|
|
60
|
+
*/
|
|
61
|
+
export async function getLoaders(templates) {
|
|
62
|
+
const { jsx } = await getZubyInternalConfig();
|
|
63
|
+
const loaders = await getTemplatesOfType(TEMPLATES.loader, templates);
|
|
64
|
+
return [...loaders, getDefaultTemplate(jsx.loaderTemplateFile, TEMPLATES.loader)];
|
|
65
|
+
}
|
|
58
66
|
/**
|
|
59
67
|
* Returns the array of page templates.
|
|
60
68
|
*/
|
package/templates/types.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare const BASE_TEMPLATES: {
|
|
|
30
30
|
app: string;
|
|
31
31
|
error: string;
|
|
32
32
|
entry: string;
|
|
33
|
+
loader: string;
|
|
33
34
|
};
|
|
34
35
|
export declare const TEMPLATES: {
|
|
35
36
|
page: string;
|
|
@@ -40,5 +41,9 @@ export declare const TEMPLATES: {
|
|
|
40
41
|
app: string;
|
|
41
42
|
error: string;
|
|
42
43
|
entry: string;
|
|
44
|
+
loader: string;
|
|
45
|
+
};
|
|
46
|
+
export declare const SYNC_TEMPLATES: {
|
|
47
|
+
loader: string;
|
|
43
48
|
};
|
|
44
49
|
export type TemplateType = (typeof TEMPLATES)[keyof typeof TEMPLATES];
|
package/templates/types.js
CHANGED
|
@@ -9,6 +9,7 @@ export const BASE_TEMPLATES = {
|
|
|
9
9
|
app: 'app',
|
|
10
10
|
error: 'error',
|
|
11
11
|
entry: 'entry',
|
|
12
|
+
loader: 'loader',
|
|
12
13
|
};
|
|
13
14
|
export const TEMPLATES = {
|
|
14
15
|
...BASE_TEMPLATES,
|
|
@@ -16,3 +17,6 @@ export const TEMPLATES = {
|
|
|
16
17
|
handler: 'handler',
|
|
17
18
|
markdown: 'markdown',
|
|
18
19
|
};
|
|
20
|
+
export const SYNC_TEMPLATES = {
|
|
21
|
+
loader: 'loader',
|
|
22
|
+
};
|
package/types.d.ts
CHANGED
|
@@ -122,6 +122,11 @@ export interface ZubyConfig {
|
|
|
122
122
|
* @private
|
|
123
123
|
*/
|
|
124
124
|
configFilePath?: string;
|
|
125
|
+
/**
|
|
126
|
+
* If you're building in multiple environments,
|
|
127
|
+
* you can use this option to generate consistent build IDs.
|
|
128
|
+
*/
|
|
129
|
+
generateBuildId?: () => string | Promise<string>;
|
|
125
130
|
}
|
|
126
131
|
export interface ZubyInternalConfig extends Required<ZubyConfig> {
|
|
127
132
|
/**
|
|
@@ -136,6 +141,10 @@ export interface ZubyInternalConfig extends Required<ZubyConfig> {
|
|
|
136
141
|
* @default []
|
|
137
142
|
*/
|
|
138
143
|
plugins: ZubyPlugin[];
|
|
144
|
+
/**
|
|
145
|
+
* The current build ID
|
|
146
|
+
*/
|
|
147
|
+
buildId: string;
|
|
139
148
|
}
|
|
140
149
|
export interface BaseCommandOptions {
|
|
141
150
|
/**
|
|
@@ -164,10 +173,27 @@ export interface BuildCommandOptions extends BaseCommandOptions {
|
|
|
164
173
|
}
|
|
165
174
|
export interface InitCommandOptions {
|
|
166
175
|
/**
|
|
167
|
-
* The
|
|
168
|
-
* @
|
|
176
|
+
* The relative path to the project directory.
|
|
177
|
+
* @example './my-zuby-app'
|
|
178
|
+
*/
|
|
179
|
+
projectPath?: string;
|
|
180
|
+
/**
|
|
181
|
+
* The name of the JSX provider,
|
|
182
|
+
* a.k.a. the framework you want to use to write your components.
|
|
183
|
+
* @example 'preact'
|
|
184
|
+
*/
|
|
185
|
+
jsxProviderName?: string;
|
|
186
|
+
/**
|
|
187
|
+
* The name of the example project to use.
|
|
188
|
+
* @example 'basic'
|
|
189
|
+
*/
|
|
190
|
+
exampleName?: string;
|
|
191
|
+
/**
|
|
192
|
+
* Set this option to true to use TypeScript
|
|
193
|
+
* for your new project.
|
|
194
|
+
* @example true
|
|
169
195
|
*/
|
|
170
|
-
|
|
196
|
+
useTypescript?: boolean;
|
|
171
197
|
}
|
|
172
198
|
export interface UpgradeCommandOptions extends BaseCommandOptions {
|
|
173
199
|
/**
|
|
@@ -195,6 +221,7 @@ export interface JsxProvider {
|
|
|
195
221
|
layoutTemplateFile: string;
|
|
196
222
|
innerLayoutTemplateFile: string;
|
|
197
223
|
errorTemplateFile: string;
|
|
224
|
+
loaderTemplateFile: string;
|
|
198
225
|
}
|
|
199
226
|
export type RenderToString = (vnode: any) => Promise<string> | string;
|
|
200
227
|
export type RenderToStream = (vnode: any) => Promise<ReadableStream> | ReadableStream;
|
|
@@ -286,6 +313,14 @@ export interface ZubyPlugin extends VitePlugin {
|
|
|
286
313
|
export interface ZubyConfigSetupHookParams {
|
|
287
314
|
config: ZubyConfig;
|
|
288
315
|
command: 'dev' | 'build';
|
|
316
|
+
addEntryTemplate: (entryFile: string) => void;
|
|
317
|
+
addAppTemplate: (appFile: string, path?: string) => void;
|
|
318
|
+
addLayoutTemplate: (layoutFile: string, path?: string) => void;
|
|
319
|
+
addInnerLayoutTemplate: (innerLayoutFile: string, path?: string) => void;
|
|
320
|
+
addErrorTemplate: (errorFile: string, path?: string) => void;
|
|
321
|
+
addLoaderTemplate: (loaderFile: string, path?: string) => void;
|
|
322
|
+
addPage: (pageFile: string, path?: string) => void;
|
|
323
|
+
addHandler: (handlerFile: string, path?: string) => void;
|
|
289
324
|
}
|
|
290
325
|
export interface ZubyConfigDoneHookParams extends ZubyConfigSetupHookParams {
|
|
291
326
|
config: ZubyInternalConfig;
|