vite-wp 0.1.0

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/dist/astro.d.ts +5 -0
  4. package/dist/astro.js +27 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +40 -0
  7. package/dist/commands/dev.d.ts +1 -0
  8. package/dist/commands/dev.js +62 -0
  9. package/dist/commands/doctor.d.ts +6 -0
  10. package/dist/commands/doctor.js +139 -0
  11. package/dist/commands/init.d.ts +1 -0
  12. package/dist/commands/init.js +55 -0
  13. package/dist/commands/smoke.d.ts +1 -0
  14. package/dist/commands/smoke.js +138 -0
  15. package/dist/commands/types.d.ts +1 -0
  16. package/dist/commands/types.js +67 -0
  17. package/dist/config.d.ts +71 -0
  18. package/dist/config.js +74 -0
  19. package/dist/content.d.ts +44 -0
  20. package/dist/content.js +190 -0
  21. package/dist/dev-toolbar/vitewp-toolbar.d.ts +23 -0
  22. package/dist/dev-toolbar/vitewp-toolbar.js +144 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.js +2 -0
  25. package/dist/live.config.d.ts +64 -0
  26. package/dist/live.config.js +21 -0
  27. package/dist/runtime/astro.d.ts +4 -0
  28. package/dist/runtime/astro.js +69 -0
  29. package/dist/runtime/composer.d.ts +2 -0
  30. package/dist/runtime/composer.js +36 -0
  31. package/dist/runtime/php.d.ts +3 -0
  32. package/dist/runtime/php.js +79 -0
  33. package/dist/runtime/ports.d.ts +3 -0
  34. package/dist/runtime/ports.js +38 -0
  35. package/dist/runtime/process.d.ts +12 -0
  36. package/dist/runtime/process.js +51 -0
  37. package/dist/runtime/proxy.d.ts +6 -0
  38. package/dist/runtime/proxy.js +204 -0
  39. package/dist/runtime/wp-config.d.ts +3 -0
  40. package/dist/runtime/wp-config.js +82 -0
  41. package/dist/wordpress/client.d.ts +98 -0
  42. package/dist/wordpress/client.js +133 -0
  43. package/dist/wordpress/generated-types.d.ts +33 -0
  44. package/dist/wordpress/generated-types.js +2 -0
  45. package/dist/wordpress/menus.d.ts +24 -0
  46. package/dist/wordpress/menus.js +20 -0
  47. package/dist/wordpress/schemas.d.ts +58 -0
  48. package/dist/wordpress/schemas.js +39 -0
  49. package/dist/wordpress/templates.d.ts +19 -0
  50. package/dist/wordpress/templates.js +88 -0
  51. package/package.json +78 -0
  52. package/starter/.env.example +15 -0
  53. package/starter/astro.config.mjs +16 -0
  54. package/starter/composer.json +37 -0
  55. package/starter/src/env.d.ts +1 -0
  56. package/starter/src/live.config.ts +22 -0
  57. package/starter/src/pages/[...slug].astro +101 -0
  58. package/starter/src/templates/404.astro +11 -0
  59. package/starter/src/templates/pages/.gitkeep +0 -0
  60. package/starter/src/templates/pages/default.astro +12 -0
  61. package/starter/src/templates/pages/front-page.astro +13 -0
  62. package/starter/src/templates/partials/Header.astro +23 -0
  63. package/starter/src/templates/partials/Pagination.astro +21 -0
  64. package/starter/src/templates/post-types/.gitkeep +0 -0
  65. package/starter/src/templates/posts/.gitkeep +0 -0
  66. package/starter/src/templates/posts/archive.astro +25 -0
  67. package/starter/src/templates/posts/single.astro +12 -0
  68. package/starter/src/templates/search.astro +30 -0
  69. package/starter/src/templates/taxonomies/.gitkeep +0 -0
  70. package/starter/src/templates/taxonomies/taxonomy-[taxonomy].astro +28 -0
  71. package/starter/src/wordpress/client.ts +263 -0
  72. package/starter/src/wordpress/generated-types.ts +38 -0
  73. package/starter/src/wordpress/menus.ts +51 -0
  74. package/starter/src/wordpress/schemas.ts +44 -0
  75. package/starter/src/wordpress/templates.ts +113 -0
  76. package/starter/tsconfig.json +4 -0
  77. package/starter/vitewp.config.ts +29 -0
  78. package/starter/wordpress/content/mu-plugins/.gitkeep +0 -0
  79. package/starter/wordpress/content/mu-plugins/vitewp-bridge.php +560 -0
  80. package/starter/wordpress/content/themes/vitewp/functions.php +9 -0
  81. package/starter/wordpress/content/themes/vitewp/index.php +22 -0
  82. package/starter/wordpress/content/themes/vitewp/style.css +10 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andreas Ekström (https://github.com/didair)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # ViteWP
2
+
3
+ ViteWP is an Astro-first WordPress development framework.
4
+
5
+ A ViteWP project **is** the local WordPress site: Composer installs WordPress, PHP runs WordPress, Astro renders the frontend, and one local URL serves admin, REST, media, previews, and Astro pages.
6
+
7
+ ```txt
8
+ http://localhost:3000
9
+ ```
10
+
11
+ PHP and Astro still run internally, but you normally only use the unified ViteWP URL.
12
+
13
+ ## Requirements
14
+
15
+ - Node.js 20+
16
+ - PHP 8.2+
17
+ - Composer
18
+ - MySQL or MariaDB
19
+
20
+ ViteWP is bring-your-own database. It does not start MySQL for you.
21
+
22
+ ## Get started
23
+
24
+ ```bash
25
+ mkdir my-site
26
+ cd my-site
27
+ npm init -y
28
+ npm install vitewp astro typescript @astrojs/check
29
+ npx vitewp init
30
+ cp .env.example .env
31
+ npm run dev
32
+ ```
33
+
34
+ Edit `.env` with your database credentials, then open:
35
+
36
+ ```txt
37
+ http://localhost:3000
38
+ ```
39
+
40
+ If the database is empty, go to `/wp-admin/` and complete the WordPress installer.
41
+
42
+ ## Commands
43
+
44
+ ```bash
45
+ npm run dev # Start the local ViteWP runtime
46
+ npm run doctor # Check PHP, Composer, WordPress, config, and environment
47
+ npm run types # Generate WordPress-derived TypeScript types
48
+ npm run check # Run Astro type checking
49
+ ```
50
+
51
+ For internal runtime diagnostics:
52
+
53
+ ```bash
54
+ npm run dev -- --verbose
55
+ ```
56
+
57
+ ## Folder structure
58
+
59
+ ```txt
60
+ my-site/
61
+ astro.config.mjs
62
+ vitewp.config.ts
63
+ composer.json
64
+ .env
65
+
66
+ src/
67
+ live.config.ts
68
+ pages/
69
+ [...slug].astro
70
+ templates/
71
+ pages/
72
+ front-page.astro
73
+ default.astro
74
+ posts/
75
+ single.astro
76
+ archive.astro
77
+ taxonomies/
78
+ taxonomy-[taxonomy].astro
79
+ search.astro
80
+ 404.astro
81
+ wordpress/
82
+ client.ts
83
+ templates.ts
84
+ menus.ts
85
+ generated-types.ts
86
+
87
+ wordpress/
88
+ public/ # Composer-installed WordPress core
89
+ content/
90
+ mu-plugins/
91
+ themes/
92
+ plugins/
93
+ uploads/
94
+ ```
95
+
96
+ ## Templates
97
+
98
+ The catch-all Astro route asks WordPress what the current URL is, then renders the first matching template from `src/templates`.
99
+
100
+ Examples:
101
+
102
+ ```txt
103
+ src/templates/pages/front-page.astro
104
+ src/templates/pages/page-about.astro
105
+ src/templates/pages/default.astro
106
+ src/templates/posts/single.astro
107
+ src/templates/posts/archive.astro
108
+ src/templates/taxonomies/category.astro
109
+ src/templates/search.astro
110
+ src/templates/404.astro
111
+ ```
112
+
113
+ ## WordPress data
114
+
115
+ ViteWP uses Astro Live Collections for WordPress data:
116
+
117
+ ```ts
118
+ import { getLiveCollection, getLiveEntry } from 'astro:content';
119
+
120
+ const route = await getLiveEntry('routes', { path: '/' });
121
+ const posts = await getLiveCollection('posts', { page: 1, perPage: 10 });
122
+ const menu = await getLiveEntry('menus', { location: 'primary' });
123
+ ```
124
+
125
+ After `npm run dev` or `npm run check`, collection names and filters should have TypeScript completion.
126
+
127
+ ## Notes
128
+
129
+ - WordPress core is installed into `wordpress/public` by Composer.
130
+ - Do not commit `wordpress/public`, `.env`, `.vitewp`, or uploads.
131
+ - The tiny placeholder theme exists only to keep WordPress admin happy.
132
+ - Docker is optional, not required.
@@ -0,0 +1,5 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ export interface ViteWpAstroOptions {
3
+ configFile?: string;
4
+ }
5
+ export default function vitewp(_options?: ViteWpAstroOptions): AstroIntegration;
package/dist/astro.js ADDED
@@ -0,0 +1,27 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ export default function vitewp(_options = {}) {
4
+ return {
5
+ name: 'vitewp',
6
+ hooks: {
7
+ 'astro:config:setup': ({ command, addDevToolbarApp, logger }) => {
8
+ if (command === 'dev') {
9
+ addDevToolbarApp({
10
+ id: 'vitewp',
11
+ name: 'ViteWP',
12
+ icon: 'sitemap',
13
+ entrypoint: getDevToolbarEntrypoint(),
14
+ });
15
+ }
16
+ logger.info('ViteWP integration loaded.');
17
+ },
18
+ },
19
+ };
20
+ }
21
+ function getDevToolbarEntrypoint() {
22
+ const sourceEntrypoint = new URL('./dev-toolbar/vitewp-toolbar.ts', import.meta.url);
23
+ if (existsSync(fileURLToPath(sourceEntrypoint))) {
24
+ return sourceEntrypoint;
25
+ }
26
+ return new URL('./dev-toolbar/vitewp-toolbar.js', import.meta.url);
27
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { runDev } from './commands/dev.js';
3
+ import { runDoctor } from './commands/doctor.js';
4
+ import { runSmoke } from './commands/smoke.js';
5
+ import { runTypes } from './commands/types.js';
6
+ import { runInit } from './commands/init.js';
7
+ const command = process.argv[2] ?? 'help';
8
+ switch (command) {
9
+ case 'dev':
10
+ await runDev();
11
+ break;
12
+ case 'doctor':
13
+ await runDoctor();
14
+ break;
15
+ case 'init':
16
+ runInit();
17
+ break;
18
+ case 'types':
19
+ await runTypes();
20
+ break;
21
+ case 'smoke':
22
+ await runSmoke();
23
+ break;
24
+ case 'help':
25
+ case '--help':
26
+ case '-h':
27
+ printHelp();
28
+ break;
29
+ default:
30
+ console.error(`Unknown command: ${command}`);
31
+ printHelp();
32
+ process.exitCode = 1;
33
+ }
34
+ function printHelp() {
35
+ console.log(`vitewp\n\nUsage:\n vitewp init Copy starter project files into the current directory
36
+ vitewp dev Start the local WordPress + Astro development runtime
37
+ vitewp doctor Check the current project setup
38
+ vitewp types Generate TypeScript types from WordPress metadata
39
+ vitewp smoke Verify the running ViteWP dev runtime\n`);
40
+ }
@@ -0,0 +1 @@
1
+ export declare function runDev(): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import { loadViteWpConfig } from '../config.js';
2
+ import { ensureComposerInstall } from '../runtime/composer.js';
3
+ import { startAstroServer, stopAstroServer } from '../runtime/astro.js';
4
+ import { startPhpServer } from '../runtime/php.js';
5
+ import { waitForExit } from '../runtime/process.js';
6
+ import { assertPortAvailable, resolveInternalPort } from '../runtime/ports.js';
7
+ import { startUnifiedProxy } from '../runtime/proxy.js';
8
+ import { phpServerUrl, writeWordPressConfig } from '../runtime/wp-config.js';
9
+ import { runDoctorChecks } from './doctor.js';
10
+ export async function runDev() {
11
+ const config = await loadViteWpConfig();
12
+ const verbose = isVerbose();
13
+ if (verbose) {
14
+ process.env.VITEWP_VERBOSE = '1';
15
+ }
16
+ console.log('ViteWP dev runtime');
17
+ console.log(`- local site: ${config.wordpress.url}`);
18
+ console.log(`- WordPress docroot: ${config.wordpress.docroot}`);
19
+ console.log(`- WordPress content: ${config.wordpress.contentDir}`);
20
+ console.log(`- templates: ${config.templates.directory}`);
21
+ console.log(`- database: ${config.database.driver}://${config.database.user}@${config.database.host}:${config.database.port}/${config.database.name}`);
22
+ console.log('');
23
+ const result = await runDoctorChecks(config);
24
+ if (result.errors > 0) {
25
+ console.log('');
26
+ console.log('Fix the errors above before starting the dev runtime.');
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ try {
31
+ await ensureComposerInstall(config);
32
+ writeWordPressConfig(config);
33
+ await stopAstroServer(config);
34
+ config.dev.phpPort = await resolveInternalPort(config.dev.phpHost, config.dev.phpPort);
35
+ config.dev.astroPort = await resolveInternalPort(config.dev.astroHost, config.dev.astroPort);
36
+ const publicUrl = new URL(config.wordpress.url);
37
+ await assertPortAvailable(publicUrl.hostname, Number(publicUrl.port || 3000), 'ViteWP proxy');
38
+ }
39
+ catch (error) {
40
+ console.error(error instanceof Error ? error.message : error);
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ process.env.VITEWP_PUBLIC_URL = config.wordpress.url;
45
+ process.env.VITEWP_PHP_URL = phpServerUrl(config);
46
+ console.log('');
47
+ if (verbose) {
48
+ console.log(`✓ WordPress/PHP internal server: ${phpServerUrl(config)}`);
49
+ console.log(`✓ Astro internal server: http://${config.dev.astroHost}:${config.dev.astroPort}`);
50
+ }
51
+ const php = startPhpServer(config);
52
+ const astro = await startAstroServer(config);
53
+ const proxy = await startUnifiedProxy(config);
54
+ console.log(`✓ ViteWP ready at ${proxy.url}`);
55
+ console.log('Press Ctrl+C to stop.');
56
+ console.log('');
57
+ await waitForExit([php, astro]);
58
+ await proxy.stop();
59
+ }
60
+ function isVerbose() {
61
+ return process.argv.includes('--verbose') || process.env.VITEWP_VERBOSE === '1';
62
+ }
@@ -0,0 +1,6 @@
1
+ import { type LoadedViteWpConfig } from '../config.js';
2
+ export declare function runDoctor(): Promise<void>;
3
+ export declare function runDoctorChecks(config: LoadedViteWpConfig): Promise<{
4
+ errors: number;
5
+ warnings: number;
6
+ }>;
@@ -0,0 +1,139 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { loadViteWpConfig } from '../config.js';
6
+ const execFileAsync = promisify(execFile);
7
+ export async function runDoctor() {
8
+ const config = await loadViteWpConfig();
9
+ const result = await runDoctorChecks(config);
10
+ process.exitCode = result.errors > 0 ? 1 : 0;
11
+ }
12
+ export async function runDoctorChecks(config) {
13
+ const checks = [];
14
+ checks.push({
15
+ status: config.configFile ? 'pass' : 'warn',
16
+ label: 'ViteWP config',
17
+ detail: config.configFile ?? 'No vitewp.config.* file found; using defaults.',
18
+ });
19
+ checks.push(fileCheck(config.root, 'astro.config.mjs', 'Astro config'));
20
+ checks.push(fileCheck(config.root, 'composer.json', 'Composer manifest'));
21
+ checks.push(composerWordPressCheck(config.root, config.composer.wordpressPackage));
22
+ checks.push(fileCheck(config.root, 'composer.lock', 'Composer lockfile'));
23
+ checks.push(fileCheck(config.root, `${config.wordpress.docroot}/wp-settings.php`, 'Composer-installed WordPress core'));
24
+ checks.push(fileCheck(config.root, `${config.wordpress.docroot}/wp-config.php`, 'Generated wp-config.php'));
25
+ checks.push(fileCheck(config.root, `${config.wordpress.contentDir}/themes/vitewp/style.css`, 'ViteWP placeholder theme'));
26
+ checks.push(fileCheck(config.root, config.templates.directory, 'Template directory'));
27
+ checks.push(databaseConfigCheck(config));
28
+ checks.push(await commandCheck('php', ['-v'], 'PHP runtime'));
29
+ checks.push(await commandCheck('composer', ['--version'], 'Composer'));
30
+ checks.push(gitignoreCheck(config.root, config.wordpress.docroot));
31
+ printChecks(checks);
32
+ const errors = checks.filter((check) => check.status === 'fail').length;
33
+ const warnings = checks.filter((check) => check.status === 'warn').length;
34
+ console.log('');
35
+ console.log(`Doctor finished with ${errors} error(s) and ${warnings} warning(s).`);
36
+ return { errors, warnings };
37
+ }
38
+ function fileCheck(root, relativePath, label) {
39
+ const path = join(root, relativePath);
40
+ return existsSync(path)
41
+ ? { status: 'pass', label, detail: relativePath }
42
+ : { status: 'warn', label, detail: `${relativePath} does not exist yet.` };
43
+ }
44
+ async function commandCheck(command, args, label) {
45
+ try {
46
+ const { stdout, stderr } = await execFileAsync(command, args);
47
+ const firstLine = `${stdout}${stderr}`.split('\n').find(Boolean);
48
+ return { status: 'pass', label, detail: firstLine };
49
+ }
50
+ catch {
51
+ return { status: 'fail', label, detail: `${command} was not found on PATH.` };
52
+ }
53
+ }
54
+ function databaseConfigCheck(config) {
55
+ const database = config.database;
56
+ const missing = [
57
+ ['host', database.host],
58
+ ['name', database.name],
59
+ ['user', database.user],
60
+ ['tablePrefix', database.tablePrefix],
61
+ ].filter(([, value]) => !value);
62
+ if (missing.length > 0) {
63
+ return {
64
+ status: 'fail',
65
+ label: 'Database config',
66
+ detail: `Missing ${missing.map(([key]) => key).join(', ')}.`,
67
+ };
68
+ }
69
+ if (database.driver !== 'mysql' && database.driver !== 'mariadb') {
70
+ return {
71
+ status: 'fail',
72
+ label: 'Database config',
73
+ detail: `Unsupported driver: ${database.driver}.`,
74
+ };
75
+ }
76
+ return {
77
+ status: 'pass',
78
+ label: 'Database config',
79
+ detail: `${database.driver}://${database.user}@${database.host}:${database.port}/${database.name} (${database.tablePrefix})`,
80
+ };
81
+ }
82
+ function composerWordPressCheck(root, wordpressPackage) {
83
+ const composerPath = join(root, 'composer.json');
84
+ if (!existsSync(composerPath)) {
85
+ return {
86
+ status: 'warn',
87
+ label: 'Composer WordPress package',
88
+ detail: 'composer.json does not exist yet.',
89
+ };
90
+ }
91
+ try {
92
+ const composer = JSON.parse(readFileSync(composerPath, 'utf8'));
93
+ const version = composer.require?.[wordpressPackage];
94
+ if (!version) {
95
+ return {
96
+ status: 'fail',
97
+ label: 'Composer WordPress package',
98
+ detail: `${wordpressPackage} is not declared in composer.json.`,
99
+ };
100
+ }
101
+ const installDir = composer.extra?.['wordpress-install-dir'];
102
+ const installDetail = installDir ? ` -> ${installDir}` : '';
103
+ return {
104
+ status: 'pass',
105
+ label: 'Composer WordPress package',
106
+ detail: `${wordpressPackage}@${version}${installDetail}`,
107
+ };
108
+ }
109
+ catch (error) {
110
+ return {
111
+ status: 'fail',
112
+ label: 'Composer WordPress package',
113
+ detail: error instanceof Error ? error.message : 'Could not parse composer.json.',
114
+ };
115
+ }
116
+ }
117
+ function gitignoreCheck(root, docroot) {
118
+ const gitignorePath = join(root, '.gitignore');
119
+ if (!existsSync(gitignorePath)) {
120
+ return { status: 'warn', label: 'Generated WordPress ignored', detail: '.gitignore is missing.' };
121
+ }
122
+ const gitignore = readFileSync(gitignorePath, 'utf8');
123
+ const normalizedDocroot = docroot.endsWith('/') ? docroot : `${docroot}/`;
124
+ return gitignore.includes(normalizedDocroot)
125
+ ? { status: 'pass', label: 'Generated WordPress ignored', detail: normalizedDocroot }
126
+ : {
127
+ status: 'warn',
128
+ label: 'Generated WordPress ignored',
129
+ detail: `Add ${normalizedDocroot} to .gitignore.`,
130
+ };
131
+ }
132
+ function printChecks(checks) {
133
+ console.log('ViteWP doctor');
134
+ console.log('');
135
+ for (const check of checks) {
136
+ const icon = check.status === 'pass' ? '✓' : check.status === 'warn' ? '!' : '✕';
137
+ console.log(`${icon} ${check.label}${check.detail ? ` — ${check.detail}` : ''}`);
138
+ }
139
+ }
@@ -0,0 +1 @@
1
+ export declare function runInit(): void;
@@ -0,0 +1,55 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, relative, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export function runInit() {
5
+ const target = resolve(process.cwd(), getTargetDirectory());
6
+ const force = process.argv.includes('--force');
7
+ const starter = resolve(dirname(fileURLToPath(import.meta.url)), '../../starter');
8
+ if (!existsSync(starter)) {
9
+ console.error('Could not find the ViteWP starter files in this package.');
10
+ process.exitCode = 1;
11
+ return;
12
+ }
13
+ mkdirSync(target, { recursive: true });
14
+ copyDirectory(starter, target, force);
15
+ updatePackageJson(target);
16
+ console.log(`ViteWP project files initialized in ${relative(process.cwd(), target) || '.'}.`);
17
+ console.log('');
18
+ console.log('Next steps:');
19
+ console.log(' 1. Copy .env.example to .env and set database credentials.');
20
+ console.log(' 2. Run npm install if dependencies are not installed yet.');
21
+ console.log(' 3. Run npm run dev.');
22
+ }
23
+ function getTargetDirectory() {
24
+ const positional = process.argv.slice(3).find((arg) => !arg.startsWith('-'));
25
+ return positional ?? '.';
26
+ }
27
+ function copyDirectory(from, to, force) {
28
+ for (const entry of readdirSync(from)) {
29
+ const source = join(from, entry);
30
+ const destination = join(to, entry);
31
+ const stats = statSync(source);
32
+ if (stats.isDirectory()) {
33
+ mkdirSync(destination, { recursive: true });
34
+ copyDirectory(source, destination, force);
35
+ continue;
36
+ }
37
+ if (existsSync(destination) && !force) {
38
+ continue;
39
+ }
40
+ cpSync(source, destination, { force: true });
41
+ }
42
+ }
43
+ function updatePackageJson(root) {
44
+ const file = join(root, 'package.json');
45
+ const packageJson = existsSync(file)
46
+ ? JSON.parse(readFileSync(file, 'utf8'))
47
+ : { private: true, type: 'module' };
48
+ packageJson.type ??= 'module';
49
+ packageJson.scripts ??= {};
50
+ packageJson.scripts.dev ??= 'vitewp dev';
51
+ packageJson.scripts.doctor ??= 'vitewp doctor';
52
+ packageJson.scripts.types ??= 'vitewp types';
53
+ packageJson.scripts.check ??= 'astro check';
54
+ writeFileSync(file, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
55
+ }
@@ -0,0 +1 @@
1
+ export declare function runSmoke(): Promise<void>;
@@ -0,0 +1,138 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import net from 'node:net';
3
+ import { loadViteWpConfig } from '../config.js';
4
+ export async function runSmoke() {
5
+ const config = await loadViteWpConfig();
6
+ const baseUrl = config.wordpress.url.replace(/\/$/, '');
7
+ const checks = [
8
+ {
9
+ label: 'Unified frontend route',
10
+ run: () => expectHttp(`${baseUrl}/`, [200]),
11
+ },
12
+ {
13
+ label: 'ViteWP dev route metadata',
14
+ run: () => expectBody(`${baseUrl}/`, 'window.__VITEWP_ROUTE_INFO__'),
15
+ },
16
+ {
17
+ label: 'WordPress admin route',
18
+ run: () => expectHttp(`${baseUrl}/wp-admin/`, [200, 302]),
19
+ },
20
+ {
21
+ label: 'Vite client through proxy',
22
+ run: () => expectHttp(`${baseUrl}/@vite/client`, [200], 'text/javascript'),
23
+ },
24
+ {
25
+ label: 'wp-content static asset',
26
+ run: () => expectHttp(`${baseUrl}/wp-content/themes/vitewp/style.css`, [200], 'text/css'),
27
+ },
28
+ {
29
+ label: 'ViteWP route bridge',
30
+ run: () => expectJson(`${baseUrl}/wp-json/vitewp/v1/resolve?path=/`, ['found', 'kind']),
31
+ },
32
+ {
33
+ label: 'ViteWP type metadata',
34
+ run: () => expectJson(`${baseUrl}/wp-json/vitewp/v1/types`, ['postTypes', 'taxonomies']),
35
+ },
36
+ {
37
+ label: 'Vite HMR websocket',
38
+ run: () => expectWebSocket(baseUrl),
39
+ },
40
+ ];
41
+ console.log('ViteWP smoke test');
42
+ console.log('');
43
+ let failures = 0;
44
+ for (const check of checks) {
45
+ try {
46
+ const detail = await check.run();
47
+ console.log(`✓ ${check.label}${detail ? ` — ${detail}` : ''}`);
48
+ }
49
+ catch (error) {
50
+ failures += 1;
51
+ console.log(`✕ ${check.label} — ${error instanceof Error ? error.message : String(error)}`);
52
+ }
53
+ }
54
+ console.log('');
55
+ console.log(`Smoke test finished with ${failures} failure(s).`);
56
+ process.exitCode = failures > 0 ? 1 : 0;
57
+ }
58
+ async function expectHttp(url, statuses, expectedContentType) {
59
+ const response = await fetchWithTimeout(url, { method: 'GET' });
60
+ const contentType = response.headers.get('content-type') ?? '';
61
+ if (!statuses.includes(response.status)) {
62
+ throw new Error(`Expected ${statuses.join('/')} from ${url}, got ${response.status}.`);
63
+ }
64
+ if (expectedContentType && !contentType.includes(expectedContentType)) {
65
+ throw new Error(`Expected content-type ${expectedContentType}, got ${contentType || 'none'}.`);
66
+ }
67
+ await response.body?.cancel();
68
+ return `${response.status}${contentType ? ` ${contentType}` : ''}`;
69
+ }
70
+ async function expectJson(url, keys) {
71
+ const response = await fetchWithTimeout(url);
72
+ if (!response.ok) {
73
+ throw new Error(`Expected 2xx from ${url}, got ${response.status}.`);
74
+ }
75
+ const json = await response.json();
76
+ const missing = keys.filter((key) => !(key in json));
77
+ if (missing.length > 0) {
78
+ throw new Error(`Missing JSON key(s): ${missing.join(', ')}.`);
79
+ }
80
+ return `${response.status}`;
81
+ }
82
+ async function expectBody(url, needle) {
83
+ const response = await fetchWithTimeout(url);
84
+ const body = await response.text();
85
+ if (!response.ok) {
86
+ throw new Error(`Expected 2xx from ${url}, got ${response.status}.`);
87
+ }
88
+ if (!body.includes(needle)) {
89
+ throw new Error(`Expected response body to include ${needle}.`);
90
+ }
91
+ return `${response.status}`;
92
+ }
93
+ async function fetchWithTimeout(url, init = {}) {
94
+ const controller = new AbortController();
95
+ const timeout = setTimeout(() => controller.abort(), 5000);
96
+ try {
97
+ return await fetch(url, { ...init, signal: controller.signal, redirect: 'manual' });
98
+ }
99
+ finally {
100
+ clearTimeout(timeout);
101
+ }
102
+ }
103
+ function expectWebSocket(baseUrl) {
104
+ const url = new URL(baseUrl);
105
+ const host = url.hostname;
106
+ const port = Number(url.port || (url.protocol === 'https:' ? 443 : 80));
107
+ const key = randomBytes(16).toString('base64');
108
+ return new Promise((resolve, reject) => {
109
+ const socket = net.connect(port, host, () => {
110
+ socket.write([
111
+ 'GET / HTTP/1.1',
112
+ `Host: ${url.host}`,
113
+ 'Connection: Upgrade',
114
+ 'Upgrade: websocket',
115
+ `Sec-WebSocket-Key: ${key}`,
116
+ 'Sec-WebSocket-Version: 13',
117
+ 'Sec-WebSocket-Protocol: vite-hmr',
118
+ '',
119
+ '',
120
+ ].join('\r\n'));
121
+ });
122
+ socket.setTimeout(5000);
123
+ socket.once('data', (data) => {
124
+ const firstLine = data.toString().split('\r\n')[0] ?? '';
125
+ socket.destroy();
126
+ if (!firstLine.includes('101')) {
127
+ reject(new Error(`Expected 101 Switching Protocols, got ${firstLine || 'no response'}.`));
128
+ return;
129
+ }
130
+ resolve('101 Switching Protocols');
131
+ });
132
+ socket.once('timeout', () => {
133
+ socket.destroy();
134
+ reject(new Error('Timed out waiting for websocket upgrade.'));
135
+ });
136
+ socket.once('error', reject);
137
+ });
138
+ }
@@ -0,0 +1 @@
1
+ export declare function runTypes(): Promise<void>;