├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── code.test.ts ├── code.ts ├── defaults.ts ├── deno_tag.ts ├── denoconfig.json └── examples ├── 1 - simple output bundle ├── complex.ts ├── index.html └── index.ts ├── 2 - running a file ├── components │ └── magic-title.html ├── deno_web_component.ts └── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | built/* 3 | \#*\# 4 | .\#* 5 | *.swp 6 | build.json 7 | *.actual 8 | **/.DS_Store 9 | **/.fusebox 10 | .settings 11 | .fusebox 12 | **/.vs 13 | **/.vscode 14 | .idea 15 | yarn.lock 16 | yarn-error.log 17 | .parallelperf.* 18 | *.map 19 | *.jsx 20 | *.code-workspace 21 | *.zip 22 | *.tar.gz 23 | *.gz 24 | internal/ 25 | dist/ 26 | config/ 27 | dist.tar.gz 28 | dist.* 29 | assets 30 | package-lock.json 31 | deploy.sh 32 | prod.sh 33 | decls/ 34 | tmp/ 35 | old/ 36 | video/ 37 | TODO 38 | .env 39 | .env_sandbox 40 | .env_production 41 | SALES 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.1.0] - 2021-03-04 11 | ### Added 12 | - README.md to the examples folder 13 | - `defaults.ts` with the default implementation for the bundler 14 | - Support the new `Deno.emit` compiler API 15 | - Support for Deno v1.8.0 16 | - Export the `DenoTagOptions` interface (no longer exported as a type object) 17 | - Updated CLI usage examples in the README file 18 | 19 | ## [1.0.2] - 2021-01-05 20 | ### Added 21 | - deno_tag first version 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | deno_tag 2 | ======== 3 | 4 | Copyright (c) 2021, Hugo Daniel Henriques Oliveira Gomes All Rights Reserved. Licensed under the EUPL. 5 | 6 | [EUPL v1.2](https://joinup.ec.europa.eu/collection/eupl/eupl-text-11-12) 7 | 8 | # EUROPEAN UNION PUBLIC LICENCE v. 1.2 9 | 10 | This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 11 | below) which is provided under the terms of this Licence. Any use of the Work, 12 | other than as authorised under this Licence is prohibited (to the extent such 13 | use is covered by a right of the copyright holder of the Work). The Work is 14 | provided under the terms of this Licence when the Licensor (as defined below) 15 | has placed the following notice immediately following the copyright notice for 16 | the Work: 17 | 18 | > Licensed under the EUPL 19 | 20 | or has expressed by any other means his willingness to license under the EUPL. 21 | 22 | ## 1. Definitions 23 | 24 | In this Licence, the following terms have the following meaning: 25 | 26 | - ‘The Licence’: this Licence. 27 | 28 | - ‘The Original Work’: the work or software distributed or communicated by the 29 | Licensor under this Licence, available as Source Code and also as Executable 30 | Code as the case may be. 31 | 32 | - ‘Derivative Works’: the works or software that could be created by the 33 | Licensee, based upon the Original Work or modifications thereof. This Licence 34 | does not define the extent of modification or dependence on the Original Work 35 | required in order to classify a work as a Derivative Work; this extent is 36 | determined by copyright law applicable in the country mentioned in Article 15. 37 | 38 | - ‘The Work’: the Original Work or its Derivative Works. 39 | 40 | - ‘The Source Code’: the human-readable form of the Work which is the most 41 | convenient for people to study and modify. 42 | 43 | - ‘The Executable Code’: any code which has generally been compiled and which is 44 | meant to be interpreted by a computer as a program. 45 | 46 | - ‘The Licensor’: the natural or legal person that distributes or communicates 47 | the Work under the Licence. 48 | 49 | - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 50 | Licence, or otherwise contributes to the creation of a Derivative Work. 51 | 52 | - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of the 53 | Work under the terms of the Licence. 54 | 55 | - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 56 | renting, distributing, communicating, transmitting, or otherwise making 57 | available, online or offline, copies of the Work or providing access to its 58 | essential functionalities at the disposal of any other natural or legal 59 | person. 60 | 61 | ## 2. Scope of the rights granted by the Licence 62 | 63 | The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 64 | sublicensable licence to do the following, for the duration of copyright vested 65 | in the Original Work: 66 | 67 | - use the Work in any circumstance and for all usage, 68 | - reproduce the Work, 69 | - modify the Work, and make Derivative Works based upon the Work, 70 | - communicate to the public, including the right to make available or display 71 | the Work or copies thereof to the public and perform publicly, as the case may 72 | be, the Work, 73 | - distribute the Work or copies thereof, 74 | - lend and rent the Work or copies thereof, 75 | - sublicense rights in the Work or copies thereof. 76 | 77 | Those rights can be exercised on any media, supports and formats, whether now 78 | known or later invented, as far as the applicable law permits so. 79 | 80 | In the countries where moral rights apply, the Licensor waives his right to 81 | exercise his moral right to the extent allowed by law in order to make effective 82 | the licence of the economic rights here above listed. 83 | 84 | The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 85 | any patents held by the Licensor, to the extent necessary to make use of the 86 | rights granted on the Work under this Licence. 87 | 88 | ## 3. Communication of the Source Code 89 | 90 | The Licensor may provide the Work either in its Source Code form, or as 91 | Executable Code. If the Work is provided as Executable Code, the Licensor 92 | provides in addition a machine-readable copy of the Source Code of the Work 93 | along with each copy of the Work that the Licensor distributes or indicates, in 94 | a notice following the copyright notice attached to the Work, a repository where 95 | the Source Code is easily and freely accessible for as long as the Licensor 96 | continues to distribute or communicate the Work. 97 | 98 | ## 4. Limitations on copyright 99 | 100 | Nothing in this Licence is intended to deprive the Licensee of the benefits from 101 | any exception or limitation to the exclusive rights of the rights owners in the 102 | Work, of the exhaustion of those rights or of other applicable limitations 103 | thereto. 104 | 105 | ## 5. Obligations of the Licensee 106 | 107 | The grant of the rights mentioned above is subject to some restrictions and 108 | obligations imposed on the Licensee. Those obligations are the following: 109 | 110 | **Attribution right**: The Licensee shall keep intact all copyright, patent or 111 | trademarks notices and all notices that refer to the Licence and to the 112 | disclaimer of warranties. The Licensee must include a copy of such notices and a 113 | copy of the Licence with every copy of the Work he/she distributes or 114 | communicates. The Licensee must cause any Derivative Work to carry prominent 115 | notices stating that the Work has been modified and the date of modification. 116 | 117 | **Copyleft clause**: If the Licensee distributes or communicates copies of the 118 | Original Works or Derivative Works, this Distribution or Communication will be 119 | done under the terms of this Licence or of a later version of this Licence 120 | unless the Original Work is expressly distributed only under this version of the 121 | Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 122 | (becoming Licensor) cannot offer or impose any additional terms or conditions on 123 | the Work or Derivative Work that alter or restrict the terms of the Licence. 124 | 125 | **Compatibility clause**: If the Licensee Distributes or Communicates Derivative 126 | Works or copies thereof based upon both the Work and another work licensed under 127 | a Compatible Licence, this Distribution or Communication can be done under the 128 | terms of this Compatible Licence. For the sake of this clause, ‘Compatible 129 | Licence’ refers to the licences listed in the appendix attached to this 130 | Licence. Should the Licensee's obligations under the Compatible Licence conflict 131 | with his/her obligations under this Licence, the obligations of the Compatible 132 | Licence shall prevail. 133 | 134 | **Provision of Source Code**: When distributing or communicating copies of the 135 | Work, the Licensee will provide a machine-readable copy of the Source Code or 136 | indicate a repository where this Source will be easily and freely available for 137 | as long as the Licensee continues to distribute or communicate the Work. Legal 138 | Protection: This Licence does not grant permission to use the trade names, 139 | trademarks, service marks, or names of the Licensor, except as required for 140 | reasonable and customary use in describing the origin of the Work and 141 | reproducing the content of the copyright notice. 142 | 143 | ## 6. Chain of Authorship 144 | 145 | The original Licensor warrants that the copyright in the Original Work granted 146 | hereunder is owned by him/her or licensed to him/her and that he/she has the 147 | power and authority to grant the Licence. 148 | 149 | Each Contributor warrants that the copyright in the modifications he/she brings 150 | to the Work are owned by him/her or licensed to him/her and that he/she has the 151 | power and authority to grant the Licence. 152 | 153 | Each time You accept the Licence, the original Licensor and subsequent 154 | Contributors grant You a licence to their contributions to the Work, under the 155 | terms of this Licence. 156 | 157 | ## 7. Disclaimer of Warranty 158 | 159 | The Work is a work in progress, which is continuously improved by numerous 160 | Contributors. It is not a finished work and may therefore contain defects or 161 | ‘bugs’ inherent to this type of development. 162 | 163 | For the above reason, the Work is provided under the Licence on an ‘as is’ basis 164 | and without warranties of any kind concerning the Work, including without 165 | limitation merchantability, fitness for a particular purpose, absence of defects 166 | or errors, accuracy, non-infringement of intellectual property rights other than 167 | copyright as stated in Article 6 of this Licence. 168 | 169 | This disclaimer of warranty is an essential part of the Licence and a condition 170 | for the grant of any rights to the Work. 171 | 172 | ## 8. Disclaimer of Liability 173 | 174 | Except in the cases of wilful misconduct or damages directly caused to natural 175 | persons, the Licensor will in no event be liable for any direct or indirect, 176 | material or moral, damages of any kind, arising out of the Licence or of the use 177 | of the Work, including without limitation, damages for loss of goodwill, work 178 | stoppage, computer failure or malfunction, loss of data or any commercial 179 | damage, even if the Licensor has been advised of the possibility of such 180 | damage. However, the Licensor will be liable under statutory product liability 181 | laws as far such laws apply to the Work. 182 | 183 | ## 9. Additional agreements 184 | 185 | While distributing the Work, You may choose to conclude an additional agreement, 186 | defining obligations or services consistent with this Licence. However, if 187 | accepting obligations, You may act only on your own behalf and on your sole 188 | responsibility, not on behalf of the original Licensor or any other Contributor, 189 | and only if You agree to indemnify, defend, and hold each Contributor harmless 190 | for any liability incurred by, or claims asserted against such Contributor by 191 | the fact You have accepted any warranty or additional liability. 192 | 193 | ## 10. Acceptance of the Licence 194 | 195 | The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 196 | placed under the bottom of a window displaying the text of this Licence or by 197 | affirming consent in any other similar way, in accordance with the rules of 198 | applicable law. Clicking on that icon indicates your clear and irrevocable 199 | acceptance of this Licence and all of its terms and conditions. 200 | 201 | Similarly, you irrevocably accept this Licence and all of its terms and 202 | conditions by exercising any rights granted to You by Article 2 of this Licence, 203 | such as the use of the Work, the creation by You of a Derivative Work or the 204 | Distribution or Communication by You of the Work or copies thereof. 205 | 206 | ## 11. Information to the public 207 | 208 | In case of any Distribution or Communication of the Work by means of electronic 209 | communication by You (for example, by offering to download the Work from a 210 | remote location) the distribution channel or media (for example, a website) must 211 | at least provide to the public the information requested by the applicable law 212 | regarding the Licensor, the Licence and the way it may be accessible, concluded, 213 | stored and reproduced by the Licensee. 214 | 215 | ## 12. Termination of the Licence 216 | 217 | The Licence and the rights granted hereunder will terminate automatically upon 218 | any breach by the Licensee of the terms of the Licence. 219 | 220 | Such a termination will not terminate the licences of any person who has 221 | received the Work from the Licensee under the Licence, provided such persons 222 | remain in full compliance with the Licence. 223 | 224 | ## 13. Miscellaneous 225 | 226 | Without prejudice of Article 9 above, the Licence represents the complete 227 | agreement between the Parties as to the Work. 228 | 229 | If any provision of the Licence is invalid or unenforceable under applicable 230 | law, this will not affect the validity or enforceability of the Licence as a 231 | whole. Such provision will be construed or reformed so as necessary to make it 232 | valid and enforceable. 233 | 234 | The European Commission may publish other linguistic versions or new versions of 235 | this Licence or updated versions of the Appendix, so far this is required and 236 | reasonable, without reducing the scope of the rights granted by the Licence. 237 | New versions of the Licence will be published with a unique version number. 238 | 239 | All linguistic versions of this Licence, approved by the European Commission, 240 | have identical value. Parties can take advantage of the linguistic version of 241 | their choice. 242 | 243 | ## 14. Jurisdiction 244 | 245 | Without prejudice to specific agreement between parties, 246 | 247 | - any litigation resulting from the interpretation of this License, arising 248 | between the European Union institutions, bodies, offices or agencies, as a 249 | Licensor, and any Licensee, will be subject to the jurisdiction of the Court 250 | of Justice of the European Union, as laid down in article 272 of the Treaty on 251 | the Functioning of the European Union, 252 | 253 | - any litigation arising between other parties and resulting from the 254 | interpretation of this License, will be subject to the exclusive jurisdiction 255 | of the competent court where the Licensor resides or conducts its primary 256 | business. 257 | 258 | ## 15. Applicable Law 259 | 260 | Without prejudice to specific agreement between parties, 261 | 262 | - this Licence shall be governed by the law of the European Union Member State 263 | where the Licensor has his seat, resides or has his registered office, 264 | 265 | - this licence shall be governed by Belgian law if the Licensor has no seat, 266 | residence or registered office inside a European Union Member State. 267 | 268 | ## *Appendix* 269 | 270 | ‘Compatible Licences’ according to Article 5 EUPL are: 271 | 272 | - GNU General Public License (GPL) v. 2, v. 3 273 | - GNU Affero General Public License (AGPL) v. 3 274 | - Open Software License (OSL) v. 2.1, v. 3.0 275 | - Eclipse Public License (EPL) v. 1.0 276 | - CeCILL v. 2.0, v. 2.1 277 | - Mozilla Public Licence (MPL) v. 2 278 | - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 279 | - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 280 | works other than software 281 | - European Union Public Licence (EUPL) v. 1.1, v. 1.2 282 | - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 283 | Reciprocity (LiLiQ-R+). 284 | 285 | The European Commission may update this Appendix to later versions of the above 286 | licences without producing a new version of the EUPL, as long as they provide 287 | the rights granted in Article 2 of this Licence and protect the covered Source 288 | Code from exclusive appropriation. 289 | 290 | All other changes or additions to this Appendix require the production of a new 291 | EUPL version. 292 | 293 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deno Tag 2 | 3 | A cli command that allows you to write `` tags in your html files. 4 | 5 | It looks for `` tags in a supplied input file and replaces the `` 6 | tags found with the output from running `deno` with their attributes. 7 | 8 | The result is written to `stdout` and can be piped to the desired output. 9 | 10 | I wrote [a blog post about this project](https://hugodaniel.com/posts/introducing-deno-tag/) 11 | that serves as a less dry description of it. 12 | 13 | ## Attributes 14 | 15 | The `` tag needs to have one of these two attributes present: 16 | 17 | - `` 18 | 19 | Calls `deno bundle file.ts` and replaces the `` tag with the produced 20 | output. 21 | 22 | - `` 23 | 24 | Calls `deno run file.ts` and replaces the `` tag with the produced 25 | `stdout` content. 26 | Any other attribute on the tag gets passed as argument to the `Deno.run` 27 | command. Boolean attributes get the string "true" added as value. 28 | 29 | ## How to run 30 | 31 | Call `deno_tag` from the command line and specify a `html` file: 32 | 33 | `> deno run --allow-read --allow-run --unstable https://deno.land/x/deno_tag@v1.1.0/deno_tag.ts index.html` 34 | 35 | It is necessary to have read and run permissions in order to read the file 36 | passed as an attribute on the `` tag and run it. 37 | 38 | 39 | ## Examples 40 | 41 | ### Example 1 - Simple output bundle 42 | 43 | In its most basic usage the `` tag can either give you a bundle from a 44 | file and its dependencies, or just the simple output that a running file 45 | produced. 46 | 47 | Bundle can be useful to produce the full code for your project and place it 48 | inside the HTML `` tag: 49 | 50 | ```html 51 | 52 | 53 | 54 | 55 | Awesome Web App 56 | 57 | 58 |

