├── .github ├── poster.svg └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.ts ├── dist ├── compiler.js └── wasm.js ├── mod.ts ├── rustfmt.toml ├── src ├── css.rs ├── error.rs ├── hmr.rs ├── lib.rs ├── minifier.rs ├── resolve_fold.rs ├── resolver.rs ├── swc.rs ├── swc_helpers.rs └── tests.rs ├── test.ts ├── testdata └── gsi-client.js ├── types.ts └── version.ts /.github/poster.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 758 | 759 | 763 | 765 | 769 | 773 | 775 | 776 | 780 | 785 | 786 | 787 | 788 | 790 | 793 | 794 | 796 | 798 | 800 | 801 | 804 | 806 | 809 | 811 | 812 | 813 | 815 | 818 | 821 | 824 | 827 | 829 | 831 | 832 | 834 | 836 | 838 | 841 | 843 | 845 | 846 | 847 | 848 | 899 | 901 | 902 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Aleph.js Compiler Testing 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | name: Testing 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | os: [macOS-latest, windows-latest, ubuntu-latest] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup rust 23 | uses: hecrj/setup-rust-action@v1 24 | with: 25 | rust-version: stable 26 | 27 | - name: Setup deno 28 | uses: denoland/setup-deno@main 29 | with: 30 | deno-version: v1.x 31 | 32 | - name: Cargo test 33 | run: cargo test --all 34 | 35 | - name: Deno test 36 | run: deno test -A 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | target/ 4 | pkg/ 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno", 4 | "rust-lang.rust-analyzer" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "denoland.vscode-deno", 4 | "editor.formatOnSave": true 5 | }, 6 | "[rust]": { 7 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 8 | "editor.formatOnSave": true, 9 | }, 10 | "deno.enable": true, 11 | "deno.lint": false 12 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at i@jex.me. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aleph-compiler" 3 | version = "0.9.4" 4 | description = "The compiler of Aleph.js written in Rust." 5 | repository = "https://github.com/alephjs/aleph.js" 6 | authors = ["The Aleph.js authors"] 7 | license = "MIT" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [dependencies] 14 | anyhow = "1.0.69" 15 | base64 = "0.21.0" 16 | import_map = "0.15.0" 17 | path-slash = "0.2.1" 18 | pathdiff = "0.2.1" 19 | regex = "1.7.1" 20 | serde = { version = "1.0.152", features = ["derive"] } 21 | serde_json = "1.0.94" 22 | url = "2.3.1" 23 | 24 | # parcel css 25 | cssparser = "0.29.6" 26 | lightningcss = "1.0.0-alpha.40" 27 | parcel_sourcemap = "2.1.1" 28 | 29 | # swc 30 | # docs: https://swc.rs 31 | # crate: https://crates.io/search?q=swc_ecmascript 32 | swc_atoms = "0.4.38" 33 | swc_common = { version = "0.29.33", features = ["sourcemap", "perf"] } 34 | swc_ecmascript = { version = "0.218.6", features = ["codegen", "parser", "utils", "visit"] } 35 | swc_ecma_transforms = { version = "0.208.4", features = ["proposal", "typescript", "react", "compat", "optimization" ] } 36 | swc_ecma_minifier = "0.171.5" 37 | 38 | # wasm-bindgen 39 | # docs: https://rustwasm.github.io/docs/wasm-bindgen 40 | wasm-bindgen = { version = "0.2.84", features = ["serde-serialize"] } 41 | serde-wasm-bindgen = "0.5.0" 42 | console_error_panic_hook = { version = "0.1.7", optional = true } 43 | js-sys = "0.3.61" 44 | 45 | [features] 46 | default = ["console_error_panic_hook"] 47 | 48 | [profile.release] 49 | # less code to include into binary 50 | panic = 'abort' 51 | # optimization over all codebase (better optimization, slower build) 52 | codegen-units = 1 53 | # optimization for size (more aggressive) 54 | opt-level = 'z' 55 | # link time optimization using using whole-program analysis 56 | lto = true 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 The Aleph.js authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Aleph.js: The Full-stack Framework in Deno.](https://raw.githubusercontent.com/alephjs/aleph-compiler/main/.github/poster.svg)](https://alephjs.org) 2 | 3 | # Aleph.js Compiler 4 | 5 | The compiler of Aleph.js written in Rust, powered by [swc](https://swc.rs) and [lightningcss](https://lightningcss.dev/). 6 | 7 | ## Usage 8 | 9 | ```ts 10 | import { transform } from "https://deno.land/x/aleph_compiler@0.8.4/mod.ts"; 11 | 12 | const code = ` 13 | import { useState, useEffect } from "react" 14 | 15 | export default function App() { 16 | const [msg, setMsg] = useState("...") 17 | 18 | useEffect(() => { 19 | setTimeout(() => { 20 | setMsg("world!") 21 | }, 1000) 22 | }, []) 23 | 24 | return

Hello {msg}

