zuby 1.0.45 → 1.0.47

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
@@ -5,6 +5,8 @@ import dev from './dev.js';
5
5
  import build from './build.js';
6
6
  import preview from './preview.js';
7
7
  import init from './init.js';
8
+ import info from './info.js';
9
+ import upgrade from './upgrade.js';
8
10
  const { name, description, version } = getZubyPackageConfig();
9
11
  const program = new Command().name(name).description(description).version(version);
10
12
  program
@@ -30,5 +32,18 @@ program
30
32
  program
31
33
  .command('init')
32
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')
33
39
  .action(async (options) => init(options));
40
+ program
41
+ .command('info')
42
+ .description('Prints useful information about your setup')
43
+ .action(async (options) => info(options));
44
+ program
45
+ .command('upgrade')
46
+ .description('Upgrades Zuby to the latest compatible version')
47
+ .option('-t, --tag <tag>', 'The tag to upgrade to (e.g. v1, v2, latest)')
48
+ .action(async (options) => upgrade(options));
34
49
  program.parse(process.argv);
@@ -0,0 +1,2 @@
1
+ import { BaseCommandOptions } from '../types.js';
2
+ export default function info({ configFile }: BaseCommandOptions): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { getZubyInternalConfig } from '../config.js';
2
+ import { getZubyPackageConfig } from '../packageConfig.js';
3
+ import os from 'os';
4
+ export default async function info({ configFile }) {
5
+ const { outDir, output, plugins, customLogger: logger } = await getZubyInternalConfig(configFile);
6
+ const { version } = getZubyPackageConfig();
7
+ logger.info(`Zuby: ${version}`);
8
+ logger.info(`Node: ${process.version}`);
9
+ logger.info(`OS: ${os.type()} ${os.release()}`);
10
+ logger.info(`Output: ${output}`);
11
+ logger.info(`Output dir: ${outDir}`);
12
+ const pluginNames = plugins.map(plugin => plugin.name).join(', ');
13
+ logger.info(`Plugins: ${plugins.length ? pluginNames : 'none'}`);
14
+ }
@@ -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,
@@ -0,0 +1,3 @@
1
+ import { UpgradeCommandOptions } from '../types.js';
2
+ export default function upgrade({ configFile, tag }: UpgradeCommandOptions): Promise<void>;
3
+ export declare function installDependencies(): boolean;
@@ -0,0 +1,80 @@
1
+ import { getZubyInternalConfig } from '../config.js';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { getZubyPackageConfig } from '../packageConfig.js';
6
+ import { getTitle } from '../branding.js';
7
+ import chalk from 'chalk';
8
+ export default async function upgrade({ configFile, tag }) {
9
+ const { customLogger: logger } = await getZubyInternalConfig(configFile);
10
+ const packageJsonPath = resolve('package.json');
11
+ const { version: currentVersion } = getZubyPackageConfig();
12
+ const [currentMajor, _currentMinor, _currentPatch] = currentVersion.split('.');
13
+ if (!existsSync(packageJsonPath)) {
14
+ logger.error(`ERROR: Could not find package.json`);
15
+ process.exit(1);
16
+ }
17
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
18
+ const res = await fetch('https://registry.npmjs.org/-/package/zuby/dist-tags');
19
+ if (!res.ok) {
20
+ logger.error(`ERROR: Failed to fetch latest version from NPM.`);
21
+ process.exit(1);
22
+ }
23
+ const tags = (await res.json());
24
+ const latestVersion = tags.latest;
25
+ const selectedVersion = tags[tag || `v${currentMajor}`] || latestVersion;
26
+ logger.info(getTitle(`Checking...`));
27
+ if (currentVersion === latestVersion) {
28
+ logger.info(`You are on the latest version of Zuby.js ${currentVersion}`);
29
+ logger.info(`Well done! 🎉`);
30
+ process.exit(0);
31
+ }
32
+ if (selectedVersion !== latestVersion) {
33
+ logger.info(chalk.bold(`New major version of Zuby.js is available: ${latestVersion}`));
34
+ logger.info(`Run 'npx zuby upgrade --tag latest' if you're prepared to upgrade to it.`);
35
+ logger.info(`\r\n`);
36
+ }
37
+ if (currentVersion === selectedVersion) {
38
+ logger.info(`You are on the latest compatible version of Zuby.js ${currentVersion}`);
39
+ process.exit(0);
40
+ }
41
+ logger.info(`Upgrading Zuby.js to ${selectedVersion}`);
42
+ Object.keys({
43
+ ...(packageJson.dependencies || {}),
44
+ ...(packageJson.devDependencies || {}),
45
+ }).forEach(name => {
46
+ if (!name.match(/^zuby|@zubyjs\/(.+)$/i))
47
+ return;
48
+ if (packageJson.dependencies?.[name]) {
49
+ packageJson.dependencies[name] = selectedVersion;
50
+ }
51
+ if (packageJson.devDependencies?.[name]) {
52
+ packageJson.devDependencies[name] = selectedVersion;
53
+ }
54
+ });
55
+ logger.info(`Writing package.json`);
56
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
57
+ if (installDependencies()) {
58
+ logger.info(`Zuby.js upgraded to ${selectedVersion}!`);
59
+ logger.info(`Well done! 🎉`);
60
+ }
61
+ else {
62
+ logger.info(`We're done! 🎉 Now it's your turn.`);
63
+ logger.info(`Please run 'npm install', 'yarn install' etc.. with your favorite package manager to finish the upgrade.`);
64
+ }
65
+ }
66
+ export function installDependencies() {
67
+ if (existsSync('package-lock.json')) {
68
+ execSync('npm install', { stdio: 'inherit' });
69
+ return true;
70
+ }
71
+ if (existsSync('yarn.lock')) {
72
+ execSync('yarn install', { stdio: 'inherit' });
73
+ return true;
74
+ }
75
+ if (existsSync('pnpm-lock.yaml')) {
76
+ execSync('pnpm install', { stdio: 'inherit' });
77
+ return true;
78
+ }
79
+ return false;
80
+ }
package/config.js CHANGED
@@ -10,6 +10,7 @@ import chunkNamingPlugin from './plugins/chunkNamingPlugin/index.js';
10
10
  import manifestPlugin from './plugins/manifestPlugin/index.js';
