├── .editorconfig ├── .github ├── COMMIT_CONVENTION.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── lock.yml ├── stale.yml └── workflows │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── commit-msg ├── .prettierignore ├── LICENSE.md ├── README.md ├── adonis-typings ├── container.ts ├── context.ts ├── index.ts └── view.ts ├── bin ├── japaTypes.ts └── test.ts ├── instructions.md ├── package.json ├── providers └── ViewProvider.ts ├── src └── Bindings │ ├── AssetsManager.ts │ ├── Repl.ts │ └── Vite.ts ├── test-helpers ├── drive.ts └── index.ts ├── test └── view-provider.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | insert_final_newline = ignore 13 | 14 | [**.min.js] 15 | indent_style = ignore 16 | insert_final_newline = ignore 17 | 18 | [MakeFile] 19 | indent_style = space 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. 6 | 7 | ``` js 8 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ 9 | ``` 10 | 11 | ## Commit Message Format 12 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 13 | 14 | > The **scope** is optional 15 | 16 | ``` 17 | feat(router): add support for prefix 18 | 19 | Prefix makes it easier to append a path to a group of routes 20 | ``` 21 | 22 | 1. `feat` is type. 23 | 2. `router` is scope and is optional 24 | 3. `add support for prefix` is the subject 25 | 4. The **body** is followed by a blank line. 26 | 5. The optional **footer** can be added after the body, followed by a blank line. 27 | 28 | ## Types 29 | Only one type can be used at a time and only following types are allowed. 30 | 31 | - feat 32 | - fix 33 | - docs 34 | - style 35 | - refactor 36 | - perf 37 | - test 38 | - workflow 39 | - ci 40 | - chore 41 | - types 42 | - build 43 | 44 | If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. 45 | 46 | ### Revert 47 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. 48 | 49 | ## Scope 50 | The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. 51 | 52 | ## Subject 53 | The subject contains succinct description of the change: 54 | 55 | - use the imperative, present tense: "change" not "changed" nor "changes". 56 | - don't capitalize first letter 57 | - no dot (.) at the end 58 | 59 | ## Body 60 | 61 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". 62 | The body should include the motivation for the change and contrast this with previous behavior. 63 | 64 | ## Footer 65 | 66 | The footer should contain any information about **Breaking Changes** and is also the place to 67 | reference GitHub issues that this commit **Closes**. 68 | 69 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. 70 | 71 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | AdonisJS is a community driven project. You are free to contribute in any of the following ways. 4 | 5 | - [Coding style](coding-style) 6 | - [Fix bugs by creating PR's](fix-bugs-by-creating-prs) 7 | - [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) 8 | - [Report security issues](report-security-issues) 9 | - [Be a part of the community](be-a-part-of-community) 10 | 11 | ## Coding style 12 | 13 | Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. 14 | 15 | ## Fix bugs by creating PR's 16 | 17 | We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. 18 | 19 | Go through the following points, before creating a new PR. 20 | 21 | 1. Create an issue discussing the bug or short-coming in the framework. 22 | 2. Once approved, go ahead and fork the REPO. 23 | 3. Make sure to start from the `develop`, since this is the upto date branch. 24 | 4. Make sure to keep commits small and relevant. 25 | 5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. 26 | 6. Once done with all the changes, create a PR against the `develop` branch. 27 | 28 | ## Share an RFC for new features or big changes 29 | 30 | Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. 31 | 32 | ### What is an RFC? 33 | 34 | RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). 35 | 36 | In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. 37 | 38 | The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. 39 | 40 | ## Report security issues 41 | 42 | All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. 43 | 44 | ## Be a part of community 45 | 46 | We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report identified bugs 4 | --- 5 | 6 | 7 | 8 | ## Prerequisites 9 | 10 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 11 | 12 | - Lots of raised issues are directly not bugs but instead are design decisions taken by us. 13 | - Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. 14 | - Ensure the issue isn't already reported. 15 | - Ensure you are reporting the bug in the correct repo. 16 | 17 | *Delete the above section and the instructions in the sections below before submitting* 18 | 19 | ## Package version 20 | 21 | 22 | ## Node.js and npm version 23 | 24 | 25 | ## Sample Code (to reproduce the issue) 26 | 27 | 28 | ## BONUS (a sample repo to reproduce the issue) 29 | 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Propose changes for adding a new feature 4 | --- 5 | 6 | 7 | 8 | ## Prerequisites 9 | 10 | We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. 11 | 12 | ## Consider an RFC 13 | 14 | Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if 15 | 16 | - Feature introduces a breaking change 17 | - Demands lots of time and changes in the current code base. 18 | 19 | *Delete the above section and the instructions in the sections below before submitting* 20 | 21 | ## Why this feature is required (specific use-cases will be appreciated)? 22 | 23 | 24 | ## Have you tried any other work arounds? 25 | 26 | 27 | ## Are you willing to work on it with little guidance? 28 | 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Proposed changes 4 | 5 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 6 | 7 | ## Types of changes 8 | 9 | What types of changes does your code introduce? 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## Checklist 18 | 19 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 20 | 21 | - [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/view/blob/master/.github/CONTRIBUTING.md) doc 22 | - [ ] Lint and unit tests pass locally with my changes 23 | - [ ] I have added tests that prove my fix is effective or that my feature works. 24 | - [ ] I have added necessary documentation (if appropriate) 25 | 26 | ## Further comments 27 | 28 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 29 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 60 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: ['Type: Security'] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: false 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: > 18 | This thread has been automatically locked since there has not been 19 | any recent activity after it was closed. Please open a new issue for 20 | related bugs. 21 | 22 | # Assign `resolved` as the reason for locking. Set to `false` to disable 23 | setLockReason: false 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - "Type: Security" 10 | 11 | # Label to use when marking an issue as stale 12 | staleLabel: "Status: Abandoned" 13 | 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | linux: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | - 14.15.4 12 | - 17.x 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: Install 20 | run: npm install 21 | - name: Run tests 22 | run: npm test 23 | windows: 24 | runs-on: windows-latest 25 | strategy: 26 | matrix: 27 | node-version: 28 | - 14.15.4 29 | - 17.x 30 | steps: 31 | - uses: actions/checkout@v2 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v1 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | - name: Install 37 | run: npm install 38 | - name: Run tests 39 | run: npm test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_STORE 4 | .nyc_output 5 | .idea 6 | .vscode/ 7 | *.sublime-project 8 | *.sublime-workspace 9 | *.log 10 | build 11 | dist 12 | yarn.lock 13 | shrinkwrap.yaml 14 | package-lock.json 15 | test/__app 16 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | docs 3 | *.md 4 | config.json 5 | .eslintrc.json 6 | package.json 7 | *.html 8 | *.txt 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2022 Harminder Virk, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 |

