exportc 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -8,12 +8,14 @@ Add [export](https://github.com/ihasq/export) to existing Vite projects.
8
8
  # In your existing Vite project
9
9
  npx exportc init
10
10
 
11
- # Install dependencies
12
- cd export && npm install
11
+ # Install export dependencies
12
+ cd export && npm install && cd ..
13
13
 
14
- # Start development (two terminals)
15
- npm run export:dev # Terminal 1: Wrangler dev server
16
- npm run dev # Terminal 2: Vite dev server
14
+ # Start development (Wrangler starts automatically!)
15
+ npm run dev
16
+
17
+ # Deploy to Cloudflare Workers Sites
18
+ npm run export
17
19
  ```
18
20
 
19
21
  ## Usage
@@ -49,7 +51,7 @@ Deploy your exports to Cloudflare Workers.
49
51
 
50
52
  ## Vite Plugin
51
53
 
52
- The `exportPlugin` transforms `export:/` imports:
54
+ The `exportPlugin` automatically starts Wrangler and transforms `export:/` imports:
53
55
 
54
56
  ```typescript
55
57
  // vite.config.ts
@@ -58,16 +60,23 @@ import { exportPlugin } from "exportc/vite";
58
60
 
59
61
  export default defineConfig({
60
62
  plugins: [
61
- exportPlugin({
62
- // Development server (default: http://localhost:8787)
63
- dev: "http://localhost:8787",
64
- // Production Worker URL (required for production builds)
65
- production: "https://my-api.workers.dev",
66
- }),
63
+ exportPlugin(),
64
+ // Production URL is auto-detected from export/package.json name
65
+ // Override with: exportPlugin({ production: "https://custom.workers.dev" })
67
66
  ],
68
67
  });
69
68
  ```
70
69
 
70
+ **Development** (`npm run dev`):
71
+ 1. Automatically starts Wrangler dev server
72
+ 2. Waits for it to be ready
73
+ 3. Transforms `export:/` imports to `http://localhost:8787`
74
+
75
+ **Production** (`npm run export`):
76
+ 1. Builds Vite app
77
+ 2. Deploys to Workers Sites (static assets + server exports)
78
+ 3. `export:/` imports resolve to `https://{worker-name}.workers.dev`
79
+
71
80
  ## Project Structure
72
81
 
73
82
  After running `exportc init`:
@@ -23,27 +23,54 @@ export async function deploy(argv) {
23
23
 
24
24
  p.intro(pc.bgCyan(pc.black(" exportc deploy ")));
25
25
 
26
- const s = p.spinner();
27
- s.start("Deploying to Cloudflare Workers...");
26
+ // Step 1: Build Vite
27
+ const s1 = p.spinner();
28
+ s1.start("Building with Vite...");
28
29
 
29
- // Run deploy
30
- const deployProcess = spawn("npm", ["run", "deploy"], {
30
+ const viteBuild = spawn("npm", ["run", "build"], {
31
+ cwd,
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ shell: true,
34
+ });
35
+
36
+ let viteBuildOutput = "";
37
+ viteBuild.stdout.on("data", (data) => { viteBuildOutput += data.toString(); });
38
+ viteBuild.stderr.on("data", (data) => { viteBuildOutput += data.toString(); });
39
+
40
+ const viteExitCode = await new Promise((resolve) => {
41
+ viteBuild.on("close", resolve);
42
+ viteBuild.on("error", () => resolve(1));
43
+ });
44
+
45
+ if (viteExitCode !== 0) {
46
+ s1.stop("Vite build failed");
47
+ console.error(viteBuildOutput);
48
+ process.exit(1);
49
+ }
50
+
51
+ s1.stop("Vite build complete");
52
+
53
+ // Step 2: Deploy to Cloudflare Workers
54
+ const s2 = p.spinner();
55
+ s2.start("Deploying to Cloudflare Workers...");
56
+
57
+ const wranglerDeploy = spawn("npm", ["run", "deploy"], {
31
58
  cwd: exportDir,
32
59
  stdio: "inherit",
33
60
  shell: true,
34
61
  });
35
62
 
36
- const exitCode = await new Promise((resolve) => {
37
- deployProcess.on("close", resolve);
38
- deployProcess.on("error", () => resolve(1));
63
+ const wranglerExitCode = await new Promise((resolve) => {
64
+ wranglerDeploy.on("close", resolve);
65
+ wranglerDeploy.on("error", () => resolve(1));
39
66
  });
40
67
 
41
- if (exitCode !== 0) {
42
- s.stop("Deployment failed");
68
+ if (wranglerExitCode !== 0) {
69
+ s2.stop("Deployment failed");
43
70
  process.exit(1);
44
71
  }
45
72
 
46
- s.stop("Deployed successfully!");
73
+ s2.stop("Deployed successfully!");
47
74
 
48
75
  // Read worker name from export/package.json
49
76
  const exportPkgPath = path.join(exportDir, "package.json");
@@ -51,13 +78,16 @@ export async function deploy(argv) {
51
78
  const workerName = exportPkg.name;
52
79
 
53
80
  p.note(
54
- `Your exports are now live at:
81
+ `Your app is now live at:
55
82
  ${pc.cyan(`https://${workerName}.workers.dev/`)}
56
83
 
57
- ${pc.bold("Update your Vite config for production:")}
58
- ${pc.dim("// vite.config.ts")}
59
- ${pc.cyan(`exportPlugin({ production: "https://${workerName}.workers.dev" })`)}`,
60
- "Deployed"
84
+ ${pc.bold("What was deployed:")}
85
+ - Static assets (Vite build output)
86
+ - Server exports (export/ directory)
87
+
88
+ ${pc.bold("Client imports will resolve to:")}
89
+ ${pc.cyan(`https://${workerName}.workers.dev/`)}`,
90
+ "Workers Sites"
61
91
  );
62
92
 
63
93
  p.outro("Done!");
package/commands/init.js CHANGED
@@ -132,6 +132,15 @@ export class Counter {
132
132
  fs.writeFileSync(indexPath, template);
133
133
  }
134
134
 
135
+ // Detect Vite build output directory from vite.config
136
+ let viteBuildOutDir = "dist"; // default
137
+ const viteConfigPath = path.join(cwd, viteConfig);
138
+ let viteConfigContent = fs.readFileSync(viteConfigPath, "utf8");
139
+ const outDirMatch = viteConfigContent.match(/outDir:\s*['"]([^'"]+)['"]/);
140
+ if (outDirMatch) {
141
+ viteBuildOutDir = outDirMatch[1];
142
+ }
143
+
135
144
  // Create export/package.json
136
145
  const exportPkgPath = path.join(exportDir, "package.json");
137
146
  const exportPkg = {
@@ -139,6 +148,7 @@ export class Counter {
139
148
  private: true,
140
149
  type: "module",
141
150
  exports: "./",
151
+ main: `../${viteBuildOutDir}`, // Static assets from Vite build
142
152
  scripts: {
143
153
  dev: "generate-export-types && wrangler dev",
144
154
  deploy: "generate-export-types && wrangler deploy",
@@ -157,26 +167,23 @@ export class Counter {
157
167
 
158
168
  fs.writeFileSync(exportPkgPath, JSON.stringify(exportPkg, null, 2) + "\n");
159
169
 
160
- // Update main package.json scripts
170
+ // Update main package.json
161
171
  pkg.scripts = pkg.scripts || {};
162
- if (!pkg.scripts["export:dev"]) {
163
- pkg.scripts["export:dev"] = "cd export && npm run dev";
164
- }
165
- if (!pkg.scripts["export:deploy"]) {
166
- pkg.scripts["export:deploy"] = "cd export && npm run deploy";
172
+
173
+ // Add export script: build + deploy as Workers Sites
174
+ if (!pkg.scripts["export"]) {
175
+ pkg.scripts["export"] = "vite build && cd export && npm run deploy";
167
176
  }
168
177
 
169
178
  // Add exportc to devDependencies
170
179
  pkg.devDependencies = pkg.devDependencies || {};
171
180
  if (!pkg.devDependencies.exportc) {
172
- pkg.devDependencies.exportc = "^0.0.1";
181
+ pkg.devDependencies.exportc = "^0.0.2";
173
182
  }
174
183
 
175
184
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
176
185
 
177
186
  // Update vite.config
178
- const viteConfigPath = path.join(cwd, viteConfig);
179
- let viteConfigContent = fs.readFileSync(viteConfigPath, "utf8");
180
187
 
181
188
  // Check if export plugin is already added
182
189
  if (!viteConfigContent.includes("exportc/vite")) {
@@ -293,16 +300,19 @@ ${pc.cyan("export-env.d.ts")} ${pc.dim("# Type declarations for export:/ imp
293
300
 
294
301
  ${pc.bold("Next steps:")}
295
302
 
296
- 1. Install dependencies:
297
- ${pc.cyan("cd export && npm install")}
303
+ 1. Install export dependencies:
304
+ ${pc.cyan("cd export && npm install && cd ..")}
298
305
 
299
- 2. Start development:
300
- ${pc.cyan("npm run export:dev")} ${pc.dim("# In one terminal")}
301
- ${pc.cyan("npm run dev")} ${pc.dim("# In another terminal")}
306
+ 2. Start development (Vite + Wrangler auto-start):
307
+ ${pc.cyan("npm run dev")}
302
308
 
303
309
  3. Import in your client code:
304
310
  ${pc.cyan(`import { hello } from "export:/";`)}
305
- ${pc.cyan(`const message = await hello("World");`)}`,
311
+ ${pc.cyan(`const message = await hello("World");`)}
312
+
313
+ 4. Deploy to Cloudflare Workers Sites:
314
+ ${pc.cyan("npm run export")}
315
+ ${pc.dim("# Builds Vite + deploys everything to Workers")}`,
306
316
  "Created"
307
317
  );
308
318
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exportc",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "CLI to add export to existing projects",
5
5
  "scripts": {
6
6
  "test": "node --test test/*.test.mjs"
package/vite-plugin.d.ts CHANGED
@@ -7,18 +7,38 @@ export interface ExportPluginOptions {
7
7
  */
8
8
  dev?: string;
9
9
 
10
+ /**
11
+ * Development server port
12
+ * @default 8787
13
+ */
14
+ port?: number;
15
+
10
16
  /**
11
17
  * Production Worker URL
12
18
  * Required for production builds
13
19
  * @example "https://my-api.workers.dev"
14
20
  */
15
21
  production?: string;
22
+
23
+ /**
24
+ * Export directory path (relative to project root)
25
+ * @default "./export"
26
+ */
27
+ exportDir?: string;
28
+
29
+ /**
30
+ * Auto-start Wrangler dev server when running Vite in dev mode
31
+ * @default true
32
+ */
33
+ autoStart?: boolean;
16
34
  }
17
35
 
18
36
  /**
19
37
  * Vite plugin for export integration
20
38
  *
21
- * Allows importing server exports using the "export:/" prefix:
39
+ * Automatically starts Wrangler when you run `npm run dev` and allows
40
+ * importing server exports using the "export:/" prefix:
41
+ *
22
42
  * ```ts
23
43
  * import { hello } from "export:/";
24
44
  * import { utils } from "export:/utils";
@@ -31,6 +51,7 @@ export interface ExportPluginOptions {
31
51
  * export default defineConfig({
32
52
  * plugins: [
33
53
  * exportPlugin({
54
+ * // Required for production builds
34
55
  * production: "https://my-api.workers.dev"
35
56
  * })
36
57
  * ]
package/vite-plugin.js CHANGED
@@ -5,51 +5,195 @@
5
5
  * import { hello } from "export:/";
6
6
  * import { utils } from "export:/utils";
7
7
  *
8
- * In development, proxies to local Wrangler dev server (localhost:8787)
9
- * In production, resolves to the deployed Worker URL
8
+ * In development:
9
+ * - Automatically starts Wrangler dev server
10
+ * - Proxies to local Wrangler (localhost:8787)
11
+ *
12
+ * In production:
13
+ * - Resolves to the deployed Worker URL
10
14
  */
11
15
 
16
+ import { spawn } from "node:child_process";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { resolve } from "node:path";
19
+
12
20
  const EXPORT_PREFIX = "export:";
13
- const DEFAULT_DEV_URL = "http://localhost:8787";
21
+ const DEFAULT_DEV_PORT = 8787;
14
22
 
15
23
  /**
16
24
  * @param {Object} options
17
25
  * @param {string} [options.dev] - Development server URL (default: http://localhost:8787)
18
- * @param {string} [options.production] - Production Worker URL (required for production builds)
26
+ * @param {string} [options.production] - Production Worker URL (auto-detected from export/package.json name if not specified)
27
+ * @param {string} [options.exportDir] - Export directory (default: ./export)
28
+ * @param {boolean} [options.autoStart] - Auto-start Wrangler in dev mode (default: true)
19
29
  * @returns {import('vite').Plugin}
20
30
  */
21
31
  export function exportPlugin(options = {}) {
22
- const devUrl = options.dev || DEFAULT_DEV_URL;
23
- const prodUrl = options.production;
32
+ const devPort = options.port || DEFAULT_DEV_PORT;
33
+ const devUrl = options.dev || `http://localhost:${devPort}`;
34
+ let prodUrl = options.production;
35
+ const exportDir = options.exportDir || "./export";
36
+ const autoStart = options.autoStart !== false;
24
37
 
25
38
  let isDev = true;
39
+ let wranglerProcess = null;
40
+ let wranglerReady = false;
41
+ let wranglerReadyPromise = null;
42
+
43
+ // Auto-detect production URL from export/package.json
44
+ const detectProductionUrl = (root) => {
45
+ if (prodUrl) return prodUrl;
46
+ try {
47
+ const exportPkgPath = resolve(root, exportDir, "package.json");
48
+ if (existsSync(exportPkgPath)) {
49
+ const exportPkg = JSON.parse(readFileSync(exportPkgPath, "utf8"));
50
+ if (exportPkg.name) {
51
+ return `https://${exportPkg.name}.workers.dev`;
52
+ }
53
+ }
54
+ } catch {}
55
+ return null;
56
+ };
57
+
58
+ const startWrangler = (root) => {
59
+ const exportPath = resolve(root, exportDir);
60
+
61
+ if (!existsSync(exportPath)) {
62
+ console.warn(`[exportc] Export directory not found: ${exportPath}`);
63
+ console.warn(`[exportc] Run 'npx exportc init' to initialize.`);
64
+ return Promise.resolve();
65
+ }
66
+
67
+ const nodeModulesPath = resolve(exportPath, "node_modules");
68
+ if (!existsSync(nodeModulesPath)) {
69
+ console.warn(`[exportc] Dependencies not installed in ${exportPath}`);
70
+ console.warn(`[exportc] Run 'cd ${exportDir} && npm install' first.`);
71
+ return Promise.resolve();
72
+ }
73
+
74
+ return new Promise((resolveReady) => {
75
+ console.log(`[exportc] Starting Wrangler dev server...`);
76
+
77
+ // Generate types first, then start wrangler
78
+ const generateTypes = spawn("npm", ["run", "dev"], {
79
+ cwd: exportPath,
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ shell: true,
82
+ });
83
+
84
+ wranglerProcess = generateTypes;
85
+
86
+ generateTypes.stdout.on("data", (data) => {
87
+ const output = data.toString();
88
+ process.stdout.write(`[export] ${output}`);
89
+
90
+ // Detect when wrangler is ready
91
+ if (output.includes("Ready on") || output.includes("Listening on") || output.includes("localhost")) {
92
+ if (!wranglerReady) {
93
+ wranglerReady = true;
94
+ console.log(`[exportc] Wrangler ready at ${devUrl}`);
95
+ resolveReady();
96
+ }
97
+ }
98
+ });
99
+
100
+ generateTypes.stderr.on("data", (data) => {
101
+ const output = data.toString();
102
+ // Filter out noisy warnings
103
+ if (!output.includes("ExperimentalWarning")) {
104
+ process.stderr.write(`[export] ${output}`);
105
+ }
106
+ // Also check stderr for ready message
107
+ if (output.includes("Ready on") || output.includes("Listening on") || output.includes("localhost:8787")) {
108
+ if (!wranglerReady) {
109
+ wranglerReady = true;
110
+ console.log(`[exportc] Wrangler ready at ${devUrl}`);
111
+ resolveReady();
112
+ }
113
+ }
114
+ });
115
+
116
+ generateTypes.on("error", (err) => {
117
+ console.error(`[exportc] Failed to start Wrangler:`, err.message);
118
+ resolveReady();
119
+ });
120
+
121
+ generateTypes.on("close", (code) => {
122
+ if (code !== 0 && code !== null) {
123
+ console.error(`[exportc] Wrangler exited with code ${code}`);
124
+ }
125
+ wranglerProcess = null;
126
+ resolveReady();
127
+ });
128
+
129
+ // Timeout after 30 seconds
130
+ setTimeout(() => {
131
+ if (!wranglerReady) {
132
+ console.warn(`[exportc] Wrangler startup timeout, continuing anyway...`);
133
+ wranglerReady = true;
134
+ resolveReady();
135
+ }
136
+ }, 30000);
137
+ });
138
+ };
139
+
140
+ const stopWrangler = () => {
141
+ if (wranglerProcess) {
142
+ console.log(`[exportc] Stopping Wrangler...`);
143
+ wranglerProcess.kill("SIGTERM");
144
+ wranglerProcess = null;
145
+ wranglerReady = false;
146
+ }
147
+ };
26
148
 
27
149
  return {
28
150
  name: "vite-plugin-export",
29
151
 
30
152
  config(config, { command }) {
31
153
  isDev = command === "serve";
154
+ // Auto-detect production URL if not specified
155
+ if (!isDev && !prodUrl) {
156
+ const root = config.root || process.cwd();
157
+ prodUrl = detectProductionUrl(root);
158
+ }
159
+ return {};
160
+ },
161
+
162
+ configureServer(server) {
163
+ if (isDev && autoStart) {
164
+ const root = server.config.root || process.cwd();
165
+ wranglerReadyPromise = startWrangler(root);
32
166
 
33
- // Add proxy for development
167
+ // Cleanup on server close
168
+ server.httpServer?.on("close", stopWrangler);
169
+ }
170
+ },
171
+
172
+ async buildStart() {
34
173
  if (isDev) {
35
- return {
36
- server: {
37
- proxy: {
38
- // Proxy WebSocket connections
39
- "^/__export_ws__": {
40
- target: devUrl.replace(/^http/, "ws"),
41
- ws: true,
42
- rewrite: (path) => path.replace("/__export_ws__", ""),
43
- },
44
- },
45
- },
46
- };
174
+ if (autoStart && wranglerReadyPromise) {
175
+ await wranglerReadyPromise;
176
+ }
177
+ this.info(`Development mode - using ${devUrl}`);
178
+ } else if (prodUrl) {
179
+ this.info(`Production mode - using ${prodUrl}`);
180
+ } else {
181
+ this.warn(`Production URL not configured. Add 'production' option to exportPlugin().`);
47
182
  }
48
183
  },
49
184
 
185
+ buildEnd() {
186
+ if (!isDev) {
187
+ stopWrangler();
188
+ }
189
+ },
190
+
191
+ closeBundle() {
192
+ stopWrangler();
193
+ },
194
+
50
195
  resolveId(source) {
51
196
  if (source.startsWith(EXPORT_PREFIX)) {
52
- // Mark as external - we'll handle it in load
53
197
  return { id: source, external: false };
54
198
  }
55
199
  return null;
@@ -60,28 +204,20 @@ export function exportPlugin(options = {}) {
60
204
  return null;
61
205
  }
62
206
 
63
- // Extract the path after "export:"
64
207
  const exportPath = id.slice(EXPORT_PREFIX.length) || "/";
65
-
66
- // In development, use the local dev server
67
- // In production, use the configured production URL
68
208
  const baseUrl = isDev ? devUrl : prodUrl;
69
209
 
70
210
  if (!baseUrl) {
71
211
  if (!isDev) {
72
212
  this.error(
73
- `[exportc] Production URL not configured. Add { production: "https://your-worker.workers.dev" } to exportPlugin() in vite.config.`
213
+ `[exportc] Production URL not configured. Add { production: "https://your-worker.workers.dev" } to exportPlugin().`
74
214
  );
75
215
  }
76
216
  this.error(`[exportc] Could not resolve base URL for export imports.`);
77
217
  }
78
218
 
79
- // Build the full URL
80
219
  const fullUrl = new URL(exportPath, baseUrl).href;
81
220
 
82
- // Generate a module that re-exports from the Worker URL
83
- // For development, we dynamically import from the Worker
84
- // For production, we use the deployed URL
85
221
  const code = `
86
222
  // Auto-generated by exportc/vite
87
223
  // Importing from: ${fullUrl}
@@ -92,24 +228,18 @@ export { default } from "${fullUrl}";
92
228
  return code;
93
229
  },
94
230
 
95
- // Transform import statements in the final bundle for production
96
231
  transform(code, id) {
97
- // Skip node_modules and non-JS files
98
232
  if (id.includes("node_modules") || !/\.(js|ts|jsx|tsx|vue|svelte)$/.test(id)) {
99
233
  return null;
100
234
  }
101
235
 
102
- // Replace dynamic imports of export: URLs
103
236
  if (code.includes('import("export:') || code.includes("import('export:")) {
104
237
  const baseUrl = isDev ? devUrl : prodUrl;
105
238
  if (!baseUrl && !isDev) {
106
- this.warn(
107
- `[exportc] Production URL not configured for dynamic imports in ${id}`
108
- );
239
+ this.warn(`[exportc] Production URL not configured for dynamic imports in ${id}`);
109
240
  return null;
110
241
  }
111
242
 
112
- // Replace export:/ with the actual URL
113
243
  const transformed = code.replace(
114
244
  /import\((['"])export:([^'"]*)\1\)/g,
115
245
  (match, quote, path) => {
@@ -125,19 +255,6 @@ export { default } from "${fullUrl}";
125
255
 
126
256
  return null;
127
257
  },
128
-
129
- // Generate TypeScript declarations hint
130
- buildStart() {
131
- if (isDev) {
132
- this.info(
133
- `[exportc] Development mode - proxying to ${devUrl}`
134
- );
135
- } else if (prodUrl) {
136
- this.info(
137
- `[exportc] Production mode - using ${prodUrl}`
138
- );
139
- }
140
- },
141
258
  };
142
259
  }
143
260