├── .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 | [](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 |
--------------------------------------------------------------------------------