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