├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── crawl.yml │ └── js_from_routes.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .rspec ├── .ruby-version ├── .standard.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── lint ├── rake ├── rspec └── standardrb ├── docs ├── .algolia │ └── config.json ├── .stylelintrc.js ├── .vitepress │ ├── config.ts │ └── theme │ │ ├── components │ │ ├── Home.vue │ │ └── Quote.vue │ │ ├── index.js │ │ └── styles │ │ └── styles.css ├── client │ └── index.md ├── config │ └── index.md ├── faqs │ └── index.md ├── guide │ ├── codegen.md │ ├── index.md │ └── introduction.md ├── index.md ├── package.json ├── pnpm-workspace.yaml ├── public │ ├── _headers │ ├── banner.png │ ├── favicon.svg │ ├── logo-with-text.png │ ├── logo-with-text.svg │ ├── logo.png │ └── logo.svg └── vite.config.ts ├── gemfiles ├── Gemfile-rails-edge ├── Gemfile-rails.7.2.x └── Gemfile-rails.8.0.x ├── js_from_routes ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin │ ├── console │ └── release ├── js_from_routes.gemspec └── lib │ ├── js_from_routes.rb │ └── js_from_routes │ ├── generator.rb │ ├── railtie.rb │ ├── template.js.erb │ ├── template_all.js.erb │ ├── template_index.js.erb │ └── version.rb ├── netlify.toml ├── package.json ├── packages ├── axios │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── test │ │ └── index.test.ts ├── client │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── api.ts │ │ ├── config.ts │ │ ├── index.ts │ │ └── types.ts │ └── test │ │ └── api.test.ts ├── core │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── urls.ts │ │ └── utils.ts │ └── test │ │ ├── urls.test.ts │ │ └── utils.test.ts ├── inertia │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── types.ts │ └── test │ │ └── index.test.ts ├── package.json └── redaxios │ ├── CHANGELOG.md │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ └── test │ └── index.test.ts ├── playground └── vanilla │ ├── .browserslistrc │ ├── .gitignore │ ├── .rspec │ ├── Gemfile │ ├── Gemfile.lock │ ├── Procfile.dev │ ├── Rakefile │ ├── app │ ├── assets │ │ ├── config │ │ │ └── manifest.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── application_controller.rb │ │ ├── comments_controller.rb │ │ ├── settings │ │ │ └── user_preferences_controller.rb │ │ ├── video_clips_controller.rb │ │ └── welcome_controller.rb │ ├── javascript │ │ ├── ApiHelpers.ts │ │ ├── Videos.vue │ │ ├── api │ │ │ ├── CommentsApi.ts │ │ │ ├── Settings │ │ │ │ └── UserPreferencesApi.ts │ │ │ ├── VideoClipsApi.ts │ │ │ ├── all.ts │ │ │ └── index.ts │ │ ├── composables │ │ │ └── api.ts │ │ └── entrypoints │ │ │ └── application.ts │ └── views │ │ ├── layouts │ │ └── application.html.erb │ │ ├── video_clips │ │ └── new.html.erb │ │ └── welcome │ │ └── home.html.erb │ ├── babel.config.js │ ├── bin │ ├── bundle │ ├── rails │ ├── rake │ ├── setup │ ├── spring │ └── vite │ ├── config.ru │ ├── config │ ├── application.rb │ ├── boot.rb │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ └── test.rb │ ├── initializers │ │ ├── application_controller_renderer.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── js_from_routes.rb │ │ ├── mime_types.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ └── vite.json │ ├── index.d.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── pnpm-workspace.yaml │ ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ └── robots.txt │ ├── spec │ ├── controllers │ │ └── video_clips_spec.rb │ ├── rails_helper.rb │ └── spec_helper.rb │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── changelog.js ├── release.js └── verifyCommit.js ├── spec ├── js_from_routes │ └── js_from_routes_spec.rb ├── spec_helper.rb └── support │ └── jquery_template.js.erb ├── tsconfig.json ├── vetur.config.js └── vitest.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage/ 3 | build/ 4 | examples/ 5 | public/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | jest: true, 5 | }, 6 | extends: ['@mussi/eslint-config'], 7 | plugins: ['jest'], 8 | rules: { 9 | '@typescript-eslint/no-var-requires': 'off', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug: pending triage' 6 | assignees: '' 7 | --- 8 | 9 | [troubleshooting section]: https://js-from-routes.netlify.app/faqs/ 10 | 11 | - [ ] I have tried upgrading by running `bundle update js_from_routes`. 12 | - [ ] I have read the __[troubleshooting section]__ before opening an issue. 13 | 14 | ### Description 📖 15 | 16 | _Provide a clear and concise description of what the bug is._ 17 | 18 | ### Reproduction/Logs 🐞📜 19 | 20 | _Please provide a link to a repo that can reproduce the problem you ran into, or logs about the problem._ 21 | 22 | ### Screenshots 📷 23 | 24 | _Provide console or browser screenshots of the problem_. 25 | 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Troubleshooting & FAQs 4 | url: https://js-from-routes.netlify.app/faqs/ 5 | about: 'Please check the most common problems before opening an issue' 6 | - name: Questions & Discussions 7 | url: https://github.com/ElMassimo/js_from_routes/discussions 8 | about: Use GitHub discussions for message-board style questions and discussions. 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 📖 2 | 3 | This pull request 4 | 5 | ### Background 📜 6 | 7 | This was happening because 8 | 9 | ### The Fix 🔨 10 | 11 | By changing 12 | 13 | ### Screenshots 📷 14 | -------------------------------------------------------------------------------- /.github/workflows/crawl.yml: -------------------------------------------------------------------------------- 1 | name: crawl 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | if: ${{ startsWith(github.event.head_commit.message, 'docs') }} 11 | name: crawl 12 | env: 13 | API_KEY: ${{secrets.ALGOLIA_API_KEY}} 14 | APPLICATION_ID: GERZE019PN 15 | ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true 16 | runs-on: ubuntu-latest 17 | container: 18 | image: algolia/docsearch-scraper 19 | volumes: 20 | - /node20217:/node20217:rw,rshared 21 | - /node20217:/__e/node20:ro,rshared 22 | steps: 23 | - name: Wait for Netlify deployment 24 | uses: whatnick/wait-action@master 25 | with: 26 | time: '50s' 27 | - name: install nodejs20glibc2.17 28 | run: | 29 | curl -LO https://unofficial-builds.nodejs.org/download/release/v20.9.0/node-v20.9.0-linux-x64-glibc-217.tar.xz 30 | tar -xf node-v20.9.0-linux-x64-glibc-217.tar.xz --strip-components 1 -C /node20217 31 | ldd /__e/node20/bin/node 32 | - uses: actions/checkout@v4 33 | - run: 'sudo apt-get install -y jq' 34 | - run: 'echo "CONFIG=$(cat docs/.algolia/config.json | jq -r tostring)" >> $GITHUB_ENV' 35 | - run: "cd /root && pipenv install" 36 | - run: "cd /root && pipenv run python -m src.index" 37 | -------------------------------------------------------------------------------- /.github/workflows/js_from_routes.yml: -------------------------------------------------------------------------------- 1 | name: JS From Routes 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | rspec: 13 | runs-on: ${{ matrix.os }} 14 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' || matrix.experimental }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest] 19 | ruby: [ 20 | "3.2", 21 | "3.3", 22 | ] 23 | gemfile: [ 24 | "Gemfile-rails.7.2.x", 25 | "Gemfile-rails.8.0.x", 26 | ] 27 | experimental: [false] 28 | include: 29 | - ruby: "3.4.0-preview2" 30 | os: ubuntu-latest 31 | gemfile: Gemfile-rails-edge 32 | experimental: true 33 | 34 | env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps 35 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }} 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - uses: actions/setup-node@v4 41 | with: 42 | node-version: '18' 43 | 44 | - uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: ${{ matrix.ruby }} 47 | bundler-cache: true 48 | 49 | - name: Setup Code Climate test-reporter 50 | if: ${{ contains(github.ref, 'main') }} 51 | run: | 52 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 53 | chmod +x ./cc-test-reporter 54 | ./cc-test-reporter before-build 55 | 56 | - name: Ruby specs 57 | run: bundle exec rspec 58 | 59 | - name: Upload code coverage to Code Climate 60 | if: ${{ contains(github.ref, 'main') }} 61 | run: | 62 | export GIT_BRANCH="${GITHUB_REF/refs\/heads\//}" 63 | ./cc-test-reporter after-build -r ${{secrets.CC_TEST_REPORTER_ID}} 64 | 65 | rubocop: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v4 69 | 70 | - uses: ruby/setup-ruby@v1 71 | with: 72 | ruby-version: "3.3.5" 73 | bundler-cache: true 74 | 75 | - name: Ruby linter 76 | run: bundle exec standardrb 77 | 78 | js: 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v4 82 | 83 | - uses: pnpm/action-setup@v4.0.0 84 | with: 85 | version: 9.8.0 86 | 87 | - name: Use Node.js 88 | uses: actions/setup-node@v4 89 | with: 90 | node-version: 18.x 91 | cache: 'pnpm' 92 | 93 | - run: pnpm install --frozen-lockfile 94 | 95 | - name: Build 96 | run: pnpm build 97 | 98 | - name: Lint 99 | run: pnpm lint 100 | 101 | - name: Test 102 | run: pnpm test 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spec/support/generated 2 | spec/support/sample_app/bin 3 | 4 | /.bundle 5 | pkg 6 | log 7 | tmp 8 | node_modules 9 | .byebug_history 10 | yarn-debug.log* 11 | yarn-error.log* 12 | .yarn-integrity 13 | gemfiles/*.lock 14 | .DS_Store 15 | 16 | # Vite on Rails 17 | /public/vite 18 | /public/vite-dev 19 | /public/vite-test 20 | test/test_app/public/vite-production 21 | node_modules 22 | *.local 23 | .DS_Store 24 | 25 | # Vitepress 26 | dist 27 | examples_dist 28 | node_modules 29 | coverage 30 | .nyc_output 31 | .rpt2_cache 32 | .env 33 | local.log 34 | .DS_Store 35 | e2e/reports 36 | e2e/screenshots 37 | __build__ 38 | playground_dist 39 | yarn-error.log 40 | temp 41 | markdown 42 | explorations 43 | selenium-server.log 44 | 45 | # Algolia 46 | .algolia.env 47 | 48 | # Hanami 49 | .env.local 50 | .env.*.local 51 | 52 | docs/pnpm-lock.yaml 53 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | node scripts/verifyCommit.js "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=progress 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 2 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | fix: true 2 | ignore: 3 | - 'packages/**/*' 4 | - 'scripts/**/*' 5 | - 'playground/**/*' 6 | - '**/*': 7 | - Style/TrailingCommaInArrayLiteral 8 | - Style/TrailingCommaInHashLiteral 9 | - Style/TrailingCommaInArguments 10 | -------------------------------------------------------------------------------- /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 making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | 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 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | 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 team at maximomussini@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further 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], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Setting Up a Development Environment 2 | 3 | 1. Install [pnpm](https://pnpm.js.org/) 4 | 5 | 2. Run the following commands to set up the development environment. 6 | 7 | ``` 8 | bundle install 9 | ``` 10 | 11 | ``` 12 | pnpm install 13 | ``` 14 | 15 | ## Making sure your changes pass all tests 16 | 17 | There are a number of automated checks which run on GitHub Actions when a pull request is created. 18 | You can run those checks on your own locally to make sure that your changes would not break the CI build. 19 | 20 | ### 1. Check the code for JavaScript style violations 21 | 22 | ``` 23 | pnpm lint 24 | ``` 25 | 26 | ### 2. Check the code for Ruby style violations 27 | ``` 28 | bin/standardrb 29 | ``` 30 | 31 | ### 3. Run the test suite 32 | ``` 33 | bin/rspec 34 | ``` 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec path: "./js_from_routes" 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: js_from_routes 3 | specs: 4 | js_from_routes (4.0.2) 5 | railties (>= 5.1, < 9) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actionpack (8.0.1) 11 | actionview (= 8.0.1) 12 | activesupport (= 8.0.1) 13 | nokogiri (>= 1.8.5) 14 | rack (>= 2.2.4) 15 | rack-session (>= 1.0.1) 16 | rack-test (>= 0.6.3) 17 | rails-dom-testing (~> 2.2) 18 | rails-html-sanitizer (~> 1.6) 19 | useragent (~> 0.16) 20 | actionview (8.0.1) 21 | activesupport (= 8.0.1) 22 | builder (~> 3.1) 23 | erubi (~> 1.11) 24 | rails-dom-testing (~> 2.2) 25 | rails-html-sanitizer (~> 1.6) 26 | activesupport (8.0.1) 27 | base64 28 | benchmark (>= 0.3) 29 | bigdecimal 30 | concurrent-ruby (~> 1.0, >= 1.3.1) 31 | connection_pool (>= 2.2.5) 32 | drb 33 | i18n (>= 1.6, < 2) 34 | logger (>= 1.4.2) 35 | minitest (>= 5.1) 36 | securerandom (>= 0.3) 37 | tzinfo (~> 2.0, >= 2.0.5) 38 | uri (>= 0.13.1) 39 | ast (2.4.2) 40 | base64 (0.2.0) 41 | benchmark (0.4.0) 42 | bigdecimal (3.1.9) 43 | builder (3.3.0) 44 | byebug (11.1.3) 45 | coderay (1.1.3) 46 | concurrent-ruby (1.3.4) 47 | connection_pool (2.5.0) 48 | crass (1.0.6) 49 | date (3.4.1) 50 | diff-lcs (1.5.0) 51 | docile (1.4.0) 52 | drb (2.2.1) 53 | erubi (1.13.1) 54 | ffi (1.15.5) 55 | given_core (3.8.2) 56 | sorcerer (>= 0.3.7) 57 | i18n (1.14.6) 58 | concurrent-ruby (~> 1.0) 59 | io-console (0.8.0) 60 | irb (1.14.3) 61 | rdoc (>= 4.0.0) 62 | reline (>= 0.4.2) 63 | json (2.6.1) 64 | language_server-protocol (3.17.0.3) 65 | lint_roller (1.1.0) 66 | listen (3.7.1) 67 | rb-fsevent (~> 0.10, >= 0.10.3) 68 | rb-inotify (~> 0.9, >= 0.9.10) 69 | logger (1.6.5) 70 | loofah (2.24.0) 71 | crass (~> 1.0.2) 72 | nokogiri (>= 1.12.0) 73 | method_source (1.0.0) 74 | mini_portile2 (2.8.8) 75 | minitest (5.25.4) 76 | nokogiri (1.18.1) 77 | mini_portile2 (~> 2.8.2) 78 | racc (~> 1.4) 79 | parallel (1.26.3) 80 | parser (3.3.5.0) 81 | ast (~> 2.4.1) 82 | racc 83 | pry (0.14.2) 84 | coderay (~> 1.1) 85 | method_source (~> 1.0) 86 | pry-byebug (3.10.1) 87 | byebug (~> 11.0) 88 | pry (>= 0.13, < 0.15) 89 | psych (5.2.2) 90 | date 91 | stringio 92 | racc (1.7.1) 93 | rack (3.1.8) 94 | rack-session (2.1.0) 95 | base64 (>= 0.1.0) 96 | rack (>= 3.0.0) 97 | rack-test (2.2.0) 98 | rack (>= 1.3) 99 | rackup (2.2.1) 100 | rack (>= 3) 101 | rails-dom-testing (2.2.0) 102 | activesupport (>= 5.0.0) 103 | minitest 104 | nokogiri (>= 1.6) 105 | rails-html-sanitizer (1.6.2) 106 | loofah (~> 2.21) 107 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 108 | railties (8.0.1) 109 | actionpack (= 8.0.1) 110 | activesupport (= 8.0.1) 111 | irb (~> 1.13) 112 | rackup (>= 1.0.0) 113 | rake (>= 12.2) 114 | thor (~> 1.0, >= 1.2.2) 115 | zeitwerk (~> 2.6) 116 | rainbow (3.1.1) 117 | rake (13.0.6) 118 | rb-fsevent (0.11.1) 119 | rb-inotify (0.10.1) 120 | ffi (~> 1.0) 121 | rdoc (6.10.0) 122 | psych (>= 4.0.0) 123 | regexp_parser (2.9.2) 124 | reline (0.6.0) 125 | io-console (~> 0.5) 126 | rexml (3.3.7) 127 | rspec (3.11.0) 128 | rspec-core (~> 3.11.0) 129 | rspec-expectations (~> 3.11.0) 130 | rspec-mocks (~> 3.11.0) 131 | rspec-core (3.11.0) 132 | rspec-support (~> 3.11.0) 133 | rspec-expectations (3.11.0) 134 | diff-lcs (>= 1.2.0, < 2.0) 135 | rspec-support (~> 3.11.0) 136 | rspec-given (3.8.2) 137 | given_core (= 3.8.2) 138 | rspec (>= 2.14.0) 139 | rspec-mocks (3.11.0) 140 | diff-lcs (>= 1.2.0, < 2.0) 141 | rspec-support (~> 3.11.0) 142 | rspec-support (3.11.0) 143 | rubocop (1.65.1) 144 | json (~> 2.3) 145 | language_server-protocol (>= 3.17.0) 146 | parallel (~> 1.10) 147 | parser (>= 3.3.0.2) 148 | rainbow (>= 2.2.2, < 4.0) 149 | regexp_parser (>= 2.4, < 3.0) 150 | rexml (>= 3.2.5, < 4.0) 151 | rubocop-ast (>= 1.31.1, < 2.0) 152 | ruby-progressbar (~> 1.7) 153 | unicode-display_width (>= 2.4.0, < 3.0) 154 | rubocop-ast (1.32.3) 155 | parser (>= 3.3.1.0) 156 | rubocop-performance (1.21.1) 157 | rubocop (>= 1.48.1, < 2.0) 158 | rubocop-ast (>= 1.31.1, < 2.0) 159 | ruby-progressbar (1.13.0) 160 | securerandom (0.4.1) 161 | simplecov (0.17.1) 162 | docile (~> 1.1) 163 | json (>= 1.8, < 3) 164 | simplecov-html (~> 0.10.0) 165 | simplecov-html (0.10.2) 166 | sorcerer (2.0.1) 167 | standard (1.40.0) 168 | language_server-protocol (~> 3.17.0.2) 169 | lint_roller (~> 1.0) 170 | rubocop (~> 1.65.0) 171 | standard-custom (~> 1.0.0) 172 | standard-performance (~> 1.4) 173 | standard-custom (1.0.2) 174 | lint_roller (~> 1.0) 175 | rubocop (~> 1.50) 176 | standard-performance (1.4.0) 177 | lint_roller (~> 1.1) 178 | rubocop-performance (~> 1.21.0) 179 | stringio (3.1.2) 180 | thor (1.3.2) 181 | tzinfo (2.0.6) 182 | concurrent-ruby (~> 1.0) 183 | unicode-display_width (2.6.0) 184 | uri (1.0.2) 185 | useragent (0.16.11) 186 | zeitwerk (2.7.1) 187 | 188 | PLATFORMS 189 | ruby 190 | 191 | DEPENDENCIES 192 | bundler (~> 2) 193 | js_from_routes! 194 | listen (~> 3.2) 195 | pry-byebug (~> 3.9) 196 | rake (~> 13) 197 | rspec-given (~> 3.8) 198 | simplecov (< 0.18) 199 | standard (~> 1.0) 200 | 201 | BUNDLED WITH 202 | 2.3.22 203 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Máximo Mussini 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 |
7 | 8 |

9 | 10 | Build Status 11 | 12 | 13 | Maintainability 14 | 15 | 16 | Test Coverage 17 | 18 | 19 | Gem Version 20 | 21 | 22 | License 23 | 24 |

25 |

26 | 27 | [routes]: https://github.com/ElMassimo/js_from_routes/blob/main/playground/vanilla/config/routes.rb#L6 28 | [client libraries]: https://js-from-routes.netlify.app/client/ 29 | [codegen]: https://js-from-routes.netlify.app/guide/codegen 30 | [rails bytes]: https://railsbytes.com/templates/X6ksgn 31 | [website]: https://js-from-routes.netlify.app 32 | [guides]: https://js-from-routes.netlify.app/guide/ 33 | [guide]: https://js-from-routes.netlify.app/guide/#usage-🚀 34 | [configuration reference]: https://js-from-routes.netlify.app/config/ 35 | [introduction]: https://js-from-routes.netlify.app/guide/introduction 36 | [ping]: https://github.com/ElMassimo/pingcrm-vite 37 | 38 | _[JS From Routes][website]_ generates path helpers and API methods from your Rails routes, allowing you to be more productive and prevent routing-related errors. 39 | 40 | Since code generation is fully customizable it can be used in very diverse scenarios. 41 | 42 | ## Why? 🤔 43 | 44 | Path helpers in Rails make it easy to build URLs, while avoiding typos and mistakes. 45 | 46 | With _[JS From Routes][website]_, it's possible the enjoy the same benefits in JS, and even more if using TypeScript. 47 | 48 | Read more about it in the [blog announcement](https://maximomussini.com/posts/js-from-routes/). 49 | 50 | ## Features ⚡️ 51 | 52 | - 🚀 Path and Request Helpers 53 | - 🔁 Serialization / Deserialization 54 | - ✅ Prevent Routing Errors 55 | - 🤖 Automatic Generation 56 | - 🛠 Customizable Generation 57 | - And [more][introduction]! 58 | 59 | ## Documentation 📖 60 | 61 | Visit the [documentation website][website] to check out the [guides] and searchable [configuration reference]. 62 | 63 | ## Installation 💿 64 | 65 | For a one liner, you can use [this template][rails bytes]: 66 | 67 | ``` 68 | rails app:template LOCATION='https://railsbytes.com/script/X6ksgn' 69 | ``` 70 | 71 | Else, add this line to your application's Gemfile in the `development` group and execute `bundle`: 72 | 73 | ```ruby 74 | group :development, :test do 75 | gem 'js_from_routes' 76 | end 77 | ``` 78 | 79 | Then, add the [client library][client libraries] to your `package.json`: 80 | 81 | ```bash 82 | npm install @js-from-routes/client # yarn add @js-from-routes/client 83 | ``` 84 | 85 | There are more [client libraries] available. 86 | 87 | ## Getting Started 🚀 88 | 89 | The following is a short excerpt from the [guide]. 90 | 91 | ### Specify the routes you want 92 | 93 | Use the `export` attribute to determine which [routes] should be taken into account when generating JS. 94 | 95 | ```ruby 96 | Rails.application.routes.draw do 97 | resources :video_clips, export: true do 98 | get :download, on: :member 99 | end 100 | 101 | # Or: 102 | defaults export: true do 103 | # All routes defined inside this block will be exported. 104 | end 105 | end 106 | ``` 107 | 108 | ### Use the path helpers in your JS application 109 | 110 | Path helpers will be [automatically generated][codegen] when refreshing the page. 111 | 112 | ```js 113 | import { videoClips } from '~/api' 114 | 115 | const video = await videoClips.show({ id: 'oHg5SJYRHA0' }) 116 | 117 | const downloadPath = videoClips.download.path(video) 118 | ``` 119 | 120 | Check the [documentation website][guide] for more information. 121 | 122 | For a working example, check [this repo][ping]. 123 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rspec/core/rake_task" 2 | 3 | RSpec::Core::RakeTask.new 4 | 5 | task default: :spec 6 | -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | bin/standardrb "$@" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rspec-core", "rspec") 30 | -------------------------------------------------------------------------------- /bin/standardrb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'standardrb' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("standard", "standardrb") 30 | -------------------------------------------------------------------------------- /docs/.algolia/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_name": "js_from_routes", 3 | "start_urls": [ 4 | { 5 | "url": "https://js-from-routes.netlify.app/config/", 6 | "selectors_key": "config", 7 | "tags": ["config"], 8 | "page_rank": 4 9 | }, 10 | { 11 | "url": "https://js-from-routes.netlify.app/client/", 12 | "selectors_key": "client", 13 | "tags": ["client"], 14 | "page_rank": 3 15 | }, 16 | { 17 | "url": "https://js-from-routes.netlify.app/guide/", 18 | "tags": ["guide"], 19 | "page_rank": 2 20 | }, 21 | { 22 | "url": "https://js-from-routes.netlify.app/faqs/", 23 | "selectors_key": "faqs", 24 | "tags": ["faqs"], 25 | "page_rank": 1 26 | } 27 | ], 28 | "selectors": { 29 | "default": { 30 | "lvl0": { 31 | "selector": "p.sidebar-heading.open", 32 | "global": true, 33 | "default_value": "Guide" 34 | }, 35 | "lvl1": ".content h1", 36 | "lvl2": ".content h2", 37 | "lvl3": ".content h3, .content li kbd", 38 | "lvl4": ".content h4", 39 | "lvl5": ".content h5", 40 | "text": ".content p, .content li", 41 | "lang": { 42 | "selector": "/html/@lang", 43 | "type": "xpath", 44 | "global": true 45 | } 46 | }, 47 | "client": { 48 | "lvl0": { 49 | "selector": ".sidebar-link-item.active", 50 | "global": true, 51 | "default_value": "Clients" 52 | }, 53 | "lvl1": ".content h1", 54 | "lvl2": ".content h2", 55 | "lvl3": ".content h3, .content li kbd", 56 | "lvl4": ".content h4", 57 | "lvl5": ".content h5", 58 | "text": ".content p, .content li", 59 | "lang": { 60 | "selector": "/html/@lang", 61 | "type": "xpath", 62 | "global": true 63 | } 64 | }, 65 | "faqs": { 66 | "lvl0": { 67 | "selector": ".sidebar-link-item.active", 68 | "global": true, 69 | "default_value": "FAQs" 70 | }, 71 | "lvl1": ".content h1", 72 | "lvl2": ".content h2", 73 | "text": ".content p, .content li" 74 | }, 75 | "config": { 76 | "lvl0": { 77 | "selector": "p.sidebar-heading.open", 78 | "global": true, 79 | "default_value": "Configuration Options" 80 | }, 81 | "lvl1": ".content h2", 82 | "lvl2": ".content h3", 83 | "text": ".content p, .content li", 84 | "lang": { 85 | "selector": "/html/@lang", 86 | "type": "xpath", 87 | "global": true 88 | } 89 | } 90 | }, 91 | "scrape_start_urls": true, 92 | "strip_chars": " .,;:#", 93 | "custom_settings": { 94 | "attributesForFaceting": [ 95 | "lang" 96 | ] 97 | }, 98 | "js_render": true 99 | } 100 | -------------------------------------------------------------------------------- /docs/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | ], 5 | plugins: [ 6 | 'stylelint-order', 7 | ], 8 | rules: { 9 | 'no-descending-specificity': null, 10 | 'no-empty-source': null, 11 | 'property-case': null, 12 | 'order/properties-order': [ 13 | ['composes'], 14 | { unspecified: 'bottomAlphabetical' }, 15 | ], 16 | 'order/order': [ 17 | 'at-rules', 18 | 'custom-properties', 19 | 'declarations', 20 | ], 21 | 'order/properties-alphabetical-order': true, 22 | 'selector-pseudo-element-no-unknown': [ 23 | true, 24 | { 25 | ignorePseudoElements: ['v-deep'], 26 | }, 27 | ], 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import baseConfig from '@mussi/vitepress-theme/config' 2 | 3 | import { defineConfigWithTheme, HeadConfig, UserConfig } from 'vitepress' 4 | import type { Config } from '@mussi/vitepress-theme' 5 | 6 | const isProd = process.env.NODE_ENV === 'production' 7 | 8 | const title = 'JS From Routes' 9 | const description = 'Generate path helpers and API methods from your Rails routes' 10 | const site = isProd ? 'https://js-from-routes.netlify.app' : 'http://localhost:3000' 11 | const image = `${site}/banner.png` 12 | 13 | const head = [ 14 | ['meta', { name: 'author', content: 'Máximo Mussini' }], 15 | ['meta', { name: 'keywords', content: 'rails, ruby, routes, codegen, js, typescript, vitejs' }], 16 | 17 | ['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }], 18 | 19 | ['meta', { name: 'HandheldFriendly', content: 'True' }], 20 | ['meta', { name: 'MobileOptimized', content: '320' }], 21 | ['meta', { name: 'theme-color', content: '#cc0000' }], 22 | 23 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 24 | ['meta', { name: 'twitter:site', content: site }], 25 | ['meta', { name: 'twitter:title', value: title }], 26 | ['meta', { name: 'twitter:description', value: description }], 27 | ['meta', { name: 'twitter:image', content: image }], 28 | 29 | ['meta', { property: 'og:type', content: 'website' }], 30 | ['meta', { property: 'og:locale', content: 'en_US' }], 31 | ['meta', { property: 'og:site', content: site }], 32 | ['meta', { property: 'og:site_name', content: title }], 33 | ['meta', { property: 'og:title', content: title }], 34 | ['meta', { property: 'og:image', content: image }], 35 | ['meta', { property: 'og:description', content: description }], 36 | ] 37 | 38 | if (isProd) 39 | head.push(['script', { src: 'https://unpkg.com/thesemetrics@latest', async: '' }]) 40 | 41 | export default defineConfigWithTheme({ 42 | extends: baseConfig as () => UserConfig, 43 | title, 44 | description, 45 | head, 46 | themeConfig: { 47 | algolia: { 48 | appId: 'GERZE019PN', 49 | apiKey: 'cdb4a3df8ecf73fadf6bde873fc1b0d2', 50 | indexName: 'js_from_routes', 51 | }, 52 | 53 | logo: '/logo.svg', 54 | 55 | author: { 56 | name: 'Maximo Mussini', 57 | link: 'https://maximomussini.com', 58 | }, 59 | 60 | socialLinks: [ 61 | { icon: 'github', link: 'https://github.com/ElMassimo/js_from_routes' }, 62 | { icon: 'twitter', link: 'https://twitter.com/MaximoMussini' }, 63 | { icon: 'discord', link: 'https://discord.gg/9sSq53jxb4' }, 64 | ], 65 | 66 | footer: { 67 | license: { 68 | text: 'MIT License', 69 | link: 'https://opensource.org/licenses/MIT', 70 | }, 71 | copyright: 'Copyright © 2021-2022', 72 | }, 73 | 74 | nav: [ 75 | { text: 'Guide', link: '/guide/' }, 76 | { text: 'Config', link: '/config/' }, 77 | { 78 | text: 'Changelog', 79 | link: 'https://github.com/ElMassimo/js_from_routes/blob/main/js_from_routes/CHANGELOG.md', 80 | }, 81 | ], 82 | 83 | sidebar: { 84 | '/': [ 85 | { 86 | text: 'Guide', 87 | items: [ 88 | { text: 'Introduction', link: '/guide/introduction' }, 89 | { text: 'Getting Started', link: '/guide/' }, 90 | { text: 'Code Generation', link: '/guide/codegen' }, 91 | ], 92 | }, 93 | { 94 | text: 'Client', 95 | items: [ 96 | { text: 'Integrations', link: '/client/' }, 97 | ], 98 | }, 99 | { 100 | text: 'FAQs', 101 | items: [ 102 | { text: 'Troubleshooting', link: '/faqs/' }, 103 | ], 104 | }, 105 | { 106 | text: 'Config', 107 | link: '/config/', 108 | items: [ 109 | { text: 'Code Generation', link: '/config/' }, 110 | ], 111 | }, 112 | ], 113 | }, 114 | }, 115 | 116 | vite: { 117 | optimizeDeps: { 118 | exclude: ['@mussi/vitepress-theme'], 119 | }, 120 | }, 121 | }) 122 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | 43 | 171 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/Quote.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import { VPTheme } from '@mussi/vitepress-theme' 2 | import Quote from './components/Quote.vue' 3 | 4 | import 'windi.css' 5 | import './styles/styles.css' 6 | 7 | export default { 8 | ...VPTheme, 9 | enhanceApp ({ app }) { 10 | app.component('Quote', Quote) 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/styles/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vt-c-brand: #c00; 3 | --vt-c-brand-light: #d53f3f; 4 | --vt-c-brand-dark: var(--vt-c-brand); 5 | --vt-c-brand-highlight: var(--vt-c-brand-light); 6 | --vt-c-contrast-light: #0ea5e9; 7 | --bg-colored-light: #f3f7f7; 8 | --kbd-border: #d1d5da; 9 | --vt-c-tip: #0ea5e9; 10 | } 11 | 12 | html.dark { 13 | --kbd-border: var(--vt-c-text-dark-3); 14 | 15 | p { 16 | --code-inline-bg-color: #ffcaca0d; 17 | } 18 | 19 | .vt-doc a { 20 | color: var(--vt-c-text-dark-1); 21 | text-decoration: underline; 22 | text-decoration-color: var(--vt-c-text-dark-2); 23 | } 24 | 25 | .vt-doc a > code { 26 | --vt-c-brand-dark: var(--vt-c-text-dark-1); 27 | } 28 | 29 | .content a:hover, 30 | .content a:hover code { 31 | color: white; 32 | text-decoration-color: currentColor; 33 | } 34 | 35 | .DocSearch.DocSearch { 36 | --docsearch-container-background: rgba(12, 12, 12, 0.8); 37 | } 38 | 39 | .page .content { 40 | --vt-c-brand: var(--vt-c-text); 41 | } 42 | 43 | .nav-link.action:not(.alt) .item, 44 | .nav-link.action .item.item:hover, 45 | .custom-block.custom-block.tip a:hover { 46 | color: white; 47 | } 48 | 49 | kbd { 50 | background-color: #333; 51 | box-shadow: none; 52 | } 53 | 54 | kbd a { 55 | color: var(--vt-c-text); 56 | font-weight: 500; 57 | } 58 | } 59 | 60 | .VPNavBarTitle .text { 61 | display: none; 62 | } 63 | 64 | .VPNavBarSearch { 65 | justify-content: flex-end; 66 | } 67 | 68 | .vt-doc .custom-block.tip { 69 | --vt-c-brand: var(--vt-c-text-1); 70 | --vt-c-brand-dark: var(--vt-c-text-1); 71 | --vt-c-brand-highlight: var(--vt-c-text-1); 72 | } 73 | 74 | @media (min-width: 720px) { 75 | .home-hero.home-hero.home-hero { 76 | margin-top: 2rem; 77 | } 78 | } 79 | 80 | .nav-bar .logo { 81 | height: 30px; 82 | margin-right: 2px; 83 | } 84 | 85 | h2 .logo { 86 | display: inline-block; 87 | height: 32px; 88 | margin-left: 4px; 89 | margin-bottom: 4px; 90 | vertical-align: middle; 91 | } 92 | 93 | kbd { 94 | background-color: #fafbfc; 95 | border: 1px solid var(--kbd-border); 96 | border-radius: 6px; 97 | box-shadow: inset 0 -1px 0 var(--kbd-border); 98 | box-sizing: border-box; 99 | display: inline-block; 100 | font-size: 0.9rem; 101 | line-height: 10px; 102 | padding: 5px; 103 | vertical-align: middle; 104 | } 105 | 106 | code kbd { 107 | font-size: 0.7rem; 108 | padding: 2px 5px; 109 | } 110 | 111 | html:not(.dark) .vt-doc [class*='language-'] code { 112 | color: white; 113 | } 114 | 115 | .DocSearch-Search-Icon.DocSearch-Search-Icon { 116 | top: 0px; 117 | } 118 | 119 | .search-key { 120 | vertical-align: 3px; 121 | } 122 | -------------------------------------------------------------------------------- /docs/client/index.md: -------------------------------------------------------------------------------- 1 | [Vite Rails]: https://vite-ruby.netlify.app/ 2 | [aliases]: https://vite-ruby.netlify.app/guide/development.html#import-aliases-%F0%9F%91%89 3 | [codegen]: /guide/codegen 4 | [axios]: https://github.com/axios/axios 5 | [redaxios]: https://github.com/developit/redaxios 6 | [Inertia.js]: https://github.com/inertiajs/inertia 7 | [@js-from-routes/client]: https://github.com/ElMassimo/js_from_routes/tree/main/packages/client 8 | [@js-from-routes/axios]: https://github.com/ElMassimo/js_from_routes/tree/main/packages/axios 9 | [@js-from-routes/inertia]: https://github.com/ElMassimo/js_from_routes/tree/main/packages/inertia 10 | [@js-from-routes/redaxios]: https://github.com/ElMassimo/js_from_routes/tree/main/packages/redaxios 11 | [client_library]: /config/#client-library 12 | [easily submit forms]: https://github.com/ElMassimo/pingcrm-vite/blob/05e462cbed63ef150b1e1f12c984ef03a2e485aa/app/javascript/Pages/Contacts/Edit.vue#L24 13 | [config.ts]: https://github.com/ElMassimo/js_from_routes/blob/main/packages/client/src/config.ts 14 | [@rails/ujs]: https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts 15 | [extracted]: https://github.com/ElMassimo/js_from_routes/blob/babeae83294efe58c4fa6bea0d76b5e146b0b92a/packages/client/src/config.ts#L37-L42 16 | 17 | # Client Libraries 18 | 19 | Several JS packages are provided to handle URL interpolation and requests. 20 | 21 | You can select which one to use by configuring [client_library], which also enables you to target your own code instead. 22 | 23 | #### [@js-from-routes/client] 24 | 25 | The default client. Uses `fetch` to avoid additional dependencies. 26 | 27 | #### [@js-from-routes/axios] 28 | 29 | Choose it if already using [axios], or have a [complex use case](https://github.com/axios/axios#creating-an-instance) with extensive usage of [interceptors](https://github.com/axios/axios#interceptors). 30 | 31 | #### [@js-from-routes/inertia] 32 | 33 | Allows you to [easily submit forms] and make requests using your existing configuration in [Inertia.js]. 34 | 35 | #### [@js-from-routes/redaxios] 36 | 37 | Choose it if already using [redaxios], for consistency within your codebase. 38 | 39 | ## Usage 🚀 40 | 41 | The methods are fully typed, and documented using JSDoc. 42 | 43 | Here a few examples when using [@js-from-routes/client], more examples coming soon. 44 | 45 | ### Importing helpers 46 | 47 | If you use [Vite Rails], [aliases] will allow you to import these files as: 48 | 49 | ```js 50 | import { videoClips } from '~/api' 51 | // or 52 | import VideoClipsApi from '~/api/VideoClipsApi' 53 | ``` 54 | 55 | When using webpack, it's highly recommended to [add an alias](https://webpack.js.org/configuration/resolve/#resolvealias) to simplify imports. 56 | 57 | ### Passing parameters 58 | 59 | You can pass a plain object as parameters; properties will be interpolated on any matching placeholders (`:id`) in the URL. 60 | 61 | ```js 62 | const video = { id: 5, title: 'New Wave' } 63 | videoClips.download.path(video) == "/video_clips/5/download" 64 | ``` 65 | 66 | Missing parameters will throw an error providing the full context. 67 | 68 | ### Submitting data 69 | 70 | You can pass `data` to specify the request body, just like in [axios]. 71 | 72 | ```js 73 | videoClips.update({ params: video, data: { title: 'New Waves' } }) 74 | ``` 75 | 76 | By default, the CSRF token will be [extracted] using the same conventions in [@rails/ujs]. 77 | 78 | ### Obtaining the raw response 79 | 80 | By default, JSON responses are automatically unwrapped. 81 | 82 | If you need access to the response, or are using MIME types, pass `responseAs`: 83 | 84 | ```js 85 | const response = await videoClips.download({ params: video, responseAs: 'response' }) 86 | ``` 87 | 88 | The type of the response object depends on which library you are using. 89 | 90 | For example, it will be an `AxiosResponse` if using [@js-from-routes/axios]. 91 | 92 | ## Configuring Requests ⚙️ 93 | 94 | You can configure the defaults in all clients by using `Config`: 95 | 96 | ```js 97 | import { Config } from '@js-from-routes/client' 98 | 99 | Config.baseUrl = process.env.API_BASE_URL 100 | ``` 101 | 102 | #### Disabling Case Conversion 103 | 104 | By default, object keys are `camelCased` when deserialized, and `snake_cased` 105 | when sent back to the server, but you can disable this behavior. 106 | 107 | ```js 108 | const noop = val => val 109 | Config.deserializeData = noop 110 | Config.serializeData = noop 111 | ``` 112 | 113 | #### Other Options 114 | 115 | You can provide default headers, intercept all requests, handle response errors, and more. 116 | 117 | A new section documenting all available configuration options will be added soon. 118 | 119 | In the meantime, refer to [the source code][config.ts]. 120 | -------------------------------------------------------------------------------- /docs/config/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | [default template library]: https://github.com/ElMassimo/js_from_routes/blob/main/js_from_routes/lib/js_from_routes/template.js.erb#L3 6 | [template all]: https://github.com/ElMassimo/js_from_routes/blob/main/js_from_routes/lib/js_from_routes/template_all.js.erb 7 | [template index]: https://github.com/ElMassimo/js_from_routes/blob/main/js_from_routes/lib/js_from_routes/template_index.js.erb 8 | [default template]: https://github.com/ElMassimo/js_from_routes/blob/main/js_from_routes/lib/js_from_routes/template.js.erb 9 | [config options]: https://github.com/ElMassimo/js_from_routes/blob/main/js_from_routes/lib/js_from_routes/generator.rb#L178-L189 10 | [generate TypeScript]: https://github.com/ElMassimo/js_from_routes/blob/main/playground/vanilla/config/initializers/js_from_routes.rb 11 | [jQuery]: https://gist.github.com/ElMassimo/cab56e64e20ff797f3054b661a883646 12 | [ping]: https://github.com/ElMassimo/pingcrm-vite 13 | 14 | [client]: /client/ 15 | [codegen]: /guide/codegen 16 | [client_library]: /config/#client-library 17 | [different template]: /guide/codegen.html#using-a-different-template 18 | 19 | # Configuration Reference 20 | 21 | The following section contains references to all configuration options in the provided libraries. 22 | 23 | ## Code Generation 24 | 25 | In order to customize code generation, you can add an initializer to your Rails app: 26 | 27 | ```ruby 28 | # config/initializers/js_from_routes.rb 29 | if Rails.env.development? 30 | JsFromRoutes.config do |config| 31 | ... 32 | end 33 | end 34 | ``` 35 | 36 | You can fully customize how code is generated, which [client libraries][client] to use, 37 | whether to [generate TypeScript], target [jQuery], or [adapt to your framework of choice][ping]. 38 | 39 | The following [config options] are available: 40 | 41 | ### `all_helpers_file` 42 | 43 | Whether to generate a file that exports all available helpers. 44 | 45 | You can specify a different name for the file, or pass `false` to disable it. 46 | 47 | __Default__: `true`, will output `index.js` in the [output_folder][config options] 48 | 49 | ```ruby 50 | config.all_helpers_file = false # Don't generate the file 51 | ``` 52 | 53 | ### `client_library` 54 | 55 | The [library][client] from which to [import `definePathHelper`][default template library] in the [default template](#template-path). 56 | 57 | Read more about it in [_Code Generation_][codegen]. 58 | 59 | __Default__: `@js-from-routes/client` 60 | 61 | ```ruby 62 | config.client_library = '~/services/ApiService' 63 | ``` 64 | 65 | ### `export_if` 66 | 67 | Allows to configure which routes should be exported. 68 | 69 | Enables advanced usages, such as exporting different routes to different paths, 70 | which is helpful for monoliths with several apps inside them. 71 | 72 | __Default__: `->(route) { route.defaults.fetch(:export, nil) }` 73 | 74 | ```ruby 75 | config.export_if = ->(route) { route.defaults[:export] == :main } 76 | ``` 77 | 78 | ### `file_suffix` 79 | 80 | This suffix is added by default to all generated files. You can modify it if 81 | you [are using TypeScript][generate TypeScript], or want to use a different convention. 82 | 83 | __Default__: `Api.js` 84 | 85 | ```ruby 86 | config.file_suffix = 'Api.ts' 87 | ``` 88 | 89 | ### `helper_mappings` 90 | 91 | Defines how to obtain a path helper name from the name of a route (controller action). 92 | 93 | __Default__: `{"index" => "list", "show" => "get"}` 94 | 95 | ```ruby 96 | config.helper_mappings = {"edit" => "infoForUpdate"} 97 | ``` 98 | 99 | ### `output_folder` 100 | 101 | The directory where the generated files are created. 102 | 103 | By default it will use the first of the following directories that exists. 104 | 105 | __Default__: `app/{frontend,javascript,packs,assets}/api` 106 | 107 | ```ruby 108 | config.output_folder = Rails.root.join('app', 'path_helpers') 109 | ``` 110 | 111 | ### `template_path` 112 | 113 | The path of an ERB template that will be used to generate a helpers file. 114 | 115 | Read more about it in [_Code Generation_][different template]. 116 | 117 | [__Default__][default template] 118 | 119 | ```ruby 120 | config.template_path = Rails.root.join('custom_js_from_routes.js.erb') 121 | ``` 122 | 123 | ### `template_all_path` 124 | 125 | The path of an ERB template that will be used to export all helper files. 126 | 127 | You can provide a path to a custom template if the default conventions don't suit your needs. 128 | 129 | [__Default__][template all] 130 | 131 | ```ruby 132 | config.template_all_path = Rails.root.join('custom_all_helpers.js.erb') 133 | ``` 134 | 135 | ### `template_index_path` 136 | 137 | Similar to the above, it re-exports all helpers, but also combines them in a default export allowing you to use a single object to access all the helpers. 138 | 139 | You can provide a path to a custom template if the default conventions don't suit your needs. 140 | 141 | [__Default__][template index] 142 | 143 | ```ruby 144 | config.template_index_path = Rails.root.join('custom_index_helpers.js.erb') 145 | ``` 146 | 147 | ## Client Configuration 148 | 149 | Coming soon... 150 | 151 | For now, check the [_Client Libraries_](/client/#configuring-requests-⚙%EF%B8%8F) section. 152 | 153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | -------------------------------------------------------------------------------- /docs/faqs/index.md: -------------------------------------------------------------------------------- 1 | [project]: https://github.com/ElMassimo/js_from_routes 2 | [GitHub Issues]: https://github.com/ElMassimo/js_from_routes/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc 3 | [GitHub Discussions]: https://github.com/ElMassimo/js_from_routes/discussions 4 | [client]: /client/ 5 | [cache key]: /guide/codegen.html#caching-📦 6 | 7 | # Troubleshooting 8 | 9 | This section lists a few common gotchas, and bugs introduced in the past. 10 | 11 | Please skim through __before__ opening an [issue][GitHub Issues]. 12 | 13 | ## Could not resolve "@js-from-routes/core" 14 | 15 | Some package managers do not install `peerDependencies`, while others [do](https://github.com/npm/rfcs/blob/latest/implemented/0025-install-peer-deps.md). 16 | 17 | Try adding the missing package explicitly: 18 | 19 | ```bash 20 | npm install @js-from-routes/core # yarn add @js-from-routes/core 21 | ``` 22 | 23 | ## Imports in the generated code are not working 24 | 25 | Make sure that you have added one of the [client libraries][client] to your `package.json` and that the packages are installed. 26 | 27 | ## Changes to my custom templates are not being picked up 28 | 29 | Modifying the template should change the [cache key], so you might have introduced additional dependencies in your custom template. 30 | 31 | Try forcing regeneration by running: 32 | 33 | ```bash 34 | JS_FROM_ROUTES_FORCE=true bin/rake js_from_routes:generate 35 | ``` 36 | 37 | ## Contact ✉️ 38 | 39 | Please visit [GitHub Issues] to report bugs you find, and [GitHub Discussions] to make feature requests, or to get help. 40 | 41 | Don't hesitate to [⭐️ star the project][project] if you find it useful! 42 | 43 | Using it in production? Always love to hear about it! 😃 44 | -------------------------------------------------------------------------------- /docs/guide/codegen.md: -------------------------------------------------------------------------------- 1 | [route dsl]: https://github.com/ElMassimo/js_from_routes/blob/main/js_from_routes/lib/js_from_routes/generator.rb#L77-L107 2 | [this pull request]: https://github.com/ElMassimo/pingcrm-vite/pull/2 3 | 4 | [exported routes]: /guide/#export-the-routes 5 | [client]: /client/ 6 | [config]: /config/#code-generation 7 | [default template]: /config/#template-path 8 | [template_path]: /config/#template-path 9 | [template all]: /config/#template-all-path 10 | 11 | [client_library]: /config/#client-library 12 | 13 | # Code Generation 🤖 14 | 15 | Whenever you add a new route and _refresh the page_, the Rails reloader will kick in and generate path helpers for any of the [exported routes]. 16 | 17 | If you are not running the development server, you can run a rake task to generate path helpers: 18 | 19 | ```bash 20 | bin/rake js_from_routes:generate 21 | ``` 22 | 23 | By default, it will generate one file per controller, with one helper per exported action: 24 | 25 | ```js 26 | // app/javascript/api/VideoClipsApi.ts 27 | import { definePathHelper } from '@js-from-routes/client' 28 | 29 | export default { 30 | download: definePathHelper('get', '/video_clips/:id/download'), 31 | 32 | update: definePathHelper('patch', '/video_clips/:id'), 33 | } 34 | ``` 35 | 36 | Notice how the HTTP verb becomes an implementation detail. 37 | 38 | Changing the verb in `routes.rb` does not require updating your client code! 39 | 40 | ## Customizing the Generated Code 🛠 41 | 42 | You can customize the code produced by the [default template], or use your own template. 43 | 44 | The following code examples assume that you are configuring _JS From Routes_ in an [initializer][config]. 45 | 46 | ### Using a different client 47 | 48 | You can use any of the [provided client libraries][client] by using [client_library]: 49 | 50 | ```ruby 51 | config.client_library = '@js-from-routes/axios' 52 | ``` 53 | 54 | ### Using your own code 55 | 56 | You can also use [client_library] to target your own code when generating path helpers: 57 | 58 | ```ruby 59 | config.client_library = '~/MyPathHelpers' 60 | ``` 61 | 62 | As a result, the [default template] will generate: 63 | 64 | ```js 65 | // app/javascript/api/VideoClipsApi.ts 66 | import { definePathHelper } from '~/MyPathHelpers' 67 | 68 | export default { 69 | ... 70 | } 71 | ``` 72 | 73 | ### Using a different template 74 | 75 | If you need to generate helpers in a different way, or want do something entirely different with exported routes, you can configure [template_path] to use your own template: 76 | 77 | ```ruby 78 | config.template_path = Rails.root.join('custom_js_from_routes.js.erb') 79 | ``` 80 | 81 | A `routes` variable will be available in the template, with the exported routes for a controller. 82 | 83 | Each `route` exposes properties such as `verb` and `path`, [check the source code][route dsl] for details. 84 | 85 | See [this pull request] to get a sense of how flexible it can be. 86 | 87 | ## Caching 📦 88 | 89 | Code generation is skipped when routes have not changed. 90 | 91 | This is achieved by adding a header to generated files: 92 | 93 | ```js 94 | // JsFromRoutes CacheKey 12d79db32ed146448798751582013757 95 | // 96 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 97 | ``` 98 | 99 | If for some reason you want to force regeneration, you can run: 100 | 101 | ```bash 102 | JS_FROM_ROUTES_FORCE=true bin/rake js_from_routes:generate 103 | ``` 104 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | [library]: https://github.com/ElMassimo/js_from_routes 2 | [GitHub Issues]: https://github.com/ElMassimo/js_from_routes/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc 3 | [GitHub Discussions]: https://github.com/ElMassimo/js_from_routes/discussions 4 | [client]: /client/ 5 | [config]: /config/ 6 | [rails bytes]: https://railsbytes.com/templates/X6ksgn 7 | [codegen]: /guide/codegen 8 | [path]: /client/#passing-parameters 9 | [request]: /client/#submitting-data 10 | [all helpers]: /config/#all-helpers-file 11 | [ping]: https://github.com/ElMassimo/pingcrm-vite 12 | 13 | [development]: /client/ 14 | [routes]: https://github.com/ElMassimo/js_from_routes/blob/main/playground/vanilla/config/routes.rb#L6 15 | [export false]: https://github.com/ElMassimo/js_from_routes/blob/main/playground/vanilla/config/routes.rb#L18 16 | 17 | # Getting Started 18 | 19 | If you are interested to learn more about JS From Routes before trying it, check out the [Introduction](./introduction). 20 | 21 | ## Installation 💿 22 | 23 | For a one-line installation, you can [run this][rails bytes]: 24 | 25 | ```bash 26 | rails app:template LOCATION='https://railsbytes.com/script/X6ksgn' 27 | ``` 28 | 29 | Or, add this line to your application's Gemfile in the `development` group and execute `bundle`: 30 | 31 | ```ruby 32 | group :development, :test do 33 | gem 'js_from_routes' 34 | end 35 | ``` 36 | 37 | And then, add a [client library][client] to your `package.json`: 38 | 39 | ```bash 40 | npm install @js-from-routes/client # yarn add @js-from-routes/client 41 | ``` 42 | 43 | ## Usage 🚀 44 | 45 | Once the library is installed, all you need to do is: 46 | 47 | ### Export the routes 48 | 49 | Use `export: true` to specify which [routes] should be taken into account when generating JS. 50 | 51 | ```ruby {2} 52 | Rails.application.routes.draw do 53 | resources :video_clips, only: [:show], export: true do 54 | get :download, on: :member 55 | end 56 | 57 | # Or: 58 | defaults export: true do 59 | # All routes defined inside this block will be exported. 60 | end 61 | end 62 | ``` 63 | 64 | It will cascade to any nested action or resource, but you can [opt out][export false] with `export: false`. 65 | 66 | ### Refresh the page 67 | 68 | Rails' reloader will detect the changes, and path helpers will be [generated][codegen] for the exported actions. 69 | 70 | ```js 71 | // app/frontend/api/VideoClipsApi.ts 72 | import { definePathHelper } from '@js-from-routes/client' 73 | 74 | export default { 75 | download: definePathHelper('get', '/video_clips/:id/download'), 76 | 77 | show: definePathHelper('get', '/video_clips/:id'), 78 | } 79 | ``` 80 | 81 | A file will be generated per controller, with a path helper per exported action, although this is [fully customizable][codegen]. 82 | 83 | You can run bin/rake js_from_routes:generate to trigger generation manually. 84 | 85 | ### Use the generated helpers 86 | 87 | Calling a helper will [make a request][request] and return a promise with the unwrapped JSON result. 88 | 89 | ```js 90 | import { videoClips } from '~/api' 91 | 92 | const video = await videoClips.show({ id: 'oHg5SJYRHA0' }) 93 | ``` 94 | 95 | Use [`path`][path] when you only need the URL. It will interpolate parameters, such as `:id`. 96 | 97 | ```js 98 | const downloadPath = videoClips.download.path(video) 99 | ``` 100 | 101 | You can also use an object that combines [all helpers]. 102 | 103 | ```js 104 | import api from '~/api' 105 | 106 | const video = await api.videoClips.show({ id: 'oHg5SJYRHA0' }) 107 | 108 | const comments = await api.comments.index() 109 | ``` 110 | 111 | Depending on your use case, you may prefer to use this object instead of explicit imports. 112 | 113 | ## Onwards 🛣 114 | 115 | You should now be able to get started with [JS From Routes][library]. 116 | 117 | Have in mind that code generation is [fully customizable][codegen]; this is just the default way to use it. 118 | 119 | For more information about using the helpers, check out [this section][development]. 120 | 121 | If you would like to see a working example, check [this repo][ping]. 122 | 123 | ### Contact ✉️ 124 | 125 | Please visit [GitHub Issues] to report bugs you find, and [GitHub Discussions] to make feature requests, or to get help. 126 | 127 | Don't hesitate to [⭐️ star the project][library] if you find it useful! 128 | 129 | Using it in production? Always love to hear about it! 😃 130 | -------------------------------------------------------------------------------- /docs/guide/introduction.md: -------------------------------------------------------------------------------- 1 | [library]: https://github.com/ElMassimo/js_from_routes 2 | [motivation]: /motivation 3 | [rails]: http://rubyonrails.org/ 4 | [blog post]: https://maximomussini.com/posts/js-from-routes/ 5 | [path helpers]: https://guides.rubyonrails.org/routing.html#path-and-url-helpers 6 | 7 | [codegen]: /guide/codegen 8 | [client]: /client/ 9 | [config]: /config/ 10 | [paths]: /client/#passing-parameters 11 | [requests]: /client/#submitting-data 12 | [case conversion]: /client/#disabling-case-conversion 13 | [responseAs]: /config/#responseAs 14 | 15 | # Introduction 16 | 17 | [__JS From Routes__][library] is a library to generate JS from routes defined in your [Rails] application. 18 | 19 | ## Why JS From Routes? 🤔 20 | 21 | [Path helpers] in Rails make it easy to build URLs, while avoiding typos and mistakes. 22 | 23 | Frontend code in Rails typically receives URLs from the server through JSON or HTML templates, or hardcodes paths of any endpoints that need to be accessed. Both approaches are fragile and cumbersome, and don't scale well. 24 | 25 | [JS From Routes][library] provides path helpers in JS, generating them from your Rails routes, so that you can make requests without manually handling paths, parameter interpolation, or HTTP verbs. 26 | 27 | Interested in hearing more? Read the original [blog post]. 28 | 29 | ## Features ⚡️ 30 | 31 | ### 🚀 Path and Request Helpers 32 | 33 | Use the controller and action name to [obtain paths][paths] or [make requests][requests]. No need to use URLs or manually interpolate parameters, preventing mistakes and saving development time. 34 | 35 | ### 🔁 Serialization / Deserialization 36 | 37 | Consuming JSON APIs works out of the box, but you can easily consume [other types of media][responseAs]. 38 | 39 | [Case conversion] between Ruby and JS is handled for you, but you can also [opt-out][case conversion]. 40 | 41 | ### ✅ Safety 42 | 43 | Prevents routing mistakes when renaming or removing an action. 44 | 45 | Path helpers are fully typed, and client libraries are entirely written in TypeScript. 46 | 47 | ### 🤖 Automatic Generation 48 | 49 | Path helpers are [generated automatically][codegen] whenever Rails reload is triggered. 50 | 51 | Add a route, refresh the page, and start using the path helper! 52 | 53 | ### 🛠 Customizable Generation 54 | 55 | Select a [client library][client] that uses `fetch` or `axios`, or use your [own code][client]. 56 | 57 | Choose different conventions by [customizing][codegen] how code is generated. 58 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | page: true 3 | sidebar: false 4 | features: 5 | - title: 🚀 Productive 6 | details: No need to specify the URL, use the controller and action name 7 | link: /guide/#use-the-generated-helpers 8 | - title: 🎩 Elegant 9 | details: Make requests with function calls that return promises 10 | link: /client/#submitting-data 11 | - title: ✅ Safer 12 | details: Path helpers are fully typed and typos are no longer possible 13 | link: /guide/introduction.html#✅-safety 14 | - title: 🛠 Customizable 15 | details: Client libraries for fetch, axios, and more. Or use your own code 16 | link: /client/ 17 | --- 18 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js_from_routes_docs", 3 | "private": true, 4 | "version": "unknown", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "vitepress dev --open", 8 | "build": "vitepress build", 9 | "preview": "vite preview .vitepress", 10 | "search": "docker run -it --env-file=.algolia/.env -e \"CONFIG=$(cat .algolia/config.json | jq -r tostring)\" algolia/docsearch-scraper", 11 | "css": "stylelint --ignore-pattern 'dist' '.vitepress/**/*.(vue|scss|css|postcss)'" 12 | }, 13 | "devDependencies": { 14 | "@mussi/vitepress-theme": "^1.0.1", 15 | "stylelint": "^13.12.0", 16 | "stylelint-config-standard": "^20.0.0", 17 | "stylelint-order": "^4.1.0", 18 | "typescript": "^4.2.3", 19 | "vite": "^2.9", 20 | "vite-plugin-windicss": "^1", 21 | "vitepress": "^0.22.3" 22 | }, 23 | "pnpm": { 24 | "peerDependencyRules": { 25 | "ignoreMissing": [ 26 | "@algolia/client-search", 27 | "react", 28 | "react-dom", 29 | "@types/react" 30 | ] 31 | } 32 | }, 33 | "dependencies": { 34 | "vue": "^3.2.36" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/pnpm-workspace.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/docs/pnpm-workspace.yaml -------------------------------------------------------------------------------- /docs/public/_headers: -------------------------------------------------------------------------------- 1 | /assets/* 2 | cache-control: max-age=31536000 3 | cache-control: immutable -------------------------------------------------------------------------------- /docs/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/docs/public/banner.png -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/public/logo-with-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/docs/public/logo-with-text.png -------------------------------------------------------------------------------- /docs/public/logo-with-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import WindiCSS from 'vite-plugin-windicss' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | WindiCSS({ 8 | preflight: false, 9 | scan: { 10 | dirs: [resolve(__dirname, '.vitepress/theme/components')], 11 | }, 12 | }), 13 | ], 14 | }) 15 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-edge: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 4 | 5 | gem 'rails', github: 'rails/rails', branch: 'main' 6 | 7 | gemspec path: '../js_from_routes' 8 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.7.2.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 7.2.1' 4 | 5 | gemspec path: '../js_from_routes' 6 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails.8.0.x: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 8.0' 4 | 5 | gemspec path: '../js_from_routes' 6 | -------------------------------------------------------------------------------- /js_from_routes/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Máximo Mussini 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /js_from_routes/README.md: -------------------------------------------------------------------------------- 1 |

