@voltkit/create-volt 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.
Files changed (52) hide show
  1. package/dist/__tests__/index.test.d.ts +1 -0
  2. package/dist/__tests__/index.test.js +158 -0
  3. package/dist/config-updater.d.ts +1 -0
  4. package/dist/config-updater.js +267 -0
  5. package/dist/create-project.d.ts +2 -0
  6. package/dist/create-project.js +46 -0
  7. package/dist/index.d.ts +11 -0
  8. package/dist/index.js +65 -0
  9. package/dist/options.d.ts +8 -0
  10. package/dist/options.js +32 -0
  11. package/dist/templates/enterprise-ts/index.html +17 -0
  12. package/dist/templates/enterprise-ts/package.json +21 -0
  13. package/dist/templates/enterprise-ts/src/backend.ts +3 -0
  14. package/dist/templates/enterprise-ts/src/main.ts +9 -0
  15. package/dist/templates/enterprise-ts/src/style.css +50 -0
  16. package/dist/templates/enterprise-ts/tsconfig.json +12 -0
  17. package/dist/templates/enterprise-ts/volt.config.ts +26 -0
  18. package/dist/templates/react-ts/index.html +12 -0
  19. package/dist/templates/react-ts/package.json +23 -0
  20. package/dist/templates/react-ts/src/App.tsx +15 -0
  21. package/dist/templates/react-ts/src/backend.ts +3 -0
  22. package/dist/templates/react-ts/src/main.tsx +10 -0
  23. package/dist/templates/react-ts/src/style.css +50 -0
  24. package/dist/templates/react-ts/tsconfig.json +13 -0
  25. package/dist/templates/react-ts/vite.config.ts +6 -0
  26. package/dist/templates/react-ts/volt.config.ts +12 -0
  27. package/dist/templates/svelte-ts/index.html +12 -0
  28. package/dist/templates/svelte-ts/package.json +20 -0
  29. package/dist/templates/svelte-ts/src/App.svelte +20 -0
  30. package/dist/templates/svelte-ts/src/backend.ts +3 -0
  31. package/dist/templates/svelte-ts/src/main.ts +7 -0
  32. package/dist/templates/svelte-ts/src/style.css +43 -0
  33. package/dist/templates/svelte-ts/tsconfig.json +12 -0
  34. package/dist/templates/svelte-ts/vite.config.ts +6 -0
  35. package/dist/templates/svelte-ts/volt.config.ts +12 -0
  36. package/dist/templates/vanilla-ts/index.html +17 -0
  37. package/dist/templates/vanilla-ts/package.json +18 -0
  38. package/dist/templates/vanilla-ts/src/backend.ts +3 -0
  39. package/dist/templates/vanilla-ts/src/main.ts +9 -0
  40. package/dist/templates/vanilla-ts/src/style.css +50 -0
  41. package/dist/templates/vanilla-ts/tsconfig.json +12 -0
  42. package/dist/templates/vanilla-ts/volt.config.ts +12 -0
  43. package/dist/templates/vue-ts/index.html +12 -0
  44. package/dist/templates/vue-ts/package.json +21 -0
  45. package/dist/templates/vue-ts/src/App.vue +24 -0
  46. package/dist/templates/vue-ts/src/backend.ts +3 -0
  47. package/dist/templates/vue-ts/src/main.ts +5 -0
  48. package/dist/templates/vue-ts/src/style.css +43 -0
  49. package/dist/templates/vue-ts/tsconfig.json +13 -0
  50. package/dist/templates/vue-ts/vite.config.ts +6 -0
  51. package/dist/templates/vue-ts/volt.config.ts +12 -0
  52. package/package.json +35 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,158 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { __testOnly } from '../index.js';