25 | } 26 | ` 27 | 28 | const ret = await transform("./app.tsx", code, { 29 | importMap: JSON.stringify({ 30 | imports: { 31 | "react": "https://esm.sh/react@18", 32 | } 33 | }), 34 | jsxImportSource: "https://esm.sh/react@18", 35 | sourceMap: true, 36 | }) 37 | 38 | console.log(ret.code, ret.map) 39 | ``` 40 | 41 | ## Development Setup 42 | 43 | You will need [rust](https://www.rust-lang.org/tools/install) 1.60+ and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). 44 | 45 | ## Build 46 | 47 | ```bash 48 | deno run -A build.ts 49 | ``` 50 | 51 | ## Run tests 52 | 53 | ```bash 54 | cargo test --all 55 | ``` 56 | -------------------------------------------------------------------------------- /build.ts: -------------------------------------------------------------------------------- 1 | import { dim } from "https://deno.land/std@0.180.0/fmt/colors.ts"; 2 | import { ensureDir } from "https://deno.land/std@0.180.0/fs/ensure_dir.ts"; 3 | import { encode } from "https://deno.land/std@0.180.0/encoding/base64.ts"; 4 | import { dirname } from "https://deno.land/std@0.180.0/path/mod.ts"; 5 | 6 | async function run(cmd: string[]) { 7 | const p = Deno.run({ 8 | cmd, 9 | stdout: "inherit", 10 | stderr: "inherit", 11 | }); 12 | const status = await p.status(); 13 | p.close(); 14 | return status.success; 15 | } 16 | 17 | async function gzip(path: string): Promise { 18 | const f = await Deno.open(path); 19 | return new Response( 20 | new Response(f.readable).body!.pipeThrough(new CompressionStream("gzip")), 21 | ).arrayBuffer(); 22 | } 23 | 24 | Deno.chdir(dirname(new URL(import.meta.url).pathname)); 25 | 26 | if (import.meta.main) { 27 | const ok = await run(["wasm-pack", "build", "--target", "web"]); 28 | if (ok) { 29 | let prevWasmSize: number; 30 | try { 31 | prevWasmSize = (await Deno.stat("./dist/wasm.js")).size; 32 | } catch (_e) { 33 | prevWasmSize = 0; 34 | } 35 | 36 | const wasmGz = encode(await gzip("./pkg/aleph_compiler_bg.wasm")); 37 | const jsCode = await Deno.readTextFile("./pkg/aleph_compiler.js"); 38 | await ensureDir("./dist"); 39 | await Deno.writeTextFile( 40 | "./dist/wasm.js", 41 | `const wasmGz = "${wasmGz}";\nconst ungzip = (data) => new Response(new Blob([data]).stream().pipeThrough(new DecompressionStream("gzip"))).arrayBuffer();\nexport default () => ungzip(Uint8Array.from(atob(wasmGz), c => c.charCodeAt(0)));`, 42 | ); 43 | await Deno.writeTextFile( 44 | "./dist/compiler.js", 45 | jsCode 46 | .replace(`import * as __wbg_star0 from 'env';`, "") 47 | .replace( 48 | `imports['env'] = __wbg_star0;`, 49 | `imports['env'] = { now: () => Date.now() };`, 50 | ) 51 | .replace( 52 | "console.error(getStringFromWasm0(arg0, arg1));", 53 | ` 54 | const msg = getStringFromWasm0(arg0, arg1); 55 | if (msg.includes('DiagnosticBuffer(["')) { 56 | const diagnostic = msg.split('DiagnosticBuffer(["')[1].split('"])')[0] 57 | throw new Error(diagnostic); 58 | } else { 59 | throw new Error(msg); 60 | } 61 | `, 62 | ), 63 | ); 64 | await run(["deno", "fmt", "-q", "./dist/compiler.js"]); 65 | const wasmSize = (await Deno.stat("./dist/wasm.js")).size; 66 | const changed = ((wasmSize - prevWasmSize) / prevWasmSize) * 100; 67 | if (changed) { 68 | console.log( 69 | `${dim("[INFO]")}: wasm.js ${changed < 0 ? "-" : "+"}${ 70 | Math.abs(changed).toFixed(2) 71 | }% (${ 72 | [prevWasmSize, wasmSize].filter(Boolean).map((n) => 73 | (n / (1024 * 1024)).toFixed(2) + "MB" 74 | ) 75 | .join(" -> ") 76 | })`, 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /dist/compiler.js: -------------------------------------------------------------------------------- 1 | let wasm; 2 | 3 | const heap = new Array(128).fill(undefined); 4 | 5 | heap.push(undefined, null, true, false); 6 | 7 | function getObject(idx) { 8 | return heap[idx]; 9 | } 10 | 11 | let WASM_VECTOR_LEN = 0; 12 | 13 | let cachedUint8Memory0 = null; 14 | 15 | function getUint8Memory0() { 16 | if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { 17 | cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); 18 | } 19 | return cachedUint8Memory0; 20 | } 21 | 22 | const cachedTextEncoder = new TextEncoder("utf-8"); 23 | 24 | const encodeString = typeof cachedTextEncoder.encodeInto === "function" 25 | ? function (arg, view) { 26 | return cachedTextEncoder.encodeInto(arg, view); 27 | } 28 | : function (arg, view) { 29 | const buf = cachedTextEncoder.encode(arg); 30 | view.set(buf); 31 | return { 32 | read: arg.length, 33 | written: buf.length, 34 | }; 35 | }; 36 | 37 | function passStringToWasm0(arg, malloc, realloc) { 38 | if (realloc === undefined) { 39 | const buf = cachedTextEncoder.encode(arg); 40 | const ptr = malloc(buf.length); 41 | getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); 42 | WASM_VECTOR_LEN = buf.length; 43 | return ptr; 44 | } 45 | 46 | let len = arg.length; 47 | let ptr = malloc(len); 48 | 49 | const mem = getUint8Memory0(); 50 | 51 | let offset = 0; 52 | 53 | for (; offset < len; offset++) { 54 | const code = arg.charCodeAt(offset); 55 | if (code > 0x7F) break; 56 | mem[ptr + offset] = code; 57 | } 58 | 59 | if (offset !== len) { 60 | if (offset !== 0) { 61 | arg = arg.slice(offset); 62 | } 63 | ptr = realloc(ptr, len, len = offset + arg.length * 3); 64 | const view = getUint8Memory0().subarray(ptr + offset, ptr + len); 65 | const ret = encodeString(arg, view); 66 | 67 | offset += ret.written; 68 | } 69 | 70 | WASM_VECTOR_LEN = offset; 71 | return ptr; 72 | } 73 | 74 | function isLikeNone(x) { 75 | return x === undefined || x === null; 76 | } 77 | 78 | let cachedInt32Memory0 = null; 79 | 80 | function getInt32Memory0() { 81 | if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { 82 | cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); 83 | } 84 | return cachedInt32Memory0; 85 | } 86 | 87 | const cachedTextDecoder = new TextDecoder("utf-8", { 88 | ignoreBOM: true, 89 | fatal: true, 90 | }); 91 | 92 | cachedTextDecoder.decode(); 93 | 94 | function getStringFromWasm0(ptr, len) { 95 | return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); 96 | } 97 | 98 | let heap_next = heap.length; 99 | 100 | function addHeapObject(obj) { 101 | if (heap_next === heap.length) heap.push(heap.length + 1); 102 | const idx = heap_next; 103 | heap_next = heap[idx]; 104 | 105 | heap[idx] = obj; 106 | return idx; 107 | } 108 | 109 | function dropObject(idx) { 110 | if (idx < 132) return; 111 | heap[idx] = heap_next; 112 | heap_next = idx; 113 | } 114 | 115 | function takeObject(idx) { 116 | const ret = getObject(idx); 117 | dropObject(idx); 118 | return ret; 119 | } 120 | 121 | let cachedFloat64Memory0 = null; 122 | 123 | function getFloat64Memory0() { 124 | if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) { 125 | cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer); 126 | } 127 | return cachedFloat64Memory0; 128 | } 129 | 130 | let cachedBigInt64Memory0 = null; 131 | 132 | function getBigInt64Memory0() { 133 | if ( 134 | cachedBigInt64Memory0 === null || cachedBigInt64Memory0.byteLength === 0 135 | ) { 136 | cachedBigInt64Memory0 = new BigInt64Array(wasm.memory.buffer); 137 | } 138 | return cachedBigInt64Memory0; 139 | } 140 | 141 | function debugString(val) { 142 | // primitive types 143 | const type = typeof val; 144 | if (type == "number" || type == "boolean" || val == null) { 145 | return `${val}`; 146 | } 147 | if (type == "string") { 148 | return `"${val}"`; 149 | } 150 | if (type == "symbol") { 151 | const description = val.description; 152 | if (description == null) { 153 | return "Symbol"; 154 | } else { 155 | return `Symbol(${description})`; 156 | } 157 | } 158 | if (type == "function") { 159 | const name = val.name; 160 | if (typeof name == "string" && name.length > 0) { 161 | return `Function(${name})`; 162 | } else { 163 | return "Function"; 164 | } 165 | } 166 | // objects 167 | if (Array.isArray(val)) { 168 | const length = val.length; 169 | let debug = "["; 170 | if (length > 0) { 171 | debug += debugString(val[0]); 172 | } 173 | for (let i = 1; i < length; i++) { 174 | debug += ", " + debugString(val[i]); 175 | } 176 | debug += "]"; 177 | return debug; 178 | } 179 | // Test for built-in 180 | const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); 181 | let className; 182 | if (builtInMatches.length > 1) { 183 | className = builtInMatches[1]; 184 | } else { 185 | // Failed to match the standard '[object ClassName]' 186 | return toString.call(val); 187 | } 188 | if (className == "Object") { 189 | // we're a user defined class or Object 190 | // JSON.stringify avoids problems with cycles, and is generally much 191 | // easier than looping through ownProperties of `val`. 192 | try { 193 | return "Object(" + JSON.stringify(val) + ")"; 194 | } catch (_) { 195 | return "Object"; 196 | } 197 | } 198 | // errors 199 | if (val instanceof Error) { 200 | return `${val.name}: ${val.message}\n${val.stack}`; 201 | } 202 | // TODO we could test for more things here, like `Set`s and `Map`s. 203 | return className; 204 | } 205 | /** 206 | * @param {string} specifier 207 | * @param {string} code 208 | * @param {any} options 209 | * @returns {any} 210 | */ 211 | export function parseDeps(specifier, code, options) { 212 | try { 213 | const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); 214 | const ptr0 = passStringToWasm0( 215 | specifier, 216 | wasm.__wbindgen_malloc, 217 | wasm.__wbindgen_realloc, 218 | ); 219 | const len0 = WASM_VECTOR_LEN; 220 | const ptr1 = passStringToWasm0( 221 | code, 222 | wasm.__wbindgen_malloc, 223 | wasm.__wbindgen_realloc, 224 | ); 225 | const len1 = WASM_VECTOR_LEN; 226 | wasm.parseDeps(retptr, ptr0, len0, ptr1, len1, addHeapObject(options)); 227 | var r0 = getInt32Memory0()[retptr / 4 + 0]; 228 | var r1 = getInt32Memory0()[retptr / 4 + 1]; 229 | var r2 = getInt32Memory0()[retptr / 4 + 2]; 230 | if (r2) { 231 | throw takeObject(r1); 232 | } 233 | return takeObject(r0); 234 | } finally { 235 | wasm.__wbindgen_add_to_stack_pointer(16); 236 | } 237 | } 238 | 239 | /** 240 | * @param {string} specifier 241 | * @param {string} code 242 | * @param {any} options 243 | * @returns {any} 244 | */ 245 | export function transform(specifier, code, options) { 246 | try { 247 | const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); 248 | const ptr0 = passStringToWasm0( 249 | specifier, 250 | wasm.__wbindgen_malloc, 251 | wasm.__wbindgen_realloc, 252 | ); 253 | const len0 = WASM_VECTOR_LEN; 254 | const ptr1 = passStringToWasm0( 255 | code, 256 | wasm.__wbindgen_malloc, 257 | wasm.__wbindgen_realloc, 258 | ); 259 | const len1 = WASM_VECTOR_LEN; 260 | wasm.transform(retptr, ptr0, len0, ptr1, len1, addHeapObject(options)); 261 | var r0 = getInt32Memory0()[retptr / 4 + 0]; 262 | var r1 = getInt32Memory0()[retptr / 4 + 1]; 263 | var r2 = getInt32Memory0()[retptr / 4 + 2]; 264 | if (r2) { 265 | throw takeObject(r1); 266 | } 267 | return takeObject(r0); 268 | } finally { 269 | wasm.__wbindgen_add_to_stack_pointer(16); 270 | } 271 | } 272 | 273 | /** 274 | * @param {string} filename 275 | * @param {string} code 276 | * @param {any} config_raw 277 | * @returns {any} 278 | */ 279 | export function parcelCSS(filename, code, config_raw) { 280 | try { 281 | const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); 282 | const ptr0 = passStringToWasm0( 283 | filename, 284 | wasm.__wbindgen_malloc, 285 | wasm.__wbindgen_realloc, 286 | ); 287 | const len0 = WASM_VECTOR_LEN; 288 | const ptr1 = passStringToWasm0( 289 | code, 290 | wasm.__wbindgen_malloc, 291 | wasm.__wbindgen_realloc, 292 | ); 293 | const len1 = WASM_VECTOR_LEN; 294 | wasm.parcelCSS(retptr, ptr0, len0, ptr1, len1, addHeapObject(config_raw)); 295 | var r0 = getInt32Memory0()[retptr / 4 + 0]; 296 | var r1 = getInt32Memory0()[retptr / 4 + 1]; 297 | var r2 = getInt32Memory0()[retptr / 4 + 2]; 298 | if (r2) { 299 | throw takeObject(r1); 300 | } 301 | return takeObject(r0); 302 | } finally { 303 | wasm.__wbindgen_add_to_stack_pointer(16); 304 | } 305 | } 306 | 307 | function handleError(f, args) { 308 | try { 309 | return f.apply(this, args); 310 | } catch (e) { 311 | wasm.__wbindgen_exn_store(addHeapObject(e)); 312 | } 313 | } 314 | 315 | async function load(module, imports) { 316 | if (typeof Response === "function" && module instanceof Response) { 317 | if (typeof WebAssembly.instantiateStreaming === "function") { 318 | try { 319 | return await WebAssembly.instantiateStreaming(module, imports); 320 | } catch (e) { 321 | if (module.headers.get("Content-Type") != "application/wasm") { 322 | console.warn( 323 | "`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", 324 | e, 325 | ); 326 | } else { 327 | throw e; 328 | } 329 | } 330 | } 331 | 332 | const bytes = await module.arrayBuffer(); 333 | return await WebAssembly.instantiate(bytes, imports); 334 | } else { 335 | const instance = await WebAssembly.instantiate(module, imports); 336 | 337 | if (instance instanceof WebAssembly.Instance) { 338 | return { instance, module }; 339 | } else { 340 | return instance; 341 | } 342 | } 343 | } 344 | 345 | function getImports() { 346 | const imports = {}; 347 | imports.wbg = {}; 348 | imports.wbg.__wbindgen_is_undefined = function (arg0) { 349 | const ret = getObject(arg0) === undefined; 350 | return ret; 351 | }; 352 | imports.wbg.__wbindgen_in = function (arg0, arg1) { 353 | const ret = getObject(arg0) in getObject(arg1); 354 | return ret; 355 | }; 356 | imports.wbg.__wbindgen_boolean_get = function (arg0) { 357 | const v = getObject(arg0); 358 | const ret = typeof (v) === "boolean" ? (v ? 1 : 0) : 2; 359 | return ret; 360 | }; 361 | imports.wbg.__wbindgen_string_get = function (arg0, arg1) { 362 | const obj = getObject(arg1); 363 | const ret = typeof (obj) === "string" ? obj : undefined; 364 | var ptr0 = isLikeNone(ret) 365 | ? 0 366 | : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 367 | var len0 = WASM_VECTOR_LEN; 368 | getInt32Memory0()[arg0 / 4 + 1] = len0; 369 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 370 | }; 371 | imports.wbg.__wbindgen_is_object = function (arg0) { 372 | const val = getObject(arg0); 373 | const ret = typeof (val) === "object" && val !== null; 374 | return ret; 375 | }; 376 | imports.wbg.__wbindgen_error_new = function (arg0, arg1) { 377 | const ret = new Error(getStringFromWasm0(arg0, arg1)); 378 | return addHeapObject(ret); 379 | }; 380 | imports.wbg.__wbindgen_jsval_eq = function (arg0, arg1) { 381 | const ret = getObject(arg0) === getObject(arg1); 382 | return ret; 383 | }; 384 | imports.wbg.__wbindgen_object_drop_ref = function (arg0) { 385 | takeObject(arg0); 386 | }; 387 | imports.wbg.__wbindgen_is_bigint = function (arg0) { 388 | const ret = typeof (getObject(arg0)) === "bigint"; 389 | return ret; 390 | }; 391 | imports.wbg.__wbindgen_number_get = function (arg0, arg1) { 392 | const obj = getObject(arg1); 393 | const ret = typeof (obj) === "number" ? obj : undefined; 394 | getFloat64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? 0 : ret; 395 | getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); 396 | }; 397 | imports.wbg.__wbindgen_bigint_from_i64 = function (arg0) { 398 | const ret = arg0; 399 | return addHeapObject(ret); 400 | }; 401 | imports.wbg.__wbindgen_bigint_from_u64 = function (arg0) { 402 | const ret = BigInt.asUintN(64, arg0); 403 | return addHeapObject(ret); 404 | }; 405 | imports.wbg.__wbindgen_object_clone_ref = function (arg0) { 406 | const ret = getObject(arg0); 407 | return addHeapObject(ret); 408 | }; 409 | imports.wbg.__wbindgen_number_new = function (arg0) { 410 | const ret = arg0; 411 | return addHeapObject(ret); 412 | }; 413 | imports.wbg.__wbindgen_string_new = function (arg0, arg1) { 414 | const ret = getStringFromWasm0(arg0, arg1); 415 | return addHeapObject(ret); 416 | }; 417 | imports.wbg.__wbindgen_jsval_loose_eq = function (arg0, arg1) { 418 | const ret = getObject(arg0) == getObject(arg1); 419 | return ret; 420 | }; 421 | imports.wbg.__wbg_getwithrefkey_5e6d9547403deab8 = function (arg0, arg1) { 422 | const ret = getObject(arg0)[getObject(arg1)]; 423 | return addHeapObject(ret); 424 | }; 425 | imports.wbg.__wbg_set_841ac57cff3d672b = function (arg0, arg1, arg2) { 426 | getObject(arg0)[takeObject(arg1)] = takeObject(arg2); 427 | }; 428 | imports.wbg.__wbg_new_abda76e883ba8a5f = function () { 429 | const ret = new Error(); 430 | return addHeapObject(ret); 431 | }; 432 | imports.wbg.__wbg_stack_658279fe44541cf6 = function (arg0, arg1) { 433 | const ret = getObject(arg1).stack; 434 | const ptr0 = passStringToWasm0( 435 | ret, 436 | wasm.__wbindgen_malloc, 437 | wasm.__wbindgen_realloc, 438 | ); 439 | const len0 = WASM_VECTOR_LEN; 440 | getInt32Memory0()[arg0 / 4 + 1] = len0; 441 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 442 | }; 443 | imports.wbg.__wbg_error_f851667af71bcfc6 = function (arg0, arg1) { 444 | try { 445 | const msg = getStringFromWasm0(arg0, arg1); 446 | if (msg.includes('DiagnosticBuffer(["')) { 447 | const diagnostic = msg.split('DiagnosticBuffer(["')[1].split('"])')[0]; 448 | throw new Error(diagnostic); 449 | } else { 450 | throw new Error(msg); 451 | } 452 | } finally { 453 | wasm.__wbindgen_free(arg0, arg1); 454 | } 455 | }; 456 | imports.wbg.__wbindgen_is_string = function (arg0) { 457 | const ret = typeof (getObject(arg0)) === "string"; 458 | return ret; 459 | }; 460 | imports.wbg.__wbindgen_is_function = function (arg0) { 461 | const ret = typeof (getObject(arg0)) === "function"; 462 | return ret; 463 | }; 464 | imports.wbg.__wbg_new_b525de17f44a8943 = function () { 465 | const ret = new Array(); 466 | return addHeapObject(ret); 467 | }; 468 | imports.wbg.__wbg_new_f841cc6f2098f4b5 = function () { 469 | const ret = new Map(); 470 | return addHeapObject(ret); 471 | }; 472 | imports.wbg.__wbg_next_b7d530c04fd8b217 = function (arg0) { 473 | const ret = getObject(arg0).next; 474 | return addHeapObject(ret); 475 | }; 476 | imports.wbg.__wbg_value_6ac8da5cc5b3efda = function (arg0) { 477 | const ret = getObject(arg0).value; 478 | return addHeapObject(ret); 479 | }; 480 | imports.wbg.__wbg_iterator_55f114446221aa5a = function () { 481 | const ret = Symbol.iterator; 482 | return addHeapObject(ret); 483 | }; 484 | imports.wbg.__wbg_new_f9876326328f45ed = function () { 485 | const ret = new Object(); 486 | return addHeapObject(ret); 487 | }; 488 | imports.wbg.__wbg_get_27fe3dac1c4d0224 = function (arg0, arg1) { 489 | const ret = getObject(arg0)[arg1 >>> 0]; 490 | return addHeapObject(ret); 491 | }; 492 | imports.wbg.__wbg_set_17224bc548dd1d7b = function (arg0, arg1, arg2) { 493 | getObject(arg0)[arg1 >>> 0] = takeObject(arg2); 494 | }; 495 | imports.wbg.__wbg_isArray_39d28997bf6b96b4 = function (arg0) { 496 | const ret = Array.isArray(getObject(arg0)); 497 | return ret; 498 | }; 499 | imports.wbg.__wbg_length_e498fbc24f9c1d4f = function (arg0) { 500 | const ret = getObject(arg0).length; 501 | return ret; 502 | }; 503 | imports.wbg.__wbg_instanceof_ArrayBuffer_a69f02ee4c4f5065 = function (arg0) { 504 | let result; 505 | try { 506 | result = getObject(arg0) instanceof ArrayBuffer; 507 | } catch { 508 | result = false; 509 | } 510 | const ret = result; 511 | return ret; 512 | }; 513 | imports.wbg.__wbg_new_15d3966e9981a196 = function (arg0, arg1) { 514 | const ret = new Error(getStringFromWasm0(arg0, arg1)); 515 | return addHeapObject(ret); 516 | }; 517 | imports.wbg.__wbg_call_95d1ea488d03e4e8 = function () { 518 | return handleError(function (arg0, arg1) { 519 | const ret = getObject(arg0).call(getObject(arg1)); 520 | return addHeapObject(ret); 521 | }, arguments); 522 | }; 523 | imports.wbg.__wbg_set_388c4c6422704173 = function (arg0, arg1, arg2) { 524 | const ret = getObject(arg0).set(getObject(arg1), getObject(arg2)); 525 | return addHeapObject(ret); 526 | }; 527 | imports.wbg.__wbg_next_88560ec06a094dea = function () { 528 | return handleError(function (arg0) { 529 | const ret = getObject(arg0).next(); 530 | return addHeapObject(ret); 531 | }, arguments); 532 | }; 533 | imports.wbg.__wbg_done_1ebec03bbd919843 = function (arg0) { 534 | const ret = getObject(arg0).done; 535 | return ret; 536 | }; 537 | imports.wbg.__wbg_isSafeInteger_8c4789029e885159 = function (arg0) { 538 | const ret = Number.isSafeInteger(getObject(arg0)); 539 | return ret; 540 | }; 541 | imports.wbg.__wbg_entries_4e1315b774245952 = function (arg0) { 542 | const ret = Object.entries(getObject(arg0)); 543 | return addHeapObject(ret); 544 | }; 545 | imports.wbg.__wbg_get_baf4855f9a986186 = function () { 546 | return handleError(function (arg0, arg1) { 547 | const ret = Reflect.get(getObject(arg0), getObject(arg1)); 548 | return addHeapObject(ret); 549 | }, arguments); 550 | }; 551 | imports.wbg.__wbg_buffer_cf65c07de34b9a08 = function (arg0) { 552 | const ret = getObject(arg0).buffer; 553 | return addHeapObject(ret); 554 | }; 555 | imports.wbg.__wbg_new_537b7341ce90bb31 = function (arg0) { 556 | const ret = new Uint8Array(getObject(arg0)); 557 | return addHeapObject(ret); 558 | }; 559 | imports.wbg.__wbg_instanceof_Uint8Array_01cebe79ca606cca = function (arg0) { 560 | let result; 561 | try { 562 | result = getObject(arg0) instanceof Uint8Array; 563 | } catch { 564 | result = false; 565 | } 566 | const ret = result; 567 | return ret; 568 | }; 569 | imports.wbg.__wbg_length_27a2afe8ab42b09f = function (arg0) { 570 | const ret = getObject(arg0).length; 571 | return ret; 572 | }; 573 | imports.wbg.__wbg_set_17499e8aa4003ebd = function (arg0, arg1, arg2) { 574 | getObject(arg0).set(getObject(arg1), arg2 >>> 0); 575 | }; 576 | imports.wbg.__wbindgen_bigint_get_as_i64 = function (arg0, arg1) { 577 | const v = getObject(arg1); 578 | const ret = typeof (v) === "bigint" ? v : undefined; 579 | getBigInt64Memory0()[arg0 / 8 + 1] = isLikeNone(ret) ? BigInt(0) : ret; 580 | getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret); 581 | }; 582 | imports.wbg.__wbindgen_debug_string = function (arg0, arg1) { 583 | const ret = debugString(getObject(arg1)); 584 | const ptr0 = passStringToWasm0( 585 | ret, 586 | wasm.__wbindgen_malloc, 587 | wasm.__wbindgen_realloc, 588 | ); 589 | const len0 = WASM_VECTOR_LEN; 590 | getInt32Memory0()[arg0 / 4 + 1] = len0; 591 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 592 | }; 593 | imports.wbg.__wbindgen_throw = function (arg0, arg1) { 594 | throw new Error(getStringFromWasm0(arg0, arg1)); 595 | }; 596 | imports.wbg.__wbindgen_memory = function () { 597 | const ret = wasm.memory; 598 | return addHeapObject(ret); 599 | }; 600 | 601 | return imports; 602 | } 603 | 604 | function initMemory(imports, maybe_memory) { 605 | } 606 | 607 | function finalizeInit(instance, module) { 608 | wasm = instance.exports; 609 | init.__wbindgen_wasm_module = module; 610 | cachedBigInt64Memory0 = null; 611 | cachedFloat64Memory0 = null; 612 | cachedInt32Memory0 = null; 613 | cachedUint8Memory0 = null; 614 | 615 | return wasm; 616 | } 617 | 618 | function initSync(module) { 619 | const imports = getImports(); 620 | 621 | initMemory(imports); 622 | 623 | if (!(module instanceof WebAssembly.Module)) { 624 | module = new WebAssembly.Module(module); 625 | } 626 | 627 | const instance = new WebAssembly.Instance(module, imports); 628 | 629 | return finalizeInit(instance, module); 630 | } 631 | 632 | async function init(input) { 633 | if (typeof input === "undefined") { 634 | input = new URL("aleph_compiler_bg.wasm", import.meta.url); 635 | } 636 | const imports = getImports(); 637 | 638 | if ( 639 | typeof input === "string" || 640 | (typeof Request === "function" && input instanceof Request) || 641 | (typeof URL === "function" && input instanceof URL) 642 | ) { 643 | input = fetch(input); 644 | } 645 | 646 | initMemory(imports); 647 | 648 | const { instance, module } = await load(await input, imports); 649 | 650 | return finalizeInit(instance, module); 651 | } 652 | 653 | export { initSync }; 654 | export default init; 655 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { ensureDir } from "https://deno.land/std@0.180.0/fs/ensure_dir.ts"; 2 | import { join } from "https://deno.land/std@0.180.0/path/mod.ts"; 3 | import init, { 4 | parcelCSS, 5 | parseDeps as parseDepsWasmFn, 6 | transform as transformWasmFn, 7 | } from "./dist/compiler.js"; 8 | import wasm from "./dist/wasm.js"; 9 | import type { 10 | DependencyDescriptor, 11 | TransformCSSOptions, 12 | TransformCSSResult, 13 | TransformOptions, 14 | TransformResult, 15 | } from "./types.ts"; 16 | import { VERSION } from "./version.ts"; 17 | 18 | let modulesCache: string | null = null; 19 | let wasmReady: Promise | boolean = false; 20 | 21 | if (typeof Deno.run === "function") { 22 | const p = Deno.run({ 23 | cmd: [Deno.execPath(), "info", "--json"], 24 | stdout: "piped", 25 | stderr: "null", 26 | }); 27 | const output = (new TextDecoder()).decode(await p.output()); 28 | const info = JSON.parse(output); 29 | modulesCache = info?.modulesCache || null; 30 | await p.status(); 31 | p.close(); 32 | } 33 | 34 | /* check whether or not the given path exists as regular file. */ 35 | async function existsFile(path: string): Promise { 36 | try { 37 | const stat = await Deno.lstat(path); 38 | return stat.isFile; 39 | } catch (err) { 40 | if (err instanceof Deno.errors.NotFound) { 41 | return false; 42 | } 43 | throw err; 44 | } 45 | } 46 | 47 | /** initialize the compiler wasm module. */ 48 | export async function initWasm() { 49 | if (import.meta.url.startsWith("https://") && modulesCache) { 50 | const cacheDir = join( 51 | modulesCache, 52 | `https/deno.land/x/aleph_compiler@${VERSION}/dist`, 53 | ); 54 | const cachePath = join(cacheDir, "compiler.wasm"); 55 | if (await existsFile(cachePath)) { 56 | const file = await Deno.open(cachePath, { read: true }); 57 | await init( 58 | new Response(file.readable, { 59 | headers: [["Content-Type", "application/wasm"]], 60 | }), 61 | ); 62 | } else { 63 | const wasmData = await wasm(); 64 | await init(wasmData); 65 | await ensureDir(cacheDir); 66 | await Deno.writeFile(cachePath, new Uint8Array(wasmData)); 67 | } 68 | } else { 69 | await init(await wasm()); 70 | } 71 | wasmReady = true; 72 | } 73 | 74 | async function getWasmReady() { 75 | if (wasmReady === true) return; 76 | if (wasmReady === false) { 77 | wasmReady = initWasm().catch(() => { 78 | wasmReady = false; 79 | }); 80 | } 81 | await wasmReady; 82 | } 83 | 84 | /** Parse the deps of the modules. */ 85 | export async function parseDeps( 86 | specifier: string, 87 | code: string, 88 | options: Pick = {}, 89 | ): Promise { 90 | await getWasmReady(); 91 | return parseDepsWasmFn(specifier, code, options); 92 | } 93 | 94 | /** 95 | * Transforms the JSX/TS module into a JS module. 96 | * 97 | * ```tsx 98 | * transform( 99 | * '/app.tsx', 100 | * ` 101 | * import React from 'https://esm.sh/react'; 102 | * 103 | * export default function App() { 104 | * return