What a great app

59 | 60 | 63 | 64 | 65 | ``` 66 | 67 | and your `index.ts` could then include a lot of imports. 68 | 69 | ```typescript 70 | // index.ts 71 | import { complexFunction } from "./complex.ts"; 72 | 73 | document.addEventListener("DOMContentLoaded", complexFunction); 74 | ``` 75 | 76 | and then have a really complex function that might need a ton of imports or not: 77 | 78 | ```typescript 79 | // complex.ts 80 | function complexFunction() { 81 | return 42; 82 | } 83 | export { complexFunction }; 84 | ``` 85 | 86 | Running `deno_tag` on it: 87 | 88 | `> deno run --allow-read --allow-run --unstable https://deno.land/x/deno_tag@v1.1.0/deno_tag.ts ./examples/1\ -\ simple\ output\ bundle/index.html` 89 | 90 | Produces the following output: 91 | 92 | ```html 93 | 94 | 95 | 96 | 97 | Awesome Web App 98 | 99 | 100 |

What a great app

101 | 102 | 108 | 109 | 110 | ``` 111 | 112 | Notice the unrolled bundle inside the `` tag. 113 | 114 | This example is available in the `examples` folder. 115 | 116 | ### Example 2 - Running a file 117 | 118 | You can run Deno on any file and place the output of the execution where the 119 | `` tag is located at. 120 | 121 | There are many use cases for this, a simple one I can think of use is to produce 122 | single file web components. This is something that is lacking from Web 123 | Components because they require us to write code in multiple files or just 124 | place the html and styles inside the Web Component constructor which can be 125 | hard to maintain. 126 | 127 | I don't intend to solve this in any way (there are whole frameworks from much 128 | smarter people out there). This is however a good idea for a very simple 129 | example. 130 | 131 | With the `` tag a basic single file web component approach could be done 132 | with something like this: 133 | 134 | ```html 135 | 136 | 137 | 138 | 139 | Magic Web App 140 | 141 | 142 | 143 | What a great app 144 | 145 | 146 | 147 | 148 | 149 | ``` 150 | 151 | Where the `` component could be defined in a single 152 | `magic-title.html` file. Here for example purposes: 153 | 154 | ```html 155 | 160 |
161 |