2 | JS From Rails Routes 3 |

4 | Build Status 5 | Inline docs 6 | Maintainability 7 | Test Coverage 8 | Gem Version 9 | License 10 |

11 |

12 | 13 | [Vite Rails]: https://vite-ruby.netlify.app/ 14 | [aliases]: https://vite-ruby.netlify.app/guide/development.html#import-aliases-%F0%9F%91%89 15 | [config options]: https://github.com/ElMassimo/js_from_routes/blob/main/lib/js_from_routes/generator.rb#L82-L85 16 | [readme]: https://github.com/ElMassimo/js_from_routes 17 | 18 | For more information, check the main [README]. 19 | 20 | ### Installation 💿 21 | 22 | Add this line to your application's Gemfile in the `development` group: 23 | 24 | ```ruby 25 | gem 'js_from_routes' 26 | ``` 27 | 28 | And then execute: 29 | 30 | $ bundle 31 | 32 | Or install it yourself as: 33 | 34 | $ gem install js_from_routes 35 | -------------------------------------------------------------------------------- /js_from_routes/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | class Bundler::GemHelper 6 | def version_tag 7 | "js_from_routes@#{version}" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /js_from_routes/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | pry -r './lib/js_from_routes.rb' 3 | -------------------------------------------------------------------------------- /js_from_routes/bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd .. && pnpm release js_from_routes "$@" 3 | -------------------------------------------------------------------------------- /js_from_routes/js_from_routes.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../lib/js_from_routes/version", __FILE__) 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "js_from_routes" 5 | s.version = JsFromRoutes::VERSION 6 | s.authors = ["Máximo Mussini"] 7 | s.email = ["maximomussini@gmail.com"] 8 | s.summary = "Generate JS automatically from Rails routes." 9 | s.description = "js_from_routes helps you by automatically generating path and API helpers from Rails route definitions, allowing you to save development effort and focus on the things that matter." 10 | s.homepage = "https://github.com/ElMassimo/js_from_routes" 11 | s.license = "MIT" 12 | s.extra_rdoc_files = ["README.md"] 13 | s.files = Dir.glob("{lib,exe,templates}/**/*") + %w[README.md CHANGELOG.md LICENSE.txt] 14 | s.require_path = "lib" 15 | 16 | s.add_dependency "railties", ">= 5.1", "< 9" 17 | 18 | s.add_development_dependency "bundler", "~> 2" 19 | s.add_development_dependency "listen", "~> 3.2" 20 | s.add_development_dependency "pry-byebug", "~> 3.9" 21 | s.add_development_dependency "rake", "~> 13" 22 | s.add_development_dependency "rspec-given", "~> 3.8" 23 | s.add_development_dependency "simplecov", "< 0.18" 24 | s.add_development_dependency "standard", "~> 1.0" 25 | end 26 | -------------------------------------------------------------------------------- /js_from_routes/lib/js_from_routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Splitting the generator file allows consumers to skip the Railtie if desired: 4 | # - gem 'js_from_routes', require: false 5 | # - require 'js_from_routes/generator' 6 | require "js_from_routes/generator" 7 | require "js_from_routes/railtie" 8 | -------------------------------------------------------------------------------- /js_from_routes/lib/js_from_routes/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/railtie" 4 | 5 | # NOTE: Not strictly required, but it helps to simplify the setup. 6 | class JsFromRoutes::Railtie < Rails::Railtie 7 | railtie_name :js_from_routes 8 | 9 | if Rails.env.development? 10 | # Allows to automatically trigger code generation after updating routes. 11 | initializer "js_from_routes.reloader" do |app| 12 | app.config.to_prepare do 13 | JsFromRoutes.generate!(app) 14 | end 15 | end 16 | end 17 | 18 | # Suitable when triggering code generation manually. 19 | rake_tasks do |app| 20 | namespace :js_from_routes do 21 | desc "Generates JavaScript files from Rails routes, one file per controller, and one function per route." 22 | task generate: :environment do 23 | JsFromRoutes.generate!(app) 24 | end 25 | end 26 | end 27 | 28 | # Prevents Rails from interpreting the :export option as a required default, 29 | # which would cause controller tests to fail. 30 | initializer "js_from_routes.required_defaults" do |app| 31 | ActionDispatch::Journey::Route.prepend Module.new { 32 | def required_default?(key) 33 | (key == :export) ? false : super 34 | end 35 | } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /js_from_routes/lib/js_from_routes/template.js.erb: -------------------------------------------------------------------------------- 1 | // 2 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 3 | import { definePathHelper } from '<%= client_library %>' 4 | 5 | export default { 6 | <% routes.each do |route| %> 7 | <%= route.helper %>: /* #__PURE__ */ definePathHelper('<%= route.verb %>', '<%= route.path %>'), 8 | <% end %> 9 | } 10 | -------------------------------------------------------------------------------- /js_from_routes/lib/js_from_routes/template_all.js.erb: -------------------------------------------------------------------------------- 1 | // 2 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 3 | <% helpers.each do |helper| %> 4 | export { default as <%= helper.js_name %> } from './<%= helper.import_filename %>' 5 | <% end %> 6 | -------------------------------------------------------------------------------- /js_from_routes/lib/js_from_routes/template_index.js.erb: -------------------------------------------------------------------------------- 1 | // 2 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 3 | import * as helpers from './all' 4 | export * from './all' 5 | export default helpers 6 | -------------------------------------------------------------------------------- /js_from_routes/lib/js_from_routes/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Public: Automatically generates JS for Rails routes with { export: true }. 4 | # Generates one file per controller, and one function per route. 5 | module JsFromRoutes 6 | # Public: This library adheres to semantic versioning. 7 | VERSION = "4.0.2" 8 | end 9 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "16" 3 | NPM_FLAGS = "--version" # uncomment if using pnpm to skip npm install 4 | 5 | [build] 6 | base = "docs/" 7 | ignore = "git diff --quiet 'HEAD^' HEAD ." 8 | publish = ".vitepress/dist" 9 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build" 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js_from_routes_monorepo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "npm -C packages run build", 8 | "docs": "npm -C docs run dev", 9 | "docs:search": "npm -C docs run search", 10 | "dev": "npm -C packages run dev", 11 | "release": "node scripts/release", 12 | "lint": "eslint . --ext .ts,.js", 13 | "postinstall": "husky install", 14 | "changelog": "node scripts/changelog", 15 | "test": "vitest" 16 | }, 17 | "devDependencies": { 18 | "@mussi/eslint-config": "^0.5.0", 19 | "@types/node": "^14.14.31", 20 | "chalk": "^4.1.0", 21 | "conventional-changelog-cli": "^2.1.1", 22 | "enquirer": "^2.3.6", 23 | "eslint": "^7.17.0", 24 | "eslint-plugin-jest": "^24.1.5", 25 | "execa": "^5.0.0", 26 | "happy-dom": "7.7.2", 27 | "husky": "^5.1.1", 28 | "lint-staged": "^10.5.4", 29 | "minimist": "^1.2.5", 30 | "semver": "^7.3.4", 31 | "typescript": "^4.0.5", 32 | "vitest": "^0.29.8" 33 | }, 34 | "lint-staged": { 35 | "*.{js,ts,tsx,jsx,vue}": [ 36 | "eslint --fix" 37 | ], 38 | "*.rb": [ 39 | "bin/standardrb --fix" 40 | ] 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/ElMassimo/js_from_routes" 45 | }, 46 | "homepage": "https://github.com/ElMassimo/js_from_routes" 47 | } 48 | -------------------------------------------------------------------------------- /packages/axios/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.5](https://github.com/ElMassimo/js_from_routes/compare/axios@1.0.4...axios@1.0.5) (2025-03-27) 2 | 3 | 4 | 5 | ## [1.0.4](https://github.com/ElMassimo/js_from_routes/compare/axios@1.0.3...axios@1.0.4) (2021-03-16) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * Ensure callbacks are passed to Inertia.js ([c456b36](https://github.com/ElMassimo/js_from_routes/commit/c456b36e6f80927fa3f10999d46f3c91c34a408a)) 11 | * Ensure packages are specified by using peerDependencies ([24b4918](https://github.com/ElMassimo/js_from_routes/commit/24b49183e3b6c7169b85eb0c0b06272b16455920)) 12 | 13 | 14 | 15 | ## 1.0.3 (2021-03-13) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * Allow using responseAs: 'response' with Axios and Redaxios ([cdaf9cd](https://github.com/ElMassimo/js_from_routes/commit/cdaf9cd895407773851df4983108dcef1b0f6182)) 21 | 22 | 23 | 24 | ## 1.0.2 (2021-03-13) 25 | 26 | - Specify bounded requirements for `@js-from-routes/client`. 27 | 28 | ## [1.0.1](https://github.com/ElMassimo/js_from_routes/compare/axios@1.0.0...axios@1.0.1) (2021-03-13) 29 | 30 | - Ensure `definePathHelper` is exported. 31 | 32 | ## [1.0.0](https://github.com/ElMassimo/js_from_routes/tree/axios%401.0.0) 33 | -------------------------------------------------------------------------------- /packages/axios/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Máximo Mussini 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/axios/README.md: -------------------------------------------------------------------------------- 1 |

