@gtkx/cli 0.9.3 → 0.10.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.
@@ -1,15 +1,37 @@
1
1
  import { type InlineConfig, type ViteDevServer } from "vite";
2
2
  /**
3
- * Options for creating a GTKX development server.
3
+ * Options for the GTKX development server.
4
4
  */
5
5
  export type DevServerOptions = {
6
- /** Path to the app entry file (e.g., "./src/app.tsx"). */
6
+ /** Path to the entry file (e.g., "src/dev.tsx") */
7
7
  entry: string;
8
- /** Vite configuration overrides for customizing the dev server. */
8
+ /** Additional Vite configuration */
9
9
  vite?: InlineConfig;
10
10
  };
11
11
  /**
12
- * Creates and starts a GTKX development server with HMR support.
12
+ * Creates a Vite-based development server with hot module replacement.
13
+ *
14
+ * Provides fast refresh for React components and full reload for other changes.
15
+ * The server watches for file changes and automatically updates the running
16
+ * GTK application.
17
+ *
18
+ * @param options - Server configuration including entry point and Vite options
19
+ * @returns A Vite development server instance
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * import { createDevServer } from "@gtkx/cli";
24
+ * import { render } from "@gtkx/react";
25
+ *
26
+ * const server = await createDevServer({
27
+ * entry: "./src/dev.tsx",
28
+ * });
29
+ *
30
+ * const mod = await server.ssrLoadModule("./src/dev.tsx");
31
+ * render(<mod.default />, mod.appId);
32
+ * ```
33
+ *
34
+ * @see {@link DevServerOptions} for configuration options
13
35
  */
14
36
  export declare const createDevServer: (options: DevServerOptions) => Promise<ViteDevServer>;
15
37
  export type { ViteDevServer };
@@ -1,18 +1,44 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { events } from "@gtkx/ffi";
3
- import { update } from "@gtkx/react";
4
- import react from "@vitejs/plugin-react";
3
+ import { setHotReloading, update } from "@gtkx/react";
5
4
  import { createServer } from "vite";
5
+ import { isReactRefreshBoundary, performRefresh } from "./refresh-runtime.js";
6
+ import { gtkxRefresh } from "./vite-plugin-gtkx-refresh.js";
7
+ import { swcSsrRefresh } from "./vite-plugin-swc-ssr-refresh.js";
6
8
  /**
7
- * Creates and starts a GTKX development server with HMR support.
9
+ * Creates a Vite-based development server with hot module replacement.
10
+ *
11
+ * Provides fast refresh for React components and full reload for other changes.
12
+ * The server watches for file changes and automatically updates the running
13
+ * GTK application.
14
+ *
15
+ * @param options - Server configuration including entry point and Vite options
16
+ * @returns A Vite development server instance
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { createDevServer } from "@gtkx/cli";
21
+ * import { render } from "@gtkx/react";
22
+ *
23
+ * const server = await createDevServer({
24
+ * entry: "./src/dev.tsx",
25
+ * });
26
+ *
27
+ * const mod = await server.ssrLoadModule("./src/dev.tsx");
28
+ * render(<mod.default />, mod.appId);
29
+ * ```
30
+ *
31
+ * @see {@link DevServerOptions} for configuration options
8
32
  */