Hello world!

105 | * } 106 | * ` 107 | * ) 108 | * ``` 109 | */ 110 | export async function transform( 111 | specifier: string, 112 | code: string, 113 | options: TransformOptions = {}, 114 | ): Promise { 115 | await getWasmReady(); 116 | try { 117 | return transformWasmFn(specifier, code, options); 118 | } catch (error) { 119 | if ( 120 | options.minify && 121 | (error.stack ?? error.messsage ?? "").includes("ThreadPoolBuildError") 122 | ) { 123 | // retry and disable minify if ThreadPoolBuildError 124 | if (options.minify.compress) { 125 | return await transform(specifier, code, { 126 | ...options, 127 | minify: { compress: false }, 128 | }); 129 | } else { 130 | return transformWasmFn(specifier, code, { 131 | ...options, 132 | minify: undefined, 133 | }); 134 | } 135 | } else { 136 | throw error; 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Compiles a CSS file, including optionally minifying and lowering syntax to the given 143 | * targets. A source map may also be generated, but this is not enabled by default. 144 | */ 145 | export async function transformCSS( 146 | specifier: string, 147 | code: string, 148 | options: TransformCSSOptions = {}, 149 | ): Promise { 150 | await getWasmReady(); 151 | return parcelCSS(specifier, code, options); 152 | } 153 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | max_width = 120 3 | imports_granularity = "Module" -------------------------------------------------------------------------------- /src/css.rs: -------------------------------------------------------------------------------- 1 | /* 2 | [parcel css] - A CSS parser, transformer, and minifier written in Rust. 3 | https://github.com/parcel-bundler/parcel-css 4 | MPL-2.0 License 5 | ! below code was copied from https://github.com/parcel-bundler/parcel-css/blob/510df4e2d825927115427b690d6706da395d2170/node/src/lib.rs, and removed node napi code 6 | */ 7 | 8 | use lightningcss::css_modules::CssModuleExports; 9 | use lightningcss::dependencies::Dependency; 10 | use lightningcss::error::{Error, MinifyErrorKind, ParserError, PrinterErrorKind}; 11 | use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, PseudoClasses, StyleSheet}; 12 | use lightningcss::targets::Browsers; 13 | use parcel_sourcemap::SourceMap; 14 | use serde::{Deserialize, Serialize}; 15 | use std::collections::HashSet; 16 | use std::sync::{Arc, RwLock}; 17 | 18 | #[derive(Serialize)] 19 | #[serde(rename_all = "camelCase")] 20 | struct SourceMapJson<'a> { 21 | version: u8, 22 | mappings: String, 23 | sources: &'a Vec, 24 | sources_content: &'a Vec, 25 | names: &'a Vec, 26 | } 27 | 28 | #[derive(Serialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct TransformResult { 31 | pub code: String, 32 | pub map: Option, 33 | pub exports: Option, 34 | pub dependencies: Option>, 35 | } 36 | 37 | #[derive(Debug, Deserialize)] 38 | #[serde(rename_all = "camelCase")] 39 | pub struct DependencyOptions { 40 | /// Whether to remove `@import` rules. 41 | pub remove_imports: bool, 42 | } 43 | 44 | #[derive(Debug, Deserialize)] 45 | #[serde(rename_all = "camelCase")] 46 | pub struct Config { 47 | pub targets: Option, 48 | pub minify: Option, 49 | pub source_map: Option, 50 | pub drafts: Option, 51 | pub css_modules: Option, 52 | pub analyze_dependencies: Option, 53 | pub pseudo_classes: Option, 54 | pub unused_symbols: Option>, 55 | } 56 | 57 | #[derive(Debug, Deserialize)] 58 | #[serde(untagged)] 59 | pub enum CssModulesOption { 60 | Bool(bool), 61 | Config(CssModulesConfig), 62 | } 63 | 64 | #[derive(Debug, Deserialize)] 65 | #[serde(rename_all = "camelCase")] 66 | pub struct CssModulesConfig { 67 | pattern: Option, 68 | #[serde(default)] 69 | dashed_idents: bool, 70 | } 71 | 72 | #[derive(Debug, Deserialize)] 73 | #[serde(rename_all = "camelCase")] 74 | pub struct OwnedPseudoClasses { 75 | pub hover: Option, 76 | pub active: Option, 77 | pub focus: Option, 78 | pub focus_visible: Option, 79 | pub focus_within: Option, 80 | } 81 | 82 | impl<'a> Into> for &'a OwnedPseudoClasses { 83 | fn into(self) -> PseudoClasses<'a> { 84 | PseudoClasses { 85 | hover: self.hover.as_deref(), 86 | active: self.active.as_deref(), 87 | focus: self.focus.as_deref(), 88 | focus_visible: self.focus_visible.as_deref(), 89 | focus_within: self.focus_within.as_deref(), 90 | } 91 | } 92 | } 93 | 94 | #[derive(Serialize, Debug, Deserialize, Default)] 95 | #[serde(rename_all = "camelCase")] 96 | pub struct Drafts { 97 | #[serde(default)] 98 | pub nesting: bool, 99 | #[serde(default)] 100 | pub custom_media: bool, 101 | } 102 | 103 | pub fn compile<'i>(filename: String, code: &'i str, config: &Config) -> Result> { 104 | let drafts = config.drafts.as_ref(); 105 | let warnings = Some(Arc::new(RwLock::new(Vec::new()))); 106 | let mut stylesheet = StyleSheet::parse( 107 | &code, 108 | ParserOptions { 109 | filename: filename.clone(), 110 | nesting: matches!(drafts, Some(d) if d.nesting), 111 | custom_media: matches!(drafts, Some(d) if d.custom_media), 112 | css_modules: if let Some(css_modules) = &config.css_modules { 113 | match css_modules { 114 | CssModulesOption::Bool(true) => Some(lightningcss::css_modules::Config::default()), 115 | CssModulesOption::Bool(false) => None, 116 | CssModulesOption::Config(c) => Some(lightningcss::css_modules::Config { 117 | pattern: c.pattern.as_ref().map_or(Default::default(), |pattern| { 118 | lightningcss::css_modules::Pattern::parse(pattern).unwrap() 119 | }), 120 | dashed_idents: c.dashed_idents, 121 | }), 122 | } 123 | } else { 124 | None 125 | }, 126 | source_index: 0, 127 | error_recovery: false, 128 | warnings: warnings.clone(), 129 | }, 130 | )?; 131 | stylesheet.minify(MinifyOptions { 132 | targets: config.targets, 133 | unused_symbols: config.unused_symbols.clone().unwrap_or_default(), 134 | })?; 135 | 136 | let mut source_map = if config.source_map.unwrap_or(false) { 137 | let mut sm = SourceMap::new("/"); 138 | sm.add_source(&filename); 139 | sm.set_source_content(0, code)?; 140 | Some(sm) 141 | } else { 142 | None 143 | }; 144 | 145 | let res = stylesheet.to_css(PrinterOptions { 146 | minify: config.minify.unwrap_or(false), 147 | source_map: source_map.as_mut(), 148 | targets: config.targets, 149 | project_root: None, 150 | analyze_dependencies: if let Some(analyze_dependencies) = &config.analyze_dependencies { 151 | Some(lightningcss::dependencies::DependencyOptions { 152 | remove_imports: analyze_dependencies.remove_imports, 153 | }) 154 | } else { 155 | None 156 | }, 157 | pseudo_classes: config.pseudo_classes.as_ref().map(|p| p.into()), 158 | })?; 159 | 160 | let map = if let Some(mut source_map) = source_map { 161 | Some(source_map_to_json(&mut source_map)?) 162 | } else { 163 | None 164 | }; 165 | 166 | Ok(TransformResult { 167 | code: res.code, 168 | map, 169 | exports: res.exports, 170 | dependencies: res.dependencies, 171 | }) 172 | } 173 | 174 | #[inline] 175 | fn source_map_to_json<'i>(source_map: &mut SourceMap) -> Result> { 176 | let mut vlq_output: Vec = Vec::new(); 177 | source_map.write_vlq(&mut vlq_output)?; 178 | 179 | let sm = SourceMapJson { 180 | version: 3, 181 | mappings: unsafe { String::from_utf8_unchecked(vlq_output) }, 182 | sources: source_map.get_sources(), 183 | sources_content: source_map.get_sources_content(), 184 | names: source_map.get_names(), 185 | }; 186 | 187 | Ok(serde_json::to_string(&sm).unwrap()) 188 | } 189 | 190 | #[derive(Serialize, Debug, Deserialize)] 191 | #[serde(rename_all = "camelCase")] 192 | struct AttrConfig { 193 | pub code: String, 194 | pub targets: Option, 195 | pub minify: Option, 196 | pub analyze_dependencies: Option, 197 | } 198 | 199 | #[derive(Serialize)] 200 | #[serde(rename_all = "camelCase")] 201 | struct AttrResult { 202 | code: String, 203 | dependencies: Option>, 204 | } 205 | 206 | #[derive(Debug)] 207 | pub enum CompileError<'i> { 208 | ParseError(Error>), 209 | MinifyError(Error), 210 | PrinterError(Error), 211 | SourceMapError(parcel_sourcemap::SourceMapError), 212 | } 213 | 214 | impl<'i> CompileError<'i> { 215 | fn reason(&self) -> String { 216 | match self { 217 | CompileError::ParseError(e) => format!("{}", e), 218 | CompileError::MinifyError(e) => format!("{}", e), 219 | CompileError::PrinterError(e) => format!("{}", e), 220 | _ => "Unknown error".into(), 221 | } 222 | } 223 | } 224 | 225 | impl<'i> From>> for CompileError<'i> { 226 | fn from(e: Error>) -> CompileError<'i> { 227 | CompileError::ParseError(e) 228 | } 229 | } 230 | 231 | impl<'i> From> for CompileError<'i> { 232 | fn from(err: Error) -> CompileError<'i> { 233 | CompileError::MinifyError(err) 234 | } 235 | } 236 | 237 | impl<'i> From> for CompileError<'i> { 238 | fn from(err: Error) -> CompileError<'i> { 239 | CompileError::PrinterError(err) 240 | } 241 | } 242 | 243 | impl<'i> From for CompileError<'i> { 244 | fn from(e: parcel_sourcemap::SourceMapError) -> CompileError<'i> { 245 | CompileError::SourceMapError(e) 246 | } 247 | } 248 | 249 | impl<'i> From> for wasm_bindgen::JsValue { 250 | fn from(e: CompileError) -> wasm_bindgen::JsValue { 251 | match e { 252 | CompileError::SourceMapError(e) => js_sys::Error::new(&e.to_string()).into(), 253 | _ => js_sys::Error::new(&e.reason()).into(), 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, sync::Arc, sync::RwLock}; 2 | use swc_common::errors::{Diagnostic, DiagnosticBuilder, Emitter}; 3 | use swc_common::{Loc, Span}; 4 | 5 | /// A buffer for collecting errors from the AST parser. 6 | #[derive(Debug, Clone)] 7 | pub struct ErrorBuffer { 8 | specifier: String, 9 | diagnostics: Arc>>, 10 | } 11 | 12 | impl ErrorBuffer { 13 | pub fn new(specifier: &str) -> Self { 14 | Self { 15 | specifier: specifier.into(), 16 | diagnostics: Arc::new(RwLock::new(Vec::new())), 17 | } 18 | } 19 | } 20 | 21 | impl Emitter for ErrorBuffer { 22 | fn emit(&mut self, diagnostic_builder: &DiagnosticBuilder) { 23 | self.diagnostics.write().unwrap().push((**diagnostic_builder).clone()); 24 | } 25 | } 26 | 27 | /// A buffer for collecting diagnostic messages from the AST parser. 28 | #[derive(Debug)] 29 | pub struct DiagnosticBuffer(Vec); 30 | 31 | impl fmt::Display for DiagnosticBuffer { 32 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | fmt.pad(&self.0.join(",")) 34 | } 35 | } 36 | 37 | impl DiagnosticBuffer { 38 | pub fn from_error_buffer(error_buffer: ErrorBuffer, get_loc: F) -> Self 39 | where 40 | F: Fn(Span) -> Loc, 41 | { 42 | let diagnostics = error_buffer.diagnostics.read().unwrap().clone(); 43 | let diagnostics = diagnostics 44 | .iter() 45 | .map(|d| { 46 | let mut message = d.message(); 47 | if let Some(span) = d.span.primary_span() { 48 | let loc = get_loc(span); 49 | message = format!( 50 | "{} at {}:{}:{}", 51 | message, error_buffer.specifier, loc.line, loc.col_display 52 | ); 53 | } 54 | message 55 | }) 56 | .collect(); 57 | 58 | Self(diagnostics) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/hmr.rs: -------------------------------------------------------------------------------- 1 | use crate::resolver::Resolver; 2 | use crate::swc_helpers::{ 3 | import_name, is_call_expr_by_name, new_member_expr, new_str, pat_id, rename_var_decl, simple_member_expr, 4 | window_assign, 5 | }; 6 | use std::{cell::RefCell, rc::Rc}; 7 | use swc_common::DUMMY_SP; 8 | use swc_ecmascript::ast::*; 9 | use swc_ecmascript::utils::quote_ident; 10 | use swc_ecmascript::visit::{noop_fold_type, Fold}; 11 | 12 | pub fn hmr(resolver: Rc>) -> impl Fold { 13 | HmrFold { resolver } 14 | } 15 | 16 | pub struct HmrFold { 17 | resolver: Rc>, 18 | } 19 | 20 | impl Fold for HmrFold { 21 | noop_fold_type!(); 22 | 23 | // resolve import/export url 24 | fn fold_module_items(&mut self, module_items: Vec) -> Vec { 25 | let resolver = self.resolver.borrow(); 26 | let mut items = Vec::::new(); 27 | let mut react_refresh = false; 28 | let aleph_pkg_uri = resolver.aleph_pkg_uri.to_owned(); 29 | 30 | // import __CREATE_HOT_CONTEXT__ from "$aleph_pkg_uri/framework/core/hmr.ts" 31 | items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { 32 | span: DUMMY_SP, 33 | specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier { 34 | span: DUMMY_SP, 35 | local: quote_ident!("__CREATE_HOT_CONTEXT__"), 36 | })], 37 | src: Box::new(new_str( 38 | &resolver.to_local_path(&(aleph_pkg_uri + "/framework/core/hmr.ts")), 39 | )), 40 | type_only: false, 41 | asserts: None, 42 | }))); 43 | // import.meta.hot = __CREATE_HOT_CONTEXT__($specifier) 44 | items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { 45 | span: DUMMY_SP, 46 | expr: Box::new(Expr::Assign(AssignExpr { 47 | span: DUMMY_SP, 48 | op: AssignOp::Assign, 49 | left: PatOrExpr::Expr(Box::new(Expr::Member(new_member_expr( 50 | simple_member_expr("import", "meta"), 51 | "hot", 52 | )))), 53 | right: Box::new(Expr::Call(CallExpr { 54 | span: DUMMY_SP, 55 | callee: Callee::Expr(Box::new(Expr::Ident(quote_ident!("__CREATE_HOT_CONTEXT__")))), 56 | args: vec![ExprOrSpread { 57 | spread: None, 58 | expr: Box::new(Expr::Lit(Lit::Str(new_str(&resolver.specifier)))), 59 | }], 60 | type_args: None, 61 | })), 62 | })), 63 | }))); 64 | 65 | for item in &module_items { 66 | if let ModuleItem::Stmt(Stmt::Expr(ExprStmt { expr, .. })) = &item { 67 | if let Expr::Call(call) = expr.as_ref() { 68 | if is_call_expr_by_name(&call, "$RefreshReg$") { 69 | react_refresh = true; 70 | break; 71 | } 72 | } 73 | } 74 | } 75 | 76 | if react_refresh { 77 | let aleph_pkg_uri = resolver.aleph_pkg_uri.to_owned(); 78 | // import { __REACT_REFRESH_RUNTIME__, __REACT_REFRESH__ } from "$aleph_pkg_uri/framework/react/refresh.ts" 79 | items.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { 80 | span: DUMMY_SP, 81 | specifiers: vec![ 82 | import_name("__REACT_REFRESH_RUNTIME__"), 83 | import_name("__REACT_REFRESH__"), 84 | ], 85 | src: Box::new(new_str( 86 | &resolver.to_local_path(&(aleph_pkg_uri + "/framework/react/refresh.ts")), 87 | )), 88 | type_only: false, 89 | asserts: None, 90 | }))); 91 | // const prevRefreshReg = $RefreshReg$ 92 | items.push(rename_var_decl("prevRefreshReg", "$RefreshReg$")); 93 | // const prevRefreshSig = $RefreshSig$ 94 | items.push(rename_var_decl("prevRefreshSig", "$RefreshSig$")); 95 | // window.$RefreshReg$ = (type, id) => __REACT_REFRESH_RUNTIME__.register(type, $specifier + "#" + id); 96 | items.push(window_assign( 97 | "$RefreshReg$", 98 | Expr::Arrow(ArrowExpr { 99 | span: DUMMY_SP, 100 | params: vec![pat_id("type"), pat_id("id")], 101 | body: BlockStmtOrExpr::Expr(Box::new(Expr::Call(CallExpr { 102 | span: DUMMY_SP, 103 | callee: Callee::Expr(Box::new(simple_member_expr("__REACT_REFRESH_RUNTIME__", "register"))), 104 | args: vec![ 105 | ExprOrSpread { 106 | spread: None, 107 | expr: Box::new(Expr::Ident(quote_ident!("type"))), 108 | }, 109 | ExprOrSpread { 110 | spread: None, 111 | expr: Box::new(Expr::Bin(BinExpr { 112 | span: DUMMY_SP, 113 | op: BinaryOp::Add, 114 | left: Box::new(Expr::Lit(Lit::Str(new_str(&resolver.specifier)))), 115 | right: Box::new(Expr::Bin(BinExpr { 116 | span: DUMMY_SP, 117 | op: BinaryOp::Add, 118 | left: Box::new(Expr::Lit(Lit::Str(new_str("#")))), 119 | right: Box::new(Expr::Ident(quote_ident!("id"))), 120 | })), 121 | })), 122 | }, 123 | ], 124 | type_args: None, 125 | }))), 126 | is_async: false, 127 | is_generator: false, 128 | type_params: None, 129 | return_type: None, 130 | }), 131 | )); 132 | // window.$RefreshSig$ = __REACT_REFRESH_RUNTIME__.createSignatureFunctionForTransform 133 | items.push(window_assign( 134 | "$RefreshSig$", 135 | simple_member_expr("__REACT_REFRESH_RUNTIME__", "createSignatureFunctionForTransform"), 136 | )); 137 | } 138 | 139 | for item in module_items { 140 | items.push(item); 141 | } 142 | 143 | if react_refresh { 144 | // window.$RefreshReg$ = prevRefreshReg 145 | items.push(window_assign( 146 | "$RefreshReg$", 147 | Expr::Ident(quote_ident!("prevRefreshReg")), 148 | )); 149 | // window.$RefreshSig$ = prevRefreshSig 150 | items.push(window_assign( 151 | "$RefreshSig$", 152 | Expr::Ident(quote_ident!("prevRefreshSig")), 153 | )); 154 | // import.meta.hot.accept(__REACT_REFRESH__) 155 | items.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt { 156 | span: DUMMY_SP, 157 | expr: Box::new(Expr::Call(CallExpr { 158 | span: DUMMY_SP, 159 | callee: Callee::Expr(Box::new(Expr::OptChain(OptChainExpr { 160 | span: DUMMY_SP, 161 | question_dot_token: DUMMY_SP, 162 | base: OptChainBase::Member(new_member_expr( 163 | Expr::Member(new_member_expr(simple_member_expr("import", "meta"), "hot")), 164 | "accept", 165 | )), 166 | }))), 167 | args: vec![ExprOrSpread { 168 | spread: None, 169 | expr: Box::new(Expr::Ident(quote_ident!("__REACT_REFRESH__"))), 170 | }], 171 | type_args: None, 172 | })), 173 | }))); 174 | } 175 | 176 | items 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod css; 2 | mod error; 3 | mod hmr; 4 | mod minifier; 5 | mod resolve_fold; 6 | mod resolver; 7 | mod swc; 8 | mod swc_helpers; 9 | 10 | #[cfg(test)] 11 | mod tests; 12 | 13 | use minifier::MinifierOptions; 14 | use resolver::{DependencyDescriptor, Resolver}; 15 | use serde::{Deserialize, Serialize}; 16 | use std::collections::HashMap; 17 | use std::str::FromStr; 18 | use std::{cell::RefCell, rc::Rc}; 19 | use swc::{EmitOptions, SWC}; 20 | use swc_ecmascript::ast::EsVersion; 21 | use url::Url; 22 | use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; 23 | 24 | #[derive(Deserialize)] 25 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 26 | pub struct Options { 27 | pub aleph_pkg_uri: Option, 28 | pub lang: Option, 29 | pub target: Option, 30 | pub import_map: Option, 31 | pub global_version: Option, 32 | pub graph_versions: Option>, 33 | pub strip_data_export: Option, 34 | pub resolve_remote_module: Option, 35 | pub is_dev: Option, 36 | pub source_map: Option, 37 | pub jsx: Option, 38 | pub jsx_pragma: Option, 39 | pub jsx_pragma_frag: Option, 40 | pub jsx_import_source: Option, 41 | pub react_refresh: Option, 42 | pub minify: Option, 43 | } 44 | 45 | #[derive(Serialize)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct TransformOutput { 48 | pub code: String, 49 | 50 | #[serde(skip_serializing_if = "Vec::is_empty")] 51 | pub deps: Vec, 52 | 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub map: Option, 55 | } 56 | 57 | #[wasm_bindgen(js_name = "parseDeps")] 58 | pub fn parse_deps(specifier: &str, code: &str, options: JsValue) -> Result { 59 | console_error_panic_hook::set_once(); 60 | 61 | let options: Options = serde_wasm_bindgen::from_value(options).unwrap(); 62 | let importmap = import_map::parse_from_json( 63 | &Url::from_str("file:///").unwrap(), 64 | options.import_map.unwrap_or("{}".into()).as_str(), 65 | ) 66 | .expect("could not pause the import map") 67 | .import_map; 68 | let resolver = Rc::new(RefCell::new(Resolver::new( 69 | specifier, 70 | "", 71 | importmap, 72 | HashMap::new(), 73 | None, 74 | false, 75 | false, 76 | ))); 77 | let module = SWC::parse(specifier, code, EsVersion::Es2022, options.lang).expect("could not parse the module"); 78 | let deps = module.parse_deps(resolver).expect("could not parse the module"); 79 | 80 | Ok(serde_wasm_bindgen::to_value(&deps).unwrap()) 81 | } 82 | 83 | #[wasm_bindgen(js_name = "transform")] 84 | pub fn transform(specifier: &str, code: &str, options: JsValue) -> Result { 85 | console_error_panic_hook::set_once(); 86 | 87 | let options: Options = serde_wasm_bindgen::from_value(options).unwrap(); 88 | let importmap = import_map::parse_from_json( 89 | &Url::from_str("file:///").unwrap(), 90 | options.import_map.unwrap_or("{}".into()).as_str(), 91 | ) 92 | .expect("could not pause the import map") 93 | .import_map; 94 | let resolver = Rc::new(RefCell::new(Resolver::new( 95 | specifier, 96 | &options.aleph_pkg_uri.unwrap_or("https://deno.land/x/aleph".into()), 97 | importmap, 98 | options.graph_versions.unwrap_or_default(), 99 | options.global_version, 100 | options.resolve_remote_module.unwrap_or_default(), 101 | options.is_dev.unwrap_or_default(), 102 | ))); 103 | let target = match options.target.unwrap_or_default().as_str() { 104 | "es2015" => EsVersion::Es2015, 105 | "es2016" => EsVersion::Es2016, 106 | "es2017" => EsVersion::Es2017, 107 | "es2018" => EsVersion::Es2018, 108 | "es2019" => EsVersion::Es2019, 109 | "es2020" => EsVersion::Es2020, 110 | "es2021" => EsVersion::Es2021, 111 | "es2022" => EsVersion::Es2022, 112 | _ => EsVersion::Es2022, // use latest version 113 | }; 114 | let module = SWC::parse(specifier, code, target, options.lang).expect("could not parse the module"); 115 | let (code, map) = module 116 | .transform( 117 | resolver.clone(), 118 | &EmitOptions { 119 | target, 120 | jsx: options.jsx, 121 | jsx_pragma: options.jsx_pragma, 122 | jsx_pragma_frag: options.jsx_pragma_frag, 123 | jsx_import_source: options.jsx_import_source, 124 | react_refresh: options.react_refresh.unwrap_or_default(), 125 | strip_data_export: options.strip_data_export.unwrap_or_default(), 126 | minify: options.minify, 127 | source_map: options.source_map.unwrap_or_default(), 128 | }, 129 | ) 130 | .expect("could not transform the module"); 131 | let r = resolver.borrow(); 132 | 133 | Ok( 134 | serde_wasm_bindgen::to_value(&TransformOutput { 135 | code, 136 | deps: r.deps.clone(), 137 | map, 138 | }) 139 | .unwrap(), 140 | ) 141 | } 142 | 143 | #[wasm_bindgen(js_name = "parcelCSS")] 144 | pub fn parcel_css(filename: &str, code: &str, config_raw: JsValue) -> Result { 145 | let config: css::Config = serde_wasm_bindgen::from_value(config_raw).unwrap(); 146 | let res = css::compile(filename.into(), code, &config)?; 147 | Ok(serde_wasm_bindgen::to_value(&res).unwrap()) 148 | } 149 | -------------------------------------------------------------------------------- /src/minifier.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use swc_common::comments::{Comments, SingleThreadedComments}; 3 | use swc_common::sync::Lrc; 4 | use swc_common::util::take::Take; 5 | use swc_common::{Mark, SourceMap}; 6 | use swc_ecma_minifier::optimize; 7 | use swc_ecma_minifier::option::{MangleOptions, MinifyOptions}; 8 | use swc_ecmascript::ast::*; 9 | use swc_ecmascript::visit::{noop_visit_mut_type, VisitMut}; 10 | 11 | pub struct MinifierPass { 12 | pub cm: Lrc, 13 | pub comments: Option, 14 | pub unresolved_mark: Mark, 15 | pub top_level_mark: Mark, 16 | pub options: MinifierOptions, 17 | } 18 | 19 | #[derive(Deserialize, Clone, Copy)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct MinifierOptions { 22 | pub compress: Option, 23 | } 24 | 25 | impl VisitMut for MinifierPass { 26 | noop_visit_mut_type!(); 27 | 28 | fn visit_mut_module(&mut self, m: &mut Module) { 29 | m.map_with_mut(|m| { 30 | optimize( 31 | m.into(), 32 | self.cm.clone(), 33 | self.comments.as_ref().map(|v| v as &dyn Comments), 34 | None, 35 | &MinifyOptions { 36 | compress: if self.options.compress.unwrap_or_default() { 37 | Some(Default::default()) 38 | } else { 39 | None 40 | }, 41 | mangle: Some(MangleOptions { 42 | top_level: Some(true), 43 | ..Default::default() 44 | }), 45 | ..Default::default() 46 | }, 47 | &swc_ecma_minifier::option::ExtraOptions { 48 | unresolved_mark: self.unresolved_mark, 49 | top_level_mark: self.top_level_mark, 50 | }, 51 | ) 52 | .expect_module() 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/resolve_fold.rs: -------------------------------------------------------------------------------- 1 | use crate::resolver::Resolver; 2 | use crate::swc_helpers::{is_call_expr_by_name, new_str}; 3 | use std::{cell::RefCell, rc::Rc}; 4 | use swc_common::{Span, DUMMY_SP}; 5 | use swc_ecmascript::ast::*; 6 | use swc_ecmascript::visit::{noop_fold_type, Fold, FoldWith}; 7 | 8 | pub fn resolve_fold( 9 | resolver: Rc>, 10 | strip_data_export: bool, 11 | mark_import_src_location: bool, 12 | ) -> impl Fold { 13 | ResolveFold { 14 | resolver, 15 | strip_data_export, 16 | mark_import_src_location, 17 | } 18 | } 19 | 20 | pub struct ResolveFold { 21 | resolver: Rc>, 22 | strip_data_export: bool, 23 | mark_import_src_location: bool, 24 | } 25 | 26 | impl Fold for ResolveFold { 27 | noop_fold_type!(); 28 | 29 | // fold&resolve import/export url 30 | fn fold_module_items(&mut self, module_items: Vec) -> Vec { 31 | let mut items = Vec::::new(); 32 | 33 | for item in module_items { 34 | match item { 35 | ModuleItem::ModuleDecl(decl) => { 36 | let item: ModuleItem = match decl { 37 | // match: import React, { useState } from "https://esm.sh/react" 38 | ModuleDecl::Import(import_decl) => { 39 | if import_decl.type_only { 40 | // ingore type import 41 | ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) 42 | } else { 43 | let mut resolver = self.resolver.borrow_mut(); 44 | let resolved_url = resolver.resolve( 45 | import_decl.src.value.as_ref(), 46 | false, 47 | mark_span(&import_decl.src.span, self.mark_import_src_location), 48 | ); 49 | ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { 50 | src: Box::new(new_str(&resolved_url)), 51 | ..import_decl 52 | })) 53 | } 54 | } 55 | // match: export { default as React, useState } from "https://esm.sh/react" 56 | // match: export * as React from "https://esm.sh/react" 57 | ModuleDecl::ExportNamed(NamedExport { 58 | type_only, 59 | specifiers, 60 | src: Some(src), 61 | span, 62 | asserts, 63 | }) => { 64 | if type_only { 65 | // ingore type export 66 | ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { 67 | span, 68 | specifiers, 69 | src: Some(src), 70 | type_only, 71 | asserts, 72 | })) 73 | } else { 74 | let mut resolver = self.resolver.borrow_mut(); 75 | let resolved_url = resolver.resolve( 76 | src.value.as_ref(), 77 | false, 78 | mark_span(&src.span, self.mark_import_src_location), 79 | ); 80 | ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(NamedExport { 81 | span, 82 | specifiers, 83 | src: Some(Box::new(new_str(&resolved_url))), 84 | type_only, 85 | asserts, 86 | })) 87 | } 88 | } 89 | // match: export * from "https://esm.sh/react" 90 | ModuleDecl::ExportAll(ExportAll { 91 | src, 92 | span, 93 | asserts, 94 | type_only, 95 | }) => { 96 | let mut resolver = self.resolver.borrow_mut(); 97 | let resolved_url = resolver.resolve( 98 | src.value.as_ref(), 99 | false, 100 | mark_span(&src.span, self.mark_import_src_location), 101 | ); 102 | ModuleItem::ModuleDecl(ModuleDecl::ExportAll(ExportAll { 103 | span, 104 | src: Box::new(new_str(&resolved_url)), 105 | asserts, 106 | type_only, 107 | })) 108 | } 109 | // match: export const data = { ... } 110 | // match: export const muation = { ... } 111 | ModuleDecl::ExportDecl(ExportDecl { 112 | decl: Decl::Var(var), 113 | span, 114 | }) => { 115 | let mut data_export_idx = -1; 116 | let mut data_export_name = "".to_string(); 117 | if self.strip_data_export && var.decls.len() > 0 { 118 | let mut i = 0; 119 | for decl in &var.decls { 120 | if let Pat::Ident(bi) = &decl.name { 121 | if decl.init.is_some() { 122 | match bi.id.sym.as_ref() { 123 | "data" | "mutation" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" => { 124 | data_export_idx = i; 125 | data_export_name = bi.id.sym.to_string(); 126 | break; 127 | } 128 | _ => {} 129 | } 130 | } 131 | } 132 | i += 1; 133 | } 134 | } 135 | if data_export_idx != -1 { 136 | let mut i = -1; 137 | let var = var.as_ref(); 138 | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { 139 | span, 140 | decl: Decl::Var(Box::new(VarDecl { 141 | decls: var 142 | .decls 143 | .clone() 144 | .into_iter() 145 | .map(|decl| { 146 | i += 1; 147 | if data_export_idx == i { 148 | let mut init = Some(Box::new(Expr::Lit(Lit::Bool(Bool { 149 | span: DUMMY_SP, 150 | value: true, 151 | })))); 152 | if data_export_name == "data" || data_export_name == "mutation" { 153 | if let Some(expr) = decl.init { 154 | if let Expr::Object(obj) = *expr { 155 | init = Some(Box::new(Expr::Object(ObjectLit { 156 | span: obj.span, 157 | props: obj 158 | .props 159 | .into_iter() 160 | .map(|prop| match prop { 161 | PropOrSpread::Prop(prop) => match *prop { 162 | Prop::Shorthand(ident) => { 163 | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { 164 | key: PropName::Ident(ident), 165 | value: Box::new(Expr::Lit(Lit::Bool(Bool { 166 | span: DUMMY_SP, 167 | value: true, 168 | }))), 169 | }))) 170 | } 171 | Prop::KeyValue(KeyValueProp { key, value, .. }) => { 172 | // if value is a boolean, we don't need to wrap it 173 | if let Expr::Lit(Lit::Bool(_)) = *value { 174 | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { key, value }))) 175 | } else { 176 | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { 177 | key, 178 | value: Box::new(Expr::Lit(Lit::Bool(Bool { 179 | span: DUMMY_SP, 180 | value: true, 181 | }))), 182 | }))) 183 | } 184 | } 185 | Prop::Method(MethodProp { key, .. }) => { 186 | PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { 187 | key, 188 | value: Box::new(Expr::Lit(Lit::Bool(Bool { 189 | span: DUMMY_SP, 190 | value: true, 191 | }))), 192 | }))) 193 | } 194 | _ => PropOrSpread::Prop(prop), 195 | }, 196 | _ => prop, 197 | }) 198 | .collect(), 199 | }))) 200 | } 201 | } 202 | } 203 | VarDeclarator { 204 | span: DUMMY_SP, 205 | init, 206 | ..decl 207 | } 208 | } else { 209 | decl 210 | } 211 | }) 212 | .collect(), 213 | span: DUMMY_SP, 214 | kind: var.kind, 215 | declare: var.declare, 216 | })), 217 | })) 218 | } else { 219 | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { 220 | span, 221 | decl: Decl::Var(var), 222 | })) 223 | } 224 | } 225 | // match: export function data/mutation/GET/POST/PUT/PATCH/DELETE { ... } 226 | ModuleDecl::ExportDecl(ExportDecl { 227 | decl: Decl::Fn(decl), 228 | span, 229 | }) => { 230 | let is_api_method = match decl.ident.sym.as_ref() { 231 | "data" | "mutation" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" => true, 232 | _ => false, 233 | }; 234 | if self.strip_data_export && is_api_method { 235 | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { 236 | span: DUMMY_SP, 237 | decl: Decl::Fn(FnDecl { 238 | ident: decl.ident.clone(), 239 | declare: decl.declare, 240 | function: Box::new(Function { 241 | span: DUMMY_SP, 242 | params: vec![], 243 | decorators: vec![], 244 | // empty body 245 | body: Some(BlockStmt { 246 | span: DUMMY_SP, 247 | stmts: vec![], 248 | }), 249 | is_generator: false, 250 | is_async: false, 251 | type_params: None, 252 | return_type: None, 253 | }), 254 | }), 255 | })) 256 | } else { 257 | ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl { 258 | span, 259 | decl: Decl::Fn(decl), 260 | })) 261 | } 262 | } 263 | _ => ModuleItem::ModuleDecl(decl), 264 | }; 265 | items.push(item.fold_children_with(self)); 266 | } 267 | _ => { 268 | items.push(item.fold_children_with(self)); 269 | } 270 | }; 271 | } 272 | 273 | items 274 | } 275 | 276 | // resolve worker import url 277 | fn fold_new_expr(&mut self, mut new_expr: NewExpr) -> NewExpr { 278 | let ok = match new_expr.callee.as_ref() { 279 | Expr::Ident(id) => id.sym.as_ref().eq("Worker"), 280 | _ => false, 281 | }; 282 | if ok { 283 | if let Some(args) = &mut new_expr.args { 284 | let src = match args.first() { 285 | Some(ExprOrSpread { expr, .. }) => match expr.as_ref() { 286 | Expr::Lit(lit) => match lit { 287 | Lit::Str(s) => Some(s), 288 | _ => None, 289 | }, 290 | _ => None, 291 | }, 292 | _ => None, 293 | }; 294 | if let Some(src) = src { 295 | let mut resolver = self.resolver.borrow_mut(); 296 | let new_src = resolver.resolve( 297 | src.value.as_ref(), 298 | true, 299 | mark_span(&src.span, self.mark_import_src_location), 300 | ); 301 | 302 | args[0] = ExprOrSpread { 303 | spread: None, 304 | expr: Box::new(Expr::Lit(Lit::Str(new_str(&new_src)))), 305 | } 306 | } 307 | } 308 | }; 309 | 310 | new_expr.fold_children_with(self) 311 | } 312 | 313 | // resolve dynamic import url 314 | fn fold_call_expr(&mut self, mut call: CallExpr) -> CallExpr { 315 | if is_call_expr_by_name(&call, "import") { 316 | let src = match call.args.first() { 317 | Some(ExprOrSpread { expr, .. }) => match expr.as_ref() { 318 | Expr::Lit(lit) => match lit { 319 | Lit::Str(s) => Some(s), 320 | _ => None, 321 | }, 322 | _ => None, 323 | }, 324 | _ => None, 325 | }; 326 | if let Some(src) = src { 327 | let mut resolver = self.resolver.borrow_mut(); 328 | let new_src = resolver.resolve( 329 | src.value.as_ref(), 330 | true, 331 | mark_span(&src.span, self.mark_import_src_location), 332 | ); 333 | 334 | call.args[0] = ExprOrSpread { 335 | spread: None, 336 | expr: Box::new(Expr::Lit(Lit::Str(new_str(&new_src)))), 337 | } 338 | } 339 | } 340 | 341 | call.fold_children_with(self) 342 | } 343 | } 344 | 345 | fn mark_span(span: &Span, ok: bool) -> Option { 346 | if ok { 347 | Some(span.clone()) 348 | } else { 349 | None 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/resolver.rs: -------------------------------------------------------------------------------- 1 | use import_map::ImportMap; 2 | use path_slash::PathBufExt; 3 | use pathdiff::diff_paths; 4 | use serde::Serialize; 5 | use std::collections::HashMap; 6 | use std::path::{Path, PathBuf}; 7 | use std::str::FromStr; 8 | use swc_common::Span; 9 | use url::Url; 10 | 11 | #[derive(Clone, Debug, Eq, PartialEq, Serialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct DependencyDescriptor { 14 | pub specifier: String, 15 | pub import_url: String, 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub loc: Option, 18 | #[serde(skip_serializing_if = "is_false")] 19 | pub dynamic: bool, 20 | } 21 | 22 | /// A Resolver to resolve esm import/export URL. 23 | pub struct Resolver { 24 | /// aleph pkg uri 25 | pub aleph_pkg_uri: String, 26 | /// the text specifier associated with the import/export statement. 27 | pub specifier: String, 28 | /// a flag indicating if the specifier is a remote(http) url. 29 | pub specifier_is_remote: bool, 30 | /// a ordered dependencies of the module 31 | pub deps: Vec, 32 | /// development mode 33 | pub is_dev: bool, 34 | /// the global version 35 | pub global_version: Option, 36 | /// the graph versions 37 | pub graph_versions: HashMap, 38 | /// should resolve remote deps 39 | pub resolve_remote_deps: bool, 40 | // import maps 41 | import_map: ImportMap, 42 | } 43 | 44 | impl Resolver { 45 | pub fn new( 46 | specifier: &str, 47 | aleph_pkg_uri: &str, 48 | import_map: ImportMap, 49 | graph_versions: HashMap, 50 | global_version: Option, 51 | resolve_remote_deps: bool, 52 | is_dev: bool, 53 | ) -> Self { 54 | Resolver { 55 | aleph_pkg_uri: aleph_pkg_uri.into(), 56 | specifier: specifier.into(), 57 | specifier_is_remote: is_http_url(specifier), 58 | deps: Vec::new(), 59 | import_map, 60 | graph_versions, 61 | global_version, 62 | is_dev, 63 | resolve_remote_deps, 64 | } 65 | } 66 | 67 | /// fix remote url for dev mode. 68 | // - `https://esm.sh/react` -> `/-/esm.sh/react` 69 | // - `https://deno.land/std/path/mod.ts` -> `/-/deno.land/std/path/mod.ts` 70 | // - `http://localhost:8080/mod.ts` -> `/-/http_localhost_8080/mod.ts` 71 | pub fn to_local_path(&self, url: &str) -> String { 72 | let url = Url::from_str(url).unwrap(); 73 | let pathname = Path::new(url.path()); 74 | let mut local_path = "/-/".to_owned(); 75 | let scheme = url.scheme(); 76 | if scheme == "http" { 77 | local_path.push_str("http_"); 78 | } 79 | local_path.push_str(url.host_str().unwrap()); 80 | if let Some(port) = url.port() { 81 | if scheme == "http" && port == 80 { 82 | } else if scheme == "https" && port == 443 { 83 | } else { 84 | local_path.push('_'); 85 | local_path.push_str(port.to_string().as_str()); 86 | } 87 | } 88 | local_path.push_str(&pathname.to_owned().to_slash().unwrap().to_string()); 89 | if let Some(query) = url.query() { 90 | local_path.push('?'); 91 | local_path.push_str(query); 92 | } 93 | local_path 94 | } 95 | 96 | /// Resolve import/export URLs. 97 | pub fn resolve(&mut self, url: &str, dynamic: bool, loc: Option) -> String { 98 | let referrer = if self.specifier_is_remote { 99 | Url::from_str(self.specifier.as_str()).unwrap() 100 | } else { 101 | Url::from_str(&("file://".to_owned() + self.specifier.trim_start_matches('.'))).unwrap() 102 | }; 103 | let resolved_url = if let Ok(ret) = self.import_map.resolve(url, &referrer) { 104 | ret.to_string() 105 | } else { 106 | url.into() 107 | }; 108 | let mut import_url = if resolved_url.starts_with("file://") { 109 | let path = resolved_url.strip_prefix("file://").unwrap(); 110 | if !self.specifier_is_remote { 111 | let mut buf = PathBuf::from(self.specifier.trim_start_matches('.')); 112 | buf.pop(); 113 | let mut path = diff_paths(&path, buf).unwrap().to_slash().unwrap().to_string(); 114 | if !path.starts_with("./") && !path.starts_with("../") { 115 | path = "./".to_owned() + &path 116 | } 117 | path 118 | } else { 119 | ".".to_owned() + path 120 | } 121 | } else { 122 | resolved_url.clone() 123 | }; 124 | let mut fixed_url: String = if resolved_url.starts_with("file://") { 125 | ".".to_owned() + resolved_url.strip_prefix("file://").unwrap() 126 | } else { 127 | resolved_url.into() 128 | }; 129 | let is_remote = is_http_url(&fixed_url); 130 | 131 | if self.is_dev && is_esm_sh_url(&fixed_url) && !fixed_url.ends_with(".development.js") { 132 | if fixed_url.contains("?") { 133 | fixed_url = fixed_url + "&dev" 134 | } else { 135 | fixed_url = fixed_url + "?dev" 136 | } 137 | import_url = fixed_url.clone(); 138 | } 139 | 140 | if is_css_url(&import_url) { 141 | if import_url.contains("?") { 142 | import_url = import_url + "&module" 143 | } else { 144 | import_url = import_url + "?module" 145 | } 146 | } 147 | 148 | if is_remote { 149 | // fix remote url to local path if allowed 150 | if self.resolve_remote_deps { 151 | import_url = self.to_local_path(&import_url); 152 | } 153 | } else { 154 | // apply graph version if exists 155 | let v = if self.graph_versions.contains_key(&fixed_url) { 156 | self.graph_versions.get(&fixed_url) 157 | } else { 158 | self.global_version.as_ref() 159 | }; 160 | if let Some(version) = v { 161 | if import_url.contains("?") { 162 | import_url = format!("{}&v={}", import_url, version); 163 | } else { 164 | import_url = format!("{}?v={}", import_url, version); 165 | } 166 | } 167 | } 168 | 169 | // update dep graph 170 | self.deps.push(DependencyDescriptor { 171 | specifier: fixed_url.clone(), 172 | import_url: import_url.clone(), 173 | loc, 174 | dynamic, 175 | }); 176 | 177 | import_url 178 | } 179 | } 180 | 181 | pub fn is_http_url(url: &str) -> bool { 182 | return url.starts_with("https://") || url.starts_with("http://"); 183 | } 184 | 185 | pub fn is_esm_sh_url(url: &str) -> bool { 186 | return url.starts_with("https://esm.sh/") || url.starts_with("http://esm.sh/"); 187 | } 188 | 189 | pub fn is_css_url(url: &str) -> bool { 190 | if is_esm_sh_url(url) { 191 | let url = Url::from_str(url).unwrap(); 192 | for (key, _value) in url.query_pairs() { 193 | if key.eq("css") { 194 | return true; 195 | } 196 | } 197 | } 198 | return url.ends_with(".css") || url.contains(".css?"); 199 | } 200 | 201 | fn is_false(value: &bool) -> bool { 202 | return !*value; 203 | } 204 | -------------------------------------------------------------------------------- /src/swc.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{DiagnosticBuffer, ErrorBuffer}; 2 | use crate::hmr::hmr; 3 | use crate::minifier::{MinifierOptions, MinifierPass}; 4 | use crate::resolve_fold::resolve_fold; 5 | use crate::resolver::{DependencyDescriptor, Resolver}; 6 | 7 | use std::{cell::RefCell, path::Path, rc::Rc}; 8 | use swc_common::comments::SingleThreadedComments; 9 | use swc_common::errors::{Handler, HandlerFlags}; 10 | use swc_common::{chain, FileName, Globals, Mark, SourceMap}; 11 | use swc_ecma_transforms::optimization::simplify::dce; 12 | use swc_ecma_transforms::pass::Optional; 13 | use swc_ecma_transforms::proposals::decorators; 14 | use swc_ecma_transforms::typescript::strip; 15 | use swc_ecma_transforms::{compat, fixer, helpers, hygiene, react, Assumptions}; 16 | use swc_ecmascript::ast::{EsVersion, Module, Program}; 17 | use swc_ecmascript::codegen::text_writer::JsWriter; 18 | use swc_ecmascript::parser::lexer::Lexer; 19 | use swc_ecmascript::parser::{EsConfig, StringInput, Syntax, TsConfig}; 20 | use swc_ecmascript::visit::{as_folder, Fold, FoldWith}; 21 | 22 | /// Options for transpiling a module. 23 | #[derive(Clone)] 24 | pub struct EmitOptions { 25 | pub target: EsVersion, 26 | pub jsx: Option, 27 | pub jsx_pragma: Option, 28 | pub jsx_pragma_frag: Option, 29 | pub jsx_import_source: Option, 30 | pub react_refresh: bool, 31 | pub strip_data_export: bool, 32 | pub minify: Option, 33 | pub source_map: bool, 34 | } 35 | 36 | impl Default for EmitOptions { 37 | fn default() -> Self { 38 | EmitOptions { 39 | target: EsVersion::Es2022, 40 | jsx: None, 41 | jsx_pragma: None, 42 | jsx_pragma_frag: None, 43 | jsx_import_source: None, 44 | react_refresh: false, 45 | strip_data_export: false, 46 | minify: None, 47 | source_map: false, 48 | } 49 | } 50 | } 51 | 52 | #[derive(Clone)] 53 | pub struct SWC { 54 | pub specifier: String, 55 | pub module: Module, 56 | pub source_map: Rc, 57 | pub comments: SingleThreadedComments, 58 | } 59 | 60 | impl SWC { 61 | /// parse source code. 62 | pub fn parse(specifier: &str, source: &str, target: EsVersion, lang: Option) -> Result { 63 | let source_map = SourceMap::default(); 64 | let source_file = source_map.new_source_file(FileName::Real(Path::new(specifier).to_path_buf()), source.into()); 65 | let sm = &source_map; 66 | let error_buffer = ErrorBuffer::new(specifier); 67 | let syntax = get_syntax(specifier, lang); 68 | let input = StringInput::from(&*source_file); 69 | let comments = SingleThreadedComments::default(); 70 | let lexer = Lexer::new(syntax, target, input, Some(&comments)); 71 | let mut parser = swc_ecmascript::parser::Parser::new_from(lexer); 72 | let handler = Handler::with_emitter_and_flags( 73 | Box::new(error_buffer.clone()), 74 | HandlerFlags { 75 | can_emit_warnings: true, 76 | dont_buffer_diagnostics: true, 77 | ..HandlerFlags::default() 78 | }, 79 | ); 80 | let module = parser 81 | .parse_module() 82 | .map_err(move |err| { 83 | let mut diagnostic = err.into_diagnostic(&handler); 84 | diagnostic.emit(); 85 | DiagnosticBuffer::from_error_buffer(error_buffer, |span| sm.lookup_char_pos(span.lo)) 86 | }) 87 | .unwrap(); 88 | 89 | Ok(SWC { 90 | specifier: specifier.into(), 91 | module, 92 | source_map: Rc::new(source_map), 93 | comments, 94 | }) 95 | } 96 | 97 | /// parse deps in the module. 98 | pub fn parse_deps(&self, resolver: Rc>) -> Result, anyhow::Error> { 99 | let program = Program::Module(self.module.clone()); 100 | let mut resolve_fold = resolve_fold(resolver.clone(), false, true); 101 | program.fold_with(&mut resolve_fold); 102 | let resolver = resolver.borrow(); 103 | Ok(resolver.deps.clone()) 104 | } 105 | 106 | /// transform a JS/TS/JSX/TSX file into a JS file, based on the supplied options. 107 | pub fn transform( 108 | self, 109 | resolver: Rc>, 110 | options: &EmitOptions, 111 | ) -> Result<(String, Option), anyhow::Error> { 112 | swc_common::GLOBALS.set(&Globals::new(), || { 113 | let unresolved_mark = Mark::new(); 114 | let top_level_mark = Mark::fresh(Mark::root()); 115 | let specifier_is_remote = resolver.borrow().specifier_is_remote; 116 | let extname = Path::new(&self.specifier) 117 | .extension() 118 | .unwrap_or_default() 119 | .to_ascii_lowercase(); 120 | let is_dev = resolver.borrow().is_dev; 121 | let is_ts = extname == "ts" || extname == "mts" || extname == "tsx"; 122 | let jsxt = options.jsx.as_deref().unwrap_or("classic"); 123 | let jsx_preserve = jsxt == "preserve"; 124 | let is_jsx = extname == "jsx" || extname == "tsx"; 125 | let react_options = if jsxt == "automatic" { 126 | let mut resolver = resolver.borrow_mut(); 127 | let import_source = options.jsx_import_source.as_deref().unwrap_or("react"); 128 | let runtime = if is_dev { "/jsx-dev-runtime" } else { "/jsx-runtime" }; 129 | let import_source = resolver.resolve(&(import_source.to_owned() + runtime), false, None); 130 | let import_source = import_source 131 | .split("?") 132 | .next() 133 | .unwrap_or(&import_source) 134 | .strip_suffix(runtime) 135 | .unwrap_or(&import_source) 136 | .to_string(); 137 | if !is_jsx { 138 | resolver.deps.pop(); 139 | } 140 | react::Options { 141 | runtime: Some(react::Runtime::Automatic), 142 | import_source: Some(import_source), 143 | ..Default::default() 144 | } 145 | } else { 146 | react::Options { 147 | pragma: options.jsx_pragma.clone(), 148 | pragma_frag: options.jsx_pragma_frag.clone(), 149 | ..Default::default() 150 | } 151 | }; 152 | let assumptions = Assumptions::all(); 153 | let passes = chain!( 154 | swc_ecma_transforms::resolver(unresolved_mark, top_level_mark, is_ts), 155 | Optional::new(react::jsx_src(is_dev, self.source_map.clone()), is_jsx && is_dev), 156 | resolve_fold(resolver.clone(), options.strip_data_export, false), 157 | decorators::decorators(decorators::Config { 158 | legacy: true, 159 | emit_metadata: false, 160 | use_define_for_class_fields: false, 161 | }), 162 | Optional::new( 163 | compat::es2022::es2022( 164 | Some(&self.comments), 165 | compat::es2022::Config { 166 | class_properties: compat::es2022::class_properties::Config { 167 | private_as_properties: assumptions.private_fields_as_properties, 168 | constant_super: assumptions.constant_super, 169 | set_public_fields: assumptions.set_public_class_fields, 170 | no_document_all: assumptions.no_document_all 171 | } 172 | } 173 | ), 174 | should_enable(options.target, EsVersion::Es2022) 175 | ), 176 | Optional::new( 177 | compat::es2021::es2021(), 178 | should_enable(options.target, EsVersion::Es2021) 179 | ), 180 | Optional::new( 181 | compat::es2020::es2020(compat::es2020::Config { 182 | nullish_coalescing: compat::es2020::nullish_coalescing::Config { 183 | no_document_all: assumptions.no_document_all 184 | }, 185 | optional_chaining: compat::es2020::opt_chaining::Config { 186 | no_document_all: assumptions.no_document_all, 187 | pure_getter: assumptions.pure_getters 188 | } 189 | }), 190 | should_enable(options.target, EsVersion::Es2020) 191 | ), 192 | Optional::new( 193 | compat::es2019::es2019(), 194 | should_enable(options.target, EsVersion::Es2019) 195 | ), 196 | Optional::new( 197 | compat::es2018(compat::es2018::Config { 198 | object_rest_spread: compat::es2018::object_rest_spread::Config { 199 | no_symbol: assumptions.object_rest_no_symbols, 200 | set_property: assumptions.set_spread_properties, 201 | pure_getters: assumptions.pure_getters, 202 | } 203 | }), 204 | should_enable(options.target, EsVersion::Es2018) 205 | ), 206 | Optional::new( 207 | compat::es2017( 208 | compat::es2017::Config { 209 | async_to_generator: compat::es2017::async_to_generator::Config { 210 | ignore_function_name: assumptions.ignore_function_name, 211 | ignore_function_length: assumptions.ignore_function_length 212 | } 213 | }, 214 | Some(&self.comments), 215 | unresolved_mark, 216 | ), 217 | should_enable(options.target, EsVersion::Es2017) 218 | ), 219 | Optional::new(compat::es2016(), should_enable(options.target, EsVersion::Es2016)), 220 | compat::reserved_words::reserved_words(), 221 | helpers::inject_helpers(top_level_mark), 222 | Optional::new( 223 | strip::strip_with_config(strip_config_from_emit_options(), top_level_mark), 224 | !is_jsx 225 | ), 226 | Optional::new( 227 | strip::strip_with_jsx( 228 | self.source_map.clone(), 229 | strip_config_from_emit_options(), 230 | &self.comments, 231 | top_level_mark 232 | ), 233 | is_jsx 234 | ), 235 | Optional::new( 236 | react::refresh( 237 | is_dev, 238 | Some(react::RefreshOptions { 239 | refresh_reg: "$RefreshReg$".into(), 240 | refresh_sig: "$RefreshSig$".into(), 241 | emit_full_signatures: false, 242 | }), 243 | self.source_map.clone(), 244 | Some(&self.comments), 245 | top_level_mark 246 | ), 247 | options.react_refresh && !specifier_is_remote 248 | ), 249 | Optional::new( 250 | react::jsx( 251 | self.source_map.clone(), 252 | Some(&self.comments), 253 | react::Options { 254 | next: Some(true), 255 | use_builtins: Some(true), 256 | development: Some(is_dev), 257 | ..react_options 258 | }, 259 | top_level_mark 260 | ), 261 | is_jsx && !jsx_preserve 262 | ), 263 | Optional::new(hmr(resolver.clone()), is_dev && !specifier_is_remote), 264 | dce::dce( 265 | dce::Config { 266 | module_mark: None, 267 | top_level: true, 268 | top_retain: vec![], 269 | preserve_imports_with_side_effects: false, 270 | }, 271 | unresolved_mark 272 | ), 273 | Optional::new( 274 | as_folder(MinifierPass { 275 | cm: self.source_map.clone(), 276 | comments: Some(self.comments.clone()), 277 | unresolved_mark, 278 | top_level_mark, 279 | options: options.minify.unwrap_or(MinifierOptions { compress: Some(false) }), 280 | }), 281 | options.minify.is_some() 282 | ), 283 | hygiene::hygiene_with_config(hygiene::Config { 284 | keep_class_names: true, 285 | top_level_mark: top_level_mark, 286 | ..Default::default() 287 | }), 288 | fixer(Some(&self.comments)), 289 | ); 290 | 291 | let (mut code, map) = self.emit(passes, options).unwrap(); 292 | 293 | // remove dead deps by tree-shaking 294 | if options.strip_data_export { 295 | let mut resolver = resolver.borrow_mut(); 296 | let mut deps: Vec = Vec::new(); 297 | let a = code.split("\"").collect::>(); 298 | for dep in resolver.deps.clone() { 299 | if dep.specifier.ends_with("/jsx-runtime") 300 | || dep.specifier.ends_with("/jsx-dev-runtime") 301 | || a.contains(&dep.import_url.as_str()) 302 | { 303 | deps.push(dep); 304 | } 305 | } 306 | resolver.deps = deps; 307 | } 308 | 309 | // resolve jsx-runtime url 310 | let mut jsx_runtime = None; 311 | let resolver = resolver.borrow(); 312 | for dep in &resolver.deps { 313 | if dep.specifier.ends_with("/jsx-runtime") || dep.specifier.ends_with("/jsx-dev-runtime") { 314 | jsx_runtime = Some((dep.specifier.clone(), dep.import_url.clone())); 315 | break; 316 | } 317 | } 318 | if let Some((jsx_runtime, import_url)) = jsx_runtime { 319 | code = code.replace( 320 | format!("\"{}\"", jsx_runtime).as_str(), 321 | format!("\"{}\"", import_url).as_str(), 322 | ); 323 | } 324 | 325 | Ok((code, map)) 326 | }) 327 | } 328 | 329 | /// Apply transform with the fold. 330 | pub fn emit(&self, mut fold: T, options: &EmitOptions) -> Result<(String, Option), anyhow::Error> { 331 | let program = Program::Module(self.module.clone()); 332 | let program = helpers::HELPERS.set(&helpers::Helpers::new(false), || program.fold_with(&mut fold)); 333 | let mut buf = Vec::new(); 334 | let mut src_map_buf = Vec::new(); 335 | let src_map = if options.source_map { 336 | Some(&mut src_map_buf) 337 | } else { 338 | None 339 | }; 340 | 341 | { 342 | let writer = Box::new(JsWriter::new(self.source_map.clone(), "\n", &mut buf, src_map)); 343 | let mut emitter = swc_ecmascript::codegen::Emitter { 344 | cfg: swc_ecmascript::codegen::Config { 345 | target: options.target, 346 | minify: options.minify.is_some(), 347 | ..Default::default() 348 | }, 349 | comments: Some(&self.comments), 350 | cm: self.source_map.clone(), 351 | wr: writer, 352 | }; 353 | emitter.emit_program(&program).unwrap(); 354 | } 355 | 356 | // output 357 | let src = String::from_utf8(buf).unwrap(); 358 | if options.source_map { 359 | let mut buf = Vec::new(); 360 | self 361 | .source_map 362 | .build_source_map_from(&mut src_map_buf, None) 363 | .to_writer(&mut buf) 364 | .unwrap(); 365 | Ok((src, Some(String::from_utf8(buf).unwrap()))) 366 | } else { 367 | Ok((src, None)) 368 | } 369 | } 370 | } 371 | 372 | fn get_es_config(jsx: bool) -> EsConfig { 373 | EsConfig { 374 | fn_bind: true, 375 | export_default_from: true, 376 | import_assertions: true, 377 | allow_super_outside_method: true, 378 | allow_return_outside_function: true, 379 | jsx, 380 | ..EsConfig::default() 381 | } 382 | } 383 | 384 | fn get_ts_config(tsx: bool) -> TsConfig { 385 | TsConfig { 386 | decorators: true, 387 | tsx, 388 | ..TsConfig::default() 389 | } 390 | } 391 | 392 | fn get_syntax(specifier: &str, lang: Option) -> Syntax { 393 | let lang = if let Some(lang) = lang { 394 | lang 395 | } else { 396 | specifier 397 | .split(|c| c == '?' || c == '#') 398 | .next() 399 | .unwrap() 400 | .split('.') 401 | .last() 402 | .unwrap_or("js") 403 | .to_lowercase() 404 | }; 405 | match lang.as_str() { 406 | "js" | "mjs" => Syntax::Es(get_es_config(false)), 407 | "jsx" => Syntax::Es(get_es_config(true)), 408 | "ts" | "mts" => Syntax::Typescript(get_ts_config(false)), 409 | "tsx" => Syntax::Typescript(get_ts_config(true)), 410 | _ => Syntax::Es(get_es_config(false)), 411 | } 412 | } 413 | 414 | fn strip_config_from_emit_options() -> strip::Config { 415 | strip::Config { 416 | import_not_used_as_values: strip::ImportsNotUsedAsValues::Remove, 417 | use_define_for_class_fields: true, 418 | no_empty_export: true, 419 | ..Default::default() 420 | } 421 | } 422 | 423 | fn should_enable(target: EsVersion, feature: EsVersion) -> bool { 424 | target < feature 425 | } 426 | -------------------------------------------------------------------------------- /src/swc_helpers.rs: -------------------------------------------------------------------------------- 1 | use swc_common::DUMMY_SP; 2 | use swc_ecmascript::ast::*; 3 | use swc_ecmascript::utils::quote_ident; 4 | 5 | pub fn rename_var_decl(new_name: &str, old: &str) -> ModuleItem { 6 | ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { 7 | span: DUMMY_SP, 8 | kind: VarDeclKind::Const, 9 | declare: false, 10 | decls: vec![VarDeclarator { 11 | span: DUMMY_SP, 12 | name: pat_id(new_name), 13 | init: Some(Box::new(Expr::Ident(quote_ident!(old)))), 14 | definite: false, 15 | }], 16 | })))) 17 | } 18 | 19 | pub fn window_assign(name: &str, expr: Expr) -> ModuleItem { 20 | ModuleItem::Stmt(Stmt::Expr(ExprStmt { 21 | span: DUMMY_SP, 22 | expr: Box::new(Expr::Assign(AssignExpr { 23 | span: DUMMY_SP, 24 | op: AssignOp::Assign, 25 | left: PatOrExpr::Expr(Box::new(simple_member_expr("window", name))), 26 | right: Box::new(expr), 27 | })), 28 | })) 29 | } 30 | 31 | pub fn pat_id(id: &str) -> Pat { 32 | Pat::Ident(BindingIdent { 33 | id: quote_ident!(id), 34 | type_ann: None, 35 | }) 36 | } 37 | 38 | pub fn import_name(name: &str) -> ImportSpecifier { 39 | ImportSpecifier::Named(ImportNamedSpecifier { 40 | span: DUMMY_SP, 41 | local: quote_ident!(name), 42 | imported: None, 43 | is_type_only: false, 44 | }) 45 | } 46 | 47 | pub fn new_member_expr(obj: Expr, key: &str) -> MemberExpr { 48 | MemberExpr { 49 | span: DUMMY_SP, 50 | obj: Box::new(obj), 51 | prop: MemberProp::Ident(quote_ident!(key)), 52 | } 53 | } 54 | 55 | pub fn simple_member_expr(obj: &str, key: &str) -> Expr { 56 | Expr::Member(MemberExpr { 57 | span: DUMMY_SP, 58 | obj: Box::new(Expr::Ident(quote_ident!(obj))), 59 | prop: MemberProp::Ident(quote_ident!(key)), 60 | }) 61 | } 62 | 63 | pub fn is_call_expr_by_name(call: &CallExpr, name: &str) -> bool { 64 | let callee = match &call.callee { 65 | Callee::Super(_) => return false, 66 | Callee::Import(_) => return name.eq("import"), 67 | Callee::Expr(callee) => callee.as_ref(), 68 | }; 69 | 70 | match callee { 71 | Expr::Ident(id) => id.sym.as_ref().eq(name), 72 | _ => false, 73 | } 74 | } 75 | 76 | pub fn new_str(s: &str) -> Str { 77 | Str { 78 | span: DUMMY_SP, 79 | value: s.into(), 80 | raw: None, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use lightningcss::targets::Browsers; 3 | use regex::Regex; 4 | use std::collections::HashMap; 5 | 6 | fn transform(specifer: &str, source: &str, is_dev: bool, options: &EmitOptions) -> (String, Rc>) { 7 | let importmap = import_map::parse_from_json( 8 | &Url::from_str("file:///").unwrap(), 9 | r#"{ 10 | "imports": { 11 | "~/": "./", 12 | "react": "https://esm.sh/react@18" 13 | } 14 | }"#, 15 | ) 16 | .expect("could not pause the import map") 17 | .import_map; 18 | let mut graph_versions: HashMap = HashMap::new(); 19 | graph_versions.insert("./foo.ts".into(), "100".into()); 20 | let module = 21 | SWC::parse(specifer, source, swc_ecmascript::ast::EsVersion::Es2022, None).expect("could not parse module"); 22 | let resolver = Rc::new(RefCell::new(Resolver::new( 23 | specifer, 24 | "https://deno.land/x/aleph", 25 | importmap, 26 | graph_versions, 27 | Some("1.0.0".into()), 28 | true, 29 | is_dev, 30 | ))); 31 | let (code, _) = module.transform(resolver.clone(), options).unwrap(); 32 | println!("{}", code); 33 | (code, resolver) 34 | } 35 | 36 | #[test] 37 | fn typescript() { 38 | let source = r#" 39 | enum D { 40 | A, 41 | B, 42 | C, 43 | } 44 | 45 | function enumerable(value: boolean) { 46 | return function ( 47 | _target: any, 48 | _propertyKey: string, 49 | descriptor: PropertyDescriptor, 50 | ) { 51 | descriptor.enumerable = value; 52 | }; 53 | } 54 | 55 | export class A { 56 | #a: string; 57 | private b: string; 58 | protected c: number = 1; 59 | e: "foo"; 60 | constructor (public d = D.A) { 61 | const e = "foo" as const; 62 | this.e = e; 63 | } 64 | @enumerable(false) 65 | bar() {} 66 | } 67 | 68 | console.log(`${toString({class: A})}`) 69 | "#; 70 | let (code, _) = transform("mod.ts", source, false, &EmitOptions::default()); 71 | assert!(code.contains("var D;")); 72 | assert!(Regex::new(r"\[\s*enumerable\(false\)\s*\]").unwrap().is_match(&code)); 73 | } 74 | 75 | #[test] 76 | fn parcel_css() { 77 | let source = r#" 78 | @custom-media --modern (color), (hover); 79 | 80 | .foo { 81 | background: yellow; 82 | 83 | -webkit-border-radius: 2px; 84 | -moz-border-radius: 2px; 85 | border-radius: 2px; 86 | 87 | -webkit-transition: background 200ms; 88 | -moz-transition: background 200ms; 89 | transition: background 200ms; 90 | 91 | &.bar { 92 | color: green; 93 | } 94 | } 95 | 96 | @media (--modern) and (width > 1024px) { 97 | .a { 98 | color: green; 99 | } 100 | } 101 | "#; 102 | let cfg = css::Config { 103 | targets: Some(Browsers { 104 | chrome: Some(95), 105 | ..Browsers::default() 106 | }), 107 | minify: Some(true), 108 | source_map: None, 109 | css_modules: None, 110 | pseudo_classes: None, 111 | unused_symbols: None, 112 | analyze_dependencies: None, 113 | drafts: Some(css::Drafts { 114 | nesting: true, 115 | custom_media: true, 116 | }), 117 | }; 118 | let res = css::compile("style.css".into(), source, &cfg).unwrap(); 119 | assert_eq!(res.code, ".foo{background:#ff0;border-radius:2px;transition:background .2s}.foo.bar{color:green}@media ((color) or (hover)) and (min-width:1024px){.a{color:green}}"); 120 | } 121 | 122 | #[test] 123 | fn import_resolving() { 124 | let source = r#" 125 | import React from "react" 126 | import { foo } from "~/foo.ts" 127 | import Layout from "./Layout.tsx" 128 | import "https://esm.sh/@fullcalendar/daygrid?css&dev" 129 | import "../../style/app.css" 130 | 131 | foo() 132 | export default () => 133 | 134 | setTimeout(() => { 135 | import("https://esm.sh/asksomeonelse") 136 | new Worker("https://esm.sh/asksomeonelse") 137 | }, 1000) 138 | "#; 139 | let (code, _) = transform("./pages/blog/$id.tsx", source, false, &EmitOptions::default()); 140 | assert!(code.contains("\"/-/esm.sh/react@18\"")); 141 | assert!(code.contains("\"../../foo.ts?v=100\"")); 142 | assert!(code.contains("\"./Layout.tsx?v=1.0.0\"")); 143 | assert!(code.contains("\"/-/esm.sh/@fullcalendar/daygrid?css&dev&module\"")); 144 | assert!(code.contains("\"../../style/app.css?module&v=1.0.0\"")); 145 | assert!(code.contains("import(\"/-/esm.sh/asksomeonelse\")")); 146 | assert!(code.contains("new Worker(\"/-/esm.sh/asksomeonelse\")")); 147 | } 148 | 149 | #[test] 150 | fn jsx_preserve() { 151 | let source = r#" 152 | export default function App() { 153 | return ( 154 | <> 155 |

Hello world!

156 | 157 | ) 158 | } 159 | "#; 160 | let (code, _) = transform( 161 | "./app.tsx", 162 | source, 163 | false, 164 | &EmitOptions { 165 | jsx: Some("preserve".into()), 166 | ..Default::default() 167 | }, 168 | ); 169 | assert!(code.contains("

Hello world!

")); 170 | assert!(code.contains("<>")); 171 | assert!(code.contains("")); 172 | } 173 | 174 | #[test] 175 | fn jsx_classic() { 176 | let source = r#" 177 | import React from "react" 178 | export default function App() { 179 | return ( 180 | <> 181 |

Hello world!

182 | 183 | ) 184 | } 185 | "#; 186 | let (code, _) = transform( 187 | "./app.tsx", 188 | source, 189 | false, 190 | &EmitOptions { 191 | jsx: Some("classic".into()), 192 | ..Default::default() 193 | }, 194 | ); 195 | assert!(code.contains("React.createElement(\"h1\"")); 196 | assert!(code.contains("React.createElement(React.Fragment,")); 197 | } 198 | 199 | #[test] 200 | fn jsx_automtic() { 201 | let source = r#" 202 | /** @jsxImportSource https://esm.sh/react@18 */ 203 | export default function App() { 204 | return ( 205 | <> 206 |