162 | 163 |

164 |
165 | 168 | ``` 169 | 170 | Finally all the work of defining the element would be done by the 171 | `deno_web_component.ts` script that the `` is running: 172 | 173 | ```typescript 174 | // As expected, it is possible to use dependencies to do whatever 175 | // In this case, a simple dom parser will be used as an example 176 | import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts"; 177 | 178 | // Arguments get passed as in the tag, which in this case is: 179 | // src="components/magic_title.ts" 180 | const file = Deno.args[0].slice( 181 | Deno.args[0].indexOf('"') + 1, 182 | Deno.args[0].lastIndexOf('"') 183 | ); 184 | const text = await Deno.readTextFile(file); 185 | // Parse the component file and take the script code in the "code" variable 186 | const doc = new DOMParser().parseFromString(text, "text/html")!; 187 | const scriptTags = doc.getElementsByTagName("script")!; 188 | let code = ""; 189 | if (scriptTags.length > 0) { 190 | code = scriptTags[0].textContent; 191 | } 192 | // Remove the `` content from the file text 193 | const template = text.slice(0, text.indexOf("`); 214 | ``` 215 | 216 | This is just for example purposes, don't use this in anything serious. 217 | 218 | The code from the `` tag is run with the `--allow-read` and `--allow-run` 219 | permissions set. 220 | 221 | After running: 222 | 223 | `> deno run --allow-read --allow-run --unstable https://deno.land/x/deno_tag@v1.1.0/deno_tag.ts index.html` 224 | 225 | The output is: 226 | 227 | ```html 228 | 229 | 230 | 231 | 232 | Magic Web App 233 | 234 | 235 | 236 | What a great app 237 | 238 | 239 | 265 | 266 | 267 | 268 | 269 | ``` 270 | 271 | Notice that the `` tag was replaced by the output of the execution of 272 | `deno_web_component.ts`. Where the `"magic-title"` web component is now being 273 | defined. 274 | -------------------------------------------------------------------------------- /code.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple tests for the deno_tag, for now most usage is being covered. 3 | * 4 | * Only the `denoTag` input function is being tested because this is also the 5 | * only export from the `code.ts` module. 6 | * 7 | * These tests must be run with the `unstable` flag: 8 | * > `deno test --unstable code.test.ts` 9 | * 10 | * This is because the code makes usage of the unstable `Deno.bundle` and 11 | * `Diagnostic` code parts. 12 | */ 13 | 14 | import { denoTag } from "./code.ts"; 15 | import { defaultBundler } from "./defaults.ts"; 16 | import { 17 | assert, 18 | assertEquals, 19 | } from "https://deno.land/std@0.83.0/testing/asserts.ts"; 20 | 21 | Deno.test( 22 | "Calls the runner function with the value from the tag", 23 | async () => { 24 | const file = "test.ts"; 25 | const runner = createRunnerMock((value) => { 26 | assert(value.cmd); 27 | assertEquals( 28 | value.cmd, 29 | ["deno", "run", "--allow-read", "--allow-run", file], 30 | ); 31 | }); 32 | const htmlText = ``; 33 | await denoTag(htmlText, { runner }); 34 | }, 35 | ); 36 | 37 | Deno.test( 38 | "Calls the runner function with output piped", 39 | async () => { 40 | const runner = createRunnerMock((value) => { 41 | assert(value.stdout); 42 | assertEquals(value.stdout, "piped"); 43 | }); 44 | const htmlText = ``; 45 | await denoTag(htmlText, { runner }); 46 | }, 47 | ); 48 | 49 | Deno.test( 50 | "Sets the runner function arguments to be the tag attributes", 51 | async () => { 52 | const runner = createRunnerMock((value) => { 53 | assert(value.cmd); 54 | assertEquals( 55 | value.cmd, 56 | [ 57 | "deno", 58 | "run", 59 | "--allow-read", 60 | "--allow-run", 61 | "file.ts", 62 | 'arg1="test"', 63 | 'arg2="something"', 64 | ], 65 | ); 66 | }); 67 | const htmlText = ``; 68 | await denoTag(htmlText, { runner }); 69 | }, 70 | ); 71 | 72 | Deno.test( 73 | 'Places a value of "true" on the boolean attributes of the tag', 74 | async () => { 75 | const runner = createRunnerMock((value) => { 76 | assert(value.cmd); 77 | assertEquals( 78 | value.cmd, 79 | [ 80 | "deno", 81 | "run", 82 | "--allow-read", 83 | "--allow-run", 84 | "file.ts", 85 | 'boolean="true"', 86 | 'isOk="true"', 87 | ], 88 | ); 89 | }); 90 | const htmlText = ``; 91 | await denoTag(htmlText, { runner }); 92 | }, 93 | ); 94 | 95 | Deno.test( 96 | "Can handle multi-line tags", 97 | async () => { 98 | const runner = createRunnerMock((value) => { 99 | assert(value.cmd); 100 | assertEquals( 101 | value.cmd, 102 | [ 103 | "deno", 104 | "run", 105 | "--allow-read", 106 | "--allow-run", 107 | "file.ts", 108 | 'boolean="true"', 109 | 'isOk="true"', 110 | ], 111 | ); 112 | }); 113 | const htmlText = ` 114 |

