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.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/astro.d.ts +5 -0
- package/dist/astro.js +27 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +62 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.js +139 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +55 -0
- package/dist/commands/smoke.d.ts +1 -0
- package/dist/commands/smoke.js +138 -0
- package/dist/commands/types.d.ts +1 -0
- package/dist/commands/types.js +67 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.js +74 -0
- package/dist/content.d.ts +44 -0
- package/dist/content.js +190 -0
- package/dist/dev-toolbar/vitewp-toolbar.d.ts +23 -0
- package/dist/dev-toolbar/vitewp-toolbar.js +144 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/live.config.d.ts +64 -0
- package/dist/live.config.js +21 -0
- package/dist/runtime/astro.d.ts +4 -0
- package/dist/runtime/astro.js +69 -0
- package/dist/runtime/composer.d.ts +2 -0
- package/dist/runtime/composer.js +36 -0
- package/dist/runtime/php.d.ts +3 -0
- package/dist/runtime/php.js +79 -0
- package/dist/runtime/ports.d.ts +3 -0
- package/dist/runtime/ports.js +38 -0
- package/dist/runtime/process.d.ts +12 -0
- package/dist/runtime/process.js +51 -0
- package/dist/runtime/proxy.d.ts +6 -0
- package/dist/runtime/proxy.js +204 -0
- package/dist/runtime/wp-config.d.ts +3 -0
- package/dist/runtime/wp-config.js +82 -0
- package/dist/wordpress/client.d.ts +98 -0
- package/dist/wordpress/client.js +133 -0
- package/dist/wordpress/generated-types.d.ts +33 -0
- package/dist/wordpress/generated-types.js +2 -0
- package/dist/wordpress/menus.d.ts +24 -0
- package/dist/wordpress/menus.js +20 -0
- package/dist/wordpress/schemas.d.ts +58 -0
- package/dist/wordpress/schemas.js +39 -0
- package/dist/wordpress/templates.d.ts +19 -0
- package/dist/wordpress/templates.js +88 -0
- package/package.json +78 -0
- package/starter/.env.example +15 -0
- package/starter/astro.config.mjs +16 -0
- package/starter/composer.json +37 -0
- package/starter/src/env.d.ts +1 -0
- package/starter/src/live.config.ts +22 -0
- package/starter/src/pages/[...slug].astro +101 -0
- package/starter/src/templates/404.astro +11 -0
- package/starter/src/templates/pages/.gitkeep +0 -0
- package/starter/src/templates/pages/default.astro +12 -0
- package/starter/src/templates/pages/front-page.astro +13 -0
- package/starter/src/templates/partials/Header.astro +23 -0
- package/starter/src/templates/partials/Pagination.astro +21 -0
- package/starter/src/templates/post-types/.gitkeep +0 -0
- package/starter/src/templates/posts/.gitkeep +0 -0
- package/starter/src/templates/posts/archive.astro +25 -0
- package/starter/src/templates/posts/single.astro +12 -0
- package/starter/src/templates/search.astro +30 -0
- package/starter/src/templates/taxonomies/.gitkeep +0 -0
- package/starter/src/templates/taxonomies/taxonomy-[taxonomy].astro +28 -0
- package/starter/src/wordpress/client.ts +263 -0
- package/starter/src/wordpress/generated-types.ts +38 -0
- package/starter/src/wordpress/menus.ts +51 -0
- package/starter/src/wordpress/schemas.ts +44 -0
- package/starter/src/wordpress/templates.ts +113 -0
- package/starter/tsconfig.json +4 -0
- package/starter/vitewp.config.ts +29 -0
- package/starter/wordpress/content/mu-plugins/.gitkeep +0 -0
- package/starter/wordpress/content/mu-plugins/vitewp-bridge.php +560 -0
- package/starter/wordpress/content/themes/vitewp/functions.php +9 -0
- package/starter/wordpress/content/themes/vitewp/index.php +22 -0
- package/starter/wordpress/content/themes/vitewp/style.css +10 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const collections: {
|
|
2
|
+
routes: import("astro/content/config").LiveCollectionConfig<import("./content.js").LiveLoader<import("./wordpress/client.js").WpResolvedRoute & Record<string, unknown>, import("./content.js").WpRouteEntryFilter, import("./content.js").WpRouteCollectionFilter>, import("zod/v4").ZodObject<{
|
|
3
|
+
kind: import("zod/v4").ZodString;
|
|
4
|
+
postType: import("zod/v4").ZodString;
|
|
5
|
+
restBase: import("zod/v4").ZodString;
|
|
6
|
+
slug: import("zod/v4").ZodString;
|
|
7
|
+
}, import("zod/v4/core").$loose>>;
|
|
8
|
+
posts: import("astro/content/config").LiveCollectionConfig<import("./content.js").LiveLoader<import("./wordpress/client.js").WpContentItem & Record<string, unknown>, import("./content.js").WpPostEntryFilter, import("./content.js").WpPostCollectionFilter>, import("zod/v4").ZodObject<{
|
|
9
|
+
id: import("zod/v4").ZodNumber;
|
|
10
|
+
slug: import("zod/v4").ZodString;
|
|
11
|
+
type: import("zod/v4").ZodString;
|
|
12
|
+
link: import("zod/v4").ZodString;
|
|
13
|
+
title: import("zod/v4").ZodObject<{
|
|
14
|
+
rendered: import("zod/v4").ZodString;
|
|
15
|
+
protected: import("zod/v4").ZodOptional<import("zod/v4").ZodBoolean>;
|
|
16
|
+
}, import("zod/v4/core").$loose>;
|
|
17
|
+
content: import("zod/v4").ZodObject<{
|
|
18
|
+
rendered: import("zod/v4").ZodString;
|
|
19
|
+
protected: import("zod/v4").ZodOptional<import("zod/v4").ZodBoolean>;
|
|
20
|
+
}, import("zod/v4/core").$loose>;
|
|
21
|
+
excerpt: import("zod/v4").ZodOptional<import("zod/v4").ZodObject<{
|
|
22
|
+
rendered: import("zod/v4").ZodString;
|
|
23
|
+
protected: import("zod/v4").ZodOptional<import("zod/v4").ZodBoolean>;
|
|
24
|
+
}, import("zod/v4/core").$loose>>;
|
|
25
|
+
date: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
|
|
26
|
+
modified: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
|
|
27
|
+
}, import("zod/v4/core").$loose>>;
|
|
28
|
+
pages: import("astro/content/config").LiveCollectionConfig<import("./content.js").LiveLoader<import("./wordpress/client.js").WpContentItem & Record<string, unknown>, import("./content.js").WpPostEntryFilter, import("./content.js").WpPostCollectionFilter>, import("zod/v4").ZodObject<{
|
|
29
|
+
id: import("zod/v4").ZodNumber;
|
|
30
|
+
slug: import("zod/v4").ZodString;
|
|
31
|
+
type: import("zod/v4").ZodString;
|
|
32
|
+
link: import("zod/v4").ZodString;
|
|
33
|
+
title: import("zod/v4").ZodObject<{
|
|
34
|
+
rendered: import("zod/v4").ZodString;
|
|
35
|
+
protected: import("zod/v4").ZodOptional<import("zod/v4").ZodBoolean>;
|
|
36
|
+
}, import("zod/v4/core").$loose>;
|
|
37
|
+
content: import("zod/v4").ZodObject<{
|
|
38
|
+
rendered: import("zod/v4").ZodString;
|
|
39
|
+
protected: import("zod/v4").ZodOptional<import("zod/v4").ZodBoolean>;
|
|
40
|
+
}, import("zod/v4/core").$loose>;
|
|
41
|
+
excerpt: import("zod/v4").ZodOptional<import("zod/v4").ZodObject<{
|
|
42
|
+
rendered: import("zod/v4").ZodString;
|
|
43
|
+
protected: import("zod/v4").ZodOptional<import("zod/v4").ZodBoolean>;
|
|
44
|
+
}, import("zod/v4/core").$loose>>;
|
|
45
|
+
date: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
|
|
46
|
+
modified: import("zod/v4").ZodOptional<import("zod/v4").ZodString>;
|
|
47
|
+
}, import("zod/v4/core").$loose>>;
|
|
48
|
+
menus: import("astro/content/config").LiveCollectionConfig<import("./content.js").LiveLoader<import("./wordpress/menus.js").WpMenu & Record<string, unknown>, import("./content.js").WpMenuEntryFilter, import("./content.js").WpMenuCollectionFilter>, import("zod/v4").ZodObject<{
|
|
49
|
+
id: import("zod/v4").ZodNumber;
|
|
50
|
+
slug: import("zod/v4").ZodString;
|
|
51
|
+
name: import("zod/v4").ZodString;
|
|
52
|
+
items: import("zod/v4").ZodArray<import("zod/v4").ZodObject<{
|
|
53
|
+
id: import("zod/v4").ZodNumber;
|
|
54
|
+
parent: import("zod/v4").ZodNumber;
|
|
55
|
+
title: import("zod/v4").ZodString;
|
|
56
|
+
url: import("zod/v4").ZodString;
|
|
57
|
+
target: import("zod/v4").ZodString;
|
|
58
|
+
classes: import("zod/v4").ZodArray<import("zod/v4").ZodString>;
|
|
59
|
+
object: import("zod/v4").ZodString;
|
|
60
|
+
objectId: import("zod/v4").ZodNumber;
|
|
61
|
+
type: import("zod/v4").ZodString;
|
|
62
|
+
}, import("zod/v4/core").$loose>>;
|
|
63
|
+
}, import("zod/v4/core").$loose>>;
|
|
64
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineLiveCollection } from 'astro/content/config';
|
|
2
|
+
import { wpMenuLoader, wpPostTypeLoader, wpRouteLoader } from './content.js';
|
|
3
|
+
import { wpContentItemSchema, wpMenuSchema, wpResolvedRouteSchema } from './wordpress/schemas.js';
|
|
4
|
+
export const collections = {
|
|
5
|
+
routes: defineLiveCollection({
|
|
6
|
+
loader: wpRouteLoader(),
|
|
7
|
+
schema: wpResolvedRouteSchema,
|
|
8
|
+
}),
|
|
9
|
+
posts: defineLiveCollection({
|
|
10
|
+
loader: wpPostTypeLoader({ postType: 'post', restBase: 'posts' }),
|
|
11
|
+
schema: wpContentItemSchema,
|
|
12
|
+
}),
|
|
13
|
+
pages: defineLiveCollection({
|
|
14
|
+
loader: wpPostTypeLoader({ postType: 'page', restBase: 'pages' }),
|
|
15
|
+
schema: wpContentItemSchema,
|
|
16
|
+
}),
|
|
17
|
+
menus: defineLiveCollection({
|
|
18
|
+
loader: wpMenuLoader(),
|
|
19
|
+
schema: wpMenuSchema,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LoadedViteWpConfig } from '../config.js';
|
|
2
|
+
import { type ManagedProcess } from './process.js';
|
|
3
|
+
export declare function stopAstroServer(config: LoadedViteWpConfig): Promise<void>;
|
|
4
|
+
export declare function startAstroServer(config: LoadedViteWpConfig): Promise<ManagedProcess>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { spawnManaged } from './process.js';
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
export async function stopAstroServer(config) {
|
|
6
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
7
|
+
await execFileAsync(npx, ['astro', 'dev', 'stop'], {
|
|
8
|
+
cwd: config.root,
|
|
9
|
+
env: process.env,
|
|
10
|
+
}).catch(() => undefined);
|
|
11
|
+
await killStaleAstroProcesses(config);
|
|
12
|
+
}
|
|
13
|
+
export async function startAstroServer(config) {
|
|
14
|
+
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
15
|
+
await stopAstroServer(config);
|
|
16
|
+
await execFileAsync(npx, [
|
|
17
|
+
'astro',
|
|
18
|
+
'dev',
|
|
19
|
+
'--background',
|
|
20
|
+
'--host',
|
|
21
|
+
config.dev.astroHost,
|
|
22
|
+
'--port',
|
|
23
|
+
String(config.dev.astroPort),
|
|
24
|
+
], {
|
|
25
|
+
cwd: config.root,
|
|
26
|
+
env: process.env,
|
|
27
|
+
});
|
|
28
|
+
const logs = spawnManaged('astro', npx, ['astro', 'dev', 'logs', '--follow'], config.root, {
|
|
29
|
+
shouldLogLine: (line) => isVerbose() || !line.includes('Local http://'),
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
...logs,
|
|
33
|
+
critical: false,
|
|
34
|
+
stop: () => {
|
|
35
|
+
logs.stop();
|
|
36
|
+
spawn(npx, ['astro', 'dev', 'stop'], {
|
|
37
|
+
cwd: config.root,
|
|
38
|
+
stdio: 'ignore',
|
|
39
|
+
detached: true,
|
|
40
|
+
}).unref();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function isVerbose() {
|
|
45
|
+
return process.env.VITEWP_VERBOSE === '1';
|
|
46
|
+
}
|
|
47
|
+
async function killStaleAstroProcesses(config) {
|
|
48
|
+
if (process.platform === 'win32')
|
|
49
|
+
return;
|
|
50
|
+
const { stdout } = await execFileAsync('ps', ['-Ao', 'pid=,command=']).catch(() => ({ stdout: '' }));
|
|
51
|
+
const lines = stdout.split('\n');
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
54
|
+
if (!match)
|
|
55
|
+
continue;
|
|
56
|
+
const pid = Number(match[1]);
|
|
57
|
+
const command = match[2] ?? '';
|
|
58
|
+
if (pid !== process.pid
|
|
59
|
+
&& command.includes(`${config.root}/node_modules/astro/bin/astro.mjs`)
|
|
60
|
+
&& command.includes(' dev')) {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(pid, 'SIGTERM');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Ignore already-exited processes.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
export async function ensureComposerInstall(config) {
|
|
5
|
+
if (!config.composer.install) {
|
|
6
|
+
console.log('Composer install disabled by config.');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const lockfile = join(config.root, 'composer.lock');
|
|
10
|
+
const vendor = join(config.root, 'vendor');
|
|
11
|
+
const wpSettings = join(config.root, config.wordpress.docroot, 'wp-settings.php');
|
|
12
|
+
if (existsSync(lockfile) && existsSync(vendor) && existsSync(wpSettings)) {
|
|
13
|
+
console.log('✓ Composer dependencies already installed');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log('Installing Composer dependencies...');
|
|
17
|
+
await runComposerInstall(config.root);
|
|
18
|
+
console.log('✓ Composer dependencies ready');
|
|
19
|
+
}
|
|
20
|
+
function runComposerInstall(cwd) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn('composer', ['install'], {
|
|
23
|
+
cwd,
|
|
24
|
+
stdio: 'inherit',
|
|
25
|
+
env: { ...process.env, COMPOSER_ALLOW_SUPERUSER: process.env.COMPOSER_ALLOW_SUPERUSER ?? '1' },
|
|
26
|
+
});
|
|
27
|
+
child.once('exit', (code) => {
|
|
28
|
+
if (code === 0) {
|
|
29
|
+
resolve();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
reject(new Error(`composer install failed with code ${code ?? 'unknown'}.`));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
import { spawnManaged } from './process.js';
|
|
4
|
+
export function startPhpServer(config) {
|
|
5
|
+
const docroot = resolve(config.root, config.wordpress.docroot);
|
|
6
|
+
const contentDir = resolve(config.root, config.wordpress.contentDir);
|
|
7
|
+
const router = writePhpRouter(config, docroot, contentDir);
|
|
8
|
+
return spawnManaged('php', 'php', ['-S', `${config.dev.phpHost}:${config.dev.phpPort}`, '-t', docroot, router], config.root, {
|
|
9
|
+
shouldLogLine: (line) => isVerbose() || !line.includes('Development Server (http://'),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function isVerbose() {
|
|
13
|
+
return process.env.VITEWP_VERBOSE === '1';
|
|
14
|
+
}
|
|
15
|
+
function writePhpRouter(config, docroot, contentDir) {
|
|
16
|
+
const runtimeDir = resolve(config.root, '.vitewp');
|
|
17
|
+
const routerPath = join(runtimeDir, 'php-router.php');
|
|
18
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
19
|
+
writeFileSync(routerPath, renderRouter(docroot, contentDir), 'utf8');
|
|
20
|
+
return relative(config.root, routerPath);
|
|
21
|
+
}
|
|
22
|
+
function renderRouter(docroot, contentDir) {
|
|
23
|
+
return `<?php
|
|
24
|
+
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/';
|
|
25
|
+
$file = ${phpString(docroot)} . $path;
|
|
26
|
+
$content_dir = ${phpString(contentDir)};
|
|
27
|
+
|
|
28
|
+
if (str_starts_with($path, '/wp-content/')) {
|
|
29
|
+
$content_file = $content_dir . substr($path, strlen('/wp-content'));
|
|
30
|
+
|
|
31
|
+
if (is_file($content_file)) {
|
|
32
|
+
$type = vitewp_content_type($content_file);
|
|
33
|
+
|
|
34
|
+
if ($type) {
|
|
35
|
+
header('Content-Type: ' . $type);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
readfile($content_file);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if ($path !== '/' && is_file($file)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if ($path !== '/' && is_dir($file) && is_file(rtrim($file, '/') . '/index.php')) {
|
|
48
|
+
$_SERVER['SCRIPT_NAME'] = rtrim($path, '/') . '/index.php';
|
|
49
|
+
$_SERVER['SCRIPT_FILENAME'] = rtrim($file, '/') . '/index.php';
|
|
50
|
+
require rtrim($file, '/') . '/index.php';
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function vitewp_content_type(string $file): string
|
|
55
|
+
{
|
|
56
|
+
return match (strtolower(pathinfo($file, PATHINFO_EXTENSION))) {
|
|
57
|
+
'css' => 'text/css; charset=UTF-8',
|
|
58
|
+
'js' => 'application/javascript; charset=UTF-8',
|
|
59
|
+
'json' => 'application/json; charset=UTF-8',
|
|
60
|
+
'svg' => 'image/svg+xml',
|
|
61
|
+
'webp' => 'image/webp',
|
|
62
|
+
'png' => 'image/png',
|
|
63
|
+
'jpg', 'jpeg' => 'image/jpeg',
|
|
64
|
+
'gif' => 'image/gif',
|
|
65
|
+
'woff' => 'font/woff',
|
|
66
|
+
'woff2' => 'font/woff2',
|
|
67
|
+
default => mime_content_type($file) ?: 'application/octet-stream',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$_SERVER['SCRIPT_NAME'] = '/index.php';
|
|
72
|
+
$_SERVER['SCRIPT_FILENAME'] = ${phpString(join(docroot, 'index.php'))};
|
|
73
|
+
require ${phpString(join(docroot, 'index.php'))};
|
|
74
|
+
return true;
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
function phpString(value) {
|
|
78
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function assertPortAvailable(host: string, port: number, label: string): Promise<void>;
|
|
2
|
+
export declare function resolveInternalPort(host: string, port: number): Promise<number>;
|
|
3
|
+
export declare function isPortAvailable(host: string, port: number): Promise<boolean>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
export async function assertPortAvailable(host, port, label) {
|
|
3
|
+
const available = await isPortAvailable(host, port);
|
|
4
|
+
if (!available) {
|
|
5
|
+
throw new Error(`${label} port ${host}:${port} is already in use.`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export async function resolveInternalPort(host, port) {
|
|
9
|
+
if (port > 0) {
|
|
10
|
+
const available = await isPortAvailable(host, port);
|
|
11
|
+
if (available) {
|
|
12
|
+
return port;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return findAvailablePort(host);
|
|
16
|
+
}
|
|
17
|
+
function findAvailablePort(host) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const server = net.createServer();
|
|
20
|
+
server.once('error', reject);
|
|
21
|
+
server.once('listening', () => {
|
|
22
|
+
const address = server.address();
|
|
23
|
+
const port = typeof address === 'object' && address ? address.port : 0;
|
|
24
|
+
server.close(() => resolve(port));
|
|
25
|
+
});
|
|
26
|
+
server.listen(0, host);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function isPortAvailable(host, port) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const server = net.createServer();
|
|
32
|
+
server.once('error', () => resolve(false));
|
|
33
|
+
server.once('listening', () => {
|
|
34
|
+
server.close(() => resolve(true));
|
|
35
|
+
});
|
|
36
|
+
server.listen(port, host);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
export interface ManagedProcess {
|
|
3
|
+
name: string;
|
|
4
|
+
child: ChildProcess;
|
|
5
|
+
stop: () => void;
|
|
6
|
+
critical?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SpawnManagedOptions {
|
|
9
|
+
shouldLogLine?: (line: string) => boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function spawnManaged(name: string, command: string, args: string[], cwd: string, options?: SpawnManagedOptions): ManagedProcess;
|
|
12
|
+
export declare function waitForExit(processes: ManagedProcess[]): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export function spawnManaged(name, command, args, cwd, options = {}) {
|
|
3
|
+
const child = spawn(command, args, {
|
|
4
|
+
cwd,
|
|
5
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
6
|
+
env: process.env,
|
|
7
|
+
});
|
|
8
|
+
child.stdout?.on('data', (chunk) => writeLines(name, chunk, options));
|
|
9
|
+
child.stderr?.on('data', (chunk) => writeLines(name, chunk, options));
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
child,
|
|
13
|
+
stop: () => {
|
|
14
|
+
if (!child.killed) {
|
|
15
|
+
child.kill('SIGTERM');
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function waitForExit(processes) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
let shuttingDown = false;
|
|
23
|
+
const stopAll = () => {
|
|
24
|
+
if (shuttingDown)
|
|
25
|
+
return;
|
|
26
|
+
shuttingDown = true;
|
|
27
|
+
for (const process of processes) {
|
|
28
|
+
process.stop();
|
|
29
|
+
}
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
process.once('SIGINT', stopAll);
|
|
33
|
+
process.once('SIGTERM', stopAll);
|
|
34
|
+
for (const managed of processes) {
|
|
35
|
+
managed.child.once('exit', (code) => {
|
|
36
|
+
if (!shuttingDown && managed.critical !== false) {
|
|
37
|
+
console.log(`\n${managed.name} exited with code ${code ?? 'unknown'}. Stopping ViteWP dev runtime.`);
|
|
38
|
+
stopAll();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function writeLines(name, chunk, options) {
|
|
45
|
+
const lines = chunk.toString().split(/\r?\n/);
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (line.length > 0 && (options.shouldLogLine?.(line) ?? true)) {
|
|
48
|
+
console.log(`[${name}] ${line}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createReadStream, statSync } from 'node:fs';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import { extname, normalize, resolve, sep } from 'node:path';
|
|
4
|
+
import { URL } from 'node:url';
|
|
5
|
+
const wordpressPrefixes = [
|
|
6
|
+
'/wp-admin',
|
|
7
|
+
'/wp-content',
|
|
8
|
+
'/wp-includes',
|
|
9
|
+
'/wp-json',
|
|
10
|
+
];
|
|
11
|
+
const wordpressFiles = [
|
|
12
|
+
'/index.php',
|
|
13
|
+
'/wp-login.php',
|
|
14
|
+
'/wp-cron.php',
|
|
15
|
+
'/wp-comments-post.php',
|
|
16
|
+
'/wp-trackback.php',
|
|
17
|
+
'/xmlrpc.php',
|
|
18
|
+
'/favicon.ico',
|
|
19
|
+
];
|
|
20
|
+
export async function startUnifiedProxy(config) {
|
|
21
|
+
const publicUrl = new URL(config.wordpress.url);
|
|
22
|
+
const phpUrl = new URL(`http://${config.dev.phpHost}:${config.dev.phpPort}`);
|
|
23
|
+
const astroUrl = new URL(`http://${config.dev.astroHost}:${config.dev.astroPort}`);
|
|
24
|
+
const contentDir = resolve(config.root, config.wordpress.contentDir);
|
|
25
|
+
const server = http.createServer((request, response) => {
|
|
26
|
+
if (serveWpContentAsset(request, response, contentDir)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
proxyHttpRequest(request, response, selectTarget(request.url ?? '/', phpUrl, astroUrl), publicUrl);
|
|
30
|
+
});
|
|
31
|
+
server.on('upgrade', (request, socket, head) => {
|
|
32
|
+
proxyUpgrade(request, socket, head, selectTarget(request.url ?? '/', phpUrl, astroUrl), publicUrl);
|
|
33
|
+
});
|
|
34
|
+
await new Promise((resolve, reject) => {
|
|
35
|
+
server.once('error', reject);
|
|
36
|
+
server.listen(Number(publicUrl.port || 3000), publicUrl.hostname, () => {
|
|
37
|
+
server.off('error', reject);
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
url: publicUrl.toString().replace(/\/$/, ''),
|
|
43
|
+
stop: () => new Promise((resolve) => {
|
|
44
|
+
server.close(() => resolve());
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function selectTarget(path, phpUrl, astroUrl) {
|
|
49
|
+
return isWordPressRequest(path) ? phpUrl : astroUrl;
|
|
50
|
+
}
|
|
51
|
+
function isWordPressRequest(path) {
|
|
52
|
+
const pathname = path.split('?')[0] ?? '/';
|
|
53
|
+
return wordpressPrefixes.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))
|
|
54
|
+
|| wordpressFiles.includes(pathname)
|
|
55
|
+
|| pathname.endsWith('.php');
|
|
56
|
+
}
|
|
57
|
+
function serveWpContentAsset(request, response, contentDir) {
|
|
58
|
+
const pathname = new URL(request.url ?? '/', 'http://vitewp.local').pathname;
|
|
59
|
+
if (!pathname.startsWith('/wp-content/')) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const relativePath = decodeURIComponent(pathname.slice('/wp-content/'.length));
|
|
63
|
+
const file = resolve(contentDir, normalize(relativePath));
|
|
64
|
+
if (!isInside(contentDir, file)) {
|
|
65
|
+
response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
|
|
66
|
+
response.end('Forbidden');
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const stats = statSync(file);
|
|
71
|
+
if (!stats.isFile()) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
response.writeHead(200, {
|
|
75
|
+
'content-type': contentType(file),
|
|
76
|
+
'content-length': stats.size,
|
|
77
|
+
'cache-control': 'no-cache',
|
|
78
|
+
});
|
|
79
|
+
if (request.method === 'HEAD') {
|
|
80
|
+
response.end();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
createReadStream(file).pipe(response);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isInside(root, file) {
|
|
91
|
+
const normalizedRoot = root.endsWith(sep) ? root : `${root}${sep}`;
|
|
92
|
+
return file.startsWith(normalizedRoot);
|
|
93
|
+
}
|
|
94
|
+
function rewriteTargetPath(path, target, publicUrl) {
|
|
95
|
+
if (target.origin === publicUrl.origin)
|
|
96
|
+
return path;
|
|
97
|
+
const url = new URL(path, publicUrl);
|
|
98
|
+
if (url.pathname === '/wp-json' || url.pathname.startsWith('/wp-json/')) {
|
|
99
|
+
const restRoute = url.pathname.replace(/^\/wp-json\/?/, '/');
|
|
100
|
+
url.pathname = '/index.php';
|
|
101
|
+
url.searchParams.set('rest_route', restRoute || '/');
|
|
102
|
+
return `${url.pathname}${url.search}`;
|
|
103
|
+
}
|
|
104
|
+
return path;
|
|
105
|
+
}
|
|
106
|
+
function proxyHttpRequest(request, response, target, publicUrl) {
|
|
107
|
+
const targetRequest = http.request({
|
|
108
|
+
protocol: target.protocol,
|
|
109
|
+
hostname: target.hostname,
|
|
110
|
+
port: target.port,
|
|
111
|
+
method: request.method,
|
|
112
|
+
path: rewriteTargetPath(request.url ?? '/', target, publicUrl),
|
|
113
|
+
headers: rewriteRequestHeaders(request, publicUrl),
|
|
114
|
+
}, (targetResponse) => {
|
|
115
|
+
response.writeHead(targetResponse.statusCode ?? 502, targetResponse.statusMessage, rewriteResponseHeaders(targetResponse.headers, target, publicUrl));
|
|
116
|
+
targetResponse.pipe(response);
|
|
117
|
+
});
|
|
118
|
+
targetRequest.on('error', (error) => {
|
|
119
|
+
response.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
120
|
+
response.end(`ViteWP proxy could not reach ${target.origin}: ${error.message}\n`);
|
|
121
|
+
});
|
|
122
|
+
request.pipe(targetRequest);
|
|
123
|
+
}
|
|
124
|
+
function proxyUpgrade(request, socket, head, target, publicUrl) {
|
|
125
|
+
socket.on('error', () => undefined);
|
|
126
|
+
const targetRequest = http.request({
|
|
127
|
+
protocol: target.protocol,
|
|
128
|
+
hostname: target.hostname,
|
|
129
|
+
port: target.port,
|
|
130
|
+
method: request.method,
|
|
131
|
+
path: rewriteTargetPath(request.url ?? '/', target, publicUrl),
|
|
132
|
+
headers: rewriteRequestHeaders(request, publicUrl),
|
|
133
|
+
});
|
|
134
|
+
targetRequest.on('upgrade', (targetResponse, targetSocket, targetHead) => {
|
|
135
|
+
targetSocket.on('error', () => socket.destroy());
|
|
136
|
+
socket.write(`HTTP/${targetResponse.httpVersion} ${targetResponse.statusCode} ${targetResponse.statusMessage}\r\n${rawHeaders(rewriteResponseHeaders(targetResponse.headers, target, publicUrl))}\r\n\r\n`);
|
|
137
|
+
if (targetHead.length > 0)
|
|
138
|
+
socket.write(targetHead);
|
|
139
|
+
if (head.length > 0)
|
|
140
|
+
targetSocket.write(head);
|
|
141
|
+
targetSocket.pipe(socket);
|
|
142
|
+
socket.pipe(targetSocket);
|
|
143
|
+
});
|
|
144
|
+
targetRequest.on('error', () => socket.destroy());
|
|
145
|
+
targetRequest.on('socket', (targetSocket) => {
|
|
146
|
+
targetSocket.on('error', () => socket.destroy());
|
|
147
|
+
});
|
|
148
|
+
targetRequest.end();
|
|
149
|
+
}
|
|
150
|
+
function rewriteRequestHeaders(request, publicUrl) {
|
|
151
|
+
return {
|
|
152
|
+
...request.headers,
|
|
153
|
+
host: publicUrl.host,
|
|
154
|
+
'x-forwarded-host': publicUrl.host,
|
|
155
|
+
'x-forwarded-proto': publicUrl.protocol.replace(':', ''),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function rewriteResponseHeaders(headers, target, publicUrl) {
|
|
159
|
+
const rewritten = { ...headers };
|
|
160
|
+
const location = headers.location;
|
|
161
|
+
if (typeof location === 'string') {
|
|
162
|
+
rewritten.location = location.replace(target.origin, publicUrl.origin);
|
|
163
|
+
}
|
|
164
|
+
delete rewritten['content-length'];
|
|
165
|
+
return rewritten;
|
|
166
|
+
}
|
|
167
|
+
function rawHeaders(headers) {
|
|
168
|
+
return Object.entries(headers)
|
|
169
|
+
.flatMap(([key, value]) => {
|
|
170
|
+
if (value === undefined)
|
|
171
|
+
return [];
|
|
172
|
+
if (Array.isArray(value))
|
|
173
|
+
return value.map((item) => `${key}: ${item}`);
|
|
174
|
+
return [`${key}: ${value}`];
|
|
175
|
+
})
|
|
176
|
+
.join('\r\n');
|
|
177
|
+
}
|
|
178
|
+
function contentType(file) {
|
|
179
|
+
switch (extname(file).toLowerCase()) {
|
|
180
|
+
case '.css':
|
|
181
|
+
return 'text/css; charset=UTF-8';
|
|
182
|
+
case '.js':
|
|
183
|
+
return 'application/javascript; charset=UTF-8';
|
|
184
|
+
case '.json':
|
|
185
|
+
return 'application/json; charset=UTF-8';
|
|
186
|
+
case '.svg':
|
|
187
|
+
return 'image/svg+xml';
|
|
188
|
+
case '.webp':
|
|
189
|
+
return 'image/webp';
|
|
190
|
+
case '.png':
|
|
191
|
+
return 'image/png';
|
|
192
|
+
case '.jpg':
|
|
193
|
+
case '.jpeg':
|
|
194
|
+
return 'image/jpeg';
|
|
195
|
+
case '.gif':
|
|
196
|
+
return 'image/gif';
|
|
197
|
+
case '.woff':
|
|
198
|
+
return 'font/woff';
|
|
199
|
+
case '.woff2':
|
|
200
|
+
return 'font/woff2';
|
|
201
|
+
default:
|
|
202
|
+
return 'application/octet-stream';
|
|
203
|
+
}
|
|
204
|
+
}
|