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 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')
@@ -1,5 +1,5 @@
1
1
  import { InitCommandOptions } from '../types.js';
2
- export default function init(options: InitCommandOptions): Promise<void>;
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(options) {
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 { projectPath, jsxProviderName, exampleName } = await inquirer.prompt([
23
- {
24
- type: 'input',
25
- name: 'projectPath',
26
- message: 'Where would you like to create your new project?',
27
- default: `./${defaultProjectName}`,
28
- prefix: `${chalk.yellowBright.bgWhite(' name ')} ❯ `,
29
- validate(projectPath) {
30
- if (existsSync(projectPath)) {
31
- return 'Project with this name already exists. Please choose a different one.';
32
- }
33
- return true;
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
- type: 'list',
38
- name: 'jsxProviderName',
39
- message: 'In which JSX framework would you like to write your components?',
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.split('/').pop() || defaultProjectName;
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
+ };
@@ -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
@@ -23,6 +23,9 @@ export class ZubyContext {
23
23
  get i18n() {
24
24
  return this.rawContext.i18n;
25
25
  }
26
+ get buildId() {
27
+ return this.rawContext.buildId;
28
+ }
26
29
  }
27
30
  const getRawContext = () => {
28
31
  return globalThis.ZubyRawContext;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuby",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "description": "Zuby.js is framework for building SPA apps using Vite",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -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
  }
@@ -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
- return contextCode + code;
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: () => import("${template.filename}"),
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,7 @@
1
+ import { ZubyPlugin } from '../../types.js';
2
+ /**
3
+ * This is internal plugin
4
+ * that generates preload manifest file
5
+ * that is used for preloading assets.
6
+ */
7
+ export default function preloadPlugin(): ZubyPlugin;
@@ -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]: 'zuby-prerender',
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;
@@ -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
@@ -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,
@@ -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
  */
@@ -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
  */
@@ -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];
@@ -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 name of the new project.
168
- * @default my-zuby-app
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
- projectName?: string;
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;