Hello world!

207 | 208 | ) 209 | } 210 | "#; 211 | let (code, resolver) = transform( 212 | "./app.tsx", 213 | source, 214 | false, 215 | &EmitOptions { 216 | jsx: Some("automatic".into()), 217 | jsx_import_source: Some("https://esm.sh/react@18".to_owned()), 218 | ..Default::default() 219 | }, 220 | ); 221 | assert!(code.contains("import { jsx as _jsx, Fragment as _Fragment } from \"/-/esm.sh/react@18/jsx-runtime\"")); 222 | assert!(code.contains("_jsx(_Fragment, {")); 223 | assert!(code.contains("_jsx(\"h1\", {")); 224 | assert!(code.contains("children: \"Hello world!\"")); 225 | assert_eq!( 226 | resolver.borrow().deps.get(0).unwrap().specifier, 227 | "https://esm.sh/react@18/jsx-runtime" 228 | ); 229 | } 230 | 231 | #[test] 232 | fn react_refresh() { 233 | let source = r#" 234 | import { useState } from "react" 235 | export default function App() { 236 | const [ msg ] = useState('Hello world!') 237 | return ( 238 |

{msg}{foo()}

239 | ) 240 | } 241 | "#; 242 | let (code, _) = transform( 243 | "./app.tsx", 244 | source, 245 | true, 246 | &EmitOptions { 247 | react_refresh: true, 248 | jsx: Some("automatic".into()), 249 | jsx_import_source: Some("https://esm.sh/react@18".to_owned()), 250 | ..Default::default() 251 | }, 252 | ); 253 | assert!(code.contains( 254 | "import { __REACT_REFRESH_RUNTIME__, __REACT_REFRESH__ } from \"/-/deno.land/x/aleph/framework/react/refresh.ts\"" 255 | )); 256 | assert!(code.contains("const prevRefreshReg = $RefreshReg$")); 257 | assert!(code.contains("const prevRefreshSig = $RefreshSig$")); 258 | assert!(code.contains( 259 | "window.$RefreshReg$ = (type, id)=>__REACT_REFRESH_RUNTIME__.register(type, \"./app.tsx\" + (\"#\" + id))" 260 | )); 261 | assert!(code.contains("window.$RefreshSig$ = __REACT_REFRESH_RUNTIME__.createSignatureFunctionForTransform")); 262 | assert!(code.contains("var _s = $RefreshSig$()")); 263 | assert!(code.contains("_s()")); 264 | assert!(code.contains("_c = App")); 265 | assert!(code.contains("$RefreshReg$(_c, \"App\")")); 266 | assert!(code.contains("window.$RefreshReg$ = prevRefreshReg")); 267 | assert!(code.contains("window.$RefreshSig$ = prevRefreshSig;")); 268 | assert!(code.contains("import.meta.hot?.accept(__REACT_REFRESH__)")); 269 | } 270 | 271 | #[test] 272 | fn strip_data_export() { 273 | let source = r#" 274 | import { json } from "./helper.ts" 275 | const count = 0; 276 | export const data = { 277 | defer: true, 278 | fake: false, 279 | fetch: (req: Request) => { 280 | return json({ count }) 281 | }, 282 | } 283 | export const mutation = { 284 | POST: (req: Request) => { 285 | return json({ count }) 286 | }, 287 | DELETE: (req: Request) => { 288 | return json({ count }) 289 | }, 290 | } 291 | export const GET = (req: Request) => { 292 | return json({ count }) 293 | } 294 | export const POST = (req: Request) => { 295 | return json({ count }) 296 | } 297 | export const PUT = (req: Request) => { 298 | return json({ count }) 299 | } 300 | export function PATCH(req: Request) { 301 | return json({ count }) 302 | } 303 | export function DELETE(req: Request) { 304 | return json({ count }) 305 | } 306 | export function log(msg: string) { 307 | console.log(msg) 308 | } 309 | export default function App() { 310 | return
Hello world!
311 | } 312 | "#; 313 | let (code, r) = transform( 314 | "./app.tsx", 315 | source, 316 | false, 317 | &EmitOptions { 318 | strip_data_export: true, 319 | jsx: Some("automatic".into()), 320 | jsx_import_source: Some("https://esm.sh/react@18".to_owned()), 321 | ..Default::default() 322 | }, 323 | ); 324 | assert!(code.contains("export const data = {")); 325 | assert!(code.contains("defer: true,")); 326 | assert!(code.contains("fake: false,")); 327 | assert!(code.contains("fetch: true\n}")); 328 | assert!(code.contains("export const mutation = {")); 329 | assert!(code.contains("POST: true,")); 330 | assert!(code.contains("DELETE: true\n")); 331 | assert!(code.contains("export const GET = true")); 332 | assert!(code.contains("export const POST = true")); 333 | assert!(code.contains("export const PUT = true")); 334 | assert!(code.contains("export function PATCH() {}")); 335 | assert!(code.contains("export function DELETE() {}")); 336 | assert!(code.contains("export function log(msg) {")); 337 | assert!(!code.contains("import { json } from \"./helper.ts\"")); 338 | assert!(!code.contains("const count = 0")); 339 | assert_eq!(r.borrow().deps.len(), 1); 340 | } 341 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | assertStringIncludes, 4 | } from "https://deno.land/std@0.180.0/testing/asserts.ts"; 5 | import { transform, transformCSS } from "./mod.ts"; 6 | 7 | Deno.test("aleph compiler", async (t) => { 8 | await t.step("transform css", async () => { 9 | const ret = await transformCSS( 10 | "./app.css", 11 | `@custom-media --modern (color), (hover); 12 | 13 | .foo { 14 | background: yellow; 15 | 16 | -webkit-border-radius: 2px; 17 | -moz-border-radius: 2px; 18 | border-radius: 2px; 19 | 20 | -webkit-transition: background 200ms; 21 | -moz-transition: background 200ms; 22 | transition: background 200ms; 23 | 24 | &.bar { 25 | color: green; 26 | } 27 | } 28 | 29 | @media (--modern) and (width > 1024px) { 30 | .a { 31 | color: green; 32 | } 33 | }`, 34 | { 35 | minify: true, 36 | targets: { 37 | chrome: 95, 38 | }, 39 | drafts: { 40 | nesting: true, 41 | customMedia: true, 42 | }, 43 | }, 44 | ); 45 | assertEquals( 46 | ret.code, 47 | `.foo{background:#ff0;border-radius:2px;transition:background .2s}.foo.bar{color:green}@media ((color) or (hover)) and (min-width:1024px){.a{color:green}}`, 48 | ); 49 | }); 50 | 51 | await t.step("transform ts", async () => { 52 | const ret = await transform( 53 | "./mod.ts", 54 | await Deno.readTextFile("./mod.ts"), 55 | ); 56 | assertStringIncludes(ret.code, `function transform(`); 57 | }); 58 | 59 | await t.step("transform jsx", async () => { 60 | const ret = await transform( 61 | "./app.jsx", 62 | ` 63 | import React from "https://esm.sh/react"; 64 | 65 | export default function App() { 66 | return

