feat: add hmr-demo example with Vue SFC HMR
- Counter + Clock components demonstrating component-level HMR - Fix hmr-loader: use project-root file:// URL prefix so Node's bare specifier resolution finds node_modules - Fix hmr-loader: export hooks directly instead of inline data URL - Fix process-manager: register loader via data URL + register() pointing to the file (matches spike pattern) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@vue-tui/example-hmr-demo",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vue-tui dev",
|
||||
"build": "vite build",
|
||||
"start": "vite build && node dist/game.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue-tui/cli": "workspace:*",
|
||||
"@vue-tui/runtime": "workspace:*",
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@vitejs/plugin-vue": "^6",
|
||||
"vite": "catalog:"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, onMounted, onUnmounted } from "vue";
|
||||
import { Box, Text, useInput } from "@vue-tui/runtime";
|
||||
import Counter from "./Counter.vue";
|
||||
import Clock from "./Clock.vue";
|
||||
|
||||
const title = "HMR Demo";
|
||||
const hint = "Try editing Counter.vue (HMR) or App.vue (reload)";
|
||||
|
||||
const showClock = shallowRef(true);
|
||||
|
||||
useInput((input) => {
|
||||
if (input === "c") showClock.value = !showClock.value;
|
||||
if (input === "q") process.exit(0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Box flexDirection="column" :paddingX="1">
|
||||
<Text bold color="cyan">{{ title }}</Text>
|
||||
<Text dimColor>{{ hint }}</Text>
|
||||
<Text dimColor>Press c=toggle clock, q=quit</Text>
|
||||
<Text> </Text>
|
||||
<Counter />
|
||||
<Clock v-if="showClock" />
|
||||
</Box>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, onMounted, onUnmounted } from "vue";
|
||||
import { Box, Text } from "@vue-tui/runtime";
|
||||
|
||||
const time = shallowRef(new Date().toLocaleTimeString());
|
||||
let timer: ReturnType<typeof setInterval>;
|
||||
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
time.value = new Date().toLocaleTimeString();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Box>
|
||||
<Text>Clock: </Text>
|
||||
<Text bold color="yellow">{{ time }}</Text>
|
||||
</Box>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from "vue";
|
||||
import { Box, Text, useInput } from "@vue-tui/runtime";
|
||||
|
||||
const count = shallowRef(0);
|
||||
|
||||
useInput((input) => {
|
||||
if (input === "+") count.value++;
|
||||
if (input === "-") count.value--;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Box>
|
||||
<Text>Count: </Text>
|
||||
<Text bold color="green">{{ count }}</Text>
|
||||
<Text dimColor> (+/- to change)</Text>
|
||||
</Box>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createApp } from "@vue-tui/runtime";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount();
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module "*.vue" {
|
||||
import type { Component } from "vue";
|
||||
const component: Component;
|
||||
export default component;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
target: "node22",
|
||||
outDir: "dist",
|
||||
emptyOutDir: true,
|
||||
minify: false,
|
||||
lib: {
|
||||
entry: `${here}src/main.ts`,
|
||||
formats: ["es"],
|
||||
fileName: () => "game.mjs",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: (id) => !id.startsWith(".") && !id.startsWith("/") && !id.startsWith("\0"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,25 +1,27 @@
|
||||
import { register } from "node:module";
|
||||
const VITE_PORT = process.env.VUE_TUI_HMR_PORT || "5173";
|
||||
const HMR_PREFIX = `file://${process.cwd()}/.vue-tui-hmr`;
|
||||
|
||||
register(
|
||||
`data:text/javascript,${encodeURIComponent(`
|
||||
const VITE_PORT = process.env.VUE_TUI_HMR_PORT || '5173';
|
||||
|
||||
export async function resolve(specifier, context, nextResolve) {
|
||||
if (/^\\/hmr_patch_\\d+\\.js$/.test(specifier)) {
|
||||
return { url: 'vite-hmr://' + specifier, shortCircuit: true };
|
||||
export async function resolve(
|
||||
specifier: string,
|
||||
context: unknown,
|
||||
nextResolve: Function,
|
||||
): Promise<{ url: string; shortCircuit?: boolean }> {
|
||||
if (/^\/hmr_patch_\d+\.js$/.test(specifier)) {
|
||||
return { url: `${HMR_PREFIX}${specifier}`, shortCircuit: true };
|
||||
}
|
||||
return nextResolve(specifier, context);
|
||||
}
|
||||
|
||||
export async function load(url, context, nextLoad) {
|
||||
if (url.startsWith('vite-hmr://')) {
|
||||
const path = url.replace('vite-hmr://', '');
|
||||
const res = await fetch('http://localhost:' + VITE_PORT + path);
|
||||
if (!res.ok) throw new Error('HMR patch fetch failed: ' + res.status);
|
||||
return { format: 'module', source: await res.text(), shortCircuit: true };
|
||||
export async function load(
|
||||
url: string,
|
||||
context: unknown,
|
||||
nextLoad: Function,
|
||||
): Promise<{ format: string; source: string; shortCircuit?: boolean }> {
|
||||
if (url.startsWith(HMR_PREFIX)) {
|
||||
const path = url.slice(HMR_PREFIX.length);
|
||||
const res = await fetch(`http://localhost:${VITE_PORT}${path}`);
|
||||
if (!res.ok) throw new Error(`HMR patch fetch failed: ${res.status}`);
|
||||
return { format: "module", source: await res.text(), shortCircuit: true };
|
||||
}
|
||||
return nextLoad(url, context);
|
||||
}
|
||||
`)}`,
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { Logger } from "./logger.ts";
|
||||
|
||||
export interface ProcessManagerOptions {
|
||||
@@ -9,7 +8,8 @@ export interface ProcessManagerOptions {
|
||||
onExit?: (code: number | null) => void;
|
||||
}
|
||||
|
||||
const loaderPath = fileURLToPath(new URL("./hmr-loader.mjs", import.meta.url));
|
||||
const loaderUrl = new URL("./hmr-loader.mjs", import.meta.url).href;
|
||||
const loaderBootstrap = `data:text/javascript,import{register}from"node:module";register(${JSON.stringify(loaderUrl)})`;
|
||||
|
||||
export function createProcessManager(options: ProcessManagerOptions) {
|
||||
let child: ChildProcess | null = null;
|
||||
@@ -17,7 +17,7 @@ export function createProcessManager(options: ProcessManagerOptions) {
|
||||
|
||||
function doSpawn() {
|
||||
options.logger.mode = "silent";
|
||||
child = spawn("node", ["--import", loaderPath, currentBundlePath], {
|
||||
child = spawn("node", ["--import", loaderBootstrap, currentBundlePath], {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
Reference in New Issue
Block a user