@stripchatdev/create-stripchat-extension 0.1.7

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 (91) hide show
  1. package/README.md +35 -0
  2. package/dist/bin/cli.d.ts +2 -0
  3. package/dist/bin/cli.js +176 -0
  4. package/dist/package.json +28 -0
  5. package/dist/src/constants.d.ts +19 -0
  6. package/dist/src/constants.js +48 -0
  7. package/dist/src/executor.d.ts +2 -0
  8. package/dist/src/executor.js +22 -0
  9. package/dist/src/executor.test.d.ts +1 -0
  10. package/dist/src/executor.test.js +77 -0
  11. package/dist/src/helpers/detectPackageManager.d.ts +2 -0
  12. package/dist/src/helpers/detectPackageManager.js +12 -0
  13. package/dist/src/helpers/detectPackageManager.test.d.ts +1 -0
  14. package/dist/src/helpers/detectPackageManager.test.js +47 -0
  15. package/dist/src/helpers/isDirectory.d.ts +1 -0
  16. package/dist/src/helpers/isDirectory.js +9 -0
  17. package/dist/src/helpers/isDirectory.test.d.ts +1 -0
  18. package/dist/src/helpers/isDirectory.test.js +22 -0
  19. package/dist/src/helpers/isTypescriptOnlyFile.d.ts +1 -0
  20. package/dist/src/helpers/isTypescriptOnlyFile.js +5 -0
  21. package/dist/src/helpers/isTypescriptOnlyFile.test.d.ts +1 -0
  22. package/dist/src/helpers/isTypescriptOnlyFile.test.js +22 -0
  23. package/dist/src/helpers/normalizeRelPath.d.ts +1 -0
  24. package/dist/src/helpers/normalizeRelPath.js +1 -0
  25. package/dist/src/helpers/normalizeRelPath.test.d.ts +1 -0
  26. package/dist/src/helpers/normalizeRelPath.test.js +19 -0
  27. package/dist/src/helpers/resolveTemplateExtension.d.ts +1 -0
  28. package/dist/src/helpers/resolveTemplateExtension.js +17 -0
  29. package/dist/src/helpers/resolveTemplateExtension.test.d.ts +1 -0
  30. package/dist/src/helpers/resolveTemplateExtension.test.js +50 -0
  31. package/dist/src/helpers/resolveTemplatesDir.d.ts +1 -0
  32. package/dist/src/helpers/resolveTemplatesDir.js +11 -0
  33. package/dist/src/helpers/resolveTemplatesDir.test.d.ts +1 -0
  34. package/dist/src/helpers/resolveTemplatesDir.test.js +21 -0
  35. package/dist/src/mappers/mapProjectConfigToTemplateModel.d.ts +2 -0
  36. package/dist/src/mappers/mapProjectConfigToTemplateModel.js +26 -0
  37. package/dist/src/mappers/mapProjectConfigToTemplateModel.test.d.ts +1 -0
  38. package/dist/src/mappers/mapProjectConfigToTemplateModel.test.js +89 -0
  39. package/dist/src/planner.d.ts +2 -0
  40. package/dist/src/planner.js +126 -0
  41. package/dist/src/planner.test.d.ts +1 -0
  42. package/dist/src/planner.test.js +46 -0
  43. package/dist/src/types.d.ts +50 -0
  44. package/dist/src/types.js +1 -0
  45. package/dist/src/validators/index.d.ts +6 -0
  46. package/dist/src/validators/index.js +6 -0
  47. package/dist/src/validators/isValidCategory.d.ts +2 -0
  48. package/dist/src/validators/isValidCategory.js +2 -0
  49. package/dist/src/validators/isValidCategory.test.d.ts +1 -0
  50. package/dist/src/validators/isValidCategory.test.js +10 -0
  51. package/dist/src/validators/isValidFramework.d.ts +2 -0
  52. package/dist/src/validators/isValidFramework.js +2 -0
  53. package/dist/src/validators/isValidFramework.test.d.ts +1 -0
  54. package/dist/src/validators/isValidFramework.test.js +10 -0
  55. package/dist/src/validators/isValidLanguage.d.ts +2 -0
  56. package/dist/src/validators/isValidLanguage.js +2 -0
  57. package/dist/src/validators/isValidLanguage.test.d.ts +1 -0
  58. package/dist/src/validators/isValidLanguage.test.js +10 -0
  59. package/dist/src/validators/validateName.d.ts +1 -0
  60. package/dist/src/validators/validateName.js +10 -0
  61. package/dist/src/validators/validateName.test.d.ts +1 -0
  62. package/dist/src/validators/validateName.test.js +33 -0
  63. package/dist/src/validators/validateProjectDir.d.ts +1 -0
  64. package/dist/src/validators/validateProjectDir.js +6 -0
  65. package/dist/src/validators/validateProjectDir.test.d.ts +1 -0
  66. package/dist/src/validators/validateProjectDir.test.js +19 -0
  67. package/dist/src/validators/validateSlots.d.ts +5 -0
  68. package/dist/src/validators/validateSlots.js +15 -0
  69. package/dist/src/validators/validateSlots.test.d.ts +1 -0
  70. package/dist/src/validators/validateSlots.test.js +34 -0
  71. package/dist/tsconfig.tsbuildinfo +1 -0
  72. package/package.json +28 -0
  73. package/templates/.gitignore.ejs +33 -0
  74. package/templates/AGENTS.md.ejs +503 -0
  75. package/templates/CLAUDE.md +1 -0
  76. package/templates/README.md.ejs +38 -0
  77. package/templates/package.json.ejs +19 -0
  78. package/templates/public/icon.svg +4 -0
  79. package/templates/public/manifest.json.ejs +16 -0
  80. package/templates/public/resolveSlotPage.js.ejs +10 -0
  81. package/templates/src/slots/background/app.ts.ejs +37 -0
  82. package/templates/src/slots/background/background.html.ejs +16 -0
  83. package/templates/src/slots/background/main.ts.ejs +3 -0
  84. package/templates/src/slots/slot/app.ts.ejs +22 -0
  85. package/templates/src/slots/slot/app.tsx.ejs +22 -0
  86. package/templates/src/slots/slot/main.css.ejs +30 -0
  87. package/templates/src/slots/slot/main.ts.ejs +3 -0
  88. package/templates/src/slots/slot/main.tsx.ejs +9 -0
  89. package/templates/src/slots/slot/slot.html.ejs +21 -0
  90. package/templates/tsconfig.json.ejs +26 -0
  91. package/templates/vite.config.ts.ejs +23 -0
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # create-stripchat-extension
2
+
3
+ CLI tool for creating new Stripchat extension projects.
4
+
5
+ The generated project includes a Vite build setup, extension manifest, slot entry points, and a `resolveSlotPage` resolver -- everything needed to start developing and building an extension. [Read the docs to learn more](https://extensions.stripchat.com/docs/).
6
+
7
+ ## Quick start
8
+
9
+ With NPM:
10
+ ```bash
11
+ npm create @stripchatdev/stripchat-extension
12
+ ```
13
+
14
+ With Yarn:
15
+ ```bash
16
+ yarn dlx @stripchatdev/create-stripchat-extension
17
+ ```
18
+
19
+ With PNPM:
20
+ ```bash
21
+ pnpm create @stripchatdev/stripchat-extension
22
+ ```
23
+
24
+ With Bun:
25
+ ```bash
26
+ bun create @stripchatdev/stripchat-extension
27
+ ```
28
+
29
+ Then follow the prompts to set up your new extension project.
30
+
31
+ You can also provide options directly via CLI arguments. For example:
32
+ ```bash
33
+ npm create @stripchatdev/stripchat-extension -- --name=my-extension --category=games-and-fun
34
+ ```
35
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ import { checkbox, input, select } from '@inquirer/prompts';
3
+ import { join } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import packageJson from '../package.json' with { type: 'json' };
6
+ import { CATEGORIES, CATEGORY_LABELS, FRAMEWORK_LABELS, FRAMEWORKS, LANGUAGE_LABELS, LANGUAGES, REQUIRED_SLOTS_BY_CATEGORY, SELECTABLE_SLOTS_BY_CATEGORY, SLOT_LABELS, } from '../src/constants.js';
7
+ import { executeCommands } from '../src/executor.js';
8
+ import { detectPackageManager } from '../src/helpers/detectPackageManager.js';
9
+ import { mapProjectConfigToTemplateData } from '../src/mappers/mapProjectConfigToTemplateModel.js';
10
+ import { planProject } from '../src/planner.js';
11
+ import { isValidCategory, isValidFramework, isValidLanguage, validateName, validateProjectDir, validateSlots, } from '../src/validators/index.js';
12
+ const HELP = `
13
+ Usage: create-stripchat-extension [options]
14
+
15
+ Create a new Stripchat extension project.
16
+
17
+ Options:
18
+ --name <name> Project name (used as directory and package name)
19
+ --category <category> Extension category (${CATEGORIES.join(', ')})
20
+ --slots <items> Comma-separated slots (e.g. "mainGameFun,rightOverlay") or empty string
21
+ --framework <framework> UI framework (${FRAMEWORKS.join(', ')})
22
+ --language <lang> Language (${LANGUAGES.join(', ')})
23
+ -h, --help Display this help message
24
+ -v, --version Show version number
25
+ `.trim();
26
+ const PACKAGE_MANAGER = detectPackageManager();
27
+ const parseCliArgs = () => {
28
+ try {
29
+ const { values } = parseArgs({
30
+ args: process.argv.slice(2),
31
+ options: {
32
+ category: { type: 'string' },
33
+ framework: { type: 'string' },
34
+ help: { short: 'h', type: 'boolean' },
35
+ language: { type: 'string' },
36
+ name: { type: 'string' },
37
+ slots: { type: 'string' },
38
+ version: { short: 'v', type: 'boolean' },
39
+ },
40
+ strict: true,
41
+ });
42
+ return values;
43
+ }
44
+ catch (err) {
45
+ console.error(`Error: ${err.message}\n`);
46
+ console.log(HELP);
47
+ process.exit(1);
48
+ }
49
+ };
50
+ const resolveName = (flagValue) => {
51
+ if (flagValue) {
52
+ const result = validateName(flagValue);
53
+ if (result !== true) {
54
+ console.error(`Error: ${result}`);
55
+ process.exit(1);
56
+ }
57
+ return Promise.resolve(flagValue.trim());
58
+ }
59
+ return input({
60
+ message: 'Project name:',
61
+ validate: validateName,
62
+ });
63
+ };
64
+ const resolveCategory = (flagValue) => {
65
+ if (flagValue) {
66
+ if (!isValidCategory(flagValue)) {
67
+ console.error(`Error: invalid category "${flagValue}". Choose one of: ${CATEGORIES.join(', ')}`);
68
+ process.exit(1);
69
+ }
70
+ return Promise.resolve(flagValue);
71
+ }
72
+ return select({
73
+ choices: CATEGORIES.map((cat) => ({
74
+ name: CATEGORY_LABELS[cat],
75
+ value: cat,
76
+ })),
77
+ message: 'Extension category:',
78
+ });
79
+ };
80
+ const resolveSlots = (flagValue, category) => {
81
+ const selectable = SELECTABLE_SLOTS_BY_CATEGORY[category];
82
+ if (flagValue !== undefined) {
83
+ const requested = flagValue.split(',').map((s) => s.trim());
84
+ if (flagValue === '')
85
+ return Promise.resolve([]);
86
+ const { invalid, valid } = validateSlots(requested, category);
87
+ if (invalid.length) {
88
+ console.error(`Error: invalid slot(s) "${invalid.join(', ')}". Choose from: ${selectable.join(', ')}`);
89
+ process.exit(1);
90
+ }
91
+ return Promise.resolve(valid);
92
+ }
93
+ return checkbox({
94
+ choices: selectable.map((slot) => ({
95
+ checked: true,
96
+ name: SLOT_LABELS[slot],
97
+ value: slot,
98
+ })),
99
+ message: 'Select optional slots:',
100
+ required: false,
101
+ });
102
+ };
103
+ const resolveFramework = (flagValue) => {
104
+ if (flagValue) {
105
+ if (!isValidFramework(flagValue)) {
106
+ console.error(`Error: invalid framework "${flagValue}". Choose one of: ${FRAMEWORKS.join(', ')}`);
107
+ process.exit(1);
108
+ }
109
+ return Promise.resolve(flagValue);
110
+ }
111
+ return select({
112
+ choices: FRAMEWORKS.map((framework) => ({
113
+ name: FRAMEWORK_LABELS[framework],
114
+ value: framework,
115
+ })),
116
+ message: 'UI framework:',
117
+ });
118
+ };
119
+ const resolveLanguage = (flagValue) => {
120
+ if (flagValue) {
121
+ if (!isValidLanguage(flagValue)) {
122
+ console.error(`Error: invalid language "${flagValue}". Choose one of: ${LANGUAGES.join(', ')}`);
123
+ process.exit(1);
124
+ }
125
+ return Promise.resolve(flagValue);
126
+ }
127
+ return select({
128
+ choices: LANGUAGES.map((lang) => ({
129
+ name: LANGUAGE_LABELS[lang],
130
+ value: lang,
131
+ })),
132
+ message: 'Language:',
133
+ });
134
+ };
135
+ const main = async () => {
136
+ const opts = parseCliArgs();
137
+ if (opts.help) {
138
+ console.log(HELP);
139
+ process.exit(0);
140
+ }
141
+ if (opts.version) {
142
+ console.log(packageJson.version);
143
+ process.exit(0);
144
+ }
145
+ const name = await resolveName(opts.name);
146
+ const category = await resolveCategory(opts.category);
147
+ const selectedSlots = await resolveSlots(opts.slots, category);
148
+ const framework = await resolveFramework(opts.framework);
149
+ const language = await resolveLanguage(opts.language);
150
+ const slots = [...selectedSlots, ...REQUIRED_SLOTS_BY_CATEGORY[category]];
151
+ const projectDir = join(process.cwd(), name);
152
+ validateProjectDir(projectDir, name);
153
+ console.log(`\nCreating project in ./${name}...\n`);
154
+ const data = mapProjectConfigToTemplateData({
155
+ category,
156
+ framework,
157
+ language,
158
+ name,
159
+ packageManager: PACKAGE_MANAGER,
160
+ slots,
161
+ });
162
+ const commands = await planProject(data, projectDir);
163
+ await executeCommands(commands, (command) => console.log(` Created ${command.destPath}`));
164
+ console.log('\nDone! Next steps:\n');
165
+ console.log(` cd ${name}`);
166
+ console.log(` ${data.packageManagerCommands.install}`);
167
+ console.log(` ${data.packageManagerCommands.dev}\n`);
168
+ };
169
+ main().catch((err) => {
170
+ if (err.name === 'ExitPromptError') {
171
+ console.log('\nAborted.');
172
+ process.exit(0);
173
+ }
174
+ console.error(err);
175
+ process.exit(1);
176
+ });
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@stripchatdev/create-stripchat-extension",
3
+ "version": "0.1.7",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "templates"
8
+ ],
9
+ "bin": {
10
+ "create-stripchat-extension": "./dist/bin/cli.js"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc -b --force",
14
+ "dev": "tsc -b --watch",
15
+ "create:temp": "rm -rf ./temp && node ./dist/bin/cli.js --name=temp",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest"
18
+ },
19
+ "preferUnplugged": true,
20
+ "dependencies": {
21
+ "@inquirer/prompts": "8.3.2",
22
+ "ejs": "3.1.10"
23
+ },
24
+ "devDependencies": {
25
+ "@types/ejs": "3.1.5",
26
+ "vitest": "4.1.2"
27
+ }
28
+ }
@@ -0,0 +1,19 @@
1
+ import type { TCategory, TPackageManager, TSlot } from './types.js';
2
+ export declare const EXT_HELPER_VERSION: string;
3
+ export declare const CATEGORIES: TCategory[];
4
+ export declare const CATEGORY_LABELS: Record<TCategory, string>;
5
+ export declare const REQUIRED_SLOTS_BY_CATEGORY: Record<TCategory, TSlot[]>;
6
+ export declare const SELECTABLE_SLOTS_BY_CATEGORY: Record<TCategory, TSlot[]>;
7
+ export declare const SLOT_LABELS: Record<TSlot, string>;
8
+ export declare const SLOT_TYPES: Record<TSlot, string>;
9
+ export declare const FRAMEWORKS: readonly ["vanilla", "preact"];
10
+ export declare const FRAMEWORK_LABELS: Record<(typeof FRAMEWORKS)[number], string>;
11
+ export declare const LANGUAGES: readonly ["ts", "js"];
12
+ export declare const LANGUAGE_LABELS: Record<(typeof LANGUAGES)[number], string>;
13
+ export declare const JSX_FRAMEWORKS: string[];
14
+ export declare const PACKAGE_MANAGER_COMMANDS_MAP: Record<TPackageManager, {
15
+ build: string;
16
+ dev: string;
17
+ install: string;
18
+ }>;
19
+ export declare const TS_ONLY_FILES: string[];
@@ -0,0 +1,48 @@
1
+ import packageJson from '../package.json' with { type: 'json' };
2
+ // All packages use the same versioning
3
+ export const EXT_HELPER_VERSION = packageJson.version;
4
+ export const CATEGORIES = ['games-and-fun', 'sex-toys'];
5
+ export const CATEGORY_LABELS = {
6
+ 'games-and-fun': 'Games & Fun',
7
+ 'sex-toys': 'Sex Toys',
8
+ };
9
+ export const REQUIRED_SLOTS_BY_CATEGORY = {
10
+ 'games-and-fun': ['mainGameFun', 'background', 'rightOverlay'],
11
+ 'sex-toys': ['mainSexToy', 'background', 'moveableOverlay'],
12
+ };
13
+ export const SELECTABLE_SLOTS_BY_CATEGORY = {
14
+ 'games-and-fun': ['moveableOverlay'],
15
+ 'sex-toys': ['rightOverlay'],
16
+ };
17
+ export const SLOT_LABELS = {
18
+ background: 'background',
19
+ mainGameFun: 'mainGameFun',
20
+ mainSexToy: 'mainSexToy',
21
+ moveableOverlay: 'moveableOverlay',
22
+ rightOverlay: 'rightOverlay',
23
+ };
24
+ export const SLOT_TYPES = {
25
+ background: 'EXTENSION_SLOT_BACKGROUND',
26
+ mainGameFun: 'EXTENSION_SLOT_MAIN_GAME_FUN',
27
+ mainSexToy: 'EXTENSION_SLOT_MAIN_SEX_TOY',
28
+ moveableOverlay: 'EXTENSION_SLOT_MOVEABLE_OVERLAY',
29
+ rightOverlay: 'EXTENSION_SLOT_RIGHT_OVERLAY',
30
+ };
31
+ export const FRAMEWORKS = ['vanilla', 'preact'];
32
+ export const FRAMEWORK_LABELS = {
33
+ preact: 'Preact',
34
+ vanilla: 'Vanilla',
35
+ };
36
+ export const LANGUAGES = ['ts', 'js'];
37
+ export const LANGUAGE_LABELS = {
38
+ js: 'JavaScript',
39
+ ts: 'TypeScript',
40
+ };
41
+ export const JSX_FRAMEWORKS = ['preact'];
42
+ export const PACKAGE_MANAGER_COMMANDS_MAP = {
43
+ npm: { build: 'npm run build', dev: 'npm run dev', install: 'npm install' },
44
+ yarn: { build: 'yarn build', dev: 'yarn dev', install: 'yarn' },
45
+ pnpm: { build: 'pnpm build', dev: 'pnpm dev', install: 'pnpm install' },
46
+ bun: { build: 'bun run build', dev: 'bun dev', install: 'bun install' },
47
+ };
48
+ export const TS_ONLY_FILES = ['tsconfig.json'];
@@ -0,0 +1,2 @@
1
+ import type { TTemplateCommand } from './types.js';
2
+ export declare const executeCommands: (commands: TTemplateCommand[], cb?: (command: TTemplateCommand) => void) => Promise<void>;
@@ -0,0 +1,22 @@
1
+ import ejs from 'ejs';
2
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ const renderTemplate = async (templatePath, data) => {
5
+ const template = await readFile(templatePath, 'utf-8');
6
+ return ejs.render(template, data, { filename: templatePath });
7
+ };
8
+ export const executeCommands = async (commands, cb) => {
9
+ for (const command of commands) {
10
+ await mkdir(dirname(command.destPath), { recursive: true });
11
+ cb?.(command);
12
+ if (command.type === 'render') {
13
+ const content = await renderTemplate(command.srcPath, command.data);
14
+ await writeFile(command.destPath, content, 'utf-8');
15
+ continue;
16
+ }
17
+ if (command.type === 'copy') {
18
+ await copyFile(command.srcPath, command.destPath);
19
+ continue;
20
+ }
21
+ }
22
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { readFile, rm, mkdtemp } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { executeCommands } from './executor.js';
6
+ import { mapProjectConfigToTemplateData } from './mappers/mapProjectConfigToTemplateModel.js';
7
+ import { planProject } from './planner.js';
8
+ const EJS_TAG_PATTERN = /<%/;
9
+ const rewriteDestPaths = (commands, destDir) => commands.map((cmd) => {
10
+ const destPath = join(destDir, cmd.destPath.replace(/^.*?\/my-ext\//, ''));
11
+ if (cmd.type === 'copy') {
12
+ return { ...cmd, destPath };
13
+ }
14
+ return { ...cmd, destPath };
15
+ });
16
+ const buildCommands = async (config, destDir) => {
17
+ const data = mapProjectConfigToTemplateData(config);
18
+ const commands = await planProject(data, '/output/my-ext');
19
+ return rewriteDestPaths(commands, destDir);
20
+ };
21
+ describe('executeCommands', () => {
22
+ let tmpDir;
23
+ beforeEach(async () => {
24
+ tmpDir = await mkdtemp(join(tmpdir(), 'executor-test-'));
25
+ });
26
+ afterEach(async () => {
27
+ await rm(tmpDir, { recursive: true });
28
+ });
29
+ it('renders EJS templates into destination files', async () => {
30
+ const config = {
31
+ category: 'games-and-fun',
32
+ framework: 'vanilla',
33
+ language: 'ts',
34
+ name: 'my-ext',
35
+ packageManager: 'npm',
36
+ slots: ['mainGameFun', 'background'],
37
+ };
38
+ const commands = await buildCommands(config, tmpDir);
39
+ const cb = vi.fn();
40
+ await executeCommands(commands, cb);
41
+ for (const command of commands) {
42
+ if (command.type === 'render') {
43
+ const content = await readFile(command.destPath, 'utf-8');
44
+ expect(content.length).toBeGreaterThan(0);
45
+ expect(content).not.toMatch(EJS_TAG_PATTERN);
46
+ continue;
47
+ }
48
+ if (command.type === 'copy') {
49
+ const [src, dest] = await Promise.all([
50
+ readFile(command.srcPath),
51
+ readFile(command.destPath),
52
+ ]);
53
+ expect(dest.equals(src)).toBe(true);
54
+ continue;
55
+ }
56
+ }
57
+ expect(cb).toHaveBeenCalledTimes(commands.length);
58
+ });
59
+ it('rejects with ENOENT when a source template is missing', async () => {
60
+ const config = {
61
+ category: 'sex-toys',
62
+ framework: 'vanilla',
63
+ language: 'js',
64
+ name: 'my-ext',
65
+ packageManager: 'npm',
66
+ slots: ['mainSexToy', 'background'],
67
+ };
68
+ const commands = await buildCommands(config, tmpDir);
69
+ const corruptedIndex = commands.findIndex((c) => c.type === 'render');
70
+ const corrupted = [...commands];
71
+ corrupted[corruptedIndex] = {
72
+ ...corrupted[corruptedIndex],
73
+ srcPath: join(tmpDir, 'nonexistent.ejs'),
74
+ };
75
+ await expect(executeCommands(corrupted)).rejects.toThrow(/ENOENT/);
76
+ });
77
+ });
@@ -0,0 +1,2 @@
1
+ import type { TPackageManager } from '../types.js';
2
+ export declare const detectPackageManager: () => TPackageManager;
@@ -0,0 +1,12 @@
1
+ export const detectPackageManager = () => {
2
+ if ('Bun' in globalThis || process?.versions?.bun)
3
+ return 'bun';
4
+ const ua = process.env.npm_config_user_agent;
5
+ if (ua?.includes('bun'))
6
+ return 'bun';
7
+ if (ua?.includes('pnpm'))
8
+ return 'pnpm';
9
+ if (ua?.includes('yarn'))
10
+ return 'yarn';
11
+ return 'npm';
12
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ /* eslint-disable camelcase */
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import { detectPackageManager } from './detectPackageManager.js';
4
+ describe('detectPackageManager', () => {
5
+ const originalEnv = process.env.npm_config_user_agent;
6
+ afterEach(() => {
7
+ process.env.npm_config_user_agent = originalEnv;
8
+ // @ts-expect-error -- cleaning up test-injected global
9
+ delete globalThis.Bun;
10
+ });
11
+ it('returns bun when globalThis.Bun exists', () => {
12
+ // @ts-expect-error -- simulating Bun runtime
13
+ globalThis.Bun = {};
14
+ process.env.npm_config_user_agent = 'pnpm/9.0.0';
15
+ expect(detectPackageManager()).toBe('bun');
16
+ });
17
+ it('returns bun when user agent contains bun', () => {
18
+ process.env.npm_config_user_agent = 'bun/1.2.0 node/v22.12.0';
19
+ expect(detectPackageManager()).toBe('bun');
20
+ });
21
+ it('returns pnpm when user agent contains pnpm', () => {
22
+ process.env.npm_config_user_agent = 'pnpm/9.0.0 node/v22.12.0';
23
+ expect(detectPackageManager()).toBe('pnpm');
24
+ });
25
+ it('returns yarn when user agent contains yarn', () => {
26
+ process.env.npm_config_user_agent = 'yarn/4.1.0 node/v22.12.0';
27
+ expect(detectPackageManager()).toBe('yarn');
28
+ });
29
+ it('returns npm when user agent contains npm', () => {
30
+ process.env.npm_config_user_agent = 'npm/10.9.0 node/v22.12.0';
31
+ expect(detectPackageManager()).toBe('npm');
32
+ });
33
+ it('returns npm when user agent is undefined', () => {
34
+ delete process.env.npm_config_user_agent;
35
+ expect(detectPackageManager()).toBe('npm');
36
+ });
37
+ it('prioritises bun over user agent', () => {
38
+ // @ts-expect-error -- simulating Bun runtime
39
+ globalThis.Bun = {};
40
+ process.env.npm_config_user_agent = 'yarn/4.1.0';
41
+ expect(detectPackageManager()).toBe('bun');
42
+ });
43
+ it('prioritises pnpm over yarn in user agent', () => {
44
+ process.env.npm_config_user_agent = 'pnpm/9.0.0 yarn/4.1.0';
45
+ expect(detectPackageManager()).toBe('pnpm');
46
+ });
47
+ });
@@ -0,0 +1 @@
1
+ export declare const isDirectory: (pathValue: string) => boolean;
@@ -0,0 +1,9 @@
1
+ import { statSync } from 'node:fs';
2
+ export const isDirectory = (pathValue) => {
3
+ try {
4
+ return statSync(pathValue).isDirectory();
5
+ }
6
+ catch {
7
+ return false;
8
+ }
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { isDirectory } from './isDirectory.js';
3
+ vi.mock('node:fs', () => ({
4
+ statSync: (path) => {
5
+ if (path === '/exists/dir')
6
+ return { isDirectory: () => true };
7
+ if (path === '/exists/file')
8
+ return { isDirectory: () => false };
9
+ throw new Error('ENOENT');
10
+ },
11
+ }));
12
+ describe('isDirectory', () => {
13
+ it('returns true for an existing directory', () => {
14
+ expect(isDirectory('/exists/dir')).toBe(true);
15
+ });
16
+ it('returns false for an existing file', () => {
17
+ expect(isDirectory('/exists/file')).toBe(false);
18
+ });
19
+ it('returns false when path does not exist', () => {
20
+ expect(isDirectory('/does/not/exist')).toBe(false);
21
+ });
22
+ });
@@ -0,0 +1 @@
1
+ export declare const isTypescriptOnlyFile: (normalizedRelPath: string) => boolean;
@@ -0,0 +1,5 @@
1
+ import { TS_ONLY_FILES } from '../constants.js';
2
+ export const isTypescriptOnlyFile = (normalizedRelPath) => {
3
+ const withoutEjs = normalizedRelPath.replace(/\.ejs$/, '');
4
+ return TS_ONLY_FILES.includes(withoutEjs);
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isTypescriptOnlyFile } from './isTypescriptOnlyFile.js';
3
+ describe('isTypescriptOnlyFile', () => {
4
+ it('returns true for tsconfig.json', () => {
5
+ expect(isTypescriptOnlyFile('tsconfig.json')).toBe(true);
6
+ });
7
+ it('returns true for tsconfig.json.ejs (strips .ejs suffix)', () => {
8
+ expect(isTypescriptOnlyFile('tsconfig.json.ejs')).toBe(true);
9
+ });
10
+ it('returns false for package.json', () => {
11
+ expect(isTypescriptOnlyFile('package.json')).toBe(false);
12
+ });
13
+ it('returns false for package.json.ejs', () => {
14
+ expect(isTypescriptOnlyFile('package.json.ejs')).toBe(false);
15
+ });
16
+ it('returns false for vite.config.ts.ejs', () => {
17
+ expect(isTypescriptOnlyFile('vite.config.ts.ejs')).toBe(false);
18
+ });
19
+ it('returns false for an empty string', () => {
20
+ expect(isTypescriptOnlyFile('')).toBe(false);
21
+ });
22
+ });
@@ -0,0 +1 @@
1
+ export declare const normalizeRelPath: (pathValue: string) => string;
@@ -0,0 +1 @@
1
+ export const normalizeRelPath = (pathValue) => pathValue.replaceAll('\\', '/');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeRelPath } from './normalizeRelPath.js';
3
+ describe('normalizeRelPath', () => {
4
+ it('replaces backslashes with forward slashes', () => {
5
+ expect(normalizeRelPath('src\\slots\\main')).toBe('src/slots/main');
6
+ });
7
+ it('replaces multiple backslashes', () => {
8
+ expect(normalizeRelPath('a\\b\\c\\d')).toBe('a/b/c/d');
9
+ });
10
+ it('leaves forward slashes unchanged', () => {
11
+ expect(normalizeRelPath('src/slots/main')).toBe('src/slots/main');
12
+ });
13
+ it('handles mixed separators', () => {
14
+ expect(normalizeRelPath('src\\slots/main\\file.ts')).toBe('src/slots/main/file.ts');
15
+ });
16
+ it('returns empty string unchanged', () => {
17
+ expect(normalizeRelPath('')).toBe('');
18
+ });
19
+ });
@@ -0,0 +1 @@
1
+ export declare const resolveTemplateExtension: (normalizedRelPath: string, isTypescript: boolean) => string;
@@ -0,0 +1,17 @@
1
+ const resolveOutputExtension = (templateExt, isTypescript) => {
2
+ const map = {
3
+ '.ts': isTypescript ? '.ts' : '.js',
4
+ '.tsx': isTypescript ? '.tsx' : '.jsx',
5
+ };
6
+ return map[templateExt] ?? templateExt;
7
+ };
8
+ export const resolveTemplateExtension = (normalizedRelPath, isTypescript) => {
9
+ const withoutEjs = normalizedRelPath.replace(/\.ejs$/, '');
10
+ const extMatch = withoutEjs.match(/\.(tsx?|jsx?)$/);
11
+ if (!extMatch) {
12
+ return withoutEjs;
13
+ }
14
+ const templateExt = extMatch[0];
15
+ const base = withoutEjs.slice(0, -templateExt.length);
16
+ return `${base}${resolveOutputExtension(templateExt, isTypescript)}`;
17
+ };