11
11
  import prerenderPlugin from './plugins/prerenderPlugin/index.js';
12
12
  import standaloneBuildPlugin from './plugins/dependenciesPlugin/index.js';
13
+ import preloadPlugin from './plugins/preloadPlugin/index.js';
13
14
  let zubyInternalConfig;
14
15
  /**
15
16
  * Returns the path to the ZubyConfig file.
@@ -148,6 +149,7 @@ export const getBuiltInPlugins = () => {
148
149
  manifestPlugin(),
149
150
  prerenderPlugin(),
150
151
  standaloneBuildPlugin(),
152
+ preloadPlugin(),
151
153
  ];
152
154
  };
153
155
  /**
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;
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuby",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
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,9 @@ 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;
169
175
  }
@@ -218,4 +218,11 @@ 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
+ }
221
228
  }
@@ -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,7 +19,8 @@ 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
  }
@@ -42,6 +45,7 @@ export async function generateTemplatesCode(ssr) {
42
45
  const innerLayouts = await getInnerLayouts(templates);
43
46
  const errors = await getErrors(templates);
44
47
  const handlers = await getHandlers(templates);
48
+ const loaders = await getLoaders(templates);
45
49
  const pagesCode = await Promise.all(pages.map(generateTemplateCode) || []);
46
50
  const appsCode = await Promise.all(apps.map(generateTemplateCode) || []);
47
51
  const errorsCode = await Promise.all(errors.map(generateTemplateCode) || []);
@@ -50,6 +54,7 @@ export async function generateTemplatesCode(ssr) {
50
54
  ? await Promise.all(innerLayouts.map(generateTemplateCode) || [])
51
55
  : [];
52
56
  const handlersCode = ssr ? await Promise.all(handlers.map(generateTemplateCode) || []) : [];
57
+ const loadersCode = await Promise.all(loaders.map(generateTemplateCode) || []);
53
58
  return `{
54
59
  pages: [${pagesCode.join(',')}],
55
60
  apps: [${appsCode.join(',')}],
@@ -57,6 +62,7 @@ export async function generateTemplatesCode(ssr) {
57
62
  layouts: [${layoutsCode.join(',')}],
58
63
  innerLayouts: [${innerLayoutsCode.join(',')}],
59
64
  handlers: [${handlersCode.join(',')}],
65
+ loaders: [${loadersCode.join(',')}],
60
66
  }`;
61
67
  }
62
68
  export async function generateTemplateCode(template) {
@@ -67,12 +73,28 @@ export async function generateTemplateCode(template) {
67
73
  pathParams: ${JSON.stringify(template.pathParams)},
68
74
  pathType: "${template.pathType}",
69
75
  templateType: "${template.templateType}",
70
- component: () => import("${template.filename}"),
76
+ component: () => ${generateImportCode(template)},
71
77
  }`;
72
78
  }
79
+ export function generateImportCode(template) {
80
+ // Sync templates are imported statically
81
+ if (Object.values(SYNC_TEMPLATES).includes(template.templateType)) {
82
+ const key = `__zuby_static_import_${staticImports.length}`;
83
+ staticImports.push({
84
+ key,
85
+ path: template.filename,
86
+ });
87
+ return key;
88
+ }
89
+ // Async templates are imported dynamically
90
+ return `import("${template.filename}")`;
91
+ }
73
92
  export async function generateRenderCode(ssr) {
74
93
  const { jsx } = await getZubyInternalConfig();
75
94
  if (!ssr)
76
95
  return '{}';
77
96
  return `await import("${jsx.renderFile}")`;
78
97
  }
98
+ export function getStaticImportsCode() {
99
+ return staticImports.map(({ key, path }) => `import ${key} from "${path}";`).join('\n');
100
+ }
@@ -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,87 @@
1
+ import { getContext } from '../context/index.js';
2
+ /**
3
+ * The set of links that were already preloaded.
4
+ */
5
+ const preloadLinks = new Set();
6
+ /**
7
+ * Preload manifest that lists all assets
8
+ * that should be preloaded for each page.
9
+ * It is generated by preloadPlugin.
10
+ * @example {
11
+ * "pages/index.tsx": [
12
+ * "/chunks/chunk-NGLgOfVh.js",
13
+ * "/chunks/chunk-8kxF91T_.css"
14
+ * ],
15
+ * }
16
+ */
17
+ let preloadManifest;
18
+ /**
19
+ * Preloads given link with the lowest priority
20
+ * in the future when network is idle and DOM loaded.
21
+ * @example preload("/api/products/1", "json")
22
+ */
23
+ export function preload(href, as = 'fetch') {
24
+ // Do nothing on server
25
+ if (typeof window === 'undefined')
26
+ return;
27
+ // Preload link only once
28
+ if (preloadLinks.has(href))
29
+ return;
30
+ preloadLinks.add(href);
31
+ // Detect asset type by extension
32
+ as = href.match(/\.css(\?.*)?$/) ? 'style' : as;
33
+ as = href.match(/\.js(\?.*)?$/) ? 'script' : as;
34
+ // Preload link in the future when network is idle
35
+ // and DOM content was loaded.
36
+ window.requestIdleCallback(() => addPreloadEntry({
37
+ href,
38
+ as,
39
+ }));
40
+ }
41
+ /**
42
+ * Preloads all required assets for matching page.
43
+ * Such as scripts, styles, props, etc.
44
+ * @example preloadPage("/products/1")
45
+ */
46
+ export function preloadPage(href, onHandle = () => { }) {
47
+ // Do nothing on server
48
+ if (typeof window === 'undefined')
49
+ return;
50
+ const context = getContext();
51
+ const pages = context.templates?.pages || [];
52
+ const page = pages.find(({ pathRegex }) => {
53
+ return pathRegex.test(href);
54
+ });
55
+ // Preload link itself
56
+ // if matching page was not found
57
+ if (!page) {
58
+ preload(href);
59
+ return;
60
+ }
61
+ window.requestIdleCallback(async () => {
62
+ await loadPreloadManifest();
63
+ // Preload assets such as scripts and styles
64
+ const preloadAssets = preloadManifest?.[page.filename] || [];
65
+ preloadAssets.forEach(href => preload(href));
66
+ onHandle();
67
+ });
68
+ }
69
+ /**
70
+ * Appends preload link into head element of page.
71
+ * For example:
72
+ * <link rel="preload" href="/api/products/1" as="json"/>
73
+ */
74
+ function addPreloadEntry({ href, as }) {
75
+ const preloadLink = document.createElement('link');
76
+ preloadLink.href = href;
77
+ preloadLink.as = as;
78
+ preloadLink.rel = 'preload';
79
+ document.head.appendChild(preloadLink);
80
+ }
81
+ async function loadPreloadManifest() {
82
+ if (preloadManifest)
83
+ return;
84
+ preloadManifest = {};
85
+ const res = await fetch('/preload-manifest.json');
86
+ preloadManifest = await res.json();
87
+ }
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
@@ -2319,6 +2319,13 @@ var ZubyPageContext = class {
2319
2319
  return path2;
2320
2320
  return `/${locale}/${path2}`.replace(/\/+/g, "/");
2321
2321
  }
