Skip to content

Conversation

matteobruni
Copy link
Contributor

@matteobruni matteobruni commented Aug 31, 2025

Summary by CodeRabbit

  • New Features

    • Clearer build output with improved error messages and human-readable size-change summaries.
  • Refactor

    • Project migrated to ES modules/NodeNext; configs and scripts converted to ESM and package now declares "type":"module".
    • Linting reworked to a new ESM config and now applies to previously-ignored build and dependency folders.
  • Tests

    • Test suite migrated from Mocha/Chai to Vitest with a new Vitest configuration.
  • Chores

    • CI/tooling upgraded (Node 22, workflow and pnpm updates) and broad dev-dependency bumps.

Copy link

coderabbitai bot commented Aug 31, 2025

Walkthrough

Convert repo from CommonJS to ESM: replace CJS configs/scripts with ESM equivalents, add eslint.config.js and vitest config, upgrade Node/CI/pnpm and many dependencies, migrate tests to Vitest, broaden TypeScript targets/moduleResolution, normalize imports to explicit .js, adjust path joins, and improve runtime package.json handling and error objects.

Changes

Cohort / File(s) Summary
ESLint configs & ignores
eslint.config.js, .eslintrc.js, .eslintignore, tsconfig.eslint.json, files/empty-project/eslint.config.js, files/empty-project/.eslintrc.js, files/empty-project/.eslintignore
Remove CJS .eslintrc.js; add ESM eslint.config.js; add tsconfig.eslint.json; remove dist and node_modules from .eslintignore; mirror config changes under files/empty-project.
Package metadata & top-level ESM
package.json, files/empty-project/package.json, files/empty-project/package.dist.json
Add "type":"module", bump version, set maineslint.config.js, change test script to Vitest, and perform wide dependency/devDependency upgrades and restructures (replace some @typescript-eslint packages with typescript-eslint).
CI, workspace & test runner
.github/workflows/node.js-ci.yml, pnpm-workspace.yaml, vitest.config.ts, tests/tsconfig.json
Upgrade GitHub Actions (checkout@v5), bump Node to 22, update pnpm setup and pnpm version input to 10, add onlyBuiltDependencies in pnpm-workspace.yaml, add Vitest config, and update tests tsconfig to NodeNext, include globs, and use Vitest types.
Scripts & runtime package reads
scripts/postversion.js, src/cli.ts, files/empty-project/webpack.config.js
Migrate scripts/CLI/webpack config to ESM, compute __dirname via fileURLToPath(import.meta.url), read package.json at runtime (fs-extra readJson / top-level await where used), switch to ESM exports (export default), and use fs-extra readJson/writeJson.
Build system & diagnostics
src/build/*.ts (build.ts, build-bundle.ts, build-eslint.ts, build-tsc.ts, build-circular-deps.ts, build-diststats.ts, build-distfiles.ts, build-bundle.ts)
Normalize error handling to Error objects/aggregated messages; remove inline ESLint baseConfig; convert const enum→enum; change diagnostics loops and formatting; refine prettier gating and stats stringification; replace some dynamic imports with fs reads; adjust path.resolve → path.join in places.
Create/generator modules & templates
src/create/**/*, src/create/**/create-*.ts, files/create-shape/src/*
Update imports to explicit .js extensions; replace many path.resolve calls with path.join; add ShapeDrawer.validTypes and remove template key arg in files/create-shape index; add token replacements for ShapeDrawer.ts.
Utilities & template handling
src/utils/file-utils.ts, src/utils/template-utils.ts
Use item.path directly and path.join for destination construction; getRepositoryUrl resolves stdout; replace path.resolve with path.join for template source paths; minor API-internal refactors to use fs-extra where applied elsewhere.
Tests: Mocha/Chai → Vitest
tests/*.test.ts (create-plugin.test.ts, create-preset.test.ts, create-shape.test.ts, file-utils.test.ts, string-utils.test.ts)
Migrate tests to Vitest: update imports/assertions to Vitest style (toBe, .not.toBe), update imports to .js, remove async describe wrappers, adjust path handling and cleanup hooks, and read package.json via explicit pkgPath variables.
TypeScript project files
tsconfig.json, src/tsconfig.json, tests/tsconfig.json, tsconfig.eslint.json
Root tsconfig switches from files to include globs (src/**/*, tests/**/*, files/**/*); change module/moduleResolution to NodeNext and add ES2023/ES2024 libs; add tsconfig.eslint.json for lint scope.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Dev as Developer
  participant Repo as Repository
  participant CI as GitHub Actions (Node 22)
  participant PNPM as pnpm (v10)
  participant CLI as CLI (ESM)
  participant ESLint as ESLint (eslint.config.js)
  participant TSC as TypeScript (NodeNext)
  participant Bundler as Webpack (ESM)
  participant Test as Vitest

  Dev->>Repo: push/PR
  CI->>Repo: checkout (actions/checkout@v5)
  CI->>PNPM: setup pnpm v10
  CI->>Test: run tests (vitest)
  CI->>CLI: run build scripts
  CLI->>ESLint: lint project (eslint.config.js)
  ESLint-->>CLI: lint results
  CLI->>TSC: typecheck (tsconfig NodeNext)
  TSC-->>CLI: diagnostics
  CLI->>Bundler: run webpack (ESM config reads package.json at runtime)
  Bundler-->>CLI: stats/errors (normalized as Error objects)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I hopped from require to import with a twitchy ear,
switched paths to join and read pkg at runtime clear.
Vitest now scampers, ESLint wears ESM bright,
Node twenty-two bounds, pnpm dances light.
A rabbit cheers: builds run snug and right. 🐇


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 637477d and 8d9a16f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • package.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
tests/file-utils.test.ts (2)

97-99: Replace after with afterAll

Fixes the runtime error.

-after(async () => {
+afterAll(async () => {
   await fs.remove(baseDir);
-});
+});

11-15: Avoid async describe and top-level awaits; move setup into hooks

Async describe callbacks and awaits at definition time are brittle. Use beforeAll/beforeEach for setup.