AdonisJS Template Engine

9 |

10 | This repo holds the template engine (Edge) used by AdonisJS. 11 |

12 |
13 | 14 |
15 | 16 |
17 | 18 | [![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![synk-image]][synk-url] 19 | 20 |
21 | 22 |
23 | 24 | ![](./benchmarks.png) 25 | 26 |
27 | 28 |
29 |

30 | 31 | Website 32 | 33 | | 34 | 35 | Guides 36 | 37 | | 38 | 39 | Contributing 40 | 41 |

42 |
43 | 44 |
45 | Built with ❤︎ by Harminder Virk 46 |
47 | 48 | [gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/view/test?style=for-the-badge 49 | [gh-workflow-url]: https://github.com/adonisjs/view/actions/workflows/test.yml "Github action" 50 | 51 | [typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript 52 | [typescript-url]: "typescript" 53 | 54 | [npm-image]: https://img.shields.io/npm/v/@adonisjs/view/latest.svg?style=for-the-badge&logo=npm 55 | [npm-url]: https://npmjs.org/package/@adonisjs/view/v/latest "npm" 56 | 57 | [license-image]: https://img.shields.io/npm/l/@adonisjs/view?color=blueviolet&style=for-the-badge 58 | [license-url]: LICENSE.md "license" 59 | 60 | [synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/view?label=Synk%20Vulnerabilities&style=for-the-badge 61 | [synk-url]: https://snyk.io/test/github/adonisjs/view?targetFile=package.json "synk" 62 | -------------------------------------------------------------------------------- /adonis-typings/container.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare module '@ioc:Adonis/Core/Application' { 11 | import { ViewContract } from '@ioc:Adonis/Core/View' 12 | 13 | interface ContainerBindings { 14 | 'Adonis/Core/View': ViewContract 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /adonis-typings/context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /** 11 | * Decorate context 12 | */ 13 | declare module '@ioc:Adonis/Core/HttpContext' { 14 | import { ViewRendererContract } from '@ioc:Adonis/Core/View' 15 | interface HttpContextContract { 16 | view: ViewRendererContract 17 | } 18 | } 19 | 20 | /** 21 | * Decorate router 22 | */ 23 | declare module '@ioc:Adonis/Core/Route' { 24 | interface BriskRouteContract { 25 | render: (template: string, data?: any) => Exclude 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /adonis-typings/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | /// 11 | /// 12 | /// 13 | -------------------------------------------------------------------------------- /adonis-typings/view.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | declare module '@ioc:Adonis/Core/View' { 11 | import { EdgeContract, EdgeRendererContract } from 'edge.js' 12 | 13 | export interface ViewContract extends EdgeContract {} 14 | export interface ViewRendererContract extends EdgeRendererContract {} 15 | 16 | export { 17 | TagContract, 18 | ParserContract, 19 | EdgeBufferContract, 20 | TagTokenContract, 21 | TemplateConstructorContract, 22 | } from 'edge.js' 23 | 24 | const View: ViewContract 25 | export default View 26 | } 27 | -------------------------------------------------------------------------------- /bin/japaTypes.ts: -------------------------------------------------------------------------------- 1 | import '@japa/assert' 2 | declare module '@japa/runner' {} 3 | -------------------------------------------------------------------------------- /bin/test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from '@japa/assert' 2 | import { specReporter } from '@japa/spec-reporter' 3 | import { runFailedTests } from '@japa/run-failed-tests' 4 | import { processCliArgs, configure, run } from '@japa/runner' 5 | 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Configure tests 9 | |-------------------------------------------------------------------------- 10 | | 11 | | The configure method accepts the configuration to configure the Japa 12 | | tests runner. 13 | | 14 | | The first method call "processCliArgs" process the command line arguments 15 | | and turns them into a config object. Using this method is not mandatory. 16 | | 17 | | Please consult japa.dev/runner-config for the config docs. 18 | */ 19 | configure({ 20 | ...processCliArgs(process.argv.slice(2)), 21 | ...{ 22 | files: ['test/**/*.spec.ts'], 23 | plugins: [assert(), runFailedTests()], 24 | reporters: [specReporter()], 25 | importer: (filePath: string) => import(filePath), 26 | }, 27 | }) 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Run tests 32 | |-------------------------------------------------------------------------- 33 | | 34 | | The following "run" method is required to execute all the tests. 35 | | 36 | */ 37 | run() 38 | -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | The package has been configured successfully. The package internally relies on the `CACHE_VIEWS` environment variables and hence we recommend recommend validating it. 2 | 3 | Open the `env.ts` file and paste the following code inside the `Env.rules` object. 4 | 5 | ```ts 6 | CACHE_VIEWS: Env.schema.boolean() 7 | ``` 8 | 9 | - Here we ensure that `CACHE_VIEWS` environment is always defined. 10 | - And it is using a boolean value. 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@adonisjs/view", 3 | "version": "6.2.0", 4 | "description": "View/template engine for AdonisJs", 5 | "main": "build/providers/ViewProvider.js", 6 | "files": [ 7 | "build/adonis-typings", 8 | "build/providers", 9 | "build/src", 10 | "build/instructions.md" 11 | ], 12 | "typings": "./build/adonis-typings/index.d.ts", 13 | "scripts": { 14 | "mrm": "mrm --preset=@adonisjs/mrm-preset", 15 | "pretest": "npm run lint", 16 | "test": "node -r @adonisjs/require-ts/build/register bin/test.ts", 17 | "lint": "eslint . --ext=.ts", 18 | "clean": "del-cli build", 19 | "compile": "npm run lint && npm run clean && tsc", 20 | "build": "npm run compile", 21 | "commit": "git-cz", 22 | "release": "np --message=\"chore(release): %s\"", 23 | "version": "npm run build", 24 | "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/view", 25 | "format": "prettier --write .", 26 | "prepublishOnly": "npm run build" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/adonisjs/view.git" 31 | }, 32 | "keywords": [ 33 | "view", 34 | "edge.js", 35 | "template" 36 | ], 37 | "author": "virk,adonisjs", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/adonisjs/view/issues" 41 | }, 42 | "homepage": "https://github.com/adonisjs/view#readme", 43 | "devDependencies": { 44 | "@adonisjs/core": "^5.9.0", 45 | "@adonisjs/mrm-preset": "^5.0.3", 46 | "@adonisjs/repl": "^3.1.11", 47 | "@adonisjs/require-ts": "^2.0.13", 48 | "@adonisjs/sink": "^5.4.2", 49 | "@japa/assert": "^1.3.6", 50 | "@japa/run-failed-tests": "^1.1.0", 51 | "@japa/runner": "^2.2.2", 52 | "@japa/spec-reporter": "^1.3.2", 53 | "@poppinss/dev-utils": "^2.0.3", 54 | "@types/node": "^18.11.9", 55 | "commitizen": "^4.2.5", 56 | "copyfiles": "^2.4.1", 57 | "cz-conventional-changelog": "^3.3.0", 58 | "del-cli": "^5.0.0", 59 | "eslint": "^8.28.0", 60 | "eslint-config-prettier": "^8.5.0", 61 | "eslint-plugin-adonis": "^2.1.1", 62 | "eslint-plugin-prettier": "^4.2.1", 63 | "github-label-sync": "^2.2.0", 64 | "husky": "^8.0.2", 65 | "japa": "^4.0.0", 66 | "mrm": "^4.1.13", 67 | "np": "^7.6.2", 68 | "prettier": "^2.8.0", 69 | "typescript": "^4.8.2" 70 | }, 71 | "peerDependencies": { 72 | "@adonisjs/core": "^5.7.0" 73 | }, 74 | "nyc": { 75 | "exclude": [ 76 | "test" 77 | ], 78 | "extension": [ 79 | ".ts" 80 | ] 81 | }, 82 | "husky": { 83 | "hooks": { 84 | "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" 85 | } 86 | }, 87 | "config": { 88 | "commitizen": { 89 | "path": "cz-conventional-changelog" 90 | } 91 | }, 92 | "np": { 93 | "contents": ".", 94 | "anyBranch": false 95 | }, 96 | "dependencies": { 97 | "edge-error": "^2.0.8", 98 | "edge-supercharged": "^3.1.1", 99 | "edge.js": "^5.5.0" 100 | }, 101 | "adonisjs": { 102 | "types": "@adonisjs/view", 103 | "instructionsMd": "./build/instructions.md", 104 | "providers": [ 105 | "@adonisjs/view" 106 | ], 107 | "env": { 108 | "CACHE_VIEWS": false 109 | }, 110 | "metaFiles": [ 111 | { 112 | "pattern": "resources/views/**/*.edge", 113 | "reloadServer": false 114 | } 115 | ] 116 | }, 117 | "publishConfig": { 118 | "access": "public", 119 | "tag": "latest" 120 | }, 121 | "mrmConfig": { 122 | "core": true, 123 | "license": "MIT", 124 | "services": [ 125 | "github-actions" 126 | ], 127 | "minNodeVersion": "14.15.4", 128 | "probotApps": [ 129 | "stale", 130 | "lock" 131 | ], 132 | "runGhActionsOnWindows": true 133 | }, 134 | "eslintConfig": { 135 | "extends": [ 136 | "plugin:adonis/typescriptPackage", 137 | "prettier" 138 | ], 139 | "plugins": [ 140 | "prettier" 141 | ], 142 | "rules": { 143 | "prettier/prettier": [ 144 | "error", 145 | { 146 | "endOfLine": "auto" 147 | } 148 | ] 149 | } 150 | }, 151 | "eslintIgnore": [ 152 | "build" 153 | ], 154 | "prettier": { 155 | "trailingComma": "es5", 156 | "semi": false, 157 | "singleQuote": true, 158 | "useTabs": false, 159 | "quoteProps": "consistent", 160 | "bracketSpacing": true, 161 | "arrowParens": "always", 162 | "printWidth": 100 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /providers/ViewProvider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { defineViteDriverBindings } from '../src/Bindings/Vite' 11 | import type { DisksList } from '@ioc:Adonis/Core/Drive' 12 | import type { ViewContract } from '@ioc:Adonis/Core/View' 13 | import type { RouterContract } from '@ioc:Adonis/Core/Route' 14 | import type { ApplicationContract } from '@ioc:Adonis/Core/Application' 15 | import type { AssetsManagerContract } from '@ioc:Adonis/Core/AssetsManager' 16 | import type { HttpContextConstructorContract } from '@ioc:Adonis/Core/HttpContext' 17 | 18 | /** 19 | * View provider to register view to the application 20 | */ 21 | export default class ViewProvider { 22 | constructor(protected app: ApplicationContract) {} 23 | 24 | /** 25 | * Add globals for resolving routes 26 | */ 27 | private addRouteGlobal(View: ViewContract, Route: RouterContract) { 28 | /** 29 | * Adding `route` global 30 | */ 31 | View.global('route', (routeIdentifier: string, params?: any, options?: any) => { 32 | return Route.makeUrl(routeIdentifier, params, options) 33 | }) 34 | 35 | /** 36 | * Adding `signedRoute` global 37 | */ 38 | View.global('signedRoute', (routeIdentifier: string, params?: any, options?: any) => { 39 | return Route.makeSignedUrl(routeIdentifier, params, options) 40 | }) 41 | } 42 | 43 | /** 44 | * Share application reference, a config and env variable with the 45 | * templates. 46 | */ 47 | private addGlobals(View: ViewContract, Application: ApplicationContract) { 48 | const Config = Application.container.resolveBinding('Adonis/Core/Config') 49 | const Env = Application.container.resolveBinding('Adonis/Core/Env') 50 | const Drive = Application.container.resolveBinding('Adonis/Core/Drive') 51 | 52 | View.global('app', Application) 53 | View.global('config', (key: string, defaultValue?: any) => Config.get(key, defaultValue)) 54 | View.global('env', (key: string, defaultValue?: any) => Env.get(key, defaultValue)) 55 | 56 | View.global('driveUrl', (location: string, disk?: keyof DisksList) => { 57 | return disk ? Drive.use(disk).getUrl(location) : Drive.getUrl(location) 58 | }) 59 | 60 | View.global('driveSignedUrl', (location: string, disk?: keyof DisksList) => { 61 | return disk ? Drive.use(disk).getSignedUrl(location) : Drive.getSignedUrl(location) 62 | }) 63 | } 64 | 65 | /** 66 | * Copy globals exposed by Edge 67 | */ 68 | private copyEdgeGlobals(View: ViewContract) { 69 | const { GLOBALS } = require('edge.js') 70 | Object.keys(GLOBALS).forEach((key) => View.global(key, GLOBALS[key])) 71 | } 72 | 73 | /** 74 | * Registering the brisk route to render view directly from the route. 75 | */ 76 | private registerBriskRoute(Route: RouterContract) { 77 | Route.BriskRoute.macro('render', function renderView(template: string, data?: any) { 78 | return this.setHandler(({ view }: { view: ViewContract }) => { 79 | return view.render(template, data) 80 | }, 'render') 81 | }) 82 | } 83 | 84 | /** 85 | * Registering the http context getter to access an isolated 86 | * view instance with the request and route. 87 | */ 88 | private registerHTTPContextGetter( 89 | HttpContext: HttpContextConstructorContract, 90 | View: ViewContract 91 | ) { 92 | HttpContext.getter( 93 | 'view', 94 | function () { 95 | return View.share({ request: this.request }) 96 | }, 97 | true 98 | ) 99 | } 100 | 101 | /** 102 | * Decide whether or not to cache views. If a user opts to remove 103 | * the valdation, then `CACHE_VIEWS` will be a string and not 104 | * a boolean, so we need to handle that case 105 | */ 106 | private shouldCacheViews(): boolean { 107 | let cacheViews = this.app.container.resolveBinding('Adonis/Core/Env').get('CACHE_VIEWS') 108 | if (typeof cacheViews === 'string') { 109 | cacheViews = cacheViews === 'true' 110 | } 111 | 112 | return cacheViews 113 | } 114 | 115 | /** 116 | * Register repl binding 117 | */ 118 | private defineReplBindings() { 119 | if (this.app.environment !== 'repl') { 120 | return 121 | } 122 | 123 | this.app.container.withBindings(['Adonis/Addons/Repl'], (Repl) => { 124 | const { defineReplBindings } = require('../src/Bindings/Repl') 125 | defineReplBindings(this.app, Repl) 126 | }) 127 | } 128 | 129 | /** 130 | * Define assets manager bindings 131 | */ 132 | private defineAssetsManagerBindings(View: ViewContract, AssetsManager: AssetsManagerContract) { 133 | const { defineAssetsManagerBindings } = require('../src/Bindings/AssetsManager') 134 | defineAssetsManagerBindings(View, AssetsManager) 135 | 136 | if (AssetsManager.name === 'vite') { 137 | defineViteDriverBindings(View) 138 | } 139 | } 140 | 141 | /** 142 | * Register view binding 143 | */ 144 | public register() { 145 | this.app.container.singleton('Adonis/Core/View', () => { 146 | const { Edge } = require('edge.js') 147 | const { Supercharged } = require('edge-supercharged') 148 | 149 | const cacheViews = this.shouldCacheViews() 150 | const edge = new Edge({ cache: cacheViews }) 151 | 152 | /** 153 | * Mount the views directory 154 | */ 155 | edge.mount(this.app.viewsPath()) 156 | 157 | /** 158 | * Enable recurring mode when not caching views, so that the 159 | * edge supercharged can re-scan components on each 160 | * render call 161 | */ 162 | edge.use(new Supercharged().wire, { recurring: !cacheViews }) 163 | 164 | return edge 165 | }) 166 | } 167 | 168 | /** 169 | * Setup view on boot 170 | */ 171 | public boot() { 172 | const View = this.app.container.resolveBinding('Adonis/Core/View') 173 | const Route = this.app.container.resolveBinding('Adonis/Core/Route') 174 | const HttpContext = this.app.container.resolveBinding('Adonis/Core/HttpContext') 175 | const AssetsManager = this.app.container.resolveBinding('Adonis/Core/AssetsManager') 176 | 177 | /** 178 | * Repl and Assets manager bindings 179 | */ 180 | this.defineReplBindings() 181 | this.defineAssetsManagerBindings(View, AssetsManager) 182 | 183 | /** 184 | * Registering globals 185 | */ 186 | this.addRouteGlobal(View, Route) 187 | this.addGlobals(View, this.app) 188 | this.copyEdgeGlobals(View) 189 | 190 | /** 191 | * Registering the brisk route 192 | */ 193 | this.registerBriskRoute(Route) 194 | 195 | /** 196 | * Registering isolated view renderer with the HTTP context 197 | */ 198 | this.registerHTTPContextGetter(HttpContext, View) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Bindings/AssetsManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { EdgeError } from 'edge-error' 11 | import { ViewContract } from '@ioc:Adonis/Core/View' 12 | import { AssetsManagerContract } from '@ioc:Adonis/Core/AssetsManager' 13 | 14 | /** 15 | * Registers the asset manager tags and globals with the template engine 16 | */ 17 | export function defineAssetsManagerBindings( 18 | View: ViewContract, 19 | AssetsManager: AssetsManagerContract 20 | ) { 21 | View.global('assetsManager', AssetsManager) 22 | View.global('asset', (filename: string) => AssetsManager.assetPath(filename)) 23 | 24 | View.registerTag({ 25 | tagName: 'entryPointStyles', 26 | seekable: true, 27 | block: false, 28 | compile(parser, buffer, token) { 29 | /** 30 | * Ensure an argument is defined 31 | */ 32 | if (!token.properties.jsArg.trim()) { 33 | throw new EdgeError('Missing entrypoint name', 'E_RUNTIME_EXCEPTION', { 34 | filename: token.filename, 35 | line: token.loc.start.line, 36 | col: token.loc.start.col, 37 | }) 38 | } 39 | 40 | const parsed = parser.utils.transformAst( 41 | parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), 42 | token.filename, 43 | parser 44 | ) 45 | 46 | const entrypointName = parser.utils.stringify(parsed) 47 | buffer.outputExpression( 48 | `state.assetsManager.entryPointStyleTags(${entrypointName})`, 49 | token.filename, 50 | token.loc.start.line, 51 | false 52 | ) 53 | }, 54 | }) 55 | 56 | View.registerTag({ 57 | tagName: 'entryPointScripts', 58 | seekable: true, 59 | block: false, 60 | compile(parser, buffer, token) { 61 | /** 62 | * Ensure an argument is defined 63 | */ 64 | if (!token.properties.jsArg.trim()) { 65 | throw new EdgeError('Missing entrypoint name', 'E_RUNTIME_EXCEPTION', { 66 | filename: token.filename, 67 | line: token.loc.start.line, 68 | col: token.loc.start.col, 69 | }) 70 | } 71 | 72 | const parsed = parser.utils.transformAst( 73 | parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), 74 | token.filename, 75 | parser 76 | ) 77 | 78 | const entrypointName = parser.utils.stringify(parsed) 79 | buffer.outputExpression( 80 | `state.assetsManager.entryPointScriptTags(${entrypointName})`, 81 | token.filename, 82 | token.loc.start.line, 83 | false 84 | ) 85 | }, 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /src/Bindings/Repl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { ReplContract } from '@ioc:Adonis/Addons/Repl' 11 | import { ApplicationContract } from '@ioc:Adonis/Core/Application' 12 | 13 | /** 14 | * Define repl bindings. The method must be invoked when application environment 15 | * is set to repl. 16 | */ 17 | export function defineReplBindings(app: ApplicationContract, Repl: ReplContract) { 18 | Repl.addMethod( 19 | 'loadView', 20 | (repl) => { 21 | repl.server.context.View = app.container.use('Adonis/Core/View') 22 | repl.notify( 23 | `Loaded View module. You can access it using the "${repl.colors.underline( 24 | 'View' 25 | )}" variable` 26 | ) 27 | }, 28 | { 29 | description: 'Load view provider and save reference to the "View" variable', 30 | } 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/Bindings/Vite.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import type { ViewContract } from '@ioc:Adonis/Core/View' 11 | 12 | export function defineViteDriverBindings(View: ViewContract) { 13 | /** 14 | * Inject a script needed for the HMR working with React 15 | */ 16 | View.registerTag({ 17 | tagName: 'viteReactRefresh', 18 | seekable: true, 19 | block: false, 20 | compile(_parser, buffer, token) { 21 | buffer.outputExpression( 22 | `state.assetsManager.driver.getReactHmrScript()`, 23 | token.filename, 24 | token.loc.start.line, 25 | false 26 | ) 27 | }, 28 | }) 29 | 30 | /** 31 | * Inject the script needed for Vite HMR 32 | */ 33 | View.registerTag({ 34 | tagName: 'vite', 35 | seekable: false, 36 | block: false, 37 | compile(_parser, buffer, token) { 38 | buffer.outputExpression( 39 | `state.assetsManager.driver.getViteHmrScript()`, 40 | token.filename, 41 | token.loc.start.line, 42 | false 43 | ) 44 | }, 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /test-helpers/drive.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:Adonis/Core/Drive' { 2 | interface DisksList { 3 | local: { 4 | implementation: LocalDriverContract 5 | config: LocalDriverConfig 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test-helpers/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { join } from 'path' 11 | import { Filesystem } from '@poppinss/dev-utils' 12 | import { Application } from '@adonisjs/core/build/standalone' 13 | 14 | export const fs = new Filesystem(join(__dirname, 'app')) 15 | export const APP_KEY = Math.random().toFixed(36).substring(2, 38) 16 | 17 | export async function setup( 18 | environment: 'web' | 'repl', 19 | setupDriveConfig: boolean = false, 20 | assetsDriver: 'vite' | 'encore' = 'encore' 21 | ) { 22 | await fs.add('.env', '') 23 | await fs.add( 24 | 'config/app.ts', 25 | ` 26 | export const appKey = '${APP_KEY}', 27 | export const http = { 28 | cookie: {}, 29 | trustProxy: () => true, 30 | } 31 | 32 | export const assets = { 33 | driver: '${assetsDriver}', 34 | } 35 | ` 36 | ) 37 | 38 | if (setupDriveConfig) { 39 | await fs.add( 40 | 'config/drive.ts', 41 | ` 42 | export const disk = 'local', 43 | export const disks = { 44 | local: { 45 | driver: 'local', 46 | basePath: '/uploads', 47 | serveFiles: true, 48 | root: '${join(fs.basePath, 'uploads').replace(/\\/g, '/')}', 49 | } 50 | } 51 | ` 52 | ) 53 | } 54 | 55 | const app = new Application(fs.basePath, environment, { 56 | providers: ['@adonisjs/core', '@adonisjs/repl', '../../providers/ViewProvider'], 57 | assetsDriver, 58 | }) 59 | 60 | await app.setup() 61 | await app.registerProviders() 62 | await app.bootProviders() 63 | 64 | return app 65 | } 66 | -------------------------------------------------------------------------------- /test/view-provider.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @adonisjs/view 3 | * 4 | * (c) Harminder Virk 5 | * 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | import { test } from '@japa/runner' 11 | import { join } from 'path' 12 | import { Edge } from 'edge.js' 13 | 14 | import { setup, fs, APP_KEY } from '../test-helpers' 15 | 16 | test.group('View Provider', (group) => { 17 | group.each.teardown(async () => { 18 | await fs.cleanup() 19 | }) 20 | 21 | test('register view provider', async ({ assert }) => { 22 | const app = await setup('web') 23 | 24 | assert.instanceOf(app.container.use('Adonis/Core/View'), Edge) 25 | assert.equal( 26 | app.container.use('Adonis/Core/View').loader.mounted.default, 27 | join(fs.basePath, 'resources/views') 28 | ) 29 | }) 30 | 31 | test('register config and env globals', async ({ assert }) => { 32 | const app = await setup('web') 33 | process.env.NODE_ENV = 'development' 34 | 35 | const output = await app.container 36 | .use('Adonis/Core/View') 37 | .renderRaw(`{{ config('app.appKey') }} {{ env('NODE_ENV') }}`) 38 | 39 | assert.equal(output, `${APP_KEY} ${process.env.NODE_ENV}`) 40 | delete process.env.NODE_ENV 41 | }) 42 | 43 | test('share route and signedRoute methods with view', async ({ assert }) => { 44 | const app = await setup('web') 45 | 46 | app.container.use('Adonis/Core/Route').get('/', async () => {}) 47 | app.container.use('Adonis/Core/Route').get('/signed', async () => {}) 48 | app.container.use('Adonis/Core/Route').commit() 49 | 50 | const view = app.container.use('Adonis/Core/View') 51 | view.registerTemplate('dummy', { template: "{{ route('/', {}, 'root') }}" }) 52 | view.registerTemplate('signedDummy', { template: "{{ signedRoute('/signed', {}, 'root') }}" }) 53 | 54 | assert.equal(await view.render('dummy'), '/') 55 | assert.match(await view.render('signedDummy'), /\/signed\?signature=/) 56 | }) 57 | 58 | test('add brisk route macro "render"', async ({ assert }) => { 59 | const app = await setup('web') 60 | assert.isFunction(app.container.use('Adonis/Core/Route').on('/').render) 61 | }) 62 | 63 | test('ensure GLOBALS object exists on the View binding', async ({ assert }) => { 64 | const app = await setup('web') 65 | assert.isDefined(app.container.use('Adonis/Core/View').GLOBALS) 66 | assert.property(app.container.use('Adonis/Core/View').GLOBALS, 'route') 67 | }) 68 | 69 | test('register repl binding', async ({ assert }) => { 70 | const app = await setup('repl') 71 | 72 | assert.property(app.container.use('Adonis/Addons/Repl')['customMethods'], 'loadView') 73 | assert.isFunction( 74 | app.container.use('Adonis/Addons/Repl')['customMethods']['loadView']['handler'] 75 | ) 76 | }) 77 | 78 | test('register view global for the assets manager', async ({ assert }) => { 79 | const app = await setup('web') 80 | assert.property(app.container.use('Adonis/Core/View').GLOBALS, 'asset') 81 | assert.property(app.container.use('Adonis/Core/View').GLOBALS, 'assetsManager') 82 | assert.property(app.container.use('Adonis/Core/View').tags, 'entryPointStyles') 83 | assert.property(app.container.use('Adonis/Core/View').tags, 'entryPointScripts') 84 | }) 85 | 86 | test('register view specific vite global only if enabled', async ({ assert }) => { 87 | const app = await setup('web', false, 'vite') 88 | 89 | assert.property(app.container.use('Adonis/Core/View').tags, 'vite') 90 | assert.property(app.container.use('Adonis/Core/View').tags, 'viteReactRefresh') 91 | }) 92 | 93 | test('should not register vite globals if not enabled', async ({ assert }) => { 94 | const app = await setup('web', false, 'encore') 95 | 96 | assert.isUndefined(app.container.use('Adonis/Core/View').tags.vite) 97 | assert.isUndefined(app.container.use('Adonis/Core/View').tags.viteReactRefresh) 98 | }) 99 | 100 | test('do not register repl binding when not in repl environment', async ({ assert }) => { 101 | const app = await setup('web') 102 | assert.notProperty(app.container.use('Adonis/Addons/Repl')['customMethods'], 'loadView') 103 | }) 104 | 105 | test('register driveUrl and driveSignedUrl globals', async ({ assert }) => { 106 | const app = await setup('web', true) 107 | 108 | app.container.use('Adonis/Core/Route').commit() 109 | 110 | const output = await app.container 111 | .use('Adonis/Core/View') 112 | .renderRaw(`{{ await driveUrl('foo.txt') }}`) 113 | 114 | assert.equal(output.trim(), '/uploads/foo.txt') 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "compilerOptions": { 4 | "skipLibCheck": true, 5 | "types": [ 6 | "@adonisjs/core", 7 | "@adonisjs/repl" 8 | ] 9 | } 10 | } 11 | --------------------------------------------------------------------------------