diff --git a/.gitignore b/.gitignore index b5cb5ef..c3cd5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /dist node_modules/ coverage/ +scratch/ yarn-error.log diff --git a/src/packages/analyze.ts b/src/packages/analyze.ts new file mode 100644 index 0000000..dbe762e --- /dev/null +++ b/src/packages/analyze.ts @@ -0,0 +1,50 @@ +import path from "path" +import { glob } from "glob" + +import type { NodeModule } from "../types" +import { Project } from "../project" +import { readFile } from "../util" +import { findNodeModulesPath } from "./util" + +export async function analyzeAll(project: Project) { + const nodeModulesPath = await findNodeModulesPath(project.projectPath) + + if (!nodeModulesPath) return + + const packages = [ + ...await glob(`${nodeModulesPath}/*stimulus*/package.json`), // for libraries like stimulus-in-library + ...await glob(`${nodeModulesPath}/*stimulus*/*/package.json`), // for libraries like @stimulus-in-namespace/some-library + ...await glob(`${nodeModulesPath}/*/*stimulus*/package.json`), // for libraries like @some-namespace/stimulus-in-library + ] + + await Promise.allSettled( + packages.map(async packagePath => { + const folder = path.dirname(packagePath) + const packageJSON = await readFile(packagePath) + const parsed = JSON.parse(packageJSON) + const packageName = parsed.name + + if (packageName === "@hotwired/stimulus") return + if (packageName === "@hotwired/stimulus-webpack-helpers") return + if (packageName === "stimulus-vite-helpers") return + + const source = parsed.source || parsed.module || parsed.main + + if (source) { + const directory = path.dirname(source) + const basePath = path.join(folder, directory) + const files = await glob(`${basePath}/**/*.{js,mjs}`) + + const detectedModule: NodeModule = { + name: packageName, + path: packagePath, + controllerRoots: [basePath] + } + + project.detectedNodeModules.push(detectedModule) + + await project.readControllerFiles(files) + } + }) + ) +} diff --git a/src/packages/index.ts b/src/packages/index.ts index 7fb76dd..6b25fa4 100644 --- a/src/packages/index.ts +++ b/src/packages/index.ts @@ -1,7 +1,9 @@ -import { Project } from '../project' +import { Project } from "../project" -import * as tailwindcssStimulusComponents from "./tailwindcss-stimulus-components" +// import * as tailwindcssStimulusComponents from "./tailwindcss-stimulus-components" +import { analyzeAll } from "./analyze" export async function detectPackages(project: Project) { - await tailwindcssStimulusComponents.analyze(project) + // await tailwindcssStimulusComponents.analyze(project) + await analyzeAll(project) } diff --git a/src/packages/tailwindcss-stimulus-components.ts b/src/packages/tailwindcss-stimulus-components.ts index 3f5c760..01f845c 100644 --- a/src/packages/tailwindcss-stimulus-components.ts +++ b/src/packages/tailwindcss-stimulus-components.ts @@ -2,7 +2,7 @@ import path from "path" import { glob } from "glob" import type { NodeModule } from "../types" -import { Project } from '../project' +import { Project } from "../project" import { hasDepedency, findPackagePath } from "./util" export async function analyze(project: Project) { diff --git a/src/parser.ts b/src/parser.ts index 1d6c96a..2792179 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -65,7 +65,7 @@ export class Parser { ClassDeclaration(node: any): void { const className = node.id?.name - const superClass = node.superClass.name + const superClass = node.superClass?.name const importStatement = importStatements.find(i => i.importedName === superClass) // TODO: this needs to be recursive @@ -85,7 +85,7 @@ export class Parser { } } else { controller.parent = { - constant: node.superClass.name, + constant: superClass, type: "unknown", } } diff --git a/src/project.ts b/src/project.ts index 8658c19..a67a5b4 100644 --- a/src/project.ts +++ b/src/project.ts @@ -1,11 +1,10 @@ import { ControllerDefinition } from "./controller_definition" import { Parser } from "./parser" -import { resolvePathWhenFileExists, nestedFolderSort } from "./util" +import { readFile, resolvePathWhenFileExists, nestedFolderSort } from "./util" import { detectPackages } from "./packages" import type { NodeModule } from "./types" import path from "path" -import { promises as fs } from "fs" import { glob } from "glob" interface ControllerFile { @@ -134,7 +133,7 @@ export class Project { async readControllerFiles(controllerFiles: string[]) { await Promise.allSettled( controllerFiles.map(async (filename: string) => { - const content = await fs.readFile(filename, "utf8") + const content = await readFile(filename) this.controllerFiles.push({ filename, content }) }) @@ -143,11 +142,13 @@ export class Project { private async getControllerFiles(): Promise { return await glob(`${this.projectPath}/**/*controller${this.fileEndingsGlob}`, { - ignore: `${this.projectPath}/node_modules/**/*`, + ignore: `${this.projectPath}/**/node_modules/**/*`, }) } get fileEndingsGlob(): string { - return `.{${Project.javascriptEndings.join(",")},${Project.typescriptEndings.join(",")}}` + const extensions = Project.javascriptEndings.concat(Project.typescriptEndings).join(",") + + return `.{${extensions}}` } } diff --git a/src/util.ts b/src/util.ts index 1337a50..afc45af 100644 --- a/src/util.ts +++ b/src/util.ts @@ -15,6 +15,10 @@ export async function resolvePathWhenFileExists(path: string): Promise { + return await fs.readFile(path, "utf8") +} + export async function fileExists(path: string): Promise { return folderExists(path) } diff --git a/test/fixtures/packages/app/package.json b/test/fixtures/packages/app/package.json new file mode 100644 index 0000000..b70d503 --- /dev/null +++ b/test/fixtures/packages/app/package.json @@ -0,0 +1,18 @@ +{ + "name": "app", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@hotwired/stimulus": "^3.2.2", + "@stimulus-library/controllers": "^1.0.6", + "stimulus-checkbox": "^2.0.0", + "stimulus-clipboard": "^4.0.1", + "stimulus-datepicker": "^1.0.6", + "stimulus-dropdown": "^2.1.0", + "stimulus-hotkeys": "^2.3.0", + "stimulus-inline-input-validations": "^1.2.0", + "stimulus-use": "^0.52.2", + "tailwindcss-stimulus-components": "^4.0.4" + } +} diff --git a/test/fixtures/packages/app/yarn.lock b/test/fixtures/packages/app/yarn.lock new file mode 100644 index 0000000..bddaadf --- /dev/null +++ b/test/fixtures/packages/app/yarn.lock @@ -0,0 +1,117 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.21.0": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" + integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw== + dependencies: + regenerator-runtime "^0.14.0" + +"@hotwired/stimulus@^3.0.0", "@hotwired/stimulus@^3.0.1", "@hotwired/stimulus@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608" + integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== + +"@stimulus-library/controllers@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@stimulus-library/controllers/-/controllers-1.0.6.tgz#8122303e6dfa1139abda15e5aa41b81ce303bedf" + integrity sha512-cdA4NmiHI2oLImHcphtJnVmhxQ2Krz2Nh85UlXez0US9OS50SiVkxe+nAajt0eJ5o6HHfrr5SUxiUilQqlK90g== + dependencies: + "@stimulus-library/mixins" "^1.0.5" + "@stimulus-library/utilities" "^1.0.5" + date-fns "^2.29.3" + +"@stimulus-library/mixins@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@stimulus-library/mixins/-/mixins-1.0.5.tgz#4b8834e7279978caf4ea277735d40c350b1e56f2" + integrity sha512-saPVDcYVMTFAebaRJ4IAoNdEnfJpMIQxMgQf/CNugEDbxQb6+r7ZEIW3AmXedkKda3roN8TKrL7nUacXWxaz5Q== + dependencies: + "@hotwired/stimulus" "^3.0.0" + "@stimulus-library/utilities" "^1.0.5" + +"@stimulus-library/utilities@^1.0.2", "@stimulus-library/utilities@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@stimulus-library/utilities/-/utilities-1.0.5.tgz#8f790e1f13e64553c0bce617f6f3b56b7be77422" + integrity sha512-cTJk4OQwMy+VaXhSRszfZuIgt2GY49fn3IzktS15IV2qdVsaxF66mIdbuYRJgdIpMsQzQ/1gW5iS2P10iDpFpg== + dependencies: + "@hotwired/stimulus" "^3.0.0" + "@stimulus-library/utilities" "^1.0.2" + mitt "^3.0.0" + +date-fns@^2.29.3: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +hotkeys-js@>=3, hotkeys-js@^3.8.7: + version "3.13.7" + resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.13.7.tgz#0188d8e2fca16a3f1d66541b48de0bb9df613726" + integrity sha512-ygFIdTqqwG4fFP7kkiYlvayZppeIQX2aPpirsngkv1xM1lP0piDY5QEh68nQnIKvz64hfocxhBaD/uK3sSK1yQ== + +mitt@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +stimulus-checkbox@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stimulus-checkbox/-/stimulus-checkbox-2.0.0.tgz#ee4908ca6263556f0015eb36495ddf70611bca56" + integrity sha512-tuflIcPariCD6Ju/qoRXLbZR+f5FiZKVHYCA3Qj6i3PVqhYG0pgEYCPBKT3bmbSIZsAfK9Xc1pi+zryqlfjl0g== + dependencies: + "@hotwired/stimulus" "^3.2.2" + +stimulus-clipboard@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/stimulus-clipboard/-/stimulus-clipboard-4.0.1.tgz#acc9212b479fedc633ecdec8f191a28c1d826a6b" + integrity sha512-dem+ihC3Q8+gbyGINdd+dK+9d5vUTnOwoH+n3KcDJvbxrFcq9lV8mWjyhEaDquGxYy3MmqSdz9FHQbG88TBqGg== + +stimulus-datepicker@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/stimulus-datepicker/-/stimulus-datepicker-1.0.6.tgz#64eb8895a8aae902fd60b672f3c957a7f9b42158" + integrity sha512-MP2WcmibFTqb76iRLEP/TC32FKmU9Ca6xeptQiov6v8PivgZ1YMGmr7fb34zrdTHiwVHgzjxTm/6oP5kis1iuQ== + +stimulus-dropdown@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/stimulus-dropdown/-/stimulus-dropdown-2.1.0.tgz#22f15cd1dc247e08f04c3f95d7ab9d8102602a07" + integrity sha512-p4Bs56/ilB2E0lfFaNajKIHZK1PMUUDnhDl74f97bn087fxIfRB7WQekVtTTWJdRlf3EIgSsDX7K1TsaLiIcLg== + dependencies: + stimulus-use "^0.51.1" + +stimulus-hotkeys@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/stimulus-hotkeys/-/stimulus-hotkeys-2.3.0.tgz#95e5c25cc47ca4eaa82a65831d57dae8cc97640a" + integrity sha512-oDQu7yrrEAsu3ohFAI1E7hNMIn/4IGLtGlyRoAds0Q3JO509Ua0MhoQkBYl0ZdVOMOKTZchjjIDhT2pNUaZHWQ== + dependencies: + "@hotwired/stimulus" "^3.0.1" + hotkeys-js "^3.8.7" + +stimulus-inline-input-validations@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/stimulus-inline-input-validations/-/stimulus-inline-input-validations-1.2.0.tgz#5d8893e3f330044214e761cca2a73dcb715bb68a" + integrity sha512-3TVEmM9YBsVJdW2boux7C50+mnfikH0cMJ38ZinKmbVfXnGR1Sdyaweqe3FPOU8o2ktmWSRzYzk1n63y3hls8w== + +stimulus-use@^0.51.1: + version "0.51.3" + resolved "https://registry.yarnpkg.com/stimulus-use/-/stimulus-use-0.51.3.tgz#d7ac671aff8d0db253296dec89d38aa6f4b27e2a" + integrity sha512-V4YETxMFL4/bpmcqlwFtaOaJg9sLF+XlWsvXrsoWVA5jffsqe7uAvV6gGPPQta7Hgx01vovA0yNsWUe2eU9Jmw== + dependencies: + hotkeys-js ">=3" + +stimulus-use@^0.52.2: + version "0.52.2" + resolved "https://registry.yarnpkg.com/stimulus-use/-/stimulus-use-0.52.2.tgz#fc992fababe03f8d8bc2d9470c8cdb40bd075917" + integrity sha512-413+tIw9n6Jnb0OFiQE1i3aP01i0hhGgAnPp1P6cNuBbhhqG2IOp8t1O/4s5Tw2lTvSYrFeLNdaY8sYlDaULeg== + +tailwindcss-stimulus-components@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/tailwindcss-stimulus-components/-/tailwindcss-stimulus-components-4.0.4.tgz#1df5f2a488aa89365561bb33357095cd59ed831a" + integrity sha512-xNlMs1WufKiTMQtVklwHfrR/iuPVaFA0Mk5uefRnHztmr7w4g6BzKAWHyfte60pjhcQbmlbshHMOZiq/dkXnhw== diff --git a/test/packages/app.test.ts b/test/packages/app.test.ts new file mode 100644 index 0000000..87d3de8 --- /dev/null +++ b/test/packages/app.test.ts @@ -0,0 +1,67 @@ +import { describe, test, expect } from "vitest" +import { Project } from "../../src" + +const project = new Project(`${process.cwd()}/test/fixtures/packages/app`) + +describe("packages", () => { + describe("app", () => { + test("detects controllers", async () => { + expect(project.controllerDefinitions.length).toEqual(0) + + await project.analyze() + + expect(project.detectedNodeModules.map(module => module.name).sort()).toEqual([ + "@stimulus-library/controllers", + "@stimulus-library/mixins", + "@stimulus-library/utilities", + "stimulus-checkbox", + "stimulus-clipboard", + "stimulus-datepicker", + "stimulus-dropdown", + "stimulus-hotkeys", + "stimulus-inline-input-validations", + "stimulus-use", + "tailwindcss-stimulus-components", + ]) + + expect(project.controllerRoots).toEqual([ + "node_modules/@stimulus-library/controllers", + "node_modules/stimulus-checkbox/src", + "node_modules/stimulus-clipboard/dist", + "node_modules/stimulus-datepicker/src", + "node_modules/stimulus-dropdown/dist", + "node_modules/stimulus-hotkeys/src", + "node_modules/stimulus-inline-input-validations/src", + "node_modules/stimulus-use/dist", + "node_modules/tailwindcss-stimulus-components/src", + "node_modules/@stimulus-library/mixins/dist", + "node_modules/@stimulus-library/utilities/dist", + ]) + + // expect(project.controllerDefinitions.length).toEqual(11) + // expect(project.controllerDefinitions.map(controller => controller.identifier).sort()).toEqual([ + // "alert", + // "autosave", + // "color-preview", + // "datepicker", + // "dropdown", + // "index", + // "input-validator", + // "iso-date", + // "modal", + // "popover", + // "slideover", + // "tabs", + // "toggle", + // "transition", + // ]) + + const controller = project.controllerDefinitions.find(controller => controller.identifier === "modal") + + expect(controller.targets).toEqual(["container", "background"]) + expect(Object.keys(controller.values)).toEqual(["open", "restoreScroll"]) + expect(controller.values.open.type).toEqual("Boolean") + expect(controller.values.restoreScroll.type).toEqual("Boolean") + }) + }) +})