-describe("file-utils", async () => {
-    const baseDir = path.join(__dirname, "tmp-files");
-
-    await fs.ensureDir(baseDir);
+describe("file-utils", () => {
+    const baseDir = path.join(testDir, "tmp-files");
+    beforeAll(async () => {
+        await fs.ensureDir(baseDir);
+    });
@@
-    describe("replace tokens in files", async () => {
-        fs.writeFileSync(path.join(baseDir, "files1.txt"), "test");
-        fs.writeFileSync(path.join(baseDir, "files2.txt"), "test");
-
-        await replaceTokensInFiles([{
+    describe("replace tokens in files", () => {
+        beforeAll(async () => {
+            fs.writeFileSync(path.join(baseDir, "files1.txt"), "test");
+            fs.writeFileSync(path.join(baseDir, "files2.txt"), "test");
+            await replaceTokensInFiles([{
                 path: path.join(baseDir, "files1.txt"),
                 tokens: [{
                     from: "test",
                     to: "test1",
                 }],
             }, {
                 path: path.join(baseDir, "files2.txt"),
                 tokens: [{
                     from: "test",
                     to: "test2",
                 }],
-        }]);
+            }]);
+        });
@@
-    describe("replace tokens in file", async () => {
-        fs.writeFileSync(path.join(baseDir, "file1.txt"), "test");
-
-        await replaceTokensInFile({
+    describe("replace tokens in file", () => {
+        beforeAll(async () => {
+            fs.writeFileSync(path.join(baseDir, "file1.txt"), "test");
+            await replaceTokensInFile({
                 path: path.join(baseDir, "file1.txt"),
                 tokens: [{
                     from: "test",
                     to: "test1",
                 }],
-        });
+            });
+        });
@@
-    describe("get destination dir", async () => {
-        const destDir = await getDestinationDir(path.join(baseDir, "baz"));
+    describe("get destination dir", () => {
+        let destDir: string;
+        beforeAll(async () => {
+            destDir = await getDestinationDir(path.join(baseDir, "baz"));
+        });

Also applies to: 16-33, 43-53, 61-63

.github/workflows/node.js-ci.yml (1)

36-37: Replace deprecated set-output with GITHUB_OUTPUT.

Prevents deprecation warnings and future breaks.

-      - name: Get pnpm store directory
+      - name: Get pnpm store directory
         id: pnpm-cache
         run: |
-          echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+          echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT"
-      - name: Get pnpm store directory
+      - name: Get pnpm store directory
         id: pnpm-cache
         run: |
-          echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
+          echo "pnpm_cache_dir=$(pnpm store path)" >> "$GITHUB_OUTPUT"

Also applies to: 74-75

src/build/build-bundle.ts (1)

38-49: Don’t resolve after rejecting; also normalize error messages.

Currently it can hit resolve(true) even after reject. Add return; handle string/unknown errors.

-                if (stats.hasErrors()) {
-                    console.error(info.errors);
-
-                    reject(new Error(info.errors?.map(error => error.message).join("\n")));
-                }
+                if (stats.hasErrors()) {
+                    console.error(info.errors);
+                    const msg =
+                        info.errors?.map((e: unknown) =>
+                            typeof e === "string" ? e : (e as { message?: string }).message ?? String(e),
+                        ).join("\n") || "Unknown webpack error";
+                    reject(new Error(msg));
+                    return;
+                }
src/build/build-tsc.ts (1)

117-121: Guard JSON parsing to fail gracefully with an explicit exit code.

A malformed tsconfig will currently throw and abort the process. Parse errors should return ParseError consistently.

Apply:

-    if (!options && data) {
-        options = JSON.parse(data);
-    }
+    if (!options && data) {
+        try {
+            options = JSON.parse(data);
+        } catch (e) {
+            console.error("Failed parsing tsconfig JSON:", e);
+            return ExitCodes.ParseError;
+        }
+    }
🧹 Nitpick comments (32)
files/empty-project/package.dist.json (1)

5-5: ESM-only distro: confirm intentional CJS drop.

Setting "type": "module" with "main": "index.js" makes require("@tsparticles/empty-template") fail in Node. If that’s intended, consider adding an explicit exports for clarity; if not, provide a CJS fallback (.cjs).

Optional clarity:

   "type": "module",
+  "exports": {
+    ".": {
+      "types": "./index.d.ts",
+      "import": "./index.js",
+      "default": "./index.js"
+    }
+  },
src/tsconfig.json (1)

30-35: Align TS module/resolution settings with ESM

Detected legacy settings in both tsconfig files:

  • src/tsconfig.json (lines 30–35)
  • tests/tsconfig.json (lines 4–5, 17–18)

Suggested changes:

--- a/src/tsconfig.json
@@
-        "module": "es2020",
+        "module": "ESNext",
@@
-        "moduleResolution": "node",
+        "moduleResolution": "Bundler",
+        "verbatimModuleSyntax": true,
--- a/tests/tsconfig.json
@@
-        "module": "commonjs",
+        "module": "ESNext",
@@
-        "moduleResolution": "node",
+        "moduleResolution": "Bundler",

Ensure your test runner supports ESM imports before migrating tests config.

src/utils/file-utils.ts (1)

75-85: Return a trimmed UTF-8 string to avoid trailing newline issues.

exec returns stdout with a trailing newline; trimming makes downstream comparisons stable and types unambiguous.

-        exec("git config --get remote.origin.url", (error, stdout) => {
+        exec("git config --get remote.origin.url", { encoding: "utf8" }, (error, stdout) => {
@@
-            resolve(stdout);
+            resolve(stdout.trim());
tests/tsconfig.json (2)

6-6: Align module settings with ESM toolchain.
Given the repo migrates to ESM/Node 22, consider module: "nodenext" and moduleResolution: "nodenext" (or moduleResolution: "bundler" for Vitest) to avoid subtle resolution mismatches in tests.


24-24: Exclude generated artifacts to keep TS fast and avoid noisy types.
Add excludes for temp dirs and build outputs under tests to prevent typechecking generated template projects.

Apply this diff:

-    "include": ["**/*.ts"]
+    "include": ["**/*.ts"],
+    "exclude": ["tmp-files/**", "**/node_modules/**", "**/dist/**", "**/build/**"]
pnpm-workspace.yaml (1)

1-3: Validate PNPM’s onlyBuiltDependencies behavior in CI.
Good optimization, but confirm PNPM 10 supports and applies this at workspace root as intended, and that no other deps require build in your workflows. If you rely on workspace package discovery, ensure packages: is defined elsewhere or intentionally omitted.

tests/create-plugin.test.ts (1)

8-8: Nit: avoid redundant resolve(join()).
Prefer path.resolve(__dirname, "tmp-files", "foo-plugin").

tests/create-preset.test.ts (1)

8-8: Nit: simplify path construction.
Use path.resolve(__dirname, "tmp-files", "foo-preset").

tsconfig.json (1)

18-22: Include globs look fine; add excludes to avoid checking generated projects.
Since tests now generate projects under tests/tmp-files, exclude them from the root TS program.

Apply this diff:

   "include": [
     "src/**/*",
     "tests/**/*",
     "files/**/*"
-  ]
+  ],
+  "exclude": [
+    "tests/tmp-files/**",
+    "**/node_modules/**",
+    "**/dist/**",
+    "**/build/**"
+  ]

Additionally, consider removing legacy Mocha/Chai globals from types to prevent conflicts with Vitest:

// tsconfig.json (types section)
"types": [
  "node",
  "fs-extra",
  "klaw",
  "madge",
  "prompts",
  "eslint"
  // removed: "chai", "mocha"
]
tsconfig.eslint.json (1)

1-7: Ensure ESLint actually uses this project TSConfig for type-aware linting

If the root flat config points parserOptions.project to a different tsconfig, tests may miss type info. Consider pointing ESLint to this file (./tsconfig.eslint.json). Also safe to set noEmit here.

 {
   "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "noEmit": true
+  },
   "include": [
     "src/**/*",
     "tests/**/*"
   ]
 }
files/empty-project/eslint.config.js (1)

1-6: Flat config is fine; tiny simplification possible

You can export the preset directly—defineConfig wrapper isn’t needed for a single item.

-import tsParticlesESLintConfig from "@tsparticles/eslint-config";
-import { defineConfig } from "eslint/config";
-
-export default defineConfig([
-    tsParticlesESLintConfig,
-]);
+import tsParticlesESLintConfig from "@tsparticles/eslint-config";
+export default tsParticlesESLintConfig;
vitest.config.ts (1)

3-10: LGTM; add a couple of quality-of-life defaults (optional)

Consider including .spec files and enabling automatic mock cleanup.

 export default defineConfig({
   test: {
     globals: true,
     environment: "node",
-    include: ["tests/**/*.test.ts"],
-    testTimeout: 30000
+    include: ["tests/**/*.{test,spec}.ts"],
+    testTimeout: 30000,
+    clearMocks: true,
+    restoreMocks: true
   }
 });
tests/file-utils.test.ts (2)

74-88: Use rejects.toThrow instead of manual try/catch in async error test

Cleaner, and assertions run even if the promise resolves unexpectedly.

-        it("should throw exception", async () => {
-            await fs.writeFile(path.join(baseDir, "baz", "tmp.txt"), "");
-
-            let ex = false;
-
-            try {
-                await getDestinationDir(path.join(baseDir, "baz"));
-
-                console.log("never");
-            } catch {
-                ex = true;
-            }
-
-            expect(ex).toBe(true);
-        });
+        it("should throw exception", async () => {
+            await fs.writeFile(path.join(baseDir, "baz", "tmp.txt"), "");
+            await expect(getDestinationDir(path.join(baseDir, "baz")))
+                .rejects.toThrow("Destination folder already exists and is not empty");
+        });

1-4: ESM-safe __dirname usage (optional)

Under pure ESM, __dirname isn’t defined. Make tests robust by deriving it from import.meta.url and avoiding reliance on Node globals.

-import { describe, it, expect, after } from "vitest";
+import { describe, it, expect, beforeAll, afterAll } from "vitest";
 import fs from "fs-extra";
-import path from "path";
+import path from "path";
+import { fileURLToPath } from "node:url";
@@
-describe("file-utils", async () => {
-    const baseDir = path.join(__dirname, "tmp-files");
+const testDir = path.dirname(fileURLToPath(import.meta.url));
+
+describe("file-utils", () => {
+    const baseDir = path.join(testDir, "tmp-files");

Also applies to: 11-13

src/build/build-circular-deps.ts (1)

23-29: LGTM; tiny improvement to support .mts files

Given the ESM migration, include .mts in madge extensions so circular checks see module-suffixed TS files too.

-                fileExtensions: ["ts"],
+                fileExtensions: ["ts", "mts"],
tests/create-shape.test.ts (2)

16-16: Ensure cleanup runs even on failure.

Wrap test bodies in try/finally to always remove tmp dirs.

-        await fs.remove(destDir);
+    } finally {
+        await fs.remove(destDir);
+    }

Also applies to: 28-28


8-9: __dirname under ESM may be undefined—verify test runtime.

If tests run as ESM, switch to fileURLToPath(import.meta.url) to compute __dirname.

+import { fileURLToPath } from "url";
+const __dirname = path.dirname(fileURLToPath(import.meta.url));

Also applies to: 20-21

src/build/build-eslint.ts (1)

28-31: Minor: guard undefined line/column.

Not required, but safer formatting avoids “undefined” when positions are missing.

-                    .map(m => `${t.filePath} (${m.line.toString()},${m.column.toString()}): ${m.message}`)
+                    .map(m => `${t.filePath} (${m.line ?? "?"},${m.column ?? "?"}): ${m.message}`)
eslint.config.js (2)

11-17: Place sourceType at languageOptions level (Flat Config).

sourceType should be a sibling of parserOptions, not inside it.

-        languageOptions: {
-            parserOptions: {
-                project: [path.resolve(__dirname, "src/tsconfig.json")],
-                tsconfigRootDir: __dirname,
-                sourceType: "module"
-            }
-        },
+        languageOptions: {
+            sourceType: "module",
+            parserOptions: {
+                project: [path.resolve(__dirname, "src/tsconfig.json")],
+                tsconfigRootDir: __dirname
+            }
+        },

8-22: Optional: scope overrides with files globs.

If you want this config to target only src TS files, add files: ["src/**/*.{ts,tsx}"] to the override block.

 export default defineConfig([
     tsParticlesESLintConfig,
     {
+        files: ["src/**/*.{ts,tsx}"],
         languageOptions: {
.github/workflows/node.js-ci.yml (2)

60-61: Align pnpm/action-setup versions.

Use v4 in both jobs for consistency.

-      - uses: pnpm/action-setup@v3
+      - uses: pnpm/action-setup@v4

45-47: Reproducibility: prefer frozen lockfile in CI.

Avoids drift from lockfile and surprise deps.

-      - run: pnpm install --no-frozen-lockfile
+      - run: pnpm install --frozen-lockfile

Also applies to: 83-85

src/build/build-bundle.ts (1)

26-33: Minor: reuse Error instance without shadowing.

Not harmful, but avoid reusing the name err for clarity.

-                    const err = new Error("No stats returned from webpack");
+                    const noStatsErr = new Error("No stats returned from webpack");
 
-                    console.error(err);
-                    reject(err);
+                    console.error(noStatsErr);
+                    reject(noStatsErr);
files/empty-project/webpack.config.js (1)

8-9: Consider importing JSON instead of sync FS read (minor).

Node supports JSON modules; this avoids sync I/O and a manual parse.

Example change:

-import fs from 'fs';
+import fs from 'fs';
+// or, when feasible
+// import pkg from './package.json' assert { type: 'json' };

-const __filename = fileURLToPath(import.meta.url),
-    __dirname = path.dirname(__filename),
-    pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')),
-    version = pkg.version;
+const __filename = fileURLToPath(import.meta.url),
+    __dirname = path.dirname(__filename),
+    pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')),
+    version = pkg.version;
// If using JSON import:
// const __filename = fileURLToPath(import.meta.url),
//     __dirname = path.dirname(__filename),
//     { version } = pkg;
src/build/build-tsc.ts (3)

128-131: Surface tsconfig parse diagnostics to the console.

Without printing, callers only see a non-zero exit code.

Apply:

-    if (parsed.errors.length) {
-        return ExitCodes.ParseError;
-    }
+    if (parsed.errors.length) {
+        const host = {
+            getCurrentDirectory: ts.sys.getCurrentDirectory,
+            getNewLine: () => ts.sys.newLine,
+            getCanonicalFileName: (f: string) => f,
+        };
+
+        console.error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, host));
+        return ExitCodes.ParseError;
+    }

138-156: Prefer TypeScript’s formatted diagnostics for consistency (optional).

Manual line/column formatting works, but formatDiagnosticsWithColorAndContext yields clearer, standardized output.

If you keep the manual formatter, consider removing the superfluous .toString() calls on numbers for brevity.


160-160: Avoid locale-dependent exit code formatting in logs (tiny nit).

toLocaleString() can add separators; not ideal for copy/paste or parsing.

-    console.log(`TSC for ${type} done with exit code: '${exitCode.toLocaleString()}'.`);
+    console.log(`TSC for ${type} done with exit code: ${exitCode}.`);
src/build/build.ts (1)

112-127: Stat messages are fine; consider thresholds and units (optional).

Switching warn/info based on increase is nice. Optionally add a threshold (e.g., >1KB) and human-readable units to reduce noise.

scripts/postversion.js (1)

12-14: Use English in code comments for consistency.

Aligns with the rest of the codebase and helps external contributors.

-        // usa readJson/writeJson di fs-extra per evitare parse manuale e problemi di typing
+        // Use fs-extra readJson/writeJson to avoid manual parsing and typing issues
package.json (3)

24-31: Script names vs behavior (minor).

build:ts:cjs suggests CommonJS, but the repo is ESM ("type": "module"). Consider renaming for clarity.

-    "build:ts:cjs": "tsc -p src",
+    "build:ts:esm": "tsc -p src",
+    "build:ts": "pnpm run build:ts:esm",

14-17: Prettier scripts only target top-level src files.

Globs like ./src/* miss nested folders.

-    "prettify:ci:src": "prettier --check ./src/*",
-    "prettify:src": "prettier --write ./src/*",
+    "prettify:ci:src": "prettier --check \"src/**/*\"",
+    "prettify:src": "prettier --write \"src/**/*\"",

1-81: Document runtime requirements (optional).

Given ESM and toolchain, declare supported Node/PNPM versions to prevent install/run surprises.

   "publishConfig": {
     "access": "public"
   },
+  "engines": {
+    "node": ">=22"
+  },
+  "packageManager": "pnpm@10"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0aa1669 and 431b685.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • .eslintignore (0 hunks)
  • .eslintrc.js (0 hunks)
  • .github/workflows/node.js-ci.yml (2 hunks)
  • eslint.config.js (1 hunks)
  • files/empty-project/.eslintignore (0 hunks)
  • files/empty-project/.eslintrc.js (0 hunks)
  • files/empty-project/eslint.config.js (1 hunks)
  • files/empty-project/package.dist.json (1 hunks)
  • files/empty-project/package.json (2 hunks)
  • files/empty-project/webpack.config.js (1 hunks)
  • package.json (2 hunks)
  • pnpm-workspace.yaml (1 hunks)
  • scripts/postversion.js (1 hunks)
  • src/build/build-bundle.ts (2 hunks)
  • src/build/build-circular-deps.ts (1 hunks)
  • src/build/build-eslint.ts (3 hunks)
  • src/build/build-tsc.ts (5 hunks)
  • src/build/build.ts (3 hunks)
  • src/tsconfig.json (1 hunks)
  • src/utils/file-utils.ts (1 hunks)
  • tests/create-plugin.test.ts (2 hunks)
  • tests/create-preset.test.ts (2 hunks)
  • tests/create-shape.test.ts (2 hunks)
  • tests/file-utils.test.ts (4 hunks)
  • tests/string-utils.test.ts (1 hunks)
  • tests/tsconfig.json (2 hunks)
  • tsconfig.eslint.json (1 hunks)
  • tsconfig.json (1 hunks)
  • vitest.config.ts (1 hunks)
💤 Files with no reviewable changes (4)
  • .eslintrc.js
  • .eslintignore
  • files/empty-project/.eslintignore
  • files/empty-project/.eslintrc.js
🧰 Additional context used
🧬 Code graph analysis (8)
tests/create-preset.test.ts (1)
src/create/preset/create-preset.ts (1)
  • createPresetTemplate (180-204)
tests/string-utils.test.ts (1)
src/utils/string-utils.ts (3)
  • capitalize (7-18)
  • camelize (26-28)
  • dash (35-41)
tests/create-shape.test.ts (1)
src/create/shape/create-shape.ts (1)
  • createShapeTemplate (161-184)
scripts/postversion.js (1)
eslint.config.js (1)
  • __dirname (3-3)
tests/file-utils.test.ts (1)
src/utils/file-utils.ts (2)
  • getDestinationDir (49-65)
  • getRepositoryUrl (70-86)
src/build/build.ts (1)
src/build/build-prettier.ts (3)
  • prettifyReadme (144-177)
  • prettifyPackageJson (60-94)
  • prettifyPackageDistJson (101-137)
tests/create-plugin.test.ts (1)
src/create/plugin/create-plugin.ts (1)
  • createPluginTemplate (162-185)
files/empty-project/webpack.config.js (2)
eslint.config.js (1)
  • __dirname (3-3)
scripts/postversion.js (1)
  • pkg (13-14)
🪛 GitHub Actions: Node.js CI
tests/create-preset.test.ts

[error] 1-1: Test timed out in 30000ms for 'should have created the preset project'

tests/string-utils.test.ts

[error] 1-1: Error: No test found in suite empty string

tests/create-shape.test.ts

[error] 1-1: Test timed out in 30000ms for 'should have created the shape project'

tests/file-utils.test.ts

[error] 97-97: TypeError: after is not a function

tests/create-plugin.test.ts

[error] 1-1: Test timed out in 30000ms for 'should have created the plugin project'


[error] 1-1: Test timed out in 30000ms for 'should have created the plugin project, w/ repo'

🔇 Additional comments (9)
files/empty-project/package.json (1)

87-87: Dev bump to @tsparticles/cli v3 looks fine.

Assuming CI is already on Node 22 (as per PR), this is good.

Please confirm the template’s engines (if any) and CI matrix include Node >=20/22 to match CLI v3 requirements.

tests/tsconfig.json (1)

6-6: Vitest types switch looks good.
The migration from Mocha/Chai to Vitest types is correct.

src/build/build-eslint.ts (1)

15-15: Use Flat Config programmatically: overrideConfig + useFlatConfig (not baseConfig).

Current code passes a Flat config array to baseConfig (legacy); this can misconfigure ESLint under v9.

-        const eslint = new ESLint({ baseConfig: defineConfig([tsParticlesESLintConfig]), fix: !ci });
+        const eslint = new ESLint({
+            useFlatConfig: true,
+            overrideConfig: defineConfig([tsParticlesESLintConfig]),
+            fix: !ci,
+        });

If tsParticlesESLintConfig already is a defineConfig([...]) result, you can pass it directly to overrideConfig without wrapping again.

Likely an incorrect or invalid review comment.

files/empty-project/webpack.config.js (1)

6-9: ESM dirname/filename computation looks correct.

The migration away from CJS globals is handled properly; reading the local package.json is fine for a config-time read.

src/build/build-tsc.ts (1)

4-10: Good call switching from const enum to enum for ESM compatibility.

Prevents inlining pitfalls and keeps runtime symbols available under modern module targets.

src/build/build.ts (1)

57-61: Prettify step gating is correct.

Short-circuiting on failure via canContinue is clear and predictable.

scripts/postversion.js (1)

16-23: Async flow and error handling look solid.

Paths resolve correctly; devDependencies is ensured; writeJson uses spacing. LGTM.

package.json (2)

3-3: Version/tag alignment check.

The PR title says 3.0.0, but version is 3.0.1. Confirm intended release version and tag.

Would you like me to open a follow-up to sync the PR title/changelog with 3.0.1?


79-80: Confirm intent of main pointing to ESLint config.

If consumers import this package for its CLI only, main may be unnecessary; if you intend to expose the config, consider adding an exports map.

Optionally:

{
  "exports": {
    ".": "./eslint.config.js"
  }
}

package.json Outdated
Comment on lines 57 to 59
"@babel/core": "^7.28.3",
"@tsparticles/cli": "^2.3.3",
"@tsparticles/engine": "^3.9.1",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove self-dependency from devDependencies.

The package lists itself as a devDependency, which can cause resolution issues.

   "devDependencies": {
-    "@babel/core": "^7.28.3",
-    "@tsparticles/cli": "^2.3.3",
+    "@babel/core": "^7.28.3",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@babel/core": "^7.28.3",
"@tsparticles/cli": "^2.3.3",
"@tsparticles/engine": "^3.9.1",
"@babel/core": "^7.28.3",
"@tsparticles/engine": "^3.9.1",
🤖 Prompt for AI Agents
package.json around lines 57-59: remove the package's own name from
devDependencies (the self-dependency entry) so the project does not list itself
as a devDependency; delete that dependency line, save package.json, then run
your package manager (npm install or yarn install) to update node_modules and
the lockfile and verify package.json remains valid.

Comment on lines +90 to 94
canContinue =
(await prettifyReadme(basePath, ci)) &&
(await prettifyPackageJson(basePath, ci)) &&
(await prettifyPackageDistJson(basePath, ci));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Skip writing package.dist.json in CI mode to keep CI read-only.

prettifyPackageDistJson currently writes regardless of ci, which can dirty the tree during --ci runs.

Apply:

-        canContinue =
-            (await prettifyReadme(basePath, ci)) &&
-            (await prettifyPackageJson(basePath, ci)) &&
-            (await prettifyPackageDistJson(basePath, ci));
+        canContinue =
+            (await prettifyReadme(basePath, ci)) &&
+            (await prettifyPackageJson(basePath, ci)) &&
+            (!ci ? await prettifyPackageDistJson(basePath, ci) : true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
canContinue =
(await prettifyReadme(basePath, ci)) &&
(await prettifyPackageJson(basePath, ci)) &&
(await prettifyPackageDistJson(basePath, ci));
}
canContinue =
(await prettifyReadme(basePath, ci)) &&
(await prettifyPackageJson(basePath, ci)) &&
(!ci ? await prettifyPackageDistJson(basePath, ci) : true);
}
🤖 Prompt for AI Agents
In src/build/build.ts around lines 90 to 94, the call to prettifyPackageDistJson
currently runs during CI and mutates files; update the logic to skip
prettifyPackageDistJson when ci is true (only run it when ci is false) so CI
mode remains read-only. Modify the condition so canContinue awaits
prettifyReadme and prettifyPackageJson always, but only awaits
prettifyPackageDistJson when ci is false (e.g., || short-circuit or conditional
call), ensuring no writes occur in CI and preserving the original success
boolean semantics.

@@ -1,18 +1,17 @@
import { describe, it } from "mocha";
import { expect } from "chai";
import { describe, it, expect } from "vitest";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Provide a fast path in the generator to skip install/build.
Expose a skipInstallBuild option (default false) so tests don’t spawn package installs/builds.

Here’s a minimal pattern to apply in src/create/plugin/create-plugin.ts (illustrative):

export async function createPluginTemplate(
  name: string,
  description: string,
  repoUrl: string,
  destPath: string,
  opts: { skipInstallBuild?: boolean } = {}
): Promise<void> {
  // ...copy work...
  if (!opts.skipInstallBuild) {
    await runInstall(destPath);
    await runBuild(destPath);
  }
}

Then in tests:

await createPluginTemplate("foo", "Foo", "", destDir, { skipInstallBuild: true });
🧰 Tools
🪛 GitHub Actions: Node.js CI

[error] 1-1: Test timed out in 30000ms for 'should have created the plugin project'


[error] 1-1: Test timed out in 30000ms for 'should have created the plugin project, w/ repo'

🤖 Prompt for AI Agents
In tests/create-plugin.test.ts around line 1, the test suite currently invokes
the plugin generator which spawns package installs/builds making tests slow; add
a fast-path option to the generator and update tests to use it. In
src/create/plugin/create-plugin.ts (or the module exporting the generator) add
an opts parameter typed { skipInstallBuild?: boolean } with default false, and
wrap the install/build calls in if (!opts.skipInstallBuild) { await
runInstall(...); await runBuild(...); } so behavior is unchanged by default.
Update any exported types/signatures and call sites accordingly, and change
tests/create-plugin.test.ts to call the generator with { skipInstallBuild: true
} to avoid running installs/builds during tests. Ensure TypeScript typings and
all internal uses propagate the optional param to avoid type errors.

Comment on lines 7 to 17
it("should have created the plugin project", async () => {
const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-plugin"));

await createPluginTemplate("foo", "Foo", "", destDir);

const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));

expect(pkgInfo.name).to.be.equal("tsparticles-plugin-foo");
expect(pkgInfo.name).toBe("tsparticles-plugin-foo");

await fs.remove(destDir);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix CI timeouts by avoiding long installs/builds in unit tests.
Both tests are timing out (30s) due to runInstall/runBuild. Make them serial, add per-test timeouts, and ensure cleanup with finally.

Apply this diff:

-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from "vitest";
 import { createPluginTemplate } from "../src/create/plugin/create-plugin";
 import path from "path";
 import fs from "fs-extra";

-describe("create-plugin", () => {
-    it("should have created the plugin project", async () => {
-        const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-plugin"));
-
-        await createPluginTemplate("foo", "Foo", "", destDir);
-
-        const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
-
-        expect(pkgInfo.name).toBe("tsparticles-plugin-foo");
-
-        await fs.remove(destDir);
-    });
+describe.sequential("create-plugin", () => {
+    it("should have created the plugin project", async () => {
+        const destDir = path.resolve(__dirname, "tmp-files", "foo-plugin");
+        try {
+            await createPluginTemplate("foo", "Foo", "", destDir);
+            const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
+            expect(pkgInfo.name).toBe("tsparticles-plugin-foo");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);

-    it("should have created the plugin project, w/ repo", async () => {
-        const destDir = path.resolve(path.join(__dirname, "tmp-files", "bar-plugin"));
-
-        await createPluginTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir);
-
-        const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
-
-        expect(pkgInfo.name).toBe("tsparticles-plugin-bar");
-
-        await fs.remove(destDir);
-    });
+    it("should have created the plugin project, w/ repo", async () => {
+        const destDir = path.resolve(__dirname, "tmp-files", "bar-plugin");
+        try {
+            await createPluginTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir);
+            const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
+            expect(pkgInfo.name).toBe("tsparticles-plugin-bar");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);
 });

Follow-up (strongly recommended):

  • Inject an option to skip install/build in template generators and use it in tests (see next comment).
  • Alternatively, raise test.testTimeout/hookTimeout in vitest.config.ts for this suite.

Also applies to: 19-29

🤖 Prompt for AI Agents
In tests/create-plugin.test.ts around lines 7-17 (and similarly 19-29), the
tests are timing out because they perform long installs/builds; make the tests
run serially, add per-test timeouts, and ensure cleanup runs regardless of
failures: convert the concurrent tests to run sequentially (use
sequential/single test execution or avoid parallel test runner features), wrap
the createPluginTemplate call and assertions in try/finally and move
fs.remove(destDir) into the finally block, set a shorter per-test timeout (e.g.,
pass a timeout param to the test or use the test framework's per-test timeout
API) and avoid invoking runInstall/runBuild in unit tests by passing an option
to skip install/build to createPluginTemplate (or stub/mock those calls) so
tests complete quickly.

Comment on lines 7 to 17
it("should have created the preset project", async () => {
const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-preset"));

await createPresetTemplate("foo", "Foo", "", destDir);

const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));

expect(pkgInfo.name).to.be.equal("tsparticles-preset-foo");
expect(pkgInfo.name).toBe("tsparticles-preset-foo");

await fs.remove(destDir);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Same timeout fix pattern as plugin tests.
Serialize, add per-test timeout, and guard cleanup.

Apply this diff:

-import { describe, it, expect } from "vitest";
+import { describe, it, expect } from "vitest";
 import { createPresetTemplate } from "../src/create/preset/create-preset";
 import path from "path";
 import fs from "fs-extra";

-describe("create-preset", () => {
-    it("should have created the preset project", async () => {
-        const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-preset"));
-
-        await createPresetTemplate("foo", "Foo", "", destDir);
-
-        const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
-
-        expect(pkgInfo.name).toBe("tsparticles-preset-foo");
-
-        await fs.remove(destDir);
-    });
+describe.sequential("create-preset", () => {
+    it("should have created the preset project", async () => {
+        const destDir = path.resolve(__dirname, "tmp-files", "foo-preset");
+        try {
+            await createPresetTemplate("foo", "Foo", "", destDir);
+            const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
+            expect(pkgInfo.name).toBe("tsparticles-preset-foo");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);

-    it("should have created the preset project, w/ repo", async () => {
-        const destDir = path.resolve(path.join(__dirname, "tmp-files", "bar-preset"));
-
-        await createPresetTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir);
-
-        const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
-
-        expect(pkgInfo.name).toBe("tsparticles-preset-bar");
-
-        await fs.remove(destDir);
-    });
+    it("should have created the preset project, w/ repo", async () => {
+        const destDir = path.resolve(__dirname, "tmp-files", "bar-preset");
+        try {
+            await createPresetTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir);
+            const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
+            expect(pkgInfo.name).toBe("tsparticles-preset-bar");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);
 });

Follow-up: add the same skipInstallBuild option to createPresetTemplate and use it in tests.

Also applies to: 19-29

🤖 Prompt for AI Agents
In tests/create-preset.test.ts around lines 7 to 17 (and similarly for lines 19
to 29), the test needs the same timeout/serialization and cleanup guards as the
plugin tests: make the test run sequentially by ensuring it doesn't rely on
shared async state, add a per-test timeout (e.g. jest.setTimeout or the test
timeout parameter) to avoid hanging, wrap the cleanup (fs.remove) in a finally
block so destDir is removed even if assertions fail, and call
createPresetTemplate with a new skipInstallBuild boolean option (add this option
to createPresetTemplate signature and implementation) then pass
skipInstallBuild: true from the test to avoid installs/builds during testing.

Comment on lines 7 to 17
it("should have created the shape project", async () => {
const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-shape"));

await createShapeTemplate("foo", "Foo", "", destDir);

const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));

expect(pkgInfo.name).to.be.equal("tsparticles-shape-foo");
expect(pkgInfo.name).toBe("tsparticles-shape-foo");

await fs.remove(destDir);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix flakiness: these tests run install/build and hit the 30s timeout.

Either stub install/build or raise the per-test timeout. Minimal fix below.

-    it("should have created the shape project", async () => {
+    it("should have created the shape project", async () => {
       const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-shape"));
 
       await createShapeTemplate("foo", "Foo", "", destDir);
 
       const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
 
       expect(pkgInfo.name).toBe("tsparticles-shape-foo");
 
       await fs.remove(destDir);
-    });
+    }, 180_000);

Alternative (preferred): mock the heavy steps inside createShapeTemplate so unit tests don’t perform network/IO. I can provide a vi.mock-based approach if you want.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("should have created the shape project", async () => {
const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-shape"));
await createShapeTemplate("foo", "Foo", "", destDir);
const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
expect(pkgInfo.name).to.be.equal("tsparticles-shape-foo");
expect(pkgInfo.name).toBe("tsparticles-shape-foo");
await fs.remove(destDir);
});
it("should have created the shape project", async () => {
const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-shape"));
await createShapeTemplate("foo", "Foo", "", destDir);
const pkgInfo = await fs.readJSON(path.join(destDir, "package.json"));
expect(pkgInfo.name).toBe("tsparticles-shape-foo");
await fs.remove(destDir);
}, 180_000);
🤖 Prompt for AI Agents
In tests/create-shape.test.ts around lines 7-17, the test runs install/build
steps and intermittently times out; either stub/mock the heavy install/build
operations inside createShapeTemplate so the unit test doesn’t perform
network/IO, or as a minimal change increase the per-test timeout (e.g., call
jest.setTimeout(...) or pass a longer timeout to the it(...) call) so the test
has enough time; preferred: replace or vi.mock the functions that perform
install/build within createShapeTemplate and assert they were called, keeping
the test fast and deterministic.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/create/preset/create-preset.ts (1)

151-154: Bug: missing closing quote in README token replacement.

Generates invalid JSON/YAML snippet in README.

-            {
-                from: /preset: "template"/g,
-                to: `preset: "${camelizedName}`,
-            },
+            {
+                from: /preset: "template"/g,
+                to: `preset: "${camelizedName}"`,
+            },
src/create/shape/create-shape.ts (3)

167-167: __dirname will be undefined under ESM; compute it via import.meta.url.

This will crash at runtime after the ESM migration.

Apply:

-import path from "path";
+import path from "path";
+import { fileURLToPath } from "url";
@@
-export async function createShapeTemplate(
+export async function createShapeTemplate(
@@
-    const sourcePath = path.resolve(__dirname, "..", "..", "..", "files", "create-shape");
+    const __dirname = path.dirname(fileURLToPath(import.meta.url));
+    const sourcePath = path.resolve(__dirname, "..", "..", "..", "files", "create-shape");

100-108: GitHub repoPath extraction breaks when URL lacks “.git”.

For common URLs like https://github.com/user/repo, substring with endIndex -1 will produce a wrong path.

Use a safer extraction:

-        stringSearch = "github.com",
-        trailingSlashSearch = "github.com/",
-        repoPath = repoUrl.includes(stringSearch)
-            ? repoUrl.substring(
-                  repoUrl.indexOf(trailingSlashSearch) + trailingSlashSearch.length,
-                  repoUrl.indexOf(".git"),
-              )
-            : "tsparticles/shape-template";
+        ghPrefix = "github.com/",
+        ghStart = repoUrl.indexOf(ghPrefix),
+        dotGit = repoUrl.indexOf(".git"),
+        repoPath =
+            ghStart >= 0
+                ? repoUrl
+                      .substring(ghStart + ghPrefix.length, dotGit > ghStart ? dotGit : undefined)
+                      .replace(/\/$/, "")
+                : "tsparticles/shape-template";

133-135: Token replacement is missing a closing quote.

Generates invalid README snippet: shape.type: "foo

-            {
-                from: /shape\.type: "template"/g,
-                to: `shape.type: "${camelizedName}`,
-            },
+            {
+                from: /shape\.type: "template"/g,
+                to: `shape.type: "${camelizedName}"`,
+            },
♻️ Duplicate comments (1)
package.json (1)

57-59: Remove self-dependency from devDependencies.

Listing @tsparticles/cli as a devDependency can cause resolver loops.

   "devDependencies": {
-    "@babel/core": "^7.28.3",
-    "@tsparticles/cli": "^2.3.3",
+    "@babel/core": "^7.28.3",
🧹 Nitpick comments (10)
package.json (4)

14-17: Prettier scripts only cover top-level files; recurse into subfolders.

-    "prettify:ci:src": "prettier --check ./src/*",
+    "prettify:ci:src": "prettier --check \"src/**/*.{ts,tsx,js,jsx}\"",
-    "prettify:src": "prettier --write ./src/*",
+    "prettify:src": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\"",

23-24: Rename script alias to reflect ESM (nit).

"build:ts:cjs" is misleading after the ESM migration.

-    "build:ts": "pnpm run build:ts:cjs",
-    "build:ts:cjs": "tsc -p src",
+    "build:ts": "pnpm run build:ts:esm",
+    "build:ts:esm": "tsc -p src",

26-26: Make chmod step cross-platform or move to a Node script.

Raw chmod can fail on Windows.

-    "build": "pnpm run clear:dist && pnpm run prettify:src && pnpm run lint && pnpm run compile && pnpm run circular-deps && pnpm run prettify:readme && chmod +x dist/cli.js && chmod +x dist/build/build.js && chmod +x dist/create/create.js && chmod +x dist/create/preset/preset.js",
+    "build": "pnpm run clear:dist && pnpm run prettify:src && pnpm run lint && pnpm run compile && pnpm run circular-deps && pnpm run prettify:readme && node scripts/make-executable.js",

If you want, I can provide scripts/make-executable.js.


1-11: Consider declaring supported Node and pnpm versions.

Add engines to communicate runtime requirements.

   "publishConfig": {
     "access": "public"
   },
+  "engines": {
+    "node": ">=20",
+    "pnpm": ">=10"
+  },
src/create/preset/create-preset.ts (1)

121-127: Repo URL parsing is brittle; use URL().

Handles non-.git HTTPS/SSH forms safely.

-        repoPath = repoUrl.includes(stringSearch)
-            ? repoUrl.substring(
-                  repoUrl.indexOf(trailingSlashSearch) + trailingSlashSearch.length,
-                  repoUrl.indexOf(".git"),
-              )
-            : "tsparticles/preset-template";
+        repoPath = (() => {
+            try {
+                const u = new URL(repoUrl.replace(/^git\+/, ""));
+                return u.hostname === "github.com" ? u.pathname.replace(/^\/+|\.git$/g, "") : "tsparticles/preset-template";
+            } catch {
+                return "tsparticles/preset-template";
+            }
+        })();
src/utils/template-utils.ts (1)

155-177: Prefer pnpm when available; add timeouts to exec.

Make installs respect local PM, and avoid hanging processes.

 export async function runInstall(destPath: string): Promise<void> {
-    if (!(await lookpath("npm"))) {
-        return;
-    }
+    const pm = (await lookpath("pnpm")) ? "pnpm" : (await lookpath("npm")) ? "npm" : (await lookpath("yarn")) ? "yarn" : undefined;
+    if (!pm) return;
@@
-        exec(
-            "npm install",
+        exec(
+            pm === "yarn" ? "yarn install" : `${pm} install`,
             {
                 cwd: destPath,
+                timeout: 15 * 60 * 1000,
+                maxBuffer: 10 * 1024 * 1024
             },
             error => {
@@
 export async function runBuild(destPath: string): Promise<void> {
-    if (!(await lookpath("npm"))) {
-        return;
-    }
+    const pm = (await lookpath("pnpm")) ? "pnpm" : (await lookpath("npm")) ? "npm" : (await lookpath("yarn")) ? "yarn" : undefined;
+    if (!pm) return;
@@
-        exec(
-            "npm run build",
+        exec(
+            pm === "yarn" ? "yarn build" : `${pm} run build`,
             {
                 cwd: destPath,
+                timeout: 15 * 60 * 1000,
+                maxBuffer: 10 * 1024 * 1024
             },
             error => {

Also applies to: 183-205

src/create/plugin/create-plugin.ts (1)

52-53: Minor: redundant camelize calls; align with preset variant for readability.

-    const camelizedName = camelize(camelize(name, "-"), " "),
+    const camelizedName = camelize(name, "-", " "),

Apply in both functions.

Also applies to: 77-78

src/create/plugin/plugin.ts (1)

38-45: Gracefully handle prompt cancellation to avoid .trim() on undefined.

If the user aborts prompts, destructured fields can be undefined and cause a crash.

-        { name, description, repositoryUrl } = (await prompts(questions)) as {
+        { name, description, repositoryUrl } = (await prompts(questions, {
+            onCancel: () => {
+                process.exit(1);
+            },
+        })) as {
src/create/preset/preset.ts (1)

38-45: Handle prompt cancellation to prevent runtime errors.

Same issue as plugin: .trim() on possibly undefined.

-        { name, description, repositoryUrl } = (await prompts(questions)) as {
+        { name, description, repositoryUrl } = (await prompts(questions, {
+            onCancel: () => {
+                process.exit(1);
+            },
+        })) as {
src/create/shape/shape.ts (1)

38-45: Handle prompt cancellation to avoid .trim() on undefined.

-        { name, description, repositoryUrl } = (await prompts(questions)) as {
+        { name, description, repositoryUrl } = (await prompts(questions, {
+            onCancel: () => {
+                process.exit(1);
+            },
+        })) as {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 431b685 and 61bd03e.

📒 Files selected for processing (11)
  • files/empty-project/package.json (2 hunks)
  • package.json (2 hunks)
  • src/cli.ts (1 hunks)
  • src/create/create.ts (1 hunks)
  • src/create/plugin/create-plugin.ts (2 hunks)
  • src/create/plugin/plugin.ts (1 hunks)
  • src/create/preset/create-preset.ts (2 hunks)
  • src/create/preset/preset.ts (1 hunks)
  • src/create/shape/create-shape.ts (2 hunks)
  • src/create/shape/shape.ts (1 hunks)
  • src/utils/template-utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • files/empty-project/package.json
🔇 Additional comments (12)
package.json (2)

5-5: ESM switch acknowledged.

"type": "module" aligns with the repo-wide migration. Nothing to fix here.


25-25: Vitest migration looks good.

"test": "vitest run" is correct for CI usage.

src/cli.ts (2)

2-3: ESM import paths (.js) look good.


4-4: Replace direct JSON import with createRequire in ESM

-import pkgInfo from "../package.json";
+import { createRequire } from "node:module";
+const require = createRequire(import.meta.url);
+const pkgInfo = require("../package.json");

package.json is always included in the published tarball; no changes needed to “files”.

src/utils/template-utils.ts (1)

5-5: ESM import path fix (.js) looks good.

src/create/plugin/create-plugin.ts (1)

1-1: ESM import path updates look good.

Also applies to: 10-10, 13-13

src/create/shape/create-shape.ts (2)

1-1: ESM import extension alignment looks good.

The .js specifiers are correct for Node ESM and tsconfig nodenext/bundler.

Also applies to: 10-10, 13-13


150-152: Confirm webpack entry function name.

Index tokenization creates load${CapitalizedName}Shape but webpack is fed "loadParticlesShape". Likely mismatch.

If intended to point to the specific loader, update:

-async function updateShapeWebpackFile(destPath: string, name: string, description: string): Promise<void> {
-    await updateWebpackFile(destPath, camelize(capitalize(name, "-", " ")), description, "loadParticlesShape");
-}
+async function updateShapeWebpackFile(destPath: string, name: string, description: string): Promise<void> {
+    const capitalizedName = capitalize(name, "-", " ");
+    await updateWebpackFile(destPath, camelize(capitalizedName), description, `load${capitalizedName}Shape`);
+}
src/create/plugin/plugin.ts (1)

1-1: ESM import extension updates: OK.

Also applies to: 4-4, 5-5

src/create/create.ts (1)

2-4: .js imports for ESM resolution look correct.

src/create/preset/preset.ts (1)

1-1: ESM import extension updates: OK.

Also applies to: 4-4, 5-5

src/create/shape/shape.ts (1)

1-1: ESM import extension updates: OK.

Also applies to: 4-4, 5-5

package.json Outdated
Comment on lines 33 to 55
"@tsparticles/eslint-config": "^3.0.3",
"@tsparticles/prettier-config": "^3.0.1",
"@tsparticles/tsconfig": "^3.0.1",
"@tsparticles/webpack-plugin": "^3.0.3",
"commander": "^14.0.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^54.1.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-tsdoc": "^0.4.0",
"fs-extra": "^11.3.1",
"klaw": "^4.1.0",
"lookpath": "^1.2.2",
"madge": "^6.1.0",
"path-scurry": "^1.10.1",
"prettier": "^3.2.5",
"prettier-plugin-multiline-arrays": "^3.0.4",
"lookpath": "^1.2.3",
"madge": "^8.0.0",
"path-scurry": "^2.0.0",
"prettier": "^3.6.2",
"prettier-plugin-multiline-arrays": "^4.0.3",
"prompts": "^2.4.2",
"rimraf": "^5.0.5",
"typescript": "^5.4.2",
"webpack": "^5.90.3"
"rimraf": "^6.0.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.41.0",
"webpack": "^5.101.3"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Dev-only tools are in dependencies; move them to devDependencies.

Shipping eslint/prettier/webpack/typescript/madge/etc. as runtime deps bloats consumers and slows installs.

Apply this diff (keep only runtime deps in "dependencies"):

   "dependencies": {
-    "@tsparticles/eslint-config": "^3.0.3",
-    "@tsparticles/prettier-config": "^3.0.1",
-    "@tsparticles/tsconfig": "^3.0.1",
-    "@tsparticles/webpack-plugin": "^3.0.3",
     "commander": "^14.0.0",
-    "eslint": "^9.34.0",
-    "eslint-config-prettier": "^10.1.8",
-    "eslint-plugin-jsdoc": "^54.1.1",
-    "eslint-plugin-prettier": "^5.5.4",
-    "eslint-plugin-tsdoc": "^0.4.0",
     "fs-extra": "^11.3.1",
     "klaw": "^4.1.0",
     "lookpath": "^1.2.3",
-    "madge": "^8.0.0",
-    "path-scurry": "^2.0.0",
-    "prettier": "^3.6.2",
-    "prettier-plugin-multiline-arrays": "^4.0.3",
     "prompts": "^2.4.2",
-    "rimraf": "^6.0.1",
-    "typescript": "^5.9.2",
-    "typescript-eslint": "^8.41.0",
-    "webpack": "^5.101.3"
+    "path-scurry": "^2.0.0"
   },
   "devDependencies": {
+    "@tsparticles/eslint-config": "^3.0.3",
+    "@tsparticles/prettier-config": "^3.0.1",
+    "@tsparticles/tsconfig": "^3.0.1",
+    "@tsparticles/webpack-plugin": "^3.0.3",
+    "eslint": "^9.34.0",
+    "eslint-config-prettier": "^10.1.8",
+    "eslint-plugin-jsdoc": "^54.1.1",
+    "eslint-plugin-prettier": "^5.5.4",
+    "eslint-plugin-tsdoc": "^0.4.0",
+    "madge": "^8.0.0",
+    "prettier": "^3.6.2",
+    "prettier-plugin-multiline-arrays": "^4.0.3",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.9.2",
+    "typescript-eslint": "^8.41.0",
+    "webpack": "^5.101.3"
   }

Run to confirm none of these are imported at runtime:


🏁 Script executed:

#!/bin/bash
# Verify runtime imports under src/
rg -nP --type=ts -C1 '\b(madge|eslint|prettier|webpack|@tsparticles/webpack-plugin|rimraf|typescript|typescript-eslint)\b' src || true

Length of output: 9260


Move build & lint tooling to devDependencies
Shift ESLint, Prettier, Webpack, TypeScript, Madge, Rimraf and all @tsparticles/* configs/plugins into devDependencies; keep only CLI/runtime packages (commander, fs-extra, klaw, lookpath, prompts and path-scurry) under dependencies.

🤖 Prompt for AI Agents
In package.json around lines 33 to 55, several build and lint tools are
incorrectly listed under "dependencies"; move ESLint, Prettier, Webpack,
TypeScript, Madge, Rimraf and all @tsparticles/* packages and related tooling
(eslint, eslint-config-prettier, eslint-plugin-*,
prettier-plugin-multiline-arrays, typescript-eslint, webpack, madge, rimraf,
@tsparticles/eslint-config, @tsparticles/prettier-config, @tsparticles/tsconfig,
@tsparticles/webpack-plugin, prettier, typescript) into "devDependencies" and
leave only runtime/CLI packages (commander, fs-extra, klaw, lookpath, prompts,
path-scurry) under "dependencies"; update package.json accordingly and run the
package manager to reinstall or move these packages so lockfile and node_modules
reflect the change.

},
"description": "tsParticles CLI",
"main": ".eslintrc.js",
"main": "eslint.config.js",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

"main" points to eslint.config.js; set it to the CLI entry or drop it.

Current value breaks consumers importing the package.

-  "main": "eslint.config.js",
+  "main": "dist/cli.js",
+  "exports": {
+    ".": "./dist/cli.js",
+    "./package.json": "./package.json"
+  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"main": "eslint.config.js",
// before (around line 79)
- "main": "eslint.config.js",
// after
"main": "dist/cli.js",
"exports": {
".": "./dist/cli.js",
"./package.json": "./package.json"
},
🤖 Prompt for AI Agents
In package.json around line 79, the "main" field incorrectly points to
eslint.config.js which breaks consumers; update the "main" field to point to
your package's actual runtime/CLI entry (e.g., the built CommonJS entry like
"lib/index.js" or the published CLI entry), or remove the "main" field entirely
if the package is only a CLI and you expose binaries via "bin"; ensure the
chosen path exists in the published package and matches your build/output.

Comment on lines 11 to +13
import fs from "fs-extra";
import path from "path";
import { replaceTokensInFile } from "../../utils/file-utils";
import { replaceTokensInFile } from "../../utils/file-utils.js";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

__dirname is undefined in ESM; compute it from import.meta.url.

 import fs from "fs-extra";
 import path from "path";
+import { fileURLToPath } from "node:url";
+const DIRNAME = path.dirname(fileURLToPath(import.meta.url));
@@
-    const sourcePath = path.resolve(__dirname, "..", "..", "..", "files", "create-plugin");
+    const sourcePath = path.resolve(DIRNAME, "..", "..", "..", "files", "create-plugin");

Also applies to: 168-168

🤖 Prompt for AI Agents
In src/create/plugin/create-plugin.ts around lines 11-13 (and also at line 168),
__dirname is undefined because the file is running as an ESM module; import and
use fileURLToPath from 'url' and compute __filename and __dirname from
import.meta.url, then replace uses of __dirname with that computed value.
Specifically, add an import for fileURLToPath, compute const __filename =
fileURLToPath(import.meta.url) and const __dirname = path.dirname(__filename),
and update the code at the referenced locations to use this __dirname instead of
the undefined global.

Comment on lines 11 to +13
import fs from "fs-extra";
import path from "path";
import { replaceTokensInFile } from "../../utils/file-utils";
import { replaceTokensInFile } from "../../utils/file-utils.js";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

__dirname is undefined in ESM; compute it from import.meta.url.

 import fs from "fs-extra";
 import path from "path";
+import { fileURLToPath } from "node:url";
+const DIRNAME = path.dirname(fileURLToPath(import.meta.url));
@@
-    const sourcePath = path.resolve(__dirname, "..", "..", "..", "files", "create-preset");
+    const sourcePath = path.resolve(DIRNAME, "..", "..", "..", "files", "create-preset");

Also applies to: 186-186

🤖 Prompt for AI Agents
In src/create/preset/create-preset.ts around lines 11-13 (and also at line 186),
__dirname is undefined under ESM so compute it from import.meta.url instead of
relying on __dirname; add a const __filename = new URL(import.meta.url).pathname
(or use fileURLToPath(import.meta.url)) and const __dirname =
path.dirname(__filename) at the top of the module, then replace any use of the
undefined __dirname with this computed __dirname (and ensure to import
fileURLToPath from 'url' if you choose that helper).

@@ -2,7 +2,7 @@ import { exec } from "child_process";
import fs from "fs-extra";
import { lookpath } from "lookpath";
import path from "path";
import { replaceTokensInFile } from "./file-utils";
import { replaceTokensInFile } from "./file-utils.js";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

__dirname usage breaks under ESM; compute from import.meta.url.

 import path from "path";
+import { fileURLToPath } from "node:url";
 import { replaceTokensInFile } from "./file-utils.js";
+const DIRNAME = path.dirname(fileURLToPath(import.meta.url));
@@
-    await fs.copy(path.resolve(__dirname, "..", "..", "files", "empty-project"), destPath, {
+    await fs.copy(path.resolve(DIRNAME, "..", "..", "files", "empty-project"), destPath, {

Also applies to: 135-140

🤖 Prompt for AI Agents
In src/utils/template-utils.ts around line 5 (and also lines 135-140), the code
uses __dirname which breaks under ESM; replace usage by deriving the directory
from import.meta.url: import fileURLToPath from 'url' and use
fileURLToPath(import.meta.url) with path.dirname to produce a __dirname-like
variable, update any path.join calls to use that computed directory, and ensure
the top of the file imports the needed functions from 'url' and 'path' so all
file path computations work under ESM.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
package.json (3)

32-55: Move dev-only tooling out of runtime dependencies.

ESLint/Prettier/TypeScript/Webpack/Madge/etc. shouldn’t be in "dependencies" for a CLI; they bloat installs. Shift them to devDependencies (only keep commander, fs-extra, klaw, lookpath, prompts, path-scurry at runtime).

Run to confirm none are imported at runtime:

#!/bin/bash
# Search for runtime imports of tooling libs under src/
rg -nP --type=ts -C2 '\b(eslint|prettier|webpack|@tsparticles/webpack-plugin|rimraf|typescript|typescript-eslint|madge|@tsparticles/(eslint-config|prettier-config|tsconfig))\b' src || true

57-59: Remove self-dependency from devDependencies.

The package lists itself (@tsparticles/cli) as a devDependency; this can cause resolution issues.

Apply:

   "devDependencies": {
-    "@babel/core": "^7.28.3",
-    "@tsparticles/cli": "^2.3.3",
+    "@babel/core": "^7.28.3",

79-80: "main" points to eslint.config.js; set it to the CLI entry or drop it.

Since "bin" already points to dist/cli.js, either remove "main" or point it to the runtime entry and add exports.

Apply:

-  "main": "eslint.config.js",
+  "main": "dist/cli.js",
+  "exports": {
+    ".": "./dist/cli.js",
+    "./package.json": "./package.json"
+  },
🧹 Nitpick comments (1)
package.json (1)

23-25: Script name is misleading for ESM output.

"build:ts:cjs" suggests CJS, but the repo is ESM ("type": "module"). Ensure tsc emits ESM (module: NodeNext/ESNext) and consider renaming to build:ts:esm.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 61bd03e and 3d88374.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • files/empty-project/package.json (2 hunks)
  • files/empty-project/webpack.config.js (1 hunks)
  • package.json (2 hunks)
  • src/cli.ts (1 hunks)
  • src/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tsconfig.json
🧰 Additional context used
🧬 Code graph analysis (1)
files/empty-project/webpack.config.js (1)
eslint.config.js (1)
  • __dirname (3-3)
🪛 Biome (2.1.2)
files/empty-project/webpack.config.js

[error] 2-2: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 2-2: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)

src/cli.ts

[error] 4-4: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 4-4: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)

🔇 Additional comments (1)
package.json (1)

3-3: Version vs PR title mismatch.

PR title says 3.0.0 but package.json is 3.0.5. Confirm intended semver before release/tagging.

@@ -2,6 +2,7 @@
"name": "@tsparticles/empty-template",
"version": "1.0.0",
"private": true,
"type": "module",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

"type": "module" conflicts with CommonJS "main" entry; use exports map or .cjs.

With "type": "module", Node treats .js as ESM. Pointing "main" at dist/cjs/index.js (CommonJS) will break require/import. Prefer an exports map and emit .cjs for CJS output.

Apply:

-  "main": "dist/cjs/index.js",
-  "module": "dist/esm/index.js",
-  "types": "dist/types/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/types/index.d.ts",
+      "import": "./dist/esm/index.js",
+      "require": "./dist/cjs/index.cjs"
+    },
+    "./package.json": "./package.json"
+  },
+  "main": "./dist/esm/index.js",
+  "types": "./dist/types/index.d.ts",

Note: ensure your build emits dist/cjs/index.cjs (or rename accordingly).

Also applies to: 81-83

🤖 Prompt for AI Agents
In files/empty-project/package.json around line 5, "type": "module" conflicts
with the CommonJS "main" entry; update package.json and build outputs so ESM and
CJS artifacts match: either remove "type": "module" to keep CommonJS, or
(preferred) keep "type": "module", change "main" to point to a CJS file like
"dist/cjs/index.cjs", add an "exports" map exposing "./" with import ->
"dist/esm/index.js" and require -> "dist/cjs/index.cjs", and ensure the build
emits dist/cjs/index.cjs (or rename your CJS output) so require consumers and
import consumers both resolve correctly.

src/cli.ts Outdated
Comment on lines 2 to 5
import { buildCommand } from "./build/build.js";
import { createCommand } from "./create/create.js";
import pkgInfo from "../package.json" assert { type: "json" };
import { program } from "commander";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use import attributes with JSON default import (ESM).

Replace assert { type: "json" } with with { type: "json" } to satisfy modern Node and parsers. No other changes needed.

Apply:

-import pkgInfo from "../package.json" assert { type: "json" };
+import pkgInfo from "../package.json" with { type: "json" };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { buildCommand } from "./build/build.js";
import { createCommand } from "./create/create.js";
import pkgInfo from "../package.json" assert { type: "json" };
import { program } from "commander";
import { buildCommand } from "./build/build.js";
import { createCommand } from "./create/create.js";
import pkgInfo from "../package.json" with { type: "json" };
import { program } from "commander";
🧰 Tools
🪛 Biome (2.1.2)

[error] 4-4: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 4-4: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)

🤖 Prompt for AI Agents
In src/cli.ts around lines 2 to 5, the JSON import uses the old attribute syntax
`assert { type: "json" }`; update that import to use the modern attribute
keyword by replacing `assert { type: "json" }` with `with { type: "json" }` so
the package.json import reads using `with { type: "json" }`; keep the rest of
the line unchanged.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package.json (1)

1-12: Enforce Node 22 at install time.

Add engines to prevent unsupported runtime usage.

"engines": {
  "node": ">=22"
}
♻️ Duplicate comments (4)
package.json (4)

57-59: Remove self-dependency.

Listing @tsparticles/cli as a devDependency can break resolution.

   "devDependencies": {
     "@babel/core": "^7.28.3",
-    "@tsparticles/cli": "^2.3.3",

79-79: "main" should not point to eslint.config.js.

Either drop main (CLI-only) or point it to the runtime entry and add exports.

-  "main": "eslint.config.js",
+  "main": "dist/cli.js",
+  "exports": {
+    ".": "./dist/cli.js",
+    "./package.json": "./package.json"
+  },

If this package isn’t meant to be imported, remove main entirely instead.


56-77: Add the build/lint tooling under devDependencies.

Completes the move started above.

   "devDependencies": {
     "@babel/core": "^7.28.3",
+    "@tsparticles/eslint-config": "^3.0.5",
+    "@tsparticles/prettier-config": "^3.0.1",
+    "@tsparticles/tsconfig": "^3.0.5",
+    "@tsparticles/webpack-plugin": "^3.0.5",
+    "eslint": "^9.34.0",
+    "eslint-config-prettier": "^10.1.8",
+    "eslint-plugin-jsdoc": "^54.1.1",
+    "eslint-plugin-prettier": "^5.5.4",
+    "eslint-plugin-tsdoc": "^0.4.0",
+    "madge": "^8.0.0",
+    "prettier": "^3.6.2",
+    "prettier-plugin-multiline-arrays": "^4.0.3",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.9.2",
+    "typescript-eslint": "^8.41.0",
+    "webpack": "^5.101.3",
     "@tsparticles/engine": "^3.9.1",
     "@types/eslint": "^9.6.1",
     "@types/estree": "^1.0.8",
     "@types/fs-extra": "^11.0.4",
     "@types/klaw": "^3.0.7",
     "@types/madge": "^5.0.3",
     "@types/node": "^24.3.0",
     "@types/prompts": "^2.4.9",
     "@types/webpack-env": "^1.18.8",
     "babel-loader": "^10.0.0",
     "browserslist": "^4.25.4",
     "copyfiles": "^2.4.1",
     "cross-env": "^10.0.0",
     "terser-webpack-plugin": "^5.3.14",
     "ts-node": "^10.9.2",
     "vitest": "^1.5.6",
     "webpack-bundle-analyzer": "^4.10.2",
     "webpack-cli": "^6.0.1"
   },

33-55: Move build/lint tooling to devDependencies.

These inflate consumer installs and aren’t needed at runtime.

   "dependencies": {
-    "@tsparticles/eslint-config": "^3.0.5",
-    "@tsparticles/prettier-config": "^3.0.1",
-    "@tsparticles/tsconfig": "^3.0.5",
-    "@tsparticles/webpack-plugin": "^3.0.5",
     "commander": "^14.0.0",
-    "eslint": "^9.34.0",
-    "eslint-config-prettier": "^10.1.8",
-    "eslint-plugin-jsdoc": "^54.1.1",
-    "eslint-plugin-prettier": "^5.5.4",
-    "eslint-plugin-tsdoc": "^0.4.0",
     "fs-extra": "^11.3.1",
     "klaw": "^4.1.0",
     "lookpath": "^1.2.3",
-    "madge": "^8.0.0",
     "path-scurry": "^2.0.0",
-    "prettier": "^3.6.2",
-    "prettier-plugin-multiline-arrays": "^4.0.3",
     "prompts": "^2.4.2",
-    "rimraf": "^6.0.1",
-    "typescript": "^5.9.2",
-    "typescript-eslint": "^8.41.0",
-    "webpack": "^5.101.3"
+    "prompts": "^2.4.2"
   },

Add the removed packages under devDependencies (see next comment).

🧹 Nitpick comments (7)
src/tsconfig.json (2)

16-28: Trim redundant libs; use a single, stable ES lib.

Including ESNext plus every yearly lib is redundant and can mask breaking lib changes. Prefer a single target (e.g., ES2024) that matches Node 22.

-        "lib": [
-            "ESNext",
-            "ES2024",
-            "ES2023",
-            "ES2022",
-            "ES2021",
-            "ES2020",
-            "ES2019",
-            "ES2018",
-            "ES2017",
-            "ES2016",
-            "ES2015"
-        ],
+        "lib": ["ES2024"],

90-97: Consider verbatimModuleSyntax for cleaner ESM/CJS interop.

Helps prevent accidental synthetic default imports and matches TS 5+ guidance when using NodeNext.

         /* Interop Constraints */
-        // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+        // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
+        "verbatimModuleSyntax": true,
eslint.config.js (1)

1-2: Use node: specifiers for built-ins.

Minor, but recommended for clarity in ESM.

-import path from "path";
-import {fileURLToPath} from "url";
+import path from "node:path";
+import {fileURLToPath} from "node:url";
files/empty-project/webpack.config.js (3)

2-4: Prefer node: specifiers for built-ins.

Keeps ESM imports explicit.

-import {fileURLToPath} from "url";
+import {fileURLToPath} from "node:url";
@@
-import path from "path";
+import path from "node:path";

6-9: Avoid FS + top-level await; import JSON directly.

Use ESM JSON import to simplify and remove runtime I/O.

-import fs from "fs-extra";
@@
-const __filename = fileURLToPath(import.meta.url),
-    __dirname = path.dirname(__filename),
-    rootPkgPath = path.join(__dirname, "package.json"),
-    pkg = await fs.readJson(rootPkgPath);
+import pkg from "./package.json" with { type: "json" };
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);

8-8: Variable naming nit.

If you keep the fs approach, rootPkgPath isn’t “root”; consider pkgPath.

-    rootPkgPath = path.join(__dirname, "package.json"),
+    pkgPath = path.join(__dirname, "package.json"),
package.json (1)

1-12: Optional: Limit published contents.

Add files to ship only dist and docs.

"files": [
  "dist",
  "README.md",
  "LICENSE"
]
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3d88374 and c1b9cd3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • eslint.config.js (1 hunks)
  • files/empty-project/package.json (2 hunks)
  • files/empty-project/webpack.config.js (1 hunks)
  • package.json (2 hunks)
  • src/cli.ts (1 hunks)
  • src/tsconfig.json (2 hunks)
  • tests/tsconfig.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/cli.ts
  • files/empty-project/package.json
  • tests/tsconfig.json
🧰 Additional context used
🧬 Code graph analysis (1)
files/empty-project/webpack.config.js (2)
eslint.config.js (1)
  • __dirname (6-6)
scripts/postversion.js (1)
  • pkg (13-14)
🔇 Additional comments (3)
src/tsconfig.json (2)

42-47: NodeNext module + resolution: LGTM.

This aligns with ESM and explicit .js specifiers across the repo.


126-127: Project references present but not built; verify intent.

You compile with “tsc -p src” (not build mode). If references are needed, switch to “tsc -b” and mark the referenced project as composite; otherwise, remove the references to reduce confusion.

eslint.config.js (1)

8-22: Flat config structure otherwise looks good.

Type-aware linting via project tsconfig and merging the shared config is solid.

eslint.config.js Outdated
Comment on lines 11 to 17
languageOptions: {
parserOptions: {
project: [path.resolve(__dirname, "src/tsconfig.json")],
tsconfigRootDir: __dirname,
sourceType: "module"
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix sourceType placement (ESLint flat config).

sourceType belongs under languageOptions, not parserOptions. Current placement may be ignored.

-        languageOptions: {
-            parserOptions: {
-                project: [path.resolve(__dirname, "src/tsconfig.json")],
-                tsconfigRootDir: __dirname,
-                sourceType: "module"
-            }
-        },
+        languageOptions: {
+            sourceType: "module",
+            parserOptions: {
+                project: [path.resolve(__dirname, "src/tsconfig.json")],
+                tsconfigRootDir: __dirname
+            }
+        },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
languageOptions: {
parserOptions: {
project: [path.resolve(__dirname, "src/tsconfig.json")],
tsconfigRootDir: __dirname,
sourceType: "module"
}
},
languageOptions: {
sourceType: "module",
parserOptions: {
project: [path.resolve(__dirname, "src/tsconfig.json")],
tsconfigRootDir: __dirname
}
},
🤖 Prompt for AI Agents
In eslint.config.js around lines 11 to 17, the sourceType is incorrectly placed
inside parserOptions (so it may be ignored by ESLint flat config); move
sourceType out of parserOptions and set it directly on languageOptions (e.g.,
languageOptions.sourceType = "module"), leaving parserOptions with only project
and tsconfigRootDir, so ESLint recognizes the module source type correctly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package.json (1)

21-31: CI failure: package.json is missing in dist; include it or update the code
Reading package.json in src/build/build-prettier.ts (line 66) breaks at runtime because dist/ lacks package.json. Either:

  • copy package.json into dist in your build scripts (e.g. add "copy:pkg": "copyfiles -u 0 package.json dist" and invoke it in build/build:ci), or
  • stop hardcoding a JSON read from CWD—import it as a JSON module (enable resolveJsonModule in tsconfig and use import pkg from '../package.json' with { type: 'json' }) or resolve the project root via path.resolve(__dirname, '..', '..', 'package.json').
♻️ Duplicate comments (4)
files/empty-project/package.json (1)

5-5: "type": "module" + CommonJS main/module combo will confuse consumers; add an exports map and emit .cjs for CJS.

With ESM type, .js is ESM. Pointing "main" to a CommonJS path without .cjs and using a separate "module" field is fragile. Prefer an explicit exports map and a .cjs filename for CJS output.

   "license": "MIT",
   "bugs": {
     "url": "https://github.com/tsparticles/empty-template/issues"
   },
-  "main": "dist/cjs/index.js",
-  "module": "dist/esm/index.js",
-  "types": "dist/types/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/types/index.d.ts",
+      "import": "./dist/esm/index.js",
+      "require": "./dist/cjs/index.cjs"
+    },
+    "./package.json": "./package.json"
+  },
+  "main": "./dist/esm/index.js",
+  "types": "./dist/types/index.d.ts",

Ensure the build emits dist/cjs/index.cjs (or rename accordingly).

Also applies to: 81-83

package.json (3)

32-55: Move build/lint tooling out of runtime dependencies.

eslint/prettier/webpack/ts/madge/rimraf and internal configs should be devDependencies to avoid bloating consumers.

   "dependencies": {
-    "@tsparticles/eslint-config": "^3.0.5",
-    "@tsparticles/prettier-config": "^3.0.1",
-    "@tsparticles/tsconfig": "^3.0.5",
-    "@tsparticles/webpack-plugin": "^3.0.5",
     "commander": "^14.0.0",
-    "eslint": "^9.34.0",
-    "eslint-config-prettier": "^10.1.8",
-    "eslint-plugin-jsdoc": "^54.1.1",
-    "eslint-plugin-prettier": "^5.5.4",
-    "eslint-plugin-tsdoc": "^0.4.0",
     "fs-extra": "^11.3.1",
     "klaw": "^4.1.0",
     "lookpath": "^1.2.3",
-    "madge": "^8.0.0",
     "path-scurry": "^2.0.0",
-    "prettier": "^3.6.2",
-    "prettier-plugin-multiline-arrays": "^4.0.3",
     "prompts": "^2.4.2",
-    "rimraf": "^6.0.1",
-    "typescript": "^5.9.2",
-    "typescript-eslint": "^8.41.0",
-    "webpack": "^5.101.3"
+    "@tsparticles/eslint-config": "^3.0.5",
+    "@tsparticles/prettier-config": "^3.0.1",
+    "@tsparticles/tsconfig": "^3.0.5",
+    "@tsparticles/webpack-plugin": "^3.0.5",
+    "eslint": "^9.34.0",
+    "eslint-config-prettier": "^10.1.8",
+    "eslint-plugin-jsdoc": "^54.1.1",
+    "eslint-plugin-prettier": "^5.5.4",
+    "eslint-plugin-tsdoc": "^0.4.0",
+    "madge": "^8.0.0",
+    "prettier": "^3.6.2",
+    "prettier-plugin-multiline-arrays": "^4.0.3",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.9.2",
+    "typescript-eslint": "^8.41.0",
+    "webpack": "^5.101.3"
   },

57-59: Remove self-dependency from devDependencies.

Listing @tsparticles/cli as a devDependency of itself can cause resolver loops.

   "devDependencies": {
     "@babel/core": "^7.28.3",
-    "@tsparticles/cli": "^2.3.3",
     "@tsparticles/engine": "^3.9.1",

79-79: "main" should not point to eslint.config.js; set to the runtime entry or drop it.

This breaks consumers importing the package. For a CLI, either point main to the built CLI or omit it and rely on "bin".

-  "main": "eslint.config.js",
+  "main": "dist/cli.js",
+  "exports": {
+    ".": "./dist/cli.js",
+    "./package.json": "./package.json"
+  },
🧹 Nitpick comments (2)
package.json (2)

5-8: Declare engines to reflect Node 22 requirement.

Helps users and CI fail fast on unsupported Node versions.

   "type": "module",
+  "engines": {
+    "node": ">=22"
+  },
   "bin": {

21-31: Minor: script name “build:ts:cjs” is misleading under ESM.

If output is ESM, rename to “build:ts” to avoid confusion.

-    "build:ts": "pnpm run build:ts:cjs",
-    "build:ts:cjs": "tsc -p src",
+    "build:ts": "tsc -p src",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c1b9cd3 and ddea6e5.

📒 Files selected for processing (3)
  • files/empty-project/package.json (2 hunks)
  • package.json (2 hunks)
  • src/build/build.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/build/build.ts
🧰 Additional context used
🪛 GitHub Actions: Node.js CI
package.json

[error] 1-1: npm run build failed. ENOENT: no such file or directory, open '/home/runner/work/cli/cli/tests/tmp-files/foo-preset/node_modules/@tsparticles/cli/dist/package.json'.


[error] 1-1: npm run build failed. ENOENT: no such file or directory, open '/home/runner/work/cli/cli/tests/tmp-files/bar-preset/node_modules/@tsparticles/cli/dist/package.json'.


[error] 1-1: npm run build failed. ENOENT: no such file or directory, open '/home/runner/work/cli/cli/tests/tmp-files/foo-plugin/node_modules/@tsparticles/cli/dist/package.json'.


[error] 1-1: npm run build failed. ENOENT: no such file or directory, open '/home/runner/work/cli/cli/tests/tmp-files/bar-plugin/node_modules/@tsparticles/cli/dist/package.json'.


[error] 1-1: npm run build failed. ENOENT: no such file or directory, open '/home/runner/work/cli/cli/tests/tmp-files/foo-shape/node_modules/@tsparticles/cli/dist/package.json'.


[error] 1-1: npm run build failed. ENOENT: no such file or directory, open '/home/runner/work/cli/cli/tests/tmp-files/bar-shape/node_modules/@tsparticles/cli/dist/package.json'.

🔇 Additional comments (1)
files/empty-project/package.json (1)

86-106: Verify ESM build settings

  • Ensure your files/empty-project/tsconfig.json (which extends @tsparticles/tsconfig/tsconfig.json) ultimately sets
    compilerOptions.module = "NodeNext" (or "ESNext") and
    compilerOptions.target ≥ "ES2022".
  • Confirm the @tsparticles/tsconfig shared config (and its base) include those values or override them locally.
  • Verify that loadParticlesTemplate (in your webpack config) emits ESM output (not CJS) and that no Babel config is falling back to CJS.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/build/build-diststats.ts (1)

60-68: Fix JSON import and path normalization; replace deprecated exists and ensure absolute compare

Dynamic-importing package.json without assertions will throw in Node 22 ESM, and mixing relative/absolute paths can break the bundleSize match. Also prefer fs.pathExists over fs.exists.

Apply this patch to robustly read JSON, normalize paths to absolute, and avoid deprecated exists:

-    const path = await import("path"),
-        distFolder = path.join(basePath, "dist"),
-        pkgInfo = (await fs.exists(path.join(distFolder, "package.json")))
-            ? ((await import(path.join(distFolder, "package.json"))) as { jsdelivr?: string })
-            : {},
-        bundlePath =
-            (await fs.exists(distFolder)) && pkgInfo.jsdelivr
-                ? path.join(distFolder, pkgInfo.jsdelivr)
-                : undefined;
+    const path = await import("path"),
+        distFolder = path.resolve(basePath, "dist"),
+        pkgJsonPath = path.join(distFolder, "package.json"),
+        pkgInfo = (await fs.pathExists(pkgJsonPath))
+            ? ((await fs.readJson(pkgJsonPath)) as { jsdelivr?: string })
+            : {},
+        bundlePath =
+            (await fs.pathExists(distFolder)) && pkgInfo.jsdelivr
+                ? path.resolve(distFolder, pkgInfo.jsdelivr)
+                : undefined;
package.json (1)

21-25: Rename build script suffix to reflect ESM output
The build:ts:cjs script actually emits ESM (module: "NodeNext") into dist, so the :cjs suffix is misleading—rename it to something like build:ts:esm.

src/utils/file-utils.ts (1)

26-33: RegExp handling breaks flags and treats string patterns as regex.

new RegExp(token.from, "g") drops original flags (e.g., i) and makes plain strings act as regex. Preserve flags and escape strings.

-        for (const token of item.tokens) {
-            const regex = new RegExp(token.from, "g");
-
-            data = data.replace(regex, token.to);
-        }
+        for (const token of item.tokens) {
+            const regex =
+                typeof token.from === "string"
+                    ? new RegExp(token.from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g")
+                    : new RegExp(
+                          token.from.source,
+                          token.from.flags.includes("g") ? token.from.flags : token.from.flags + "g",
+                      );
+            data = data.replace(regex, token.to);
+        }
src/create/shape/create-shape.ts (1)

129-136: Bug: missing closing quote in README token replacement

The replacement for shape.type loses the closing quote.

-                to: `shape.type: "${camelizedName}`,
+                to: `shape.type: "${camelizedName}"`,
♻️ Duplicate comments (4)
src/create/plugin/create-plugin.ts (1)

168-168: __dirname is undefined in ESM — compute it from import.meta.url.

This will throw at runtime under "type": "module".

 import path from "path";
+import { fileURLToPath } from "node:url";
+const DIRNAME = path.dirname(fileURLToPath(import.meta.url));
@@
-    const sourcePath = path.join(__dirname, "..", "..", "..", "files", "create-plugin");
+    const sourcePath = path.join(DIRNAME, "..", "..", "..", "files", "create-plugin");
package.json (3)

58-58: Remove self-dependency from devDependencies.

Listing @tsparticles/cli under its own devDependencies can cause resolver loops.

-    "@tsparticles/cli": "^2.3.3",

79-79: “main” points to eslint.config.js — this breaks consumers.

Point main/exports to the runtime entry (dist/cli.js) or drop main if CLI-only.

-  "main": "eslint.config.js",
+  "main": "dist/cli.js",
+  "exports": {
+    ".": "./dist/cli.js",
+    "./package.json": "./package.json"
+  },

33-55: Move build/lint tooling to devDependencies to avoid bloating consumers.

ESLint/Prettier/Webpack/TypeScript/Madge/Rimraf and related tsparticles configs are dev-only.

   "dependencies": {
-    "@tsparticles/eslint-config": "^3.0.5",
-    "@tsparticles/prettier-config": "^3.0.1",
-    "@tsparticles/tsconfig": "^3.0.5",
-    "@tsparticles/webpack-plugin": "^3.0.5",
     "commander": "^14.0.0",
-    "eslint": "^9.34.0",
-    "eslint-config-prettier": "^10.1.8",
-    "eslint-plugin-jsdoc": "^54.1.1",
-    "eslint-plugin-prettier": "^5.5.4",
-    "eslint-plugin-tsdoc": "^0.4.0",
     "fs-extra": "^11.3.1",
     "klaw": "^4.1.0",
     "lookpath": "^1.2.3",
-    "madge": "^8.0.0",
     "path-scurry": "^2.0.0",
-    "prettier": "^3.6.2",
-    "prettier-plugin-multiline-arrays": "^4.0.3",
     "prompts": "^2.4.2",
-    "rimraf": "^6.0.1",
-    "typescript": "^5.9.2",
-    "typescript-eslint": "^8.41.0",
-    "webpack": "^5.101.3"
   },
   "devDependencies": {
+    "@tsparticles/eslint-config": "^3.0.5",
+    "@tsparticles/prettier-config": "^3.0.1",
+    "@tsparticles/tsconfig": "^3.0.5",
+    "@tsparticles/webpack-plugin": "^3.0.5",
+    "eslint": "^9.34.0",
+    "eslint-config-prettier": "^10.1.8",
+    "eslint-plugin-jsdoc": "^54.1.1",
+    "eslint-plugin-prettier": "^5.5.4",
+    "eslint-plugin-tsdoc": "^0.4.0",
+    "madge": "^8.0.0",
+    "prettier": "^3.6.2",
+    "prettier-plugin-multiline-arrays": "^4.0.3",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.9.2",
+    "typescript-eslint": "^8.41.0",
+    "webpack": "^5.101.3"
   }
🧹 Nitpick comments (13)
src/build/build-diststats.ts (1)

45-47: Normalize compare path inside getFolderStats (optional hardening)

Ensure both sides are absolute before equality to avoid false negatives when callers pass relative basePath.

TypeScript snippet (outside the changed hunk):

// before
if (bundlePath && path.join(folderPath, dirent.name) === bundlePath) {
  stats.bundleSize += fileStats.size;
}

// after
const fullPath = path.resolve(folderPath, dirent.name);
if (bundlePath && fullPath === bundlePath) {
  stats.bundleSize += fileStats.size;
}

Optional: hoist a single top-level import for path and drop the per-function dynamic import:

import path from "node:path";
tests/string-utils.test.ts (1)

6-6: Sharpen test description wording.

Prefer “returns empty string for empty input” for clarity.

-        it("should successfully compare empty strings", () => {
+        it("returns empty string for empty input", () => {

Apply similarly to the empty cases in camelize and dash.

src/create/plugin/create-plugin.ts (1)

101-109: Handle repo URLs without “.git”.

If the remote URL lacks .git, substring will misbehave. Safer to strip an optional suffix.

-        repoPath = repoUrl.includes(stringSearch)
-            ? repoUrl.substring(
-                  repoUrl.indexOf(trailingSlashSearch) + trailingSlashSearch.length,
-                  repoUrl.indexOf(".git"),
-              )
+        repoPath = repoUrl.includes(stringSearch)
+            ? repoUrl
+                  .slice(repoUrl.indexOf(trailingSlashSearch) + trailingSlashSearch.length)
+                  .replace(/\.git$/, "")
             : "tsparticles/plugin-template";
tests/create-preset.test.ts (3)

6-6: Stabilize heavy integration tests (serialize, timeout, guaranteed cleanup).

Project generation runs installs/builds; guard with sequential suite, large timeout, and finally cleanup.

-describe("create-preset", () => {
+describe.sequential("create-preset", () => {
     it("should have created the preset project", async () => {
-        const destDir = path.join(DIRNAME, "tmp-files", "foo-preset");
-
-        await createPresetTemplate("foo", "Foo", "", destDir);
-
-        const pkgPath = path.join(destDir, "package.json");
-
-        console.log(pkgPath);
-
-        const pkgInfo = await fs.readJSON(pkgPath);
-
-        expect(pkgInfo.name).toBe("tsparticles-preset-foo");
-
-        await fs.remove(destDir);
-    });
+        const destDir = path.join(DIRNAME, "tmp-files", "foo-preset");
+        try {
+            await createPresetTemplate("foo", "Foo", "", destDir);
+            const pkgPath = path.join(destDir, "package.json");
+            const pkgInfo = await fs.readJSON(pkgPath);
+            expect(pkgInfo.name).toBe("tsparticles-preset-foo");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);
@@
-    it("should have created the preset project, w/ repo", async () => {
+    it("should have created the preset project, w/ repo", async () => {
         const destDir = path.join(DIRNAME, "tmp-files", "bar-preset");
-
-        await createPresetTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir);
-
-        const pkgPath = path.join(destDir, "package.json");
-
-        console.log(pkgPath);
-
-        const pkgInfo = await fs.readJSON(pkgPath);
-
-        expect(pkgInfo.name).toBe("tsparticles-preset-bar");
-
-        await fs.remove(destDir);
-    });
+        try {
+            await createPresetTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir);
+            const pkgPath = path.join(destDir, "package.json");
+            const pkgInfo = await fs.readJSON(pkgPath);
+            expect(pkgInfo.name).toBe("tsparticles-preset-bar");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);

Also applies to: 7-21, 23-37


14-16: Remove noisy console logging in CI.

These logs add noise without aiding assertions.

-        console.log(pkgPath);

Also applies to: 30-32


2-2: Avoid network-heavy installs/builds in tests (introduce skip flag).

Consider adding an options param to createPresetTemplate({ skipInstallBuild: true }) and gating runInstall/runBuild behind it; then set it in tests.

Would you like me to open a follow-up PR adding the flag across create-plugin/preset/shape and updating tests?

package.json (1)

1-31: Declare supported Node versions.

Add engines to reflect Node 22 target (and 20 LTS if supported).

   "publishConfig": {
     "access": "public"
   },
+  "engines": {
+    "node": ">=20.11 <21 || >=22"
+  },
src/utils/file-utils.ts (1)

83-84: Trim git config output.

exec returns a string ending with a newline; trim to avoid subtle token/URL issues.

-            resolve(stdout);
+            resolve(stdout.trim());
tests/file-utils.test.ts (1)

91-95: Stabilize repo URL test (environment-dependent)

getRepositoryUrl() returns "" if git isn’t present or remote isn’t set, making this flaky in CI. Mock lookpath/child_process.exec or make the assertion conditional.

Example (preferred, mock within this test block):

// at top of the file (before importing getRepositoryUrl), or use dynamic import below
// vi.mock("lookpath", () => ({ lookpath: async () => "git" }));
// vi.mock("node:child_process", () => ({ exec: (_: any, cb: any) => cb(null, "git@github.com:foo/bar.git\n") }));

it("should return the repository url", async () => {
  const url = await getRepositoryUrl();
  if (!url) {
    // skip in environments without git/remote
    return;
  }
  expect(url.trim()).not.toBe("");
});
tests/create-shape.test.ts (4)

1-3: Mock heavy steps to avoid timeouts; or increase per-test timeout

Stub runInstall/runBuild to keep tests fast and deterministic. Ensure mocks are declared before importing the SUT.

-import { describe, it, expect } from "vitest";
-import { createShapeTemplate } from "../src/create/shape/create-shape.js";
+import { describe, it, expect, vi } from "vitest";
+vi.mock("../src/utils/template-utils.js", async () => {
+  const mod = await vi.importActual<typeof import("../src/utils/template-utils.js")>("../src/utils/template-utils.js");
+  return {
+    ...mod,
+    runInstall: vi.fn(async () => {}),
+    runBuild: vi.fn(async () => {}),
+  };
+});
+import { createShapeTemplate } from "../src/create/shape/create-shape.js";

If you prefer not to mock, pass a larger timeout to each it(..., 180_000).


14-15: Remove debug logging

console.log(pkgPath) is noisy in CI.

-        console.log(pkgPath);

Also applies to: 30-31


7-21: Ensure cleanup on failures

Wrap body in try/finally so the temp dir is removed even if assertions fail.

-    it("should have created the shape project", async () => {
+    it("should have created the shape project", async () => {
         const destDir = path.join(__dirname, "tmp-files", "foo-shape");
-
-        await createShapeTemplate("foo", "Foo", "", destDir);
-
-        const pkgPath = path.join(destDir, "package.json");
-
-        const pkgInfo = await fs.readJSON(pkgPath);
-
-        expect(pkgInfo.name).toBe("tsparticles-shape-foo");
-
-        await fs.remove(destDir);
+        try {
+            await createShapeTemplate("foo", "Foo", "", destDir);
+            const pkgPath = path.join(destDir, "package.json");
+            const pkgInfo = await fs.readJSON(pkgPath);
+            expect(pkgInfo.name).toBe("tsparticles-shape-foo");
+        } finally {
+            await fs.remove(destDir);
+        }
-    });
+    });

24-37: Mirror cleanup pattern in second test

Apply the same try/finally cleanup to the repo-URL variant.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ddea6e5 and d9c9098.

📒 Files selected for processing (14)
  • eslint.config.js (1 hunks)
  • package.json (2 hunks)
  • src/build/build-diststats.ts (1 hunks)
  • src/create/plugin/create-plugin.ts (5 hunks)
  • src/create/preset/create-preset.ts (6 hunks)
  • src/create/shape/create-shape.ts (5 hunks)
  • src/utils/file-utils.ts (3 hunks)
  • src/utils/template-utils.ts (5 hunks)
  • tests/create-plugin.test.ts (1 hunks)
  • tests/create-preset.test.ts (1 hunks)
  • tests/create-shape.test.ts (1 hunks)
  • tests/file-utils.test.ts (4 hunks)
  • tests/string-utils.test.ts (1 hunks)
  • vitest.config.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/create-plugin.test.ts
  • src/utils/template-utils.ts
  • src/create/preset/create-preset.ts
  • vitest.config.ts
  • eslint.config.js
🧰 Additional context used
🧬 Code graph analysis (6)
tests/string-utils.test.ts (1)
src/utils/string-utils.ts (3)
  • capitalize (7-18)
  • camelize (26-28)
  • dash (35-41)
tests/create-shape.test.ts (2)
eslint.config.js (1)
  • __dirname (6-6)
src/create/shape/create-shape.ts (1)
  • createShapeTemplate (161-184)
src/create/plugin/create-plugin.ts (1)
eslint.config.js (1)
  • __dirname (6-6)
tests/file-utils.test.ts (1)
src/utils/file-utils.ts (2)
  • getDestinationDir (49-65)
  • getRepositoryUrl (70-86)
tests/create-preset.test.ts (2)
eslint.config.js (1)
  • __dirname (6-6)
src/create/preset/create-preset.ts (1)
  • createPresetTemplate (180-204)
src/create/shape/create-shape.ts (1)
eslint.config.js (1)
  • __dirname (6-6)
🪛 GitHub Actions: Node.js CI
src/build/build-diststats.ts

[error] 1-1: Prettier formatting check failed for file 'src/build/build-diststats.ts' (prettier --check ./src/*). Run 'prettier --write' to fix code style issues.

🔇 Additional comments (6)
src/build/build-diststats.ts (1)

1-1: Fix Prettier check failure

The CI reports Prettier formatting errors for this file. Please run Prettier locally to align with the repo config.

Commands:

  • pnpm: pnpm prettier --write src/build/build-diststats.ts
  • npm: npx prettier --write src/build/build-diststats.ts
tests/string-utils.test.ts (1)

6-8: Vitest structure fix looks good.

Empty-string assertions are now inside it() blocks and will execute. Nice catch-up with earlier feedback.

Also applies to: 54-56, 102-104

src/utils/file-utils.ts (1)

50-65: Path resolution change is fine.

path.join(process.cwd(), destination) remains absolute; behavior matches prior resolve(). LGTM.

src/create/shape/create-shape.ts (2)

150-152: Verify exported loader name used in webpack config

updateShapeWebpackFile hardcodes "loadParticlesShape", while other replacements use load${Capitalized}Shape. Confirm the template expects the generic name; otherwise pass the specific one to avoid broken UMD/webpack entry.


1-1: ESM import paths and path.join usage look good

Explicit .js extensions and path.join-based paths align with the repo’s ESM strategy.

Also applies to: 10-10, 13-13, 25-26, 110-110

tests/file-utils.test.ts (1)

97-99: Teardown LGTM

afterAll cleanup is correct.

Comment on lines 12 to 14
import path from "path";
import { replaceTokensInFile } from "../../utils/file-utils";
import { replaceTokensInFile } from "../../utils/file-utils.js";

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix: __dirname is undefined under ESM

__dirname isn’t available in ESM. Compute it from import.meta.url before using it at Line 167.

Apply:

 import fs from "fs-extra";
 import path from "path";
+import { fileURLToPath } from "node:url";
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
-    const sourcePath = path.join(__dirname, "..", "..", "..", "files", "create-shape");
+    const sourcePath = path.join(__dirname, "..", "..", "..", "files", "create-shape");

Also applies to: 167-168

🤖 Prompt for AI Agents
In src/create/shape/create-shape.ts around lines 12 and 167-168, __dirname is
used but undefined under ESM; add a computed dirname using import.meta.url and
fileURLToPath before its use: import fileURLToPath from 'url' (or add a named
import) at the top and compute const __dirname =
path.dirname(fileURLToPath(import.meta.url)); then replace the direct __dirname
usage at lines 167-168 with the computed value; ensure the new import is added
alongside existing imports and no CommonJS globals remain.

import { describe, it } from "mocha";
import { expect } from "chai";
import { createPresetTemplate } from "../src/create/preset/create-preset";
import { describe, it, expect } from "vitest";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Audit other ESM files for __dirname usage.

create-preset implementation still uses __dirname per repo snippet; scan and fix globally.


🏁 Script executed:

#!/bin/bash
# List residual __dirname usages under src/ and tests/
rg -nP '\b__dirname\b' -g 'src/**' -g 'tests/**' -S

Length of output: 1298


Replace __dirname in all ESM modules

The following ESM files still reference __dirname and must be converted to URL-based resolution (e.g.

import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));

):

  • src/cli.ts
  • src/utils/template-utils.ts
  • src/create/shape/create-shape.ts
  • src/create/preset/create-preset.ts
  • src/create/plugin/create-plugin.ts
  • tests/create-shape.test.ts
  • tests/create-preset.test.ts
  • tests/create-plugin.test.ts
🤖 Prompt for AI Agents
In tests/create-preset.test.ts around line 1 (and the other listed ESM files:
src/cli.ts, src/utils/template-utils.ts, src/create/shape/create-shape.ts,
src/create/preset/create-preset.ts, src/create/plugin/create-plugin.ts,
tests/create-shape.test.ts, tests/create-plugin.test.ts), remove any use of the
CommonJS __dirname and instead derive the directory via the URL API: import
fileURLToPath from 'url' and compute __dirname with fileURLToPath(new URL('.',
import.meta.url)); update all path resolutions that relied on __dirname to use
this computed value (or use new URL(..., import.meta.url) directly) so the
modules remain valid ESM.

it("should have created the preset project", async () => {
const destDir = path.resolve(path.join(__dirname, "tmp-files", "foo-preset"));
const destDir = path.join(__dirname, "tmp-files", "foo-preset");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

__dirname is undefined in ESM tests — derive it via import.meta.url.

Without this, path.join will fail under Vitest ESM.

 import path from "path";
+import { fileURLToPath } from "node:url";
+const DIRNAME = path.dirname(fileURLToPath(import.meta.url));
@@
-        const destDir = path.join(__dirname, "tmp-files", "foo-preset");
+        const destDir = path.join(DIRNAME, "tmp-files", "foo-preset");
@@
-        const destDir = path.join(__dirname, "tmp-files", "bar-preset");
+        const destDir = path.join(DIRNAME, "tmp-files", "bar-preset");

Also applies to: 24-24

🤖 Prompt for AI Agents
In tests/create-preset.test.ts around lines 8 and 24, the test uses __dirname
which is undefined under ESM; replace uses of __dirname with a derived dirname
from import.meta.url (i.e., compute the file path via
fileURLToPath(import.meta.url) and then path.dirname(...) or equivalent) and
update the two occurrences so path.join uses that derived directory instead.

@@ -1,16 +1,15 @@
import { describe, it } from "mocha";
import { expect } from "chai";
import {afterAll, describe, it, expect} from "vitest";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Do not use async describe; move async setup into beforeAll hooks

Vitest doesn’t await async describe bodies; current awaits may run unpredictably. Use beforeAll for async setup.

-import {afterAll, describe, it, expect} from "vitest";
+import {afterAll, beforeAll, describe, it, expect} from "vitest";
@@
-describe("file-utils", async () => {
-    const baseDir = path.resolve("tmp-files");
-
-    await fs.ensureDir(baseDir);
+describe("file-utils", () => {
+    const baseDir = path.resolve("tmp-files");
+    beforeAll(async () => {
+        await fs.ensureDir(baseDir);
+    });
@@
-    describe("replace tokens in files", async () => {
-        fs.writeFileSync(path.join(baseDir, "files1.txt"), "test");
-        fs.writeFileSync(path.join(baseDir, "files2.txt"), "test");
-
-        await replaceTokensInFiles([{
-            path: path.join(baseDir, "files1.txt"),
-            tokens: [{
-                from: "test",
-                to: "test1",
-            }],
-        }, {
-            path: path.join(baseDir, "files2.txt"),
-            tokens: [{
-                from: "test",
-                to: "test2",
-            }],
-        }]);
+    describe("replace tokens in files", () => {
+        beforeAll(async () => {
+            fs.writeFileSync(path.join(baseDir, "files1.txt"), "test");
+            fs.writeFileSync(path.join(baseDir, "files2.txt"), "test");
+            await replaceTokensInFiles([
+                { path: path.join(baseDir, "files1.txt"), tokens: [{ from: "test", to: "test1" }] },
+                { path: path.join(baseDir, "files2.txt"), tokens: [{ from: "test", to: "test2" }] },
+            ]);
+        });
@@
-    describe("replace tokens in file", async () => {
-        fs.writeFileSync(path.join(baseDir, "file1.txt"), "test");
-
-        await replaceTokensInFile({
-            path: path.join(baseDir, "file1.txt"),
-            tokens: [{
-                from: "test",
-                to: "test1",
-            }],
-        });
+    describe("replace tokens in file", () => {
+        beforeAll(async () => {
+            fs.writeFileSync(path.join(baseDir, "file1.txt"), "test");
+            await replaceTokensInFile({
+                path: path.join(baseDir, "file1.txt"),
+                tokens: [{ from: "test", to: "test1" }],
+            });
+        });
@@
-    describe("get destination dir", async () => {
-        const destDir = await getDestinationDir(path.join("tmp-files", "baz"));
+    describe("get destination dir", () => {
+        let destDir: string;
+        let destDir2: string;
+        beforeAll(async () => {
+            destDir = await getDestinationDir(path.join("tmp-files", "baz"));
+            destDir2 = await getDestinationDir(path.join("tmp-files", "baz"));
+        });

Also applies to: 11-16, 16-33, 43-53, 61-63

🤖 Prompt for AI Agents
In tests/file-utils.test.ts lines 1 and the ranges 11-16, 16-33, 43-53, 61-63:
the test file uses async describe bodies which Vitest does not await; change
each async describe to a plain synchronous describe and move any await/async
initialization inside beforeAll (and cleanup into afterAll) hooks at the top of
that describe block. Ensure any shared async variables are declared in outer
scope, assign them inside beforeAll using await, and remove awaits from the
describe callback so all describe callbacks are synchronous.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/build/build-distfiles.ts (1)

30-34: Preserve both dependencies and peerDependencies; avoid JSON stringify/parse hack.

Packages can have both; current else-if drops peers. Also replace only leading "workspace:" tokens.

-        if (pkgInfo.dependencies) {
-            libObj.dependencies = JSON.parse(JSON.stringify(pkgInfo.dependencies).replaceAll("workspace:", ""));
-        } else if (pkgInfo.peerDependencies) {
-            libObj.peerDependencies = JSON.parse(JSON.stringify(pkgInfo.peerDependencies).replaceAll("workspace:", ""));
-        }
+        if (pkgInfo.dependencies) {
+            libObj.dependencies = Object.fromEntries(
+                Object.entries(pkgInfo.dependencies).map(([k, v]) => [k, v.replace(/^workspace:/, "")]),
+            );
+        }
+        if (pkgInfo.peerDependencies) {
+            libObj.peerDependencies = Object.fromEntries(
+                Object.entries(pkgInfo.peerDependencies).map(([k, v]) => [k, v.replace(/^workspace:/, "")]),
+            );
+        }
♻️ Duplicate comments (5)
tests/create-preset.test.ts (1)

6-31: Serialize, add timeout, and always clean up temp dirs.

Prevents CI flakiness and leaks; mirrors prior plugin test fix.

-describe("create-preset", () => {
-    it("should have created the preset project", async () => {
+describe.sequential("create-preset", () => {
+    it("should have created the preset project", async () => {
-        const destDir = path.join(DIRNAME, "tmp-files", "foo-preset");
-
-        await createPresetTemplate("foo", "Foo", "", destDir);
-
-        const pkgPath = path.join(destDir, "package.json"),
-            pkgInfo = await fs.readJSON(pkgPath);
-
-        expect(pkgInfo.name).toBe("tsparticles-preset-foo");
-
-        await fs.remove(destDir);
-    });
+        const destDir = path.join(DIRNAME, "tmp-files", "foo-preset");
+        try {
+            await createPresetTemplate("foo", "Foo", "", destDir /* consider: { skipInstallBuild: true } */);
+            const pkgPath = path.join(destDir, "package.json");
+            const pkgInfo = await fs.readJSON(pkgPath);
+            expect(pkgInfo.name).toBe("tsparticles-preset-foo");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);
@@
-    it("should have created the preset project, w/ repo", async () => {
-        const destDir = path.join(DIRNAME, "tmp-files", "bar-preset");
-
-        await createPresetTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir);
-
-        const pkgPath = path.join(destDir, "package.json"),
-            pkgInfo = await fs.readJSON(pkgPath);
-
-        expect(pkgInfo.name).toBe("tsparticles-preset-bar");
-
-        await fs.remove(destDir);
-    });
+    it("should have created the preset project, w/ repo", async () => {
+        const destDir = path.join(DIRNAME, "tmp-files", "bar-preset");
+        try {
+            await createPresetTemplate("bar", "Bar", "https://github.com/matteobruni/tsparticles", destDir /* consider: { skipInstallBuild: true } */);
+            const pkgPath = path.join(destDir, "package.json");
+            const pkgInfo = await fs.readJSON(pkgPath);
+            expect(pkgInfo.name).toBe("tsparticles-preset-bar");
+        } finally {
+            await fs.remove(destDir);
+        }
+    }, 180000);

Follow-up: if needed, I can add a skipInstallBuild option to createPresetTemplate and update tests accordingly.

files/empty-project/package.json (1)

81-83: Prefer exports map and .cjs for CJS with "type":"module".

Keeps Node resolution unambiguous; aligns ESM/CJS entry points.

-  "main": "dist/cjs/index.js",
-  "module": "dist/esm/index.js",
-  "types": "dist/types/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/types/index.d.ts",
+      "import": "./dist/esm/index.js",
+      "require": "./dist/cjs/index.cjs"
+    },
+    "./package.json": "./package.json"
+  },
+  "main": "./dist/esm/index.js",
+  "types": "dist/types/index.d.ts",

Ensure the build emits dist/cjs/index.cjs (or rename).

package.json (3)

79-79: "main" should not point to eslint.config.js; set to CLI entry or drop.

-  "main": "eslint.config.js",
+  "main": "dist/cli.js",
+  "exports": {
+    ".": "./dist/cli.js",
+    "./package.json": "./package.json"
+  },

33-55: Move build/lint tools to devDependencies to avoid bloating consumers.

   "dependencies": {
-    "@tsparticles/eslint-config": "^3.0.5",
-    "@tsparticles/prettier-config": "^3.0.1",
-    "@tsparticles/tsconfig": "^3.0.5",
-    "@tsparticles/webpack-plugin": "^3.0.5",
     "commander": "^14.0.0",
-    "eslint": "^9.34.0",
-    "eslint-config-prettier": "^10.1.8",
-    "eslint-plugin-jsdoc": "^54.1.1",
-    "eslint-plugin-prettier": "^5.5.4",
-    "eslint-plugin-tsdoc": "^0.4.0",
     "fs-extra": "^11.3.1",
     "klaw": "^4.1.0",
     "lookpath": "^1.2.3",
-    "madge": "^8.0.0",
-    "path-scurry": "^2.0.0",
-    "prettier": "^3.6.2",
-    "prettier-plugin-multiline-arrays": "^4.0.3",
     "prompts": "^2.4.2",
-    "rimraf": "^6.0.1",
-    "typescript": "^5.9.2",
-    "typescript-eslint": "^8.41.0",
-    "webpack": "^5.101.3"
+    "path-scurry": "^2.0.0"
   },
   "devDependencies": {
+    "@tsparticles/eslint-config": "^3.0.5",
+    "@tsparticles/prettier-config": "^3.0.1",
+    "@tsparticles/tsconfig": "^3.0.5",
+    "@tsparticles/webpack-plugin": "^3.0.5",
+    "eslint": "^9.34.0",
+    "eslint-config-prettier": "^10.1.8",
+    "eslint-plugin-jsdoc": "^54.1.1",
+    "eslint-plugin-prettier": "^5.5.4",
+    "eslint-plugin-tsdoc": "^0.4.0",
+    "madge": "^8.0.0",
+    "prettier": "^3.6.2",
+    "prettier-plugin-multiline-arrays": "^4.0.3",
+    "rimraf": "^6.0.1",
+    "typescript": "^5.9.2",
+    "typescript-eslint": "^8.41.0",
+    "webpack": "^5.101.3"
   }

57-59: Remove self-dependency from devDependencies.

-    "@tsparticles/cli": "^2.3.3",
🧹 Nitpick comments (4)
files/create-shape/src/ShapeDrawer.ts (1)

4-5: Embedding validTypes in the drawer matches the new API; tiny alloc nit.

Hoist the array to a module-level const to avoid per-instance allocation (optionally freeze).

+const SHAPE_TYPES = Object.freeze(["#template#"] as const);
 
 export class ShapeDrawer implements IShapeDrawer {
-    readonly validTypes = ["#template#"] as const;
+    readonly validTypes = SHAPE_TYPES;
src/build/build-eslint.ts (1)

28-30: Avoid potential runtime error: prefer String() over toString() on possibly undefined fields.

ESLint messages can occasionally omit line/column (e.g., file-level). m.line.toString() will throw; String(m.line) safely coerces.

-                    .map(m => `${t.filePath} (${m.line.toString()},${m.column.toString()}): ${m.message}`)
+                    .map(m => `${t.filePath} (${String(m.line)},${String(m.column)}): ${m.message}`)
src/build/build-distfiles.ts (2)

15-20: Use fs-extra’s readJSON for simpler, safer parse (with encoding) and better errors.

-        const pkgInfo = JSON.parse((await fs.readFile(path.join(basePath, "package.json"))).toString()) as {
+        const pkgInfo = await fs.readJSON(path.join(basePath, "package.json")) as {
             dependencies?: Record<string, string>;
             peerDependencies?: Record<string, string>;
             publishConfig?: { directory?: string };
             version: string;
         },

89-91: Ensure submodule dirs exist before writing type package.json files.

Avoid ENOENT on clean builds.

-        await fs.writeFile(path.join(distPath, "cjs", "package.json"), `{ "type": "commonjs" }`);
-        await fs.writeFile(path.join(distPath, "esm", "package.json"), `{ "type": "module" }`);
-        await fs.writeFile(path.join(distPath, "browser", "package.json"), `{ "type": "module" }`);
+        await fs.ensureDir(path.join(distPath, "cjs"));
+        await fs.ensureDir(path.join(distPath, "esm"));
+        await fs.ensureDir(path.join(distPath, "browser"));
+        await fs.writeFile(path.join(distPath, "cjs", "package.json"), `{ "type": "commonjs" }`);
+        await fs.writeFile(path.join(distPath, "esm", "package.json"), `{ "type": "module" }`);
+        await fs.writeFile(path.join(distPath, "browser", "package.json"), `{ "type": "module" }`);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d9c9098 and 637477d.

📒 Files selected for processing (13)
  • files/create-shape/src/ShapeDrawer.ts (1 hunks)
  • files/create-shape/src/index.ts (1 hunks)
  • files/empty-project/package.json (2 hunks)
  • package.json (2 hunks)
  • src/build/build-distfiles.ts (1 hunks)
  • src/build/build-diststats.ts (1 hunks)
  • src/build/build-eslint.ts (1 hunks)
  • src/cli.ts (1 hunks)
  • src/create/plugin/create-plugin.ts (5 hunks)
  • src/create/shape/create-shape.ts (5 hunks)
  • tests/create-plugin.test.ts (1 hunks)
  • tests/create-preset.test.ts (1 hunks)
  • tests/create-shape.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/cli.ts
  • src/build/build-diststats.ts
  • src/create/plugin/create-plugin.ts
  • tests/create-plugin.test.ts
  • src/create/shape/create-shape.ts
  • tests/create-shape.test.ts
🧰 Additional context used
🧬 Code graph analysis (2)
files/create-shape/src/index.ts (1)
files/create-shape/src/ShapeDrawer.ts (1)
  • ShapeDrawer (3-16)
tests/create-preset.test.ts (2)
eslint.config.js (1)
  • __dirname (6-6)
src/create/preset/create-preset.ts (1)
  • createPresetTemplate (180-204)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (1)
files/create-shape/src/index.ts (1)

9-9: No legacy two-arg addShape calls detected; migration verified.

Comment on lines 3 to 4
import path from "path";
import fs from "fs-extra";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

__dirname is undefined in ESM/Vitest — derive via import.meta.url.

 import path from "path";
 import fs from "fs-extra";
+import { fileURLToPath } from "node:url";
+const DIRNAME = path.dirname(fileURLToPath(import.meta.url));
@@
-        const destDir = path.join(__dirname, "tmp-files", "foo-preset");
+        const destDir = path.join(DIRNAME, "tmp-files", "foo-preset");
@@
-        const destDir = path.join(__dirname, "tmp-files", "bar-preset");
+        const destDir = path.join(DIRNAME, "tmp-files", "bar-preset");

Also applies to: 8-8, 21-21

🤖 Prompt for AI Agents
In tests/create-preset.test.ts around lines 3-4 (and also references at lines 8
and 21), the code uses __dirname which is undefined under ESM/Vitest; derive the
directory from import.meta.url instead: import fileURLToPath from 'url' (or
destructure fileURLToPath) and compute const __filename =
fileURLToPath(import.meta.url) and const __dirname = path.dirname(__filename),
then replace any direct __dirname usages on the mentioned lines with this
computed __dirname so the test resolves paths correctly in ESM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant