@stripchatdev/create-stripchat-extension 0.1.22 → 0.1.24

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/dist/bin/cli.js CHANGED
@@ -3,7 +3,7 @@ import { checkbox, input, select } from '@inquirer/prompts';
3
3
  import { join } from 'node:path';
4
4
  import { parseArgs } from 'node:util';
5
5
  import packageJson from '../package.json' with { type: 'json' };
6
- import { CATEGORY_SLOTS, CATEGORIES, CATEGORY_LABELS, FRAMEWORK_LABELS, FRAMEWORKS, LANGUAGE_LABELS, LANGUAGES, REQUIRED_SLOTS_BY_CATEGORY, SELECTABLE_SLOTS_BY_CATEGORY, SLOT_LABELS, } from '../src/constants.js';
6
+ import { CATEGORIES, CATEGORY_LABELS, FRAMEWORK_LABELS, FRAMEWORKS, LANGUAGE_LABELS, LANGUAGES, REQUIRED_SLOTS_BY_CATEGORY, SLOT_RULES_BY_CATEGORY, SLOT_LABELS, } from '../src/constants.js';
7
7
  import { executeCommands } from '../src/executor.js';
8
8
  import { detectPackageManager } from '../src/helpers/detectPackageManager.js';
9
9
  import { mapProjectConfigToTemplateData } from '../src/mappers/mapProjectConfigToTemplateModel.js';
@@ -15,9 +15,9 @@ Usage: create-stripchat-extension [options]
15
15
  Create a new Stripchat extension project.
16
16
 
17
17
  Options:
18
- --name <name> Project name (used as directory and package name)
18
+ --name <name> Project name (used as a directory and package name)
19
19
  --category <category> Extension category (${CATEGORIES.join(', ')})
20
- --slots <items> Comma-separated slots (e.g. "mainGameFun,rightOverlay") or empty string
20
+ --slots <items> Comma-separated slots (e.g. "EXTENSION_SLOT_MAIN_GAME_FUN,EXTENSION_SLOT_RIGHT_OVERLAY") or empty string
21
21
  --framework <framework> UI framework (${FRAMEWORKS.join(', ')})
22
22
  --language <lang> Language (${LANGUAGES.join(', ')})
23
23
  -h, --help Display this help message
@@ -77,54 +77,62 @@ const resolveCategory = (flagValue) => {
77
77
  message: 'Extension category:',
78
78
  });
79
79
  };