This is a simple test

115 | `; 120 | await denoTag(htmlText, { runner }); 121 | }, 122 | ); 123 | 124 | Deno.test( 125 | "Can handle tags inside other tags", 126 | async () => { 127 | const runner = createRunnerMock((value) => { 128 | assert(value.cmd); 129 | assertEquals( 130 | value.cmd, 131 | [ 132 | "deno", 133 | "run", 134 | "--allow-read", 135 | "--allow-run", 136 | "file.ts", 137 | 'boolean="true"', 138 | 'isOk="true"', 139 | ], 140 | ); 141 | }); 142 | const htmlText = ` 143 |

This is a simple test 144 | 145 |

`; 146 | await denoTag(htmlText, { runner }); 147 | }, 148 | ); 149 | 150 | Deno.test( 151 | "Calls the bundler function with the value from the tag", 152 | async () => { 153 | const file = "test.ts"; 154 | const bundler: typeof defaultBundler = (value, opts) => { 155 | assertEquals(value, file); 156 | return Promise.resolve( 157 | { files: { "deno:///bundle.js": "" } } as unknown as Deno.EmitResult, 158 | ); 159 | }; 160 | const htmlText = ``; 161 | await denoTag(htmlText, { bundler }); 162 | }, 163 | ); 164 | 165 | /** 166 | * Helper function that creates a mock of the `Deno.run` function that runs 167 | * the function provided as argument and always returns a dummy `Deno.Process` 168 | * object. 169 | **/ 170 | const createRunnerMock = (run: (value: Deno.RunOptions) => void) => 171 | (value: Deno.RunOptions) => { 172 | run(value); 173 | return ({ 174 | rid: 0, 175 | pid: 0, 176 | stdin: null, 177 | stdout: null, 178 | stderr: null, 179 | status() { 180 | return Promise.resolve({ success: true, code: 0, signal: undefined }); 181 | }, 182 | output() { 183 | return Promise.resolve(new Uint8Array()); 184 | }, 185 | stderrOutput() { 186 | return Promise.resolve(new Uint8Array()); 187 | }, 188 | close() {}, 189 | kill(signo: number) {}, 190 | }) as ReturnType; 191 | }; 192 | -------------------------------------------------------------------------------- /code.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Hugo Daniel Henriques Oliveira Gomes. All rights reserved. 2 | // Licensed under the EUPL 3 | import { defaultBundler } from "./defaults.ts"; 4 | /** 5 | * The main function of deno-tag, it does three things: 6 | * 1. Looks for `` tags on the supplied text and reads their attributes 7 | * 2. Runs `deno` for each found `` tag 8 | * 3. Creates a new text where each `` tag is replaced by the output of 9 | * its respective run (from the previous point) 10 | * 11 | * Finally it returns this new changed text. 12 | * 13 | * There are two supported attributes for the `` tag: 14 | * - `` 15 | * * Replaces the tag with the output from running the `options.bundler` 16 | * `someCode.ts` (by default the `options.bundler` is `Deno.bundler`) 17 | * - `` 18 | * * Replaces the tag with the console output from running `options.runner` 19 | * on `someCode.ts` (by default the `options.runner` is `Deno.run`) 20 | * 21 | * It assumes that the file path can be found from `Deno.cwd`. (see `cli.ts`) 22 | * The file paths passed on the `` are relative to the location of the 23 | * file that holds the `` tag. 24 | * 25 | * **Note:** Padding is preserved, the output is set on the text at the same 26 | * indentation level that the respective `` tag is written at. 27 | */ 28 | export async function denoTag(text: string, options?: DenoTagOptions) { 29 | // Parsing the deno tags is about reading the text string, looking for 30 | // `` tags, and process their attributes and values. 31 | // This `parseDenoTag` function returns the attributes of each deno tag as 32 | // well as their location on the original text string. 33 | const parseResults = parseDenoTag(text); 34 | // Executing the deno tags is about running either the bundler or the deno 35 | // `run` on each of the found tag. The `executeDenoTags` processes each of 36 | // the parsed arguments on the `parseResults.tags` Map. 37 | // It returns a list of Result objects, these objects specify the output 38 | // string for each deno bundle/run action performed as well as the lines 39 | // that should be replaced on the original file with this output. 40 | const executionResults = await executeDenoTags(parseResults, options); 41 | // Finally perform the replace action on the text. It removes the lines 42 | // that correspond to each deno tag and replaces them with the contents from 43 | // the output of its respective bundle/run. This is a pure action, a new text 44 | // is returned. Original text is preserved. 45 | const changedText = replaceTagsWithResults(text, executionResults); 46 | 47 | return changedText; 48 | } 49 | 50 | /** 51 | * Optionally the `denoTag()` function can be IO free, all IO is optional 52 | * and can be set through its second argument, which expects an object of 53 | * this type. 54 | * 55 | * The defaults are: 56 | * 57 | * - `bundler`: `Deno.bundle` 58 | * - `runner`: `Deno.run` 59 | * 60 | * Bundler is called when a `` is found. 61 | * Runner is called when a `` is found. 62 | */ 63 | export interface DenoTagOptions { 64 | bundler?: typeof defaultBundler; 65 | bundleOptions?: Deno.EmitOptions; 66 | runner?: typeof Deno.run; 67 | runOptions?: Deno.RunOptions; 68 | } 69 | 70 | /** 71 | * This function will perform the `bundler` or the `runner` action on a 72 | * single processed `` tag argument Map. 73 | * 74 | * By default the `bundler` is done by calling `Deno.bundle` and the `runner` 75 | * is done by calling `Deno.run` (which runs the cli command `deno run [file]`). 76 | * 77 | * These can be overridden on the `options` argument and can even be plain 78 | * functions that do not perform IO (e.g. for testing purposes). 79 | * 80 | * It returns the string output of the respective action performed 81 | * (either a file deno bundle or a deno run). 82 | **/ 83 | async function runDeno(args: Map, options?: DenoTagOptions) { 84 | // These are the default "action" and "file" (which is no file, this is ok 85 | // because this function is only intended to be run after some validation on 86 | // the input arguments have been performed) 87 | let action = "run"; 88 | let file = ""; 89 | const flags: string[] = []; 90 | // Read the deno command arguments, this prepares the variables declared 91 | // above, so they can be used to get the desired output after 92 | for (const [attr, value] of args.entries()) { 93 | switch (attr) { 94 | case "run": 95 | file = value.slice(1, -1); // Remove the wrapping "" chars 96 | break; 97 | case "bundle": 98 | action = "bundle"; 99 | file = value.slice(1, -1); // Remove the wrapping "" chars 100 | break; 101 | default: 102 | flags.push(`${attr}=${value}`); 103 | } 104 | } 105 | // Now that the action, file, and flags variables are set, it is time to 106 | // check the options object and fill it with sane defaults. 107 | const runner = options?.runner || Deno.run; 108 | const runOptions = options?.runOptions || { 109 | cmd: ["deno", "run", "--allow-read", "--allow-run"], 110 | stdout: "piped", 111 | }; // ^ by default call "deno run" with the stdout piped to a Uint8Array, this 112 | // allows output to be caught by this process (parent) and handled later on. 113 | const bundler = options?.bundler || defaultBundler; 114 | // Prepare to run the supplied action attribute on the file from its value 115 | // The output of the runner (`Deno.run` by default) is a Uint8Array which is 116 | // processed by the TextDecoder into a JS string after the run finishes. 117 | let runOutput: Uint8Array; 118 | // deno-lint-ignore prefer-const 119 | let bundleOutput: Deno.EmitResult; 120 | let result = ""; 121 | switch (action) { 122 | case "run": 123 | // Append the [file] to the command line array to be run, and also all the 124 | // extra attributes found on the deno tag 125 | runOptions.cmd = [...runOptions.cmd, file, ...flags]; 126 | try { 127 | // Runs the file provided and transforms its output to be a JS string 128 | runOutput = await runner(runOptions).output(); 129 | result = new TextDecoder("utf-8").decode(runOutput); 130 | } catch (e) { 131 | console.error(e); 132 | } 133 | break; 134 | case "bundle": 135 | // The "files" of the result will contain a single key named 136 | // "deno:///bundle.js" of which the value with be the resulting bundle. 137 | bundleOutput = await bundler( 138 | file, 139 | options?.bundleOptions, 140 | ); 141 | // The bundler output is an EmitResult object. 142 | // The result is the single key "deno:///bundle.js" of the files object 143 | result = bundleOutput.files["deno:///bundle.js"]; 144 | break; 145 | } 146 | // The string with the output of the action that was performed 147 | return result; 148 | } 149 | 150 | /** Attributes in a `` tag. A Map of "attribute"="value" strings */ 151 | type DenoTagAttributes = Map; 152 | /** 153 | * The result of reading a text and looking for `` tags and their 154 | * attributes 155 | **/ 156 | interface ParsedText { 157 | // The parsed text without any modifications done on it 158 | original: string; 159 | // The `` tags found in the original text, this is a map of the 160 | // line number where the tag is opened and closed. 161 | // There can be multiple `` tags per each lineOpened/lineClosed pair, 162 | // and this is why the Value for the map is an array of DenoTagAttributes 163 | tags: Map< 164 | { lineOpened: number; lineClosed: number; indent: number }, 165 | DenoTagAttributes[] 166 | >; 167 | } 168 | 169 | /** 170 | * This function reads a single line with one or more `` tags and 171 | * transforms those tags into `DenoTagAttributes` objects. 172 | * These objects are collected in an array to be returned. 173 | * 174 | * Attributes without values have their value set to the string "true" 175 | * Every string value on the resulting Map's includes the enclosing \" \" 176 | * string characters. 177 | */ 178 | function parseDenoTagArgs(line: string): DenoTagAttributes[] { 179 | const result = [] as DenoTagAttributes[]; 180 | const denoTags = line.split(" tag 182 | for (let i = 1; i < denoTags.length; i++) { 183 | const attributes = denoTags[i] 184 | .slice(0, denoTags[i].indexOf(">")) 185 | .split(" "); 186 | // ^ split the inside of the " tag 197 | let token = attributes[j]; 198 | // parse only if this token does not belong to the list of tokens to 199 | // ignore 200 | if (["", "/"].includes(token)) continue; 201 | // if the token includes an "=" char, then it has a key (left value of =) 202 | // and it has a value that can be made of multiple tokens (i.e. everything 203 | // between " ") 204 | if (token.includes("=")) { 205 | const splitToken = token.split("="); 206 | // the left part is the key 207 | key = splitToken[0]; 208 | // it is possible to process the rest of the token (right part) 209 | // on this loop - check if it ends with the " char or build the partial 210 | // value if it doesn't 211 | token = splitToken[1]; 212 | } 213 | if (token.endsWith('"')) { 214 | value = partialValue + token; 215 | partialValue = ""; // Reset the partialValue 216 | } else { 217 | if (!key) { 218 | // this is a single attribute with no "=" value 219 | key = token; 220 | value = '"true"'; 221 | } else { 222 | // A key is present, and this token does not end with a \" char. 223 | // This means that this is an attribute with a multiple space string 224 | // as a value that did not finish at this token 225 | // i.e. key="this is a value", 226 | partialValue += token; 227 | } 228 | } 229 | // Decide if this is the right time to add 230 | if (key && value) { 231 | tagAttributes.push([key, value]); 232 | key = null; 233 | value = null; 234 | } 235 | } 236 | // Create a new Map of the deno attributes, and place it in the array of 237 | // deno executions; deno will be called once for each element on `denoRuns` 238 | result.push(new Map(tagAttributes)); 239 | } 240 | return result; 241 | } 242 | 243 | /** 244 | * Reads the full text and processes the attributes of each `` into 245 | * a `ParsedText` object to be returned. 246 | * 247 | * **Note:** Takes in consideration multi-line `` tags and multiple tags 248 | * per line 249 | **/ 250 | function parseDenoTag(text: string): ParsedText { 251 | // A Map of attributes is created for each `` tag found on the text 252 | // Deno will be called once for each element on `denoRuns`, each element of 253 | // this array is a Map of the attributes on a `` tag 254 | const result: ParsedText = { original: text, tags: new Map() }; 255 | 256 | const lines = text.split("\n"); 257 | let isMultiLine = false; 258 | // Holds all lines that belong to a `` tag 259 | let tagLines = [] as string[]; 260 | // Line number where the opening `` tag was found 261 | let lineStart = 0; 262 | // The number of padding spaces on the `lineStart` line 263 | let lineStartPad = 0; 264 | // Loop through each line and look for `" or with "" 273 | const closedTags = (line.split("/>").length - 1) + 274 | (line.split("").length - 1); 275 | // Mark this line as belonging to a (deno) tag if there is an opened 276 | // deno tag in it or if a multi-line tag has not ended yet 277 | if (openedDenoTags > 0 || isMultiLine) { 278 | tagLines.push(line); 279 | // The following contents has already run if this line belongs to a 280 | // multi-line deno tag 281 | if (!isMultiLine) { 282 | // A new line with a tag, set it as the start of processing 283 | lineStart = lineNumber; 284 | // Store the padding for this line (useful to adjust the contents of 285 | // the deno tag result) 286 | lineStartPad = line.length - line.trimLeft().length; 287 | } 288 | } 289 | // Flag if this line belongs to a tag 290 | const hasAnOpenTag: boolean = isMultiLine || openedDenoTags > 0; 291 | // Flag if this line closes all opened `` tags 292 | // A multi-line tag counts as 1 opened `` tag. 293 | const closesWhatOpens: boolean = 294 | (Number(isMultiLine) + openedDenoTags) === closedTags; 295 | if (hasAnOpenTag && closesWhatOpens) { 296 | // Process line and clear tagLines 297 | result.tags.set( 298 | { lineOpened: lineStart, lineClosed: lineNumber, indent: lineStartPad }, 299 | // This is where the deno tag gets transformed into a 300 | // `DenoTagAttributes` array 301 | parseDenoTagArgs(tagLines.join(" ")), 302 | ); 303 | // Clear line carry state 304 | tagLines = []; 305 | isMultiLine = false; 306 | } else { 307 | // Start a multi-line carry state if there is an opened tag 308 | // without a matching close on this line 309 | isMultiLine = hasAnOpenTag && !closesWhatOpens; 310 | } 311 | } 312 | 313 | return result; 314 | } 315 | 316 | /** 317 | * Represents the output of running a `` tag. Includes the output of the 318 | * deno bundle/run as well as the location where it should be placed on the 319 | * original text string (the line numbers "from"/"to" that will be removed for 320 | * these contents). 321 | **/ 322 | interface Result { 323 | contents: string; 324 | from: number; 325 | to: number; 326 | } 327 | /** 328 | * This function calls `runDeno()` for each `` tag found. 329 | * The output string for each run is then properly padded to match the 330 | * indentation supplied for each tag on the `ParsedText` argument. 331 | */ 332 | async function executeDenoTags( 333 | parseResults: ParsedText, 334 | options?: DenoTagOptions, 335 | ): Promise { 336 | const results = []; // Each deno output is pushed into this results array 337 | 338 | // Loop through each tag found, its attributes and line meta-information: 339 | // location and indentation 340 | for (const [lineLimits, denoAttributes] of parseResults.tags.entries()) { 341 | const result = []; 342 | for (let i = 0; i < denoAttributes.length; i++) { 343 | result.push(await runDeno(denoAttributes[i], options)); 344 | } 345 | // Set padding on the final string from this tag run 346 | const contents = result 347 | // join results, each result can have multiple lines, this places them 348 | // all in a single string 349 | .join("\n") 350 | .split("\n") // now split the final result into lines 351 | .map((line) => 352 | // for each line apply the padding on the tag 353 | line.padStart( 354 | lineLimits.indent + line.length, // the new line length 355 | " ", // padding is done with spaces (sorry tabs people) 356 | ) 357 | ) 358 | .join("\n"); // merge all padded lines into a single string 359 | // wrap the contents in a Result object, which also propagates the line 360 | // limits, and push it into the results array to be returned 361 | results.push({ 362 | from: lineLimits.lineOpened, 363 | to: lineLimits.lineClosed, 364 | contents, 365 | }); 366 | } 367 | return results; 368 | } 369 | 370 | /** 371 | * This function removes the lines where the `` tags were present 372 | * and places the deno output string in their place 373 | */ 374 | function replaceTagsWithResults(original: string, results: Result[]) { 375 | // All lines are copied to this array, removing a line means skipping 376 | // pushing it into this `newLines` array. 377 | const newLines = []; 378 | // Loop through each line on the original text 379 | const lines = original.split("\n"); 380 | for (let i = 0; i < lines.length; i++) { 381 | const line = lines[i]; 382 | let ignore = false; // don't ignore lines by default 383 | // For each line, loop through all the deno results 384 | // For the big majority of the use cases of deno-tag, the number of deno 385 | // results is expected to be much smaller than the number of lines on a 386 | // text file. 387 | for (let r = 0; r < results.length; r++) { 388 | const result = results[r]; 389 | // Ignore the line if it is inside the limits of a tag 390 | ignore = ignore || (i >= result.from && i <= result.to); 391 | // If this is the first line of the limits of a tag... 392 | if (i === result.from) { 393 | // ...replace it with the deno result contents 394 | newLines.push(result.contents); 395 | } 396 | } 397 | if (ignore) continue; // don't push the line into the newLines array 398 | newLines.push(line); 399 | } 400 | // Return a single string by merging all new lines. 401 | return newLines.join("\n"); 402 | } 403 | -------------------------------------------------------------------------------- /defaults.ts: -------------------------------------------------------------------------------- 1 | export async function defaultBundler( 2 | file: string, 3 | options: Deno.EmitOptions | undefined = {}, 4 | ) { 5 | return await Deno.emit(file, { ...options, ...{ bundle: "esm" } }); 6 | } 7 | -------------------------------------------------------------------------------- /deno_tag.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Hugo Daniel Henriques Oliveira Gomes. All rights reserved. 2 | // Licensed under the EUPL 3 | import { denoTag } from "./code.ts"; 4 | 5 | /** 6 | * Prints the command line help text. 7 | * 8 | * This is a simple line saying to run this file with: 9 | * 10 | * `deno run --allow-read --allow-run --unstable deno_tag.ts someFile.html` 11 | */ 12 | function printUsage(code = 1) { 13 | console.log( 14 | "> deno run --allow-read --allow-run --unstable deno_tag.ts [FILE]", 15 | ); 16 | Deno.exit(code); 17 | } 18 | 19 | /** Displays usage and exits when there were not enough arguments passed */ 20 | function validateArgs() { 21 | if (Deno.args.length === 0) { 22 | printUsage(); 23 | } 24 | } 25 | 26 | /** 27 | * Running this file will process the text on the supplied file and replace 28 | * every `` tag occurrence with the output produced by the deno action 29 | * attribute on its file. 30 | */ 31 | try { 32 | // Assert that the right number of arguments have been provided 33 | validateArgs(); 34 | const file = Deno.args[0]; 35 | // Get Current Working Directory 36 | // Get the directory of the argument file to process 37 | const realPath = await Deno.realPath(file); 38 | const fileFolder = realPath.slice(0, realPath.lastIndexOf("/")); 39 | // Read the contents of the file into a string 40 | const text = await Deno.readTextFile(file); 41 | // Change to the directory of the file to process 42 | Deno.chdir(fileFolder); 43 | // Print the new updated contents to the stdout 44 | console.log(await denoTag(text)); 45 | } catch (e) { 46 | console.error(`\n> ${Deno.args[0]}: ${e.message}\n`); 47 | printUsage(); 48 | } 49 | -------------------------------------------------------------------------------- /denoconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2016", "dom", "deno.ns"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/1 - simple output bundle/complex.ts: -------------------------------------------------------------------------------- 1 | // complex.ts 2 | function complexFunction() { 3 | return 42; 4 | } 5 | export { complexFunction }; 6 | -------------------------------------------------------------------------------- /examples/1 - simple output bundle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Awesome Web App 8 | 9 | 10 |

