@tsparticles/cli-create-utils 4.0.0-beta.12 → 4.0.0-beta.16

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.
@@ -0,0 +1,1154 @@
1
+ /* eslint-disable sort-imports */
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { camelize, capitalize, dash } from "./string-utils.js";
5
+ import {
6
+ type IProjectMetadata,
7
+ copyEmptyTemplateFiles,
8
+ runBuild,
9
+ runInstall,
10
+ updatePackageDistFile,
11
+ updatePackageFile,
12
+ } from "./template-utils.js";
13
+
14
+ export type CreateProjectKind =
15
+ | "bundle"
16
+ | "effect"
17
+ | "interaction"
18
+ | "palette"
19
+ | "path"
20
+ | "plugin"
21
+ | "preset"
22
+ | "shape"
23
+ | "updater";
24
+
25
+ export type InteractionType = "external" | "generic" | "particles";
26
+
27
+ export type PluginType = "color-manager" | "easing" | "emitters-shape" | "export" | "generic";
28
+
29
+ type SubType = InteractionType | PluginType | undefined;
30
+
31
+ export interface ICreateProjectOptions {
32
+ description: string;
33
+ destination: string;
34
+ kind: CreateProjectKind;
35
+ name: string;
36
+ repositoryUrl: string;
37
+ type?: SubType;
38
+ }
39
+
40
+ interface IProjectConfig {
41
+ description: string;
42
+ fileName: string;
43
+ jsDelivrFileName: string;
44
+ kindLabel: string;
45
+ loadFunction: string;
46
+ moduleName: string;
47
+ packageName: string;
48
+ packageSuffix: string;
49
+ registerName: string;
50
+ rollupFactory: string;
51
+ rollupNameKey: string;
52
+ rollupNameValue: string;
53
+ srcFiles: Record<string, string>;
54
+ withBundleFile: boolean;
55
+ }
56
+
57
+ interface INameData {
58
+ camelName: string;
59
+ dashedName: string;
60
+ folderName: string;
61
+ pascalName: string;
62
+ }
63
+
64
+ /**
65
+ *
66
+ * @param name
67
+ * @param destination
68
+ */
69
+ function getNameData(name: string, destination: string): INameData {
70
+ const pascalName = capitalize(name.trim(), "-", "_", " "),
71
+ camelName = camelize(pascalName),
72
+ dashedName = dash(camelName),
73
+ folderName = path.basename(destination);
74
+
75
+ return {
76
+ camelName,
77
+ dashedName,
78
+ folderName,
79
+ pascalName,
80
+ };
81
+ }
82
+
83
+ /**
84
+ *
85
+ * @param loadFunction
86
+ */
87
+ function getNoopLazyIndex(loadFunction: string): string {
88
+ return `import type { Engine } from "@tsparticles/engine/lazy";
89
+
90
+ /**
91
+ * @param engine - The engine instance
92
+ */
93
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
94
+ const { ${loadFunction}: load } = await import("./index.js");
95
+
96
+ await load(engine);
97
+ }
98
+ `;
99
+ }
100
+
101
+ /**
102
+ *
103
+ * @param loadFunction
104
+ */
105
+ function getBrowserFile(loadFunction: string): string {
106
+ return `import { ${loadFunction} } from "./index.js";
107
+
108
+ const globalObject = globalThis as typeof globalThis & {
109
+ __tsParticlesInternals?: Record<string, unknown>;
110
+ ${loadFunction}?: typeof ${loadFunction};
111
+ };
112
+ globalObject.__tsParticlesInternals = globalObject.__tsParticlesInternals ?? {};
113
+ globalObject.${loadFunction} = ${loadFunction};
114
+
115
+ export * from "./index.js";
116
+ `;
117
+ }
118
+
119
+ /**
120
+ *
121
+ * @param loadFunction
122
+ * @param reexportEngine
123
+ */
124
+ function getBundleFile(loadFunction: string, reexportEngine: boolean): string {
125
+ return `import { ${loadFunction} } from "./index.js";
126
+
127
+ ${reexportEngine ? 'export { tsParticles } from "@tsparticles/engine";' : ""}
128
+ export { ${loadFunction} } from "./index.js";
129
+
130
+ const globalObject = globalThis as typeof globalThis & {
131
+ __tsParticlesInternals?: Record<string, unknown>;
132
+ ${loadFunction}?: typeof ${loadFunction};
133
+ };
134
+ globalObject.__tsParticlesInternals = globalObject.__tsParticlesInternals ?? {};
135
+
136
+ globalObject.${loadFunction} = ${loadFunction};
137
+ `;
138
+ }
139
+
140
+ /**
141
+ *
142
+ * @param kindLabel
143
+ * @param description
144
+ */
145
+ function getProjectDescription(kindLabel: string, description: string): string {
146
+ return `tsParticles ${description} ${kindLabel}`;
147
+ }
148
+
149
+ /**
150
+ *
151
+ * @param repositoryUrl
152
+ * @param packageSuffix
153
+ */
154
+ function getRepoUrl(repositoryUrl: string, packageSuffix: string): string {
155
+ if (repositoryUrl) {
156
+ return repositoryUrl;
157
+ }
158
+
159
+ return `https://github.com/tsparticles/${packageSuffix}.git`;
160
+ }
161
+
162
+ /**
163
+ *
164
+ * @param config
165
+ */
166
+ function getRollupConfig(config: IProjectConfig): string {
167
+ return `import { ${config.rollupFactory} } from "@tsparticles/rollup-plugin";
168
+ import { fileURLToPath } from "node:url";
169
+ import { readFile } from "node:fs/promises";
170
+ import path from "node:path";
171
+
172
+ const __filename = fileURLToPath(import.meta.url),
173
+ __dirname = path.dirname(__filename),
174
+ rootPkgPath = path.join(__dirname, "package.json"),
175
+ pkg = JSON.parse(await readFile(rootPkgPath, "utf-8"));
176
+
177
+ export default ${config.rollupFactory}({
178
+ moduleName: "${config.moduleName}",
179
+ ${config.rollupNameKey}: "${config.rollupNameValue}",
180
+ version: pkg.version,
181
+ dir: __dirname,
182
+ });
183
+ `;
184
+ }
185
+
186
+ /**
187
+ *
188
+ * @param config
189
+ */
190
+ function getReadme(config: IProjectConfig): string {
191
+ return `[![banner](https://particles.js.org/images/banner2.png)](https://particles.js.org)
192
+
193
+ # ${config.description}
194
+
195
+ [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/${config.packageSuffix}/badge)](https://www.jsdelivr.com/package/npm/${config.packageSuffix})
196
+ [![npmjs](https://badge.fury.io/js/${config.packageSuffix}.svg)](https://www.npmjs.com/package/${config.packageSuffix})
197
+
198
+ ## Installation
199
+
200
+ \`\`\`bash
201
+ npm install ${config.packageSuffix}
202
+ \`\`\`
203
+
204
+ ## Usage
205
+
206
+ \`\`\`ts
207
+ import { tsParticles } from "@tsparticles/engine";
208
+ import { ${config.loadFunction} } from "${config.packageSuffix}";
209
+
210
+ await ${config.loadFunction}(tsParticles);
211
+ \`\`\`
212
+ `;
213
+ }
214
+
215
+ /**
216
+ *
217
+ * @param nameData
218
+ * @param description
219
+ */
220
+ function createBundleConfig(nameData: INameData, description: string): IProjectConfig {
221
+ const loadFunction = `load${nameData.pascalName}`;
222
+
223
+ return {
224
+ description: getProjectDescription("bundle", description),
225
+ fileName: `tsparticles.${nameData.camelName}.bundle.min.js`,
226
+ jsDelivrFileName: `tsparticles.${nameData.camelName}.bundle.min.js`,
227
+ kindLabel: "Bundle",
228
+ loadFunction,
229
+ moduleName: nameData.dashedName,
230
+ packageName: `@tsparticles/${nameData.dashedName}`,
231
+ packageSuffix: `@tsparticles/${nameData.dashedName}`,
232
+ registerName: nameData.pascalName,
233
+ rollupFactory: "loadParticlesBundle",
234
+ rollupNameKey: "bundleName",
235
+ rollupNameValue: nameData.pascalName,
236
+ srcFiles: {
237
+ "src/index.ts": `import { type Engine } from "@tsparticles/engine";
238
+
239
+ declare const __VERSION__: string;
240
+
241
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
242
+ engine.checkVersion(__VERSION__);
243
+
244
+ await engine.pluginManager.register(async () => {
245
+ // TODO: load dependencies for this bundle
246
+ });
247
+ }
248
+ `,
249
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
250
+ "src/browser.ts": getBrowserFile(loadFunction),
251
+ "src/bundle.ts": getBundleFile(loadFunction, true),
252
+ },
253
+ withBundleFile: true,
254
+ };
255
+ }
256
+
257
+ /**
258
+ *
259
+ * @param nameData
260
+ * @param description
261
+ */
262
+ function createEffectConfig(nameData: INameData, description: string): IProjectConfig {
263
+ const loadFunction = `load${nameData.pascalName}Effect`,
264
+ drawerName = `${nameData.pascalName}EffectDrawer`;
265
+
266
+ return {
267
+ description: getProjectDescription("effect", description),
268
+ fileName: `tsparticles.effect.${nameData.camelName}.min.js`,
269
+ jsDelivrFileName: `tsparticles.effect.${nameData.camelName}.min.js`,
270
+ kindLabel: "Effect",
271
+ loadFunction,
272
+ moduleName: nameData.dashedName,
273
+ packageName: `@tsparticles/effect-${nameData.dashedName}`,
274
+ packageSuffix: `@tsparticles/effect-${nameData.dashedName}`,
275
+ registerName: nameData.camelName,
276
+ rollupFactory: "loadParticlesEffect",
277
+ rollupNameKey: "effectName",
278
+ rollupNameValue: nameData.pascalName,
279
+ srcFiles: {
280
+ "src/index.ts": `import { type Engine, type IEffectDrawer, type IShapeDrawData, type Particle } from "@tsparticles/engine";
281
+
282
+ declare const __VERSION__: string;
283
+
284
+ class ${drawerName} implements IEffectDrawer<Particle> {
285
+ drawAfter(_data: IShapeDrawData<Particle>): void {
286
+ // TODO: implement drawAfter
287
+ }
288
+ }
289
+
290
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
291
+ engine.checkVersion(__VERSION__);
292
+
293
+ await engine.pluginManager.register(e => {
294
+ e.pluginManager.addEffect("${nameData.camelName}", () => {
295
+ return Promise.resolve(new ${drawerName}());
296
+ });
297
+ });
298
+ }
299
+ `,
300
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
301
+ "src/browser.ts": getBrowserFile(loadFunction),
302
+ },
303
+ withBundleFile: false,
304
+ };
305
+ }
306
+
307
+ /**
308
+ *
309
+ * @param nameData
310
+ * @param description
311
+ * @param type
312
+ */
313
+ function createInteractionConfig(nameData: INameData, description: string, type: InteractionType): IProjectConfig {
314
+ const isExternal = type === "external" || type === "generic",
315
+ isParticles = type === "particles" || type === "generic";
316
+
317
+ let loadFunction = `load${nameData.pascalName}Interaction`,
318
+ packageName = `@tsparticles/interaction-${nameData.dashedName}`,
319
+ fileName = `tsparticles.interaction.${nameData.camelName}.min.js`,
320
+ rollupFactory = "loadParticlesInteraction";
321
+
322
+ if (type === "external") {
323
+ loadFunction = `loadExternal${nameData.pascalName}Interaction`;
324
+ packageName = `@tsparticles/interaction-external-${nameData.dashedName}`;
325
+ fileName = `tsparticles.interaction.external.${nameData.camelName}.min.js`;
326
+ rollupFactory = "loadParticlesInteractionExternal";
327
+ } else if (type === "particles") {
328
+ loadFunction = `loadParticles${nameData.pascalName}Interaction`;
329
+ packageName = `@tsparticles/interaction-particles-${nameData.dashedName}`;
330
+ fileName = `tsparticles.interaction.particles.${nameData.camelName}.min.js`;
331
+ rollupFactory = "loadParticlesInteractionParticles";
332
+ }
333
+
334
+ return {
335
+ description: getProjectDescription("interaction", description),
336
+ fileName,
337
+ jsDelivrFileName: fileName,
338
+ kindLabel: "Interaction",
339
+ loadFunction,
340
+ moduleName: nameData.dashedName,
341
+ packageName,
342
+ packageSuffix: packageName,
343
+ registerName: nameData.camelName,
344
+ rollupFactory,
345
+ rollupNameKey: "pluginName",
346
+ rollupNameValue: nameData.pascalName,
347
+ srcFiles: {
348
+ "src/index.ts": `import { type Engine, type IDelta, type Particle } from "@tsparticles/engine";
349
+
350
+ declare const __VERSION__: string;
351
+
352
+ interface IInteractivityData {
353
+ type?: string;
354
+ }
355
+
356
+ interface IInteractor {
357
+ clear(): void;
358
+ init(): void;
359
+ interact(..._args: unknown[]): void;
360
+ isEnabled(..._args: unknown[]): boolean;
361
+ reset(): void;
362
+ }
363
+
364
+ type InteractorFactory = (container: unknown) => Promise<IInteractor>;
365
+
366
+ type PluginManagerWithInteractors = Engine["pluginManager"] & {
367
+ addInteractor?: (name: string, interactor: InteractorFactory) => void;
368
+ };
369
+
370
+ ${
371
+ isExternal
372
+ ? `class External${nameData.pascalName}Interactor implements IInteractor {
373
+ clear(): void {
374
+ // TODO: implement clear
375
+ }
376
+
377
+ init(): void {
378
+ // TODO: implement init
379
+ }
380
+
381
+ interact(_data: IInteractivityData, _delta: IDelta): void {
382
+ // TODO: implement interact
383
+ }
384
+
385
+ isEnabled(_data: IInteractivityData, _particle?: Particle): boolean {
386
+ return false;
387
+ }
388
+
389
+ reset(): void {
390
+ // TODO: implement reset
391
+ }
392
+ }
393
+
394
+ `
395
+ : ""
396
+ }${
397
+ isParticles
398
+ ? `class Particles${nameData.pascalName}Interactor implements IInteractor {
399
+ clear(): void {
400
+ // TODO: implement clear
401
+ }
402
+
403
+ init(): void {
404
+ // TODO: implement init
405
+ }
406
+
407
+ interact(_particle: Particle, _data: IInteractivityData, _delta: IDelta): void {
408
+ // TODO: implement interact
409
+ }
410
+
411
+ isEnabled(_particle: Particle, _data: IInteractivityData): boolean {
412
+ return false;
413
+ }
414
+
415
+ reset(): void {
416
+ // TODO: implement reset
417
+ }
418
+ }
419
+
420
+ `
421
+ : ""
422
+ }export async function ${loadFunction}(engine: Engine): Promise<void> {
423
+ engine.checkVersion(__VERSION__);
424
+
425
+ await engine.pluginManager.register(e => {
426
+ const pluginManager = e.pluginManager as PluginManagerWithInteractors;
427
+
428
+ ${
429
+ isExternal
430
+ ? ` pluginManager.addInteractor?.("external${nameData.pascalName}", () => {
431
+ return Promise.resolve(new External${nameData.pascalName}Interactor());
432
+ });
433
+ `
434
+ : ""
435
+ }${
436
+ isParticles
437
+ ? ` pluginManager.addInteractor?.("particles${nameData.pascalName}", () => {
438
+ return Promise.resolve(new Particles${nameData.pascalName}Interactor());
439
+ });
440
+ `
441
+ : ""
442
+ } });
443
+ }
444
+ `,
445
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
446
+ "src/browser.ts": getBrowserFile(loadFunction),
447
+ },
448
+ withBundleFile: false,
449
+ };
450
+ }
451
+
452
+ /**
453
+ *
454
+ * @param nameData
455
+ * @param description
456
+ */
457
+ function createPaletteConfig(nameData: INameData, description: string): IProjectConfig {
458
+ const loadFunction = `load${nameData.pascalName}Palette`;
459
+
460
+ return {
461
+ description: getProjectDescription("palette", description),
462
+ fileName: `tsparticles.palette.${nameData.camelName}.min.js`,
463
+ jsDelivrFileName: `tsparticles.palette.${nameData.camelName}.min.js`,
464
+ kindLabel: "Palette",
465
+ loadFunction,
466
+ moduleName: `palette-${nameData.dashedName}`,
467
+ packageName: `@tsparticles/palette-${nameData.dashedName}`,
468
+ packageSuffix: `@tsparticles/palette-${nameData.dashedName}`,
469
+ registerName: nameData.dashedName,
470
+ rollupFactory: "loadParticlesPalette",
471
+ rollupNameKey: "paletteName",
472
+ rollupNameValue: `${nameData.pascalName} Palette`,
473
+ srcFiles: {
474
+ "src/options.ts": `import type { ISourceOptions } from "@tsparticles/engine";
475
+
476
+ export const options: ISourceOptions = {
477
+ // TODO: palette options
478
+ };
479
+ `,
480
+ "src/index.ts": `import { type Engine } from "@tsparticles/engine";
481
+ import { options } from "./options.js";
482
+
483
+ export const paletteName = "${nameData.dashedName}";
484
+
485
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
486
+ await engine.pluginManager.register(e => {
487
+ e.pluginManager.addPalette(paletteName, options);
488
+ });
489
+ }
490
+ `,
491
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
492
+ "src/browser.ts": getBrowserFile(loadFunction),
493
+ },
494
+ withBundleFile: false,
495
+ };
496
+ }
497
+
498
+ /**
499
+ *
500
+ * @param nameData
501
+ * @param description
502
+ */
503
+ function createPathConfig(nameData: INameData, description: string): IProjectConfig {
504
+ const loadFunction = `load${nameData.pascalName}Path`,
505
+ generatorName = `${nameData.pascalName}PathGenerator`;
506
+
507
+ return {
508
+ description: getProjectDescription("path", description),
509
+ fileName: `tsparticles.path.${nameData.camelName}.min.js`,
510
+ jsDelivrFileName: `tsparticles.path.${nameData.camelName}.min.js`,
511
+ kindLabel: "Path",
512
+ loadFunction,
513
+ moduleName: nameData.dashedName,
514
+ packageName: `@tsparticles/path-${nameData.dashedName}`,
515
+ packageSuffix: `@tsparticles/path-${nameData.dashedName}`,
516
+ registerName: nameData.camelName,
517
+ rollupFactory: "loadParticlesPath",
518
+ rollupNameKey: "pluginName",
519
+ rollupNameValue: nameData.pascalName,
520
+ srcFiles: {
521
+ "src/index.ts": `import { type Engine, type IDelta, type Particle, Vector } from "@tsparticles/engine";
522
+
523
+ declare const __VERSION__: string;
524
+
525
+ interface IPathGenerator {
526
+ generate(particle: Particle, delta: IDelta): Vector;
527
+ init(): void;
528
+ reset(): void;
529
+ update(): void;
530
+ }
531
+
532
+ type PluginManagerWithPath = Engine["pluginManager"] & {
533
+ addPathGenerator?: (name: string, initializer: (container: unknown) => Promise<IPathGenerator>) => void;
534
+ };
535
+
536
+ class ${generatorName} implements IPathGenerator {
537
+ generate(_particle: Particle, _delta: IDelta): Vector {
538
+ return Vector.origin;
539
+ }
540
+
541
+ init(): void {
542
+ // TODO: initialize generator
543
+ }
544
+
545
+ reset(): void {
546
+ // TODO: reset state
547
+ }
548
+
549
+ update(): void {
550
+ // TODO: update state
551
+ }
552
+ }
553
+
554
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
555
+ engine.checkVersion(__VERSION__);
556
+
557
+ await engine.pluginManager.register(e => {
558
+ const pluginManager = e.pluginManager as PluginManagerWithPath;
559
+
560
+ pluginManager.addPathGenerator?.("${nameData.camelName}PathGenerator", () => {
561
+ return Promise.resolve(new ${generatorName}());
562
+ });
563
+ });
564
+ }
565
+ `,
566
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
567
+ "src/browser.ts": getBrowserFile(loadFunction),
568
+ },
569
+ withBundleFile: false,
570
+ };
571
+ }
572
+
573
+ /**
574
+ *
575
+ * @param nameData
576
+ * @param loadFunction
577
+ * @param pluginClass
578
+ */
579
+ function createGenericPluginIndex(loadFunction: string, pluginClass: string): string {
580
+ return `import { type Engine } from "@tsparticles/engine";
581
+ import { ${pluginClass} } from "./${pluginClass}.js";
582
+
583
+ declare const __VERSION__: string;
584
+
585
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
586
+ engine.checkVersion(__VERSION__);
587
+
588
+ await engine.pluginManager.register(e => {
589
+ e.pluginManager.addPlugin(new ${pluginClass}());
590
+ });
591
+ }
592
+ `;
593
+ }
594
+
595
+ /**
596
+ *
597
+ * @param nameData
598
+ * @param description
599
+ * @param type
600
+ */
601
+ function createPluginConfig(nameData: INameData, description: string, type: PluginType): IProjectConfig {
602
+ if (type === "generic") {
603
+ const loadFunction = `load${nameData.pascalName}Plugin`,
604
+ pluginClass = `${nameData.pascalName}Plugin`;
605
+
606
+ return {
607
+ description: getProjectDescription("plugin", description),
608
+ fileName: `tsparticles.plugin.${nameData.camelName}.min.js`,
609
+ jsDelivrFileName: `tsparticles.plugin.${nameData.camelName}.min.js`,
610
+ kindLabel: "Plugin",
611
+ loadFunction,
612
+ moduleName: nameData.dashedName,
613
+ packageName: `@tsparticles/plugin-${nameData.dashedName}`,
614
+ packageSuffix: `@tsparticles/plugin-${nameData.dashedName}`,
615
+ registerName: nameData.camelName,
616
+ rollupFactory: "loadParticlesPlugin",
617
+ rollupNameKey: "pluginName",
618
+ rollupNameValue: nameData.pascalName,
619
+ srcFiles: {
620
+ "src/PluginInstance.ts": `import { type Container, type IContainerPlugin } from "@tsparticles/engine";
621
+
622
+ export class PluginInstance implements IContainerPlugin {
623
+ constructor(private readonly _container: Container) {}
624
+
625
+ get pluginContainer(): Container {
626
+ return this._container;
627
+ }
628
+
629
+ draw(): void {
630
+ // TODO: draw plugin content
631
+ }
632
+
633
+ init(): void {
634
+ // TODO: init plugin instance
635
+ }
636
+
637
+ particleBounce(): void {
638
+ // TODO: handle bounce
639
+ }
640
+
641
+ reset(): void {
642
+ // TODO: reset plugin instance
643
+ }
644
+
645
+ stop(): void {
646
+ // TODO: stop plugin instance
647
+ }
648
+ }
649
+ `,
650
+ "src/Plugin.ts": `import { type Container, type IPlugin, type ISourceOptions, type Options } from "@tsparticles/engine";
651
+ import type { PluginInstance } from "./PluginInstance.js";
652
+
653
+ export class ${pluginClass} implements IPlugin {
654
+ readonly id = "${nameData.camelName}";
655
+
656
+ async getPlugin(container: Container): Promise<PluginInstance> {
657
+ const { PluginInstance } = await import("./PluginInstance.js");
658
+
659
+ return new PluginInstance(container);
660
+ }
661
+
662
+ loadOptions(_container: Container, _options: Options, _source?: ISourceOptions): void {
663
+ // TODO: load plugin options
664
+ }
665
+
666
+ needsPlugin(_options?: ISourceOptions): boolean {
667
+ return true;
668
+ }
669
+ }
670
+ `,
671
+ "src/index.ts": createGenericPluginIndex(loadFunction, pluginClass),
672
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
673
+ "src/browser.ts": getBrowserFile(loadFunction),
674
+ },
675
+ withBundleFile: false,
676
+ };
677
+ }
678
+
679
+ if (type === "emitters-shape") {
680
+ const loadFunction = `loadEmittersShape${nameData.pascalName}`,
681
+ className = `${nameData.pascalName}EmittersShapeGenerator`;
682
+
683
+ return {
684
+ description: getProjectDescription("emitters shape plugin", description),
685
+ fileName: `tsparticles.plugin.emitters.shape.${nameData.camelName}.min.js`,
686
+ jsDelivrFileName: `tsparticles.plugin.emitters.shape.${nameData.camelName}.min.js`,
687
+ kindLabel: "Plugin",
688
+ loadFunction,
689
+ moduleName: nameData.dashedName,
690
+ packageName: `@tsparticles/plugin-emitters-shape-${nameData.dashedName}`,
691
+ packageSuffix: `@tsparticles/plugin-emitters-shape-${nameData.dashedName}`,
692
+ registerName: nameData.dashedName,
693
+ rollupFactory: "loadParticlesPluginEmittersShape",
694
+ rollupNameKey: "pluginName",
695
+ rollupNameValue: nameData.pascalName,
696
+ srcFiles: {
697
+ "src/index.ts": `import { type Engine, type ICoordinates, type IDimension } from "@tsparticles/engine";
698
+
699
+ declare const __VERSION__: string;
700
+
701
+ interface IEmitterShape {
702
+ init(): Promise<void>;
703
+ randomPosition(): { offset: ICoordinates } | null;
704
+ resize(position: ICoordinates, size: IDimension): void;
705
+ }
706
+
707
+ interface IEmitterShapeGenerator {
708
+ generate(container: unknown, position: ICoordinates, size: IDimension, fill: boolean, options: unknown): IEmitterShape;
709
+ }
710
+
711
+ type PluginManagerWithEmitterShapes = Engine["pluginManager"] & {
712
+ addEmitterShapeGenerator?: (name: string, generator: IEmitterShapeGenerator) => void;
713
+ };
714
+
715
+ class ${className} implements IEmitterShapeGenerator {
716
+ generate(_container: unknown, position: ICoordinates): IEmitterShape {
717
+ return {
718
+ init: () => Promise.resolve(),
719
+ randomPosition: () => ({ offset: position }),
720
+ resize: () => {
721
+ // TODO: resize emitter shape
722
+ },
723
+ };
724
+ }
725
+ }
726
+
727
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
728
+ engine.checkVersion(__VERSION__);
729
+
730
+ await engine.pluginManager.register(e => {
731
+ const pluginManager = e.pluginManager as PluginManagerWithEmitterShapes;
732
+
733
+ pluginManager.addEmitterShapeGenerator?.("${nameData.dashedName}", new ${className}());
734
+ });
735
+ }
736
+ `,
737
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
738
+ "src/browser.ts": getBrowserFile(loadFunction),
739
+ },
740
+ withBundleFile: false,
741
+ };
742
+ }
743
+
744
+ if (type === "easing") {
745
+ const loadFunction = `loadEasing${nameData.pascalName}Plugin`;
746
+
747
+ return {
748
+ description: getProjectDescription("easing plugin", description),
749
+ fileName: `tsparticles.plugin.easing.${nameData.camelName}.min.js`,
750
+ jsDelivrFileName: `tsparticles.plugin.easing.${nameData.camelName}.min.js`,
751
+ kindLabel: "Plugin",
752
+ loadFunction,
753
+ moduleName: nameData.dashedName,
754
+ packageName: `@tsparticles/plugin-easing-${nameData.dashedName}`,
755
+ packageSuffix: `@tsparticles/plugin-easing-${nameData.dashedName}`,
756
+ registerName: nameData.dashedName,
757
+ rollupFactory: "loadParticlesPluginEasing",
758
+ rollupNameKey: "pluginName",
759
+ rollupNameValue: nameData.pascalName,
760
+ srcFiles: {
761
+ "src/index.ts": `import { type Engine } from "@tsparticles/engine";
762
+
763
+ declare const __VERSION__: string;
764
+
765
+ const easingName = "${nameData.camelName}";
766
+
767
+ function easing(value: number): number {
768
+ return value;
769
+ }
770
+
771
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
772
+ engine.checkVersion(__VERSION__);
773
+
774
+ await engine.pluginManager.register(e => {
775
+ e.pluginManager.addEasing(easingName, easing);
776
+ });
777
+ }
778
+ `,
779
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
780
+ "src/browser.ts": getBrowserFile(loadFunction),
781
+ },
782
+ withBundleFile: false,
783
+ };
784
+ }
785
+
786
+ if (type === "export") {
787
+ const loadFunction = `loadExport${nameData.pascalName}Plugin`,
788
+ pluginClass = `${nameData.pascalName}ExportPlugin`;
789
+
790
+ return {
791
+ description: getProjectDescription("export plugin", description),
792
+ fileName: `tsparticles.plugin.export.${nameData.camelName}.min.js`,
793
+ jsDelivrFileName: `tsparticles.plugin.export.${nameData.camelName}.min.js`,
794
+ kindLabel: "Plugin",
795
+ loadFunction,
796
+ moduleName: nameData.dashedName,
797
+ packageName: `@tsparticles/plugin-export-${nameData.dashedName}`,
798
+ packageSuffix: `@tsparticles/plugin-export-${nameData.dashedName}`,
799
+ registerName: nameData.camelName,
800
+ rollupFactory: "loadParticlesPluginExport",
801
+ rollupNameKey: "pluginName",
802
+ rollupNameValue: nameData.pascalName,
803
+ srcFiles: {
804
+ "src/ExportPlugin.ts": `import { type Container, type IContainerPlugin, type IPlugin, type ISourceOptions, type Options } from "@tsparticles/engine";
805
+
806
+ class ${nameData.pascalName}ExportPluginInstance implements IContainerPlugin {
807
+ constructor(private readonly _container: Container) {}
808
+
809
+ get pluginContainer(): Container {
810
+ return this._container;
811
+ }
812
+
813
+ draw(): void {
814
+ // TODO: export draw
815
+ }
816
+
817
+ init(): void {
818
+ // TODO: export init
819
+ }
820
+
821
+ particleBounce(): void {
822
+ // TODO: export bounce
823
+ }
824
+
825
+ reset(): void {
826
+ // TODO: export reset
827
+ }
828
+
829
+ stop(): void {
830
+ // TODO: export stop
831
+ }
832
+ }
833
+
834
+ export class ${pluginClass} implements IPlugin {
835
+ readonly id = "export-${nameData.camelName}";
836
+
837
+ async getPlugin(container: Container): Promise<IContainerPlugin> {
838
+ return new ${nameData.pascalName}ExportPluginInstance(container);
839
+ }
840
+
841
+ loadOptions(_container: Container, _options: Options, _source?: ISourceOptions): void {
842
+ // TODO: load export options
843
+ }
844
+
845
+ needsPlugin(_options?: ISourceOptions): boolean {
846
+ return true;
847
+ }
848
+ }
849
+ `,
850
+ "src/index.ts": createGenericPluginIndex(loadFunction, pluginClass),
851
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
852
+ "src/browser.ts": getBrowserFile(loadFunction),
853
+ },
854
+ withBundleFile: false,
855
+ };
856
+ }
857
+
858
+ const loadFunction = `load${nameData.pascalName}ColorPlugin`,
859
+ managerClass = `${nameData.pascalName}ColorManager`;
860
+
861
+ return {
862
+ description: getProjectDescription("color manager plugin", description),
863
+ fileName: `tsparticles.plugin.${nameData.camelName}Color.min.js`,
864
+ jsDelivrFileName: `tsparticles.plugin.${nameData.camelName}Color.min.js`,
865
+ kindLabel: "Plugin",
866
+ loadFunction,
867
+ moduleName: `${nameData.camelName}Color`,
868
+ packageName: `@tsparticles/plugin-${nameData.dashedName}-color`,
869
+ packageSuffix: `@tsparticles/plugin-${nameData.dashedName}-color`,
870
+ registerName: nameData.dashedName,
871
+ rollupFactory: "loadParticlesPlugin",
872
+ rollupNameKey: "pluginName",
873
+ rollupNameValue: `${nameData.pascalName} Color`,
874
+ srcFiles: {
875
+ "src/ColorManager.ts": `import { type IColor, type IColorManager, type IRangeColor, type IRgb, type IRgba } from "@tsparticles/engine";
876
+
877
+ export class ${managerClass} implements IColorManager {
878
+ accepts(input: string): boolean {
879
+ return input.startsWith("${nameData.camelName}(");
880
+ }
881
+
882
+ handleColor(_color: IColor): IRgb | undefined {
883
+ return undefined;
884
+ }
885
+
886
+ handleRangeColor(_color: IRangeColor): IRgb | undefined {
887
+ return undefined;
888
+ }
889
+
890
+ parseString(_input: string): IRgba | undefined {
891
+ return undefined;
892
+ }
893
+ }
894
+ `,
895
+ "src/index.ts": `import { type Engine } from "@tsparticles/engine";
896
+ import { ${managerClass} } from "./ColorManager.js";
897
+
898
+ declare const __VERSION__: string;
899
+
900
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
901
+ engine.checkVersion(__VERSION__);
902
+
903
+ await engine.pluginManager.register(e => {
904
+ e.pluginManager.addColorManager("${nameData.camelName}", new ${managerClass}());
905
+ });
906
+ }
907
+ `,
908
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
909
+ "src/browser.ts": getBrowserFile(loadFunction),
910
+ },
911
+ withBundleFile: false,
912
+ };
913
+ }
914
+
915
+ /**
916
+ *
917
+ * @param nameData
918
+ * @param description
919
+ */
920
+ function createPresetConfig(nameData: INameData, description: string): IProjectConfig {
921
+ const loadFunction = `load${nameData.pascalName}Preset`;
922
+
923
+ return {
924
+ description: getProjectDescription("preset", description),
925
+ fileName: `tsparticles.preset.${nameData.camelName}.min.js`,
926
+ jsDelivrFileName: `tsparticles.preset.${nameData.camelName}.min.js`,
927
+ kindLabel: "Preset",
928
+ loadFunction,
929
+ moduleName: nameData.dashedName,
930
+ packageName: `@tsparticles/preset-${nameData.dashedName}`,
931
+ packageSuffix: `@tsparticles/preset-${nameData.dashedName}`,
932
+ registerName: nameData.camelName,
933
+ rollupFactory: "loadParticlesPreset",
934
+ rollupNameKey: "presetName",
935
+ rollupNameValue: nameData.pascalName,
936
+ srcFiles: {
937
+ "src/options.ts": `import type { ISourceOptions } from "@tsparticles/engine";
938
+
939
+ export const options: ISourceOptions = {
940
+ // TODO: preset options
941
+ };
942
+ `,
943
+ "src/index.ts": `import { type Engine } from "@tsparticles/engine";
944
+ import { options } from "./options.js";
945
+
946
+ declare const __VERSION__: string;
947
+
948
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
949
+ engine.checkVersion(__VERSION__);
950
+
951
+ await engine.pluginManager.register(e => {
952
+ e.pluginManager.addPreset("${nameData.camelName}", options);
953
+ });
954
+ }
955
+ `,
956
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
957
+ "src/browser.ts": getBrowserFile(loadFunction),
958
+ "src/bundle.ts": getBundleFile(loadFunction, false),
959
+ },
960
+ withBundleFile: true,
961
+ };
962
+ }
963
+
964
+ /**
965
+ *
966
+ * @param nameData
967
+ * @param description
968
+ */
969
+ function createShapeConfig(nameData: INameData, description: string): IProjectConfig {
970
+ const loadFunction = `load${nameData.pascalName}Shape`,
971
+ className = `${nameData.pascalName}ShapeDrawer`;
972
+
973
+ return {
974
+ description: getProjectDescription("shape", description),
975
+ fileName: `tsparticles.shape.${nameData.camelName}.min.js`,
976
+ jsDelivrFileName: `tsparticles.shape.${nameData.camelName}.min.js`,
977
+ kindLabel: "Shape",
978
+ loadFunction,
979
+ moduleName: nameData.dashedName,
980
+ packageName: `@tsparticles/shape-${nameData.dashedName}`,
981
+ packageSuffix: `@tsparticles/shape-${nameData.dashedName}`,
982
+ registerName: nameData.camelName,
983
+ rollupFactory: "loadParticlesShape",
984
+ rollupNameKey: "shapeName",
985
+ rollupNameValue: nameData.pascalName,
986
+ srcFiles: {
987
+ "src/ShapeDrawer.ts": `import { type IShapeDrawData, type IShapeDrawer } from "@tsparticles/engine";
988
+
989
+ export class ${className} implements IShapeDrawer {
990
+ readonly validTypes = ["${nameData.camelName}"] as const;
991
+
992
+ draw(_data: IShapeDrawData): void {
993
+ // TODO: draw shape
994
+ }
995
+ }
996
+ `,
997
+ "src/index.ts": `import { type Engine } from "@tsparticles/engine";
998
+ import { ${className} } from "./ShapeDrawer.js";
999
+
1000
+ declare const __VERSION__: string;
1001
+
1002
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
1003
+ engine.checkVersion(__VERSION__);
1004
+
1005
+ await engine.pluginManager.register(e => {
1006
+ e.pluginManager.addShape(["${nameData.camelName}"], () => {
1007
+ return Promise.resolve(new ${className}());
1008
+ });
1009
+ });
1010
+ }
1011
+ `,
1012
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
1013
+ "src/browser.ts": getBrowserFile(loadFunction),
1014
+ },
1015
+ withBundleFile: false,
1016
+ };
1017
+ }
1018
+
1019
+ /**
1020
+ *
1021
+ * @param nameData
1022
+ * @param description
1023
+ */
1024
+ function createUpdaterConfig(nameData: INameData, description: string): IProjectConfig {
1025
+ const loadFunction = `load${nameData.pascalName}Updater`,
1026
+ className = `${nameData.pascalName}Updater`;
1027
+
1028
+ return {
1029
+ description: getProjectDescription("updater", description),
1030
+ fileName: `tsparticles.updater.${nameData.camelName}.min.js`,
1031
+ jsDelivrFileName: `tsparticles.updater.${nameData.camelName}.min.js`,
1032
+ kindLabel: "Updater",
1033
+ loadFunction,
1034
+ moduleName: nameData.dashedName,
1035
+ packageName: `@tsparticles/updater-${nameData.dashedName}`,
1036
+ packageSuffix: `@tsparticles/updater-${nameData.dashedName}`,
1037
+ registerName: nameData.camelName,
1038
+ rollupFactory: "loadParticlesUpdater",
1039
+ rollupNameKey: "updaterName",
1040
+ rollupNameValue: nameData.pascalName,
1041
+ srcFiles: {
1042
+ "src/Updater.ts": `import { type IDelta, type IParticleUpdater, type Particle } from "@tsparticles/engine";
1043
+
1044
+ export class ${className} implements IParticleUpdater {
1045
+ init(_particle: Particle): void {
1046
+ // TODO: init updater
1047
+ }
1048
+
1049
+ isEnabled(_particle: Particle): boolean {
1050
+ return true;
1051
+ }
1052
+
1053
+ update(_particle: Particle, _delta: IDelta): void {
1054
+ // TODO: update particle
1055
+ }
1056
+ }
1057
+ `,
1058
+ "src/index.ts": `import { type Engine } from "@tsparticles/engine";
1059
+ import { ${className} } from "./Updater.js";
1060
+
1061
+ declare const __VERSION__: string;
1062
+
1063
+ export async function ${loadFunction}(engine: Engine): Promise<void> {
1064
+ engine.checkVersion(__VERSION__);
1065
+
1066
+ await engine.pluginManager.register(e => {
1067
+ e.pluginManager.addParticleUpdater("${nameData.camelName}", () => {
1068
+ return Promise.resolve(new ${className}());
1069
+ });
1070
+ });
1071
+ }
1072
+ `,
1073
+ "src/index.lazy.ts": getNoopLazyIndex(loadFunction),
1074
+ "src/browser.ts": getBrowserFile(loadFunction),
1075
+ },
1076
+ withBundleFile: false,
1077
+ };
1078
+ }
1079
+
1080
+ /**
1081
+ *
1082
+ * @param options
1083
+ * @param nameData
1084
+ */
1085
+ function resolveProjectConfig(options: ICreateProjectOptions, nameData: INameData): IProjectConfig {
1086
+ switch (options.kind) {
1087
+ case "bundle":
1088
+ return createBundleConfig(nameData, options.description);
1089
+ case "effect":
1090
+ return createEffectConfig(nameData, options.description);
1091
+ case "interaction":
1092
+ return createInteractionConfig(
1093
+ nameData,
1094
+ options.description,
1095
+ (options.type as InteractionType | undefined) ?? "generic",
1096
+ );
1097
+ case "palette":
1098
+ return createPaletteConfig(nameData, options.description);
1099
+ case "path":
1100
+ return createPathConfig(nameData, options.description);
1101
+ case "plugin":
1102
+ return createPluginConfig(nameData, options.description, (options.type as PluginType | undefined) ?? "generic");
1103
+ case "preset":
1104
+ return createPresetConfig(nameData, options.description);
1105
+ case "shape":
1106
+ return createShapeConfig(nameData, options.description);
1107
+ case "updater":
1108
+ return createUpdaterConfig(nameData, options.description);
1109
+ default:
1110
+ throw new Error(`Unsupported kind: ${String(options.kind)}`);
1111
+ }
1112
+ }
1113
+
1114
+ /**
1115
+ *
1116
+ * @param destination
1117
+ * @param srcFiles
1118
+ */
1119
+ async function writeSourceFiles(destination: string, srcFiles: Record<string, string>): Promise<void> {
1120
+ for (const [filePath, content] of Object.entries(srcFiles)) {
1121
+ const targetPath = path.join(destination, filePath),
1122
+ folderPath = path.dirname(targetPath);
1123
+
1124
+ await mkdir(folderPath, { recursive: true });
1125
+ await writeFile(targetPath, content);
1126
+ }
1127
+ }
1128
+
1129
+ /**
1130
+ *
1131
+ * @param options
1132
+ */
1133
+ export async function createProjectTemplate(options: ICreateProjectOptions): Promise<void> {
1134
+ const nameData = getNameData(options.name, options.destination),
1135
+ config = resolveProjectConfig(options, nameData),
1136
+ repoUrl = getRepoUrl(options.repositoryUrl, config.packageSuffix),
1137
+ metadata: IProjectMetadata = {
1138
+ description: config.description,
1139
+ directory: nameData.folderName,
1140
+ packageName: config.packageName,
1141
+ repoUrl,
1142
+ unpkgFileName: config.jsDelivrFileName,
1143
+ };
1144
+
1145
+ await copyEmptyTemplateFiles(options.destination);
1146
+ await writeSourceFiles(options.destination, config.srcFiles);
1147
+ await writeFile(path.join(options.destination, "rollup.config.js"), getRollupConfig(config));
1148
+ await writeFile(path.join(options.destination, "README.md"), getReadme(config));
1149
+ await updatePackageFile(options.destination, metadata);
1150
+ await updatePackageDistFile(options.destination, metadata);
1151
+
1152
+ await runInstall(options.destination);
1153
+ await runBuild(options.destination);
1154
+ }