9
33
  export const createDevServer = async (options) => {
10
34
  const { entry, vite: viteConfig } = options;
35
+ const moduleExports = new Map();
11
36
  const server = await createServer({
12
37
  ...viteConfig,
13
38
  appType: "custom",
14
39
  plugins: [
15
- react(),
40
+ swcSsrRefresh(),
41
+ gtkxRefresh(),
16
42
  {
17
43
  name: "gtkx:remove-react-dom-optimized",
18
44
  enforce: "post",
@@ -37,29 +63,58 @@ export const createDevServer = async (options) => {
37
63
  },
38
64
  });
39
65
  const loadModule = async () => {
40
- return server.ssrLoadModule(entry);
66
+ const mod = (await server.ssrLoadModule(entry));
67
+ moduleExports.set(entry, { ...mod });
68
+ return mod;
41
69
  };
42
70
  const invalidateAllModules = () => {
43
71
  for (const module of server.moduleGraph.idToModuleMap.values()) {
44
72
  server.moduleGraph.invalidateModule(module);
45
73
  }
46
74
  };
75
+ const invalidateModuleAndImporters = (filePath) => {
76
+ const module = server.moduleGraph.getModuleById(filePath);
77
+ if (module) {
78
+ server.moduleGraph.invalidateModule(module);
79
+ for (const importer of module.importers) {
80
+ server.moduleGraph.invalidateModule(importer);
81
+ }
82
+ }
83
+ };
47
84
  events.on("stop", () => {
48
85
  server.close();
49
86
  });
50
87
  server.watcher.on("change", async (changedPath) => {
51
88
  console.log(`[gtkx] File changed: ${changedPath}`);
52
- invalidateAllModules();
53
89
  try {
90
+ const module = server.moduleGraph.getModuleById(changedPath);
91
+ if (module) {
92
+ invalidateModuleAndImporters(changedPath);
93
+ const newMod = (await server.ssrLoadModule(changedPath));
94
+ moduleExports.set(changedPath, { ...newMod });
95
+ if (isReactRefreshBoundary(newMod)) {
96
+ console.log("[gtkx] Fast refreshing...");
97
+ performRefresh();
98
+ console.log("[gtkx] Fast refresh complete");
99
+ return;
100
+ }
101
+ }
102
+ console.log("[gtkx] Full reload...");
103
+ invalidateAllModules();
54
104
  const mod = await loadModule();
55
105
  const App = mod.default;
56
106
  if (typeof App !== "function") {
57
107
  console.error("[gtkx] Entry file must export a default function component");
58
108
  return;
59
109
  }
60
- console.log("[gtkx] Hot reloading...");
61
- update(_jsx(App, {}));
62
- console.log("[gtkx] Hot reload complete");
110
+ setHotReloading(true);
111
+ try {
112
+ await update(_jsx(App, {}));
113
+ }
114
+ finally {
115
+ setHotReloading(false);
116
+ }
117
+ console.log("[gtkx] Full reload complete");
63
118
  }
64
119
  catch (error) {
65
120
  console.error("[gtkx] Hot reload failed:", error);
@@ -0,0 +1,9 @@
1
+ import RefreshRuntime from "react-refresh/runtime";
2
+ type ComponentType = (...args: unknown[]) => unknown;
3
+ export declare function createModuleRegistration(moduleId: string): {
4
+ $RefreshReg$: (type: ComponentType, id: string) => void;
5
+ $RefreshSig$: typeof RefreshRuntime.createSignatureFunctionForTransform;
6
+ };
7
+ export declare function isReactRefreshBoundary(moduleExports: Record<string, unknown>): boolean;
8
+ export declare function performRefresh(): void;
9
+ export {};
@@ -0,0 +1,44 @@
1
+ import RefreshRuntime from "react-refresh/runtime";
2
+ RefreshRuntime.injectIntoGlobalHook(globalThis);
3
+ globalThis.$RefreshReg$ = () => { };
4
+ globalThis.$RefreshSig$ = () => (type) => type;
5
+ export function createModuleRegistration(moduleId) {
6
+ return {
7
+ $RefreshReg$: (type, id) => {
8
+ RefreshRuntime.register(type, `${moduleId} ${id}`);
9
+ },
10
+ $RefreshSig$: RefreshRuntime.createSignatureFunctionForTransform,
11
+ };
12
+ }
13
+ function isLikelyComponentType(value) {
14
+ if (typeof value !== "function") {
15
+ return false;
16
+ }
17
+ const func = value;
18
+ if (func.$$typeof === Symbol.for("react.memo") || func.$$typeof === Symbol.for("react.forward_ref")) {
19
+ return true;
20
+ }
21
+ const name = value.name;
22
+ if (typeof name === "string" && /^[A-Z]/.test(name)) {
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+ export function isReactRefreshBoundary(moduleExports) {
28
+ if (RefreshRuntime.isLikelyComponentType(moduleExports)) {
29
+ return true;
30
+ }
31
+ for (const key in moduleExports) {
32
+ if (key === "__esModule") {
33
+ continue;
34
+ }
35
+ const value = moduleExports[key];
36
+ if (!isLikelyComponentType(value)) {
37
+ return false;
38
+ }
39
+ }
40
+ return Object.keys(moduleExports).filter((k) => k !== "__esModule").length > 0;
41
+ }
42
+ export function performRefresh() {
43
+ RefreshRuntime.performReactRefresh();
44
+ }
@@ -0,0 +1,8 @@
1
+ import type { TestingFramework } from "./create.js";
2
+ export interface TemplateContext {
3
+ name: string;
4
+ appId: string;
5
+ title: string;
6
+ testing: TestingFramework;
7
+ }
8
+ export declare const renderFile: (templateName: string, context: TemplateContext) => string;
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import ejs from "ejs";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const getTemplatesDir = () => {
8
+ return join(__dirname, "..", "templates");
9
+ };
10
+ const renderTemplate = (templatePath, context) => {
11
+ const templateContent = readFileSync(templatePath, "utf-8");
12
+ return ejs.render(templateContent, context);
13
+ };
14
+ export const renderFile = (templateName, context) => {
15
+ const templatesDir = getTemplatesDir();
16
+ const templatePath = join(templatesDir, templateName);
17
+ return renderTemplate(templatePath, context);
18
+ };
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from "vite";
2
+ type GtkxRefreshOptions = {
3
+ include?: RegExp;
4
+ exclude?: RegExp;
5
+ };
6
+ export declare function gtkxRefresh(options?: GtkxRefreshOptions): Plugin;
7
+ export {};
@@ -0,0 +1,36 @@
1
+ const defaultInclude = /\.[tj]sx?$/;
2
+ const defaultExclude = /node_modules/;
3
+ const refreshRuntimePath = "@gtkx/cli/refresh-runtime";
4
+ export function gtkxRefresh(options = {}) {
5
+ const include = options.include ?? defaultInclude;
6
+ const exclude = options.exclude ?? defaultExclude;
7
+ return {
8
+ name: "gtkx:refresh",
9
+ enforce: "post",
10
+ transform(code, id, transformOptions) {
11
+ if (!transformOptions?.ssr) {
12
+ return;
13
+ }
14
+ if (!include.test(id)) {
15
+ return;
16
+ }
17
+ if (exclude.test(id)) {
18
+ return;
19
+ }
20
+ const hasRefreshReg = code.includes("$RefreshReg$");
21
+ const hasRefreshSig = code.includes("$RefreshSig$");
22
+ if (!hasRefreshReg && !hasRefreshSig) {
23
+ return;
24
+ }
25
+ const moduleIdJson = JSON.stringify(id);
26
+ const header = `
27
+ import { createModuleRegistration as __createModuleRegistration__ } from "${refreshRuntimePath}";
28
+ const { $RefreshReg$, $RefreshSig$ } = __createModuleRegistration__(${moduleIdJson});
29
+ `;
30
+ return {
31
+ code: header + code,
32
+ map: null,
33
+ };
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from "vite";
2
+ type SwcSsrRefreshOptions = {
3
+ include?: RegExp;
4
+ exclude?: RegExp;
5
+ };
6
+ export declare function swcSsrRefresh(options?: SwcSsrRefreshOptions): Plugin;
7
+ export {};
@@ -0,0 +1,45 @@
1
+ import { transform } from "@swc/core";
2
+ const defaultInclude = /\.[tj]sx?$/;
3
+ const defaultExclude = /node_modules/;
4
+ export function swcSsrRefresh(options = {}) {
5
+ const include = options.include ?? defaultInclude;
6
+ const exclude = options.exclude ?? defaultExclude;
7
+ return {
8
+ name: "gtkx:swc-ssr-refresh",
9
+ enforce: "pre",
10
+ async transform(code, id, transformOptions) {
11
+ if (!transformOptions?.ssr) {
12
+ return;
13
+ }
14
+ if (!include.test(id)) {
15
+ return;
16
+ }
17
+ if (exclude.test(id)) {
18
+ return;
19
+ }
20
+ const isTsx = id.endsWith(".tsx");
21
+ const isTs = id.endsWith(".ts") || isTsx;
22
+ const swcOptions = {
23
+ filename: id,
24
+ sourceFileName: id,
25
+ sourceMaps: true,
26
+ jsc: {
27
+ parser: isTs ? { syntax: "typescript", tsx: isTsx } : { syntax: "ecmascript", jsx: true },
28
+ transform: {
29
+ react: {
30
+ runtime: "automatic",
31
+ development: true,
32
+ refresh: true,
33
+ },
34
+ },
35
+ target: "es2022",
36
+ },
37
+ };
38
+ const result = await transform(code, swcOptions);
39
+ return {
40
+ code: result.code,
41
+ map: result.map,
42
+ };
43
+ },
44
+ };
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/cli",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "CLI for GTKX - create and develop GTK4 React applications",
5
5
  "keywords": [
6
6
  "gtk",
@@ -33,24 +33,33 @@
33
33
  "./dev-server": {
34
34
  "types": "./dist/dev-server.d.ts",
35
35
  "default": "./dist/dev-server.js"
36
+ },
37
+ "./refresh-runtime": {
38
+ "types": "./dist/refresh-runtime.d.ts",
39
+ "default": "./dist/refresh-runtime.js"
36
40
  }
37
41
  },
38
42
  "bin": {
39
- "gtkx": "dist/cli.js"
43
+ "gtkx": "bin/gtkx.js"
40
44
  },
41
45
  "files": [
46
+ "bin",
42
47
  "dist",
43
48
  "templates"
44
49
  ],
45
50
  "dependencies": {
46
51
  "@clack/prompts": "^0.11.0",
47
- "@vitejs/plugin-react": "^5.1.2",
52
+ "@swc/core": "^1.15.7",
48
53
  "citty": "^0.1.6",
54
+ "ejs": "^3.1.10",
55
+ "react-refresh": "^0.18.0",
49
56
  "vite": "^7.3.0",
50
- "@gtkx/ffi": "0.9.3",
51
- "@gtkx/react": "0.9.3"
57
+ "@gtkx/ffi": "0.10.0",
58
+ "@gtkx/react": "0.10.0"
52
59
  },
53
60
  "devDependencies": {
61
+ "@types/ejs": "^3.1.5",
62
+ "@types/react-refresh": "^0.14.7",
54
63
  "memfs": "^4.51.1"
55
64
  },
56
65
  "peerDependencies": {