exportc 0.0.2 → 0.0.4

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
@@ -13,15 +13,18 @@ cd export && npm install && cd ..
13
13
 
14
14
  # Start development (Wrangler starts automatically!)
15
15
  npm run dev
16
+
17
+ # Deploy to Cloudflare Workers Sites
18
+ npm run export
16
19
  ```
17
20
 
18
21
  ## Usage
19
22
 
20
- After initialization, import your server exports using the `export:/` prefix:
23
+ After initialization, import your server exports using the `export/` prefix:
21
24
 
22
25
  ```typescript
23
26
  // In your Vite app
24
- import { hello, Counter } from "export:/";
27
+ import { hello, Counter } from "export/";
25
28
 
26
29
  const message = await hello("World"); // "Hello, World!"
27
30
 
@@ -48,7 +51,7 @@ Deploy your exports to Cloudflare Workers.
48
51
 
49
52
  ## Vite Plugin
50
53
 
51
- The `exportPlugin` automatically starts Wrangler and transforms `export:/` imports:
54
+ The `exportPlugin` automatically starts Wrangler and transforms `export/` imports:
52
55
 
53
56
  ```typescript
54
57
  // vite.config.ts
@@ -57,22 +60,22 @@ import { exportPlugin } from "exportc/vite";
57
60
 
58
61
  export default defineConfig({
59
62
  plugins: [
60
- exportPlugin({
61
- // Production Worker URL (required for production builds)
62
- production: "https://my-api.workers.dev",
63
-
64
- // Optional: customize dev server
65
- // port: 8787,
66
- // autoStart: true, // Auto-start Wrangler (default: true)
67
- }),
63
+ exportPlugin(),
64
+ // Production URL is auto-detected from export/package.json name
65
+ // Override with: exportPlugin({ production: "https://custom.workers.dev" })
68
66
  ],
69
67
  });
70
68
  ```
71
69
 
72
- When you run `npm run dev`, the plugin:
70
+ **Development** (`npm run dev`):
73
71
  1. Automatically starts Wrangler dev server
74
72
  2. Waits for it to be ready
75
- 3. Transforms `export:/` imports to point to the local server
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`
76
79
 
77
80
  ## Project Structure
78
81
 
@@ -90,20 +93,26 @@ my-vite-app/
90
93
 
91
94
  ## TypeScript Support
92
95
 
93
- The `export-env.d.ts` file provides type declarations for `export:/` imports. Update it when you add new exports:
96
+ The `export-env.d.ts` file is **automatically generated** when you run `npm run dev`. The Vite plugin watches for changes to your export files and regenerates type declarations automatically.
94
97
 
95
98
  ```typescript