@js-from-routes/axios

2 | 3 |

Define path helpers to make API requests or interpolate URLs

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 |
15 | 16 | [client]: https://github.com/ElMassimo/js_from_routes/tree/main/packages/client 17 | [js_from_routes]: https://github.com/ElMassimo/js_from_routes 18 | [axios]: https://github.com/axios/axios 19 | 20 | This package extends [@js-from-routes/client][client] to use [Axios]. 21 | 22 | It's useful when already using [axios], or when you have a complex API that requires [interceptors](https://github.com/axios/axios#interceptors) or [different instances](https://github.com/axios/axios#creating-an-instance). 23 | 24 | ## Installation 💿 25 | 26 | ```bash 27 | npm i @js-from-routes/axios # yarn add @js-from-routes/axios 28 | ``` 29 | 30 | ## Usage 🚀 31 | 32 | ```ts 33 | import { formatUrl, request } from '@js-from-routes/axios' 34 | 35 | formatUrl('/video_clips/:id/download', { id: 5 }) == '/video_clips/5/download' 36 | 37 | const video = await request('get', '/video_clips/:id', { id: 5 }) 38 | ``` 39 | 40 | ## License 41 | 42 | This library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 43 | -------------------------------------------------------------------------------- /packages/axios/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-from-routes/axios", 3 | "description": "Make API requests to a Rails apps with ease.", 4 | "version": "1.0.5", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "license": "MIT", 10 | "author": "Máximo Mussini ", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ElMassimo/js_from_routes" 14 | }, 15 | "homepage": "https://github.com/ElMassimo/js_from_routes", 16 | "bugs": "https://github.com/ElMassimo/js_from_routes/issues", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "test": "jest", 22 | "clean": "rm -rf ./dist", 23 | "dev": "npm run build -- --watch", 24 | "build": "tsup src/index.ts --dts --format cjs,esm", 25 | "prerelease": "npm run build", 26 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag axios@$PACKAGE_VERSION && git push --tags" 27 | }, 28 | "peerDependencies": { 29 | "@js-from-routes/client": "^1.0.0", 30 | "axios": "^1.8.3" 31 | }, 32 | "devDependencies": { 33 | "@js-from-routes/client": "^1.0.0", 34 | "axios": "^1.8.3", 35 | "tsup": "^5.12.1", 36 | "typescript": "^4.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/axios/src/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import type { AxiosRequestConfig, AxiosInstance, ResponseType, AxiosResponse } from 'axios' 3 | import { definePathHelper, formatUrl, request, Config } from '@js-from-routes/client' 4 | import type { ResponseAs, FetchOptions, ResponseError } from '@js-from-routes/client' 5 | 6 | /** 7 | * Allows modifying the Axios instance used to make the requests. 8 | */ 9 | const AxiosConfig = { 10 | instance: axios as AxiosInstance, 11 | } 12 | 13 | /** 14 | * Unwrap the response based on the `responseAs` value in the request. 15 | * @returns json, text, or the response. 16 | */ 17 | async function unwrapResponse (response: AxiosResponse, responseAs: ResponseAs) { 18 | Config.withResponse(response as unknown as Response) 19 | return responseAs === 'response' ? response : response.data 20 | } 21 | 22 | /** 23 | * Replace the default strategy which uses fetch to use Axios. 24 | */ 25 | async function fetch (args: FetchOptions) { 26 | const { responseAs, ...options } = args 27 | const responseType = responseAs === 'response' ? undefined : responseAs.toLowerCase() as ResponseType 28 | const config = { responseType, ...options } 29 | 30 | return AxiosConfig.instance.request(config as AxiosRequestConfig) 31 | .catch(error => Config.onResponseError(error as ResponseError)) 32 | } 33 | 34 | // NOTE: Replace the original `fetch` and `unwrapResponse`. 35 | Object.assign(Config, { fetch, unwrapResponse }) 36 | 37 | export { 38 | AxiosConfig, 39 | Config, 40 | definePathHelper, 41 | formatUrl, 42 | request, 43 | } 44 | -------------------------------------------------------------------------------- /packages/axios/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { definePathHelper, request, formatUrl } from '../src' 3 | 4 | describe('formatUrl', () => { 5 | it('is exported correctly', () => { 6 | expect(formatUrl('/users/:id', { id: '5' })).toEqual('/users/5') 7 | }) 8 | }) 9 | 10 | describe('request', () => { 11 | it('can unwrap a JSON response', async () => { 12 | expect(await request('get', 'https://pokeapi.co/api/v2/pokemon/:pokemon', { pokemon: 'pikachu' })).toMatchObject({ 13 | name: 'pikachu', 14 | }) 15 | }) 16 | 17 | it('can return the raw response', async () => { 18 | const fakeFetch = async (...args: any[]) => ({ status: 200, body: args }) 19 | expect(await request('get', '/videos/:id/download', { id: 2, fetch: fakeFetch, responseAs: 'response' })).toEqual({ 20 | status: 200, 21 | body: [{ 22 | method: 'GET', 23 | responseAs: 'response', 24 | url: '/videos/2/download', 25 | data: undefined, 26 | headers: { 27 | Accept: 'application/json', 28 | 'Content-Type': 'application/json', 29 | 'X-CSRF-Token': undefined, 30 | }, 31 | }], 32 | }) 33 | }) 34 | }) 35 | 36 | describe('definePathHelper', () => { 37 | it('returns a path helper with all the properties', async () => { 38 | const helper = definePathHelper('get', '/videos/:id/download') 39 | 40 | expect(helper.httpMethod).toEqual('get') 41 | expect(helper.pathTemplate).toEqual('/videos/:id/download') 42 | expect(helper.path({ i: 2, id: 5 })).toEqual('/videos/5/download') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /packages/client/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.4](https://github.com/ElMassimo/js_from_routes/compare/client@1.0.3...client@1.0.4) (2023-03-30) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * obtaining csrf token from response headers ([#38](https://github.com/ElMassimo/js_from_routes/issues/38)) ([76ee563](https://github.com/ElMassimo/js_from_routes/commit/76ee5631896e9e0190c9fac78e475cc2c77b2795)) 7 | 8 | 9 | 10 | ## [1.0.3](https://github.com/ElMassimo/js_from_routes/compare/client@1.0.2...client@1.0.3) (2022-03-28) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * avoid sending header for csrf token when empty ([2481a9f](https://github.com/ElMassimo/js_from_routes/commit/2481a9f1a05ee319716efa1d6de112bd34b96afe)) 16 | * Ensure packages are specified by using peerDependencies ([24b4918](https://github.com/ElMassimo/js_from_routes/commit/24b49183e3b6c7169b85eb0c0b06272b16455920)) 17 | 18 | 19 | 20 | ## [1.0.2](https://github.com/ElMassimo/js_from_routes/compare/client@1.0.1...client@1.0.2) (2021-03-14) 21 | 22 | 23 | ### Features 24 | 25 | * Add baseUrl configuration option to target APIs in a different domain ([babeae8](https://github.com/ElMassimo/js_from_routes/commit/babeae83294efe58c4fa6bea0d76b5e146b0b92a)) 26 | 27 | 28 | 29 | ## 1.0.1 (2021-03-13) 30 | 31 | - Specify bounded requirements for `@js-from-routes/core`. 32 | 33 | ## [1.0.0](https://github.com/ElMassimo/js_from_routes/tree/client%401.0.0) 34 | -------------------------------------------------------------------------------- /packages/client/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Máximo Mussini 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 |