What a great app

11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/1 - simple output bundle/index.ts: -------------------------------------------------------------------------------- 1 | // index.ts 2 | import { complexFunction } from "./complex.ts"; 3 | 4 | document.addEventListener("DOMContentLoaded", complexFunction); 5 | -------------------------------------------------------------------------------- /examples/2 - running a file/components/magic-title.html: -------------------------------------------------------------------------------- 1 | 6 |
7 |

8 | 9 |

10 |
11 | -------------------------------------------------------------------------------- /examples/2 - running a file/deno_web_component.ts: -------------------------------------------------------------------------------- 1 | // As expected, it is possible to use dependencies to do whatever 2 | // In this case, a simple dom parser will be used as an example 3 | import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts"; 4 | 5 | // Arguments get passed as in the tag, which in this case is: 6 | // src="components/magic_title.ts" 7 | const file = Deno.args[0].slice( 8 | Deno.args[0].indexOf('"') + 1, 9 | Deno.args[0].lastIndexOf('"'), 10 | ); 11 | const text = await Deno.readTextFile(file); 12 | // Parse the component file and take the script code in the "code" variable 13 | const doc = new DOMParser().parseFromString(text, "text/html")!; 14 | const scriptTags = doc.getElementsByTagName("script")!; 15 | let code = ""; 16 | if (scriptTags.length > 0) { 17 | code = scriptTags[0].textContent; 18 | } 19 | // Remove the `` content from the file text 20 | const template = text.slice(0, text.indexOf("`); 41 | -------------------------------------------------------------------------------- /examples/2 - running a file/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Magic Web App 7 | 8 | 9 | 10 | What a great app 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Running the examples locally 2 | 3 | 1. Clone the repo 4 | 2. Call the command `deno run --allow-read --allow-run --unstable deno_tag.ts ./examples/1\ -\ simple\ output\ bundle/index.html` 5 | --------------------------------------------------------------------------------