96
- // export-env.d.ts
97
- declare module "export:/" {
99
+ // export-env.d.ts (auto-generated)
100
+ declare module "export/" {
98
101
  export function hello(name: string): Promise<string>;
99
- export function myNewFunction(): Promise<void>;
102
+ export class Counter {
103
+ constructor(initial?: number);
104
+ increment(): Promise<number>;
105
+ [Symbol.dispose](): Promise<void>;
106
+ }
100
107
  }
101
108
 
102
- declare module "export:/utils" {
109
+ declare module "export/utils" {
103
110
  export function formatDate(date: Date): Promise<string>;
104
111
  }
105
112
  ```
106
113
 
114
+ Types are inferred from your actual export code, so you get accurate type information with zero manual maintenance.
115
+
107
116
  ## License
108
117
 
109
118
  MIT
@@ -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/dev.js CHANGED
@@ -45,7 +45,7 @@ export async function dev(argv) {
45
45
  `Wrangler dev server is running at ${pc.cyan("http://localhost:8787")}
46
46
 
47
47
  ${pc.bold("In your Vite app:")}
48
- ${pc.cyan(`import { hello } from "export:/";`)}
48
+ ${pc.cyan(`import { hello } from "export/";`)}
49
49
  ${pc.cyan(`const message = await hello("World");`)}
50
50
 
51
51
  ${pc.dim("Press Ctrl+C to stop")}`,
package/commands/init.js CHANGED
@@ -76,7 +76,7 @@ export async function init(argv) {
76
76
  if (!fs.existsSync(indexPath)) {
77
77
  const template = isTypeScript
78
78
  ? `// Server-side exports - these will be available to your client code
79
- // Import from "export:/" in your client code
79
+ // Import from "export/" in your client code
80
80
 
81
81
  export async function hello(name: string): Promise<string> {
82
82
  return \`Hello, \${name}!\`;
@@ -103,7 +103,7 @@ export class Counter {
103
103
  }
104
104
  `
105
105
  : `// Server-side exports - these will be available to your client code
106
- // Import from "export:/" in your client code
106
+ // Import from "export/" in your client code
107
107
 
108
108
  export async function hello(name) {
109
109
  return \`Hello, \${name}!\`;
@@ -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",
@@ -160,25 +170,20 @@ export class Counter {
160
170
  // Update main package.json
161
171
  pkg.scripts = pkg.scripts || {};
162
172
 
163
- // Add deploy script for export
164
- if (!pkg.scripts["deploy"]) {
165
- pkg.scripts["deploy"] = "cd export && npm run deploy";
166
- } else if (!pkg.scripts["deploy"].includes("export")) {
167
- // Add export deploy to existing deploy script
168
- pkg.scripts["deploy:export"] = "cd export && npm run deploy";
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";
169
176
  }
170
177
 
171
178
  // Add exportc to devDependencies
172
179
  pkg.devDependencies = pkg.devDependencies || {};
173
180
  if (!pkg.devDependencies.exportc) {
174
- pkg.devDependencies.exportc = "^0.0.1";
181
+ pkg.devDependencies.exportc = "^0.0.2";
175
182
  }
176
183
 
177
184
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
178
185
 
179
186
  // Update vite.config
180
- const viteConfigPath = path.join(cwd, viteConfig);
181
- let viteConfigContent = fs.readFileSync(viteConfigPath, "utf8");
182
187
 
183
188
  // Check if export plugin is already added
184
189
  if (!viteConfigContent.includes("exportc/vite")) {
@@ -232,13 +237,13 @@ wrangler.toml
232
237
  if (isTypeScript) {
233
238
  const envDtsPath = path.join(cwd, "export-env.d.ts");
234
239
  if (!fs.existsSync(envDtsPath)) {
235
- const envDtsContent = `// Type declarations for export:/ imports
240
+ const envDtsContent = `// Type declarations for export/ imports
236
241
  // This file is auto-generated by exportc. You can modify it to add custom types.
237
242
 
238
243
  // Re-export types from your export directory
239
244
  // Update this when you add new exports
240
245
 
241
- declare module "export:/" {
246
+ declare module "export/" {
242
247
  export function hello(name: string): Promise<string>;
243
248
  export function add(a: number, b: number): Promise<number>;
244
249
  export class Counter {
@@ -250,7 +255,7 @@ declare module "export:/" {
250
255
  }
251
256
 
252
257
  // Add more module declarations for subpaths:
253
- // declare module "export:/utils" {
258
+ // declare module "export/utils" {
254
259
  // export function myUtil(): Promise<void>;
255
260
  // }
256
261
  `;
@@ -284,7 +289,7 @@ declare module "export:/" {
284
289
  ├── package.json ${pc.dim("# Worker configuration")}
285
290
  └── .gitignore
286
291
 
287
- ${pc.cyan("export-env.d.ts")} ${pc.dim("# Type declarations for export:/ imports")}`
292
+ ${pc.cyan("export-env.d.ts")} ${pc.dim("# Type declarations for export/ imports")}`
288
293
  : `${pc.cyan("export/")}
289
294
  ├── index.${ext} ${pc.dim("# Your server exports")}
290
295
  ├── package.json ${pc.dim("# Worker configuration")}
@@ -302,11 +307,12 @@ ${pc.bold("Next steps:")}
302
307
  ${pc.cyan("npm run dev")}
303
308
 
304
309
  3. Import in your client code:
305
- ${pc.cyan(`import { hello } from "export:/";`)}
310
+ ${pc.cyan(`import { hello } from "export/";`)}
306
311
  ${pc.cyan(`const message = await hello("World");`)}
307
312
 
308
- 4. Deploy to Cloudflare:
309
- ${pc.cyan("npm run deploy")}`,
313
+ 4. Deploy to Cloudflare Workers Sites:
314
+ ${pc.cyan("npm run export")}
315
+ ${pc.dim("# Builds Vite + deploys everything to Workers")}`,
310
316
  "Created"
311
317
  );
312
318
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exportc",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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
@@ -37,11 +37,11 @@ export interface ExportPluginOptions {
37
37
  * Vite plugin for export integration
38
38
  *
39
39
  * Automatically starts Wrangler when you run `npm run dev` and allows
40
- * importing server exports using the "export:/" prefix:
40
+ * importing server exports using the "export/" prefix:
41
41
  *
42
42
  * ```ts
43
- * import { hello } from "export:/";
44
- * import { utils } from "export:/utils";
43
+ * import { hello } from "export/";
44
+ * import { utils } from "export/utils";
45
45
  * ```
46
46
  *
47
47
  * @example
package/vite-plugin.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Vite plugin for export integration
3
3
  *
4
- * Allows importing server exports using the "export:/" prefix:
5
- * import { hello } from "export:/";
6
- * import { utils } from "export:/utils";
4
+ * Allows importing server exports using the "export/" prefix:
5
+ * import { hello } from "export/";
6
+ * import { utils } from "export/utils";
7
7
  *
8
8
  * In development:
9
9
  * - Automatically starts Wrangler dev server
@@ -14,16 +14,16 @@
14
14
  */
15
15
 
16
16
  import { spawn } from "node:child_process";
17
- import { existsSync } from "node:fs";
17
+ import { existsSync, readFileSync, writeFileSync, watchFile, unwatchFile } from "node:fs";
18
18
  import { resolve } from "node:path";
19
19
 
20
- const EXPORT_PREFIX = "export:";
20
+ const EXPORT_PREFIX = "export/";
21
21
  const DEFAULT_DEV_PORT = 8787;
22
22
 
23
23
  /**
24
24
  * @param {Object} options
25
25
  * @param {string} [options.dev] - Development server URL (default: http://localhost:8787)
26
- * @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
27
  * @param {string} [options.exportDir] - Export directory (default: ./export)
28
28
  * @param {boolean} [options.autoStart] - Auto-start Wrangler in dev mode (default: true)
29
29
  * @returns {import('vite').Plugin}
@@ -31,7 +31,7 @@ const DEFAULT_DEV_PORT = 8787;
31
31
  export function exportPlugin(options = {}) {
32
32
  const devPort = options.port || DEFAULT_DEV_PORT;
33
33
  const devUrl = options.dev || `http://localhost:${devPort}`;
34
- const prodUrl = options.production;
34
+ let prodUrl = options.production;
35
35
  const exportDir = options.exportDir || "./export";
36
36
  const autoStart = options.autoStart !== false;
37
37
 
@@ -40,6 +40,59 @@ export function exportPlugin(options = {}) {
40
40
  let wranglerReady = false;
41
41
  let wranglerReadyPromise = null;
42
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
+ // Generate export-env.d.ts from .export-types.js
59
+ const generateTypeDeclarations = (root) => {
60
+ try {
61
+ const typesFilePath = resolve(root, exportDir, ".export-types.js");
62
+ if (!existsSync(typesFilePath)) return;
63
+
64
+ const typesContent = readFileSync(typesFilePath, "utf8");
65
+ // Extract the first line which contains the default export
66
+ const firstLine = typesContent.split('\n')[0];
67
+ if (!firstLine.startsWith('export default ')) return;
68
+
69
+ // Remove "export default " prefix and trailing ";"
70
+ const jsonStr = firstLine.slice('export default '.length).replace(/;$/, '');
71
+
72
+ // Parse the types map
73
+ const typesMap = JSON.parse(jsonStr);
74
+
75
+ // Generate declare module statements
76
+ const declarations = ['// Auto-generated by exportc. Do not edit manually.', '// Re-run "npm run dev" to regenerate after changing export/ files.', ''];
77
+
78
+ for (const [route, types] of Object.entries(typesMap)) {
79
+ const modulePath = route === "" ? "export/" : `export/${route}`;
80
+ declarations.push(`declare module "${modulePath}" {`);
81
+ // Indent the type declarations
82
+ const indented = types.split('\n').map(line => line ? ` ${line}` : '').join('\n');
83
+ declarations.push(indented);
84
+ declarations.push('}');
85
+ declarations.push('');
86
+ }
87
+
88
+ const envDtsPath = resolve(root, "export-env.d.ts");
89
+ writeFileSync(envDtsPath, declarations.join('\n'));
90
+ console.log(`[exportc] Generated type declarations: export-env.d.ts`);
91
+ } catch (err) {
92
+ console.warn(`[exportc] Failed to generate type declarations:`, err.message);
93
+ }
94
+ };
95
+
43
96
  const startWrangler = (root) => {
44
97
  const exportPath = resolve(root, exportDir);
45
98
 
@@ -136,6 +189,11 @@ export function exportPlugin(options = {}) {
136
189
 
137
190
  config(config, { command }) {
138
191
  isDev = command === "serve";
192
+ // Auto-detect production URL if not specified
193
+ if (!isDev && !prodUrl) {
194
+ const root = config.root || process.cwd();
195
+ prodUrl = detectProductionUrl(root);
196
+ }
139
197
  return {};
140
198
  },
141
199
 
@@ -144,8 +202,27 @@ export function exportPlugin(options = {}) {
144
202
  const root = server.config.root || process.cwd();
145
203
  wranglerReadyPromise = startWrangler(root);
146
204
 
205
+ // Generate types after Wrangler is ready
206
+ wranglerReadyPromise.then(() => {
207
+ // Initial generation
208
+ setTimeout(() => generateTypeDeclarations(root), 1000);
209
+
210
+ // Watch for changes to .export-types.js
211
+ const typesFilePath = resolve(root, exportDir, ".export-types.js");
212
+ if (existsSync(typesFilePath)) {
213
+ watchFile(typesFilePath, { interval: 1000 }, () => {
214
+ generateTypeDeclarations(root);
215
+ });
216
+ }
217
+ });
218
+
147
219
  // Cleanup on server close
148
- server.httpServer?.on("close", stopWrangler);
220
+ server.httpServer?.on("close", () => {
221
+ stopWrangler();
222
+ // Stop watching
223
+ const typesFilePath = resolve(root, exportDir, ".export-types.js");
224
+ unwatchFile(typesFilePath);
225
+ });
149
226
  }
150
227
  },
151
228
 
@@ -213,7 +290,7 @@ export { default } from "${fullUrl}";
213
290
  return null;
214
291
  }
215
292
 
216
- if (code.includes('import("export:') || code.includes("import('export:")) {
293
+ if (code.includes('import("export/') || code.includes("import('export/")) {
217
294
  const baseUrl = isDev ? devUrl : prodUrl;
218
295
  if (!baseUrl && !isDev) {
219
296
  this.warn(`[exportc] Production URL not configured for dynamic imports in ${id}`);
@@ -221,7 +298,7 @@ export { default } from "${fullUrl}";
221
298
  }
222
299
 
223
300
  const transformed = code.replace(
224
- /import\((['"])export:([^'"]*)\1\)/g,
301
+ /import\((['"])export\/([^'"]*)\1\)/g,
225
302
  (match, quote, path) => {
226
303
  const fullUrl = new URL(path || "/", baseUrl).href;
227
304
  return `import(${quote}${fullUrl}${quote})`;