@js-from-routes/client

2 | 3 |

Define path helpers to make API requests or interpolate URLs

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 |
15 | 16 | [js_from_routes]: https://github.com/ElMassimo/js_from_routes 17 | 18 | ## Installation 💿 19 | 20 | ```bash 21 | npm i @js-from-routes/client # yarn add @js-from-routes/client 22 | ``` 23 | 24 | ## Usage 🚀 25 | 26 | ```ts 27 | import { formatUrl, request } from '@js-from-routes/client' 28 | 29 | formatUrl('/video_clips/:id/download', { id: 5 }) == '/video_clips/5/download' 30 | 31 | const video = await request('get', '/video_clips/:id', { id: 5 }) 32 | ``` 33 | 34 | ## License 35 | 36 | This library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 37 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-from-routes/client", 3 | "description": "Make API requests to a Rails apps with ease.", 4 | "version": "1.0.4", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "license": "MIT", 10 | "author": "Máximo Mussini ", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ElMassimo/js_from_routes" 14 | }, 15 | "homepage": "https://github.com/ElMassimo/js_from_routes", 16 | "bugs": "https://github.com/ElMassimo/js_from_routes/issues", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "test": "jest", 22 | "clean": "rm -rf ./dist", 23 | "dev": "npm run build -- --watch", 24 | "build": "tsup src/index.ts --dts --format cjs,esm", 25 | "prerelease": "npm run build", 26 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag client@$PACKAGE_VERSION && git push --tags" 27 | }, 28 | "dependencies": { 29 | "@js-from-routes/core": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "tsup": "^5.12.1", 33 | "typescript": "^4.2.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/client/src/api.ts: -------------------------------------------------------------------------------- 1 | import type { UrlOptions } from '@js-from-routes/core' 2 | import { formatUrl as coreFormatUrl } from '@js-from-routes/core' 3 | import type { Method, Options, PathHelper } from './types' 4 | import { Config } from './config' 5 | 6 | /** 7 | * Defines a path helper that can make a request or interpolate a URL path. 8 | * 9 | * @param {Method} method An HTTP method 10 | * @param {string} pathTemplate The path with params placeholders (if any). 11 | */ 12 | function definePathHelper (method: Method, pathTemplate: string): PathHelper { 13 | const helper = (options?: Options) => request(method, pathTemplate, options) as Promise 14 | helper.path = (options?: UrlOptions) => formatUrl(pathTemplate, options) 15 | helper.pathTemplate = pathTemplate 16 | helper.httpMethod = method 17 | return helper 18 | } 19 | 20 | /** 21 | * Formats a url, replacing segments like /:id/ with the parameter of that name. 22 | * @param {string} urlTemplate A template URL with placeholders for params 23 | * @param {Query} query Query parameters to append to the URL 24 | * @param {Params} params Parameters to interpolateUrl in the URL placeholders 25 | * @return {string} The interpolated URL with the provided query params (if any) 26 | * @example 27 | * formatUrl('/users/:id', { id: '5' }) returns '/users/5' 28 | * formatUrl('/users', { query: { id: '5' } }) returns '/users?id=5' 29 | */ 30 | function formatUrl (urlTemplate: string, options: UrlOptions = {}): string { 31 | let base = urlTemplate.startsWith('/') ? Config.baseUrl : '' 32 | if (base.endsWith('/')) base = base.slice(0, base.length - 1) 33 | return coreFormatUrl(`${base}${urlTemplate}`, options) 34 | } 35 | 36 | /** 37 | * Makes an AJAX request to the API server. 38 | * @param {Method} method HTTP request method 39 | * @param {string} url May be a template with param placeholders 40 | * @param {Options} options Can optionally pass params as a shorthand 41 | * @return {Promise} The result of the request 42 | */ 43 | async function request (_method: Method, url: string, options: Options = {}): Promise { 44 | const { 45 | data, 46 | deserializeData = Config.deserializeData, 47 | fetch = Config.fetch, 48 | fetchOptions, 49 | headers, 50 | params = (options.data || options), 51 | responseAs = 'json', 52 | serializeData = Config.serializeData, 53 | } = options 54 | 55 | const method = (options.method || _method).toUpperCase() as Method 56 | if (data && (method === 'HEAD' || method === 'GET')) 57 | throw Object.assign(new Error('Request with GET/HEAD method cannot have data.'), { data }) 58 | 59 | url = formatUrl(url, params) 60 | 61 | const requestOptions = { 62 | method, 63 | url, 64 | data: serializeData(data), 65 | responseAs, 66 | headers: { ...Config.headers({ method, url, options }), ...headers }, 67 | ...fetchOptions, 68 | } 69 | 70 | return fetch(Config.modifyRequest(requestOptions) || requestOptions) 71 | .then(async (response: Response) => await Config.unwrapResponse(response, responseAs)) 72 | .then((data: any) => responseAs === 'json' ? deserializeData(data) : data) 73 | } 74 | 75 | export { 76 | definePathHelper, 77 | formatUrl, 78 | request, 79 | } 80 | -------------------------------------------------------------------------------- /packages/client/src/config.ts: -------------------------------------------------------------------------------- 1 | import { camelCase, snakeCase, deepConvertKeys } from '@js-from-routes/core' 2 | 3 | import type { FetchOptions, HeaderOptions, ResponseAs } from './types' 4 | 5 | export interface ResponseError extends Error { 6 | body?: any 7 | response?: Response 8 | } 9 | 10 | /** 11 | * You may customize these options to configure how requests are sent. 12 | */ 13 | export const Config = { 14 | /** 15 | * An optional base URL when the API is hosted on a different domain. 16 | */ 17 | baseUrl: '', 18 | 19 | /** 20 | * The function used to transform the data received from the server. 21 | * @default camelizeKeys 22 | */ 23 | deserializeData: (data: any) => deepConvertKeys(data, camelCase), 24 | 25 | /** 26 | * The function used to convert the data before sending it to the server. 27 | * @default snakeCaseKeys 28 | */ 29 | serializeData: (data: any) => deepConvertKeys(data, snakeCase), 30 | 31 | /** 32 | * The CSRF token to use in the requests. 33 | * @see Config.getCSRFToken 34 | */ 35 | csrfToken: '', 36 | 37 | /** 38 | * Strategy to obtain the CSRF token. 39 | */ 40 | getCSRFToken () { 41 | return Config.csrfToken || document.querySelector('meta[name=csrf-token]')?.content 42 | }, 43 | 44 | /** 45 | * Allows to replace the default strategy to use Axios or other libraries. 46 | */ 47 | async fetch (args: FetchOptions): Promise { 48 | const { url, data, responseAs, ...options } = args 49 | 50 | const requestInit: RequestInit = { 51 | body: data === undefined ? data : JSON.stringify(data), 52 | credentials: 'include', 53 | redirect: 'follow', 54 | ...options, 55 | } 56 | 57 | return fetch(url, requestInit) 58 | .then(async (response) => { 59 | if (response.status >= 200 && response.status < 300) return response 60 | throw await Config.unwrapResponseError(response, responseAs) 61 | }) 62 | .catch(Config.onResponseError) 63 | }, 64 | 65 | /** 66 | * Default headers to be sent in the request, JSON is used as the default MIME. 67 | */ 68 | headers (_requestInfo: HeaderOptions) { 69 | const csrfToken = Config.getCSRFToken() 70 | return { 71 | Accept: 'application/json', 72 | 'Content-Type': 'application/json', 73 | ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}), 74 | } 75 | }, 76 | 77 | /** 78 | * Allows changes to the request data before it is sent to the server. 79 | */ 80 | modifyRequest (request: FetchOptions): FetchOptions | undefined { 81 | return request 82 | }, 83 | 84 | /** 85 | * Allows to intercept errors globally. 86 | */ 87 | async onResponseError (error: ResponseError) { 88 | throw error 89 | }, 90 | 91 | /** 92 | * Unwrap the response based on the `responseAs` value in the request. 93 | * @returns json, text, or the response. 94 | */ 95 | async unwrapResponse (response: Response, responseAs: ResponseAs) { 96 | Config.withResponse(response) 97 | 98 | if (responseAs === 'response') 99 | return response 100 | 101 | if (response.status === 204) 102 | return null 103 | 104 | return await response[responseAs]().catch(() => null) 105 | }, 106 | 107 | /** 108 | * Similar to unwrapResponse, but when a request fails. 109 | */ 110 | async unwrapResponseError (response: Response, responseAs: ResponseAs) { 111 | const error = new Error(response.statusText) as ResponseError 112 | error.response = response 113 | try { 114 | const body = await Config.unwrapResponse(response, responseAs) 115 | error.body = responseAs === 'json' ? Config.deserializeData(body) : body 116 | } 117 | catch {} 118 | return error 119 | }, 120 | 121 | /** 122 | * Convenience hook to extract headers from the response. 123 | */ 124 | withResponse (response: Response) { 125 | const headers = response.headers || {} 126 | 127 | // Extract a CSRF token provided in the headers. 128 | const updatedToken = 'get' in headers 129 | ? headers.get('x-csrf-token') 130 | : headers['x-csrf-token'] 131 | 132 | if (updatedToken) 133 | Config.csrfToken = updatedToken 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api' 2 | export * from './config' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /packages/client/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { UrlOptions } from '@js-from-routes/core' 2 | 3 | export type Method = 4 | | 'GET' | 'get' 5 | | 'DELETE' | 'delete' 6 | | 'HEAD' | 'head' 7 | | 'OPTIONS' | 'options' 8 | | 'POST' | 'post' 9 | | 'PUT' | 'put' 10 | | 'PATCH' | 'patch' 11 | | 'PURGE' | 'purge' 12 | | 'LINK' | 'link' 13 | | 'UNLINK' | 'unlink' 14 | 15 | export type ResponseAs = 'response' | 'json' | 'text' | 'blob' | 'formData' | 'arrayBuffer' 16 | 17 | /** 18 | * Options for `fetch` that can be customized. 19 | */ 20 | export type BaseFetchOptions = Omit 21 | 22 | /** 23 | * Passed to the `fetch` function when performing a request. 24 | */ 25 | export interface FetchOptions extends BaseFetchOptions { 26 | url: string 27 | method: Method 28 | data: any 29 | responseAs: ResponseAs 30 | headers: Record 31 | } 32 | 33 | /** 34 | * 35 | */ 36 | export interface RequestOptions { 37 | /** 38 | * The query string parameters to interpolate in the URL. 39 | */ 40 | params?: UrlOptions 41 | 42 | /** 43 | * The body of the request, should be a plain Object. 44 | */ 45 | data?: any 46 | 47 | /** 48 | * The function used to transform the data received from the server. 49 | * @default camelizeKeys 50 | */ 51 | deserializeData?: (data: any) => any 52 | 53 | /** 54 | * Use a different function to make the request. 55 | * @default Config.fetch 56 | */ 57 | fetch: (options: FetchOptions) => Promise 58 | 59 | /** 60 | * Additional options for the `fetch` function. 61 | */ 62 | fetchOptions: BaseFetchOptions 63 | 64 | /** 65 | * Override the default method for the path helper. 66 | */ 67 | method: Method 68 | 69 | /** 70 | * The function used to convert the data before sending it to the server. 71 | * @default snakeCaseKeys 72 | */ 73 | serializeData?: (data: any) => any 74 | 75 | /** 76 | * What kind of response is expected. Defaults to `json`. `response` will 77 | * return the raw response from `fetch`. 78 | * @default 'json' 79 | */ 80 | responseAs?: ResponseAs 81 | 82 | /** 83 | * Headers to send in the request. 84 | */ 85 | headers?: Record 86 | } 87 | 88 | export type Options = RequestOptions | UrlOptions 89 | 90 | export interface HeaderOptions { 91 | method: Method 92 | url: string 93 | options: Options 94 | } 95 | 96 | export interface PathHelper { 97 | (options?: Options): Promise 98 | path: (params?: UrlOptions) => string 99 | pathTemplate: string 100 | httpMethod: Method 101 | } 102 | -------------------------------------------------------------------------------- /packages/client/test/api.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, afterEach, it, expect } from 'vitest' 2 | import { definePathHelper, request, formatUrl, Config } from '../src' 3 | 4 | describe('formatUrl', () => { 5 | afterEach(() => { 6 | Config.baseUrl = '' 7 | }) 8 | 9 | it('is exported correctly', () => { 10 | expect(formatUrl('/users/:id', { id: '5' })).toEqual('/users/5') 11 | }) 12 | 13 | it('uses baseUrl correctly', () => { 14 | Config.baseUrl = 'https://pokeapi.co/api/v2' 15 | expect(formatUrl('/pokemon/:pokemon', { pokemon: 'pikachu' })).toEqual('https://pokeapi.co/api/v2/pokemon/pikachu') 16 | 17 | Config.baseUrl = 'https://pokeapi.co/api/v2/' 18 | expect(formatUrl('/pokemon/:pokemon', { pokemon: 'pikachu' })).toEqual('https://pokeapi.co/api/v2/pokemon/pikachu') 19 | 20 | expect(formatUrl('https://pokeapi.co/api/v3/pokemon/:pokemon', { pokemon: 'pikachu' })).toEqual('https://pokeapi.co/api/v3/pokemon/pikachu') 21 | }) 22 | }) 23 | 24 | describe('request', () => { 25 | it('can unwrap a JSON response', async () => { 26 | expect(await request('get', 'https://pokeapi.co/api/v2/pokemon/:pokemon', { pokemon: 'pikachu' })).toMatchObject({ 27 | name: 'pikachu', 28 | }) 29 | }) 30 | 31 | it('can return the raw response', async () => { 32 | const fakeFetch = async (...args: any[]) => ({ status: 200, body: args }) 33 | expect(await request('get', '/videos/:id/download', { id: 2, fetch: fakeFetch, responseAs: 'response' })).toEqual({ 34 | status: 200, 35 | body: [{ 36 | method: 'GET', 37 | responseAs: 'response', 38 | url: '/videos/2/download', 39 | data: undefined, 40 | headers: { 41 | Accept: 'application/json', 42 | 'Content-Type': 'application/json', 43 | 'X-CSRF-Token': undefined, 44 | }, 45 | }], 46 | }) 47 | }) 48 | }) 49 | 50 | describe('definePathHelper', () => { 51 | it('returns a path helper with all the properties', async () => { 52 | const helper = definePathHelper('get', '/videos/:id/download') 53 | 54 | expect(helper.httpMethod).toEqual('get') 55 | expect(helper.pathTemplate).toEqual('/videos/:id/download') 56 | expect(helper.path({ i: 2, id: 5 })).toEqual('/videos/5/download') 57 | 58 | const fakeFetch = async (...args: any[]) => ({ status: 200, json: () => Promise.all(args) }) 59 | expect(await helper({ id: 2, fetch: fakeFetch })).toEqual([{ 60 | data: undefined, 61 | headers: { 62 | Accept: 'application/json', 63 | ContentType: 'application/json', 64 | XCSRFToken: undefined, 65 | }, 66 | method: 'GET', 67 | responseAs: 'json', 68 | url: '/videos/2/download', 69 | }]) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2](https://github.com/ElMassimo/js_from_routes/compare/core@1.0.1...core@1.0.2) (2022-03-28) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * interpolation for parameters before optional params ([#27](https://github.com/ElMassimo/js_from_routes/issues/27)) ([da2e7d4](https://github.com/ElMassimo/js_from_routes/commit/da2e7d4fe1620bf8692d8d82a73456203e0b5ab0)) 7 | 8 | 9 | 10 | ## [1.0.1](https://github.com/ElMassimo/js_from_routes/compare/core@1.0.0...core@1.0.1) (2021-03-14) 11 | 12 | - dx: Print `none` when no parameters are passed and there are missing parameters. 13 | 14 | ## [1.0.0](https://github.com/ElMassimo/js_from_routes/tree/core%401.0.0) 15 | -------------------------------------------------------------------------------- /packages/core/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Máximo Mussini 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 |

