zuby 1.0.20 → 1.0.22

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.
Files changed (39) hide show
  1. package/commands/init.d.ts +16 -0
  2. package/commands/init.js +120 -13
  3. package/commands/preview.js +1 -1
  4. package/config.d.ts +1 -6
  5. package/config.js +7 -22
  6. package/examples/basic/js/components/Card/index.css +37 -0
  7. package/examples/basic/js/components/Card/index.jsx +17 -0
  8. package/examples/basic/js/components/Card/index.module.css +36 -0
  9. package/examples/basic/js/package.json +14 -0
  10. package/examples/basic/js/pages/about.jsx +7 -0
  11. package/examples/basic/js/pages/app.css +49 -0
  12. package/examples/basic/js/pages/app.jsx +7 -0
  13. package/examples/basic/js/pages/index.jsx +24 -0
  14. package/examples/basic/js/pages/products/[id].jsx +7 -0
  15. package/examples/basic/js/zuby.config.mjs +6 -0
  16. package/examples/basic/ts/components/Card/index.module.css +36 -0
  17. package/examples/basic/ts/components/Card/index.tsx +21 -0
  18. package/examples/basic/ts/package.json +14 -0
  19. package/examples/basic/ts/pages/about.tsx +7 -0
  20. package/examples/basic/ts/pages/app.css +49 -0
  21. package/examples/basic/ts/pages/app.tsx +9 -0
  22. package/examples/basic/ts/pages/index.tsx +24 -0
  23. package/examples/basic/ts/pages/products/[id].tsx +7 -0
  24. package/examples/basic/ts/tsconfig.json +32 -0
  25. package/examples/basic/ts/zuby.config.ts +6 -0
  26. package/package.json +10 -7
  27. package/plugins/chunkNamingPlugin/index.js +4 -1
  28. package/plugins/contextPlugin/index.js +3 -0
  29. package/plugins/prerenderPlugin/index.js +33 -10
  30. package/providers/index.d.ts +20 -0
  31. package/providers/index.js +75 -0
  32. package/providers/preact/index.d.ts +1 -1
  33. package/providers/preact/index.js +3 -1
  34. package/providers/preact/router.js +4 -4
  35. package/providers/types.d.ts +17 -0
  36. package/providers/types.js +1 -0
  37. package/server/index.js +2 -2
  38. package/templates/types.d.ts +1 -0
  39. package/types.d.ts +3 -15
@@ -1,2 +1,18 @@
1
1
  import { InitCommandOptions } from '../types.js';
2
2
  export default function init(options: InitCommandOptions): Promise<void>;
3
+ export declare function initExample(example: Example, options: InitOptions): Promise<void>;
4
+ export declare function getExamples(): Example[];
5
+ export declare function updateDependencies({ projectPath, jsxProvider }: InitOptions): Promise<void>;
6
+ export interface Example {
7
+ name: string;
8
+ path: string;
9
+ isJs: boolean;
10
+ isTs: boolean;
11
+ }
12
+ export interface InitOptions {
13
+ projectName?: string;
14
+ projectPath?: string;
15
+ jsxProvider?: string;
16
+ useTypescript?: boolean;
17
+ zubyVersion?: string;
18
+ }
package/commands/init.js CHANGED
@@ -2,33 +2,140 @@ import inquirer from 'inquirer';
2
2
  import { createLogger } from '../logger/index.js';
3
3
  import { getTitle } from '../branding.js';
4
4
  import chalk from 'chalk';