80
+ const getSlotsErrorMessage = (category, requestedSlots) => {
81
+ const rules = SLOT_RULES_BY_CATEGORY[category];
82
+ const { invalid, violations } = validateSlots(requestedSlots, category);
83
+ if (invalid.length > 0) {
84
+ if (rules.optional.length === 0) {
85
+ return `Error: invalid slot(s): "${invalid.join(', ')}". This category doesn't have optional slots.`;
86
+ }
87
+ return `Error: invalid slot(s) "${invalid.join(', ')}". Choose from: ${rules.optional.join(', ')}`;
88
+ }
89
+ if (violations.length > 0) {
90
+ return `Error: ${violations.join(' ')}`;
91
+ }
92
+ return undefined;
93
+ };
94
+ const normalizePromptSlots = (value) => value.flatMap((item) => {
95
+ if (typeof item === 'string') {
96
+ return [item];
97
+ }
98
+ if (typeof item === 'object' && item !== null && 'value' in item && typeof item.value === 'string') {
99
+ return [item.value];
100
+ }
101
+ return [];
102
+ });
80
103
  const resolveSlots = async (flagValue, category) => {
81
- const { selectable, validations } = CATEGORY_SLOTS[category];
82
- const exactOneOfValidation = validations?.find((validation) => validation.type === 'exactlyOneOf');
83
104
  if (flagValue !== undefined) {
84
105
  const requested = flagValue.split(',').map((s) => s.trim());
85
106
  if (flagValue === '') {
86
- const { errors } = validateSlots([], category);
87
- if (errors.length > 0) {
88
- console.error(`Error: ${errors.join('. ')}`);
89
- process.exit(1);
90
- }
91
- return Promise.resolve([]);
92
- }
93
- const { errors, invalid, valid } = validateSlots(requested, category);
94
- if (invalid.length) {
95
- if (selectable.length !== 0) {
96
- console.error(`Error: invalid slot(s) "${invalid.join(', ')}". Choose from: ${selectable.join(', ')}`);
97
- }
98
- else {
99
- console.error(`Error: invalid slot(s): "${invalid.join(', ')}". This category doesn't have optional slots.`);
100
- }
101
- process.exit(1);
102
- }
103
- if (errors.length) {
104
- console.error(`Error: ${errors.join('. ')}`);
105
- process.exit(1);
107
+ return {
108
+ errorMessage: getSlotsErrorMessage(category, []),
109
+ slots: [],
110
+ };
106
111
  }
107
- return Promise.resolve(valid);
112
+ return {
113
+ errorMessage: getSlotsErrorMessage(category, requested),
114
+ slots: validateSlots(requested, category).selected,
115
+ };
108
116
  }
109
- if (exactOneOfValidation && exactOneOfValidation.slots.length === selectable.length) {
110
- const slot = await select({
111
- choices: selectable.map((slot) => ({
117
+ const optionalSlots = SLOT_RULES_BY_CATEGORY[category].optional;
118
+ const rawSelections = optionalSlots.length === 0
119
+ ? []
120
+ : await checkbox({
121
+ choices: optionalSlots.map((slot) => ({
122
+ checked: false,
112
123
  name: SLOT_LABELS[slot],
113
124
  value: slot,
114
125
  })),
115
- message: 'Select one optional slot:',
126
+ message: 'Select optional slots:',
127
+ required: false,
128
+ validate: (value) => getSlotsErrorMessage(category, normalizePromptSlots(value)) ?? true,
116
129
  });
117
- return [slot];
118
- }
119
- return checkbox({
120
- choices: selectable.map((slot) => ({
121
- checked: false,
122
- name: SLOT_LABELS[slot],
123
- value: slot,
124
- })),
125
- message: 'Select optional slots:',
126
- required: false,
127
- });
130
+ const selections = normalizePromptSlots(rawSelections);
131
+ const { selected } = validateSlots(selections, category);
132
+ return {
133
+ errorMessage: getSlotsErrorMessage(category, selections),
134
+ slots: selected,
135
+ };
128
136
  };
129
137
  const resolveFramework = (flagValue) => {
130
138
  if (flagValue) {
@@ -170,10 +178,15 @@ const main = async () => {
170
178
  }
171
179
  const name = await resolveName(opts.name);
172
180
  const category = await resolveCategory(opts.category);
173
- const hasSelectableSlots = SELECTABLE_SLOTS_BY_CATEGORY[category].length > 0;
181
+ const hasSelectableSlots = SLOT_RULES_BY_CATEGORY[category].optional.length > 0;
174
182
  let selectedSlots = [];
175
183
  if (hasSelectableSlots) {
176
- selectedSlots = await resolveSlots(opts.slots, category);
184
+ const resolvedSlots = await resolveSlots(opts.slots, category);
185
+ if (resolvedSlots.errorMessage) {
186
+ console.error(resolvedSlots.errorMessage);
187
+ process.exit(1);
188
+ }
189
+ selectedSlots = resolvedSlots.slots;
177
190
  }
178
191
  const framework = category === 'chat-bots' ? 'vanilla' : await resolveFramework(opts.framework);
179
192
  const language = await resolveLanguage(opts.language);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stripchatdev/create-stripchat-extension",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -1,22 +1,12 @@
1
- import type { TCategory, TPackageManager, TSlot } from './types.js';
1
+ import type { TCategory, TCategorySlotRules, TPackageManager, TSlot } from './types.js';
2
2
  export declare const EXT_HELPER_VERSION: string;
3
3
  export declare const CATEGORIES: TCategory[];
4
4
  export declare const CATEGORY_LABELS: Record<TCategory, string>;
5
- export type TSlotValidationRule = {
6
- slots: TSlot[];
7
- type: 'exactlyOneOf';
8
- };
9
- export type TCategorySlotsConfig = {
10
- required: TSlot[];
11
- selectable: TSlot[];
12
- validations?: TSlotValidationRule[];
13
- };
14
- export declare const CATEGORY_SLOTS: Record<TCategory, TCategorySlotsConfig>;
5
+ export declare const SLOT_RULES_BY_CATEGORY: Record<TCategory, TCategorySlotRules>;
15
6
  export declare const REQUIRED_SLOTS_BY_CATEGORY: Record<TCategory, TSlot[]>;
16
7
  export declare const SELECTABLE_SLOTS_BY_CATEGORY: Record<TCategory, TSlot[]>;
17
8
  export declare const SLOT_LABELS: Record<TSlot, string>;
18
- export declare const SLOT_TYPES: Record<TSlot, string>;
19
- export declare const FRAMEWORKS: readonly ["vanilla", "preact"];
9
+ export declare const FRAMEWORKS: readonly ["preact", "vanilla"];
20
10
  export declare const FRAMEWORK_LABELS: Record<(typeof FRAMEWORKS)[number], string>;
21
11
  export declare const LANGUAGES: readonly ["ts", "js"];
22
12
  export declare const LANGUAGE_LABELS: Record<(typeof LANGUAGES)[number], string>;
@@ -1,5 +1,5 @@
1
- import packageJson from '../package.json' with { type: 'json' };
2
1
  // All packages use the same versioning
2
+ import packageJson from '../package.json' with { type: 'json' };
3
3
  export const EXT_HELPER_VERSION = packageJson.version;
4
4
  export const CATEGORIES = ['games-and-fun', 'sex-toys', 'chat-bots'];
5
5
  export const CATEGORY_LABELS = {
@@ -7,53 +7,56 @@ export const CATEGORY_LABELS = {
7
7
  'sex-toys': 'Sex Toys',
8
8
  'chat-bots': 'Chat Bots',
9
9
  };
10
- export const CATEGORY_SLOTS = {
10
+ export const SLOT_RULES_BY_CATEGORY = {
11
11
  'games-and-fun': {
12
- required: ['mainGameFun', 'background'],
13
- selectable: ['rightOverlay', 'videoDecorativeOverlay'],
14
- validations: [
12
+ groups: [
15
13
  {
16
- slots: ['rightOverlay', 'videoDecorativeOverlay'],
17
- type: 'exactlyOneOf',
14
+ kind: 'exactly-one',
15
+ slots: ['EXTENSION_SLOT_RIGHT_OVERLAY', 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY'],
18
16
  },
19
17
  ],
18
+ optional: ['EXTENSION_SLOT_RIGHT_OVERLAY', 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY'],
19
+ requiredBase: ['EXTENSION_SLOT_MAIN_GAME_FUN', 'EXTENSION_SLOT_BACKGROUND'],
20
20
  },
21
21
  'sex-toys': {
22
- required: ['mainSexToy', 'background', 'moveableOverlay'],
23
- selectable: ['rightOverlay', 'videoDecorativeOverlay'],
22
+ groups: [
23
+ {
24
+ kind: 'at-most-one',
25
+ slots: ['EXTENSION_SLOT_RIGHT_OVERLAY', 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY'],
26
+ },
27
+ ],
28
+ optional: [
29
+ 'EXTENSION_SLOT_MOVEABLE_OVERLAY',
30
+ 'EXTENSION_SLOT_RIGHT_OVERLAY',
31
+ 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY',
32
+ ],
33
+ requiredBase: ['EXTENSION_SLOT_MAIN_SEX_TOY', 'EXTENSION_SLOT_BACKGROUND'],
24
34
  },
25
35
  'chat-bots': {
26
- required: ['background'],
27
- selectable: [],
36
+ groups: [],
37
+ optional: ['EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY'],
38
+ requiredBase: ['EXTENSION_SLOT_BACKGROUND'],
28
39
  },
29
40
  };
30
- export const REQUIRED_SLOTS_BY_CATEGORY = Object.fromEntries(CATEGORIES.map((category) => [category, CATEGORY_SLOTS[category].required]));
31
- export const SELECTABLE_SLOTS_BY_CATEGORY = Object.fromEntries(CATEGORIES.map((category) => [category, CATEGORY_SLOTS[category].selectable]));
41
+ export const REQUIRED_SLOTS_BY_CATEGORY = Object.fromEntries(Object.entries(SLOT_RULES_BY_CATEGORY).map(([category, rules]) => [category, rules.requiredBase]));
42
+ export const SELECTABLE_SLOTS_BY_CATEGORY = Object.fromEntries(Object.entries(SLOT_RULES_BY_CATEGORY).map(([category, rules]) => [category, rules.optional]));
32
43
  export const SLOT_LABELS = {
33
- background: 'background',
34
- mainGameFun: 'mainGameFun',
35
- mainSexToy: 'mainSexToy',
36
- moveableOverlay: 'moveableOverlay',
37
- rightOverlay: 'rightOverlay',
38
- videoDecorativeOverlay: 'videoDecorativeOverlay',
39
- };
40
- export const SLOT_TYPES = {
41
- background: 'EXTENSION_SLOT_BACKGROUND',
42
- mainGameFun: 'EXTENSION_SLOT_MAIN_GAME_FUN',
43
- mainSexToy: 'EXTENSION_SLOT_MAIN_SEX_TOY',
44
- moveableOverlay: 'EXTENSION_SLOT_MOVEABLE_OVERLAY',
45
- rightOverlay: 'EXTENSION_SLOT_RIGHT_OVERLAY',
46
- videoDecorativeOverlay: 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY',
44
+ EXTENSION_SLOT_BACKGROUND: 'background',
45
+ EXTENSION_SLOT_MAIN_GAME_FUN: 'mainGameFun',
46
+ EXTENSION_SLOT_MAIN_SEX_TOY: 'mainSexToy',
47
+ EXTENSION_SLOT_MOVEABLE_OVERLAY: 'moveableOverlay',
48
+ EXTENSION_SLOT_RIGHT_OVERLAY: 'rightOverlay',
49
+ EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY: 'videoDecorativeOverlay',
47
50
  };
48
- export const FRAMEWORKS = ['vanilla', 'preact'];
51
+ export const FRAMEWORKS = ['preact', 'vanilla'];
49
52
  export const FRAMEWORK_LABELS = {
50
- preact: 'Preact',
53
+ preact: 'Preact (Recommended)',
51
54
  vanilla: 'Vanilla',
52
55
  };
53
56
  export const LANGUAGES = ['ts', 'js'];
54
57
  export const LANGUAGE_LABELS = {
55
58
  js: 'JavaScript',
56
- ts: 'TypeScript',
59
+ ts: 'TypeScript (Recommended)',
57
60
  };
58
61
  export const JSX_FRAMEWORKS = ['preact'];
59
62
  export const PACKAGE_MANAGER_COMMANDS_MAP = {
@@ -33,7 +33,7 @@ describe('executeCommands', () => {
33
33
  language: 'ts',
34
34
  name: 'my-ext',
35
35
  packageManager: 'npm',
36
- slots: ['mainGameFun', 'background'],
36
+ slots: ['EXTENSION_SLOT_MAIN_GAME_FUN', 'EXTENSION_SLOT_BACKGROUND'],
37
37
  };
38
38
  const commands = await buildCommands(config, tmpDir);
39
39
  const cb = vi.fn();
@@ -63,7 +63,7 @@ describe('executeCommands', () => {
63
63
  language: 'js',
64
64
  name: 'my-ext',
65
65
  packageManager: 'npm',
66
- slots: ['mainSexToy', 'background'],
66
+ slots: ['EXTENSION_SLOT_MAIN_SEX_TOY', 'EXTENSION_SLOT_BACKGROUND'],
67
67
  };
68
68
  const commands = await buildCommands(config, tmpDir);
69
69
  const corruptedIndex = commands.findIndex((c) => c.type === 'render');
@@ -1,6 +1,6 @@
1
- import { EXT_HELPER_VERSION, JSX_FRAMEWORKS, PACKAGE_MANAGER_COMMANDS_MAP, SLOT_TYPES } from '../constants.js';
1
+ import { EXT_HELPER_VERSION, JSX_FRAMEWORKS, PACKAGE_MANAGER_COMMANDS_MAP, SLOT_LABELS } from '../constants.js';
2
2
  const buildSlotResolverEntries = (config) => config.slots.map((slot) => ({
3
- slotType: SLOT_TYPES[slot],
3
+ slotCamelCased: SLOT_LABELS[slot],
4
4
  slot,
5
5
  }));
6
6
  export const mapProjectConfigToTemplateData = (config) => {
@@ -20,7 +20,6 @@ export const mapProjectConfigToTemplateData = (config) => {
20
20
  name: config.name,
21
21
  packageManager: config.packageManager,
22
22
  packageManagerCommands: PACKAGE_MANAGER_COMMANDS_MAP[config.packageManager],
23
- slotResolverEntries: buildSlotResolverEntries(config),
24
- slots: config.slots,
23
+ slotsEntries: buildSlotResolverEntries(config),
25
24
  };
26
25
  };
@@ -7,7 +7,7 @@ const makeConfig = (overrides = {}) => ({
7
7
  language: 'ts',
8
8
  name: 'test-ext',
9
9
  packageManager: 'npm',
10
- slots: ['mainGameFun', 'background'],
10
+ slots: ['EXTENSION_SLOT_MAIN_GAME_FUN', 'EXTENSION_SLOT_BACKGROUND'],
11
11
  ...overrides,
12
12
  });
13
13
  describe('mapProjectConfigToTemplateData', () => {
@@ -32,32 +32,38 @@ describe('mapProjectConfigToTemplateData', () => {
32
32
  expect(result.isJsxFramework).toBe(false);
33
33
  });
34
34
  it('maps each slot to its SLOT_TYPE constant', () => {
35
- const result = mapProjectConfigToTemplateData(makeConfig({ slots: ['mainGameFun', 'background', 'rightOverlay'] }));
36
- expect(result.slotResolverEntries).toEqual([
37
- { slot: 'mainGameFun', slotType: 'EXTENSION_SLOT_MAIN_GAME_FUN' },
38
- { slot: 'background', slotType: 'EXTENSION_SLOT_BACKGROUND' },
39
- { slot: 'rightOverlay', slotType: 'EXTENSION_SLOT_RIGHT_OVERLAY' },
35
+ const result = mapProjectConfigToTemplateData(makeConfig({
36
+ slots: [
37
+ 'EXTENSION_SLOT_MAIN_GAME_FUN',
38
+ 'EXTENSION_SLOT_BACKGROUND',
39
+ 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY',
40
+ ],
41
+ }));
42
+ expect(result.slotsEntries).toEqual([
43
+ { slot: 'EXTENSION_SLOT_MAIN_GAME_FUN', slotCamelCased: 'mainGameFun' },
44
+ { slot: 'EXTENSION_SLOT_BACKGROUND', slotCamelCased: 'background' },
45
+ { slot: 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY', slotCamelCased: 'videoDecorativeOverlay' },
40
46
  ]);
41
47
  });
42
48
  it('maps sex-toys category slots correctly', () => {
43
49
  const result = mapProjectConfigToTemplateData(makeConfig({
44
50
  category: 'sex-toys',
45
- slots: ['mainSexToy', 'background', 'moveableOverlay'],
51
+ slots: ['EXTENSION_SLOT_MAIN_SEX_TOY', 'EXTENSION_SLOT_BACKGROUND', 'EXTENSION_SLOT_MOVEABLE_OVERLAY'],
46
52
  }));
47
- expect(result.slotResolverEntries).toEqual([
48
- { slot: 'mainSexToy', slotType: 'EXTENSION_SLOT_MAIN_SEX_TOY' },
49
- { slot: 'background', slotType: 'EXTENSION_SLOT_BACKGROUND' },
50
- { slot: 'moveableOverlay', slotType: 'EXTENSION_SLOT_MOVEABLE_OVERLAY' },
53
+ expect(result.slotsEntries).toEqual([
54
+ { slot: 'EXTENSION_SLOT_MAIN_SEX_TOY', slotCamelCased: 'mainSexToy' },
55
+ { slot: 'EXTENSION_SLOT_BACKGROUND', slotCamelCased: 'background' },
56
+ { slot: 'EXTENSION_SLOT_MOVEABLE_OVERLAY', slotCamelCased: 'moveableOverlay' },
51
57
  ]);
52
58
  });
53
59
  it('preserves slot order from config', () => {
54
- const result = mapProjectConfigToTemplateData(makeConfig({ slots: ['background', 'mainGameFun'] }));
55
- expect(result.slotResolverEntries[0].slot).toBe('background');
56
- expect(result.slotResolverEntries[1].slot).toBe('mainGameFun');
60
+ const result = mapProjectConfigToTemplateData(makeConfig({ slots: ['EXTENSION_SLOT_BACKGROUND', 'EXTENSION_SLOT_MAIN_GAME_FUN'] }));
61
+ expect(result.slotsEntries[0].slot).toBe('EXTENSION_SLOT_BACKGROUND');
62
+ expect(result.slotsEntries[1].slot).toBe('EXTENSION_SLOT_MAIN_GAME_FUN');
57
63
  });
58
64
  it('returns empty array when no slots provided', () => {
59
65
  const result = mapProjectConfigToTemplateData(makeConfig({ slots: [] }));
60
- expect(result.slotResolverEntries).toEqual([]);
66
+ expect(result.slotsEntries).toEqual([]);
61
67
  });
62
68
  it('passes config reference unchanged', () => {
63
69
  const config = makeConfig();
@@ -69,13 +75,15 @@ describe('mapProjectConfigToTemplateData', () => {
69
75
  framework: 'preact',
70
76
  language: 'js',
71
77
  name: 'my-project',
72
- slots: ['mainGameFun'],
78
+ slots: ['EXTENSION_SLOT_MAIN_GAME_FUN'],
73
79
  });
74
80
  const result = mapProjectConfigToTemplateData(config);
75
81
  expect(result.name).toBe('my-project');
76
82
  expect(result.framework).toBe('preact');
77
83
  expect(result.language).toBe('js');
78
- expect(result.slots).toEqual(['mainGameFun']);
84
+ expect(result.slotsEntries).toEqual([
85
+ { slot: 'EXTENSION_SLOT_MAIN_GAME_FUN', slotCamelCased: 'mainGameFun' },
86
+ ]);
79
87
  });
80
88
  it('does not include entryModulePath or slot', () => {
81
89
  const result = mapProjectConfigToTemplateData(makeConfig());
@@ -121,6 +121,6 @@ const planSlotTemplates = async (projectDir, data, slotKey) => {
121
121
  };
122
122
  export const planProject = async (data, projectDir) => {
123
123
  const globalCommands = await planGlobalTemplates(projectDir, data);
124
- const slotCommandArrays = await Promise.all(data.slots.map((slot) => planSlotTemplates(projectDir, data, slot)));
124
+ const slotCommandArrays = await Promise.all(data.slotsEntries.map(({ slotCamelCased }) => planSlotTemplates(projectDir, data, slotCamelCased)));
125
125
  return [...globalCommands, ...slotCommandArrays.flat()];
126
126
  };
@@ -1,23 +1,25 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { CATEGORY_SLOTS, CATEGORIES, FRAMEWORKS, LANGUAGES, REQUIRED_SLOTS_BY_CATEGORY, SELECTABLE_SLOTS_BY_CATEGORY, } from './constants.js';
2
+ import { CATEGORIES, FRAMEWORKS, LANGUAGES, SLOT_RULES_BY_CATEGORY, } from './constants.js';
3
3
  import { mapProjectConfigToTemplateData } from './mappers/mapProjectConfigToTemplateModel.js';
4
4
  import { planProject } from './planner.js';
5
5
  const PROJECT_DIR = '/output/my-ext';
6
6
  const buildLabel = (language, framework, category, slots) => `${language} + ${framework}, ${category} (${slots.join(', ')})`;
7
- const SLOT_COMBOS_BY_CATEGORY = Object.fromEntries(CATEGORIES.map((category) => {
8
- const required = REQUIRED_SLOTS_BY_CATEGORY[category];
9
- const selectable = SELECTABLE_SLOTS_BY_CATEGORY[category];
10
- const exactOneOfValidation = CATEGORY_SLOTS[category].validations?.find((validation) => validation.type === 'exactlyOneOf');
11
- if (exactOneOfValidation) {
12
- return [
13
- category,
14
- exactOneOfValidation.slots.map((slot) => [...required, slot]),
15
- ];
7
+ const buildSlotCombos = (category) => {
8
+ const rules = SLOT_RULES_BY_CATEGORY[category];
9
+ const base = [...rules.requiredBase];
10
+ const combos = [base];
11
+ if (category === 'games-and-fun') {
12
+ combos.length = 0;
13
+ combos.push([...base, 'EXTENSION_SLOT_RIGHT_OVERLAY'], [...base, 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY']);
14
+ return combos;
15
+ }
16
+ if (category === 'sex-toys') {
17
+ combos.push([...base, 'EXTENSION_SLOT_MOVEABLE_OVERLAY'], [...base, 'EXTENSION_SLOT_RIGHT_OVERLAY'], [...base, 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY'], [...base, 'EXTENSION_SLOT_MOVEABLE_OVERLAY', 'EXTENSION_SLOT_RIGHT_OVERLAY'], [...base, 'EXTENSION_SLOT_MOVEABLE_OVERLAY', 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY']);
16
18
  }
17
- return [
18
- category,
19
- [required, ...(selectable.length > 0 ? [[...required, ...selectable]] : [])],
20
- ];
19
+ return combos;
20
+ };
21
+ const SLOT_COMBOS_BY_CATEGORY = Object.fromEntries(CATEGORIES.map((category) => {
22
+ return [category, buildSlotCombos(category)];
21
23
  }));
22
24
  const TEST_MATRIX = CATEGORIES.flatMap((category) => SLOT_COMBOS_BY_CATEGORY[category].flatMap((slots) => FRAMEWORKS.flatMap((framework) => LANGUAGES.map((language) => ({
23
25
  category,
@@ -1,7 +1,7 @@
1
1
  export type TCategory = 'games-and-fun' | 'sex-toys' | 'chat-bots';
2
2
  export type TFramework = 'vanilla' | 'preact';
3
3
  export type TLanguage = 'ts' | 'js';
4
- export type TSlot = 'mainGameFun' | 'mainSexToy' | 'rightOverlay' | 'moveableOverlay' | 'background' | 'videoDecorativeOverlay';
4
+ export type TSlot = 'EXTENSION_SLOT_MAIN_GAME_FUN' | 'EXTENSION_SLOT_MAIN_SEX_TOY' | 'EXTENSION_SLOT_RIGHT_OVERLAY' | 'EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY' | 'EXTENSION_SLOT_MOVEABLE_OVERLAY' | 'EXTENSION_SLOT_BACKGROUND';
5
5
  export type TPackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
6
6
  export type TProjectConfig = {
7
7
  category: TCategory;
@@ -11,9 +11,10 @@ export type TProjectConfig = {
11
11
  packageManager: TPackageManager;
12
12
  slots: TSlot[];
13
13
  };
14
- export type TSlotResolverEntry = {
15
- slot: string;
16
- slotType: string;
14
+ export type TSlotEntry = {
15
+ slot: TSlot;
16
+ /** The camel cased version of the slot (e.g. `mainGameFun`). It's used as html file names */
17
+ slotCamelCased: string;
17
18
  };
18
19
  export type TPackageManagerCommands = {
19
20
  build: string;
@@ -34,8 +35,7 @@ export type TTemplateData = {
34
35
  packageManager: TPackageManager;
35
36
  packageManagerCommands: TPackageManagerCommands;
36
37
  slot?: string;
37
- slotResolverEntries: TSlotResolverEntry[];
38
- slots: TSlot[];
38
+ slotsEntries: TSlotEntry[];
39
39
  stylePath?: string;
40
40
  };
41
41
  export type TTemplateCommand = {
@@ -48,3 +48,12 @@ export type TTemplateCommand = {
48
48
  srcPath: string;
49
49
  type: 'render';
50
50
  };
51
+ export type TSlotGroupRule = {
52
+ kind: 'at-most-one' | 'exactly-one';
53
+ slots: TSlot[];
54
+ };
55
+ export type TCategorySlotRules = {
56
+ groups: TSlotGroupRule[];
57
+ optional: TSlot[];
58
+ requiredBase: TSlot[];
59
+ };
@@ -1,7 +1,7 @@
1
1
  import type { TCategory, TSlot } from '../types.js';
2
2
  export type TValidateSlotsResult = {
3
- errors: string[];
4
3
  invalid: string[];
5
- valid: TSlot[];
4
+ selected: TSlot[];
5
+ violations: string[];
6
6
  };
7
7
  export declare const validateSlots: (slots: string[], category: TCategory) => TValidateSlotsResult;
@@ -1,27 +1,31 @@
1
- import { CATEGORY_SLOTS } from '../constants.js';
1
+ import { SLOT_RULES_BY_CATEGORY } from '../constants.js';
2
+ const formatGroupMessage = (kind, slots) => {
3
+ const prefix = kind === 'exactly-one' ? 'Choose exactly one of' : 'Choose at most one of';
4
+ return `${prefix}: ${slots.join(', ')}.`;
5
+ };
2
6
  export const validateSlots = (slots, category) => {
3
- const { selectable, validations } = CATEGORY_SLOTS[category];
4
- const allowed = new Set(selectable);
5
- const valid = [];
7
+ const rules = SLOT_RULES_BY_CATEGORY[category];
8
+ const allowed = new Set(rules.optional);
9
+ const selected = [];
6
10
  const invalid = [];
7
- const errors = [];
8
- const hasOptionalSlots = allowed.size > 0;
9
- if (!hasOptionalSlots) {
10
- return { errors, invalid: slots, valid };
11
- }
12
11
  for (const slot of slots) {
13
12
  if (allowed.has(slot)) {
14
- valid.push(slot);
13
+ selected.push(slot);
15
14
  }
16
15
  else {
17
16
  invalid.push(slot);
18
17
  }
19
18
  }
20
- validations?.forEach((validation) => {
21
- const selectedCount = validation.slots.filter((slot) => valid.includes(slot)).length;
22
- if (validation.type === 'exactlyOneOf' && selectedCount !== 1) {
23
- errors.push(`Select exactly one of: ${validation.slots.join(', ')}`);
19
+ const selectedSet = new Set(selected);
20
+ const violations = rules.groups.flatMap((group) => {
21
+ const count = group.slots.filter((slot) => selectedSet.has(slot)).length;
22
+ if (group.kind === 'exactly-one' && count !== 1) {
23
+ return [formatGroupMessage(group.kind, group.slots)];
24
+ }
25
+ if (group.kind === 'at-most-one' && count > 1) {
26
+ return [formatGroupMessage(group.kind, group.slots)];
24
27
  }
28
+ return [];
25
29
  });
26
- return { errors, invalid, valid };
30
+ return { invalid, selected, violations };
27
31
  };