@js-from-routes/core

2 | 3 |

URL interpolation and JSON transform utilities

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 |
15 | 16 | [js_from_routes]: https://github.com/ElMassimo/js_from_routes 17 | 18 | ## Installation 💿 19 | 20 | Normally you wouldn't need to install it manually, instead follow the instructions 21 | of [js_from_routes][js_from_routes]. 22 | 23 | ```bash 24 | npm i @js-from-routes/core # yarn add @js-from-routes/core 25 | ``` 26 | 27 | ## Usage 🚀 28 | 29 | ```ts 30 | import { formatUrl } from '@js-from-routes/core' 31 | 32 | formatUrl('/video_clips/:id/download', { id: 5 }) == '/video_clips/5/download' 33 | ``` 34 | 35 | ## License 36 | 37 | This library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 38 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-from-routes/core", 3 | "description": "Utilities for formatting URLs and transforming responses.", 4 | "version": "1.0.2", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "license": "MIT", 10 | "author": "Máximo Mussini ", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ElMassimo/js_from_routes" 14 | }, 15 | "homepage": "https://github.com/ElMassimo/js_from_routes", 16 | "bugs": "https://github.com/ElMassimo/js_from_routes/issues", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "test": "jest", 22 | "clean": "rm -rf ./dist", 23 | "dev": "npm run build -- --watch", 24 | "build": "tsup src/index.ts --dts --format cjs,esm", 25 | "prerelease": "npm run build", 26 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag core@$PACKAGE_VERSION && git push --tags" 27 | }, 28 | "devDependencies": { 29 | "tsup": "^5.12.1", 30 | "typescript": "^4.2.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './urls' 2 | export * from './utils' 3 | -------------------------------------------------------------------------------- /packages/core/src/urls.ts: -------------------------------------------------------------------------------- 1 | import { deepConvertKeys, escapeRegExp, forEach, isURLSearchParams, isDate, isObject, snakeCase } from './utils' 2 | 3 | export type Query = Record 4 | export type Params = Record 5 | 6 | export type UrlOptions = { 7 | query?: Query 8 | [key: string]: any 9 | } 10 | 11 | const REQUIRED_PARAMETER = /:[^\W\d]+/g 12 | const OPTIONAL_PARAMETER = /\(\/:[^\W\d]+\)/g 13 | 14 | // Internal: Encodes a value to be used as an URL query parameter. 15 | function encode (val: any): string { 16 | return encodeURIComponent(val) 17 | .replace(/%3A/gi, ':') 18 | .replace(/%24/g, '$') 19 | .replace(/%2C/gi, ',') 20 | .replace(/%20/g, '+') 21 | .replace(/%5B/gi, '[') 22 | .replace(/%5D/gi, ']') 23 | } 24 | 25 | /** 26 | * Serializes the provided parameters as an URL query string. 27 | */ 28 | function serializeQuery (params: Query): string { 29 | const parts: string[] = [] 30 | 31 | forEach(params, (val, key) => { 32 | if (val === null || typeof val === 'undefined') 33 | return 34 | 35 | if (Array.isArray(val)) 36 | key = `${key}[]` 37 | else 38 | val = [val] 39 | 40 | forEach(val, (v) => { 41 | if (isDate(v)) 42 | v = v.toISOString() 43 | else if (isObject(v)) 44 | v = JSON.stringify(v) 45 | 46 | parts.push(`${encode(key)}=${encode(v)}`) 47 | }) 48 | }) 49 | 50 | return parts.join('&') 51 | } 52 | 53 | /** 54 | * Build a URL by appending params to the end 55 | * 56 | * @param {string} url The base of the url (e.g., http://www.google.com) 57 | * @param {Params} params The params to be appended 58 | * @returns {string} The formatted url 59 | */ 60 | function buildURL (url: string, params: Query | undefined): string { 61 | if (!params) return url 62 | 63 | const queryStr = isURLSearchParams(params) 64 | ? params.toString() 65 | : serializeQuery(deepConvertKeys(params, snakeCase)) 66 | 67 | if (queryStr) { 68 | const hashmarkIndex = url.indexOf('#') 69 | if (hashmarkIndex !== -1) 70 | url = url.slice(0, hashmarkIndex) 71 | 72 | url += `${url.includes('?') ? '&' : '?'}${queryStr}` 73 | } 74 | 75 | return url 76 | } 77 | 78 | /** 79 | * Replaces any placeholder in the string with the provided parameters. 80 | * 81 | * @param {string} template The URL template with `:placeholders` 82 | * @param {Params} params Parameters to inject in the placeholders 83 | * @return {string} The resulting URL with replaced placeholders 84 | */ 85 | export function interpolateUrl (template: string, params: Params): string { 86 | let value = template.toString() 87 | Object.entries(params).forEach(([paramName, paramValue]) => { 88 | paramName = snakeCase(paramName) 89 | value = value 90 | .replace(new RegExp(escapeRegExp(`(/:${paramName})`), 'g'), `/${paramValue}`) 91 | .replace(new RegExp(`:${escapeRegExp(paramName)}(\\/|\\.|\\(|$)`, 'g'), `${paramValue}$1`) 92 | }) 93 | 94 | // Remove any optional path if the parameters were not provided. 95 | value = value.replace(OPTIONAL_PARAMETER, '') 96 | 97 | const missingParams = value.match(REQUIRED_PARAMETER) 98 | if (missingParams) { 99 | const missing = missingParams.join(', ') 100 | const provided = (params && Object.keys(params).join(', ')) || 'none.' 101 | throw new TypeError(`Missing URL Parameter ${missing} for ${template}. Params provided: ${provided}`) 102 | } 103 | return value 104 | } 105 | 106 | /** 107 | * Formats a url, replacing segments like /:id/ with the parameter of that name. 108 | * @param {string} urlTemplate A template URL with placeholders for params 109 | * @param {Query} query Query parameters to append to the URL 110 | * @param {Params} params Parameters to interpolateUrl in the URL placeholders 111 | * @return {string} The interpolated URL with the provided query params (if any) 112 | * @example 113 | * formatUrl('/users/:id', { id: '5' }) returns '/users/5' 114 | * formatUrl('/users', { query: { id: '5' } }) returns '/users?id=5' 115 | */ 116 | export function formatUrl (urlTemplate: string, { query, ...params }: UrlOptions = {}): string { 117 | return buildURL(interpolateUrl(urlTemplate, params), query) 118 | } 119 | -------------------------------------------------------------------------------- /packages/core/test/urls.test.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { describe, it, expect } from 'vitest' 3 | import { interpolateUrl, formatUrl } from '../src' 4 | 5 | describe('interpolateUrl', () => { 6 | it('replaces parameters correctly', () => { 7 | expect(interpolateUrl('/video_clips/:video_clip_id/comments/:id', { video: '5', videoClipId: '5', id: '3' })) 8 | .toEqual('/video_clips/5/comments/3') 9 | }) 10 | 11 | it('replaces duplicate parameters', () => { 12 | expect(interpolateUrl('/video_clips/:id/comments/:id', { video: '5', videoClipId: '5', id: '3' })) 13 | .toEqual('/video_clips/3/comments/3') 14 | }) 15 | 16 | it('fails when there are missing parameters', () => { 17 | expect(() => interpolateUrl('/video_clips/:video_clip_id/comments/:id', { video: '5', videoClipId: '5' })) 18 | .toThrow(/Missing URL Parameter :id/) 19 | 20 | expect(() => interpolateUrl('/video_clips/:video_clip_id/comments/:id', { id: '5' })) 21 | .toThrow(/Params provided: id/) 22 | }) 23 | 24 | it('deals with optional parameters', () => { 25 | expect(interpolateUrl('(/:locale)/users/:id', { loc: '9000', locale: 'en', id: '5' })) 26 | .toEqual('/en/users/5') 27 | 28 | expect(interpolateUrl('(/:locale)/users/:id', { id: '5', loc: '9000' })) 29 | .toEqual('/users/5') 30 | 31 | expect(interpolateUrl('/users/:id(/:page)', { id: '5' })) 32 | .toEqual('/users/5') 33 | }) 34 | }) 35 | 36 | describe('formatUrl', () => { 37 | it('replaces parameters correctly', () => { 38 | expect(formatUrl('/users/:id', { id: '5' })).toEqual('/users/5') 39 | }) 40 | 41 | it('uses snake case for url parameters', () => { 42 | expect(formatUrl('/video_clip/:video_clip_id', { videoClipId: '1' })).toEqual('/video_clip/1') 43 | }) 44 | 45 | it('adds query parameters when it has to', () => { 46 | expect(formatUrl('/users/:id', { query: { singleParam: '2' }, id: '1' })) 47 | .toEqual('/users/1?single_param=2') 48 | 49 | expect(formatUrl('/users/:id', { query: { multiParam: ['2', '3'] }, id: '1' })) 50 | .toEqual('/users/1?multi_param[]=2&multi_param[]=3') 51 | 52 | const objectParam = { a: '2', b: '3' } 53 | const objectQueryUrl = formatUrl('/users/:id', { query: { objectParam }, id: '1' }) 54 | expect(objectQueryUrl) 55 | .toEqual('/users/1?object_param=%7B%22a%22:%222%22,%22b%22:%223%22%7D') 56 | 57 | const url = new URL(objectQueryUrl, 'http://example.com') 58 | expect(url.searchParams.get('object_param')).toEqual(JSON.stringify(objectParam)) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /packages/core/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { deepConvertKeys, snakeCase, camelCase } from '../src' 3 | 4 | function numberCase (val: string) { 5 | return snakeCase(val).replace(/_([0-9]+?)_/g, '$1_') 6 | } 7 | 8 | const camelCaseObject = { 9 | collectionCount: 3, 10 | collectionItems: [ 11 | { itemName: 'First', itemChildren: [{ childName: 'First Child' }] }, 12 | { itemName: 'Second', itemChildren: [{ childName: 'Second Child' }] }, 13 | { itemName: 'Third', itemChildren: [{ childName: 'Third Child' }] }, 14 | ], 15 | } 16 | 17 | const snakeCaseObject = { 18 | collection_count: 3, 19 | collection_items: [ 20 | { item_name: 'First', item_children: [{ child_name: 'First Child' }] }, 21 | { item_name: 'Second', item_children: [{ child_name: 'Second Child' }] }, 22 | { item_name: 'Third', item_children: [{ child_name: 'Third Child' }] }, 23 | ], 24 | } 25 | 26 | describe('deepConvertKeys', () => { 27 | it('converts nested objects', () => { 28 | expect(deepConvertKeys(snakeCaseObject, camelCase)).toEqual(camelCaseObject) 29 | expect(deepConvertKeys(camelCaseObject, snakeCase)).toEqual(snakeCaseObject) 30 | }) 31 | 32 | it('can be safely applied twice', () => { 33 | expect(deepConvertKeys(camelCaseObject, camelCase)).toEqual(camelCaseObject) 34 | expect(deepConvertKeys(snakeCaseObject, snakeCase)).toEqual(snakeCaseObject) 35 | }) 36 | 37 | it('accepts custom functions', () => { 38 | Object.assign(camelCaseObject, { three3ItemsCollection: true }) 39 | Object.assign(snakeCaseObject, { three3_items_collection: true }) 40 | expect(deepConvertKeys(camelCaseObject, numberCase)).toEqual(snakeCaseObject) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /packages/inertia/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.10.2](https://github.com/ElMassimo/js_from_routes/compare/inertia@1.10.1...inertia@1.10.2) (2022-05-23) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * update peer deps to inertia@0.11 ([1c22dad](https://github.com/ElMassimo/js_from_routes/commit/1c22dade7b5a747bc2946f6db2aedb47cd783b6a)) 7 | 8 | 9 | 10 | ## [1.10.1](https://github.com/ElMassimo/js_from_routes/compare/inertia@1.10.0...inertia@1.10.1) (2022-05-23) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * export types for inertia path helpers ([ee3e53f](https://github.com/ElMassimo/js_from_routes/commit/ee3e53f6f9040e5e62d5211869fb1c9f7cc252f6)) 16 | 17 | 18 | 19 | ## [1.0.3](https://github.com/ElMassimo/js_from_routes/compare/inertia@1.0.2...inertia@1.0.3) (2021-03-16) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * Ensure packages are specified by using peerDependencies ([24b4918](https://github.com/ElMassimo/js_from_routes/commit/24b49183e3b6c7169b85eb0c0b06272b16455920)) 25 | 26 | 27 | 28 | ## [1.0.2](https://github.com/ElMassimo/js_from_routes/compare/inertia@1.0.1...inertia@1.0.2) (2021-03-16) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * Avoid using `visit` directly to avoid missing this when doing `setPage` ([d8068ea](https://github.com/ElMassimo/js_from_routes/commit/d8068ea90a82ac8a05901c3ae81ad99df0848429)) 34 | 35 | 36 | 37 | ## [1.0.1](https://github.com/ElMassimo/js_from_routes/compare/inertia@1.0.0...inertia@1.0.1) (2021-03-16) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * Ensure callbacks are passed to Inertia.js ([c456b36](https://github.com/ElMassimo/js_from_routes/commit/c456b36e6f80927fa3f10999d46f3c91c34a408a)) 43 | 44 | 45 | 46 | ## [1.0.0](https://github.com/ElMassimo/js_from_routes/tree/inertia%401.0.0) 47 | -------------------------------------------------------------------------------- /packages/inertia/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Máximo Mussini 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/inertia/README.md: -------------------------------------------------------------------------------- 1 |

