├── .changeset ├── README.md └── config.json ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .husky └── pre-commit ├── .syncpackrc.json ├── .vscode ├── launch.json └── tasks.json ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bun.lock ├── eslint.config.mjs ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── bin.ts │ ├── test │ │ ├── __snapshots__ │ │ │ └── bin.test.ts.snap │ │ └── bin.test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── core │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── context.test.ts │ │ ├── context.ts │ │ ├── functions.test.ts │ │ ├── functions.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vitest.config.ts └── vscode │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── README.md │ ├── assets │ ├── current-and-nested.gif │ ├── current-only.gif │ ├── current-with-subfolders.gif │ └── logo.png │ ├── package.json │ ├── scripts │ └── package.ts │ ├── src │ ├── extension.mts │ └── index.mts │ ├── tsconfig.json │ └── vite.config.ts ├── release.sh └── tsconfig.base.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { "repo": "mikededo/dart-barrel-file-generator" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: oven-sh/setup-bun@v2 15 | - name: Setup node 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 22 19 | - name: Install deps 20 | run: bun install --frozen-lockfile 21 | - name: Lint 22 | run: bun lint 23 | - name: Test 24 | run: bun run test:ci 25 | - name: Test build 26 | run: bun run build 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | permissions: {} 8 | jobs: 9 | release: 10 | if: github.repository == 'mikededo/dart-barrel-file-generator' 11 | permissions: 12 | contents: write # to create release 13 | pull-requests: write # to create pull request (changesets/action) 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - uses: oven-sh/setup-bun@v2 21 | - name: Setup node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22 25 | registry-url: https://registry.npmjs.org/ 26 | - name: Install 27 | run: bun install --frozen-lockfile 28 | - name: Capture new release versions 29 | run: | 30 | bun changeset status --output=release.json 31 | echo "CHANGED_PACKAGES=$(jq -r '.releases | map(.name + "@" + .newVersion) | join(", ")' release.json)" >> "$GITHUB_ENV" 32 | rm release.json 33 | - name: Create PR release or publish to npm 34 | id: changesets 35 | uses: changesets/action@v1 36 | with: 37 | version: bun changeset version 38 | publish: ./release.sh 39 | commit: 'chore(release): ${{env.CHANGED_PACKAGES}}' 40 | title: 'chore(release): ${{env.CHANGED_PACKAGES}}' 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 44 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | dist/ 5 | *.vsix 6 | .turbo 7 | 8 | .env 9 | .DS_Store 10 | yarn.lock 11 | 12 | # Tests 13 | html 14 | coverage 15 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | bun lint 2 | -------------------------------------------------------------------------------- /.syncpackrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "versionGroups": [ 3 | { 4 | "label": "Use workspace protocol when developing local packages", 5 | "dependencies": ["$LOCAL"], 6 | "dependencyTypes": ["!local"], 7 | "pinVersion": "workspace:*" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "autoAttachChildProcesses": true, 10 | "args": [ 11 | "--disable-updates", 12 | "--disable-workspace-trust", 13 | "--profile-temp", 14 | "--skip-release-notes", 15 | "--skip-welcome", 16 | "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode" 17 | ], 18 | "env": { 19 | "NODE_OPTIONS": "--enable-source-maps" 20 | }, 21 | "outFiles": ["${workspaceFolder}/packages/vscode/dist/**/*.js"], 22 | "skipFiles": ["/**", "**/typescript/**"], 23 | "preLaunchTask": "build" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "command": "bun", 7 | "args": ["run", "--filter=@dbfg/vscode", "dev"], 8 | "label": "build", 9 | "presentation": { 10 | "clear": true, 11 | "echo": true, 12 | "reveal": "always", 13 | "focus": false, 14 | "panel": "shared" 15 | }, 16 | "isBackground": true, 17 | "problemMatcher": [ 18 | { 19 | "pattern": [ 20 | { 21 | "regexp": "build started...", 22 | "file": 1, 23 | "line": 2, 24 | "column": 3, 25 | "message": 4 26 | } 27 | ], 28 | "background": { 29 | "activeOnStart": true, 30 | "beginsPattern": "watching for file changes...", 31 | "endsPattern": "built in \\d+ms." 32 | } 33 | } 34 | ], 35 | "group": { 36 | "kind": "build", 37 | "isDefault": true 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and 9 | expression, level of experience, education, socio-economic status, nationality, 10 | personal appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project admin at miquelddg@gmail.com. All complaints 59 | will be reviewed and investigated and will result in a response that is deemed 60 | necessary and appropriate to the circumstances. The project team is obligated to 61 | maintain confidentiality with regard to the reporter of an incident. Further 62 | details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at 72 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for reaching the project and wanting to improve the extension! This file 4 | has few guidelines that will help you along the way. 5 | 6 | ## Code of Conduct 7 | 8 | Please, make sure you read the [Code of Conduct](CODE-OF-CONDUCT.md) file before 9 | contributing! It is short, it will not take you much time :wink:! 10 | 11 | ## How to 12 | 13 | ### Issue 14 | 15 | The first thing would be to create an issue, which would notify the 16 | contributors. Furthermore, it gives more visibility to the problem or feature 17 | and may attract other people on helping. You can propose a solution to the issue 18 | by creating a pull request. If you do not feel like contributing, no worries, a 19 | maintainer will come to help! 20 | 21 | ### Developing 22 | 23 | When developing, make sure to following the below steps: 24 | 25 | 1. Fork the repository. 26 | 2. Clone the repository to your local machine, adding the original repo as an 27 | upstream remote: 28 | 29 | ````sh git clone git@github:/dart-barrel-file-generator.git # You can 30 | also use https # git clone 31 | https://github.com//dartBarrelFileGenereator.git cd 32 | dart-barrel-file-generator git remote add upstream 33 | https://github.com/mikededo/dart-barrel-file-generator.git ``` 34 | 35 | 3. Synchronize the branch: 36 | 37 | ```sh git checkout main git pull upstream main ``` 38 | 39 | 4. Install all the dependencies: 40 | 41 | ```sh yarn ``` 42 | 43 | 5. Create the new branch: 44 | 45 | ```sh git checkout -b branch-with-sense ``` 46 | 47 | 6. Save changes and push to your fork: 48 | 49 | ```sh git push -u origin HEAD ``` 50 | 51 | 7. Create a pull request with your changes. 52 | 53 | #### Commits 54 | 55 | Make sure to commit using semantic commits. Also, its recommended to: 56 | 57 | - Check for the linting and formatting, using ESLint (see the [config file](.eslintrc.json)). 58 | - Add a changeset to your changes, with `bun changeset`. 59 | 60 | #### Merging a PR 61 | 62 | After your code has been reviewed and approved, one of the administrators will 63 | squash the changes to the base branch. 64 | 65 | ## License 66 | 67 | By contributing your code to this repository, you agree to license your 68 | contribution under the [MIT License](LICENSE). 69 | ```` 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Miquel de Domingo i Giralt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dart Barrel File Generator 2 | 3 | ![Logo](./packages/vscode/assets/logo.png) 4 | 5 | Monorepo for the VSCode and cli to generate and maintain Dart barrel files 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config'; 2 | import perfectionist from 'eslint-plugin-perfectionist'; 3 | 4 | export default antfu({ 5 | formatters: true, 6 | gitignore: true, 7 | jsonc: true, 8 | lessOpinionated: true, 9 | stylistic: { 10 | indent: 2, 11 | quotes: 'single', 12 | semi: true 13 | }, 14 | typescript: { 15 | overrides: { 16 | 'no-use-before-define': 'off', 17 | 'ts/consistent-type-definitions': ['error', 'type'], 18 | 'ts/consistent-type-imports': [ 19 | 'error', 20 | { 21 | disallowTypeAnnotations: false, 22 | fixStyle: 'separate-type-imports', 23 | prefer: 'type-imports' 24 | } 25 | ], 26 | 'ts/no-unused-vars': [ 27 | 'error', 28 | { 29 | args: 'all', 30 | argsIgnorePattern: '^_', 31 | caughtErrors: 'all', 32 | caughtErrorsIgnorePattern: '^_', 33 | destructuredArrayIgnorePattern: '^_', 34 | ignoreRestSiblings: true, 35 | varsIgnorePattern: '^_' 36 | } 37 | ], 38 | 'ts/no-use-before-define': [ 39 | 'error', 40 | { 41 | classes: false, 42 | enums: false, 43 | functions: true, 44 | ignoreTypeReferences: true, 45 | typedefs: false, 46 | variables: true 47 | } 48 | ] 49 | } 50 | } 51 | }) 52 | .override('antfu/stylistic/rules', { 53 | rules: { 54 | 'arrow-body-style': ['error', 'as-needed'], 55 | 'style/arrow-parens': ['error', 'always'], 56 | 'style/brace-style': ['error', '1tbs'], 57 | 'style/comma-dangle': ['error', 'never'], 58 | 'style/indent': [ 59 | 'error', 60 | 2, 61 | { 62 | flatTernaryExpressions: true, 63 | offsetTernaryExpressions: true, 64 | SwitchCase: 1 65 | } 66 | ], 67 | 'style/no-multiple-empty-lines': ['error', { max: 1 }], 68 | 'style/operator-linebreak': [ 69 | 'error', 70 | 'after', 71 | { 72 | overrides: { ':': 'before', '?': 'before' } 73 | } 74 | ], 75 | 'style/quote-props': ['error', 'as-needed'] 76 | } 77 | }) 78 | .override('antfu/perfectionist/setup', { 79 | rules: { 80 | ...(perfectionist.configs['recommended-alphabetical'].rules ?? {}), 81 | 'perfectionist/sort-exports': [ 82 | 'error', 83 | { 84 | groupKind: 'types-first', 85 | ignoreCase: true, 86 | order: 'asc', 87 | type: 'alphabetical' 88 | } 89 | ], 90 | 'perfectionist/sort-imports': [ 91 | 'error', 92 | { 93 | environment: 'bun', 94 | groups: [ 95 | 'style', 96 | 'internal-type', 97 | ['parent-type', 'sibling-type', 'index-type'], 98 | ['builtin', 'external'], 99 | 'internal', 100 | ['parent', 'sibling', 'index'], 101 | 'object', 102 | 'unknown' 103 | ], 104 | ignoreCase: true, 105 | internalPattern: ['\\@dbfg\\/+'], 106 | maxLineLength: undefined, 107 | newlinesBetween: 'always', 108 | order: 'asc', 109 | type: 'alphabetical' 110 | } 111 | ], 112 | 'perfectionist/sort-modules': ['error', { partitionByNewLine: true }], 113 | 'perfectionist/sort-object-types': [ 114 | 'error', 115 | { 116 | customGroups: { callbacks: 'on*' }, 117 | groupKind: 'required-first', 118 | groups: ['unknown', 'callbacks', 'multiline'], 119 | ignoreCase: true, 120 | order: 'asc', 121 | partitionByNewLine: true, 122 | type: 'alphabetical' 123 | } 124 | ] 125 | } 126 | }); 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "miquelddg", 3 | "name": "dbfg", 4 | "displayName": "Dart Barrel File Generator", 5 | "private": true, 6 | "packageManager": "bun@1.2.2", 7 | "description": "Visual studio code to generate barrel files for the Dart language.", 8 | "author": { 9 | "name": "Miquel de Domingo i Giralt", 10 | "email": "miquelddg@gmail.com", 11 | "url": "https://www.github.com/mikededo" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mikededo/dart-barrel-file-generator" 16 | }, 17 | "bugs": { 18 | "email": "miquelddg@gmail.com", 19 | "url": "https://www.github.com/mikededo/dart-barrel-file-generator/issues" 20 | }, 21 | "scripts": { 22 | "build": "bun run workspace build", 23 | "dev": "bun run workspace dev", 24 | "lint": "eslint . --max-warnings 0", 25 | "lint:fix": "eslint . --fix", 26 | "test": "bun run workspace test", 27 | "test:ci": "bun run test --watch false", 28 | "workspace": "bun run --filter '@dbfg/*'" 29 | }, 30 | "devDependencies": { 31 | "@antfu/eslint-config": "4.12.0", 32 | "@changesets/cli": "2.29.2", 33 | "@svitejs/changesets-changelog-github-compact": "1.2.0", 34 | "@types/node": "22.14.1", 35 | "@typescript-eslint/eslint-plugin": "8.30.1", 36 | "@typescript-eslint/parser": "8.30.1", 37 | "@vitest/coverage-v8": "3.1.1", 38 | "@vitest/ui": "3.1.1", 39 | "eslint": "9.25.0", 40 | "eslint-plugin-format": "1.0.1", 41 | "eslint-plugin-perfectionist": "4.11.0", 42 | "husky": "9.1.7", 43 | "neverthrow": "8.2.0", 44 | "picocolors": "1.1.1", 45 | "typescript": "5.8.3", 46 | "vite": "6.3.2", 47 | "vitest": "3.1.1" 48 | }, 49 | "workspaces": [ 50 | "packages/*" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @dbfg/cli 2 | 3 | ## 1.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Dependency update ([#138](https://github.com/mikededo/dart-barrel-file-generator/pull/138)) 8 | 9 | ## 1.2.0 10 | 11 | ### Minor Changes 12 | 13 | - Migrate to `neverthrow` ([#136](https://github.com/mikededo/dart-barrel-file-generator/pull/136)) 14 | 15 | ## 1.1.1 16 | 17 | ### Patch Changes 18 | 19 | - Add `-q, --quiet` option ([`1bed1cf`](https://github.com/mikededo/dart-barrel-file-generator/commit/1bed1cf94a348638b5960d43f7d2f3acbeb60d0e)) 20 | 21 | - Improve output logs ([`1c9d5a8`](https://github.com/mikededo/dart-barrel-file-generator/commit/1c9d5a892de3257c6bef5c35e9cc675b2026893e)) 22 | 23 | ## 1.1.0 24 | 25 | ### Minor Changes 26 | 27 | - Add named executable ([`d02321a`](https://github.com/mikededo/dart-barrel-file-generator/commit/d02321a247bd063b9899081b422fd6496a31abf2)) 28 | 29 | ### Patch Changes 30 | 31 | - Add docs ([`d02321a`](https://github.com/mikededo/dart-barrel-file-generator/commit/d02321a247bd063b9899081b422fd6496a31abf2)) 32 | 33 | ## 1.0.2 34 | 35 | ### Patch Changes 36 | 37 | - Update build ([`8ce92ad`](https://github.com/mikededo/dart-barrel-file-generator/commit/8ce92adc60d7f9987504cc2c3063485e48f878db)) 38 | 39 | ## 1.0.1 40 | 41 | ### Patch Changes 42 | 43 | - Change package scope ([`4ea5d3d`](https://github.com/mikededo/dart-barrel-file-generator/commit/4ea5d3db75e62de4a4ef4dd478d0d4bc94e859f8)) 44 | 45 | - Updated dependencies [[`4ea5d3d`](https://github.com/mikededo/dart-barrel-file-generator/commit/4ea5d3db75e62de4a4ef4dd478d0d4bc94e859f8)]: 46 | - @dbfg/core@2.2.1 47 | 48 | ## 1.0.0 49 | 50 | ### Major Changes 51 | 52 | - Complete implementation of the `@dbfg/cli` app ([`4a3a0e5`](https://github.com/mikededo/dart-barrel-file-generator/commit/4a3a0e55b4d208aabb751700ae92dc83215b3a10)) 53 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @dbfg/cli 2 | 3 | Command-line interface for generating and maintaining Dart barrel files. 4 | 5 | ## Recommended usage 6 | 7 | The recommended usage is through `bunx` or the equivalent from your package of 8 | choice: 9 | 10 | ```sh 11 | bunx @dbfg/cli@latest [options] 12 | ``` 13 | 14 | ## Installation 15 | 16 | Globally install it with: 17 | 18 | ```sh 19 | bun install -g @dbfg/cli 20 | ``` 21 | 22 | You will now have the `dbfg-cli` executable available. 23 | 24 | ## Usage 25 | 26 | `dbfg-cli` creates barrel files for your Dart projects, making it easier to manage exports. 27 | 28 | ### Generation Modes 29 | 30 | The CLI supports three generation modes: 31 | 32 | 1. **Regular mode** (default): Generates a barrel file for only the specified directory 33 | 34 | ```sh 35 | dbfg-cli 36 | ``` 37 | 38 | 2. **Recursive mode**: Generates barrel files for the target directory and all 39 | nested subdirectories 40 | 41 | ```sh 42 | dbfg-cli --recursive 43 | ``` 44 | 45 | 3. **Subfolders mode**: Generates a single barrel file that includes exports 46 | from all files of subdirectories 47 | 48 | ```sh 49 | dbfg-cli --subfolders 50 | ``` 51 | 52 | ### Options 53 | 54 | ```sh 55 | -V, --version output the version number 56 | -s, --subfolders Include subfolders in the barrel file 57 | -r, --recursive Generate barrel files recursively for all nested directories 58 | -c, --config Path to configuration file 59 | -n, --default-barrel-name Default name for barrel files (default: "") 60 | --excluded-dirs Comma-separated list of directories to exclude (default: []) 61 | --excluded-files Comma-separated list of files to exclude (default: []) 62 | --exclude-freezed Exclude freezed files (default: false) 63 | --exclude-generated Exclude generated files (default: false) 64 | --skip-empty Skip directories with no files (default: false) 65 | --append-folder-name Append folder name to barrel file name (default: false) 66 | --prepend-folder-name Prepend folder name to barrel file name (default: false) 67 | --prepend-package Prepend package name to exports in lib folder (default: false) 68 | -h, --help display help for command 69 | ``` 70 | 71 | ### Examples 72 | 73 | Generate a barrel file for the `lib` directory: 74 | 75 | ```sh 76 | dbfg ./lib 77 | ``` 78 | 79 | Generate barrel files recursively for the `src` directory and all its 80 | subdirectories: 81 | 82 | ```sh 83 | dbfg --recursive ./src 84 | ``` 85 | 86 | Generate a barrel file for `lib` with a custom name: 87 | 88 | ```sh 89 | dbfg ./lib -n index 90 | ``` 91 | 92 | Exclude generated files: 93 | 94 | ```sh 95 | dbfg ./src --exclude-freezed --exclude-generated 96 | ``` 97 | 98 | Exclude specific files using patterns: 99 | 100 | ```sh 101 | dbfg ./src --excluded-files "*_one.dart" "*_test.dart" 102 | ``` 103 | 104 | ### Configuration File 105 | 106 | You can use a JSON configuration file to define default options: 107 | 108 | ```json 109 | { 110 | "defaultBarrelName": "index", 111 | "excludeFreezed": true, 112 | "excludeGenerated": true, 113 | "excludeFileList": ["*_test.dart"], 114 | "excludeDirList": ["test"], 115 | "prependPackageToLibExport": true 116 | } 117 | ``` 118 | 119 | Then simply pass the `--config` option: 120 | 121 | ```sh 122 | dbfg ./src --config ./dbfg.json 123 | ``` 124 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dbfg/cli", 3 | "type": "module", 4 | "version": "1.2.1", 5 | "private": false, 6 | "description": "CLI to generate Dart barrel files", 7 | "bin": { 8 | "dbfg-cli": "./dist/bin.cjs" 9 | }, 10 | "engines": { 11 | "node": "^20.0.0 || >=22.0.0" 12 | }, 13 | "scripts": { 14 | "build": "vite build", 15 | "dev": "vite-node src/bin.ts", 16 | "test": "vitest", 17 | "test:cov": "vitest --coverage", 18 | "test:ui": "vitest --ui --reporter html" 19 | }, 20 | "dependencies": { 21 | "@commander-js/extra-typings": "13.1.0", 22 | "commander": "13.1.0", 23 | "valibot": "1.0.0" 24 | }, 25 | "devDependencies": { 26 | "@dbfg/core": "workspace:*", 27 | "rollup-plugin-node-externals": "8.0.0", 28 | "vite-node": "3.1.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/cli/src/bin.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | /* eslint-disable node/prefer-global/process */ 3 | 4 | import type { GenerationType } from '@dbfg/core'; 5 | 6 | import { program } from '@commander-js/extra-typings'; 7 | import { ok, ResultAsync } from 'neverthrow'; 8 | import fs from 'node:fs'; 9 | import { readFile } from 'node:fs/promises'; 10 | import path from 'node:path'; 11 | import pc from 'picocolors'; 12 | import type { InferInput } from 'valibot'; 13 | import * as v from 'valibot'; 14 | 15 | import { createContext, toPosixPath } from '@dbfg/core'; 16 | 17 | import { description, name as packageName, version } from '../package.json'; 18 | 19 | const log = pc.bgBlueBright(pc.black(' INFO ')); 20 | const done = pc.bgGreen(pc.black(' DONE ')); 21 | const warn = pc.bgYellow(pc.black(' WARN ')); 22 | const error = pc.bgRed(pc.black(' ERROR ')); 23 | const logger = (prefix: string) => (...messages: string[]) => console.log(prefix, ...messages); 24 | const cliLogger = { 25 | done: logger(done), 26 | error: logger(error), 27 | log: logger(log), 28 | warn: logger(warn) 29 | }; 30 | 31 | const SUCCESS_MESSAGES = { 32 | RECURSIVE: 'Successfully generated recursive barrel files for {path}', 33 | REGULAR: 'Successfully generated barrel file for {path}', 34 | REGULAR_SUBFOLDERS: 'Successfully generated barrel file with subfolders for {path}' 35 | }; 36 | 37 | const Directory = v.string('Provided directory should be a string'); 38 | const ConfigPath = v.optional(v.string('`config` must be a string')); 39 | const ConfigSchema = v.object({ 40 | appendFolderName: v.boolean('`appendFolderName` must be a boolean'), 41 | defaultBarrelName: v.string('`name` must be a string'), 42 | excludeDirList: v.array(v.string('`excludeDirs` must be an array of strings')), 43 | excludeFileList: v.array(v.string('`excludeFiles` must be an array of strings')), 44 | excludeFreezed: v.boolean('`excludeFreezed` must be a boolean'), 45 | excludeGenerated: v.boolean('`excludeGenerated` must be a boolean'), 46 | prependFolderName: v.boolean('`prependFolderName` must be a boolean'), 47 | prependPackageToLibExport: v.boolean('`prependPackageToLibExport` must be a boolean'), 48 | skipEmpty: v.boolean('`skipEmpty` must be a boolean') 49 | }); 50 | 51 | const DEFAULT_CONFIG: InferInput = { 52 | appendFolderName: false, 53 | defaultBarrelName: '', 54 | excludeDirList: [], 55 | excludeFileList: [], 56 | excludeFreezed: false, 57 | excludeGenerated: false, 58 | prependFolderName: false, 59 | prependPackageToLibExport: false, 60 | skipEmpty: false 61 | }; 62 | 63 | const run = async (directory: string, type: GenerationType, config: InferInput) => { 64 | const Context = createContext({ 65 | config: { ...config, promptName: false }, 66 | logger: cliLogger, 67 | options: { 68 | logTimestamps: false 69 | } 70 | }); 71 | 72 | const result = await Context.start({ 73 | fsPath: directory, 74 | path: toPosixPath(directory), 75 | type 76 | }); 77 | if (result.isErr()) { 78 | Context.onError(error); 79 | Context.endGeneration(); 80 | } else { 81 | Context.endGeneration(); 82 | } 83 | 84 | return result; 85 | }; 86 | 87 | const loadConfigFile = (configPath: string): ResultAsync => 88 | ResultAsync.fromPromise( 89 | readFile(configPath, 'utf8') 90 | .then((content) => JSON.parse(content)) 91 | .then((parsed) => ({ ...DEFAULT_CONFIG, ...parsed })), 92 | (error) => new Error(`Failed to load configuration file: ${error}`) 93 | ); 94 | 95 | program 96 | .name(packageName) 97 | .description(description) 98 | .version(version) 99 | .showSuggestionAfterError() 100 | .showHelpAfterError() 101 | .argument('', 'Target directory for barrel file generation') 102 | .option('-s, --subfolders', 'Include subfolders in the barrel file') 103 | .option('-r, --recursive', 'Generate barrel files recursively for all nested directories') 104 | .option('-c, --config ', 'Path to configuration file', undefined) 105 | .option('-n, --default-barrel-name ', 'Default name for barrel files', '') 106 | .option('-q, --quiet', 'Hide all logs', false) 107 | .option('--excluded-dirs ', 'Comma-separated list of directories to exclude', []) 108 | .option('--excluded-files ', 'Comma-separated list of files to exclude', []) 109 | .option('--exclude-freezed', 'Exclude freezed files', false) 110 | .option('--exclude-generated', 'Exclude generated files', false) 111 | .option('--skip-empty', 'Skip directories with no files', false) 112 | .option('--append-folder-name', 'Append folder name to barrel file name', false) 113 | .option('--prepend-folder-name', 'Prepend folder name to barrel file name', false) 114 | .option('--prepend-package', 'Prepend package name to exports in lib folder', false) 115 | .action(async (directory, { config, quiet, recursive, subfolders, ...opts }) => { 116 | const configFile = v.safeParse(ConfigPath, config); 117 | if (!configFile.success) { 118 | configFile.issues.forEach((i) => { 119 | cliLogger.error(i.message); 120 | }); 121 | process.exit(1); 122 | } 123 | const type = recursive ? 'RECURSIVE' : subfolders ? 'REGULAR_SUBFOLDERS' : 'REGULAR'; 124 | 125 | if (quiet) { 126 | Object.assign(cliLogger, { 127 | done: () => {}, 128 | error: () => {}, 129 | log: () => {}, 130 | warn: () => {} 131 | }); 132 | } 133 | 134 | const dir = v.parse(Directory, directory); 135 | const resolvedPath = path.resolve(dir); 136 | 137 | if (!fs.existsSync(resolvedPath)) { 138 | cliLogger.error(`Error: Directory does not exist: ${resolvedPath}`); 139 | process.exit(1); 140 | } 141 | 142 | const { excludedDirs, excludedFiles, prependPackage, ...rest } = opts; 143 | const configResult = configFile.output 144 | ? await loadConfigFile(configFile.output) 145 | : ok({ 146 | excludeDirList: excludedDirs, 147 | excludeFileList: excludedFiles, 148 | prependPackageToLibExport: prependPackage, 149 | ...rest 150 | }); 151 | 152 | if (configResult.isErr()) { 153 | cliLogger.error(configResult.error.message); 154 | process.exit(1); 155 | } 156 | 157 | const parsedConfig = v.safeParse(ConfigSchema, configResult.value); 158 | if (!parsedConfig.success) { 159 | cliLogger.error(`Invalid configuration: ${parsedConfig.issues.map((i) => i.message).join(', ')}`); 160 | process.exit(1); 161 | } 162 | 163 | const res = await run(resolvedPath, type, parsedConfig.output); 164 | const code = res.match( 165 | (path) => { 166 | cliLogger.done(SUCCESS_MESSAGES[type].replace('{path}', path)); 167 | return 0; 168 | }, 169 | (err) => { 170 | cliLogger.error(`${err.message}`); 171 | return 1; 172 | } 173 | ); 174 | process.exit(code); 175 | }); 176 | 177 | program.parse(); 178 | 179 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/bin.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`@dbfg/cli > --recursive > /dbf-cli/lib --prepend-package > /lib/lib.dart 1`] = ` 4 | "export 'package:dbf-cli/component.dart'; 5 | export 'package:dbf-cli/main.dart'; 6 | " 7 | `; 8 | 9 | exports[`@dbfg/cli > --recursive > /dbf-cli/src > /src/components/components.dart 1`] = ` 10 | "export 'component_one.dart'; 11 | export 'component_three.dart'; 12 | export 'component_three.g.dart'; 13 | export 'component_two.dart'; 14 | export 'component_two.freezed.dart'; 15 | export 'nested/nested.dart'; 16 | " 17 | `; 18 | 19 | exports[`@dbfg/cli > --recursive > /dbf-cli/src > /src/components/nested/nested.dart 1`] = ` 20 | "export 'component_one.dart'; 21 | export 'component_three.dart'; 22 | export 'component_two.dart'; 23 | " 24 | `; 25 | 26 | exports[`@dbfg/cli > --recursive > /dbf-cli/src > /src/src.dart 1`] = ` 27 | "export 'components/components.dart'; 28 | export 'main.dart'; 29 | " 30 | `; 31 | 32 | exports[`@dbfg/cli > --recursive > /dbf-cli/src --excluded-files main.dart > /src/components/components.dart 1`] = ` 33 | "export 'component_one.dart'; 34 | export 'component_three.dart'; 35 | export 'component_three.g.dart'; 36 | export 'component_two.dart'; 37 | export 'component_two.freezed.dart'; 38 | export 'nested/nested.dart'; 39 | " 40 | `; 41 | 42 | exports[`@dbfg/cli > --recursive > /dbf-cli/src --excluded-files main.dart > /src/components/nested/nested.dart 1`] = ` 43 | "export 'component_one.dart'; 44 | export 'component_three.dart'; 45 | export 'component_two.dart'; 46 | " 47 | `; 48 | 49 | exports[`@dbfg/cli > --recursive > /dbf-cli/src --excluded-files main.dart > /src/src.dart 1`] = ` 50 | "export 'components/components.dart'; 51 | export 'main.dart'; 52 | " 53 | `; 54 | 55 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components > /src/components/components.dart 1`] = ` 56 | "export 'component_one.dart'; 57 | export 'component_three.dart'; 58 | export 'component_three.g.dart'; 59 | export 'component_two.dart'; 60 | export 'component_two.freezed.dart'; 61 | export 'nested/nested.dart'; 62 | " 63 | `; 64 | 65 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components > /src/components/nested/nested.dart 1`] = ` 66 | "export 'component_one.dart'; 67 | export 'component_three.dart'; 68 | export 'component_two.dart'; 69 | " 70 | `; 71 | 72 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --append-folder-name > /src/components/components_components.dart 1`] = ` 73 | "export 'component_one.dart'; 74 | export 'component_three.dart'; 75 | export 'component_three.g.dart'; 76 | export 'component_two.dart'; 77 | export 'component_two.freezed.dart'; 78 | export 'nested/nested_nested.dart'; 79 | " 80 | `; 81 | 82 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --append-folder-name > /src/components/nested/nested_nested.dart 1`] = ` 83 | "export 'component_one.dart'; 84 | export 'component_three.dart'; 85 | export 'component_two.dart'; 86 | " 87 | `; 88 | 89 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --exclude-freezed > /src/components/components.dart 1`] = ` 90 | "export 'component_one.dart'; 91 | export 'component_three.dart'; 92 | export 'component_three.g.dart'; 93 | export 'component_two.dart'; 94 | export 'nested/nested.dart'; 95 | " 96 | `; 97 | 98 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --exclude-freezed > /src/components/nested/nested.dart 1`] = ` 99 | "export 'component_one.dart'; 100 | export 'component_three.dart'; 101 | export 'component_two.dart'; 102 | " 103 | `; 104 | 105 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --exclude-generated > /src/components/components.dart 1`] = ` 106 | "export 'component_one.dart'; 107 | export 'component_three.dart'; 108 | export 'component_two.dart'; 109 | export 'component_two.freezed.dart'; 110 | export 'nested/nested.dart'; 111 | " 112 | `; 113 | 114 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --exclude-generated > /src/components/nested/nested.dart 1`] = ` 115 | "export 'component_one.dart'; 116 | export 'component_three.dart'; 117 | export 'component_two.dart'; 118 | " 119 | `; 120 | 121 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --prepend-folder-name > /src/components/components_components.dart 1`] = ` 122 | "export 'component_one.dart'; 123 | export 'component_three.dart'; 124 | export 'component_three.g.dart'; 125 | export 'component_two.dart'; 126 | export 'component_two.freezed.dart'; 127 | export 'nested/nested_nested.dart'; 128 | " 129 | `; 130 | 131 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components --prepend-folder-name > /src/components/nested/nested_nested.dart 1`] = ` 132 | "export 'component_one.dart'; 133 | export 'component_three.dart'; 134 | export 'component_two.dart'; 135 | " 136 | `; 137 | 138 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components -n out > /src/components/nested/out.dart 1`] = ` 139 | "export 'component_one.dart'; 140 | export 'component_three.dart'; 141 | export 'component_two.dart'; 142 | " 143 | `; 144 | 145 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components -n out > /src/components/out.dart 1`] = ` 146 | "export 'component_one.dart'; 147 | export 'component_three.dart'; 148 | export 'component_three.g.dart'; 149 | export 'component_two.dart'; 150 | export 'component_two.freezed.dart'; 151 | export 'nested/out.dart'; 152 | " 153 | `; 154 | 155 | exports[`@dbfg/cli > --recursive > /dbf-cli/src/components/nested > /src/components/nested/nested.dart 1`] = ` 156 | "export 'component_one.dart'; 157 | export 'component_three.dart'; 158 | export 'component_two.dart'; 159 | " 160 | `; 161 | 162 | exports[`@dbfg/cli > --recursive > runs a complex example (--exclude-freezed --exclude-generated --excluded-files *_one.dart -n complex) > /src/components/complex.dart 1`] = ` 163 | "export 'component_one.dart'; 164 | export 'component_three.dart'; 165 | export 'component_two.dart'; 166 | export 'nested/complex.dart'; 167 | " 168 | `; 169 | 170 | exports[`@dbfg/cli > --recursive > runs a complex example (--exclude-freezed --exclude-generated --excluded-files *_one.dart -n complex) > /src/components/nested/complex.dart 1`] = ` 171 | "export 'component_one.dart'; 172 | export 'component_three.dart'; 173 | export 'component_two.dart'; 174 | " 175 | `; 176 | 177 | exports[`@dbfg/cli > --recursive > uses a config file > /src/components/json.dart 1`] = ` 178 | "export 'component_one.dart'; 179 | export 'component_three.dart'; 180 | export 'component_three.g.dart'; 181 | export 'component_two.dart'; 182 | export 'component_two.freezed.dart'; 183 | export 'nested/json.dart'; 184 | " 185 | `; 186 | 187 | exports[`@dbfg/cli > --recursive > uses a config file > /src/components/nested/json.dart 1`] = ` 188 | "export 'component_one.dart'; 189 | export 'component_three.dart'; 190 | export 'component_two.dart'; 191 | " 192 | `; 193 | 194 | exports[`@dbfg/cli > --regular > /dbf-cli/lib --prepend-package 1`] = ` 195 | "export 'package:dbf-cli/component.dart'; 196 | export 'package:dbf-cli/main.dart'; 197 | " 198 | `; 199 | 200 | exports[`@dbfg/cli > --regular > /dbf-cli/src --excluded-files main.dart 1`] = ` 201 | "export 'main.dart'; 202 | " 203 | `; 204 | 205 | exports[`@dbfg/cli > --regular > /dbf-cli/src 1`] = ` 206 | "export 'main.dart'; 207 | " 208 | `; 209 | 210 | exports[`@dbfg/cli > --regular > /dbf-cli/src/components --append-folder-name 1`] = ` 211 | "export 'component_one.dart'; 212 | export 'component_three.dart'; 213 | export 'component_three.g.dart'; 214 | export 'component_two.dart'; 215 | export 'component_two.freezed.dart'; 216 | " 217 | `; 218 | 219 | exports[`@dbfg/cli > --regular > /dbf-cli/src/components --exclude-freezed 1`] = ` 220 | "export 'component_one.dart'; 221 | export 'component_three.dart'; 222 | export 'component_three.g.dart'; 223 | export 'component_two.dart'; 224 | " 225 | `; 226 | 227 | exports[`@dbfg/cli > --regular > /dbf-cli/src/components --exclude-generated 1`] = ` 228 | "export 'component_one.dart'; 229 | export 'component_three.dart'; 230 | export 'component_two.dart'; 231 | export 'component_two.freezed.dart'; 232 | " 233 | `; 234 | 235 | exports[`@dbfg/cli > --regular > /dbf-cli/src/components --prepend-folder-name 1`] = ` 236 | "export 'component_one.dart'; 237 | export 'component_three.dart'; 238 | export 'component_three.g.dart'; 239 | export 'component_two.dart'; 240 | export 'component_two.freezed.dart'; 241 | " 242 | `; 243 | 244 | exports[`@dbfg/cli > --regular > /dbf-cli/src/components -n out 1`] = ` 245 | "export 'component_one.dart'; 246 | export 'component_three.dart'; 247 | export 'component_three.g.dart'; 248 | export 'component_two.dart'; 249 | export 'component_two.freezed.dart'; 250 | " 251 | `; 252 | 253 | exports[`@dbfg/cli > --regular > /dbf-cli/src/components 1`] = ` 254 | "export 'component_one.dart'; 255 | export 'component_three.dart'; 256 | export 'component_three.g.dart'; 257 | export 'component_two.dart'; 258 | export 'component_two.freezed.dart'; 259 | " 260 | `; 261 | 262 | exports[`@dbfg/cli > --regular > /dbf-cli/src/components/nested 1`] = ` 263 | "export 'component_one.dart'; 264 | export 'component_three.dart'; 265 | export 'component_two.dart'; 266 | " 267 | `; 268 | 269 | exports[`@dbfg/cli > --regular > runs a complex example (--exclude-freezed --exclude-generated --excluded-files *_one.dart -n complex) 1`] = ` 270 | "export 'component_one.dart'; 271 | export 'component_three.dart'; 272 | export 'component_two.dart'; 273 | " 274 | `; 275 | 276 | exports[`@dbfg/cli > --regular > uses a config file 1`] = ` 277 | "export 'component_one.dart'; 278 | export 'component_three.dart'; 279 | export 'component_three.g.dart'; 280 | export 'component_two.dart'; 281 | export 'component_two.freezed.dart'; 282 | " 283 | `; 284 | 285 | exports[`@dbfg/cli > --subfolders > /dbf-cli/lib --prepend-package 1`] = ` 286 | "export 'package:dbf-cli/component.dart'; 287 | export 'package:dbf-cli/main.dart'; 288 | " 289 | `; 290 | 291 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src --excluded-files main.dart 1`] = ` 292 | "export 'components/component_one.dart'; 293 | export 'components/component_three.dart'; 294 | export 'components/component_three.g.dart'; 295 | export 'components/component_two.dart'; 296 | export 'components/component_two.freezed.dart'; 297 | export 'components/nested/component_one.dart'; 298 | export 'components/nested/component_three.dart'; 299 | export 'components/nested/component_two.dart'; 300 | export 'main.dart'; 301 | " 302 | `; 303 | 304 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src 1`] = ` 305 | "export 'components/component_one.dart'; 306 | export 'components/component_three.dart'; 307 | export 'components/component_three.g.dart'; 308 | export 'components/component_two.dart'; 309 | export 'components/component_two.freezed.dart'; 310 | export 'components/nested/component_one.dart'; 311 | export 'components/nested/component_three.dart'; 312 | export 'components/nested/component_two.dart'; 313 | export 'main.dart'; 314 | " 315 | `; 316 | 317 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src/components --append-folder-name 1`] = ` 318 | "export 'component_one.dart'; 319 | export 'component_three.dart'; 320 | export 'component_three.g.dart'; 321 | export 'component_two.dart'; 322 | export 'component_two.freezed.dart'; 323 | export 'nested/component_one.dart'; 324 | export 'nested/component_three.dart'; 325 | export 'nested/component_two.dart'; 326 | " 327 | `; 328 | 329 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src/components --exclude-freezed 1`] = ` 330 | "export 'component_one.dart'; 331 | export 'component_three.dart'; 332 | export 'component_three.g.dart'; 333 | export 'component_two.dart'; 334 | export 'nested/component_one.dart'; 335 | export 'nested/component_three.dart'; 336 | export 'nested/component_two.dart'; 337 | " 338 | `; 339 | 340 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src/components --exclude-generated 1`] = ` 341 | "export 'component_one.dart'; 342 | export 'component_three.dart'; 343 | export 'component_two.dart'; 344 | export 'component_two.freezed.dart'; 345 | export 'nested/component_one.dart'; 346 | export 'nested/component_three.dart'; 347 | export 'nested/component_two.dart'; 348 | " 349 | `; 350 | 351 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src/components --prepend-folder-name 1`] = ` 352 | "export 'component_one.dart'; 353 | export 'component_three.dart'; 354 | export 'component_three.g.dart'; 355 | export 'component_two.dart'; 356 | export 'component_two.freezed.dart'; 357 | export 'nested/component_one.dart'; 358 | export 'nested/component_three.dart'; 359 | export 'nested/component_two.dart'; 360 | " 361 | `; 362 | 363 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src/components -n out 1`] = ` 364 | "export 'component_one.dart'; 365 | export 'component_three.dart'; 366 | export 'component_three.g.dart'; 367 | export 'component_two.dart'; 368 | export 'component_two.freezed.dart'; 369 | export 'nested/component_one.dart'; 370 | export 'nested/component_three.dart'; 371 | export 'nested/component_two.dart'; 372 | " 373 | `; 374 | 375 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src/components 1`] = ` 376 | "export 'component_one.dart'; 377 | export 'component_three.dart'; 378 | export 'component_three.g.dart'; 379 | export 'component_two.dart'; 380 | export 'component_two.freezed.dart'; 381 | export 'nested/component_one.dart'; 382 | export 'nested/component_three.dart'; 383 | export 'nested/component_two.dart'; 384 | " 385 | `; 386 | 387 | exports[`@dbfg/cli > --subfolders > /dbf-cli/src/components/nested 1`] = ` 388 | "export 'component_one.dart'; 389 | export 'component_three.dart'; 390 | export 'component_two.dart'; 391 | " 392 | `; 393 | 394 | exports[`@dbfg/cli > --subfolders > runs a complex example (--exclude-freezed --exclude-generated --excluded-files *_one.dart -n complex) 1`] = ` 395 | "export 'component_one.dart'; 396 | export 'component_three.dart'; 397 | export 'component_two.dart'; 398 | export 'nested/component_one.dart'; 399 | export 'nested/component_three.dart'; 400 | export 'nested/component_two.dart'; 401 | " 402 | `; 403 | 404 | exports[`@dbfg/cli > --subfolders > uses a config file 1`] = ` 405 | "export 'component_one.dart'; 406 | export 'component_three.dart'; 407 | export 'component_three.g.dart'; 408 | export 'component_two.dart'; 409 | export 'component_two.freezed.dart'; 410 | export 'nested/component_one.dart'; 411 | export 'nested/component_three.dart'; 412 | export 'nested/component_two.dart'; 413 | " 414 | `; 415 | -------------------------------------------------------------------------------- /packages/cli/test/bin.test.ts: -------------------------------------------------------------------------------- 1 | import type { GenerationConfig } from '@dbfg/core'; 2 | 3 | import { exec as _exec } from 'node:child_process'; 4 | import { existsSync, readdirSync } from 'node:fs'; 5 | import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; 6 | import os from 'node:os'; 7 | import { dirname, join } from 'node:path'; 8 | import { promisify } from 'node:util'; 9 | 10 | const JSON_CONFIG: Partial = { 11 | defaultBarrelName: 'json', 12 | prependPackageToLibExport: true 13 | }; 14 | const TREE = [ 15 | 'src/components/component_one.dart', 16 | 'src/components/component_two.dart', 17 | 'src/components/component_two.freezed.dart', 18 | 'src/components/component_three.dart', 19 | 'src/components/component_three.g.dart', 20 | 'src/components/nested/component_one.dart', 21 | 'src/components/nested/component_two.dart', 22 | 'src/components/nested/component_three.dart', 23 | 'src/main.dart', 24 | 'lib/main.dart', 25 | 'lib/component.dart' 26 | ]; 27 | 28 | const TMP_DIR = join(os.tmpdir(), 'dbf-cli'); 29 | const SRC_DIR = join(TMP_DIR, 'src'); 30 | const LIB_DIR = join(TMP_DIR, 'lib'); 31 | const COMPONENTS_DIR = join(SRC_DIR, 'components'); 32 | const NESTED_DIR = join(COMPONENTS_DIR, 'nested'); 33 | 34 | const COMPLEX_ARGS = Object.entries({ 35 | '--exclude-freezed': true, 36 | '--exclude-generated': true, 37 | '--excluded-files': '*_one.dart', 38 | '-n': 'complex' 39 | }).reduce((agg, [arg, value]) => `${agg} ${value === true ? arg : `${arg} ${value}`}`, '').trim(); 40 | 41 | const exec = promisify(_exec); 42 | 43 | const getOptionTitle = (dir: string, option?: string, args?: string) => `${dir.replace(os.tmpdir(), '')} ${option ?? ''} ${args ?? ''}`.trim(); 44 | 45 | const getAllBarrelFiles = async (from: string, defaultName?: string): Promise => { 46 | const result: string[] = []; 47 | const stack = [from]; 48 | 49 | while (stack.length > 0) { 50 | const currentDir = stack.pop()!; 51 | const files = readdirSync(currentDir, { withFileTypes: true }); 52 | 53 | for (const file of files) { 54 | const fullPath = join(currentDir, file.name); 55 | if (file.isDirectory()) { 56 | stack.push(fullPath); 57 | } else if ( 58 | (defaultName && fullPath.endsWith(defaultName)) || 59 | (!defaultName && !TREE.includes(fullPath.replace(TMP_DIR, '').slice(1))) 60 | ) { 61 | result.push(fullPath); 62 | } 63 | } 64 | } 65 | 66 | return result; 67 | }; 68 | 69 | beforeEach(async () => { 70 | await Promise.all(TREE.map(async (filePath) => { 71 | const fullPath = join(TMP_DIR, filePath); 72 | await mkdir(dirname(fullPath), { recursive: true }); 73 | return writeFile(fullPath, '', { flag: 'w' }); 74 | })); 75 | }); 76 | 77 | afterEach(async () => { 78 | if (existsSync(TMP_DIR)) { 79 | await rm(TMP_DIR, { force: true, recursive: true }); 80 | } 81 | }); 82 | 83 | describe('@dbfg/cli', () => { 84 | describe.each(['--regular', '--recursive', '--subfolders'])('%s', (genType) => { 85 | const type = genType === '--regular' ? ' ' : ` ${genType} `; 86 | 87 | [ 88 | { dir: SRC_DIR, out: 'src.dart' }, 89 | { dir: COMPONENTS_DIR, out: 'components.dart' }, 90 | { dir: NESTED_DIR, out: 'nested.dart' } 91 | ].forEach(({ dir, out }) => { 92 | it(getOptionTitle(dir), async () => { 93 | await exec(`bun ./src/bin.ts${type}${dir}`); 94 | 95 | if (genType === '--recursive') { 96 | // For recursive, we need to check all generated barrel files 97 | const barrelFiles = await getAllBarrelFiles(dir); 98 | expect(barrelFiles.length).toBeGreaterThan(0); 99 | 100 | for (const file of barrelFiles) { 101 | expect(await readFile(file, 'utf-8')).toMatchSnapshot(file.replace(TMP_DIR, '')); 102 | } 103 | } else { 104 | // For regular and subfolders modes, check only the expected output file 105 | expect(await readFile(join(dir, out), 'utf-8')).toMatchSnapshot(); 106 | } 107 | }); 108 | }); 109 | 110 | [ 111 | { args: 'out', dir: COMPONENTS_DIR, option: '-n', out: 'out.dart' }, 112 | { args: 'main.dart', dir: SRC_DIR, option: '--excluded-files', out: 'src.dart' }, 113 | { dir: COMPONENTS_DIR, option: '--exclude-freezed', out: 'components.dart' }, 114 | { dir: COMPONENTS_DIR, option: '--exclude-generated', out: 'components.dart' }, 115 | { dir: COMPONENTS_DIR, option: '--append-folder-name', out: 'components_components.dart' }, 116 | { dir: COMPONENTS_DIR, option: '--prepend-folder-name', out: 'components_components.dart' }, 117 | { dir: LIB_DIR, option: '--prepend-package', out: 'lib.dart' } 118 | ].forEach(({ args, dir, option, out }) => { 119 | it(getOptionTitle(dir, option, args), async () => { 120 | await exec(`bun ./src/bin.ts${type}${dir} ${option}${args ? ` ${args}` : ''}`); 121 | 122 | if (genType === '--recursive') { 123 | const barrelFiles = await getAllBarrelFiles(dir); 124 | expect(barrelFiles.length).toBeGreaterThan(0); 125 | 126 | for (const file of barrelFiles) { 127 | expect(await readFile(file, 'utf-8')).toMatchSnapshot(file.replace(TMP_DIR, '')); 128 | } 129 | } else { 130 | // For regular and subfolders, check only the expected output file 131 | expect(await readFile(join(dir, out), 'utf-8')).toMatchSnapshot(); 132 | } 133 | }); 134 | }); 135 | 136 | it(`runs a complex example (${COMPLEX_ARGS})`, async () => { 137 | await exec(`bun ./src/bin.ts${type}${COMPONENTS_DIR} ${COMPLEX_ARGS}`); 138 | 139 | if (genType === '--recursive') { 140 | const barrelFiles = await getAllBarrelFiles(COMPONENTS_DIR); 141 | expect(barrelFiles.length).toBeGreaterThan(0); 142 | 143 | for (const file of barrelFiles) { 144 | expect(await readFile(file, 'utf-8')).toMatchSnapshot(file.replace(TMP_DIR, '')); 145 | } 146 | } else { 147 | expect(await readFile(join(COMPONENTS_DIR, 'complex.dart'), 'utf-8')).toMatchSnapshot(); 148 | } 149 | }); 150 | 151 | it('uses a config file', async () => { 152 | const path = join(TMP_DIR, 'config.json'); 153 | await writeFile(path, JSON.stringify(JSON_CONFIG)); 154 | 155 | await exec(`bun ./src/bin.ts${type}${COMPONENTS_DIR} --config ${path}`); 156 | const defaultName = `${JSON_CONFIG.defaultBarrelName}.dart`; 157 | 158 | if (genType === '--recursive') { 159 | const barrelFiles = await getAllBarrelFiles(COMPONENTS_DIR, defaultName); 160 | expect(barrelFiles.length).toBeGreaterThan(0); 161 | 162 | for (const file of barrelFiles) { 163 | expect(await readFile(file, 'utf-8')).toMatchSnapshot(file.replace(TMP_DIR, '')); 164 | } 165 | } else { 166 | expect(await readFile(join(COMPONENTS_DIR, defaultName), 'utf-8')).toMatchSnapshot(); 167 | } 168 | }); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "resolveJsonModule": true, 8 | "types": ["vitest/globals"], 9 | "outDir": "./dist", 10 | "esModuleInterop": true 11 | }, 12 | "include": ["src/**/*", "test/**/*", "package.json"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { nodeExternals } from 'rollup-plugin-node-externals'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: ['src/bin.ts'], 9 | formats: ['cjs'] 10 | }, 11 | minify: false, 12 | outDir: 'dist', 13 | sourcemap: true, 14 | ssr: true, 15 | target: 'node20' 16 | }, 17 | plugins: [ 18 | nodeExternals({ exclude: ['@dbfg/core'] }) 19 | ], 20 | resolve: { 21 | alias: { 22 | '@dbfg/core': resolve(__dirname, '../core/src') 23 | } 24 | }, 25 | ssr: { noExternal: true } 26 | }); 27 | -------------------------------------------------------------------------------- /packages/cli/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | // Set up Vitest configuration 4 | export default defineConfig({ 5 | test: { 6 | disableConsoleIntercept: true, 7 | globals: true, 8 | include: ['./test/bin.test.ts'] 9 | } 10 | }); 11 | 12 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @dbfg/core 2 | 3 | ## 3.0.1 4 | 5 | ### Patch Changes 6 | 7 | - Dependency update ([#138](https://github.com/mikededo/dart-barrel-file-generator/pull/138)) 8 | 9 | ## 3.0.0 10 | 11 | ### Major Changes 12 | 13 | - Migrate to `neverthrow` ([#136](https://github.com/mikededo/dart-barrel-file-generator/pull/136)) 14 | 15 | ## 2.3.0 16 | 17 | ### Minor Changes 18 | 19 | - Allow disabling logging timestamps ([`fd071bc`](https://github.com/mikededo/dart-barrel-file-generator/commit/fd071bc41711f9b82ec3ab14dc2140dbac2cb418)) 20 | 21 | ## 2.2.1 22 | 23 | ### Patch Changes 24 | 25 | - Change package scope ([`4ea5d3d`](https://github.com/mikededo/dart-barrel-file-generator/commit/4ea5d3db75e62de4a4ef4dd478d0d4bc94e859f8)) 26 | 27 | ## 2.2.0 28 | 29 | ### Minor Changes 30 | 31 | - Adds tests and updates build step ([#123](https://github.com/mikededo/dart-barrel-file-generator/pull/123)) 32 | 33 | ## 2.1.0 34 | 35 | ### Minor Changes 36 | 37 | - Validate `workspace` folders in `vscode` package as it's specific feature ([#121](https://github.com/mikededo/dart-barrel-file-generator/pull/121)) 38 | 39 | ## 2.0.2 40 | 41 | ### Patch Changes 42 | 43 | - Use `fsPath` for generation ([`0ec29d2`](https://github.com/mikededo/dart-barrel-file-generator/commit/0ec29d2408e11e4c94860cdd3d971ade7b3bc4ea)) 44 | 45 | ## 2.0.1 46 | 47 | ### Patch Changes 48 | 49 | - Implement missing `onError` ([`279a8a2`](https://github.com/mikededo/dart-barrel-file-generator/commit/279a8a2794fd83ae1d2d350aac4e060098c010df)) 50 | 51 | ## 2.0.0 52 | 53 | ### Major Changes 54 | 55 | - Update the implementation of the `core`, and move all the generation logic into it. ([#116](https://github.com/mikededo/dart-barrel-file-generator/pull/116)) 56 | 57 | ## 1.0.1 58 | 59 | ### Patch Changes 60 | 61 | - Replace `dartBarrelFileGenerator` for `dart-barrel-file-generator` ([`0a4b3d6`](https://github.com/mikededo/dart-barrel-file-generator/commit/0a4b3d6e1188aa528d33aa33f578416ccb684b11)) 62 | 63 | ## 1.0.0 64 | 65 | ### Major Changes 66 | 67 | - a864a2a: Refactored into a monorepo setup. While this adds a breaking change, it does not 68 | affect the extension itself. It's a major release as it involves a major update 69 | into the codebase. 70 | Plus, the repository has also been cleaned up from dependencies and other 71 | issues. 72 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dbfg/core", 3 | "type": "module", 4 | "version": "3.0.1", 5 | "private": true, 6 | "exports": { 7 | "default": "./src/index.ts" 8 | }, 9 | "main": "./dist/index.js", 10 | "types": "./dist/index.d.ts", 11 | "scripts": { 12 | "test": "vitest", 13 | "test:cov": "vitest --coverage", 14 | "test:ui": "vitest --ui --reporter html" 15 | }, 16 | "dependencies": { 17 | "minimatch": "10.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const FILE_REGEX = { 2 | /** 3 | * Returns a regex that will match if the filename has 4 | * the same name as the barrel file of the `folder` param 5 | * 6 | * @param {string} folder The folder name 7 | * @returns {RegExp} The regex 8 | */ 9 | base: (folder: string): RegExp => new RegExp(`^${folder}\\.dart$`), 10 | 11 | /** 12 | * Used to check whether the current file name has a 13 | * dart file extension 14 | */ 15 | dart: /.+(\.dart)$/, 16 | 17 | /** 18 | * Used to check whether the current filename has a 19 | * dart file extension suffixed with the given value 20 | */ 21 | suffixed: (suffix: string) => new RegExp(`.+(\\.${suffix}\\.dart)$`) 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/src/context.test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GenerationConfig, 3 | GenerationLogger, 4 | GenerationType 5 | } from './types.js'; 6 | 7 | import * as fs from 'node:fs'; 8 | import * as fsPromises from 'node:fs/promises'; 9 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 10 | 11 | import { createContext } from './context.js'; 12 | import * as fn from './functions.js'; 13 | 14 | vi.mock('node:fs'); 15 | vi.mock('node:fs/promises'); 16 | vi.mock('./functions'); 17 | 18 | const createTestConfig = (overrides: Partial = {}): GenerationConfig => ({ 19 | appendFolderName: false, 20 | defaultBarrelName: '', 21 | excludeDirList: [], 22 | excludeFileList: [], 23 | excludeFreezed: false, 24 | excludeGenerated: false, 25 | prependFolderName: false, 26 | prependPackageToLibExport: false, 27 | promptName: false, 28 | skipEmpty: false, 29 | ...overrides 30 | }); 31 | 32 | const createTestLogger = (): GenerationLogger => ({ 33 | done: vi.fn(), 34 | error: vi.fn(), 35 | log: vi.fn(), 36 | warn: vi.fn() 37 | }); 38 | 39 | beforeEach(() => { 40 | vi.resetAllMocks(); 41 | 42 | vi.mocked(fn.toPosixPath).mockImplementation((path) => path); 43 | vi.mocked(fn.toOsSpecificPath).mockImplementation((path) => path); 44 | vi.mocked(fn.fileSort).mockImplementation((a, b) => a.localeCompare(b)); 45 | vi.mocked(fsPromises.writeFile).mockResolvedValue(undefined); 46 | }); 47 | 48 | describe('createContext', () => { 49 | describe('start', () => { 50 | it.each([ 51 | ['REGULAR' as GenerationType], 52 | ['RECURSIVE' as GenerationType], 53 | ['REGULAR_SUBFOLDERS' as GenerationType] 54 | ])('should handle %s generation type', async (type) => { 55 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 56 | 57 | if (type === 'REGULAR_SUBFOLDERS') { 58 | vi.mocked(fn.getAllFilesFromSubfolders) 59 | .mockReturnValue(['file1.dart', 'file2.dart']); 60 | } else { 61 | vi.mocked(fn.getFilesAndDirsFromPath) 62 | .mockReturnValue([['file1.dart', 'file2.dart'], new Set()]); 63 | } 64 | 65 | const config = createTestConfig(); 66 | const logger = createTestLogger(); 67 | const context = createContext({ config, logger }); 68 | 69 | const result = await context.start({ 70 | fsPath: '/test/path', 71 | path: '/test/path', 72 | type 73 | }); 74 | 75 | // Verify the generation process 76 | expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Generation started')); 77 | expect(logger.log).toHaveBeenCalledWith(expect.stringContaining(`Type: ${type.toLowerCase()}`)); 78 | expect(fs.lstatSync).toHaveBeenCalledWith('/test/path'); 79 | expect(fsPromises.writeFile).toHaveBeenCalled(); 80 | expect(result.isOk()).toBe(true); 81 | }); 82 | 83 | it('should return error when path is not a directory', async () => { 84 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => false } as fs.Stats); 85 | 86 | const config = createTestConfig(); 87 | const logger = createTestLogger(); 88 | const context = createContext({ config, logger }); 89 | 90 | const result = await context.start({ 91 | fsPath: '/test/file.txt', 92 | path: '/test/file.txt', 93 | type: 'REGULAR' 94 | }); 95 | 96 | expect(result.isErr()).toBe(true); 97 | expect(result._unsafeUnwrapErr().message).toBe('Select a folder from the workspace'); 98 | }); 99 | 100 | it('should skip empty folders when skipEmpty is true', async () => { 101 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 102 | vi.mocked(fn.getFilesAndDirsFromPath).mockReturnValue([[], new Set()]); 103 | 104 | const config = createTestConfig({ skipEmpty: true }); 105 | const logger = createTestLogger(); 106 | const context = createContext({ config, logger }); 107 | 108 | const result = await context.start({ 109 | fsPath: '/test/path', 110 | path: '/test/path', 111 | type: 'REGULAR' 112 | }); 113 | 114 | expect(fsPromises.writeFile).not.toHaveBeenCalled(); 115 | expect(result.isOk()).toBe(true); 116 | }); 117 | 118 | it('should use custom barrel name when provided', async () => { 119 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 120 | vi.mocked(fn.getFilesAndDirsFromPath).mockReturnValue([['file1.dart'], new Set()]); 121 | 122 | const config = createTestConfig({ defaultBarrelName: 'custom_barrel' }); 123 | const logger = createTestLogger(); 124 | const context = createContext({ config, logger }); 125 | 126 | await context.start({ 127 | fsPath: '/test/path', 128 | path: '/test/path', 129 | type: 'REGULAR' 130 | }); 131 | 132 | expect(fsPromises.writeFile).toHaveBeenCalledWith( 133 | '/test/path/custom_barrel.dart', 134 | expect.any(String), 135 | 'utf8' 136 | ); 137 | }); 138 | 139 | it('should prepend package name when configured and target is lib folder', async () => { 140 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 141 | vi.mocked(fn.getFilesAndDirsFromPath).mockReturnValue([['file1.dart'], new Set()]); 142 | vi.mocked(fn.isTargetLibFolder).mockReturnValue(true); 143 | 144 | const config = createTestConfig({ prependPackageToLibExport: true }); 145 | const logger = createTestLogger(); 146 | const context = createContext({ config, logger }); 147 | 148 | await context.start({ 149 | fsPath: '/test/my_package/lib', 150 | path: '/test/my_package/lib', 151 | type: 'REGULAR' 152 | }); 153 | 154 | expect(fn.isTargetLibFolder).toHaveBeenCalled(); 155 | // Verify file was written with package name in the contents 156 | expect(fsPromises.writeFile).toHaveBeenCalledWith( 157 | '/test/my_package/lib/lib.dart', 158 | expect.stringContaining('export \'package:'), 159 | 'utf8' 160 | ); 161 | }); 162 | 163 | it('should handle recursive generation for subdirectories', async () => { 164 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 165 | vi.mocked(fn.getFilesAndDirsFromPath).mockImplementation((_, path) => { 166 | if (path === '/test/path') { 167 | return [['file1.dart'], new Set(['subfolder'])]; 168 | } else if (path === '/test/path/subfolder') { 169 | return [['subfile1.dart'], new Set()]; 170 | } 171 | 172 | return [[], new Set()]; 173 | }); 174 | 175 | const config = createTestConfig(); 176 | const logger = createTestLogger(); 177 | const context = createContext({ config, logger }); 178 | 179 | await context.start({ 180 | fsPath: '/test/path', 181 | path: '/test/path', 182 | type: 'RECURSIVE' 183 | }); 184 | 185 | // Expect two writeFile calls - one for main dir and one for subdir 186 | expect(fsPromises.writeFile).toHaveBeenCalledTimes(2); 187 | }); 188 | }); 189 | 190 | describe('errors', () => { 191 | it('should handle writeFile errors', async () => { 192 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 193 | vi.mocked(fn.getFilesAndDirsFromPath).mockReturnValue([['file1.dart'], new Set()]); 194 | vi.mocked(fsPromises.writeFile).mockRejectedValue(new Error('Write error')); 195 | 196 | const config = createTestConfig(); 197 | const logger = createTestLogger(); 198 | const context = createContext({ config, logger }); 199 | 200 | const result = await context.start({ 201 | fsPath: '/test/path', 202 | path: '/test/path', 203 | type: 'REGULAR' 204 | }); 205 | 206 | expect(result.isErr()).toBe(true); 207 | expect(result._unsafeUnwrapErr().message).toBe('Write error'); 208 | }); 209 | 210 | it('should log errors with onError method', () => { 211 | const config = createTestConfig(); 212 | const logger = createTestLogger(); 213 | const context = createContext({ config, logger }); 214 | 215 | context.onError('Test error message'); 216 | 217 | expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('An error occurred')); 218 | expect(logger.error).toHaveBeenCalledWith('Test error message'); 219 | }); 220 | }); 221 | 222 | describe('utility methods', () => { 223 | it('should provide fsPath accessor', async () => { 224 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 225 | vi.mocked(fn.getFilesAndDirsFromPath).mockReturnValue([['file1.dart'], new Set()]); 226 | 227 | const config = createTestConfig(); 228 | const logger = createTestLogger(); 229 | const context = createContext({ config, logger }); 230 | 231 | expect(context.fsPath.isErr()).toBe(true); 232 | 233 | await context.start({ 234 | fsPath: '/test/path', 235 | path: '/test/path', 236 | type: 'REGULAR' 237 | }); 238 | 239 | expect(context.fsPath.isOk()).toBe(true); 240 | expect(context.fsPath._unsafeUnwrap()).toBe('/test/path'); 241 | }); 242 | 243 | it('should provide path accessor', async () => { 244 | vi.mocked(fs.lstatSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); 245 | vi.mocked(fn.getFilesAndDirsFromPath).mockReturnValue([['file1.dart'], new Set()]); 246 | 247 | const config = createTestConfig(); 248 | const logger = createTestLogger(); 249 | const context = createContext({ config, logger }); 250 | 251 | expect(context.path.isErr()).toBe(true); 252 | 253 | await context.start({ 254 | fsPath: '/test/path', 255 | path: '/test/path', 256 | type: 'REGULAR' 257 | }); 258 | 259 | expect(context.path.isOk()).toBe(true); 260 | expect(context.path._unsafeUnwrap()).toBe('/test/path'); 261 | }); 262 | 263 | it('should log completion with endGeneration method', () => { 264 | const config = createTestConfig(); 265 | const logger = createTestLogger(); 266 | const context = createContext({ config, logger }); 267 | 268 | context.endGeneration(); 269 | 270 | expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Generation finished')); 271 | }); 272 | }); 273 | }); 274 | -------------------------------------------------------------------------------- /packages/core/src/context.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GenerationConfig, 3 | GenerationLogger, 4 | GenerationType 5 | } from './types.js'; 6 | 7 | import type { Result } from 'neverthrow'; 8 | import { err, ok, ResultAsync } from 'neverthrow'; 9 | import { lstatSync } from 'node:fs'; 10 | import { writeFile } from 'node:fs/promises'; 11 | 12 | import { 13 | fileSort, 14 | formatDate, 15 | getAllFilesFromSubfolders, 16 | getFilesAndDirsFromPath, 17 | isTargetLibFolder, 18 | toOsSpecificPath, 19 | toPosixPath 20 | } from './functions.js'; 21 | 22 | type CreateContextOptions = { 23 | config: GenerationConfig; 24 | logger: GenerationLogger; 25 | options?: { logTimestamps?: boolean }; 26 | }; 27 | type StartParams = { 28 | fsPath: string; 29 | path: string; 30 | type: GenerationType; 31 | }; 32 | type State = { 33 | fsPath: string; 34 | path: string; 35 | startTimestamp?: number; 36 | type?: GenerationType; 37 | promptedName?: string; 38 | }; 39 | 40 | const DEFAULT_OPTIONS: CreateContextOptions['options'] = { 41 | logTimestamps: true 42 | }; 43 | export const createContext = ({ config, logger, options = DEFAULT_OPTIONS }: CreateContextOptions) => { 44 | const state: State = { fsPath: '', path: '', type: undefined }; 45 | 46 | const parseLogString = (log: string) => { 47 | if (!options.logTimestamps) { 48 | return log; 49 | } 50 | 51 | return `[${formatDate()}] ${log}`; 52 | }; 53 | 54 | const endGeneration = () => { 55 | logger.log(parseLogString('Generation finished')); 56 | }; 57 | 58 | const definedOrError = (value: T): Result => { 59 | if (!state[value]) { 60 | return err( 61 | new Error(`Cannot access ${value} in context. Did you initialise the context?`) 62 | ); 63 | } 64 | 65 | return ok(state[value]); 66 | }; 67 | 68 | const getPackageName = () => { 69 | const parts = toPosixPath(state.fsPath).split('/lib'); 70 | const path = parts[0].split('/'); 71 | return `package:${path[path.length - 1]}/`; 72 | }; 73 | 74 | /** 75 | * 76 | * @param targetPath The target path of the barrel file 77 | * @param dirName The barrel file directory name 78 | * @param files The file names to write to the barrel file 79 | * @returns A promise with the path of the written barrel file 80 | */ 81 | const writeBarrelFile = ( 82 | targetPath: string, 83 | dirName: string, 84 | files: string[] 85 | ): ResultAsync => { 86 | let exports = ''; 87 | // Check if we should prepend the package 88 | const shouldPrependPackage = config.prependPackageToLibExport && isTargetLibFolder(targetPath); 89 | for (const file of files) { 90 | exports = `${exports}export '${shouldPrependPackage ? getPackageName() : ''}${file}';\n`; 91 | } 92 | 93 | logger.log(parseLogString(`Exporting ${targetPath} - found ${files.length} Dart files`)); 94 | const barrelFile = `${targetPath}/${dirName}.dart`; 95 | const path = toOsSpecificPath(barrelFile); 96 | 97 | return ResultAsync.fromPromise( 98 | writeFile(path, exports, 'utf8') 99 | .then(() => { 100 | logger.log(parseLogString(`Generated successfull barrel file at ${path}`)); 101 | return path; 102 | }), 103 | (error: unknown) => { 104 | logger.log(error as any); 105 | return error instanceof Error ? error : new Error(String(error)); 106 | } 107 | ); 108 | }; 109 | 110 | /** 111 | * @param targetPath The target path of the barrel file 112 | * @returns A promise with the name of the barrel file 113 | */ 114 | const getBarrelFile = (targetPath: string): string => { 115 | const shouldAppend = config.appendFolderName; 116 | const shouldPrepend = config.prependFolderName; 117 | 118 | // Selected target is in the current workspace 119 | // This could be optional 120 | const splitDir = targetPath.split('/'); 121 | const prependedDir = shouldPrepend ? `${splitDir[splitDir.length - 1]}_` : ''; 122 | const appendedDir = shouldAppend ? `_${splitDir[splitDir.length - 1]}` : ''; 123 | 124 | // Check if the user has the defaultBarrelName config set 125 | if (config.defaultBarrelName) { 126 | return `${prependedDir}${config.defaultBarrelName.replace(/ /g, '_').toLowerCase()}${appendedDir}`; 127 | } 128 | 129 | return `${prependedDir}${splitDir[splitDir.length - 1]}${appendedDir}`; 130 | }; 131 | 132 | /** 133 | * Generates the contents of the barrel file, recursively when the 134 | * option chosen is recursive 135 | * 136 | * @param targetPath The target path of the barrel file 137 | * @returns A promise with the path of the written barrel file 138 | */ 139 | const generate = async (targetPath: string) => { 140 | const skipEmpty = config.skipEmpty; 141 | const barrelFileName = getBarrelFile(targetPath); 142 | 143 | if (state.type === 'REGULAR_SUBFOLDERS') { 144 | const files = getAllFilesFromSubfolders( 145 | barrelFileName, 146 | targetPath, 147 | config 148 | ).sort(fileSort); 149 | 150 | if (files.length === 0 && skipEmpty) { 151 | return Promise.resolve(ok('')); 152 | } 153 | 154 | return writeBarrelFile(targetPath, barrelFileName, files); 155 | } 156 | 157 | const [files, dirs] = getFilesAndDirsFromPath(barrelFileName, targetPath, config); 158 | if (state.type === 'RECURSIVE' && dirs.size > 0) { 159 | for (const d of dirs) { 160 | const maybeGenerated = await generate(`${targetPath}/${d}`); 161 | if (maybeGenerated.isErr()) { 162 | logger.error(maybeGenerated.error.message); 163 | continue; 164 | } 165 | 166 | if (!maybeGenerated.value && skipEmpty) { 167 | continue; 168 | } 169 | 170 | files.push( 171 | toPosixPath(maybeGenerated.value).split(`${targetPath}/`)[1] 172 | ); 173 | } 174 | } 175 | 176 | if (files.length === 0 && skipEmpty) { 177 | return Promise.resolve(ok('')); 178 | } 179 | 180 | // Sort files 181 | return writeBarrelFile(targetPath, barrelFileName, files.sort(fileSort)); 182 | }; 183 | 184 | const validateAndGenerate = async () => { 185 | const dir = toPosixPath(state.fsPath); 186 | if (!lstatSync(dir).isDirectory()) { 187 | return err(new Error('Select a folder from the workspace')); 188 | } 189 | 190 | return generate(dir); 191 | }; 192 | 193 | const start = ({ fsPath, path, type }: StartParams) => { 194 | const ts = new Date(); 195 | state.startTimestamp = ts.getTime(); 196 | 197 | state.fsPath = fsPath; 198 | state.path = path; 199 | state.type = type; 200 | 201 | logger.log( 202 | parseLogString(`Generation started ${options.logTimestamps ? formatDate(ts) : ''}`) 203 | ); 204 | logger.log(parseLogString(`Type: ${type.toLowerCase()} - Path: ${fsPath}`)); 205 | 206 | return validateAndGenerate(); 207 | }; 208 | 209 | return { 210 | endGeneration, 211 | get fsPath() { 212 | return definedOrError('fsPath'); 213 | }, 214 | onError: (error: string) => { 215 | logger.log(parseLogString(`An error occurred:`)); 216 | logger.error(error); 217 | }, 218 | get path() { 219 | return definedOrError('path'); 220 | }, 221 | start 222 | }; 223 | }; 224 | -------------------------------------------------------------------------------- /packages/core/src/functions.test.ts: -------------------------------------------------------------------------------- 1 | import type { GenerationConfig } from './types.js'; 2 | 3 | import type * as fs from 'node:fs'; 4 | 5 | import { 6 | fileSort, 7 | formatDate, 8 | getAllFilesFromSubfolders, 9 | getFilesAndDirsFromPath, 10 | isBarrelFile, 11 | isDartFile, 12 | isTargetLibFolder, 13 | matchesGlob, 14 | shouldExport, 15 | shouldExportDirectory, 16 | toOsSpecificPath, 17 | toPosixPath 18 | } from './functions.js'; 19 | 20 | const DEFAULT_CONFIG_OPTIONS: GenerationConfig = { 21 | appendFolderName: false, 22 | defaultBarrelName: undefined, 23 | excludeDirList: [], 24 | excludeFileList: [], 25 | excludeFreezed: false, 26 | excludeGenerated: false, 27 | prependFolderName: false, 28 | prependPackageToLibExport: false, 29 | promptName: false, 30 | skipEmpty: false 31 | }; 32 | 33 | const mockSepValue = vi.hoisted(vi.fn); 34 | vi.mock('node:path', async () => { 35 | const actual = await vi.importActual('node:path'); 36 | mockSepValue.mockReturnValue('/'); 37 | 38 | return { 39 | ...actual, 40 | get sep() { 41 | return mockSepValue(); 42 | } 43 | }; 44 | }); 45 | 46 | const mockReaddirSync = vi.hoisted(vi.fn); 47 | vi.mock('node:fs', async (importActual) => ({ 48 | ...(await importActual()), 49 | readdirSync: mockReaddirSync 50 | })); 51 | 52 | const notImplemented = (fn: string) => () => { 53 | throw new Error(`${fn} not implemented.`); 54 | }; 55 | 56 | type ReaddirSync = ReturnType[number]; 57 | const createReaddirSyncMock = (props: Partial): ReaddirSync => ({ 58 | isBlockDevice: notImplemented('isBlockDevice'), 59 | isCharacterDevice: notImplemented('isCharacterDevice'), 60 | isDirectory: notImplemented('isDirectory'), 61 | isFIFO: notImplemented('isFIFO'), 62 | isFile: notImplemented('isFile'), 63 | isSocket: notImplemented('isSocket'), 64 | isSymbolicLink: notImplemented('isSymbolicLink'), 65 | name: '', 66 | parentPath: '', 67 | path: '', 68 | ...props 69 | }); 70 | 71 | describe('toPosixPath', () => { 72 | it('converts windows-style paths to posix paths', () => { 73 | mockSepValue.mockImplementationOnce(() => '\\'); 74 | 75 | const windowsPath = 'C:\\Users\\test\\dart\\project'; 76 | expect(toPosixPath(windowsPath)).toBe('C:/Users/test/dart/project'); 77 | }); 78 | 79 | it('leaves posix paths unchanged', () => { 80 | const posixPath = '/Users/test/dart/project'; 81 | expect(toPosixPath(posixPath)).toBe(posixPath); 82 | }); 83 | }); 84 | 85 | describe('formatDate', () => { 86 | it('formats the current date correctly', () => { 87 | expect(formatDate(new Date('2023-05-15T12:30:45.123Z'))).toBe('2023-05-15 12:30:45'); 88 | }); 89 | 90 | it('uses current date when no date is provided', () => { 91 | expect(formatDate()).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); 92 | }); 93 | }); 94 | 95 | describe('isTargetLibFolder', () => { 96 | it('returns true when path ends with lib', () => { 97 | expect(isTargetLibFolder('/project/lib')).toBe(true); 98 | expect(isTargetLibFolder('C:/dart/project/lib')).toBe(true); 99 | }); 100 | 101 | it('returns false when path does not end with lib', () => { 102 | expect(isTargetLibFolder('/project/src')).toBe(false); 103 | expect(isTargetLibFolder('/project/lib/src')).toBe(false); 104 | expect(isTargetLibFolder('lib/src')).toBe(false); 105 | }); 106 | }); 107 | 108 | describe('toOsSpecificPath', () => { 109 | afterEach(() => { 110 | mockSepValue.mockRestore(); 111 | }); 112 | 113 | it('converts posix paths to OS-specific paths (/)', () => { 114 | const posixPath = '/Users/test/dart/project'; 115 | expect(toOsSpecificPath(posixPath)).toBe(posixPath.split('/').join('/')); 116 | }); 117 | 118 | it('converts posix paths to OS-specific paths (\\\\)', () => { 119 | mockSepValue.mockImplementation(() => '\\\\'); 120 | 121 | const posixPath = '/Users/test/dart/project'; 122 | expect(toOsSpecificPath(posixPath)).toBe(posixPath.split('/').join('\\\\')); 123 | }); 124 | }); 125 | 126 | describe('isDartFile', () => { 127 | it('returns true for dart files', () => { 128 | expect(isDartFile('test.dart')).toBe(true); 129 | expect(isDartFile('complex_name.dart')).toBe(true); 130 | }); 131 | 132 | it('returns false for non-dart files', () => { 133 | expect(isDartFile('test.txt')).toBe(false); 134 | expect(isDartFile('dart.js')).toBe(false); 135 | expect(isDartFile('test.dart.bak')).toBe(false); 136 | }); 137 | }); 138 | 139 | describe('isBarrelFile', () => { 140 | it('returns true when file matches directory barrel name', () => { 141 | expect(isBarrelFile('src', 'src.dart')).toBe(true); 142 | expect(isBarrelFile('models', 'models.dart')).toBe(true); 143 | }); 144 | 145 | it('returns false when file does not match directory barrel name', () => { 146 | expect(isBarrelFile('src', 'main.dart')).toBe(false); 147 | expect(isBarrelFile('models', 'user.dart')).toBe(false); 148 | }); 149 | }); 150 | 151 | describe('matchesGlob', () => { 152 | it('matches files with glob pattern correctly', () => { 153 | expect(matchesGlob('test.dart', '*.dart')).toBe(true); 154 | expect(matchesGlob('/path/to/test.dart', '**/*.dart')).toBe(true); 155 | expect(matchesGlob('test.freezed.dart', '*.freezed.dart')).toBe(true); 156 | }); 157 | 158 | it('returns false when file does not match glob pattern', () => { 159 | expect(matchesGlob('test.dart', '*.js')).toBe(false); 160 | expect(matchesGlob('/path/to/test.dart', '/other/path/**/*.dart')).toBe(false); 161 | }); 162 | }); 163 | 164 | describe('fileSort', () => { 165 | it('sorts strings alphabetically', () => { 166 | expect(fileSort('a', 'b')).toBe(-1); 167 | expect(fileSort('b', 'a')).toBe(1); 168 | expect(fileSort('a', 'a')).toBe(0); 169 | 170 | const files = ['c.dart', 'a.dart', 'b.dart']; 171 | expect(files.sort(fileSort)).toEqual(['a.dart', 'b.dart', 'c.dart']); 172 | }); 173 | }); 174 | 175 | describe('shouldExport', () => { 176 | it('returns true for regular dart files', () => { 177 | expect( 178 | shouldExport('test.dart', '/path/test.dart', 'path', DEFAULT_CONFIG_OPTIONS) 179 | ).toBe(true); 180 | }); 181 | 182 | it('returns false for barrel files', () => { 183 | expect( 184 | shouldExport('folder.dart', '/path/folder.dart', 'folder', DEFAULT_CONFIG_OPTIONS) 185 | ).toBe(false); 186 | }); 187 | 188 | it('returns false for non-dart files', () => { 189 | expect( 190 | shouldExport('test.js', '/path/test.js', 'path', DEFAULT_CONFIG_OPTIONS) 191 | ).toBe(false); 192 | }); 193 | 194 | it.each([true, false])( 195 | 'handles freezed files according to config (excludeFreezed: %o)', 196 | (excludeFreezed) => { 197 | expect( 198 | shouldExport( 199 | 'test.freezed.dart', 200 | '/path/test.freezed.dart', 201 | 'path', 202 | { ...DEFAULT_CONFIG_OPTIONS, excludeFreezed } 203 | ) 204 | ).toBe(!excludeFreezed); 205 | } 206 | ); 207 | 208 | it.each([true, false])( 209 | 'handles generated files according to config (excludeGenerated: %o)', 210 | (excludeGenerated) => { 211 | expect( 212 | shouldExport( 213 | 'test.g.dart', 214 | '/path/test.g.dart', 215 | 'path', 216 | { ...DEFAULT_CONFIG_OPTIONS, excludeGenerated } 217 | ) 218 | ).toBe(!excludeGenerated); 219 | } 220 | ); 221 | 222 | it('respects exclude file list', () => { 223 | expect(shouldExport('excluded.dart', '/path/excluded.dart', 'path', { 224 | ...DEFAULT_CONFIG_OPTIONS, 225 | excludeFileList: ['**/excluded.dart'] 226 | })).toBe(false); 227 | 228 | expect(shouldExport('included.dart', '/path/included.dart', 'path', { 229 | ...DEFAULT_CONFIG_OPTIONS, 230 | excludeFileList: ['**/excluded.dart'] 231 | })).toBe(true); 232 | }); 233 | }); 234 | 235 | describe('shouldExportDirectory', () => { 236 | it('returns true for non-excluded directories', () => { 237 | expect(shouldExportDirectory('/path/dir', DEFAULT_CONFIG_OPTIONS)).toBe(true); 238 | }); 239 | 240 | it('returns false for excluded directories', () => { 241 | expect(shouldExportDirectory('/path/excluded', { 242 | ...DEFAULT_CONFIG_OPTIONS, 243 | excludeDirList: ['**/excluded'] 244 | })).toBe(false); 245 | }); 246 | 247 | it('returns true when directory does not match exclusion pattern', () => { 248 | expect(shouldExportDirectory('/path/included', { 249 | ...DEFAULT_CONFIG_OPTIONS, 250 | excludeDirList: ['**/excluded'] 251 | })).toBe(true); 252 | }); 253 | }); 254 | 255 | describe('getFilesAndDirsFromPath', () => { 256 | beforeEach(() => { 257 | mockReaddirSync.mockClear(); 258 | }); 259 | 260 | afterAll(() => { 261 | mockReaddirSync.mockRestore(); 262 | }); 263 | 264 | it('correctly identifies files and directories', () => { 265 | mockReaddirSync.mockReturnValueOnce([ 266 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file.dart' }), 267 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'dir' }) 268 | ]); 269 | 270 | const [files, dirs] = getFilesAndDirsFromPath('barrel', '/path', DEFAULT_CONFIG_OPTIONS); 271 | 272 | expect(files).toEqual(['file.dart']); 273 | expect(dirs.has('dir')).toBe(true); 274 | expect(dirs.size).toBe(1); 275 | }); 276 | 277 | it('filters out barrel files', () => { 278 | mockReaddirSync.mockReturnValueOnce([ 279 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'barrel.dart' }), 280 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file.dart' }) 281 | ]); 282 | 283 | const [files, dirs] = getFilesAndDirsFromPath('barrel', '/path', DEFAULT_CONFIG_OPTIONS); 284 | 285 | expect(files).toEqual(['file.dart']); 286 | expect(files).not.toContain('barrel.dart'); 287 | expect(dirs.size).toBe(0); 288 | }); 289 | 290 | it('respects exclude configurations for files', () => { 291 | mockReaddirSync.mockReturnValueOnce([ 292 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file.dart' }), 293 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'excluded.dart' }) 294 | ]); 295 | 296 | const [files] = getFilesAndDirsFromPath('barrel', '/path', { 297 | ...DEFAULT_CONFIG_OPTIONS, 298 | excludeFileList: ['**/excluded.dart'] 299 | }); 300 | 301 | expect(files).toEqual(['file.dart']); 302 | expect(files).not.toContain('excluded.dart'); 303 | }); 304 | 305 | it('respects exclude configurations for directories', () => { 306 | mockReaddirSync.mockReturnValueOnce([ 307 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'dir' }), 308 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'excluded_dir' }) 309 | ]); 310 | 311 | const [_, dirs] = getFilesAndDirsFromPath('barrel', '/path', { 312 | ...DEFAULT_CONFIG_OPTIONS, 313 | excludeDirList: ['**/excluded_dir'] 314 | }); 315 | 316 | expect(dirs.has('dir')).toBe(true); 317 | expect(dirs.has('excluded_dir')).toBe(false); 318 | }); 319 | 320 | it('ignores non-dart files', () => { 321 | mockReaddirSync.mockReturnValueOnce([ 322 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file.dart' }), 323 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file.js' }) 324 | ]); 325 | 326 | const [files] = getFilesAndDirsFromPath('barrel', '/path', DEFAULT_CONFIG_OPTIONS); 327 | 328 | expect(files).toEqual(['file.dart']); 329 | expect(files).not.toContain('file.js'); 330 | }); 331 | }); 332 | 333 | describe('getAllFilesFromSubfolders', () => { 334 | beforeEach(() => { 335 | mockReaddirSync.mockClear(); 336 | }); 337 | 338 | afterAll(() => { 339 | mockReaddirSync.mockRestore(); 340 | }); 341 | 342 | it('collects files from current directory', () => { 343 | mockReaddirSync.mockReturnValueOnce([ 344 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file1.dart' }), 345 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file2.dart' }) 346 | ]); 347 | 348 | const files = getAllFilesFromSubfolders('barrel', '/path', DEFAULT_CONFIG_OPTIONS); 349 | 350 | expect(files).toEqual(['file1.dart', 'file2.dart']); 351 | }); 352 | 353 | it('collects files from subdirectories with correct paths', () => { 354 | // Root directory 355 | mockReaddirSync.mockReturnValueOnce([ 356 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file.dart' }), 357 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'subdir' }) 358 | ]); 359 | // Subdir 360 | mockReaddirSync.mockReturnValueOnce([ 361 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'subfile.dart' }) 362 | ]); 363 | 364 | const files = getAllFilesFromSubfolders('barrel', '/path', DEFAULT_CONFIG_OPTIONS); 365 | 366 | expect(files).toContain('file.dart'); 367 | expect(files).toContain('subdir/subfile.dart'); 368 | expect(files.length).toBe(2); 369 | }); 370 | 371 | it('handles deeply nested directories', () => { 372 | // Root directory 373 | mockReaddirSync.mockReturnValueOnce([ 374 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'file.dart' }), 375 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'level1' }) 376 | ]); 377 | // L1 378 | mockReaddirSync.mockReturnValueOnce([ 379 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'level1_file.dart' }), 380 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'level2' }) 381 | ]); 382 | // L2 383 | mockReaddirSync.mockReturnValueOnce([ 384 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'level2_file.dart' }) 385 | ]); 386 | 387 | const files = getAllFilesFromSubfolders('barrel', '/path', DEFAULT_CONFIG_OPTIONS); 388 | 389 | expect(files).toContain('file.dart'); 390 | expect(files).toContain('level1/level1_file.dart'); 391 | expect(files).toContain('level1/level2/level2_file.dart'); 392 | expect(files.length).toBe(3); 393 | }); 394 | 395 | it('respects exclusions in nested directories', () => { 396 | // Root directory 397 | mockReaddirSync.mockReturnValueOnce([ 398 | createReaddirSyncMock({ 399 | isDirectory: () => false, 400 | isFile: () => true, 401 | name: 'file.dart', 402 | parentPath: '', 403 | path: '' 404 | }), 405 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'included' }), 406 | createReaddirSyncMock({ isDirectory: () => true, isFile: () => false, name: 'excluded' }) 407 | ]); 408 | // Included directory 409 | mockReaddirSync.mockReturnValueOnce([ 410 | createReaddirSyncMock({ isDirectory: () => false, isFile: () => true, name: 'included_file.dart' }) 411 | ]); 412 | 413 | const files = getAllFilesFromSubfolders('barrel', '/path', { 414 | ...DEFAULT_CONFIG_OPTIONS, 415 | excludeDirList: ['**/excluded'] 416 | }); 417 | 418 | expect(files).toContain('file.dart'); 419 | expect(files).toContain('included/included_file.dart'); 420 | expect(files.length).toBe(2); 421 | expect(mockReaddirSync).toHaveBeenCalledTimes(2); // Only root and included dir 422 | }); 423 | }); 424 | -------------------------------------------------------------------------------- /packages/core/src/functions.ts: -------------------------------------------------------------------------------- 1 | import type { GenerationConfig } from './types.js'; 2 | 3 | import { minimatch } from 'minimatch'; 4 | import * as fs from 'node:fs'; 5 | import * as path from 'node:path'; 6 | 7 | import { FILE_REGEX } from './constants.js'; 8 | 9 | type PosixPath = string; 10 | 11 | /** 12 | * Converts a path to a generic `PosixPath` 13 | * 14 | * @param pathname Path location 15 | * @returns An equal location with posix separators 16 | */ 17 | export const toPosixPath = (pathname: string): PosixPath => 18 | pathname.split(path.sep).join(path.posix.sep); 19 | 20 | export const formatDate = (date: Date = new Date()) => 21 | date.toISOString().replace(/T/, ' ').replace(/\..+/, ''); 22 | 23 | /** 24 | * Checks if the given target path is the lib folder 25 | * 26 | * @param targetPath The barrel file target path 27 | * @returns If the path is the `lib` folder 28 | */ 29 | export const isTargetLibFolder = (targetPath: string) => { 30 | const parts = targetPath.split('/'); 31 | return parts[parts.length - 1] === 'lib'; 32 | }; 33 | 34 | /** 35 | * Converts a `PosixPath` to a OS specific path 36 | * 37 | * @param pathname Current path 38 | * @returns A location path with os specific separators 39 | */ 40 | export const toOsSpecificPath = (pathname: PosixPath) => 41 | pathname.split(path.posix.sep).join(path.sep); 42 | 43 | /** 44 | * Checks if a file is a dart file 45 | * 46 | * @param fileName The file name to evaluate 47 | * @returns If the file name is a valid dart file 48 | */ 49 | export const isDartFile = (fileName: string) => FILE_REGEX.dart.test(fileName); 50 | 51 | /** 52 | * Checks if a file has the name of the current folder barrel file 53 | * 54 | * @param dirName The directory name 55 | * @param fileName The file name to evaluate 56 | * @returns If the file name equals the name of the 57 | * barrel file 58 | */ 59 | export const isBarrelFile = (dirName: string, fileName: string) => 60 | FILE_REGEX.base(dirName).test(fileName); 61 | 62 | /** 63 | * Checks if the given file name matches the given glob 64 | * 65 | * @param fileName The file to check for 66 | * @param glob The glob to compare the string with 67 | * @returns Whether it matches the glob or not 68 | */ 69 | export const matchesGlob = (fileName: string, glob: string) => 70 | minimatch(fileName, glob); 71 | 72 | /** 73 | * Sorts the file names alphabetically 74 | */ 75 | export const fileSort = (a: string, b: string): number => 76 | a < b ? -1 : a > b ? 1 : 0; 77 | 78 | /** 79 | * Checks if the given `posixPath` is a dart file, it has a different 80 | * name than the folder barrel file and is not excluded by any configuration 81 | * 82 | * @param fileName File name 83 | * @param filePath File path 84 | * @param dirName The current directory name 85 | * @returns If the given `posixPath` should be added to the list of 86 | * exports 87 | */ 88 | export const shouldExport = ( 89 | fileName: PosixPath, 90 | filePath: PosixPath, 91 | dirName: string, 92 | { excludeFileList, excludeFreezed, excludeGenerated }: GenerationConfig 93 | ) => { 94 | if (isDartFile(fileName) && !isBarrelFile(dirName, fileName)) { 95 | if (FILE_REGEX.suffixed('freezed').test(fileName)) { 96 | // Export only if files are not excluded 97 | return !excludeFreezed; 98 | } 99 | 100 | if (FILE_REGEX.suffixed('g').test(fileName)) { 101 | // Export only if files are not excluded 102 | return !excludeGenerated; 103 | } 104 | 105 | const globs: string[] = excludeFileList ?? []; 106 | return globs.every((glob) => !matchesGlob(filePath, glob)); 107 | } 108 | 109 | return false; 110 | }; 111 | 112 | /** 113 | * Checks if the given `posixPath` is not excluded by any configuration 114 | * 115 | * @param posixPath The path to check 116 | * @returns If the given `posixPath` should be added to the list of 117 | * exports 118 | */ 119 | export const shouldExportDirectory = (posixPath: PosixPath, { excludeDirList }: GenerationConfig) => 120 | (excludeDirList ?? []).every((glob) => !matchesGlob(posixPath, glob)); 121 | 122 | /** 123 | * Gets the list of files and a set of directories from the given path 124 | * without the path in the name. 125 | * 126 | * @param barrel The name of the barrel file 127 | * @param path The name of the path to check for 128 | */ 129 | export const getFilesAndDirsFromPath = ( 130 | barrel: string, 131 | path: string, 132 | config: GenerationConfig 133 | ): [string[], Set] => { 134 | const files: string[] = []; 135 | const dirs = new Set(); 136 | 137 | for (const curr of fs.readdirSync(path, { withFileTypes: true })) { 138 | const fullPath = `${path}/${curr.name}`; 139 | if (curr.isFile()) { 140 | if (shouldExport(curr.name, fullPath, barrel, config)) { 141 | files.push(curr.name); 142 | } 143 | } else if (curr.isDirectory()) { 144 | if (shouldExportDirectory(fullPath, config)) { 145 | dirs.add(curr.name); 146 | } 147 | } 148 | } 149 | 150 | return [files, dirs]; 151 | }; 152 | 153 | /** 154 | * Gets the list of all the files from the current path and its 155 | * nested folders (including all subfolders) 156 | * 157 | * @param barrel The name of the barrel file 158 | * @param path The name of the path to check for 159 | */ 160 | export const getAllFilesFromSubfolders = ( 161 | barrel: string, 162 | path: string, 163 | opts: GenerationConfig 164 | ) => { 165 | const resultFiles: string[] = []; 166 | const [files, dirs] = getFilesAndDirsFromPath(barrel, path, opts); 167 | 168 | resultFiles.push(...files); 169 | 170 | if (dirs.size > 0) { 171 | for (const d of dirs) { 172 | const folderFiles = getAllFilesFromSubfolders(barrel, `${path}/${d}`, opts); 173 | resultFiles.push(...folderFiles.map((f) => `${d}/${f}`)); 174 | } 175 | } 176 | 177 | return resultFiles; 178 | }; 179 | 180 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export type * from './types.js'; 2 | 3 | export * from './constants.js'; 4 | export * from './context.js'; 5 | export * from './functions.js'; 6 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | export type FocusedGenerations = 'REGULAR_FOCUSED'; 2 | export type GenerationType = FocusedGenerations | RegularGenerations; 3 | export type RegularGenerations = 4 | | 'RECURSIVE' 5 | | 'REGULAR_SUBFOLDERS' 6 | | 'REGULAR'; 7 | 8 | export type Maybe = T | undefined; 9 | 10 | export type GenerationConfig = { 11 | defaultBarrelName: string | undefined; 12 | excludeDirList: string[]; 13 | excludeFileList: string[]; 14 | excludeFreezed: boolean; 15 | excludeGenerated: boolean; 16 | skipEmpty: boolean; 17 | appendFolderName: boolean; 18 | prependFolderName: boolean; 19 | prependPackageToLibExport: boolean; 20 | promptName: boolean; 21 | }; 22 | export type GenerationConfigKeys = keyof GenerationConfig; 23 | export type GenerationLogger = { 24 | warn: LogFn; 25 | done: LogFn; 26 | error: LogFn; 27 | log: LogFn; 28 | }; 29 | type LogFn = (...args: string[]) => void; 30 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "types": ["vitest/globals"], 6 | "outDir": "./dist" 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | exclude: ['src/index.ts'], 7 | include: ['src/*.ts'], 8 | provider: 'v8' 9 | }, 10 | globals: true 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /packages/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .turbo 2 | .vscode/** 3 | .vscode-test/** 4 | out/test/** 5 | src/** 6 | node_modules/ 7 | .gitignore 8 | vsc-extension-quickstart.md 9 | webpack.config.js 10 | **/tsconfig.json 11 | **/tslint.json 12 | **/*.map 13 | **/*.ts 14 | -------------------------------------------------------------------------------- /packages/vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @dbfg/vscode 2 | 3 | ## 7.4.1 4 | 5 | ### Patch Changes 6 | 7 | - Dependency update ([#138](https://github.com/mikededo/dart-barrel-file-generator/pull/138)) 8 | 9 | ## 7.4.0 10 | 11 | ### Minor Changes 12 | 13 | - Migrate to `neverthrow` ([#136](https://github.com/mikededo/dart-barrel-file-generator/pull/136)) 14 | 15 | ### Patch Changes 16 | 17 | - Updated dependencies [[`8ed3f9d`](https://github.com/mikededo/dart-barrel-file-generator/commit/8ed3f9d0bec24252510c774c6cec907a1165e63f)]: 18 | - @dbfg/core@3.0.0 19 | 20 | ## 7.3.4 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [[`fd071bc`](https://github.com/mikededo/dart-barrel-file-generator/commit/fd071bc41711f9b82ec3ab14dc2140dbac2cb418)]: 25 | - @dbfg/core@2.3.0 26 | 27 | ## 7.3.3 28 | 29 | ### Patch Changes 30 | 31 | - Correct package script ([`1509748`](https://github.com/mikededo/dart-barrel-file-generator/commit/150974869b2fb50e11ff9cbf15472228905fb314)) 32 | 33 | ## 7.3.2 34 | 35 | ### Patch Changes 36 | 37 | - Change package scope ([`4ea5d3d`](https://github.com/mikededo/dart-barrel-file-generator/commit/4ea5d3db75e62de4a4ef4dd478d0d4bc94e859f8)) 38 | 39 | - Updated dependencies [[`4ea5d3d`](https://github.com/mikededo/dart-barrel-file-generator/commit/4ea5d3db75e62de4a4ef4dd478d0d4bc94e859f8)]: 40 | - @dbfg/core@2.2.1 41 | 42 | ## 7.3.1 43 | 44 | ### Patch Changes 45 | 46 | - Update scripts ([`af33028`](https://github.com/mikededo/dart-barrel-file-generator/commit/af33028c547c470d83ba0c00da608321136b3f83)) 47 | 48 | ## 7.3.0 49 | 50 | ### Minor Changes 51 | 52 | - Reverts build configuration changes ([`90bea99`](https://github.com/mikededo/dart-barrel-file-generator/commit/90bea99967c917ce2efde149fc1b2d764f657e09)) 53 | 54 | ## 7.2.4 55 | 56 | ### Patch Changes 57 | 58 | - Adds tests and updates build step ([#123](https://github.com/mikededo/dart-barrel-file-generator/pull/123)) 59 | 60 | - Updated dependencies [[`cf0073e`](https://github.com/mikededo/dart-barrel-file-generator/commit/cf0073e579e51a9dd31f50369dcf9ad616bc1c6f)]: 61 | - @dbfg/core@2.2.0 62 | 63 | ## 7.2.3 64 | 65 | ### Patch Changes 66 | 67 | - Validate `workspace` folders in `vscode` package as it's specific feature ([#121](https://github.com/mikededo/dart-barrel-file-generator/pull/121)) 68 | 69 | - Updated dependencies [[`9e574db`](https://github.com/mikededo/dart-barrel-file-generator/commit/9e574db3569dea7c8723bfce417045908fcab11e)]: 70 | - @dbfg/core@2.1.0 71 | 72 | ## 7.2.2 73 | 74 | ### Patch Changes 75 | 76 | - Updated dependencies [[`0ec29d2`](https://github.com/mikededo/dart-barrel-file-generator/commit/0ec29d2408e11e4c94860cdd3d971ade7b3bc4ea)]: 77 | - @dbfg/core@2.0.2 78 | 79 | ## 7.2.1 80 | 81 | ### Patch Changes 82 | 83 | - Updated dependencies [[`279a8a2`](https://github.com/mikededo/dart-barrel-file-generator/commit/279a8a2794fd83ae1d2d350aac4e060098c010df)]: 84 | - @dbfg/core@2.0.1 85 | 86 | ## 7.2.0 87 | 88 | ### Minor Changes 89 | 90 | - Remove generation logic from the package, and adapt to new `core` interface. ([`8444cfd`](https://github.com/mikededo/dart-barrel-file-generator/commit/8444cfd9b99a7f28837f7426e9fea84bd30c448b)) 91 | 92 | ### Patch Changes 93 | 94 | - Updated dependencies [[`db2d0be`](https://github.com/mikededo/dart-barrel-file-generator/commit/db2d0be7f3efdd0701ba940736e50e415407b987)]: 95 | - @dbfg/core@2.0.0 96 | 97 | ## 7.1.0 98 | 99 | ### Minor Changes 100 | 101 | - Convert extension into ESM ([`ba55600`](https://github.com/mikededo/dart-barrel-file-generator/commit/ba55600e928c6869cd7c60a304f3dc7f19df3dc0)) 102 | 103 | ### Patch Changes 104 | 105 | - Update incorrect image URLs ([`b533e50`](https://github.com/mikededo/dart-barrel-file-generator/commit/b533e5062869d171fab252bed4a6e59289166e18)) 106 | 107 | ## 7.0.2 108 | 109 | ### Patch Changes 110 | 111 | - Replace `dartBarrelFileGenerator` for `dart-barrel-file-generator` ([`0a4b3d6`](https://github.com/mikededo/dart-barrel-file-generator/commit/0a4b3d6e1188aa528d33aa33f578416ccb684b11)) 112 | 113 | - Updated dependencies [[`0a4b3d6`](https://github.com/mikededo/dart-barrel-file-generator/commit/0a4b3d6e1188aa528d33aa33f578416ccb684b11)]: 114 | - @dbfg/core@1.0.1 115 | 116 | ## 7.0.1 117 | 118 | ### Patch Changes 119 | 120 | - ebf01e1: Correct build entry point 121 | 122 | ## 7.0.0 123 | 124 | ### Major Changes 125 | 126 | - a864a2a: Refactored into a monorepo setup. While this adds a breaking change, it does not 127 | affect the extension itself. It's a major release as it involves a major update 128 | into the codebase. 129 | Plus, the repository has also been cleaned up from dependencies and other 130 | issues. 131 | 132 | ### Patch Changes 133 | 134 | - Updated dependencies [a864a2a] 135 | - @dbfg/core@1.0.0 136 | -------------------------------------------------------------------------------- /packages/vscode/README.md: -------------------------------------------------------------------------------- 1 | # Dart Barrel File Generator 2 | 3 | VSCode extension that generate barrel files for folders containing dart files. 4 | 5 | ## Installation 6 | 7 | Dart Barrel File Generator either by 8 | [searching for the extension in VSCode](https://code.visualstudio.com/docs/editor/extension-gallery#_search-for-an-extension) 9 | or from the [marketplace](https://marketplace.visualstudio.com/). 10 | 11 | ## Overview 12 | 13 | It can create barrel files only two the selected folder 14 | 15 | ![this-folder](https://raw.githubusercontent.com/mikededo/dart-barrel-file-generator/main/packages/vscode/assets/current-only.gif) 16 | 17 | It creates a barrel file for the selected folder and all the nested folders from 18 | the selected. Likewise, it also adds the nested folder barrel file to its parent 19 | barrel file. 20 | 21 | ![folders-recursive](https://raw.githubusercontent.com/mikededo/dart-barrel-file-generator/main/packages/vscode/assets/current-and-nested.gif) 22 | 23 | Alternatively, the extension can create a barrel file with all the names of the 24 | nested folders (for each subfolder), without creating additional barrel files. 25 | 26 | ![folders-files-recursive](https://raw.githubusercontent.com/mikededo/dart-barrel-file-generator/main/packages/vscode/assets/current-with-subfolders.gif) 27 | 28 | ## Commands 29 | 30 | | Command | Description | 31 | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | 32 | | `GDBF: This folder` | Creates a barrel file for the selected folder | 33 | | `GDBF: Folders (recursive)` | Creates a barrel file for the selected and its nested folders | 34 | | `GDBF: Folders' files (recursive)` | Creates a barrel file for the selected exporting all files with the entire path | 35 | | `GDBF: Focused (parent)` | Through the command palette and when focusing the editor, generates a barrel file to the focused file parent | 36 | 37 | Both commands can be used by typing in the command palette. It will then ask you to 38 | choose a folder. If it is done from the folder tree, it will use the selected 39 | folder as the root folder. 40 | 41 | ## Options 42 | 43 | ### Excluding files 44 | 45 | You can also exclude `.freezed.dart` and `.g.dart` (generated) files by modifying the 46 | following options in your settings: 47 | 48 | - `dart-barrel-file-generator.excludeFreezed: false` (by default). 49 | - `dart-barrel-file-generator.excludeGenerated: false` (by default). 50 | 51 | It is also possible to exclude glob patterns: 52 | 53 | - For files, you can add a list of file globs in the `dartBarrelFile.excludeFileList` 54 | option. 55 | - For directories, you can add a list of directories globs in the 56 | `dartBarrelFile.excludeDirList` option. 57 | 58 | ### Default barrel file name 59 | 60 | The extension will create a barrel file with the `.dart` by default. This 61 | behaviour can be changed if the `dart-barrel-file-generator.defaultBarrelName` option is 62 | set. By changing this option, whenever a barrel file is created, it will use the name 63 | set in the configuration instead of the default. 64 | 65 | > **Note**: If the name contains any white-space, such will be replaced by `_`. 66 | 67 | ### Custom file name 68 | 69 | By default, the extension will create a new file named as the folder name, appended by 70 | the `.dart` extension. However, if you want to set the name, you can activate the 71 | following option: 72 | 73 | - `dart-barrel-file-generator.promptName: false` (by default). 74 | 75 | Whenever you create a new barrel file, a prompt will appear to ask for the file name. 76 | It can be used for both options. 77 | 78 | > **Note**: When entering the name, the `.dart` extension is not required. 79 | 80 | ### Other options 81 | 82 | - Skipping empty folders: by default, `dart-barrel-file-generator` will not 83 | generate a barrel file for a folder that does not have any file to export. You 84 | can change this behaviour by setting `dart-barrel-file-generator.skipEmpty` to 85 | `false`. 86 | - Exporting as `package:/` if the extension is executed in the `./lib` 87 | folder. Enable it by setting 88 | `dart-barrel-file-generator.prependPackageLibToExport` to `true`. Disabled by 89 | default. 90 | 91 | ## Attributions 92 | 93 | Extension icon made by [Freepik](https://www.flaticon.com/authors/freepik) from [flaticon](www.flaticon.com). 94 | -------------------------------------------------------------------------------- /packages/vscode/assets/current-and-nested.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikededo/dart-barrel-file-generator/962b5b7fcfffb311eaa71c320700fcd9c44a1aee/packages/vscode/assets/current-and-nested.gif -------------------------------------------------------------------------------- /packages/vscode/assets/current-only.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikededo/dart-barrel-file-generator/962b5b7fcfffb311eaa71c320700fcd9c44a1aee/packages/vscode/assets/current-only.gif -------------------------------------------------------------------------------- /packages/vscode/assets/current-with-subfolders.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikededo/dart-barrel-file-generator/962b5b7fcfffb311eaa71c320700fcd9c44a1aee/packages/vscode/assets/current-with-subfolders.gif -------------------------------------------------------------------------------- /packages/vscode/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikededo/dart-barrel-file-generator/962b5b7fcfffb311eaa71c320700fcd9c44a1aee/packages/vscode/assets/logo.png -------------------------------------------------------------------------------- /packages/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "publisher": "miquelddg", 3 | "name": "@dbfg/vscode", 4 | "displayName": "Dart Barrel File Generator", 5 | "version": "7.4.1", 6 | "private": true, 7 | "description": "Visual studio code to generate barrel files for the Dart language.", 8 | "author": { 9 | "name": "Miquel de Domingo i Giralt", 10 | "email": "miquelddg@gmail.com", 11 | "url": "https://www.github.com/mikededo" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mikededo/dart-barrel-file-generator" 16 | }, 17 | "bugs": { 18 | "email": "miquelddg@gmail.com", 19 | "url": "https://www.github.com/mikededo/dart-barrel-file-generator/issues" 20 | }, 21 | "keywords": [ 22 | "dart", 23 | "barrel file", 24 | "flutter" 25 | ], 26 | "categories": [ 27 | "Programming Languages", 28 | "Other" 29 | ], 30 | "main": "./dist/extension.js", 31 | "icon": "assets/logo.png", 32 | "engines": { 33 | "node": ">=22", 34 | "vscode": "^1.99.0" 35 | }, 36 | "activationEvents": [ 37 | "onLanguage:dart" 38 | ], 39 | "contributes": { 40 | "commands": [ 41 | { 42 | "command": "dart-barrel-file-generator.generateCurrent", 43 | "title": "GDBF: This folder" 44 | }, 45 | { 46 | "command": "dart-barrel-file-generator.generateCurrentAndNested", 47 | "title": "GDBF: Folders (recursive)" 48 | }, 49 | { 50 | "command": "dart-barrel-file-generator.generateCurrentWithSubfolders", 51 | "title": "GDBF: Folders' files (recursive)" 52 | }, 53 | { 54 | "command": "dart-barrel-file-generator.generateFocusedParent", 55 | "title": "GDBF: Focused (parent)", 56 | "when": "editorTextFocus" 57 | } 58 | ], 59 | "configuration": [ 60 | { 61 | "title": "Dart Barrel File Generator", 62 | "properties": { 63 | "dart-barrel-file-generator.defaultBarrelName": { 64 | "title": "Default barrel file name", 65 | "type": [ 66 | "string", 67 | "null" 68 | ], 69 | "default": null, 70 | "markdownDescription": "The default name for the barrel file is `.dart`. You can override this name by setting a custom name. If there's any whitespace in the name it will be replaced by a '-' and the name **will be transformed to lowercase**." 71 | }, 72 | "dart-barrel-file-generator.promptName": { 73 | "title": "Open input on file generation", 74 | "type": "boolean", 75 | "default": false, 76 | "description": "Get promted asking for the name of the barrel file that will be created" 77 | }, 78 | "dart-barrel-file-generator.excludeFreezed": { 79 | "title": "Exclude freezed (.freezed.dart) files", 80 | "type": "boolean", 81 | "default": true, 82 | "markdownDescription": "Exclude `.freezed.dart` files" 83 | }, 84 | "dart-barrel-file-generator.excludeGenerated": { 85 | "title": "Exclude generated (.g.dart) files", 86 | "type": "boolean", 87 | "default": true, 88 | "markdownDescription": "Exclude `.g.dart` files" 89 | }, 90 | "dart-barrel-file-generator.excludeFileList": { 91 | "title": "Additional patterns to exclude files", 92 | "type": "array", 93 | "default": [], 94 | "description": "Add the file patterns that you want to exclude (as glob patterns). Excluded files will not be added to the barrel file." 95 | }, 96 | "dart-barrel-file-generator.excludeDirList": { 97 | "title": "Additional patterns to exclude directories", 98 | "type": "array", 99 | "default": [], 100 | "description": "Add the directory patterns that you want to exclude (as glob patterns). A barrel file will not be generated for these directories." 101 | }, 102 | "dart-barrel-file-generator.prependFolderName": { 103 | "title": "Prepend name of the folder to the generated file", 104 | "type": "boolean", 105 | "default": false, 106 | "markdownDescription": "Prepend the name of the folder to the generated files, e.g.: `_.dart`." 107 | }, 108 | "dart-barrel-file-generator.appendFolderName": { 109 | "title": "Append name of the folder to the generated file", 110 | "type": "boolean", 111 | "default": false, 112 | "markdownDescription": "Append the name of the folder to the generated files, e.g.: `_.dart`." 113 | }, 114 | "dart-barrel-file-generator.skipEmpty": { 115 | "title": "Skip empty folders", 116 | "type": "boolean", 117 | "default": true, 118 | "description": "Skip generating barrel files for folders that are empty (or do not contain any valid dart file)" 119 | }, 120 | "dart-barrel-file-generator.prependPackageToLibExport": { 121 | "title": "Append package name to lib exports", 122 | "type": "boolean", 123 | "default": false, 124 | "markdownDescription": "It will prepend `package:/...` as a prefix of the export exclusively for the root file under `lib/`. It detects the name of the package by analysing the path of the project." 125 | } 126 | } 127 | } 128 | ], 129 | "menus": { 130 | "explorer/context": [ 131 | { 132 | "command": "dart-barrel-file-generator.generateCurrent", 133 | "group": "7_modification", 134 | "when": "explorerResourceIsFolder" 135 | }, 136 | { 137 | "command": "dart-barrel-file-generator.generateCurrentAndNested", 138 | "group": "7_modification", 139 | "when": "explorerResourceIsFolder" 140 | }, 141 | { 142 | "command": "dart-barrel-file-generator.generateCurrentWithSubfolders", 143 | "group": "7_modification", 144 | "when": "explorerResourceIsFolder" 145 | } 146 | ] 147 | } 148 | }, 149 | "scripts": { 150 | "build": "vite build", 151 | "dev": "vite build --watch", 152 | "package": "bun ./scripts/package.ts", 153 | "publish": "bun vsce publish --packagePath ./extension.vsix --skip-license --skip-duplicate" 154 | }, 155 | "dependencies": { 156 | "@dbfg/core": "workspace:*" 157 | }, 158 | "devDependencies": { 159 | "@types/vscode": "1.99.1", 160 | "@vscode/vsce": "3.3.2" 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /packages/vscode/scripts/package.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | 3 | import { execSync } from 'node:child_process'; 4 | import fs from 'node:fs'; 5 | import path from 'node:path'; 6 | import pc from 'picocolors'; 7 | 8 | // eslint-disable-next-line node/prefer-global/process 9 | const args = process.argv.slice(2); 10 | const isCi = args.includes('--ci'); 11 | 12 | const logPrefix = pc.bgBlueBright(pc.black(' INFO ')); 13 | const okPrefix = pc.bgGreen(pc.black(' OK ')); 14 | 15 | // eslint-disable-next-line node/prefer-global/process 16 | const packageJsonPath = path.join(process.cwd(), 'package.json'); 17 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); 18 | 19 | console.log( 20 | `${logPrefix} Found name: ${pc.green(packageJson.name)}, temporarily renaming to: ${pc.green('dart-barrel-file-generator')}\n` 21 | ); 22 | 23 | const tempName = packageJson.name; 24 | packageJson.name = 'dart-barrel-file-generator'; 25 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 26 | 27 | console.log(`${logPrefix} Packaging extension...\n`); 28 | execSync( 29 | `bun vsce package --no-dependencies --skip-license${isCi ? ' --out ./extension.vsix' : ''}`, 30 | { stdio: 'inherit' } 31 | ); 32 | 33 | console.log(`\n${logPrefix} Restoring ${pc.green('package.json')} name to ${pc.green(tempName)}...`); 34 | packageJson.name = tempName; 35 | fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); 36 | 37 | console.log(`${okPrefix} Done!`); 38 | -------------------------------------------------------------------------------- /packages/vscode/src/extension.mts: -------------------------------------------------------------------------------- 1 | import type { GenerationConfig, GenerationConfigKeys, GenerationType } from '@dbfg/core'; 2 | 3 | import type { Uri } from 'vscode'; 4 | import { window, workspace } from 'vscode'; 5 | 6 | import { createContext, toPosixPath } from '@dbfg/core'; 7 | 8 | const EXTENSION_KEY = 'dart-barrel-file-generator'; 9 | 10 | const getConfig = (config: T): GenerationConfig[T] | undefined => 11 | workspace.getConfiguration().get([EXTENSION_KEY, config].join('.')); 12 | 13 | const logger = window.createOutputChannel('DartBarrelFile'); 14 | const Context = createContext({ 15 | config: { 16 | appendFolderName: !!getConfig('appendFolderName'), 17 | defaultBarrelName: getConfig('defaultBarrelName'), 18 | excludeDirList: getConfig('excludeDirList') ?? [], 19 | excludeFileList: getConfig('excludeFileList') ?? [], 20 | excludeFreezed: !!getConfig('excludeFreezed'), 21 | excludeGenerated: !!getConfig('excludeGenerated'), 22 | prependFolderName: !!getConfig('prependFolderName'), 23 | prependPackageToLibExport: !!getConfig('prependPackageToLibExport'), 24 | promptName: !!getConfig('promptName'), 25 | skipEmpty: !!getConfig('skipEmpty') 26 | }, 27 | logger: { 28 | done: logger.appendLine, 29 | error: logger.appendLine, 30 | log: logger.appendLine, 31 | warn: logger.appendLine 32 | } 33 | }); 34 | 35 | /** 36 | * Entry point of the extension. When this function is called 37 | * the context should have already been set up 38 | */ 39 | export const init = async (uri: Uri, type: GenerationType) => { 40 | if (!type) { 41 | Context.onError( 42 | 'Extension did not launch properly. Create an issue if this error persists' 43 | ); 44 | Context.endGeneration(); 45 | 46 | window.showErrorMessage('GBDF: Error on initialising the extension'); 47 | } 48 | 49 | if (!workspace.workspaceFolders) { 50 | throw new Error('The workspace has no folders'); 51 | } 52 | 53 | const workspaceDir = toPosixPath(workspace.workspaceFolders[0].uri.fsPath); 54 | if (!toPosixPath(uri.fsPath).includes(workspaceDir)) { 55 | throw new Error('Select a folder from the workspace'); 56 | } 57 | 58 | const result = await Context.start({ 59 | fsPath: uri.fsPath, 60 | path: uri.path, 61 | type 62 | }); 63 | 64 | if (result.isErr()) { 65 | Context.onError(result.error.message); 66 | Context.endGeneration(); 67 | 68 | window.showErrorMessage('GDBF: Error on generating the file', result.error.message); 69 | return; 70 | } 71 | 72 | if (result.value) { 73 | await window.showInformationMessage('GDBF: Generated files!', result.value); 74 | } else { 75 | await window.showInformationMessage('GDBF: No dart barrel file has been generated!'); 76 | } 77 | 78 | Context.endGeneration(); 79 | }; 80 | 81 | export const deactivate = () => { 82 | logger.dispose(); 83 | }; 84 | -------------------------------------------------------------------------------- /packages/vscode/src/index.mts: -------------------------------------------------------------------------------- 1 | import type { FocusedGenerations, GenerationType } from '@dbfg/core'; 2 | 3 | import type { ExtensionContext } from 'vscode'; 4 | import { commands, Uri, window } from 'vscode'; 5 | 6 | import { init } from './extension.mjs'; 7 | 8 | export { deactivate } from './extension.mjs'; 9 | 10 | /** 11 | * Curried function that, from the given type of the generation, 12 | * it will set up the context with the `uri` received from the curried fn 13 | * and the given type 14 | */ 15 | const generate = (type: GenerationType) => async (uri: undefined | Uri) => { 16 | const maybeUri = uri ?? await window.showOpenDialog({ 17 | canSelectFiles: false, 18 | canSelectFolders: true, 19 | canSelectMany: false, 20 | openLabel: 'Select the folder ' 21 | }).then((uris) => uris && uris[0]); 22 | 23 | if (!maybeUri) { 24 | return; 25 | } 26 | 27 | await init(maybeUri, type); 28 | }; 29 | 30 | const generateFocused = (type: FocusedGenerations) => async () => { 31 | const activeEditor = window.activeTextEditor; 32 | if (!activeEditor) { 33 | return; 34 | } 35 | 36 | const parent = Uri.joinPath(activeEditor.document.uri, '..'); 37 | await generate(type)(parent); 38 | }; 39 | 40 | export const activate = (context: ExtensionContext) => { 41 | // Generate current 42 | context.subscriptions.push( 43 | commands.registerCommand( 44 | 'dart-barrel-file-generator.generateCurrent', 45 | generate('REGULAR') 46 | ) 47 | ); 48 | // For current folder 49 | context.subscriptions.push( 50 | commands.registerCommand( 51 | 'dart-barrel-file-generator.generateFocusedParent', 52 | generateFocused('REGULAR_FOCUSED') 53 | ) 54 | ); 55 | 56 | // Generate current with subfolders 57 | context.subscriptions.push( 58 | commands.registerCommand( 59 | 'dart-barrel-file-generator.generateCurrentWithSubfolders', 60 | generate('REGULAR_SUBFOLDERS') 61 | ) 62 | ); 63 | 64 | // Generate current and nested 65 | context.subscriptions.push( 66 | commands.registerCommand( 67 | 'dart-barrel-file-generator.generateCurrentAndNested', 68 | generate('RECURSIVE') 69 | ) 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /packages/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/vscode/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve(__dirname, 'src/index.mts'), 8 | fileName: 'extension', 9 | formats: ['cjs'] 10 | }, 11 | minify: false, 12 | outDir: resolve(__dirname, 'dist'), 13 | rollupOptions: { 14 | external: ['vscode', 'node:fs', 'node:path', 'node:fs/promises'] 15 | }, 16 | sourcemap: true 17 | }, 18 | resolve: { 19 | alias: { 20 | '@dbfg/core': resolve(__dirname, '../core/src') 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/bash 2 | # Build all packages 3 | bun run build 4 | bun run --cwd packages/vscode package --ci 5 | 6 | bun changeset publish 7 | bun run --cwd packages/vscode publish 8 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "composite": true, 5 | "target": "ES2022", 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext", 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "esModuleInterop": true, 11 | "skipDefaultLibCheck": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | --------------------------------------------------------------------------------