Hello world!

67 | } 68 | `, 69 | ); 70 | assertStringIncludes(ret.code, `React.createElement("h1"`); 71 | }); 72 | 73 | await t.step("transform jsx (preserve)", async () => { 74 | const ret = await transform( 75 | "./app.jsx", 76 | ` 77 | export default function App() { 78 | return

Hello world!

79 | } 80 | `, 81 | { 82 | jsx: "preserve", 83 | }, 84 | ); 85 | assertStringIncludes(ret.code, `

Hello world!

`); 86 | }); 87 | 88 | await t.step("transform jsx (automatic)", async () => { 89 | const ret = await transform( 90 | "./app.jsx", 91 | ` 92 | export default function App() { 93 | return

Hello world!

94 | } 95 | `, 96 | { 97 | jsx: "automatic", 98 | jsxImportSource: "https://esm.sh/react", 99 | resolveRemoteModule: true, 100 | }, 101 | ); 102 | assertStringIncludes( 103 | ret.code, 104 | `import { jsx as _jsx } from "/-/esm.sh/react/jsx-runtime"`, 105 | ); 106 | assertStringIncludes(ret.code, `_jsx("h1"`); 107 | }); 108 | 109 | await t.step("transform large js", async () => { 110 | const ret = await transform( 111 | "./gsi-client.js", 112 | await Deno.readTextFile("./testdata/gsi-client.js"), 113 | { minify: { compress: true } }, 114 | ); 115 | assertStringIncludes(ret.code, `this.default_gsi`); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export type EsmaVersion = 2 | | "es2015" 3 | | "es2016" 4 | | "es2017" 5 | | "es2018" 6 | | "es2019" 7 | | "es2020" 8 | | "es2021" 9 | | "es2022"; 10 | 11 | export type TransformOptions = { 12 | alephPkgUri?: string; 13 | lang?: "ts" | "tsx" | "js" | "jsx"; 14 | target?: EsmaVersion; 15 | importMap?: string; 16 | globalVersion?: string; 17 | graphVersions?: Record; 18 | resolveRemoteModule?: boolean; 19 | stripDataExport?: boolean; 20 | isDev?: boolean; 21 | reactRefresh?: boolean; 22 | sourceMap?: boolean; 23 | jsx?: "automatic" | "classic" | "preserve"; 24 | jsxPragma?: string; 25 | jsxPragmaFrag?: string; 26 | jsxImportSource?: string; 27 | minify?: { compress: boolean }; 28 | }; 29 | 30 | export type TransformResult = { 31 | readonly code: string; 32 | readonly map?: string; 33 | readonly deps?: DependencyDescriptor[]; 34 | }; 35 | 36 | export type DependencyDescriptor = { 37 | readonly specifier: string; 38 | readonly importUrl: string; 39 | readonly loc?: { start: number; end: number; ctxt: number }; 40 | readonly dynamic?: boolean; 41 | }; 42 | 43 | export interface Targets { 44 | android?: number; 45 | chrome?: number; 46 | edge?: number; 47 | firefox?: number; 48 | ie?: number; 49 | ios_saf?: number; 50 | opera?: number; 51 | safari?: number; 52 | samsung?: number; 53 | } 54 | 55 | export interface DependencyOptions { 56 | removeImports: boolean; 57 | } 58 | 59 | export interface TransformCSSOptions { 60 | /** Whether to enable minification. */ 61 | minify?: boolean; 62 | /** Whether to output a source map. */ 63 | sourceMap?: boolean; 64 | /** The browser targets for the generated code. */ 65 | targets?: Targets; 66 | /** Whether to enable various draft syntax. */ 67 | drafts?: Drafts; 68 | /** Whether to compile this file as a CSS module. */ 69 | cssModules?: boolean | CSSModulesConfig; 70 | /** 71 | * Whether to analyze dependencies (e.g. `@import` and `url()`). 72 | * When enabled, `@import` rules are removed, and `url()` dependencies 73 | * are replaced with hashed placeholders that can be replaced with the final 74 | * urls later (after bundling). Dependencies are returned as part of the result. 75 | */ 76 | analyzeDependencies?: DependencyOptions; 77 | /** 78 | * Replaces user action pseudo classes with class names that can be applied from JavaScript. 79 | * This is useful for polyfills, for example. 80 | */ 81 | pseudoClasses?: PseudoClasses; 82 | /** 83 | * A list of class names, ids, and custom identifiers (e.g. @keyframes) that are known 84 | * to be unused. These will be removed during minification. Note that these are not 85 | * selectors but individual names (without any . or # prefixes). 86 | */ 87 | unusedSymbols?: string[]; 88 | } 89 | 90 | export interface Drafts { 91 | /** Whether to enable CSS nesting. */ 92 | nesting?: boolean; 93 | /** Whether to enable @custom-media rules. */ 94 | customMedia?: boolean; 95 | } 96 | 97 | export interface PseudoClasses { 98 | hover?: string; 99 | active?: string; 100 | focus?: string; 101 | focusVisible?: string; 102 | focusWithin?: string; 103 | } 104 | 105 | export interface TransformCSSResult { 106 | /** The transformed code. */ 107 | readonly code: string; 108 | /** The generated source map, if enabled. */ 109 | readonly map?: string; 110 | /** CSS module exports, if enabled. */ 111 | readonly exports?: CSSModuleExports; 112 | /** `@import` and `url()` dependencies, if enabled. */ 113 | readonly dependencies?: Dependency[]; 114 | } 115 | 116 | export interface CSSModulesConfig { 117 | /** The pattern to use when renaming class names and other identifiers. Default is `[hash]_[local]`. */ 118 | pattern?: string; 119 | /** Whether to rename dashed identifiers, e.g. custom properties. */ 120 | dashedIdents?: boolean; 121 | } 122 | 123 | export type CSSModuleExports = { 124 | /** Maps exported (i.e. original) names to local names. */ 125 | readonly [name: string]: CSSModuleExport; 126 | }; 127 | 128 | export interface CSSModuleExport { 129 | /** The local (compiled) name for this export. */ 130 | readonly name: string; 131 | /** Whether the export is referenced in this file. */ 132 | readonly isReferenced: boolean; 133 | /** Other names that are composed by this export. */ 134 | readonly composes: CSSModuleReference[]; 135 | } 136 | 137 | export type CSSModuleReference = 138 | | LocalCSSModuleReference 139 | | GlobalCSSModuleReference 140 | | DependencyCSSModuleReference; 141 | 142 | export interface LocalCSSModuleReference { 143 | readonly type: "local"; 144 | /** The local (compiled) name for the reference. */ 145 | readonly name: string; 146 | } 147 | 148 | export interface GlobalCSSModuleReference { 149 | readonly type: "global"; 150 | /** The referenced global name. */ 151 | readonly name: string; 152 | } 153 | 154 | export interface DependencyCSSModuleReference { 155 | readonly type: "dependency"; 156 | /** The name to reference within the dependency. */ 157 | readonly name: string; 158 | /** The dependency specifier for the referenced file. */ 159 | readonly specifier: string; 160 | } 161 | 162 | export type Dependency = ImportDependency | UrlDependency; 163 | 164 | export interface ImportDependency { 165 | readonly type: "import"; 166 | /** The url of the `@import` dependency. */ 167 | readonly url: string; 168 | /** The media query for the `@import` rule. */ 169 | readonly media: string | null; 170 | /** The `supports()` query for the `@import` rule. */ 171 | readonly supports: string | null; 172 | /** The source location where the `@import` rule was found. */ 173 | readonly loc: SourceLocation; 174 | } 175 | 176 | export interface UrlDependency { 177 | readonly type: "url"; 178 | /** The url of the dependency. */ 179 | readonly url: string; 180 | /** The source location where the `url()` was found. */ 181 | readonly loc: SourceLocation; 182 | /** The placeholder that the url was replaced with. */ 183 | readonly placeholder: string; 184 | } 185 | 186 | export interface SourceLocation { 187 | /** The file path in which the dependency exists. */ 188 | readonly filePath: string; 189 | /** The start location of the dependency. */ 190 | readonly start: Location; 191 | /** The end location (inclusive) of the dependency. */ 192 | readonly end: Location; 193 | } 194 | 195 | export interface Location { 196 | /** The line number (1-based). */ 197 | readonly line: number; 198 | /** The column number (0-based). */ 199 | readonly column: number; 200 | } 201 | -------------------------------------------------------------------------------- /version.ts: -------------------------------------------------------------------------------- 1 | /** `VERSION` managed by https://deno.land/x/publish */ 2 | export const VERSION = "0.9.4"; 3 | 4 | /** `prepublish` will be invoked before publish */ 5 | export async function prepublish(version: string): Promise { 6 | const p = Deno.run({ 7 | cmd: ["deno", "run", "-A", "build.ts"], 8 | stdout: "inherit", 9 | stderr: "inherit", 10 | }); 11 | const { success } = await p.status(); 12 | if (success) { 13 | const toml = await Deno.readTextFile("./Cargo.toml"); 14 | await Deno.writeTextFile( 15 | "./Cargo.toml", 16 | toml.replace(/version = "[\d\.]+"/, `version = "${version}"`), 17 | ); 18 | } 19 | p.close(); 20 | return success; 21 | } 22 | --------------------------------------------------------------------------------