@js-from-routes/inertia

2 | 3 |

Define path helpers to make API requests or interpolate URLs

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 |
15 | 16 | [client]: https://github.com/ElMassimo/js_from_routes/tree/main/packages/client 17 | [js_from_routes]: https://github.com/ElMassimo/js_from_routes 18 | [inertia.js]: https://github.com/inertiajs/inertia 19 | 20 | This package extends [@js-from-routes/client][client] to use [Inertia.js]. 21 | 22 | It's useful when using [Inertia.js], since it allows request helpers to handle forms as well. 23 | 24 | ## Installation 💿 25 | 26 | ```bash 27 | npm i @js-from-routes/inertia # yarn add @js-from-routes/inertia 28 | ``` 29 | 30 | ## Usage 🚀 31 | 32 | ```ts 33 | import { definePathHelper } from '@js-from-routes/inertia' 34 | 35 | const get = definePathHelper('get', '/video_clips/:id') 36 | 37 | get.path({ id: 5 }) == '/video_clips/5/download' 38 | 39 | const video = await get({ id: 5 }) 40 | ``` 41 | 42 | ## License 43 | 44 | This library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 45 | -------------------------------------------------------------------------------- /packages/inertia/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-from-routes/inertia", 3 | "description": "Make API requests to a Rails apps with ease.", 4 | "version": "1.10.2", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "license": "MIT", 10 | "author": "Máximo Mussini ", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ElMassimo/js_from_routes" 14 | }, 15 | "homepage": "https://github.com/ElMassimo/js_from_routes", 16 | "bugs": "https://github.com/ElMassimo/js_from_routes/issues", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "test": "jest", 22 | "clean": "rm -rf ./dist", 23 | "dev": "npm run build -- --watch", 24 | "build": "tsup src/index.ts --dts --format cjs,esm", 25 | "prerelease": "npm run build", 26 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag inertia@$PACKAGE_VERSION && git push --tags" 27 | }, 28 | "peerDependencies": { 29 | "@inertiajs/inertia": "^0.11", 30 | "@js-from-routes/core": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "@inertiajs/inertia": "^0.11", 34 | "@js-from-routes/core": "^1.0.0", 35 | "tsup": "^5.12.1", 36 | "typescript": "^4.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/inertia/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Inertia } from '@inertiajs/inertia' 2 | 3 | import { formatUrl } from '@js-from-routes/core' 4 | import { FormHelper, Method, Options, PathHelper, VisitOptions, UrlOptions } from './types' 5 | 6 | export * from './types' 7 | 8 | /** 9 | * Defines a path helper that can make a request or interpolate a URL path. 10 | * 11 | * @param {Method} method An HTTP method 12 | * @param {string} pathTemplate The path with params placeholders (if any). 13 | */ 14 | function definePathHelper (method: Method, pathTemplate: string): PathHelper { 15 | const helper = (options?: Options) => request(method, pathTemplate, options) as Promise 16 | helper.path = (options?: UrlOptions) => formatUrl(pathTemplate, options) 17 | helper.pathTemplate = pathTemplate 18 | helper.httpMethod = method 19 | return helper 20 | } 21 | 22 | /** 23 | * Returns true if the object is an Inertia.js form helper. 24 | */ 25 | function isFormHelper (val: any, method: Method): val is FormHelper { 26 | // eslint-disable-next-line no-prototype-builtins 27 | return val?.hasOwnProperty('data') && val[method] 28 | } 29 | 30 | /** 31 | * Makes an AJAX request to the API server. 32 | * @param {Method} method HTTP request method 33 | * @param {string} url May be a template with param placeholders 34 | * @param {Options} options Can optionally pass params as a shorthand 35 | * @return {Promise} The result of the request 36 | */ 37 | async function request (_method: Method, url: string, options: Options = {}): Promise { 38 | const { params = (options.data || options), data, form = data, ...otherOptions } = options 39 | 40 | const config = otherOptions as VisitOptions 41 | const method = (options.method || _method).toLowerCase() as Method 42 | url = formatUrl(url, params) 43 | 44 | if (isFormHelper(form, method)) return form[method](url, config) 45 | 46 | const args = method === 'delete' ? [{ ...options, data }] : [data, options] 47 | return Inertia[method](url, ...args) 48 | } 49 | 50 | export { 51 | definePathHelper, 52 | formatUrl, 53 | request, 54 | } 55 | -------------------------------------------------------------------------------- /packages/inertia/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { UrlOptions } from '@js-from-routes/core' 2 | import type { VisitOptions } from '@inertiajs/inertia' 3 | 4 | export { UrlOptions, VisitOptions } 5 | 6 | export type Method = 7 | | 'get' 8 | | 'delete' 9 | | 'post' 10 | | 'put' 11 | | 'patch' 12 | 13 | type RequestMethod = (url: string, config: VisitOptions) => Promise 14 | 15 | /** 16 | * An Inertia.js form helper. 17 | */ 18 | export interface FormHelper { 19 | get: RequestMethod 20 | delete: RequestMethod 21 | post: RequestMethod 22 | put: RequestMethod 23 | patch: RequestMethod 24 | } 25 | 26 | /** 27 | * Options that can be passed to the request method. 28 | */ 29 | export interface RequestOptions extends VisitOptions { 30 | /** 31 | * The query string parameters to interpolate in the URL. 32 | */ 33 | params?: UrlOptions 34 | 35 | /** 36 | * The body of the request, should be a plain Object or an Inertia.js form. 37 | */ 38 | data?: any 39 | 40 | /** 41 | * An Inertia.js form to submit in the request. 42 | */ 43 | form?: FormHelper 44 | } 45 | 46 | export type Options = RequestOptions | UrlOptions 47 | 48 | export interface PathHelper { 49 | (options?: Options): Promise 50 | path: (params?: UrlOptions) => string 51 | pathTemplate: string 52 | httpMethod: Method 53 | } 54 | -------------------------------------------------------------------------------- /packages/inertia/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { definePathHelper, formatUrl } from '../src' 3 | 4 | describe('formatUrl', () => { 5 | it('is exported correctly', () => { 6 | expect(formatUrl('/users/:id', { id: '5' })).toEqual('/users/5') 7 | }) 8 | }) 9 | 10 | describe('definePathHelper', () => { 11 | it('returns a path helper with all the properties', async () => { 12 | const helper = definePathHelper('get', '/videos/:id/download') 13 | 14 | expect(helper.httpMethod).toEqual('get') 15 | expect(helper.pathTemplate).toEqual('/videos/:id/download') 16 | expect(helper.path({ i: 2, id: 5 })).toEqual('/videos/5/download') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "npm -C core run 'build' && npm -C client run 'build' && npm -C axios run 'build' && npm -C redaxios run 'build' && npm -C inertia run 'build'", 5 | "dev": "pnpm -r --parallel --filter . run dev" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/redaxios/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.4](https://github.com/ElMassimo/js_from_routes/compare/redaxios@1.0.3...redaxios@1.0.4) (2021-03-16) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * Ensure callbacks are passed to Inertia.js ([c456b36](https://github.com/ElMassimo/js_from_routes/commit/c456b36e6f80927fa3f10999d46f3c91c34a408a)) 7 | * Ensure packages are specified by using peerDependencies ([24b4918](https://github.com/ElMassimo/js_from_routes/commit/24b49183e3b6c7169b85eb0c0b06272b16455920)) 8 | 9 | 10 | 11 | ## 1.0.3 (2021-03-13) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * Allow using responseAs: 'response' with Axios and Redaxios ([cdaf9cd](https://github.com/ElMassimo/js_from_routes/commit/cdaf9cd895407773851df4983108dcef1b0f6182)) 17 | 18 | 19 | 20 | ## 1.0.2 (2021-03-13) 21 | 22 | - Specify bounded requirements for `@js-from-routes/client`. 23 | 24 | ## [1.0.1](https://github.com/ElMassimo/js_from_routes/compare/redaxios@1.0.0...redaxios@1.0.1) (2021-03-13) 25 | 26 | - Ensure `definePathHelper` is exported. 27 | 28 | ## [1.0.0](https://github.com/ElMassimo/js_from_routes/tree/redaxios%401.0.0) 29 | -------------------------------------------------------------------------------- /packages/redaxios/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Máximo Mussini 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/redaxios/README.md: -------------------------------------------------------------------------------- 1 |

@js-from-routes/redaxios

2 | 3 |

Define path helpers to make API requests or interpolate URLs