6
+ const tempDirs = [];
7
+ function createTempDir(prefix) {
8
+ const dir = mkdtempSync(join(tmpdir(), prefix));
9
+ tempDirs.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(() => {
13
+ while (tempDirs.length > 0) {
14
+ const dir = tempDirs.pop();
15
+ if (dir) {
16
+ rmSync(dir, { recursive: true, force: true });
17
+ }
18
+ }
19
+ });
20
+ describe('create-volt helpers', () => {
21
+ it('normalizes valid project names', () => {
22
+ expect(__testOnly.normalizeProjectName('My App')).toBe('my-app');
23
+ expect(__testOnly.normalizeProjectName('demo_app')).toBe('demo_app');
24
+ expect(__testOnly.normalizeProjectName('demo.app')).toBe('demo.app');
25
+ });
26
+ it('rejects invalid project names', () => {
27
+ expect(() => __testOnly.normalizeProjectName('')).toThrow('cannot be empty');
28
+ expect(() => __testOnly.normalizeProjectName('.')).toThrow('cannot be "." or ".."');
29
+ expect(() => __testOnly.normalizeProjectName('../bad')).toThrow('single directory segment');
30
+ expect(() => __testOnly.normalizeProjectName('Bad*Name')).toThrow('must be lowercase');
31
+ });
32
+ it('builds human-friendly display names', () => {
33
+ expect(__testOnly.toDisplayName('my-volt-app')).toBe('My Volt App');
34
+ expect(__testOnly.toDisplayName('todo_app')).toBe('Todo App');
35
+ });
36
+ it('escapes html entities in title content', () => {
37
+ expect(__testOnly.escapeHtml('<Volt & App>')).toBe('&lt;Volt &amp; App&gt;');
38
+ });
39
+ it('updates only top-level name and window.title fields in volt config templates', () => {
40
+ const input = `
41
+ import { defineConfig } from 'voltkit';
42
+
43
+ const metadata = {
44
+ name: 'metadata-name-should-stay',
45
+ title: 'metadata-title-should-stay',
46
+ };
47
+
48
+ export default defineConfig({
49
+ name: 'template-name',
50
+ version: '0.1.0',
51
+ window: {
52
+ title: 'template-title',
53
+ width: 800,
54
+ height: 600,
55
+ },
56
+ menu: {
57
+ title: 'menu-title-should-stay',
58
+ },
59
+ });
60
+ `.trim();
61
+ const updated = __testOnly.updateVoltConfigContent(input, 'My Demo App');
62
+ expect(updated).toContain(`name: "My Demo App"`);
63
+ expect(updated).toContain(`title: "My Demo App"`);
64
+ expect(updated).toContain(`name: 'metadata-name-should-stay'`);
65
+ expect(updated).toContain(`title: 'metadata-title-should-stay'`);
66
+ expect(updated).toContain(`title: 'menu-title-should-stay'`);
67
+ });
68
+ it('creates a project from templates and rewrites key files', async () => {
69
+ const workspaceRoot = createTempDir('create-volt-workspace-');
70
+ const previousCwd = process.cwd();
71
+ process.chdir(workspaceRoot);
72
+ try {
73
+ await __testOnly.createProject({
74
+ name: 'my-demo-app',
75
+ displayName: 'My Demo App',
76
+ framework: 'vanilla',
77
+ });
78
+ }
79
+ finally {
80
+ process.chdir(previousCwd);
81
+ }
82
+ const targetDir = resolve(workspaceRoot, 'my-demo-app');
83
+ const packageJson = JSON.parse(readFileSync(resolve(targetDir, 'package.json'), 'utf8'));
84
+ const config = readFileSync(resolve(targetDir, 'volt.config.ts'), 'utf8');
85
+ const html = readFileSync(resolve(targetDir, 'index.html'), 'utf8');
86
+ expect(packageJson.name).toBe('my-demo-app');
87
+ expect(config).toContain('name: "My Demo App"');
88
+ expect(config).toContain('title: "My Demo App"');
89
+ expect(html).toContain('<title>My Demo App</title>');
90
+ });
91
+ it('creates an enterprise starter with enterprise packaging defaults', async () => {
92
+ const workspaceRoot = createTempDir('create-volt-enterprise-workspace-');
93
+ const previousCwd = process.cwd();
94
+ process.chdir(workspaceRoot);
95
+ try {
96
+ await __testOnly.createProject({
97
+ name: 'enterprise-demo',
98
+ displayName: 'Enterprise Demo',
99
+ framework: 'enterprise',
100
+ });
101
+ }
102
+ finally {
103
+ process.chdir(previousCwd);
104
+ }
105
+ const targetDir = resolve(workspaceRoot, 'enterprise-demo');
106
+ const packageJson = JSON.parse(readFileSync(resolve(targetDir, 'package.json'), 'utf8'));
107
+ const config = readFileSync(resolve(targetDir, 'volt.config.ts'), 'utf8');
108
+ expect(packageJson.name).toBe('enterprise-demo');
109
+ expect(packageJson.scripts?.doctor).toBe('volt doctor');
110
+ expect(packageJson.scripts?.package).toBe('volt package');
111
+ expect(config).toContain('name: "Enterprise Demo"');
112
+ expect(config).toContain('title: "Enterprise Demo"');
113
+ expect(config).toContain('package: {');
114
+ expect(config).toContain("installMode: 'perMachine'");
115
+ expect(config).toContain('generateAdmx: true');
116
+ });
117
+ it('exits with code 1 when target directory already exists', async () => {
118
+ const workspaceRoot = createTempDir('create-volt-existing-');
119
+ const previousCwd = process.cwd();
120
+ mkdirSync(resolve(workspaceRoot, 'my-existing-app'), { recursive: true });
121
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code) => {
122
+ const normalized = code === undefined || code === null ? 0 : Number(code);
123
+ throw new Error(`__PROCESS_EXIT__${normalized}`);
124
+ }));
125
+ process.chdir(workspaceRoot);
126
+ try {
127
+ await expect(__testOnly.createProject({
128
+ name: 'my-existing-app',
129
+ displayName: 'My Existing App',
130
+ framework: 'vanilla',
131
+ })).rejects.toThrow('__PROCESS_EXIT__1');
132
+ }
133
+ finally {
134
+ process.chdir(previousCwd);
135
+ exitSpy.mockRestore();
136
+ }
137
+ });
138
+ it('exits with code 1 when framework template is missing', async () => {
139
+ const workspaceRoot = createTempDir('create-volt-missing-template-');
140
+ const previousCwd = process.cwd();
141
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code) => {
142
+ const normalized = code === undefined || code === null ? 0 : Number(code);
143
+ throw new Error(`__PROCESS_EXIT__${normalized}`);
144
+ }));
145
+ process.chdir(workspaceRoot);
146
+ try {
147
+ await expect(__testOnly.createProject({
148
+ name: 'bad-template-app',
149
+ displayName: 'Bad Template App',
150
+ framework: 'missing-template',
151
+ })).rejects.toThrow('__PROCESS_EXIT__1');
152
+ }
153
+ finally {
154
+ process.chdir(previousCwd);
155
+ exitSpy.mockRestore();
156
+ }
157
+ });
158
+ });
@@ -0,0 +1 @@
1
+ export declare function updateVoltConfigContent(configContent: string, displayName: string): string;
@@ -0,0 +1,267 @@
1
+ function isIdentifierStart(character) { return /^[A-Za-z_$]$/u.test(character); }
2
+ function isIdentifierPart(character) { return /^[A-Za-z0-9_$]$/u.test(character); }
3
+ function isWhitespace(character) {
4
+ return character === ' ' || character === '\t' || character === '\n' || character === '\r';
5
+ }
6
+ function skipQuotedString(source, startIndex) {
7
+ const quote = source[startIndex];
8
+ let index = startIndex + 1;
9
+ while (index < source.length) {
10
+ if (source[index] === '\\') {
11
+ index += 2;
12
+ continue;
13
+ }
14
+ if (source[index] === quote) {
15
+ return index + 1;
16
+ }
17
+ index += 1;
18
+ }
19
+ return source.length;
20
+ }
21
+ function skipLineComment(source, startIndex) {
22
+ let index = startIndex + 2;
23
+ while (index < source.length && source[index] !== '\n') {
24
+ index += 1;
25
+ }
26
+ return index;
27
+ }
28
+ function skipBlockComment(source, startIndex) {
29
+ let index = startIndex + 2;
30
+ while (index + 1 < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
31
+ index += 1;
32
+ }
33
+ return Math.min(index + 2, source.length);
34
+ }
35
+ function skipTokenIfStringOrComment(source, startIndex) {
36
+ const current = source[startIndex];
37
+ const next = source[startIndex + 1];
38
+ if (current === '\'' || current === '"' || current === '`') {
39
+ return skipQuotedString(source, startIndex);
40
+ }
41
+ if (current === '/' && next === '/') {
42
+ return skipLineComment(source, startIndex);
43
+ }
44
+ if (current === '/' && next === '*') {
45
+ return skipBlockComment(source, startIndex);
46
+ }
47
+ return null;
48
+ }
49
+ function skipWhitespaceAndComments(source, startIndex) {
50
+ let index = startIndex;
51
+ while (index < source.length) {
52
+ if (isWhitespace(source[index])) {
53
+ index += 1;
54
+ continue;
55
+ }
56
+ const nextIndex = skipTokenIfStringOrComment(source, index);
57
+ if (nextIndex === null || nextIndex <= index) {
58
+ return index;
59
+ }
60
+ if (source[index] === '\'' || source[index] === '"' || source[index] === '`') {
61
+ return index;
62
+ }
63
+ index = nextIndex;
64
+ }
65
+ return index;
66
+ }
67
+ function findMatchingBrace(source, openBraceIndex) {
68
+ let depth = 0;
69
+ let index = openBraceIndex;
70
+ while (index < source.length) {
71
+ const nextIndex = skipTokenIfStringOrComment(source, index);
72
+ if (nextIndex !== null) {
73
+ index = nextIndex;
74
+ continue;
75
+ }
76
+ if (source[index] === '{') {
77
+ depth += 1;
78
+ }
79
+ else if (source[index] === '}') {
80
+ depth -= 1;
81
+ if (depth === 0) {
82
+ return index;
83
+ }
84
+ }
85
+ index += 1;
86
+ }
87
+ return -1;
88
+ }
89
+ function findDefineConfigObjectBounds(source) {
90
+ const needle = 'defineConfig';
91
+ let searchStart = 0;
92
+ while (searchStart < source.length) {
93
+ const callIndex = source.indexOf(needle, searchStart);
94
+ if (callIndex === -1) {
95
+ return null;
96
+ }
97
+ let index = skipWhitespaceAndComments(source, callIndex + needle.length);
98
+ if (source[index] !== '(') {
99
+ searchStart = callIndex + needle.length;
100
+ continue;
101
+ }
102
+ index = skipWhitespaceAndComments(source, index + 1);
103
+ if (source[index] !== '{') {
104
+ searchStart = callIndex + needle.length;
105
+ continue;
106
+ }
107
+ const closeIndex = findMatchingBrace(source, index);
108
+ return closeIndex === -1 ? null : { start: index, end: closeIndex + 1 };
109
+ }
110
+ return null;
111
+ }
112
+ function findPropertyValueEnd(source, valueStart) {
113
+ let index = valueStart;
114
+ let curlyDepth = 0;
115
+ let squareDepth = 0;
116
+ let parenDepth = 0;
117
+ while (index < source.length) {
118
+ const nextIndex = skipTokenIfStringOrComment(source, index);
119
+ if (nextIndex !== null) {
120
+ index = nextIndex;
121
+ continue;
122
+ }
123
+ const current = source[index];
124
+ if (current === '{') {
125
+ curlyDepth += 1;
126
+ }
127
+ else if (current === '}') {
128
+ if (curlyDepth === 0 && squareDepth === 0 && parenDepth === 0) {
129
+ return index;
130
+ }
131
+ curlyDepth = Math.max(0, curlyDepth - 1);
132
+ }
133
+ else if (current === '[') {
134
+ squareDepth += 1;
135
+ }
136
+ else if (current === ']') {
137
+ squareDepth = Math.max(0, squareDepth - 1);
138
+ }
139
+ else if (current === '(') {
140
+ parenDepth += 1;
141
+ }
142
+ else if (current === ')') {
143
+ parenDepth = Math.max(0, parenDepth - 1);
144
+ }
145
+ else if (current === ',' && curlyDepth === 0 && squareDepth === 0 && parenDepth === 0) {
146
+ return index;
147
+ }
148
+ index += 1;
149
+ }
150
+ return source.length;
151
+ }
152
+ function parsePropertyAt(source, startIndex) {
153
+ let index = skipWhitespaceAndComments(source, startIndex);
154
+ if (index >= source.length) {
155
+ return null;
156
+ }
157
+ let key;
158
+ if (source[index] === '\'' || source[index] === '"') {
159
+ const endQuote = skipQuotedString(source, index);
160
+ if (endQuote <= index + 1 || endQuote > source.length) {
161
+ return null;
162
+ }
163
+ key = source.slice(index + 1, endQuote - 1);
164
+ index = endQuote;
165
+ }
166
+ else if (isIdentifierStart(source[index])) {
167
+ const keyStart = index;
168
+ index += 1;
169
+ while (index < source.length && isIdentifierPart(source[index])) {
170
+ index += 1;
171
+ }
172
+ key = source.slice(keyStart, index);
173
+ }
174
+ else {
175
+ return null;
176
+ }
177
+ index = skipWhitespaceAndComments(source, index);
178
+ if (source[index] !== ':') {
179
+ return null;
180
+ }
181
+ const valueStart = index + 1;
182
+ return {
183
+ key,
184
+ start: valueStart,
185
+ end: findPropertyValueEnd(source, valueStart),
186
+ };
187
+ }
188
+ function findTopLevelPropertyValueRange(objectLiteral, propertyName) {
189
+ let index = 1;
190
+ let depth = 1;
191
+ while (index < objectLiteral.length) {
192
+ const nextIndex = skipTokenIfStringOrComment(objectLiteral, index);
193
+ if (nextIndex !== null) {
194
+ index = nextIndex;
195
+ continue;
196
+ }
197
+ if (objectLiteral[index] === '{') {
198
+ depth += 1;
199
+ index += 1;
200
+ continue;
201
+ }
202
+ if (objectLiteral[index] === '}') {
203
+ depth -= 1;
204
+ index += 1;
205
+ continue;
206
+ }
207
+ if (depth === 1) {
208
+ const property = parsePropertyAt(objectLiteral, index);
209
+ if (property) {
210
+ if (property.key === propertyName) {
211
+ return { start: property.start, end: property.end };
212
+ }
213
+ index = property.end;
214
+ continue;
215
+ }
216
+ }
217
+ index += 1;
218
+ }
219
+ return null;
220
+ }
221
+ function replacePropertyValue(objectLiteral, propertyName, replacementLiteral) {
222
+ const propertyRange = findTopLevelPropertyValueRange(objectLiteral, propertyName);
223
+ if (!propertyRange) {
224
+ return objectLiteral;
225
+ }
226
+ const currentValue = objectLiteral.slice(propertyRange.start, propertyRange.end);
227
+ const leadingWhitespace = currentValue.match(/^\s*/u)?.[0] ?? '';
228
+ return (objectLiteral.slice(0, propertyRange.start)
229
+ + leadingWhitespace
230
+ + replacementLiteral
231
+ + objectLiteral.slice(propertyRange.end));
232
+ }
233
+ function replaceWindowTitle(configObject, displayNameLiteral) {
234
+ const windowRange = findTopLevelPropertyValueRange(configObject, 'window');
235
+ if (!windowRange) {
236
+ return configObject;
237
+ }
238
+ const windowValue = configObject.slice(windowRange.start, windowRange.end);
239
+ const objectStart = skipWhitespaceAndComments(windowValue, 0);
240
+ if (windowValue[objectStart] !== '{') {
241
+ return configObject;
242
+ }
243
+ const objectEnd = findMatchingBrace(windowValue, objectStart);
244
+ if (objectEnd === -1) {
245
+ return configObject;
246
+ }
247
+ const windowObject = windowValue.slice(objectStart, objectEnd + 1);
248
+ const updatedWindowObject = replacePropertyValue(windowObject, 'title', displayNameLiteral);
249
+ const updatedWindowValue = (windowValue.slice(0, objectStart)
250
+ + updatedWindowObject
251
+ + windowValue.slice(objectEnd + 1));
252
+ return (configObject.slice(0, windowRange.start)
253
+ + updatedWindowValue
254
+ + configObject.slice(windowRange.end));
255
+ }
256
+ export function updateVoltConfigContent(configContent, displayName) {
257
+ const configBounds = findDefineConfigObjectBounds(configContent);
258
+ if (!configBounds) {
259
+ return configContent;
260
+ }
261
+ const displayNameLiteral = JSON.stringify(displayName);
262
+ const configObject = configContent.slice(configBounds.start, configBounds.end);
263
+ const updatedConfigObject = replaceWindowTitle(replacePropertyValue(configObject, 'name', displayNameLiteral), displayNameLiteral);
264
+ return (configContent.slice(0, configBounds.start)
265
+ + updatedConfigObject
266
+ + configContent.slice(configBounds.end));
267
+ }
@@ -0,0 +1,2 @@
1
+ import { ProjectOptions } from './options.js';
2
+ export declare function createProject(options: ProjectOptions): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { updateVoltConfigContent } from './config-updater.js';
5
+ import { escapeHtml } from './options.js';
6
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
7
+ export async function createProject(options) {
8
+ const targetDir = resolve(process.cwd(), options.name);
9
+ if (existsSync(targetDir)) {
10
+ console.error(` Error: Directory "${options.name}" already exists.`);
11
+ process.exit(1);
12
+ }
13
+ const templateName = `${options.framework}-ts`;
14
+ const templateDir = resolve(__dirname, 'templates', templateName);
15
+ if (!existsSync(templateDir)) {
16
+ console.error(` Error: Template "${templateName}" not found.`);
17
+ process.exit(1);
18
+ }
19
+ console.log(` Creating project in ${targetDir}...`);
20
+ console.log();
21
+ mkdirSync(targetDir, { recursive: true });
22
+ cpSync(templateDir, targetDir, { recursive: true });
23
+ const pkgPath = resolve(targetDir, 'package.json');
24
+ if (existsSync(pkgPath)) {
25
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
26
+ pkg.name = options.name;
27
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
28
+ }
29
+ const configPath = resolve(targetDir, 'volt.config.ts');
30
+ if (existsSync(configPath)) {
31
+ const config = updateVoltConfigContent(readFileSync(configPath, 'utf-8'), options.displayName);
32
+ writeFileSync(configPath, config);
33
+ }
34
+ const htmlPath = resolve(targetDir, 'index.html');
35
+ if (existsSync(htmlPath)) {
36
+ let html = readFileSync(htmlPath, 'utf-8');
37
+ html = html.replace(/<title>.*?<\/title>/is, `<title>${escapeHtml(options.displayName)}</title>`);
38
+ writeFileSync(htmlPath, html);
39
+ }
40
+ console.log(' Done! Next steps:');
41
+ console.log();
42
+ console.log(` cd ${options.name}`);
43
+ console.log(' pnpm install');
44
+ console.log(' pnpm run dev');
45
+ console.log();
46
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { updateVoltConfigContent } from './config-updater.js';
3
+ import { createProject } from './create-project.js';
4
+ import { escapeHtml, normalizeProjectName, toDisplayName } from './options.js';
5
+ export declare const __testOnly: {
6
+ normalizeProjectName: typeof normalizeProjectName;
7
+ toDisplayName: typeof toDisplayName;
8
+ escapeHtml: typeof escapeHtml;
9
+ updateVoltConfigContent: typeof updateVoltConfigContent;
10
+ createProject: typeof createProject;
11
+ };
package/dist/index.js ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ import prompts from 'prompts';
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { updateVoltConfigContent } from './config-updater.js';
6
+ import { createProject } from './create-project.js';
7
+ import { escapeHtml, normalizeProjectName, toDisplayName } from './options.js';
8
+ async function main() {
9
+ const argName = process.argv[2];
10
+ console.log();
11
+ console.log(' Volt - Lightweight Desktop App Framework');
12
+ console.log();
13
+ const response = await prompts([
14
+ {
15
+ type: argName ? null : 'text',
16
+ name: 'name',
17
+ message: 'Project name:',
18
+ initial: 'my-volt-app',
19
+ },
20
+ {
21
+ type: 'select',
22
+ name: 'framework',
23
+ message: 'Select a framework:',
24
+ choices: [
25
+ { title: 'Vanilla', value: 'vanilla', description: 'Plain HTML/CSS/TypeScript' },
26
+ { title: 'React', value: 'react', description: 'React 19 + TypeScript' },
27
+ { title: 'Svelte', value: 'svelte', description: 'Svelte 5 + TypeScript' },
28
+ { title: 'Vue', value: 'vue', description: 'Vue 3 + TypeScript' },
29
+ { title: 'Enterprise', value: 'enterprise', description: 'Vanilla + enterprise-ready packaging defaults' },
30
+ ],
31
+ },
32
+ ], {
33
+ onCancel: () => {
34
+ console.log('Cancelled.');
35
+ process.exit(0);
36
+ },
37
+ });
38
+ const projectName = normalizeProjectName(String(argName ?? response.name ?? ''));
39
+ const options = {
40
+ name: projectName,
41
+ displayName: toDisplayName(projectName),
42
+ framework: response.framework ?? 'vanilla',
43
+ };
44
+ await createProject(options);
45
+ }
46
+ export const __testOnly = {
47
+ normalizeProjectName,
48
+ toDisplayName,
49
+ escapeHtml,
50
+ updateVoltConfigContent,
51
+ createProject,
52
+ };
53
+ const isEntrypoint = (() => {
54
+ const argvPath = process.argv[1];
55
+ if (!argvPath) {
56
+ return false;
57
+ }
58
+ return resolve(argvPath) === fileURLToPath(import.meta.url);
59
+ })();
60
+ if (isEntrypoint) {
61
+ main().catch((error) => {
62
+ console.error(error);
63
+ process.exitCode = 1;
64
+ });
65
+ }
@@ -0,0 +1,8 @@
1
+ export interface ProjectOptions {
2
+ name: string;
3
+ displayName: string;
4
+ framework: 'vanilla' | 'react' | 'svelte' | 'vue' | 'enterprise';
5
+ }
6
+ export declare function normalizeProjectName(input: string): string;
7
+ export declare function toDisplayName(projectName: string): string;
8
+ export declare function escapeHtml(value: string): string;
@@ -0,0 +1,32 @@
1
+ import { isAbsolute } from 'node:path';
2
+ const PROJECT_NAME_RE = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
3
+ export function normalizeProjectName(input) {
4
+ const trimmed = input.trim();
5
+ if (!trimmed) {
6
+ throw new Error('Project name cannot be empty.');
7
+ }
8
+ if (trimmed === '.' || trimmed === '..') {
9
+ throw new Error('Project name cannot be "." or "..".');
10
+ }
11
+ if (isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\')) {
12
+ throw new Error('Project name must be a single directory segment, not a path.');
13
+ }
14
+ const normalized = trimmed.toLowerCase().replace(/\s+/g, '-');
15
+ if (!PROJECT_NAME_RE.test(normalized)) {
16
+ throw new Error('Project name must be lowercase and use letters, numbers, ".", "_" or "-".');
17
+ }
18
+ return normalized;
19
+ }
20
+ export function toDisplayName(projectName) {
21
+ const parts = projectName.split(/[-_]+/).filter(Boolean);
22
+ if (parts.length === 0) {
23
+ return 'Volt App';
24
+ }
25
+ return parts.map((part) => part[0].toUpperCase() + part.slice(1)).join(' ');
26
+ }
27
+ export function escapeHtml(value) {
28
+ return value
29
+ .replace(/&/g, '&amp;')
30
+ .replace(/</g, '&lt;')
31
+ .replace(/>/g, '&gt;');
32
+ }
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>My Volt App</title>
7
+ <link rel="stylesheet" href="/src/style.css">
8
+ </head>
9
+ <body>
10
+ <div id="app">
11
+ <h1>Volt Enterprise Starter</h1>
12
+ <p>Baseline scaffold with enterprise packaging defaults.</p>
13
+ <button id="counter" type="button">Count: 0</button>
14
+ </div>
15
+ <script type="module" src="/src/main.ts"></script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "my-volt-app",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "scripts": {
6
+ "dev": "volt dev",
7
+ "build": "volt build",
8
+ "preview": "volt preview",
9
+ "doctor": "volt doctor",
10
+ "package": "volt package",
11
+ "package:msix": "volt package --target win32 --format msix"
12
+ },
13
+ "dependencies": {
14
+ "voltkit": "^0.1.0"
15
+ },
16
+ "devDependencies": {
17
+ "@voltkit/volt-cli": "^0.1.0",
18
+ "typescript": "^5.7.0",
19
+ "vite": "^6.0.0"
20
+ }
21
+ }
@@ -0,0 +1,3 @@
1
+ import { ipcMain } from 'volt:ipc';
2
+
3
+ ipcMain.handle('app:ping', () => ({ ok: true }));
@@ -0,0 +1,9 @@
1
+ let count = 0;
2
+
3
+ const button = document.getElementById('counter')!;
4
+ button.addEventListener('click', () => {
5
+ count++;
6
+ button.textContent = `Count: ${count}`;
7
+ });
8
+
9
+ console.log('Volt enterprise starter is running.');