5
+ import { existsSync, readdirSync, cpSync, statSync } from 'fs';
6
+ import { readFile, writeFile } from 'fs/promises';
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname, join } from 'path';
9
+ import { normalizePath } from '../utils/pathUtils.js';
10
+ import { getZubyPackageConfig } from '../packageConfig.js';
11
+ import { globSync } from 'glob';
12
+ import { ProviderDependencies, Providers } from '../providers/index.js';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
5
15
  export default async function init(options) {
6
16
  const logger = createLogger();
7
17
  logger.clearScreen('info');
8
18
  logger?.info(`${getTitle(`Let's get started with Zuby`)}\r\n`);
9
19
  logger.info("Initializing a new Zuby project");
10
- const { projectName } = await inquirer.prompt([
20
+ const examples = getExamples();
21
+ const defaultProjectName = 'my-zuby-app';
22
+ const defaultExample = examples.find(example => example.name === 'basic');
23
+ const { projectPath, jsxProvider, exampleName, } = await inquirer.prompt([
11
24
  {
12
25
  type: 'input',
13
- name: 'projectName',
14
- message: 'What\'s the name of your new app?',
15
- default: './my-zuby-app',
16
- prefix: `${chalk.yellowBright.bgWhite(' name ')} ❯ `,
26
+ name: 'projectPath',
27
+ message: 'Where would you like to create your new project?',
28
+ default: `./${defaultProjectName}`,
29
+ prefix: `${chalk.yellowBright.bgWhite(' name ')} ❯ `,
30
+ validate(projectPath) {
31
+ if (existsSync(projectPath)) {
32
+ return 'Project with this name already exists. Please choose a different one.';
33
+ }
34
+ return true;
35
+ }
17
36
  },
18
37
  {
19
38
  type: 'list',
20
39
  name: 'jsxProvider',
21
40
  message: 'In which JSX framework would you like to write your components?',
22
- choices: ['preact', 'react'],
41
+ choices: Object.keys(Providers),
23
42
  default: 'preact',
24
- prefix: `${chalk.yellowBright.bgWhite(' jsx ')} ❯ `,
43
+ prefix: `${chalk.yellowBright.bgWhite(' jsx ')} ❯ `,
25
44
  },
26
45
  {
27
- type: 'confirm',
28
- name: 'useTypescript',
29
- message: 'Do you want to write in TypeScript?',
30
- default: true,
31
- prefix: `${chalk.yellowBright.bgWhite(' ts ')} ❯ `,
32
- }
46
+ type: 'list',
47
+ name: 'exampleName',
48
+ message: 'Which Zuby example project would you like to use?',
49
+ choices: examples.map(example => example.name),
50
+ default: defaultExample?.name,
51
+ prefix: `${chalk.yellowBright.bgWhite(' example ')} ❯ `,
52
+ },
33
53
  ]);
54
+ let useTypescript = false;
55
+ const selectedExample = examples.find(example => example.name === exampleName);
56
+ if (!selectedExample) {
57
+ logger.error(`Example ${exampleName} was not found`);
58
+ process.exit(1);
59
+ }
60
+ if (selectedExample?.isTs) {
61
+ ({
62
+ useTypescript,
63
+ } = await inquirer.prompt([
64
+ {
65
+ type: 'confirm',
66
+ name: 'useTypescript',
67
+ message: 'Do you want to write in TypeScript?',
68
+ default: true,
69
+ prefix: `${chalk.yellowBright.bgWhite(' ts ')} ❯ `,
70
+ }
71
+ ]));
72
+ }
73
+ const projectName = projectPath.split('/').pop() || defaultProjectName;
74
+ const zubyVersion = getZubyPackageConfig().version;
75
+ await initExample(selectedExample, {
76
+ projectName,
77
+ projectPath,
78
+ jsxProvider,
79
+ useTypescript,
80
+ zubyVersion,
81
+ });
82
+ await updateDependencies({
83
+ projectPath,
84
+ jsxProvider,
85
+ });
86
+ logger?.info(`\r\nCreated ${chalk.bold(projectName)} at ${chalk.bold(projectPath)}`);
87
+ logger?.info(`${chalk.greenBright.bold('We are done! 🎉')}`);
88
+ logger?.info(`\r\n`);
89
+ logger?.info(`Now enter your project directory using ${chalk.bold(`cd ${projectPath}`)}`);
90
+ logger?.info(`and run ${chalk.bold('npm install')} or ${chalk.bold('yarn install')} to install all dependencies.`);
91
+ }
92
+ export async function initExample(example,
93
+ // Following values can be used in templates
94
+ // and will be replaced during init.
95
+ // Example: <jsxProvider> => preact
96
+ options) {
97
+ const { projectPath = '', useTypescript, } = options;
98
+ // Copy example files
99
+ cpSync(normalizePath(join(example.path, useTypescript ? 'ts' : 'js')), projectPath, {
100
+ recursive: true
101
+ });
102
+ // Interpolate values in files of the example
103
+ const transformPromises = globSync(normalizePath(join(projectPath, '**', '*')))
104
+ .map(async (file) => {
105
+ if (statSync(file).isDirectory())
106
+ return;
107
+ let fileContent = await readFile(file, 'utf-8');
108
+ Object.entries(options).forEach(([key, value]) => {
109
+ fileContent = fileContent.replace(new RegExp(`<${key}>`, 'g'), value.toString());
110
+ });
111
+ await writeFile(file, fileContent);
112
+ });
113
+ await Promise.all(transformPromises);
114
+ }
115
+ export function getExamples() {
116
+ const examplesPath = normalizePath(join(__dirname, '..', 'examples'));
117
+ const exampleNames = readdirSync(examplesPath);
118
+ return exampleNames.map(exampleName => ({
119
+ name: exampleName,
120
+ path: normalizePath(join(examplesPath, exampleName)),
121
+ isJs: existsSync(normalizePath(join(examplesPath, exampleName, 'js'))),
122
+ isTs: existsSync(normalizePath(join(examplesPath, exampleName, 'ts'))),
123
+ }));
124
+ }
125
+ export async function updateDependencies({ projectPath = '', jsxProvider = '' }) {
126
+ const packageJsonPath = normalizePath(join(projectPath, 'package.json'));
127
+ let packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
128
+ // Receive dependencies for the selected jsxProvider
129
+ const dependencies = ProviderDependencies[jsxProvider]?.dependencies || {};
130
+ const devDependencies = ProviderDependencies[jsxProvider]?.devDependencies || {};
131
+ // Merge all dependencies
132
+ packageJson.dependencies = {
133
+ ...packageJson.dependencies,
134
+ ...dependencies,
135
+ };
136
+ packageJson.devDependencies = {
137
+ ...packageJson.devDependencies,
138
+ ...devDependencies,
139
+ };
140
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
34
141
  }
@@ -7,6 +7,6 @@ export default async function preview(options) {
7
7
  if (outDir && !existsSync(outDir)) {
8
8
  throw new Error(`The outDir '${outDir}' does not exist. Did you forget to run 'zuby build' first?`);
9
9
  }
10
- const serverPath = normalizePath(resolve(outDir, 'server.js'));
10
+ const serverPath = normalizePath(resolve(outDir, 'server.mjs'));
11
11
  await import(`file:///${serverPath}`);
12
12
  }
package/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JsxProvider, ZubyConfig } from './types.js';
1
+ import { ZubyConfig } from './types.js';
2
2
  /**
3
3
  * This function loads the config file and returns the config object with default values.
4
4
  * It also caches the config object for future use.
@@ -15,10 +15,5 @@ export declare const mergeDefaultConfig: (config?: ZubyConfig) => Promise<ZubyCo
15
15
  * This function returns the array of built-in Zuby plugins.
16
16
  * Check which framework components are relying on each plugin
17
17
  * before removing any of them.
18
- * @param config
19
18
  */
20
19
  export declare const getBuiltInPlugins: () => import("vite").PluginOption[];
21
- /**
22
- * Returns the instance of the JSX provider.
23
- */
24
- export declare const getJsxProvider: (jsx: string) => JsxProvider;
package/config.js CHANGED
@@ -4,14 +4,11 @@ import { bundleRequire } from 'bundle-require';
4
4
  import { getApps, getErrors, getInnerLayouts, getLayouts, getPages, getTemplates } from './templates/index.js';
5
5
  import { createLogger } from './logger/index.js';
6
6
  // Plugins
7
- import { preact as preactPlugin } from '@preact/preset-vite';
8
7
  import contextPlugin from './plugins/contextPlugin/index.js';
9
8
  import compileTimePlugin from './plugins/compileTimePlugin/index.js';
10
9
  import chunkNamingPlugin from './plugins/chunkNamingPlugin/index.js';
11
10
  import prerenderPlugin from './plugins/prerenderPlugin/index.js';
12
- // JSX providers
13
- import preactProvider from './providers/preact/index.js';
14
- import { generateBuildId } from './utils/buildIdUtils.js';
11
+ import { checkProviderDependencies, getProvider } from './providers/index.js';
15
12
  let zubyConfig;
16
13
  /**
17
14
  * This function loads the config file and returns the config object with default values.
@@ -44,6 +41,8 @@ export const getZubyConfig = async (configFile = ZUBY_CONFIG_FILE, cache = true)
44
41
  throw new Error(`No valid default export found in config file: ${foundConfigFile}`);
45
42
  }
46
43
  zubyConfig = await mergeDefaultConfig(zubyConfig);
44
+ // Check provider dependencies
45
+ await checkProviderDependencies(zubyConfig?.jsxProvider?.name || '');
47
46
  return zubyConfig;
48
47
  };
49
48
  /**
@@ -63,8 +62,6 @@ export const mergeDefaultConfig = async (config = {}) => {
63
62
  config.server = config.server ?? {};
64
63
  config.server.host = config.server.host ?? 'localhost';
65
64
  config.server.port = config.server.port ?? 3000;
66
- // Generate the build id if not set
67
- config.buildId = config.buildId ?? generateBuildId();
68
65
  // Add minification
69
66
  config.minifyCSS = config.minifyCSS ?? true;
70
67
  config.minifyHTML = config.minifyHTML ?? true;
@@ -88,14 +85,15 @@ export const mergeDefaultConfig = async (config = {}) => {
88
85
  config.vite.logLevel = config.vite.logLevel ?? config.logLevel;
89
86
  config.vite.customLogger = config.vite.customLogger ?? config.customLogger;
90
87
  config.vite.server = config.vite.server ?? config.server;
88
+ // Load jsx provider
89
+ config.jsx = config.jsx ?? 'preact';
90
+ config.jsxProvider = config.jsxProvider ?? await getProvider(config.jsx);
91
91
  // Merge built-in plugins with user plugins
92
92
  config.vite.plugins = [
93
93
  ...getBuiltInPlugins(),
94
+ ...(config.jsxProvider?.getPlugins() ?? []),
94
95
  ...(config.vite?.plugins ?? []),
95
96
  ];
96
- // Load jsx provider
97
- config.jsx = config.jsx ?? 'preact';
98
- config.jsxProvider = config.jsxProvider ?? getJsxProvider(config.jsx);
99
97
  // Load templates
100
98
  const templates = await getTemplates();
101
99
  config.templates = config.templates ?? {};
@@ -110,25 +108,12 @@ export const mergeDefaultConfig = async (config = {}) => {
110
108
  * This function returns the array of built-in Zuby plugins.
111
109
  * Check which framework components are relying on each plugin
112
110
  * before removing any of them.
113
- * @param config
114
111
  */
115
112
  export const getBuiltInPlugins = () => {
116
113
  return [
117
- preactPlugin(),
118
114
  contextPlugin(),
119
115
  compileTimePlugin(),
120
116
  chunkNamingPlugin(),
121
117
  prerenderPlugin(),
122
118
  ];
123
119
  };
124
- /**
125
- * Returns the instance of the JSX provider.
126
- */
127
- export const getJsxProvider = (jsx) => {
128
- const provider = ({
129
- 'preact': preactProvider,
130
- })[jsx] || undefined;
131
- if (!provider)
132
- throw new Error(`JSX provider '${jsx}' is not supported.`);
133
- return provider;
134
- };
@@ -0,0 +1,37 @@
1
+ .link-card {
2
+ list-style: none;
3
+ display: flex;
4
+ padding: 1px;
5
+ background-color: #23262d;
6
+ background-image: none;
7
+ background-size: 400%;
8
+ border-radius: 7px;
9
+ background-position: 100%;
10
+ transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
11
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
12
+ }
13
+ .link-card > a {
14
+ width: 100%;
15
+ text-decoration: none;
16
+ line-height: 1.4;
17
+ padding: calc(1.5rem - 1px);
18
+ border-radius: 8px;
19
+ color: white;
20
+ background-color: #23262d;
21
+ opacity: 0.8;
22
+ }
23
+ h2 {
24
+ margin: 0;
25
+ font-size: 1.25rem;
26
+ transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
27
+ }
28
+ p {
29
+ margin-top: 0.5rem;
30
+ margin-bottom: 0;
31
+ }
32
+ .link-card:is(:hover, :focus-within) {
33
+ background-position: 0;
34
+ }
35
+ .link-card:is(:hover, :focus-within) h2 {
36
+ color: #ffcc00;
37
+ }
@@ -0,0 +1,17 @@
1
+ import styles from './index.module.css';
2
+
3
+ export default function Card({ href, title, body }) {
4
+ return (
5
+ <li className={styles.linkCard}>
6
+ <a href={href}>
7
+ <h2>
8
+ {title}
9
+ <span>&rarr;</span>
10
+ </h2>
11
+ <p>
12
+ {body}
13
+ </p>
14
+ </a>
15
+ </li>
16
+ );
17
+ }
@@ -0,0 +1,36 @@
1
+ .linkCard {
2
+ list-style: none;
3
+ display: flex;
4
+ padding: 1px;
5
+ background-color: #fbf9f8;
6
+ background-image: none;
7
+ background-size: 400%;
8
+ border-radius: 7px;
9
+ background-position: 100%;
10
+ transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
11
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
12
+ }
13
+ .linkCard > a {
14
+ width: 100%;
15
+ text-decoration: none;
16
+ line-height: 1.4;
17
+ padding: calc(1.5rem - 1px);
18
+ border-radius: 8px;
19
+ color: #000;
20
+ opacity: 0.8;
21
+ }
22
+ h2 {
23
+ margin: 0;
24
+ font-size: 1.25rem;
25
+ transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
26
+ }
27
+ p {
28
+ margin-top: 0.5rem;
29
+ margin-bottom: 0;
30
+ }
31
+ .linkCard:is(:hover, :focus-within) {
32
+ background-position: 0;
33
+ }
34
+ .linkCard:is(:hover, :focus-within) h2 {
35
+ color: #ffcc00;
36
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "<projectName>",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "npx zuby dev",
8
+ "build": "npx zuby build",
9
+ "preview": "npx zuby preview"
10
+ },
11
+ "dependencies": {
12
+ "zuby": "^<zubyVersion>"
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ export default function About() {
2
+ return (
3
+ <main>
4
+ <h1>Hello from <span className="text-gradient">/about</span> page</h1>
5
+ </main>
6
+ );
7
+ }
@@ -0,0 +1,49 @@
1
+ html {
2
+ font-family: system-ui, sans-serif;
3
+ background: #f5f0f0;
4
+ background-size: 224px;
5
+ }
6
+ code {
7
+ font-family: 'Segoe UI', Verdana, sans-serif;
8
+ }
9
+ main {
10
+ margin: auto;
11
+ padding: 1rem;
12
+ width: 800px;
13
+ max-width: calc(100% - 2rem);
14
+ color: #202020;
15
+ font-size: 20px;
16
+ line-height: 1.6;
17
+ }
18
+ h1 {
19
+ font-size: 4rem;
20
+ font-weight: 700;
21
+ line-height: 1;
22
+ text-align: center;
23
+ margin-bottom: 1em;
24
+ }
25
+ .text-gradient {
26
+ -webkit-background-clip: text;
27
+ -webkit-text-fill-color: transparent;
28
+ background-color: #ef5734;
29
+ background-image: linear-gradient(315deg, #ef5734 0%, #ffcc2f 74%);
30
+ background-position: 0;
31
+ }
32
+ .instructions {
33
+ margin-bottom: 2rem;
34
+ padding: 1.5rem;
35
+ border-radius: 8px;
36
+ text-align: center;
37
+ }
38
+ .instructions code {
39
+ font-size: 0.8em;
40
+ font-weight: bold;
41
+ border-radius: 4px;
42
+ padding: 0.3em 0.4em;
43
+ }
44
+ .link-card-grid {
45
+ display: grid;
46
+ grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
47
+ gap: 2rem;
48
+ padding: 0;
49
+ }
@@ -0,0 +1,7 @@
1
+ import './app.css'
2
+
3
+ export default function App({
4
+ children
5
+ }) {
6
+ return children;
7
+ }
@@ -0,0 +1,24 @@
1
+ import Card from '../components/Card/index.jsx';
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main>
6
+ <h1>Hello from <span className="text-gradient">Zuby.js</span> site</h1>
7
+ <p className="instructions">
8
+ To get started, open the directory <code>./pages</code> in your project and edit <code>index</code> file.
9
+ </p>
10
+ <ul role="list" className="link-card-grid">
11
+ <Card
12
+ href="https://zuby.futrou.com/docs"
13
+ title="Documentation"
14
+ body="Find config API, tips and tricks, and more in the Zuby.js docs."
15
+ />
16
+ <Card
17
+ href="https://zuby.futrou.com/examples"
18
+ title="Examples"
19
+ body="See what's possible to build with Zuby.js by browsing examples."
20
+ />
21
+ </ul>
22
+ </main>
23
+ );
24
+ }
@@ -0,0 +1,7 @@
1
+ export default function Product() {
2
+ return (
3
+ <main>
4
+ <h1>Hello from the dynamic product page</h1>
5
+ </main>
6
+ );
7
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'zuby';
2
+
3
+ export default defineConfig({
4
+ outDir: '.zuby',
5
+ jsx: '<jsxProvider>',
6
+ })
@@ -0,0 +1,36 @@
1
+ .linkCard {
2
+ list-style: none;
3
+ display: flex;
4
+ padding: 1px;
5
+ background-color: #fbf9f8;
6
+ background-image: none;
7
+ background-size: 400%;
8
+ border-radius: 7px;
9
+ background-position: 100%;
10
+ transition: background-position 0.6s cubic-bezier(0.22, 1, 0.36, 1);
11
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
12
+ }
13
+ .linkCard > a {
14
+ width: 100%;
15
+ text-decoration: none;
16
+ line-height: 1.4;
17
+ padding: calc(1.5rem - 1px);
18
+ border-radius: 8px;
19
+ color: #000;
20
+ opacity: 0.8;
21
+ }
22
+ h2 {
23
+ margin: 0;
24
+ font-size: 1.25rem;
25
+ transition: color 0.6s cubic-bezier(0.22, 1, 0.36, 1);
26
+ }
27
+ p {
28
+ margin-top: 0.5rem;
29
+ margin-bottom: 0;
30
+ }
31
+ .linkCard:is(:hover, :focus-within) {
32
+ background-position: 0;
33
+ }
34
+ .linkCard:is(:hover, :focus-within) h2 {
35
+ color: #ffcc00;
36
+ }
@@ -0,0 +1,21 @@
1
+ import styles from './index.module.css';
2
+
3
+ export default function Card({ href, title, body } : {
4
+ href: string,
5
+ title: string,
6
+ body: string,
7
+ }) {
8
+ return (
9
+ <li className={styles.linkCard}>
10
+ <a href={href}>
11
+ <h2>
12
+ {title}
13
+ <span>&rarr;</span>
14
+ </h2>
15
+ <p>
16
+ {body}
17
+ </p>
18
+ </a>
19
+ </li>
20
+ );
21
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "<projectName>",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "npx zuby dev",
8
+ "build": "npx zuby build",
9
+ "preview": "npx zuby preview"
10
+ },
11
+ "dependencies": {
12
+ "zuby": "^<zubyVersion>"
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ export default function About() {
2
+ return (
3
+ <main>
4
+ <h1>Hello from <span className="text-gradient">/about</span> page</h1>
5
+ </main>
6
+ );
7
+ }
@@ -0,0 +1,49 @@
1
+ html {
2
+ font-family: system-ui, sans-serif;
3
+ background: #f5f0f0;
4
+ background-size: 224px;
5
+ }
6
+ code {
7
+ font-family: 'Segoe UI', Verdana, sans-serif;
8
+ }
9
+ main {
10
+ margin: auto;
11
+ padding: 1rem;
12
+ width: 800px;
13
+ max-width: calc(100% - 2rem);
14
+ color: #202020;
15
+ font-size: 20px;
16
+ line-height: 1.6;
17
+ }
18
+ h1 {
19
+ font-size: 4rem;
20
+ font-weight: 700;
21
+ line-height: 1;
22
+ text-align: center;
23
+ margin-bottom: 1em;
24
+ }
25
+ .text-gradient {
26
+ -webkit-background-clip: text;
27
+ -webkit-text-fill-color: transparent;
28
+ background-color: #ef5734;
29
+ background-image: linear-gradient(315deg, #ef5734 0%, #ffcc2f 74%);
30
+ background-position: 0;
31
+ }
32
+ .instructions {
33
+ margin-bottom: 2rem;
34
+ padding: 1.5rem;
35
+ border-radius: 8px;
36
+ text-align: center;
37
+ }
38
+ .instructions code {
39
+ font-size: 0.8em;
40
+ font-weight: bold;
41
+ border-radius: 4px;
42
+ padding: 0.3em 0.4em;
43
+ }
44
+ .link-card-grid {
45
+ display: grid;
46
+ grid-template-columns: repeat(auto-fit, minmax(24ch, 1fr));
47
+ gap: 2rem;
48
+ padding: 0;
49
+ }
@@ -0,0 +1,9 @@
1
+ import './app.css'
2
+
3
+ export default function App({
4
+ children
5
+ }: {
6
+ children: any
7
+ }) {
8
+ return children;
9
+ }
@@ -0,0 +1,24 @@
1
+ import Card from '../components/Card';
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main>
6
+ <h1>Hello from <span className="text-gradient">Zuby.js</span> site</h1>
7
+ <p className="instructions">
8
+ To get started, open the directory <code>./pages</code> in your project and edit <code>index</code> file.
9
+ </p>
10
+ <ul role="list" className="link-card-grid">
11
+ <Card
12
+ href="https://zuby.futrou.com/docs"
13
+ title="Documentation"
14
+ body="Find config API, tips and tricks, and more in the Zuby.js docs."
15
+ />
16
+ <Card
17
+ href="https://zuby.futrou.com/examples"
18
+ title="Examples"
19
+ body="See what's possible to build with Zuby.js by browsing examples."
20
+ />
21
+ </ul>
22
+ </main>
23
+ );
24
+ }
@@ -0,0 +1,7 @@
1
+ export default function Product() {
2
+ return (
3
+ <main>
4
+ <h1>Hello from the dynamic product page</h1>
5
+ </main>
6
+ );
7
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "skipLibCheck": true,
8
+ "types": ["vite/client", "<jsxProvider>"],
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+ "jsxImportSource": "<jsxProvider>",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+
25
+ "composite": true,
26
+ "allowSyntheticDefaultImports": true
27
+ },
28
+ "include": [
29
+ "./**/*",
30
+ "zuby.config.ts"
31
+ ]
32
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'zuby';
2
+
3
+ export default defineConfig({
4
+ outDir: '.zuby',
5
+ jsx: '<jsxProvider>',
6
+ })
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "zuby",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "Zuby.js is Preact Framework for building SPA apps using Vite",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "scripts": {
8
8
  "release": "npm publish --access public ./dist/ ",
9
9
  "bump-version": "npm version patch",
10
- "build": "rm -rf dist/ && tsc && cp -rf package.json README.md dist/ && npm run bundle-server && npm run bundle-express",
10
+ "build": "rm -rf dist/ && tsc && cp -rf package.json README.md src/examples dist/ && npm run bundle-server && npm run bundle-express",
11
11
  "bundle-server": "esbuild src/server/index.ts --bundle --platform=node --format=esm --outfile=dist/server/index.js",
12
12
  "bundle-express": "esbuild src/server/expressApp.ts --bundle --platform=node --format=cjs --minify --outfile=dist/server/expressApp.cjs",
13
13
  "watch-build": "npm run build && tsc --watch",
@@ -16,33 +16,33 @@
16
16
  "test": "echo \"Error: no test specified\" && exit 1"
17
17
  },
18
18
  "dependencies": {
19
+ "@preact/preset-vite": "^2.6.0",
19
20
  "@vercel/nft": "^0.24.3",
20
21
  "bundle-require": "^4.0.2",
21
22
  "chalk": "^5.3.0",
22
23
  "commander": "^11.0.0",
23
24
  "devalue": "^4.3.2",
24
25
  "esbuild": "^0.19.5",
26
+ "glob": "^10.3.10",
25
27
  "inquirer": "^9.2.11",
26
28
  "magic-string": "^0.30.5",
27
- "preact-render-to-string": "^6.2.2",
28
29
  "rollup": "^4.0.2",
29
30
  "vite": "^4.4.11",
30
- "wouter-preact": "^2.12.0",
31
- "glob": "^10.3.10"
31
+ "wouter-preact": "^2.12.0"
32
32
  },
33
33
  "devDependencies": {
34
- "express": "^4.18.2",
35
- "@preact/preset-vite": "^2.5.0",
36
34
  "@types/express": "^4.17.20",
37
35
  "@types/inquirer": "^9.0.6",
38
36
  "@types/node": "^20.8.3",
39
37
  "@types/react": "^18.2.28",
40
38
  "@types/react-dom": "^18.2.13",
39
+ "express": "^4.18.2",
41
40
  "prettier": "^3.0.3",
42
41
  "typescript": "^5.2.2"
43
42
  },
44
43
  "peerDependencies": {
45
44
  "preact": "^10.0.0 || ^11.0.0",
45
+ "preact-render-to-string": "^6.0.0 || ^7.0.0",
46
46
  "react": "^18.0.0 || ^19.0.0",
47
47
  "react-dom": "^18.0.0 || ^19.0.0"
48
48
  },
@@ -50,6 +50,9 @@
50
50
  "preact": {
51
51
  "optional": true
52
52
  },
53
+ "preact-render-to-string": {
54
+ "optional": true
55
+ },
53
56
  "react": {
54
57
  "optional": true
55
58
  },
@@ -28,7 +28,10 @@ export default function chunkNamingPlugin() {
28
28
  }
29
29
  return `${folder}[name]-[hash].js`;
30
30
  };
31
- const transformAssetFileName = (chunkInfo) => {
31
+ const transformAssetFileName = (_chunkInfo) => {
32
+ if (config?.mode === MODES.production) {
33
+ return "chunks/chunk-[hash][extname]";
34
+ }
32
35
  return "chunks/[name]-[hash][extname]";
33
36
  };
34
37
  config.build.rollupOptions.output.chunkFileNames = transformJsFileName;
@@ -1,4 +1,6 @@
1
1
  import { getZubyConfig } from '../../config.js';
2
+ import { relative } from 'path';
3
+ import { normalizePath } from '../../utils/pathUtils.js';
2
4
  let viteConfig;
3
5
  export default function index() {
4
6
  return {
@@ -39,6 +41,7 @@ export async function generateTemplatesCode() {
39
41
  }
40
42
  export async function generateTemplateCode(template) {
41
43
  return `{
44
+ filename: "${normalizePath(relative(viteConfig?.root || '', template.filename))}",
42
45
  path: "${template.path}",
43
46
  pathRegex: ${template.pathRegex},
44
47
  pathParams: ${JSON.stringify(template.pathParams)},
@@ -1,5 +1,5 @@
1
1
  import { findMatchingTemplate, getStaticPages } from '../../templates/index.js';
2
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { basename, dirname, join, resolve } from 'path';
4
4
  import { getZubyConfig } from '../../config.js';
5
5
  import { getContext } from '../../context/index.js';
@@ -36,11 +36,21 @@ export default function prerenderPlugin() {
36
36
  const entry = entryModule.default || entryModule;
37
37
  const entryClientJs = (await glob(`${outDir}/client/chunks/entry-*.js`)).pop();
38
38
  const entryClientCss = (await glob(`${outDir}/client/chunks/entry-*.css`)).pop();
39
+ const ssrManifest = JSON.parse(readFileSync(resolve(outDir, 'server', 'ssr-manifest.json'), 'utf-8'));
39
40
  for (const path of paths) {
40
41
  const zubyContext = getContext().update({
41
42
  currentPath: path,
42
43
  title: undefined
43
44
  });
45
+ // Page
46
+ const { pages = [] } = zubyContext.templates || {};
47
+ const { template: page } = findMatchingTemplate(pages, path);
48
+ // App
49
+ const { apps = [] } = zubyContext.templates || {};
50
+ const { template: app } = findMatchingTemplate(apps, path);
51
+ // Nothing to pre-render
52
+ if (!page || !app)
53
+ continue;
44
54
  // Load layout template component
45
55
  const { layouts = [] } = zubyContext.templates || {};
46
56
  const { template: layout } = findMatchingTemplate(layouts, path);
@@ -63,17 +73,30 @@ export default function prerenderPlugin() {
63
73
  children: entry()
64
74
  }),
65
75
  })) || '';
66
- // Insert document type
67
- html = "<!DOCTYPE html>" + html;
68
- // Insert client SPA assets
69
- // Insert main entry js
70
- if (entryClientJs) {
71
- html = html.replace(/(<\/head>)/, `<script type="module" src="/chunks/${basename(entryClientJs)}"></script>$1`);
72
- }
73
- // Insert main entry css
76
+ const staticImports = [
77
+ ...(ssrManifest[app.filename] || []),
78
+ ...(ssrManifest[page.filename] || []),
79
+ ];
80
+ const cssImports = staticImports.filter((imp) => imp.endsWith('.css'));
81
+ const jsImports = [];
82
+ // Entry css
74
83
  if (entryClientCss) {
75
- html = html.replace(/(<\/head>)/, `<link rel='stylesheet' href="/chunks/${basename(entryClientCss)}"/>$1`);
84
+ cssImports.unshift(`/chunks/${basename(entryClientCss)}`);
85
+ }
86
+ // Entry js
87
+ if (entryClientJs) {
88
+ jsImports.unshift(`/chunks/${basename(entryClientJs)}`);
76
89
  }
90
+ // Insert document type
91
+ html = "<!DOCTYPE html>" + html;
92
+ // Insert client CSS imports
93
+ cssImports.forEach((imp) => {
94
+ html = html.replace(/(<\/head>)/, `<link rel='stylesheet' href="${imp}"/>$1`);
95
+ });
96
+ // Insert client JS imports
97
+ jsImports.forEach((imp) => {
98
+ html = html.replace(/(<\/head>)/, `<script type="module" src="${imp}" defer></script>$1`);
99
+ });
77
100
  const titleHtml = `<title>${zubyContext.title || ''}<\/title>`;
78
101
  // Insert title
79
102
  if (html.match(/<title>(.+)<\/title>/)) {
@@ -0,0 +1,20 @@
1
+ import { JsxProvider } from './types.js';
2
+ export type ProviderName = keyof typeof Providers;
3
+ export declare const Providers: {
4
+ preact: () => Promise<any>;
5
+ };
6
+ export declare const ProviderDependencies: {
7
+ [key: string]: {
8
+ dependencies: {
9
+ [key: string]: string;
10
+ };
11
+ devDependencies: {
12
+ [key: string]: string;
13
+ };
14
+ };
15
+ };
16
+ /**
17
+ * Returns the instance of the JSX provider.
18
+ */
19
+ export declare const getProvider: (jsx: string) => Promise<JsxProvider>;
20
+ export declare const checkProviderDependencies: (jsx: string) => Promise<void>;
@@ -0,0 +1,75 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, join } from 'path';
3
+ import { normalizePath } from '../utils/pathUtils.js';
4
+ import { readFile } from 'fs/promises';
5
+ import chalk from 'chalk';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ export const Providers = {
9
+ preact: async () => (await import(`file:///${normalizePath(join(__dirname, 'preact', 'index.js'))}`)).default,
10
+ };
11
+ export const ProviderDependencies = {
12
+ preact: {
13
+ dependencies: {
14
+ 'preact': '^10.18.1',
15
+ },
16
+ devDependencies: {
17
+ 'preact-render-to-string': '^6.2.2',
18
+ }
19
+ },
20
+ react: {
21
+ dependencies: {
22
+ 'react': '^18.2.0',
23
+ 'react-dom': '^18.2.0',
24
+ },
25
+ devDependencies: {}
26
+ }
27
+ };
28
+ /**
29
+ * Returns the instance of the JSX provider.
30
+ */
31
+ export const getProvider = async (jsx) => {
32
+ const providerImport = Providers[jsx];
33
+ if (!providerImport)
34
+ throw new Error(`JSX provider '${jsx}' is not supported.`);
35
+ return providerImport();
36
+ };
37
+ export const checkProviderDependencies = async (jsx) => {
38
+ const packageJsonPath = normalizePath(join(process.cwd(), 'package.json'));
39
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
40
+ const providerDependencies = ProviderDependencies[jsx]?.dependencies || {};
41
+ const providerDevDependencies = ProviderDependencies[jsx]?.devDependencies || {};
42
+ const dependencies = packageJson?.dependencies || {};
43
+ const devDependencies = packageJson?.devDependencies || {};
44
+ const getMissingDependencies = (installed, required) => {
45
+ return Object.entries(required)
46
+ .reduce((missing, [key, value]) => {
47
+ if (!installed[key])
48
+ return {
49
+ ...missing,
50
+ [key]: value
51
+ };
52
+ return missing;
53
+ }, {});
54
+ };
55
+ const missingDependencies = getMissingDependencies(dependencies, providerDependencies);
56
+ const missingDevDependencies = getMissingDependencies({
57
+ ...dependencies,
58
+ ...devDependencies
59
+ }, providerDevDependencies);
60
+ if (Object.keys(missingDependencies).length === 0 && Object.keys(missingDevDependencies).length === 0) {
61
+ return;
62
+ }
63
+ console.error(`${chalk.bgRed.bold.whiteBright(` ERROR `)} ${chalk.gray(`missing dependencies...`)}`);
64
+ console.error(`Zuby JSX provider '${jsx}' requires following dependencies:`);
65
+ if (Object.keys(missingDependencies).length > 0) {
66
+ console.error(`${chalk.bold(`dependencies`)}:`);
67
+ console.error(JSON.stringify(missingDependencies, null, 2));
68
+ }
69
+ if (Object.keys(missingDevDependencies).length > 0) {
70
+ console.error(`${chalk.bold(`devDependencies`)}:`);
71
+ console.error(JSON.stringify(missingDevDependencies, null, 2));
72
+ }
73
+ console.error(`Please add them to you package.json and install:`);
74
+ process.exit(1);
75
+ };
@@ -1,3 +1,3 @@
1
- import { JsxProvider } from '../../types.js';
1
+ import { JsxProvider } from '../types.js';
2
2
  declare const _default: JsxProvider;
3
3
  export default _default;
@@ -2,14 +2,16 @@ import render from './render.js';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, resolve } from 'path';
4
4
  import { normalizePath } from '../../utils/pathUtils.js';
5
+ import { preact } from '@preact/preset-vite';
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
7
8
  export default {
8
- name: 'preactProvider',
9
+ name: 'preact',
9
10
  appTemplateFile: normalizePath(resolve(__dirname, 'templates', 'app.js')),
10
11
  entryTemplateFile: normalizePath(resolve(__dirname, 'templates', 'entry.js')),
11
12
  layoutTemplateFile: normalizePath(resolve(__dirname, 'templates', 'layout.js')),
12
13
  innerLayoutTemplateFile: normalizePath(resolve(__dirname, 'templates', 'innerLayout.js')),
13
14
  errorTemplateFile: normalizePath(resolve(__dirname, 'templates', 'error.js')),
14
15
  render,
16
+ getPlugins: () => preact()
15
17
  };
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
1
+ import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { Router as WouterRouter, Route as WouterRoute, Switch as WouterSwitch, } from 'wouter-preact';
3
3
  import { Suspense, lazy } from 'preact/compat';
4
4
  import { getContext } from '../../context/index.js';
@@ -31,7 +31,7 @@ export default function Router() {
31
31
  return pathRegex.test(zubyContext.currentPath ?? '');
32
32
  });
33
33
  const page = (_jsx(WouterSwitch, { children: pages.map((page) => (_jsx(WouterRoute, { path: page.path, component: page.component }))) }));
34
- return (_jsxs(_Fragment, { children: [_jsx("h1", { children: "Router file" }), _jsx(WouterRouter, { ssrPath: zubyContext.currentPath, children: _jsx(Suspense, { fallback: _jsx("div", { children: "Loading..." }), children: createElement(app?.component, {
35
- children: page,
36
- }) }) })] }));
34
+ return (_jsx(WouterRouter, { ssrPath: zubyContext.currentPath, children: _jsx(Suspense, { fallback: _jsx("div", { children: "Loading..." }), children: createElement(app?.component, {
35
+ children: page,
36
+ }) }) }));
37
37
  }
@@ -0,0 +1,17 @@
1
+ import { Plugin } from "vite";
2
+ export interface JsxProvider {
3
+ name: string;
4
+ render(node: any): Promise<string>;
5
+ getPlugins(): Plugin[] | Plugin[][];
6
+ appTemplateFile: string;
7
+ entryTemplateFile: string;
8
+ layoutTemplateFile: string;
9
+ innerLayoutTemplateFile: string;
10
+ errorTemplateFile: string;
11
+ dependencies?: {
12
+ [key: string]: string;
13
+ };
14
+ devDependencies?: {
15
+ [key: string]: string;
16
+ };
17
+ }
@@ -0,0 +1 @@
1
+ export {};
package/server/index.js CHANGED
@@ -644,8 +644,8 @@ var envPort = process.env["PORT"] ? Number(process.env["PORT"]) : void 0;
644
644
  var envHost = process.env["HOST"];
645
645
  var port = envPort || 3e3;
646
646
  var host = envHost || "localhost";
647
- expressApp.get("/api", function(req, res) {
648
- res.send("Hello World!");
647
+ expressApp.use(function(req, res) {
648
+ res.sendFile(resolve2(__dirname, "client", "index.html"));
649
649
  });
650
650
  expressApp.listen(port, host, () => {
651
651
  logger?.info(` ${getTitle("")}\r
@@ -16,6 +16,7 @@ export interface LazyTemplate {
16
16
  path: string;
17
17
  pathRegex: RegExp;
18
18
  pathParams: string[];
19
+ filename: string;
19
20
  templateType: TemplateType;
20
21
  component: any;
21
22
  }
package/types.d.ts CHANGED
@@ -2,6 +2,8 @@ import { UserConfig as ViteUserConfig } from 'vite';
2
2
  import type { Plugin as VitePlugin } from "vite";
3
3
  import { Template } from './templates/types.js';
4
4
  import { ZubyLogger } from './logger/types.js';
5
+ import { ProviderName } from './providers/index.js';
6
+ import { JsxProvider } from './providers/types.js';
5
7
  export interface ZubyConfig {
6
8
  /**
7
9
  * The relative path to directory were your zuby project is located in.
@@ -12,7 +14,7 @@ export interface ZubyConfig {
12
14
  * The name of the framework which will be used to render the components.
13
15
  * @default 'preact'
14
16
  */
15
- jsx?: 'preact' | 'react';
17
+ jsx?: ProviderName;
16
18
  /**
17
19
  * The JSX provider. This option is set automatically based on the `jsx` option
18
20
  * in case no provider is specified.
@@ -99,11 +101,6 @@ export interface ZubyConfig {
99
101
  * @default ZubyLogger
100
102
  */
101
103
  customLogger?: ZubyLogger;
102
- /**
103
- * The id which is unique for each build.
104
- * @private
105
- */
106
- buildId?: string;
107
104
  /**
108
105
  * Server options for both the dev and production server.
109
106
  */
@@ -123,15 +120,6 @@ export interface ZubyConfig {
123
120
  port?: number;
124
121
  };
125
122
  }
126
- export interface JsxProvider {
127
- name: string;
128
- render(node: any): Promise<string>;
129
- appTemplateFile: string;
130
- entryTemplateFile: string;
131
- layoutTemplateFile: string;
132
- innerLayoutTemplateFile: string;
133
- errorTemplateFile: string;
134
- }
135
123
  export interface BaseCommandOptions {
136
124
  /**
137
125
  * The path to the zuby config file.