4 | 5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 |
15 | 16 | [client]: https://github.com/ElMassimo/js_from_routes/tree/main/packages/client 17 | [js_from_routes]: https://github.com/ElMassimo/js_from_routes 18 | [redaxios]: https://github.com/developit/redaxios 19 | 20 | This package extends [@js-from-routes/client][client] to use [redaxios]. 21 | 22 | It's useful when already using [redaxios], for consistency when using the response object directly. 23 | 24 | ## Installation 💿 25 | 26 | ```bash 27 | npm i @js-from-routes/redaxios # yarn add @js-from-routes/redaxios 28 | ``` 29 | 30 | ## Usage 🚀 31 | 32 | ```ts 33 | import { formatUrl, request } from '@js-from-routes/redaxios' 34 | 35 | formatUrl('/video_clips/:id/download', { id: 5 }) == '/video_clips/5/download' 36 | 37 | const video = await request('get', '/video_clips/:id', { id: 5 }) 38 | ``` 39 | 40 | ## License 41 | 42 | This library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 43 | -------------------------------------------------------------------------------- /packages/redaxios/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-from-routes/redaxios", 3 | "description": "Make API requests to a Rails apps with ease.", 4 | "version": "1.0.4", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "type": "module", 9 | "license": "MIT", 10 | "author": "Máximo Mussini ", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ElMassimo/js_from_routes" 14 | }, 15 | "homepage": "https://github.com/ElMassimo/js_from_routes", 16 | "bugs": "https://github.com/ElMassimo/js_from_routes/issues", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "test": "jest", 22 | "clean": "rm -rf ./dist", 23 | "dev": "npm run build -- --watch", 24 | "build": "tsup src/index.ts --dts --format cjs,esm", 25 | "prerelease": "npm run build", 26 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag redaxios@$PACKAGE_VERSION && git push --tags" 27 | }, 28 | "peerDependencies": { 29 | "@js-from-routes/client": "^1.0.0", 30 | "redaxios": "^0.4.1" 31 | }, 32 | "devDependencies": { 33 | "@js-from-routes/client": "^1.0.0", 34 | "redaxios": "^0.4.1", 35 | "tsup": "^5.12.1", 36 | "typescript": "^4.2.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/redaxios/src/index.ts: -------------------------------------------------------------------------------- 1 | import redaxios, { Options, Response as RedaxiosResponse } from 'redaxios' 2 | import { definePathHelper, formatUrl, request, Config } from '@js-from-routes/client' 3 | import type { ResponseAs, FetchOptions, ResponseError } from '@js-from-routes/client' 4 | 5 | /** 6 | * Unwrap the response based on the `responseAs` value in the request. 7 | * @returns json, text, or the response. 8 | */ 9 | async function unwrapResponse (response: RedaxiosResponse, responseAs: ResponseAs) { 10 | Config.withResponse(response as unknown as Response) 11 | return responseAs === 'response' ? response : response.data 12 | } 13 | 14 | /** 15 | * Replace the default strategy which uses fetch to use Axios. 16 | */ 17 | async function fetch (args: FetchOptions) { 18 | const { url, method, responseAs, ...options } = args 19 | const responseType = responseAs === 'response' ? undefined : responseAs.toLowerCase() as Options['responseType'] 20 | const config: Options = { 21 | method: method as Options['method'], 22 | responseType, 23 | ...options, 24 | } 25 | return redaxios.request(url, config) 26 | .catch(error => Config.onResponseError(error as ResponseError)) 27 | } 28 | 29 | // NOTE: Replace the original `fetch` and `unwrapResponse`. 30 | Object.assign(Config, { fetch, unwrapResponse }) 31 | 32 | export { 33 | Config, 34 | definePathHelper, 35 | formatUrl, 36 | request, 37 | } 38 | -------------------------------------------------------------------------------- /packages/redaxios/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { definePathHelper, request, formatUrl } from '../src' 3 | 4 | describe('formatUrl', () => { 5 | it('is exported correctly', () => { 6 | expect(formatUrl('/users/:id', { id: '5' })).toEqual('/users/5') 7 | }) 8 | }) 9 | 10 | describe('request', () => { 11 | it('can unwrap a JSON response', async () => { 12 | expect(await request('get', 'https://pokeapi.co/api/v2/pokemon/:pokemon', { pokemon: 'pikachu' })).toMatchObject({ 13 | name: 'pikachu', 14 | }) 15 | }) 16 | 17 | it('can return the raw response', async () => { 18 | const fakeFetch = async (...args: any[]) => ({ status: 200, body: args }) 19 | expect(await request('get', '/videos/:id/download', { id: 2, fetch: fakeFetch, responseAs: 'response' })).toEqual({ 20 | status: 200, 21 | body: [{ 22 | method: 'GET', 23 | responseAs: 'response', 24 | url: '/videos/2/download', 25 | data: undefined, 26 | headers: { 27 | Accept: 'application/json', 28 | 'Content-Type': 'application/json', 29 | 'X-CSRF-Token': undefined, 30 | }, 31 | }], 32 | }) 33 | }) 34 | }) 35 | 36 | describe('definePathHelper', () => { 37 | it('returns a path helper with all the properties', async () => { 38 | const helper = definePathHelper('get', '/videos/:id/download') 39 | 40 | expect(helper.httpMethod).toEqual('get') 41 | expect(helper.pathTemplate).toEqual('/videos/:id/download') 42 | expect(helper.path({ i: 2, id: 5 })).toEqual('/videos/5/download') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /playground/vanilla/.browserslistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | -------------------------------------------------------------------------------- /playground/vanilla/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | /db/*.sqlite3-* 14 | 15 | # Ignore all logfiles and tempfiles. 16 | /log/* 17 | /tmp/* 18 | !/log/.keep 19 | !/tmp/.keep 20 | 21 | # Ignore pidfiles, but keep the directory. 22 | /tmp/pids/* 23 | !/tmp/pids/ 24 | !/tmp/pids/.keep 25 | 26 | # Ignore uploaded files in development. 27 | /storage/* 28 | !/storage/.keep 29 | 30 | /public/assets 31 | .byebug_history 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | 36 | /public/packs 37 | /public/packs-test 38 | /node_modules 39 | /yarn-error.log 40 | yarn-debug.log* 41 | .yarn-integrity 42 | 43 | # Vite Ruby 44 | /public/vite 45 | /public/vite-dev 46 | /public/vite-test 47 | node_modules 48 | *.local 49 | .DS_Store 50 | 51 | -------------------------------------------------------------------------------- /playground/vanilla/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /playground/vanilla/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | gem "js_from_routes", path: "../../js_from_routes" 5 | 6 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 7 | gem "rails", "~> 6.0.3", ">= 6.0.3.2" 8 | 9 | # Use Puma as the app server 10 | gem "puma", "~> 4.1" 11 | 12 | # Use Vite.js as the frontend tool. Read more: https://github.com/ElMassimo/vite_ruby 13 | gem "vite_rails" 14 | 15 | group :development, :test do 16 | gem "listen", "~> 3.7" 17 | gem "pry-byebug" 18 | gem "rspec-rails" 19 | gem "rails-controller-testing" 20 | end 21 | -------------------------------------------------------------------------------- /playground/vanilla/Procfile.dev: -------------------------------------------------------------------------------- 1 | vite: bin/vite dev 2 | web: bin/rails s --port 3000 3 | -------------------------------------------------------------------------------- /playground/vanilla/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /playground/vanilla/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /playground/vanilla/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /playground/vanilla/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /playground/vanilla/app/controllers/comments_controller.rb: -------------------------------------------------------------------------------- 1 | class CommentsController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /playground/vanilla/app/controllers/settings/user_preferences_controller.rb: -------------------------------------------------------------------------------- 1 | class Settings::UserPreferencesController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /playground/vanilla/app/controllers/video_clips_controller.rb: -------------------------------------------------------------------------------- 1 | class VideoClipsController < ApplicationController 2 | def download 3 | render body: "", status: 204 4 | end 5 | 6 | def latest 7 | render json: [ 8 | {id: 11, title: "Smoke Signals", composer_name: "Dylan Ryche"}, 9 | {id: 10, title: "Camino Libre", composer_name: "Máximo Mussini"}, 10 | {id: 9, title: "Sin Querer", composer_name: "León Gieco"}, 11 | {id: 8, title: "Tabula Rasa", composer_name: "Calum Graham"}, 12 | {id: 7, title: "Raindance", composer_name: "Matteo Brenci"}, 13 | {id: 6, title: "Ragamuffin", composer_name: "Michael Hedges"}, 14 | {id: 5, title: "Vals Venezolano Nº 2", composer_name: "Antonio Lauro"}, 15 | {id: 4, title: "Xaranga do Vovô", composer_name: "Celso Machado"}, 16 | {id: 3, title: "Café 1930", composer_name: "Astor Piazzolla"}, 17 | {id: 2, title: "Milonga (Uruguay)", composer_name: "Jorge Cardoso"}, 18 | {id: 1, title: "Divagando", composer_name: "Domingos Semenzato"} 19 | ] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /playground/vanilla/app/controllers/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class WelcomeController < ApplicationController 2 | end 3 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/ApiHelpers.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * NOT RECOMMENDED 5 | * 6 | * Starting in 2.0 a `all_helpers` file is generated which also provides the 7 | * benefit of autocompletion and type-safety. 8 | */ 9 | 10 | // Example: Combine all generated path helpers into a single object. 11 | export default Object.entries(import.meta.globEager('./api/**/*.{js,ts}')) 12 | .reduce((routes: Record, [fileName, module]) => { 13 | routes[controllerName(fileName)] = module.default 14 | return routes 15 | }, {}) 16 | 17 | /** 18 | * Removes the leading directory and the suffix, for easier access. 19 | * 20 | * NOTE: This is just an example, you could use any conventions, such as 21 | * lowercase, or combine namespaces into separate objects. 22 | */ 23 | function controllerName (fileName: string) { 24 | return fileName 25 | .slice('./api/'.length, fileName.lastIndexOf('.')) 26 | .replace('/', '_') 27 | .replace(/Api?$/, '') 28 | .replace(/^\w/, match => match.toLowerCase()) 29 | } 30 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/Videos.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/api/CommentsApi.ts: -------------------------------------------------------------------------------- 1 | // JsFromRoutes CacheKey 4ec871277a399f0b2bc4300ed1928842 2 | // 3 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 4 | import { definePathHelper } from '@js-from-routes/client' 5 | 6 | export default { 7 | index: /* #__PURE__ */ definePathHelper('get', '/video_clips/:video_clip_id/comments'), 8 | show: /* #__PURE__ */ definePathHelper('get', '/comments/:id'), 9 | } 10 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/api/Settings/UserPreferencesApi.ts: -------------------------------------------------------------------------------- 1 | // JsFromRoutes CacheKey ad45663323f4c6da5ccdfbe4de75c005 2 | // 3 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 4 | import { definePathHelper } from '@js-from-routes/client' 5 | 6 | export default { 7 | switchToClassicNavbar: /* #__PURE__ */ definePathHelper('patch', '/user_preferences/switch_to_classic_navbar'), 8 | switchToClassic: /* #__PURE__ */ definePathHelper('get', '/user_preferences/switch_to_classic/:page'), 9 | switchToBeta: /* #__PURE__ */ definePathHelper('get', '/user_preferences/switch_to_beta/:page'), 10 | } 11 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/api/VideoClipsApi.ts: -------------------------------------------------------------------------------- 1 | // JsFromRoutes CacheKey 932961009743a2c9330efd16b43845e8 2 | // 3 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 4 | import { definePathHelper } from '@js-from-routes/client' 5 | 6 | export default { 7 | download: /* #__PURE__ */ definePathHelper('get', '/video_clips/:id/download'), 8 | addToPlaylist: /* #__PURE__ */ definePathHelper('patch', '/video_clips/:id/add_to_playlist'), 9 | removeFromPlaylist: /* #__PURE__ */ definePathHelper('patch', '/video_clips/:id/remove_from_playlist'), 10 | latest: /* #__PURE__ */ definePathHelper('get', '/video_clips/latest'), 11 | thumbnail: /* #__PURE__ */ definePathHelper('get', '/video_clips/:id/thumbnail/:thumbnail_id'), 12 | create: /* #__PURE__ */ definePathHelper('post', '/video_clips'), 13 | new: /* #__PURE__ */ definePathHelper('get', '/video_clips/new'), 14 | edit: /* #__PURE__ */ definePathHelper('get', '/video_clips/:id/edit'), 15 | update: /* #__PURE__ */ definePathHelper('patch', '/video_clips/:id'), 16 | destroy: /* #__PURE__ */ definePathHelper('delete', '/video_clips/:id'), 17 | } 18 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/api/all.ts: -------------------------------------------------------------------------------- 1 | // JsFromRoutes CacheKey 4617b928f68edbd8c6d0b43c990b88db 2 | // 3 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 4 | export { default as videoClips } from './VideoClipsApi' 5 | export { default as comments } from './CommentsApi' 6 | export { default as settingsUserPreferences } from './Settings/UserPreferencesApi' 7 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/api/index.ts: -------------------------------------------------------------------------------- 1 | // JsFromRoutes CacheKey bbcf7b407076ca71753aa50dff72e82f 2 | // 3 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 4 | import * as helpers from './all' 5 | export * from './all' 6 | export default helpers 7 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/composables/api.ts: -------------------------------------------------------------------------------- 1 | import apiHelpers from '~/ApiHelpers' 2 | 3 | /** 4 | * Allows to access the api helpers in a Vue component. 5 | */ 6 | export function useApi () { 7 | // NOTE: Using provide/inject is unnecessary since it's static and globally available. 8 | return apiHelpers 9 | } 10 | -------------------------------------------------------------------------------- /playground/vanilla/app/javascript/entrypoints/application.ts: -------------------------------------------------------------------------------- 1 | import 'windi.css' 2 | 3 | import { createApp } from 'vue' 4 | import { Config, request, formatUrl } from '@js-from-routes/client' 5 | import Videos from '~/Videos.vue' 6 | 7 | // Example: Combine all exported routes in a single object. 8 | import api, { videoClips } from '~/api' 9 | 10 | // Example: Expose it globally to the entire app (not recommended, prefer injection). 11 | Object.assign(window, { api, request, formatUrl, Config }) 12 | 13 | const app = createApp(Videos) 14 | app.mount('#app') 15 | 16 | // Example: Configure the fetch strategy. 17 | const previousFetch = Config.fetch 18 | Config.fetch = (...args) => { 19 | console.log('fetch', ...args) 20 | return previousFetch(...args) 21 | } 22 | 23 | // Example: Using the path helper, both path interpolation and request. 24 | console.log({ path: videoClips.thumbnail.path({ id: 5, thumbnailId: 8 }) }) 25 | videoClips.latest().then((videos: any) => { console.log({ videos }) }) 26 | -------------------------------------------------------------------------------- /playground/vanilla/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sample App 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= vite_client_tag %> 10 | <%= vite_typescript_tag 'application' %> 11 | 12 | 13 | 14 |
15 | <%= yield %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /playground/vanilla/app/views/video_clips/new.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/playground/vanilla/app/views/video_clips/new.html.erb -------------------------------------------------------------------------------- /playground/vanilla/app/views/welcome/home.html.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/playground/vanilla/app/views/welcome/home.html.erb -------------------------------------------------------------------------------- /playground/vanilla/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | const validEnv = ['development', 'test', 'production'] 3 | const currentEnv = api.env() 4 | const isDevelopmentEnv = api.env('development') 5 | const isProductionEnv = api.env('production') 6 | const isTestEnv = api.env('test') 7 | 8 | if (!validEnv.includes(currentEnv)) { 9 | throw new Error( 10 | `${'Please specify a valid `NODE_ENV` or ' 11 | + '`BABEL_ENV` environment variables. Valid values are "development", ' 12 | + '"test", and "production". Instead, received: '}${ 13 | JSON.stringify(currentEnv) 14 | }.`, 15 | ) 16 | } 17 | 18 | return { 19 | presets: [ 20 | isTestEnv && [ 21 | '@babel/preset-env', 22 | { 23 | targets: { 24 | node: 'current', 25 | }, 26 | }, 27 | ], 28 | (isProductionEnv || isDevelopmentEnv) && [ 29 | '@babel/preset-env', 30 | { 31 | forceAllTransforms: true, 32 | useBuiltIns: 'entry', 33 | corejs: 3, 34 | modules: false, 35 | exclude: ['transform-typeof-symbol'], 36 | }, 37 | ], 38 | ].filter(Boolean), 39 | plugins: [ 40 | 'babel-plugin-macros', 41 | '@babel/plugin-syntax-dynamic-import', 42 | isTestEnv && 'babel-plugin-dynamic-import-node', 43 | '@babel/plugin-transform-destructuring', 44 | [ 45 | '@babel/plugin-proposal-class-properties', 46 | { 47 | loose: true, 48 | }, 49 | ], 50 | [ 51 | '@babel/plugin-proposal-object-rest-spread', 52 | { 53 | useBuiltIns: true, 54 | }, 55 | ], 56 | [ 57 | '@babel/plugin-transform-runtime', 58 | { 59 | helpers: false, 60 | regenerator: true, 61 | corejs: false, 62 | }, 63 | ], 64 | [ 65 | '@babel/plugin-transform-regenerator', 66 | { 67 | async: false, 68 | }, 69 | ], 70 | ].filter(Boolean), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /playground/vanilla/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/o 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/o 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= 65 | env_var_version || cli_arg_version || 66 | lockfile_version 67 | end 68 | 69 | def bundler_requirement 70 | return "#{Gem::Requirement.default}.a" unless bundler_version 71 | 72 | bundler_gem_version = Gem::Version.new(bundler_version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /playground/vanilla/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?("spring") 6 | end 7 | APP_PATH = File.expand_path("../config/application", __dir__) 8 | require_relative "../config/boot" 9 | require "rails/commands" 10 | -------------------------------------------------------------------------------- /playground/vanilla/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?("spring") 6 | end 7 | require_relative "../config/boot" 8 | require "rake" 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /playground/vanilla/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # Install JavaScript dependencies 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! "bin/rails db:prepare" 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! "bin/rails log:clear tmp:clear" 33 | 34 | puts "\n== Restarting application server ==" 35 | system! "bin/rails restart" 36 | end 37 | -------------------------------------------------------------------------------- /playground/vanilla/bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads Spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem "spring", spring.version 15 | require "spring/binstub" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /playground/vanilla/bin/vite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'vite' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300)) 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("vite_ruby", "vite") 30 | -------------------------------------------------------------------------------- /playground/vanilla/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /playground/vanilla/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "action_controller/railtie" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module SampleApp 10 | class Application < Rails::Application 11 | # Application configuration can go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded after loading 13 | # the framework and any gems in your application. 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /playground/vanilla/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /playground/vanilla/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /playground/vanilla/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Store uploaded files on the local file system (see config/storage.yml for options). 16 | # Print deprecation notices to the Rails logger. 17 | config.active_support.deprecation = :log 18 | 19 | # Raises error for missing translations. 20 | # config.action_view.raise_on_missing_translations = true 21 | 22 | # Use an evented file watcher to asynchronously detect changes in source code, 23 | # routes, locales, etc. This feature depends on the listen gem. 24 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 25 | end 26 | -------------------------------------------------------------------------------- /playground/vanilla/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | config.cache_classes = true 4 | config.eager_load = true 5 | end 6 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy 4 | # For further information see the following documentation 5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 6 | 7 | # Rails.application.config.content_security_policy do |policy| 8 | # policy.default_src :self, :https 9 | # policy.font_src :self, :https, :data 10 | # policy.img_src :self, :https, :data 11 | # policy.object_src :none 12 | # policy.script_src :self, :https 13 | # Allow @vite/client to hot reload changes in development 14 | # policy.script_src *policy.script_src, :unsafe_eval, "http://localhost:3036" if Rails.env.development? 15 | 16 | # You may need to enable this in production as well depending on your setup. 17 | # policy.script_src *policy.script_src, :blob if Rails.env.test? 18 | # policy.style_src :self, :https 19 | # # If you are using webpack-dev-server then specify webpack-dev-server host 20 | # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? 21 | # Allow @vite/client to hot reload changes in development 22 | # policy.connect_src *policy.connect_src, "ws://#{ ViteRuby.config.host_with_port }" if Rails.env.development? 23 | 24 | # # Specify URI for violation reports 25 | # # policy.report_uri "/csp-violation-report-endpoint" 26 | # end 27 | 28 | # If you are using UJS then enable automatic nonce generation 29 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } 30 | 31 | # Set the nonce only to specific directives 32 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) 33 | 34 | # Report CSP violations to a specified URI 35 | # For further information see the following documentation: 36 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 37 | # Rails.application.config.content_security_policy_report_only = true 38 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/js_from_routes.rb: -------------------------------------------------------------------------------- 1 | if Rails.env.development? 2 | # Example: Generate TypeScript files. 3 | JsFromRoutes.config do |config| 4 | config.file_suffix = "Api.ts" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /playground/vanilla/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) { wrap_parameters format: [:json] } 8 | 9 | # To enable root element in JSON for ActiveRecord objects. 10 | # ActiveSupport.on_load(:active_record) do 11 | # self.include_root_in_json = true 12 | # end 13 | -------------------------------------------------------------------------------- /playground/vanilla/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /playground/vanilla/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS", max_threads_count) 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch("PORT", 3000) 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch("RAILS_ENV") { "development" } 18 | 19 | # Specifies the `pidfile` that Puma will use. 20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 21 | 22 | # Specifies the number of `workers` to boot in clustered mode. 23 | # Workers are forked web server processes. If using threads and workers together 24 | # the concurrency of the application would be max `threads` * `workers`. 25 | # Workers do not work on JRuby or Windows (both of which do not support 26 | # processes). 27 | # 28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 29 | 30 | # Use the `preload_app!` method when specifying a `workers` number. 31 | # This directive tells Puma to first boot the application and load code 32 | # before forking the application. This takes advantage of Copy On Write 33 | # process behavior so workers use less memory. 34 | # 35 | # preload_app! 36 | 37 | # Allow puma to be restarted by `rails restart` command. 38 | plugin :tmp_restart 39 | -------------------------------------------------------------------------------- /playground/vanilla/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: "welcome#home" 3 | 4 | defaults export: true do 5 | get "/hi" => redirect("/welcome") 6 | end 7 | 8 | resources :welcome 9 | 10 | resources :video_clips, only: [:new, :edit, :create, :update, :destroy], export: true do 11 | get :download, on: :member 12 | patch :add_to_playlist, on: :member 13 | patch :remove_from_playlist, on: :member 14 | get :latest, on: :collection 15 | get "/thumbnail/:thumbnail_id", as: :thumbnail, action: :thumbnail, on: :member 16 | 17 | resources :comments, only: [:show, :index], shallow: true 18 | end 19 | 20 | namespace :settings, path: "/" do 21 | resources :user_preferences, only: [], export: true do 22 | patch :switch_to_classic_navbar, on: :collection 23 | get :switch_to_beta_navbar, on: :collection, export: false 24 | get "/switch_to_classic/:page", action: :switch_to_classic, on: :collection 25 | get "/switch_to_beta/:page", action: :switch_to_beta, on: :collection, as: :switch_to_beta_page 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /playground/vanilla/config/vite.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": { 3 | "sourceCodeDir": "app/javascript", 4 | "watchAdditionalPaths": [] 5 | }, 6 | "development": { 7 | "autoBuild": true, 8 | "publicOutputDir": "vite-dev", 9 | "port": 3036 10 | }, 11 | "test": { 12 | "autoBuild": true, 13 | "publicOutputDir": "vite-test" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /playground/vanilla/index.d.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | 3 | declare module '*.vue' { 4 | const Component: ReturnType 5 | export default Component 6 | } 7 | 8 | declare module '*.md' { 9 | const Component: ReturnType 10 | export default Component 11 | } 12 | -------------------------------------------------------------------------------- /playground/vanilla/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample_app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@js-from-routes/client": "^1.0.3", 7 | "@rails/ujs": "^6.0.0", 8 | "vue": "^3.0.7" 9 | }, 10 | "devDependencies": { 11 | "@vitejs/plugin-vue": "^1", 12 | "typescript": "^4", 13 | "vite": "^2.8.6", 14 | "vite-plugin-ruby": "^3.0.8", 15 | "vite-plugin-windicss": "^1.6" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /playground/vanilla/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - . 3 | -------------------------------------------------------------------------------- /playground/vanilla/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /playground/vanilla/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /playground/vanilla/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /playground/vanilla/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/playground/vanilla/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /playground/vanilla/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/playground/vanilla/public/apple-touch-icon.png -------------------------------------------------------------------------------- /playground/vanilla/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ElMassimo/js_from_routes/036e768f2723b36baaf0938b96325075437b14d2/playground/vanilla/public/favicon.ico -------------------------------------------------------------------------------- /playground/vanilla/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /playground/vanilla/spec/controllers/video_clips_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe VideoClipsController, type: :controller do 4 | describe "GET /" do 5 | it "should make a request" do 6 | get :new 7 | expect(response).to render_template(:new) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /playground/vanilla/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require "spec_helper" 3 | ENV["RAILS_ENV"] ||= "test" 4 | require File.expand_path("../config/environment", __dir__) 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require "rspec/rails" 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | 10 | # Requires supporting ruby files with custom matchers and macros, etc, in 11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 12 | # run as spec files by default. This means that files in spec/support that end 13 | # in _spec.rb will both be required and run as specs, causing the specs to be 14 | # run twice. It is recommended that you do not name files matching this glob to 15 | # end with _spec.rb. You can configure this pattern with the --pattern 16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 17 | # 18 | # The following line is provided for convenience purposes. It has the downside 19 | # of increasing the boot-up time by auto-requiring all files in the support 20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 21 | # require only the support files necessary. 22 | # 23 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } 24 | 25 | RSpec.configure do |config| 26 | # Remove this line to enable support for ActiveRecord 27 | config.use_active_record = false 28 | 29 | # If you enable ActiveRecord support you should unncomment these lines, 30 | # note if you'd prefer not to run each example within a transaction, you 31 | # should set use_transactional_fixtures to false. 32 | # 33 | # config.fixture_path = "#{::Rails.root}/spec/fixtures" 34 | # config.use_transactional_fixtures = true 35 | 36 | # RSpec Rails can automatically mix in different behaviours to your tests 37 | # based on their file location, for example enabling you to call `get` and 38 | # `post` in specs under `spec/controllers`. 39 | # 40 | # You can disable this behaviour by removing the line below, and instead 41 | # explicitly tag your specs with their type, e.g.: 42 | # 43 | # RSpec.describe UsersController, type: :controller do 44 | # # ... 45 | # end 46 | # 47 | # The different available types are documented in the features, such as in 48 | # https://relishapp.com/rspec/rspec-rails/docs 49 | config.infer_spec_type_from_file_location! 50 | 51 | # Filter lines from Rails gems in backtraces. 52 | config.filter_rails_from_backtrace! 53 | # arbitrary gems may also be filtered via: 54 | # config.filter_gems_from_backtrace("gem name") 55 | end 56 | -------------------------------------------------------------------------------- /playground/vanilla/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | # # This allows you to limit a spec run to individual examples or groups 50 | # # you care about by tagging them with `:focus` metadata. When nothing 51 | # # is tagged with `:focus`, all examples get run. RSpec also provides 52 | # # aliases for `it`, `describe`, and `context` that include `:focus` 53 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 54 | # config.filter_run_when_matching :focus 55 | # 56 | # # Allows RSpec to persist some state between runs in order to support 57 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 58 | # # you configure your source control system to ignore this file. 59 | # config.example_status_persistence_file_path = "spec/examples.txt" 60 | # 61 | # # Limits the available syntax to the non-monkey patched syntax that is 62 | # # recommended. For more details, see: 63 | # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 64 | # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 65 | # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 66 | # config.disable_monkey_patching! 67 | # 68 | # # Many RSpec users commonly either run the entire suite or an individual 69 | # # file, and it's useful to allow more verbose output when running an 70 | # # individual spec file. 71 | # if config.files_to_run.one? 72 | # # Use the documentation formatter for detailed output, 73 | # # unless a formatter has already been configured 74 | # # (e.g. via a command-line flag). 75 | # config.default_formatter = "doc" 76 | # end 77 | # 78 | # # Print the 10 slowest examples and example groups at the 79 | # # end of the spec run, to help surface which specs are running 80 | # # particularly slow. 81 | # config.profile_examples = 10 82 | # 83 | # # Run specs in random order to surface order dependencies. If you find an 84 | # # order dependency and want to debug it, you can fix the order by providing 85 | # # the seed, which is printed after each run. 86 | # # --seed 1234 87 | # config.order = :random 88 | # 89 | # # Seed global randomization in this process using the `--seed` CLI option. 90 | # # Setting this allows you to use `--seed` to deterministically reproduce 91 | # # test failures related to randomization by passing the same `--seed` value 92 | # # as the one that triggered the failure. 93 | # Kernel.srand config.seed 94 | end 95 | -------------------------------------------------------------------------------- /playground/vanilla/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "es2017", 6 | "lib": ["ESNext", "DOM"], 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "moduleResolution": "Node", 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "paths": { 14 | "@/*": ["./app/javascript/*"], 15 | "~/*": ["./app/javascript/*"] 16 | } 17 | }, 18 | "exclude": [ 19 | "**/dist", 20 | "**/node_modules", 21 | "**/test" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /playground/vanilla/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import RubyPlugin from 'vite-plugin-ruby' 3 | import VuePlugin from '@vitejs/plugin-vue' 4 | import WindiCSSPlugin from 'vite-plugin-windicss' 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | VuePlugin(), 9 | RubyPlugin(), 10 | WindiCSSPlugin({ 11 | root: __dirname, 12 | scan: { 13 | fileExtensions: ['html', 'erb', 'vue', 'js', 'ts'], 14 | dirs: ['app/javascript', 'app/views'], 15 | }, 16 | }), 17 | ], 18 | }) 19 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/core/ 3 | - packages/client/ 4 | - packages/axios/ 5 | - packages/redaxios/ 6 | - packages/inertia/ 7 | -------------------------------------------------------------------------------- /scripts/changelog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path') 3 | const fs = require('fs') 4 | const args = require('minimist')(process.argv.slice(2)) 5 | const execa = require('execa') 6 | const chalk = require('chalk') 7 | 8 | const name = args._[0]?.trim() 9 | 10 | if (!name) { 11 | console.error(chalk.red(`Expected library name as an argument, received ${name}`)) 12 | process.exit(1) 13 | } 14 | 15 | const isRubyPackage = name === 'js_from_routes' 16 | const packagePath = isRubyPackage ? name : `packages/${name}` 17 | 18 | /** 19 | * @param {string} bin 20 | * @param {string[]} args 21 | * @param {object} opts 22 | */ 23 | const run = (bin, args, opts = {}) => 24 | execa(bin, args, { stdio: 'inherit', ...opts }) 25 | 26 | /** 27 | * @param {string} paths 28 | */ 29 | const resolve = paths => path.resolve(__dirname, `../${packagePath}/${paths}`) 30 | 31 | /** 32 | * @param {string} name 33 | */ 34 | function writePackageJson (name) { 35 | const versionRegex = /VERSION = "([\d.]+)"/ 36 | const versionFile = fs.readFileSync(resolve(`lib/${name}/version.rb`), 'utf-8') 37 | const versionCaptures = versionFile.match(versionRegex) 38 | const version = versionCaptures && versionCaptures[1] 39 | if (!version) { 40 | console.error(chalk.red(`Could not infer version for ${name}.`)) 41 | process.exit(1) 42 | } 43 | fs.writeFileSync(resolve('package.json'), `{ "version": "${version}" }`) 44 | } 45 | 46 | async function main () { 47 | if (isRubyPackage) writePackageJson(name) 48 | 49 | await run('npx', [ 50 | 'conventional-changelog', 51 | '-p', 'angular', 52 | '-i', `${packagePath}/CHANGELOG.md`, 53 | '-s', 54 | '-t', `${name}@`, 55 | '--pkg', `./${packagePath}/package.json`, 56 | '--commit-path', `./${packagePath}`, 57 | ]) 58 | 59 | if (isRubyPackage) fs.rmSync(resolve('package.json')) 60 | } 61 | 62 | main().catch((err) => { 63 | console.error(err) 64 | }) 65 | -------------------------------------------------------------------------------- /scripts/verifyCommit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | /* eslint-disable import/order */ 3 | const args = require('minimist')(process.argv.slice(2)) 4 | const msgPath = args._[0] 5 | const msg = require('fs').readFileSync(msgPath, 'utf-8').trim() 6 | 7 | const releaseRE = /^v\d/ 8 | const commitRE = /^(revert: )?(feat|fix|docs|dx|refactor|perf|test|workflow|build|ci|chore|types|wip|release|deps)(\(.+\))?: .{1,50}/ 9 | 10 | if (!releaseRE.test(msg) && !commitRE.test(msg)) { 11 | console.log() 12 | const chalk = require('chalk') 13 | console.error( 14 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( 15 | 'invalid commit message format.', 16 | )}\n\n${ 17 | chalk.red( 18 | ' Proper commit message format is required for automated changelog generation. Examples:\n\n', 19 | ) 20 | } ${chalk.green('feat: add \'comments\' option')}\n` 21 | + ` ${chalk.green('fix: handle events on blur (close #28)')}\n\n${ 22 | chalk.red(' See .github/commit-convention.md for more details.\n')}`, 23 | ) 24 | process.exit(1) 25 | } 26 | -------------------------------------------------------------------------------- /spec/js_from_routes/js_from_routes_spec.rb: -------------------------------------------------------------------------------- 1 | require "vanilla/config/application" 2 | require "vanilla/config/routes" 3 | 4 | describe JsFromRoutes do 5 | original_template_path = JsFromRoutes.config.template_path 6 | 7 | let(:output_dir) { Pathname.new File.expand_path("../support/generated", __dir__) } 8 | let(:sample_dir) { Rails.root.join("app", "javascript", "api") } 9 | let(:different_template_path) { File.expand_path("../support/jquery_template.js.erb", __dir__) } 10 | let(:controllers_with_exported_routes) { %w[Comments Settings/UserPreferences VideoClips] } 11 | 12 | def file_for(dir, name) 13 | dir.join("#{name}Api.ts") 14 | end 15 | 16 | def sample_file_for(name) 17 | file_for(sample_dir, name) 18 | end 19 | 20 | def output_file_for(name) 21 | file_for(output_dir, name) 22 | end 23 | 24 | def expect_templates 25 | expect_any_instance_of(JsFromRoutes::Template) 26 | end 27 | 28 | def be_rendered 29 | receive(:render_template) 30 | end 31 | 32 | before do 33 | # Sanity checks 34 | expect(sample_dir.exist?).to eq true 35 | expect(Rails.application.routes.routes).to be_present 36 | 37 | # Remove directory from a previous test run. 38 | begin 39 | FileUtils.remove_dir(output_dir) 40 | rescue 41 | nil 42 | end 43 | 44 | # Change the configuration to use a different directory. 45 | JsFromRoutes.config do |config| 46 | config.file_suffix = "Api.ts" 47 | config.all_helpers_file = false 48 | config.output_folder = output_dir 49 | config.template_path = original_template_path 50 | end 51 | end 52 | 53 | # NOTE: We do a manual snapshot test for now, more tests coming in the future. 54 | it "should generate the files as expected" do 55 | expect_templates.to be_rendered.exactly(3).times.and_call_original 56 | JsFromRoutes.generate! 57 | 58 | # It does not generate routes that don't have `export: true`. 59 | expect(output_file_for("Welcome").exist?).to eq false 60 | 61 | # It generates one file per controller with exported routes. 62 | controllers_with_exported_routes.each do |file_name| 63 | expect(output_file_for(file_name).read).to eq sample_file_for(file_name).read 64 | end 65 | 66 | # It does not render if generating again. 67 | JsFromRoutes.generate! 68 | end 69 | 70 | context "changing the template" do 71 | before do 72 | JsFromRoutes.generate! 73 | 74 | JsFromRoutes.config do |config| 75 | config.template_path = different_template_path 76 | end 77 | end 78 | 79 | it "detects changes and re-renders" do 80 | expect_templates.to be_rendered.exactly(3).times.and_call_original 81 | JsFromRoutes.generate! 82 | 83 | # These files should no longer match the sample ones. 84 | controllers_with_exported_routes.each do |file_name| 85 | expect(output_file_for(file_name).read).not_to eq sample_file_for(file_name).read 86 | end 87 | 88 | # It should not rewrite the files if the cache key has not changed. 89 | JsFromRoutes.generate! 90 | end 91 | end 92 | 93 | context "when generating all_helpers_file" do 94 | before do 95 | JsFromRoutes.generate! 96 | 97 | JsFromRoutes.config do |config| 98 | config.all_helpers_file = true 99 | end 100 | end 101 | 102 | it "generates a file with all helpers" do 103 | JsFromRoutes.generate! 104 | expect(output_dir.join("all.ts").exist?).to eq true 105 | expect(output_dir.join("index.ts").exist?).to eq true 106 | 107 | # Should not trigger another render. 108 | expect_templates.not_to be_rendered 109 | JsFromRoutes.generate! 110 | end 111 | end 112 | 113 | it "should have a rake task available" do 114 | Rails.application.load_tasks 115 | expect_templates.to be_rendered.exactly(3).times 116 | expect { Rake::Task["js_from_routes:generate"].invoke }.not_to raise_error 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start { 3 | add_filter "/spec/" 4 | add_filter "/playground/" 5 | } 6 | 7 | require "rails" 8 | require "js_from_routes" 9 | require "rspec/given" 10 | require "pry-byebug" 11 | 12 | $LOAD_PATH.push File.expand_path("../playground", __dir__) 13 | -------------------------------------------------------------------------------- /spec/support/jquery_template.js.erb: -------------------------------------------------------------------------------- 1 | // 2 | // DO NOT MODIFY: This file was automatically generated by JsFromRoutes. 3 | import { formatUrl } from '@js-from-routes/core' 4 | 5 | export default { 6 | <% routes.each_with_index do |route, index| %> 7 | <% if index > 0 %><%= "\n" %><% end 8 | %> <%= route.helper %>: options => 9 | <% unless route.export == :path_only 10 | %>$.<%= route.verb %>({ url: <% end %>formatUrl('<%= route.path %>', options)<% 11 | unless route.export == :path_only %>, ...options })<% end %>, 12 | <% end %> 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "es2020", 5 | "lib": ["ESNext", "DOM"], 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "strictNullChecks": true, 9 | "moduleResolution": "Node", 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true 12 | }, 13 | "exclude": [ 14 | "**/dist", 15 | "**/node_modules", 16 | "**/test" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /vetur.config.js: -------------------------------------------------------------------------------- 1 | // vetur.config.js 2 | /** @type {import('vls').VeturConfig} */ 3 | module.exports = { 4 | // **optional** default: `{}` 5 | // override vscode settings 6 | // Notice: It only affects the settings used by Vetur. 7 | settings: { 8 | 'vetur.useWorkspaceDependencies': true, 9 | 'vetur.experimental.templateInterpolationService': true, 10 | }, 11 | // **optional** default: `[{ root: './' }]` 12 | // support monorepos 13 | projects: [ 14 | { 15 | // **required** 16 | // Where is your project? 17 | // It is relative to `vetur.config.js`. 18 | root: './playground/vanilla', 19 | // **optional** default: `'package.json'` 20 | // Where is `package.json` in the project? 21 | // We use it to determine the version of vue. 22 | // It is relative to root property. 23 | package: './package.json', 24 | // **optional** 25 | // Where is TypeScript config file in the project? 26 | // It is relative to root property. 27 | tsconfig: './tsconfig.json', 28 | // **optional** default: `'./.vscode/vetur/snippets'` 29 | // Where is vetur custom snippets folders? 30 | // snippetFolder: './.vscode/vetur/snippets', 31 | // **optional** default: `[]` 32 | // Register globally Vue component glob. 33 | // If you set it, you can get completion by that components. 34 | // It is relative to root property. 35 | // Notice: It won't actually do it. You need to use `require.context` or `Vue.component` 36 | // globalComponents: [ 37 | // './src/components/**/*.vue' 38 | // ] 39 | }, 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'happy-dom', // or 'jsdom', 'node' 6 | }, 7 | }) 8 | --------------------------------------------------------------------------------