2322
+ /**
2323
+ * Returns true if the current request
2324
+ * was made by the Zuby.js pre-render build step.
2325
+ */
2326
+ get isPrerendering() {
2327
+ return this._request?.headers?.get("user-agent") === "zuby-prerender";
2328
+ }
2322
2329
  };
2323
2330
 
2324
2331
  // src/server/zubyRenderer.ts
@@ -8507,7 +8514,8 @@ var BASE_TEMPLATES = {
8507
8514
  innerLayout: "innerLayout",
8508
8515
  app: "app",
8509
8516
  error: "error",
8510
- entry: "entry"
8517
+ entry: "entry",
8518
+ loader: "loader"
8511
8519
  };
8512
8520
  var TEMPLATES = {
8513
8521
  ...BASE_TEMPLATES,
@@ -8812,6 +8820,9 @@ var ZubyServer = class {
8812
8820
  "Content-Length": fileStats.size.toString(),
8813
8821
  "Last-Modified": fileStats.mtime.toUTCString()
8814
8822
  };
8823
+ if (basename2(file).match(/^(chunk|entry)-/)) {
8824
+ headers["Cache-Control"] = "public, max-age=31536000, immutable";
8825
+ }
8815
8826
  let streamOptions = {
8816
8827
  start: 0,
8817
8828
  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
@@ -164,10 +164,34 @@ export interface BuildCommandOptions extends BaseCommandOptions {
164
164
  }
165
165
  export interface InitCommandOptions {
166
166
  /**
167
- * The name of the new project.
168
- * @default my-zuby-app
167
+ * The relative path to the project directory.
168
+ * @example './my-zuby-app'
169
169
  */
170
- projectName?: string;
170
+ projectPath?: string;
171
+ /**
172
+ * The name of the JSX provider,
173
+ * a.k.a. the framework you want to use to write your components.
174
+ * @example 'preact'
175
+ */
176
+ jsxProviderName?: string;
177
+ /**
178
+ * The name of the example project to use.
179
+ * @example 'basic'
180
+ */
181
+ exampleName?: string;
182
+ /**
183
+ * Set this option to true to use TypeScript
184
+ * for your new project.
185
+ * @example true
186
+ */
187
+ useTypescript?: boolean;
188
+ }
189
+ export interface UpgradeCommandOptions extends BaseCommandOptions {
190
+ /**
191
+ * The version tag to upgrade to.
192
+ * Defaults to the latest compatible version.
193
+ */
194
+ tag?: string;
171
195
  }
172
196
  export declare const MODES: {
173
197
  development: string;
@@ -188,6 +212,7 @@ export interface JsxProvider {
188
212
  layoutTemplateFile: string;
189
213
  innerLayoutTemplateFile: string;
190
214
  errorTemplateFile: string;
215
+ loaderTemplateFile: string;
191
216
  }
192
217
  export type RenderToString = (vnode: any) => Promise<string> | string;
193
218
  export type RenderToStream = (vnode: any) => Promise<ReadableStream> | ReadableStream;