mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 10:47:35 -05:00
Compare commits
791 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e433275938 | ||
|
|
87256a662b | ||
|
|
7336ae545e | ||
|
|
d68260c0c7 | ||
|
|
14cc05fb80 | ||
|
|
42c6cb775e | ||
|
|
b11fb2e893 | ||
|
|
3223a9c7e9 | ||
|
|
139ce9b5fb | ||
|
|
3b1af0688c | ||
|
|
7284f7f15f | ||
|
|
16ebc50481 | ||
|
|
b911ea99fb | ||
|
|
46ca9154c4 | ||
|
|
afd7a8eb00 | ||
|
|
b22f384d73 | ||
|
|
caeab77a8e | ||
|
|
f2bbc6ed5f | ||
|
|
1d666b92a2 | ||
|
|
290eb03388 | ||
|
|
55031f2347 | ||
|
|
8168383653 | ||
|
|
3ff2c171bc | ||
|
|
edbac13713 | ||
|
|
46239caa19 | ||
|
|
0366e48fad | ||
|
|
b13317d32f | ||
|
|
57b1440a10 | ||
|
|
fc53356a1d | ||
|
|
5be99456f0 | ||
|
|
7adedcc704 | ||
|
|
d143b229ed | ||
|
|
0d74a60227 | ||
|
|
ce923a354f | ||
|
|
7e65836423 | ||
|
|
796373cfa6 | ||
|
|
0a1d3bb678 | ||
|
|
4e43d3e92d | ||
|
|
5acc2afaf3 | ||
|
|
56c64cbee7 | ||
|
|
4ff02a24d1 | ||
|
|
a5dc94792b | ||
|
|
0db79e0f02 | ||
|
|
283eabdb30 | ||
|
|
3f19e00410 | ||
|
|
9233d5e45a | ||
|
|
fe580cd90b | ||
|
|
00e6cfed29 | ||
|
|
6b1330d28a | ||
|
|
5d751493f6 | ||
|
|
c3517f7973 | ||
|
|
b1c33b7f79 | ||
|
|
722abda895 | ||
|
|
8061bc0143 | ||
|
|
993e812e7b | ||
|
|
a6b6dd8479 | ||
|
|
febbb031ac | ||
|
|
f827efe2fc | ||
|
|
8c6bfecbff | ||
|
|
f562860e49 | ||
|
|
62c78b0f42 | ||
|
|
4fe6ad80af | ||
|
|
89a0a556f3 | ||
|
|
af06bc4826 | ||
|
|
6fab9a1c26 | ||
|
|
386966ebd2 | ||
|
|
c257b704e6 | ||
|
|
fc599d96a2 | ||
|
|
8de97a7e7b | ||
|
|
2642be863b | ||
|
|
9dad29ec17 | ||
|
|
abb1eb28fe | ||
|
|
532ac0c80f | ||
|
|
c1849d28e0 | ||
|
|
ad2af2392b | ||
|
|
957ab69c21 | ||
|
|
f5d49ee52c | ||
|
|
06138bbb48 | ||
|
|
ae66d13d57 | ||
|
|
5aaeebf32c | ||
|
|
a6b0964185 | ||
|
|
dd86169f2a | ||
|
|
84c7d69db9 | ||
|
|
ca9b565adc | ||
|
|
b2d1018db1 | ||
|
|
c4e25d879e | ||
|
|
c82dbb755e | ||
|
|
1ed77321a5 | ||
|
|
e36a57eb06 | ||
|
|
ee3cd76022 | ||
|
|
eb31e51a45 | ||
|
|
c8c2dd0806 | ||
|
|
e525d54be0 | ||
|
|
7b9f7c13f5 | ||
|
|
7582cbef9c | ||
|
|
b395cde49c | ||
|
|
9f46234f71 | ||
|
|
f4c4edeb29 | ||
|
|
7166c32384 | ||
|
|
ed126889ae | ||
|
|
0d0ea96435 | ||
|
|
fafdf74007 | ||
|
|
999897df12 | ||
|
|
d47fb58ede | ||
|
|
f316341e73 | ||
|
|
8c46fcabf7 | ||
|
|
e4ff4d8fab | ||
|
|
f20a0003ed | ||
|
|
6c938dfe3b | ||
|
|
d884586a82 | ||
|
|
f8f7ba89c1 | ||
|
|
67d47511d5 | ||
|
|
6f49f18937 | ||
|
|
7b8ef7ebe2 | ||
|
|
461ae38763 | ||
|
|
4b90ebdef5 | ||
|
|
15c19aa708 | ||
|
|
d0406be84c | ||
|
|
aab015b9b8 | ||
|
|
a1e5a05b49 | ||
|
|
f1a787e031 | ||
|
|
b41cfb7b60 | ||
|
|
1c675307e1 | ||
|
|
ac56f82c6d | ||
|
|
2ac5ca79d7 | ||
|
|
cb9ee6f7e2 | ||
|
|
9abaef33bd | ||
|
|
320a537db2 | ||
|
|
9b775ce015 | ||
|
|
66f72eda1e | ||
|
|
39ca29749c | ||
|
|
85da548447 | ||
|
|
9985787e4b | ||
|
|
18ec6ce775 | ||
|
|
ed6d0136b8 | ||
|
|
e7216d26e7 | ||
|
|
d1a0d8ea98 | ||
|
|
04222b0d03 | ||
|
|
8ec3381789 | ||
|
|
9bd4737708 | ||
|
|
c49cb9231b | ||
|
|
d1d1c5740f | ||
|
|
1a05ea5fd2 | ||
|
|
5b4e8527da | ||
|
|
83c2afeaf1 | ||
|
|
643b76479f | ||
|
|
cf92996071 | ||
|
|
c653296821 | ||
|
|
44cd6273ba | ||
|
|
d7d2dfb383 | ||
|
|
2d5cf096e0 | ||
|
|
6ee8217593 | ||
|
|
6d45728787 | ||
|
|
65954a627e | ||
|
|
2f1b764251 | ||
|
|
1fb6cddd70 | ||
|
|
239add4e20 | ||
|
|
4e4ac56729 | ||
|
|
1447cb3094 | ||
|
|
2f3659b676 | ||
|
|
910c666319 | ||
|
|
eee2f64c1d | ||
|
|
d080246a0f | ||
|
|
98c0f5e50d | ||
|
|
a1268f1aa2 | ||
|
|
69b8884045 | ||
|
|
df1d699fe6 | ||
|
|
84f197b657 | ||
|
|
5bed5ede52 | ||
|
|
f6d5cf06c8 | ||
|
|
30c6c830ae | ||
|
|
d7084f7f55 | ||
|
|
a87d2b3fea | ||
|
|
81a26363a3 | ||
|
|
53e945201d | ||
|
|
181d276c8b | ||
|
|
bcaab0eb93 | ||
|
|
3ff0f84391 | ||
|
|
2b411fc635 | ||
|
|
2128572ce5 | ||
|
|
63f2453091 | ||
|
|
f6470dcad5 | ||
|
|
a9717afeb7 | ||
|
|
cea52b4334 | ||
|
|
7ad7f0abfb | ||
|
|
1efd73af8f | ||
|
|
1e7fb9af44 | ||
|
|
154e00d320 | ||
|
|
0f788fa284 | ||
|
|
355866a1f1 | ||
|
|
6eca06ac0b | ||
|
|
a4aef0b530 | ||
|
|
136e95498f | ||
|
|
6a3e2834b6 | ||
|
|
c0d45d368b | ||
|
|
f18ec3d20a | ||
|
|
b0377cc7ab | ||
|
|
96e671b55f | ||
|
|
40e99abbdf | ||
|
|
8b6b055681 | ||
|
|
8e5605fa42 | ||
|
|
06e1fdecc2 | ||
|
|
a82e8334d6 | ||
|
|
539bc2ae0e | ||
|
|
0711acd30e | ||
|
|
1476131ab4 | ||
|
|
89902a440c | ||
|
|
156c23d550 | ||
|
|
30396ba79a | ||
|
|
a4343c62ca | ||
|
|
4b89c84692 | ||
|
|
df68449b82 | ||
|
|
48e3383f66 | ||
|
|
e750fa7393 | ||
|
|
5a15199a3a | ||
|
|
1801472fc4 | ||
|
|
ab15ac37ff | ||
|
|
0955a6be49 | ||
|
|
d58237ea15 | ||
|
|
2d50ca86a6 | ||
|
|
f1a46be738 | ||
|
|
3e2a67d434 | ||
|
|
aef028be6e | ||
|
|
c8ec29a3d8 | ||
|
|
e81830a2ea | ||
|
|
54df7171a2 | ||
|
|
b31af823d1 | ||
|
|
72f266532b | ||
|
|
d9bf5cae12 | ||
|
|
cd95a42e5e | ||
|
|
e67eb06d8b | ||
|
|
28d37cdead | ||
|
|
13604e0a47 | ||
|
|
aeb6f1a755 | ||
|
|
92e6f711b7 | ||
|
|
a24113f42b | ||
|
|
7a6f8ab3ad | ||
|
|
6dd242f3ce | ||
|
|
88fa82c61a | ||
|
|
2299ba5f61 | ||
|
|
117df6ca38 | ||
|
|
4256a81653 | ||
|
|
d5b6935c0b | ||
|
|
b4503ef729 | ||
|
|
a00a6750b4 | ||
|
|
a08f891b20 | ||
|
|
bc1cac9c41 | ||
|
|
50f7ab0f34 | ||
|
|
fdc35ce3ed | ||
|
|
5c4e400d32 | ||
|
|
7a23e355b9 | ||
|
|
dffac642a1 | ||
|
|
97699eaded | ||
|
|
c6aaacdbf1 | ||
|
|
abfc68765f | ||
|
|
3ac2ac0982 | ||
|
|
b9a1227e47 | ||
|
|
801c63947a | ||
|
|
ffee4add4a | ||
|
|
f0be7ef418 | ||
|
|
e4eedd80bc | ||
|
|
c9e7fe16e4 | ||
|
|
5079dd19cb | ||
|
|
b4c686f411 | ||
|
|
287d0fa1af | ||
|
|
b78455c4c1 | ||
|
|
312b6b0706 | ||
|
|
924e530096 | ||
|
|
ef8918f3a7 | ||
|
|
91ae242e49 | ||
|
|
fd307e52ae | ||
|
|
a68967c773 | ||
|
|
52da45bb9c | ||
|
|
ad0dde3f17 | ||
|
|
8f3c36deea | ||
|
|
23e1ab81b3 | ||
|
|
77b40aa348 | ||
|
|
f6decfd93d | ||
|
|
e8d5138cfa | ||
|
|
a088fbd6fb | ||
|
|
19214901f9 | ||
|
|
c330a623b2 | ||
|
|
f77241e977 | ||
|
|
ed6de66c08 | ||
|
|
5191c45113 | ||
|
|
840bc803b7 | ||
|
|
00fdc73015 | ||
|
|
9660976d1d | ||
|
|
7f666dc6a0 | ||
|
|
e2a2292a6f | ||
|
|
4d89cbde01 | ||
|
|
d8e1cb8b0f | ||
|
|
3aef5a99dc | ||
|
|
7994207c78 | ||
|
|
f376097a15 | ||
|
|
2a2ff4066d | ||
|
|
32c3fb72cc | ||
|
|
e44e18114d | ||
|
|
7d2df4895e | ||
|
|
59db56feec | ||
|
|
fd60b4789a | ||
|
|
0696e4682d | ||
|
|
d56eeb59ed | ||
|
|
1d015c7534 | ||
|
|
264675d0c3 | ||
|
|
37d4cb7c48 | ||
|
|
cabb1c72b6 | ||
|
|
489a2bb20e | ||
|
|
d5f42e57ce | ||
|
|
94b0bf4131 | ||
|
|
12428c0617 | ||
|
|
ef44df5dda | ||
|
|
da3b43abdd | ||
|
|
4cc9647dc6 | ||
|
|
74cd7c840a | ||
|
|
0f2deeb71a | ||
|
|
93539c9b5a | ||
|
|
e48e6276e1 | ||
|
|
75a57a49f5 | ||
|
|
8a1db288fc | ||
|
|
84dcde188b | ||
|
|
27c91e9703 | ||
|
|
b5a0cd4057 | ||
|
|
77d8fe3562 | ||
|
|
a484aff457 | ||
|
|
c96f5912df | ||
|
|
8a01a56e51 | ||
|
|
2774e49ab9 | ||
|
|
26e7a54f1f | ||
|
|
f0e69cbc36 | ||
|
|
413428f535 | ||
|
|
0c54036466 | ||
|
|
2555833831 | ||
|
|
7e0aceced1 | ||
|
|
77234f6df3 | ||
|
|
45af96aad4 | ||
|
|
184d29055e | ||
|
|
9e73181816 | ||
|
|
0b0e03456c | ||
|
|
c6b5ce7f55 | ||
|
|
a14e701be4 | ||
|
|
7813c3f03f | ||
|
|
3a3cb7b11d | ||
|
|
d7b0731385 | ||
|
|
df8973736f | ||
|
|
9121071ba3 | ||
|
|
bf6470c046 | ||
|
|
3b7099cd3d | ||
|
|
f6dfc5361e | ||
|
|
0a7e1ce0d7 | ||
|
|
d6b1c393f6 | ||
|
|
bccd5e3750 | ||
|
|
6df5905b2b | ||
|
|
6284c02032 | ||
|
|
db27d52352 | ||
|
|
8ba28989fb | ||
|
|
da544929ac | ||
|
|
bb364b0524 | ||
|
|
818614b798 | ||
|
|
50b1a1d7c5 | ||
|
|
7d3b792a79 | ||
|
|
af72e232c3 | ||
|
|
0cdbfbeb30 | ||
|
|
339e40063a | ||
|
|
4467898473 | ||
|
|
17d16b987f | ||
|
|
8e86daac71 | ||
|
|
856720da49 | ||
|
|
8f2c150d1e | ||
|
|
7d8b4c980a | ||
|
|
932756c7a0 | ||
|
|
538aac9a28 | ||
|
|
856bf8f5fb | ||
|
|
e1758ae2e2 | ||
|
|
61b3154461 | ||
|
|
fb9b30d144 | ||
|
|
b0df96b13f | ||
|
|
a469062a32 | ||
|
|
89d5d5c7db | ||
|
|
b8c2d6b05d | ||
|
|
b247864414 | ||
|
|
d3bcd87cfa | ||
|
|
82e5b64bad | ||
|
|
73e0271c23 | ||
|
|
a2dabee0e9 | ||
|
|
6a27c6d9f2 | ||
|
|
213ced0c7f | ||
|
|
5086c23d47 | ||
|
|
ee345a5206 | ||
|
|
f74cddc3b1 | ||
|
|
5b986b8b26 | ||
|
|
14887b9814 | ||
|
|
ecc40315b3 | ||
|
|
e7aed7fcf0 | ||
|
|
cd1aa948f9 | ||
|
|
82613d016a | ||
|
|
3a66be585f | ||
|
|
0a4e36ae09 | ||
|
|
92643539cf | ||
|
|
a1281d1331 | ||
|
|
074ca0ef8f | ||
|
|
464a9633dc | ||
|
|
fc2d91c5bb | ||
|
|
d68169bffb | ||
|
|
7efdb04e1e | ||
|
|
0155e122fd | ||
|
|
eb03f16a77 | ||
|
|
5ac39641ab | ||
|
|
8d1e48e400 | ||
|
|
0021ccb49f | ||
|
|
8590c7e5b8 | ||
|
|
8c5475f78f | ||
|
|
dfa116eb70 | ||
|
|
3a9fd3c074 | ||
|
|
5a92ef3c11 | ||
|
|
d3902f5c93 | ||
|
|
c886f887ae | ||
|
|
fc5089ac59 | ||
|
|
e3602f464b | ||
|
|
f3db6a339c | ||
|
|
c05195c045 | ||
|
|
af981fc719 | ||
|
|
088a264910 | ||
|
|
d7e80ad51b | ||
|
|
b53ddd401f | ||
|
|
e9122bca9d | ||
|
|
b61e8435d1 | ||
|
|
146afb6532 | ||
|
|
854e9d1378 | ||
|
|
689878ce32 | ||
|
|
d7ab177cc5 | ||
|
|
f4c6093c47 | ||
|
|
9fedfe3699 | ||
|
|
26f07246e1 | ||
|
|
3ae4b3c4de | ||
|
|
c8f9f16791 | ||
|
|
88f0738500 | ||
|
|
03c79d5f2f | ||
|
|
e7c3b7bcfe | ||
|
|
c8becca044 | ||
|
|
543a27271f | ||
|
|
a62aba83a0 | ||
|
|
53c6cf5f45 | ||
|
|
89842e20da | ||
|
|
ef793aecf3 | ||
|
|
51d51409d3 | ||
|
|
371b5eac45 | ||
|
|
5319bd13d5 | ||
|
|
e10d055453 | ||
|
|
716254e655 | ||
|
|
4c00b1683f | ||
|
|
37c9db09c6 | ||
|
|
653e2c9be4 | ||
|
|
a2a9613da1 | ||
|
|
e8d92d0d34 | ||
|
|
755b98a8c0 | ||
|
|
13e9252260 | ||
|
|
6a9c27325a | ||
|
|
a1cb78eb85 | ||
|
|
716b57ebd3 | ||
|
|
8e231313b8 | ||
|
|
84e4e361c5 | ||
|
|
41a8d804e3 | ||
|
|
03e798a079 | ||
|
|
34a0205757 | ||
|
|
ba145f04ea | ||
|
|
22fd023635 | ||
|
|
08f34f748b | ||
|
|
7ffe6a598e | ||
|
|
71d24a445e | ||
|
|
6bcbbfb085 | ||
|
|
04fe1348d8 | ||
|
|
3033c779b0 | ||
|
|
4483f0db0f | ||
|
|
727267ae22 | ||
|
|
b5d15c2f7e | ||
|
|
589c614e57 | ||
|
|
4588e90226 | ||
|
|
8665a14dec | ||
|
|
43d598d951 | ||
|
|
68018cf078 | ||
|
|
ef4ab0d7a8 | ||
|
|
e66a2702df | ||
|
|
c57d4a7054 | ||
|
|
a36f08f0f1 | ||
|
|
760a8c75a5 | ||
|
|
740fd921e1 | ||
|
|
065c697070 | ||
|
|
e2c2459290 | ||
|
|
11c79a5344 | ||
|
|
429fe4c356 | ||
|
|
a18b4edfc0 | ||
|
|
b14a2bba5f | ||
|
|
1f825edc28 | ||
|
|
6ed834807a | ||
|
|
9a908e5fd0 | ||
|
|
4c30359b71 | ||
|
|
34dfe2d80b | ||
|
|
25bcff10b7 | ||
|
|
81268d0545 | ||
|
|
8f0a7706d7 | ||
|
|
46150f9b80 | ||
|
|
247745b7e7 | ||
|
|
94cc09b610 | ||
|
|
a210b2d5f5 | ||
|
|
12bf6db331 | ||
|
|
697ac9de9a | ||
|
|
4124bb5edc | ||
|
|
d55340a817 | ||
|
|
0de8cd9ab7 | ||
|
|
4e8281c749 | ||
|
|
357fbc644d | ||
|
|
7947a8a2dc | ||
|
|
35de3aa154 | ||
|
|
1ea687beb8 | ||
|
|
bb5c59307a | ||
|
|
5a3c414c8f | ||
|
|
cc4b460183 | ||
|
|
470c3489dd | ||
|
|
e1b4415193 | ||
|
|
77d98a565e | ||
|
|
412da2de08 | ||
|
|
dbdcd0b3d0 | ||
|
|
5c67384fbf | ||
|
|
35b0f9d377 | ||
|
|
95783bc284 | ||
|
|
4b840f7cbd | ||
|
|
f73d6cd9f2 | ||
|
|
15bb8f03ea | ||
|
|
059dbc88c9 | ||
|
|
c0f36aa047 | ||
|
|
d4120d2af3 | ||
|
|
dd1c008447 | ||
|
|
3721d2cd72 | ||
|
|
e0dda0e547 | ||
|
|
3c7568c72c | ||
|
|
6be1758548 | ||
|
|
25809660ef | ||
|
|
6b9eff45bb | ||
|
|
08e83feaf5 | ||
|
|
4f05b5afc6 | ||
|
|
9a5bf9918e | ||
|
|
1c7cf0ba7d | ||
|
|
ce2d1a4513 | ||
|
|
7d50d7eea0 | ||
|
|
e9411dc796 | ||
|
|
5cf2de16d1 | ||
|
|
e53bcf15a9 | ||
|
|
bec70b60b8 | ||
|
|
8b7fb89c68 | ||
|
|
b2bbdda73d | ||
|
|
ee2f46cfb9 | ||
|
|
4337e6833a | ||
|
|
a73c73b814 | ||
|
|
e8318a98f0 | ||
|
|
94f2ac6204 | ||
|
|
cc6cb4ded0 | ||
|
|
c61d4191c3 | ||
|
|
e284da7c09 | ||
|
|
9992096654 | ||
|
|
c696d92f40 | ||
|
|
af60299324 | ||
|
|
5aa9135a34 | ||
|
|
72e23ac86f | ||
|
|
33d49ad87d | ||
|
|
aa2335ca2e | ||
|
|
b31428006c | ||
|
|
dc1d583791 | ||
|
|
4299a74e40 | ||
|
|
3e408b7baa | ||
|
|
446c131ccb | ||
|
|
b062efcf17 | ||
|
|
30e31a86ef | ||
|
|
182272e8c7 | ||
|
|
06df21e8e3 | ||
|
|
cafebd68f2 | ||
|
|
061d4b3f72 | ||
|
|
6586e79d5e | ||
|
|
cda6c6bc7e | ||
|
|
a628026838 | ||
|
|
6700856b9f | ||
|
|
411aa0bbed | ||
|
|
0d79d31b96 | ||
|
|
cb05a9b067 | ||
|
|
536f359fb9 | ||
|
|
56e888ed33 | ||
|
|
687b93d148 | ||
|
|
0e1c396d7c | ||
|
|
7e24289703 | ||
|
|
0b23310a06 | ||
|
|
41ebaaf366 | ||
|
|
b79ceea7a8 | ||
|
|
b990bcb67a | ||
|
|
3f0f2d9910 | ||
|
|
4333f5f979 | ||
|
|
07e75293b8 | ||
|
|
a9ca7106cb | ||
|
|
da2728e6df | ||
|
|
adfa9a9b05 | ||
|
|
be9b9f66d3 | ||
|
|
9521bc7175 | ||
|
|
b445f8a834 | ||
|
|
3c3dffd5ed | ||
|
|
4c8443fd00 | ||
|
|
06a5a54103 | ||
|
|
0d3c3eef4e | ||
|
|
f0a6fb913f | ||
|
|
5f0c508fed | ||
|
|
16d9657982 | ||
|
|
40d098310e | ||
|
|
1345449d57 | ||
|
|
515858f313 | ||
|
|
2f452e9dc7 | ||
|
|
66119157a7 | ||
|
|
5b671dd1d0 | ||
|
|
1017362eec | ||
|
|
f67b8e0285 | ||
|
|
4c635fe84c | ||
|
|
68e463493e | ||
|
|
f1979d60b7 | ||
|
|
9150ebafec | ||
|
|
9543019336 | ||
|
|
1c53d91c6b | ||
|
|
4850f39b5a | ||
|
|
ab085c2d92 | ||
|
|
bf4d835948 | ||
|
|
214e39537b | ||
|
|
2d33afc195 | ||
|
|
87ea24ebd4 | ||
|
|
5380f8b9b3 | ||
|
|
00121ff8ba | ||
|
|
80e5d20e37 | ||
|
|
58f7c2137d | ||
|
|
f9194cc833 | ||
|
|
d9b8b48972 | ||
|
|
aa85f5f596 | ||
|
|
5341a0be4a | ||
|
|
0cfe20ca65 | ||
|
|
c352b502c4 | ||
|
|
58b4df6b3d | ||
|
|
29ba9436c8 | ||
|
|
2a044e88ad | ||
|
|
63092f9d72 | ||
|
|
0209324d57 | ||
|
|
1587273868 | ||
|
|
beb3aa1574 | ||
|
|
fe708c9fb4 | ||
|
|
e45d8bf973 | ||
|
|
b184c92f01 | ||
|
|
6c8afb05a7 | ||
|
|
d3dd4573cf | ||
|
|
d5cf68391a | ||
|
|
4e54e93450 | ||
|
|
e4f6387f18 | ||
|
|
54cb35b60a | ||
|
|
18ede2b729 | ||
|
|
f138b5a4f4 | ||
|
|
11a517bba4 | ||
|
|
66b57bf812 | ||
|
|
a9357bd97e | ||
|
|
d7c6d42c3d | ||
|
|
4dd1dc28b1 | ||
|
|
1e05ff7c95 | ||
|
|
e8e2e65584 | ||
|
|
3727e60152 | ||
|
|
0254012db6 | ||
|
|
8b97e4757f | ||
|
|
c70e121078 | ||
|
|
a4f97e6e46 | ||
|
|
800145a83c | ||
|
|
c75f885cb4 | ||
|
|
4011a51013 | ||
|
|
f4165dabcf | ||
|
|
de6c26eb05 | ||
|
|
5f319452d5 | ||
|
|
60d505d2d1 | ||
|
|
f64cc4dcae | ||
|
|
60e6f4293a | ||
|
|
00ab9a8d02 | ||
|
|
7d5f6c9ead | ||
|
|
a295edf19d | ||
|
|
b674515d06 | ||
|
|
d033ab04da | ||
|
|
304d76d088 | ||
|
|
978afdad97 | ||
|
|
d4e41e679d | ||
|
|
146264ff12 | ||
|
|
dcb107ae65 | ||
|
|
c236269d13 | ||
|
|
8f658e6d85 | ||
|
|
d203b60f44 | ||
|
|
a1a16aba74 | ||
|
|
6ded003447 | ||
|
|
4841e29fc6 | ||
|
|
0b014eea56 | ||
|
|
1c0be16f30 | ||
|
|
27ba8bea2f | ||
|
|
c566977749 | ||
|
|
5c1b785b4b | ||
|
|
8657dfb5da | ||
|
|
dfa837754e | ||
|
|
0a7df78770 | ||
|
|
066ecbe022 | ||
|
|
6c80db810f | ||
|
|
6023c413ab | ||
|
|
7910d040b6 | ||
|
|
5bd99f5224 | ||
|
|
f3157b377f | ||
|
|
e31e03afde | ||
|
|
eddde7c94c | ||
|
|
7be72ee4c1 | ||
|
|
6731467514 | ||
|
|
8dd699d235 | ||
|
|
6cb81b5c3d | ||
|
|
17187ba3ec | ||
|
|
531ee928b0 | ||
|
|
db806a5df9 | ||
|
|
9de154595a | ||
|
|
9e4cb79679 | ||
|
|
b7834073b8 | ||
|
|
b0e56577b5 | ||
|
|
ccb0e6b269 | ||
|
|
edfd4baa1f | ||
|
|
0f50f4a9fd | ||
|
|
47494e62a7 | ||
|
|
7aa25712d9 | ||
|
|
1db155570d | ||
|
|
1054e8e644 | ||
|
|
aa429f34d8 | ||
|
|
e351889811 | ||
|
|
24a70a8273 | ||
|
|
3f26657116 | ||
|
|
d41669af8b | ||
|
|
56466c2a00 | ||
|
|
fa7a97ca30 | ||
|
|
8aba271a42 | ||
|
|
8275aa2810 | ||
|
|
410ddf314c | ||
|
|
fa217bee20 | ||
|
|
817d0edc69 | ||
|
|
513dfe0b42 | ||
|
|
bd7a20309b | ||
|
|
a726be3c7c | ||
|
|
10f2054e9a | ||
|
|
2fa47f310d | ||
|
|
5b927a70c2 | ||
|
|
2a59ff8e68 | ||
|
|
e4d1befcdb | ||
|
|
844e04ff96 | ||
|
|
cc05a98b0e | ||
|
|
006d161a32 | ||
|
|
a4839db79a | ||
|
|
87e3b5b1dc | ||
|
|
faa900d502 | ||
|
|
a5275db3ec | ||
|
|
77e017a574 | ||
|
|
8ed8ddbf76 | ||
|
|
eb31978488 | ||
|
|
677d708588 | ||
|
|
ade0dca8f9 | ||
|
|
8e1cd0b268 | ||
|
|
9102768366 | ||
|
|
0c722b9164 | ||
|
|
b6f514451a | ||
|
|
c49fdfc56c | ||
|
|
676e04b28e | ||
|
|
6aa864a351 | ||
|
|
72acb4826c | ||
|
|
734be5f355 | ||
|
|
cede06ae19 | ||
|
|
19491d8010 | ||
|
|
c580aac991 | ||
|
|
032d1aaad7 | ||
|
|
afa216dc5e | ||
|
|
69339fe3de | ||
|
|
571bb2b294 | ||
|
|
91a09a09f7 | ||
|
|
9b3433f6ae | ||
|
|
ee9b0960f7 | ||
|
|
506ac2574f | ||
|
|
dc84d7c1b5 | ||
|
|
fcaa57307f | ||
|
|
d25e754beb | ||
|
|
7f6f411ea8 | ||
|
|
96a73e31f3 | ||
|
|
c7942d7d8f | ||
|
|
479348eec9 | ||
|
|
ebfed27630 | ||
|
|
8923485169 | ||
|
|
d62de26683 | ||
|
|
1dd9c5b009 |
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -11,7 +11,9 @@ body:
|
|||||||
|
|
||||||
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
||||||
|
|
||||||
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists
|
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists.
|
||||||
|
|
||||||
|
Finally, if you are opening **a bug report related to PyScript.com** please [use this repository instead](https://github.com/anaconda/pyscript-dot-com-issues/issues/new/choose).
|
||||||
|
|
||||||
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
|
|||||||
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Question
|
- name: Feature Proposals
|
||||||
url: https://community.anaconda.cloud/c/tech-topics/pyscript
|
url: https://github.com/pyscript/pyscript/discussions/new?category=proposals
|
||||||
|
about: Create a feature request to make PyScript even better
|
||||||
|
- name: Questions
|
||||||
|
url: https://github.com/pyscript/pyscript/discussions/new?category=q-a
|
||||||
about: For questions or discussions about pyscript
|
about: For questions or discussions about pyscript
|
||||||
|
|||||||
67
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
67
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: Feature Request
|
|
||||||
description: Create a feature request to make PyScript even better
|
|
||||||
labels: ["type: enhancement", "needs-triage"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
### Thanks for helping PyScript! 🐍
|
|
||||||
|
|
||||||
Going through feature requests and issues takes up a lot of time, so please be so kind and take a few minutes to fill out all the areas to the best of your ability.
|
|
||||||
|
|
||||||
There will always be more great ideas than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so everyone can focus our time on helping people like you who fill out the form completely. Thank you for your collaboration!
|
|
||||||
|
|
||||||
There are also already a lot of open requests, so please take 2 minutes and search through existing ones to see if your idea already exists. If you find something close, please upvote that request and comment.
|
|
||||||
|
|
||||||
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
|
||||||
|
|
||||||
### Lets make sure you are in the right place. If you have an idea/request for:
|
|
||||||
|
|
||||||
- #### A specific package/library (such as pandas or scikit learn):
|
|
||||||
|
|
||||||
Search for that respective library on github repo or website. You will have much more success there.
|
|
||||||
|
|
||||||
- #### A general Python question/feature request:
|
|
||||||
|
|
||||||
Try out a forum post [here](https://discuss.python.org/c/users/7)
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: checks
|
|
||||||
attributes:
|
|
||||||
label: Checklist
|
|
||||||
description: Please confirm and check all the following options
|
|
||||||
options:
|
|
||||||
- label: I added a descriptive title
|
|
||||||
required: true
|
|
||||||
- label: I searched for other feature requests and couldn't find a duplicate (including also the ``type-feature`` tag)
|
|
||||||
required: true
|
|
||||||
- label: I confirmed that it's not related to another project are area (see the above section)
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: request-idea
|
|
||||||
attributes:
|
|
||||||
label: What is the idea?
|
|
||||||
description: Describe what the feature is and the desired state
|
|
||||||
placeholder: This feature would allow any user of PyScript to type in a simple command in the console and show all variables currently in use
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: why
|
|
||||||
attributes:
|
|
||||||
label: Why is this needed
|
|
||||||
description: |
|
|
||||||
Who would benefit from this and why would this add value to them? What problem does this solve?
|
|
||||||
placeholder: This would benefit users who would like to see what is being used so they can learn and debug faster
|
|
||||||
- type: textarea
|
|
||||||
id: what
|
|
||||||
attributes:
|
|
||||||
label: What should happen?
|
|
||||||
description: |
|
|
||||||
What should be the user experience with the feature? Describe from a user perpective what they would do and see
|
|
||||||
placeholder: A user would type in ``PyScript debug`` in the browser console and see a list of all variables created.
|
|
||||||
- type: textarea
|
|
||||||
id: context
|
|
||||||
attributes:
|
|
||||||
label: Additional Context
|
|
||||||
description: |
|
|
||||||
Is there any other information that you think would be valuable for the team to know?
|
|
||||||
37
.github/ISSUE_TEMPLATE/misc.yml
vendored
37
.github/ISSUE_TEMPLATE/misc.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: Miscellaneous
|
|
||||||
description: For issues that don't belong in other categories
|
|
||||||
labels: ["type: misc", "needs-triage"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for helping PyScript! 🐍
|
|
||||||
|
|
||||||
This issue is for things that doesn't make sense to put into the other issue categories and we don't want it to get lost.
|
|
||||||
|
|
||||||
Going through issues takes up a lot of time, so please be so kind and take a few minutes to fill out all the areas to the best of your ability.
|
|
||||||
|
|
||||||
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
|
||||||
|
|
||||||
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists
|
|
||||||
|
|
||||||
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
|
||||||
- type: checkboxes
|
|
||||||
id: checks
|
|
||||||
attributes:
|
|
||||||
label: Checklist
|
|
||||||
description: Please confirm and check all the following options.
|
|
||||||
options:
|
|
||||||
- label: I added a descriptive title
|
|
||||||
required: true
|
|
||||||
- label: I searched for other issues and couldn't find a duplication
|
|
||||||
required: true
|
|
||||||
- label: I already searched in Google and didn't find any good information or help
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: what
|
|
||||||
attributes:
|
|
||||||
label: What is the issue/comment/problem?
|
|
||||||
description: This is a miscellaneous issue so this could be just about anything. We simply ask that you provide as many details as you can to help spur discussion or the outcome you want.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
<!--Please describe the changes in your pull request in few words here. -->
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
<!-- List the technical changes done to fix a bug or introduce a new feature. -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have checked `make build` works locally.
|
||||||
|
- [ ] I have created / updated documentation for this change (if applicable).
|
||||||
18
.github/dashboard.yaml
vendored
18
.github/dashboard.yaml
vendored
@@ -1,18 +0,0 @@
|
|||||||
name: Push issue to Github Project dashboard
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
add_to_project:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/add-to-project@v0.0.3
|
|
||||||
with:
|
|
||||||
project-url: https://github.com/orgs/pyscript/projects/4/
|
|
||||||
github-token: ${{ secrets.PROJECT_TOKEN }}
|
|
||||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*" # Group all Actions updates into a single larger pull request
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
74
.github/workflows/build-latest.yml
vendored
74
.github/workflows/build-latest.yml
vendored
@@ -1,74 +0,0 @@
|
|||||||
name: '[CI] Build Latest'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push: # Only run on merges into main that modify files under pyscriptjs/
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- pyscriptjs/**
|
|
||||||
- .github/workflows/build-latest.yml # Test that workflow works when changed
|
|
||||||
|
|
||||||
pull_request: # Run on any PR that modifies files in pyscriptjs/
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- pyscriptjs/**
|
|
||||||
- .github/workflows/build-latest.yml # Test that workflow works when changed
|
|
||||||
|
|
||||||
env:
|
|
||||||
MINICONDA_PYTHON_VERSION: py38
|
|
||||||
MINICONDA_VERSION: 4.11.0
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: pyscriptjs
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18.x
|
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: cache-node-modules
|
|
||||||
with:
|
|
||||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: setup Miniconda
|
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
|
||||||
|
|
||||||
- name: Setup Environment
|
|
||||||
run: make setup
|
|
||||||
|
|
||||||
- name: Build and Test
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
if: github.ref == 'refs/heads/main' # Only deploy on merge into main
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
|
|
||||||
- name: Sync to S3
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
run: aws s3 sync --quiet ./examples/build/ s3://pyscript.net/latest/
|
|
||||||
56
.github/workflows/build-release.yml
vendored
56
.github/workflows/build-release.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: '[CI] Build Release'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9]+' # YYYY.MM.MICRO
|
|
||||||
|
|
||||||
env:
|
|
||||||
MINICONDA_PYTHON_VERSION: py38
|
|
||||||
MINICONDA_VERSION: 4.11.0
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: pyscriptjs
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18.x
|
|
||||||
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: cache-node-modules
|
|
||||||
with:
|
|
||||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
|
|
||||||
- name: setup Miniconda
|
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
|
||||||
|
|
||||||
- name: Setup Environment
|
|
||||||
run: make setup
|
|
||||||
|
|
||||||
- name: Build and Test
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
- name: Prepare Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
draft: true
|
|
||||||
prerelease: true
|
|
||||||
generate_release_notes: true
|
|
||||||
60
.github/workflows/docs-latest.yml
vendored
60
.github/workflows/docs-latest.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: '[Docs] Build Latest'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- docs/**
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
env:
|
|
||||||
SPHINX_HTML_BASE_URL: https://docs.pyscript.net/
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
|
|
||||||
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
|
|
||||||
|
|
||||||
- name: Setup
|
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
|
||||||
with:
|
|
||||||
auto-update-conda: true
|
|
||||||
activate-environment: docs
|
|
||||||
environment-file: docs/environment.yml
|
|
||||||
python-version: '3.9'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
shell: bash -l {0}
|
|
||||||
run: |
|
|
||||||
cd docs/
|
|
||||||
make html
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: pyscript-docs-latest
|
|
||||||
path: docs/_build/html/
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
|
|
||||||
- name: Copy redirect file
|
|
||||||
run: aws s3 cp --quiet ./docs/_build/html/_static/redirect.html s3://docs.pyscript.net/index.html
|
|
||||||
|
|
||||||
# - name: Delete latest directory
|
|
||||||
# run: aws s3 rm --recursive s3://docs.pyscript.net/latest/
|
|
||||||
|
|
||||||
- name: Sync to S3
|
|
||||||
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/latest/
|
|
||||||
57
.github/workflows/docs-release.yml
vendored
57
.github/workflows/docs-release.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: '[Docs] Build Release'
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
env:
|
|
||||||
SPHINX_HTML_BASE_URL: https://docs.pyscript.net/
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
|
|
||||||
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
|
|
||||||
|
|
||||||
- name: Setup
|
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
|
||||||
with:
|
|
||||||
auto-update-conda: true
|
|
||||||
activate-environment: docs
|
|
||||||
environment-file: docs/environment.yml
|
|
||||||
python-version: '3.9'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
shell: bash -l {0}
|
|
||||||
run: |
|
|
||||||
cd docs/
|
|
||||||
make html
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: pyscript-docs-${{ github.ref_name }}
|
|
||||||
path: docs/_build/html/
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
|
|
||||||
- name: Copy redirect file
|
|
||||||
run: aws s3 cp --quiet ./docs/_build/html/_static/redirect.html s3://docs.pyscript.net/index.html
|
|
||||||
|
|
||||||
# - name: Delete release directory
|
|
||||||
# run: aws s3 rm --recursive s3://docs.pyscript.net/${{ github.ref_name }}/
|
|
||||||
|
|
||||||
- name: Sync to S3
|
|
||||||
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/${{ github.ref_name }}/
|
|
||||||
78
.github/workflows/docs-review.yml
vendored
78
.github/workflows/docs-review.yml
vendored
@@ -1,78 +0,0 @@
|
|||||||
name: '[Docs] Build Review'
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- '*'
|
|
||||||
paths:
|
|
||||||
- docs/**
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
# Concurrency group that uses the workflow name and PR number if available
|
|
||||||
# or commit SHA as a fallback. If a new build is triggered under that
|
|
||||||
# concurrency group while a previous build is running it will be canceled.
|
|
||||||
# Repeated pushes to a PR will cancel all previous builds, while multiple
|
|
||||||
# merges to main will not cancel.
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
if: >-
|
|
||||||
!github.event.repository.fork
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
env:
|
|
||||||
SPHINX_HTML_BASE_URL: https://docs.pyscript.net/
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal access token.
|
|
||||||
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
|
|
||||||
|
|
||||||
- name: Setup
|
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
|
||||||
with:
|
|
||||||
auto-update-conda: true
|
|
||||||
activate-environment: docs
|
|
||||||
environment-file: docs/environment.yml
|
|
||||||
python-version: '3.9'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
shell: bash -l {0}
|
|
||||||
run: |
|
|
||||||
cd docs/
|
|
||||||
make html
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: pyscript-docs-review-${{ github.event.number }}
|
|
||||||
path: docs/_build/html/
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
|
|
||||||
- name: Copy redirect file
|
|
||||||
run: aws s3 cp --quiet ./docs/_build/html/_static/redirect.html s3://docs.pyscript.net/index.html
|
|
||||||
|
|
||||||
# - name: Delete review directory
|
|
||||||
# run: aws s3 rm --recursive s3://docs.pyscript.net/review/${{ github.event.number }}/
|
|
||||||
|
|
||||||
- name: Sync to S3
|
|
||||||
run: aws s3 sync --quiet ./docs/_build/html/ s3://docs.pyscript.net/review/${{ github.event.number }}/
|
|
||||||
|
|
||||||
- name: Adding step summary
|
|
||||||
run: |
|
|
||||||
echo "### Review documentation" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "As with any pull request, you can find the rendered documentation version for pull request ${{ github.event.number }} here:"
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY # this is a blank line
|
|
||||||
echo "https://docs.pyscript.net/review/${{ github.event.number }}/" >> $GITHUB_STEP_SUMMARY
|
|
||||||
71
.github/workflows/prepare-release.yml
vendored
Normal file
71
.github/workflows/prepare-release.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: "Prepare Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9]+" # YYYY.MM.MICRO
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./core
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: NPM Install
|
||||||
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Generate index.html
|
||||||
|
working-directory: .
|
||||||
|
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
|
- name: Zip dist folder
|
||||||
|
run: zip -r -q ./build.zip ./dist
|
||||||
|
|
||||||
|
- name: Prepare Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: true
|
||||||
|
files: ./build.zip
|
||||||
61
.github/workflows/publish-release.yml
vendored
61
.github/workflows/publish-release.yml
vendored
@@ -1,34 +1,45 @@
|
|||||||
name: '[CI] Publish Release'
|
name: "Publish Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
env:
|
|
||||||
MINICONDA_PYTHON_VERSION: py38
|
|
||||||
MINICONDA_VERSION: 4.11.0
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: pyscriptjs
|
working-directory: ./core
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
publish-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
|
||||||
id-token: write
|
id-token: write
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install node
|
- name: Install node
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
env:
|
env:
|
||||||
cache-name: cache-node-modules
|
cache-name: cache-node-modules
|
||||||
with:
|
with:
|
||||||
@@ -40,22 +51,28 @@ jobs:
|
|||||||
${{ runner.os }}-build-
|
${{ runner.os }}-build-
|
||||||
${{ runner.os }}-
|
${{ runner.os }}-
|
||||||
|
|
||||||
- name: setup Miniconda
|
- name: npm install
|
||||||
uses: conda-incubator/setup-miniconda@v2
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
- name: Setup Environment
|
- name: build
|
||||||
run: make setup
|
run: npm run build
|
||||||
|
|
||||||
- name: Build and Test
|
- name: Generate index.html in snapshot
|
||||||
run: make test
|
working-directory: .
|
||||||
|
run: sed -e 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' -e 's#_DOC_VERSION_#${{ github.ref_name }}#' -e 's#_TAG_VERSION_#/tag/${{ github.ref_name }}#' -e 's#_VERSION_#${{ github.ref_name }}#' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
|
- name: Generate release.tar from snapshot and put it in dist/
|
||||||
|
working-directory: .
|
||||||
|
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||||
|
|
||||||
# Upload to S3
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
with:
|
with:
|
||||||
aws-region: ${{ secrets.AWS_REGION }}
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
- name: Sync to S3
|
- name: Sync to S3
|
||||||
run: | # Overwrite "latest" alpha + versioned subdirectory
|
run:
|
||||||
aws s3 sync --quiet ./examples/build/ s3://pyscript.net/releases/${{ github.ref_name }}
|
| # Update /latest and create an explicitly versioned directory under releases/YYYY.MM.MICRO/
|
||||||
|
aws s3 sync --quiet ./dist/ s3://pyscript.net/latest/
|
||||||
|
aws s3 sync --quiet ./dist/ s3://pyscript.net/releases/${{ github.ref_name }}/
|
||||||
|
|||||||
76
.github/workflows/publish-snapshot.yml
vendored
Normal file
76
.github/workflows/publish-snapshot.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: "Publish Snapshot"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
snapshot_version:
|
||||||
|
description: "The calver version of this snapshot: 2022.09.1 or 2022.09.1.RC1"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./core
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-snapshot:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
|
- name: Build Pyscript.core
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
|
- name: Generate index.html in snapshot
|
||||||
|
working-directory: .
|
||||||
|
run: sed -e 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' -e 's#_DOC_VERSION_#${{ inputs.snapshot_version }}#' -e 's#_TAG_VERSION_#/tag/${{ inputs.snapshot_version }}#' -e 's#_VERSION_#${{ inputs.snapshot_version }}#' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
|
- name: Copy to Snapshot
|
||||||
|
run: >
|
||||||
|
aws s3 sync ./dist/ s3://pyscript.net/snapshots/${{ inputs.snapshot_version }}/
|
||||||
76
.github/workflows/publish-unstable.yml
vendored
Normal file
76
.github/workflows/publish-unstable.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: "Publish Unstable"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push: # Only run on merges into main that modify files under core/ and examples/
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- core/**
|
||||||
|
- examples/**
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-unstable:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./core
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Python venv
|
||||||
|
run: python -m venv env
|
||||||
|
|
||||||
|
- name: Activate Python
|
||||||
|
run: source env/bin/activate
|
||||||
|
|
||||||
|
- name: Update pip
|
||||||
|
run: pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Install PyMinifier
|
||||||
|
run: pip install --ignore-requires-python python-minifier
|
||||||
|
|
||||||
|
- name: Install Setuptools
|
||||||
|
run: pip install setuptools
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: NPM Install
|
||||||
|
run: npm install && npx playwright install chromium
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Generate index.html in snapshot
|
||||||
|
working-directory: .
|
||||||
|
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
|
- name: Sync to S3
|
||||||
|
run: aws s3 sync --quiet ./dist/ s3://pyscript.net/unstable/
|
||||||
33
.github/workflows/sync-examples.yml
vendored
33
.github/workflows/sync-examples.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: '[CI] Sync Examples'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push: # Only run on merges into main that modify files under examples/
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- examples/**
|
|
||||||
- .github/workflows/sync-examples.yml # Test that workflow works when changed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: examples
|
|
||||||
|
|
||||||
steps:
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
- name: Sync to S3
|
|
||||||
# Sync outdated or new files, delete ones no longer in source
|
|
||||||
run: aws s3 sync --quiet --delete . s3://pyscript.net/examples/ # Sync directory, delete what is not in source
|
|
||||||
80
.github/workflows/test.yml
vendored
Normal file
80
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: "[CI] Test"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push: # Only run on merges into main that modify certain files
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- core/**
|
||||||
|
- .github/workflows/test.yml
|
||||||
|
|
||||||
|
pull_request: # Only run on merges into main that modify certain files
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- core/**
|
||||||
|
- .github/workflows/test.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
BuildAndTest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
MINICONDA_PYTHON_VERSION: py38
|
||||||
|
MINICONDA_VERSION: 4.11.0
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 3
|
||||||
|
|
||||||
|
# display a git log: when you run CI on PRs, github automatically
|
||||||
|
# merges the PR into main and run the CI on that commit. The idea
|
||||||
|
# here is to show enough of git log to understand what is the
|
||||||
|
# actual commit (in the PR) that we are using. See also
|
||||||
|
# 'fetch-depth: 3' above.
|
||||||
|
- name: git log
|
||||||
|
run: git log --graph -3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: setup Miniconda
|
||||||
|
uses: conda-incubator/setup-miniconda@v3
|
||||||
|
|
||||||
|
- name: Create and activate virtual environment
|
||||||
|
run: |
|
||||||
|
python3 -m venv test_venv
|
||||||
|
source test_venv/bin/activate
|
||||||
|
echo PATH=$PATH >> $GITHUB_ENV
|
||||||
|
echo VIRTUAL_ENV=$VIRTUAL_ENV >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup dependencies in virtual environment
|
||||||
|
run: |
|
||||||
|
make setup
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: make build # Integration tests run in the build step.
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: pyscript
|
||||||
|
path: |
|
||||||
|
core/dist/
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -51,7 +51,6 @@ coverage.xml
|
|||||||
*.py,cover
|
*.py,cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
pyscriptjs/examples
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
@@ -136,3 +135,18 @@ dmypy.json
|
|||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# junit xml for test results
|
||||||
|
test_results
|
||||||
|
|
||||||
|
# @pyscript/core npm artifacts
|
||||||
|
core/test-results/*
|
||||||
|
core/core.*
|
||||||
|
core/dist
|
||||||
|
core/dist.zip
|
||||||
|
core/src/plugins.js
|
||||||
|
core/src/stdlib/pyscript.js
|
||||||
|
core/src/3rd-party/*
|
||||||
|
!core/src/3rd-party/READMEmd
|
||||||
|
|||||||
@@ -1,89 +1,53 @@
|
|||||||
# This is the configuration for pre-commit, a local framework for managing pre-commit hooks
|
# This is the configuration for pre-commit, a local framework for managing pre-commit hooks
|
||||||
# Check out the docs at: https://pre-commit.com/
|
# Check out the docs at: https://pre-commit.com/
|
||||||
|
ci:
|
||||||
|
#skip: [eslint]
|
||||||
|
autoupdate_schedule: monthly
|
||||||
|
|
||||||
default_stages: [commit]
|
default_stages: [pre-commit]
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.3.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-docstring-first
|
- id: check-docstring-first
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
- id: check-json
|
- id: check-json
|
||||||
exclude: tsconfig.json
|
exclude: tsconfig\.json
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
|
exclude: bad\.toml
|
||||||
- id: check-xml
|
- id: check-xml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: \.min\.js$
|
exclude: core/dist|\.min\.js$
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
|
||||||
rev: 1.7.4
|
|
||||||
hooks:
|
|
||||||
- id: bandit
|
|
||||||
args:
|
|
||||||
- --skip=B201
|
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.3.0
|
rev: 25.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
exclude: core/tests
|
||||||
|
args: ["-l", "88", "--skip-string-normalization"]
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.1.0
|
rev: v2.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell # See 'setup.cfg' for args
|
- id: codespell # See 'pyproject.toml' for args
|
||||||
|
exclude: fs\.py|\.js\.map$
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 4.0.1
|
|
||||||
hooks:
|
|
||||||
- id: flake8 # See 'setup.cfg' for args
|
|
||||||
additional_dependencies: [flake8-bugbear, flake8-comprehensions]
|
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
rev: 5.10.1
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
name: isort (python)
|
|
||||||
args: [--profile, black]
|
|
||||||
|
|
||||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
|
||||||
rev: v2.3.0
|
|
||||||
hooks:
|
|
||||||
- id: pretty-format-yaml
|
|
||||||
args: [--autofix, --indent, '4']
|
|
||||||
exclude: .github/ISSUE_TEMPLATE/.*\.yml$
|
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
|
||||||
rev: v2.34.0
|
|
||||||
hooks:
|
|
||||||
- id: pyupgrade
|
|
||||||
args:
|
|
||||||
- --py310-plus
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
|
||||||
rev: v8.18.0
|
|
||||||
hooks:
|
|
||||||
- id: eslint
|
|
||||||
files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
|
|
||||||
types: [file]
|
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- eslint
|
- tomli
|
||||||
- eslint-plugin-svelte3
|
|
||||||
- typescript
|
|
||||||
- '@typescript-eslint/eslint-plugin'
|
|
||||||
- '@typescript-eslint/parser'
|
|
||||||
|
|
||||||
# Commented out until mdformat-myst supports custom extensions
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# See https://github.com/executablebooks/mdformat-myst/pull/9
|
rev: v0.11.8
|
||||||
# - repo: https://github.com/executablebooks/mdformat
|
hooks:
|
||||||
# rev: 0.7.14 # Use the ref you want to point at
|
- id: ruff
|
||||||
# hooks:
|
exclude: core/tests
|
||||||
# - id: mdformat
|
|
||||||
# additional_dependencies:
|
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
||||||
# - mdformat-gfm
|
rev: "v3.0.0-alpha.6"
|
||||||
# - mdformat-myst
|
hooks:
|
||||||
# - mdformat-black
|
- id: prettier
|
||||||
|
exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
|
||||||
|
args: [--tab-width, "4"]
|
||||||
|
|||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
ISSUE_TEMPLATE
|
||||||
|
*.min.*
|
||||||
|
package-lock.json
|
||||||
|
bridge/
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# .readthedocs.yaml
|
|
||||||
# Read the Docs configuration file
|
|
||||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
||||||
|
|
||||||
# Required
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
# Set the version of Python and other tools you might need
|
|
||||||
build:
|
|
||||||
os: ubuntu-20.04
|
|
||||||
tools:
|
|
||||||
python: miniconda3-4.7
|
|
||||||
|
|
||||||
# Build documentation in the docs/ directory with Sphinx
|
|
||||||
sphinx:
|
|
||||||
configuration: docs/conf.py
|
|
||||||
|
|
||||||
conda:
|
|
||||||
environment: docs/environment.yml
|
|
||||||
|
|
||||||
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
|
||||||
# formats:
|
|
||||||
# - pdf
|
|
||||||
|
|
||||||
# Optionally declare the Python requirements required to build your docs
|
|
||||||
python:
|
|
||||||
install:
|
|
||||||
- requirements: docs/requirements.txt
|
|
||||||
@@ -1,98 +1,4 @@
|
|||||||
# Contributing to PyScript
|
# Contributing to PyScript
|
||||||
|
|
||||||
Thank you for wanting to contribute to the PyScript project!
|
Please see our guide to contributing to PyScript
|
||||||
|
[in our documentation](https://docs.pyscript.net/latest/contributing/).
|
||||||
## Table of contents
|
|
||||||
|
|
||||||
* [Code of Conduct](#code-of-conduct)
|
|
||||||
* [Contributing](#contributing)
|
|
||||||
* [Reporting bugs](#reporting-bugs)
|
|
||||||
* [Reporting security issues](#reporting-security-issues)
|
|
||||||
* [Asking questions](#asking-questions)
|
|
||||||
* [Setting up your environment](#setting-up-your-environment)
|
|
||||||
* [Places to start](#places-to-start)
|
|
||||||
* [Submitting a change](#submitting-a-change)
|
|
||||||
* [License terms for contributions](#license-terms-for-contributions)
|
|
||||||
* [Becoming a maintainer](#becoming-a-maintainer)
|
|
||||||
* [Trademarks](#trademarks)
|
|
||||||
|
|
||||||
## Code of Conduct
|
|
||||||
|
|
||||||
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
### Reporting bugs
|
|
||||||
|
|
||||||
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
|
|
||||||
|
|
||||||
#### Creating useful issues
|
|
||||||
|
|
||||||
* Use a clear and descriptive title.
|
|
||||||
* Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
|
|
||||||
* Describe the behavior you observed, and the behavior you had expected.
|
|
||||||
* Include screenshots if they help make the issue clear.
|
|
||||||
|
|
||||||
### Reporting security issues
|
|
||||||
|
|
||||||
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
|
|
||||||
|
|
||||||
### Asking questions
|
|
||||||
|
|
||||||
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
|
|
||||||
|
|
||||||
### Setting up your environment
|
|
||||||
|
|
||||||
* clone the repo
|
|
||||||
```
|
|
||||||
git clone https://github.com/pyscript/pyscript
|
|
||||||
```
|
|
||||||
* cd into the main project folder
|
|
||||||
```
|
|
||||||
cd pyscript/pyscriptjs
|
|
||||||
```
|
|
||||||
* install the dependencies with npm install - make sure to use nodejs version >= 16
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
* run npm run dev to build and run the dev server. This will also watch for changes and rebuild when a file is saved.
|
|
||||||
```
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Places to start
|
|
||||||
|
|
||||||
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions.
|
|
||||||
|
|
||||||
* **Read over the existing documentation.** Are there things missing, or could they be clearer? Make some changes/additions to those documents.
|
|
||||||
* **Review the open issues.** Are they clear? Can you reproduce them? You can add comments, clarifications, or additions to those issues. If you think you have an idea of how to address the issue, submit a fix!
|
|
||||||
* **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
|
|
||||||
* **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
|
|
||||||
|
|
||||||
### Submitting a change
|
|
||||||
|
|
||||||
All contributions must be licensed Apache 2.0, and all files must have a copy of the boilerplate license comment (can be copied from an existing file).
|
|
||||||
|
|
||||||
To create a change for PyScript, you can follow the process described [here](https://docs.github.com/en/get-started/quickstart/contributing-to-projects).
|
|
||||||
|
|
||||||
* Fork a personal copy of the PyScript project.
|
|
||||||
* Make the changes you would like (don't forget to test them!)
|
|
||||||
* Please squash all commits for a change into a single commit (this can be done using "git rebase -i"). Do your best to have a well-formed commit message for the change.
|
|
||||||
* Open a pull request back to the PyScript project and address any comments/questions from the maintainers and other contributors.
|
|
||||||
|
|
||||||
## License terms for contributions
|
|
||||||
|
|
||||||
This Project welcomes contributions, suggestions, and feedback. All contributions, suggestions, and feedback you submitted are accepted under the [Apache 2.0](./LICENSE) license. You represent that if you do not own copyright in the code that you have the authority to submit it under the [Apache 2.0](./LICENSE) license. All feedback, suggestions, or contributions are not confidential.
|
|
||||||
|
|
||||||
|
|
||||||
## Becoming a maintainer
|
|
||||||
|
|
||||||
Contributors are invited to be maintainers of the project by demonstrating good decision making in their contributions, a commitment to the goals of the project, and consistent adherence to the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md). New maintainers are invited by a 3/4 vote of the existing maintainers.
|
|
||||||
|
|
||||||
## Trademarks
|
|
||||||
|
|
||||||
The Project abides by the Organization's [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
Part of MVG-0.1-beta.
|
|
||||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
|
||||||
|
|||||||
@@ -41,5 +41,6 @@ Any names, trademarks, logos, or goodwill developed by and associated with the P
|
|||||||
Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers, with approval by the Organization's Steering Committee.
|
Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers, with approval by the Organization's Steering Committee.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Part of MVG-0.1-beta.
|
Part of MVG-0.1-beta.
|
||||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|||||||
6
LICENSE
6
LICENSE
@@ -186,7 +186,11 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
Copyright (c) 2022-present, PyScript Development Team
|
||||||
|
|
||||||
|
Originated at Anaconda, Inc. in 2022
|
||||||
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -3,16 +3,22 @@
|
|||||||
This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](https://github.com/pyscript/pyscript/blob/main/GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies.
|
This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](https://github.com/pyscript/pyscript/blob/main/GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies.
|
||||||
|
|
||||||
| **NAME** | **Organization** |
|
| **NAME** | **Organization** |
|
||||||
| ---------------- | ---------------- |
|
| -------------------- | ---------------- |
|
||||||
| Fabio Pliger | Anaconda, Inc |
|
| Fabio Pliger | Anaconda, Inc |
|
||||||
| Antonio Cuni | Anaconda, Inc |
|
| Antonio Cuni | Anaconda, Inc |
|
||||||
| Philipp Rudiger | Anaconda, Inc |
|
| Philipp Rudiger | Anaconda, Inc |
|
||||||
| Peter Wang | Anaconda, Inc |
|
| Peter Wang | Anaconda, Inc |
|
||||||
| Kevin Goldsmith | Anaconda, Inc |
|
| Kevin Goldsmith | Anaconda, Inc |
|
||||||
| Mariana Meireles | Anaconda, Inc |
|
| Mariana Meireles | |
|
||||||
| --- | --- |
|
| Nicholas H.Tollervey | Anaconda, Inc |
|
||||||
|
| Madhur Tandon | Anaconda, Inc |
|
||||||
|
| Ted Patrick | Anaconda, Inc |
|
||||||
|
| Jeff Glass | |
|
||||||
|
| Paul Everitt | |
|
||||||
|
| Fabio Rosado | Anaconda, Inc |
|
||||||
|
| Andrea Giammarchi | Anaconda, Inc |
|
||||||
|
|
||||||
______________________________________________________________________
|
---
|
||||||
|
|
||||||
Part of MVG-0.1-beta.
|
Part of MVG-0.1-beta.
|
||||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|||||||
93
Makefile
Normal file
93
Makefile
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
MIN_NODE_VER := 20
|
||||||
|
MIN_NPM_VER := 6
|
||||||
|
MIN_PY3_VER := 8
|
||||||
|
NODE_VER := $(shell node -v | cut -d. -f1 | sed 's/^v\(.*\)/\1/')
|
||||||
|
NPM_VER := $(shell npm -v | cut -d. -f1)
|
||||||
|
PY3_VER := $(shell python3 -c "import sys;t='{v[1]}'.format(v=list(sys.version_info[:2]));print(t)")
|
||||||
|
PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
|
||||||
|
|
||||||
|
all:
|
||||||
|
@echo "\nThere is no default Makefile target right now. Try:\n"
|
||||||
|
@echo "make setup - check your environment and install the dependencies."
|
||||||
|
@echo "make update - update dependencies."
|
||||||
|
@echo "make clean - clean up auto-generated assets."
|
||||||
|
@echo "make build - build PyScript."
|
||||||
|
@echo "make precommit-check - run the precommit checks (run eslint)."
|
||||||
|
@echo "make test - run all automated tests in playwright."
|
||||||
|
@echo "make fmt - format the code."
|
||||||
|
@echo "make fmt-check - check the code formatting.\n"
|
||||||
|
|
||||||
|
.PHONY: check-node
|
||||||
|
check-node:
|
||||||
|
@if [ $(NODE_VER) -lt $(MIN_NODE_VER) ]; then \
|
||||||
|
echo "\033[0;31mBuild requires Node $(MIN_NODE_VER).x or higher: $(NODE_VER) detected.\033[0m"; \
|
||||||
|
false; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: check-npm
|
||||||
|
check-npm:
|
||||||
|
@if [ $(NPM_VER) -lt $(MIN_NPM_VER) ]; then \
|
||||||
|
echo "\033[0;31mBuild requires Node $(MIN_NPM_VER).x or higher: $(NPM_VER) detected.\033[0m"; \
|
||||||
|
false; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: check-python
|
||||||
|
check-python:
|
||||||
|
@if [ $(PY_OK) -eq 0 ]; then \
|
||||||
|
echo "\033[0;31mRequires Python 3.$(MIN_PY3_VER).x or higher: 3.$(PY3_VER) detected.\033[0m"; \
|
||||||
|
false; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check the environment, install the dependencies.
|
||||||
|
setup: check-node check-npm check-python
|
||||||
|
cd core && npm ci && cd ..
|
||||||
|
ifeq (,$(VIRTUAL_ENV)$(CONDA_PREFIX))
|
||||||
|
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv or conda env is not activated.\033[0m"
|
||||||
|
false
|
||||||
|
else
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Clean up generated assets.
|
||||||
|
clean:
|
||||||
|
find . -name \*.py[cod] -delete
|
||||||
|
rm -rf $(env) *.egg-info
|
||||||
|
rm -rf .pytest_cache .coverage coverage.xml
|
||||||
|
|
||||||
|
# Build PyScript.
|
||||||
|
build: precommit-check
|
||||||
|
cd core && npx playwright install chromium && npm run build
|
||||||
|
|
||||||
|
# Update the dependencies.
|
||||||
|
update:
|
||||||
|
python -m pip install -r requirements.txt --upgrade
|
||||||
|
|
||||||
|
# Run the precommit checks (run eslint).
|
||||||
|
precommit-check:
|
||||||
|
pre-commit run --all-files
|
||||||
|
|
||||||
|
# Run all automated tests in playwright.
|
||||||
|
test:
|
||||||
|
cd core && npm run test:integration
|
||||||
|
|
||||||
|
# Serve the repository with the correct headers.
|
||||||
|
serve:
|
||||||
|
npx mini-coi .
|
||||||
|
|
||||||
|
# Format the code.
|
||||||
|
fmt: fmt-py
|
||||||
|
@echo "Format completed"
|
||||||
|
|
||||||
|
# Check the code formatting.
|
||||||
|
fmt-check: fmt-py-check
|
||||||
|
@echo "Format check completed"
|
||||||
|
|
||||||
|
# Format Python code.
|
||||||
|
fmt-py:
|
||||||
|
black -l 88 --skip-string-normalization .
|
||||||
|
|
||||||
|
# Check the format of Python code.
|
||||||
|
fmt-py-check:
|
||||||
|
black -l 88 --check .
|
||||||
|
|
||||||
|
.PHONY: $(MAKECMDGOALS)
|
||||||
107
README.md
107
README.md
@@ -1,47 +1,94 @@
|
|||||||
# PyScript
|
# PyScript
|
||||||
|
|
||||||
## What is PyScript
|
## PyScript is an open source platform for Python in the browser.
|
||||||
|
|
||||||
### Summary
|
Using PyScript is as simple as:
|
||||||
|
|
||||||
PyScript is a Pythonic alternative to Scratch, JSFiddle, and other "easy to use" programming frameworks, with the goal of making the web a friendly, hackable place where anyone can author interesting and interactive applications.
|
|
||||||
|
|
||||||
To get started see the [getting started tutorial](docs/tutorials/getting-started.md).
|
|
||||||
|
|
||||||
For examples see [here](examples).
|
|
||||||
|
|
||||||
### Longer Version
|
|
||||||
PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers.
|
|
||||||
|
|
||||||
## Try PyScript
|
|
||||||
|
|
||||||
To try PyScript, import the appropriate pyscript files to your html page with:
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
|
<!doctype html>
|
||||||
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>PyScript!</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://pyscript.net/releases/2025.7.1/core.css"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="https://pyscript.net/releases/2025.7.1/core.js"
|
||||||
|
></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- type mpy (MicroPython) or py (Pyodide) to run some Python -->
|
||||||
|
<script type="mpy" terminal>
|
||||||
|
print("Hello, world!")
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
```
|
```
|
||||||
You can then use PyScript components in your html page. PyScript currently implements the following elements:
|
|
||||||
|
|
||||||
* `<py-script>`: can be used to define python code that is executable within the web page. The element itself is not rendered to the page and is only used to add logic
|
PyScript enables the creation of rich Python applications in the browser using
|
||||||
* `<py-repl>`: creates a REPL component that is rendered to the page as a code editor and allows users to write executable code
|
[Pyodide](https://pyodide.org/en/stable/) (a version of
|
||||||
|
[CPython](https://python.org/)), [MicroPython](https://micropython.org/),
|
||||||
|
[WASM](https://webassembly.org/), and modern web technologies. It means Python
|
||||||
|
now runs anywhere a browser runs: desktop, laptop, mobile, tablet, or any other
|
||||||
|
browser enabled device.
|
||||||
|
|
||||||
Check out the [the examples directory](examples) folder for more examples on how to use it, all you need to do is open them in Chrome.
|
To start building, read the
|
||||||
|
[Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
||||||
|
|
||||||
## How to Contribute
|
For example applications, see [here](https://pyscript.com/@examples).
|
||||||
|
|
||||||
Read the [contributing guide](CONTRIBUTING.md) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
Other useful resources:
|
||||||
|
|
||||||
## Resources
|
- Our [Home Page](https://pyscript.net/) as an open source project.
|
||||||
|
- The [official technical docs](https://docs.pyscript.net/).
|
||||||
|
- A [YouTube channel](https://www.youtube.com/@PyScriptTV) with helpful videos
|
||||||
|
and community content.
|
||||||
|
- A free-to-use [online IDE](https://pyscript.com/) for trying PyScript.
|
||||||
|
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in
|
||||||
|
touch .
|
||||||
|
|
||||||
* [Discussion board](https://community.anaconda.cloud/c/tech-topics/pyscript)
|
Every Tuesday at 15:30 UTC there is the _PyScript Community Call_ on zoom,
|
||||||
* [Home Page](https://pyscript.net/)
|
where we can talk about PyScript development in the open. Most of the
|
||||||
* [Blog Post](https://engineering.anaconda.com/2022/04/welcome-pyscript.html)
|
maintainers regularly participate in the call, and everybody is welcome to
|
||||||
|
join. This meeting is recorded and uploaded to our YouTube channel.
|
||||||
|
|
||||||
## Notes
|
Every other Thursday at 16:00 UTC there is the _PyScript FUN_ call: the focus
|
||||||
|
of this call is to share fun projects, goofy hacks or clever uses of PyScript.
|
||||||
|
It's a supportive, energetic and entertaining meeting. This meeting is also
|
||||||
|
recorded and uploaded to our YouTube channel.
|
||||||
|
|
||||||
* This is an extremely experimental project, so expect things to break!
|
For more details on how to join the calls and up to date schedule, consult the
|
||||||
* PyScript has been only tested on Chrome at the moment.
|
official calendar:
|
||||||
|
|
||||||
|
- [Google calendar](https://calendar.google.com/calendar/u/0/embed?src=d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0@group.calendar.google.com&ctz=UTC) in UTC time;
|
||||||
|
- [iCal format](https://calendar.google.com/calendar/ical/d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0%40group.calendar.google.com/public/basic.ics).
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
For technical details of the code, please see the [README](core/README.md) in
|
||||||
|
the `core` directory.
|
||||||
|
|
||||||
|
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
|
||||||
|
to learn about our development process, reporting bugs and improvements,
|
||||||
|
creating issues and asking questions.
|
||||||
|
|
||||||
|
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||||
|
documentation for more information on how to setup your development environment.
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|
||||||
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
The [PyScript organization governance](https://github.com/pyscript/governance)
|
||||||
|
is documented in a separate repository.
|
||||||
|
|
||||||
|
## Supporters
|
||||||
|
|
||||||
|
PyScript is an independent open source project.
|
||||||
|
|
||||||
|
However, PyScript was born at [Anaconda Inc](https://anaconda.com/) and its
|
||||||
|
core contributors are currently employed by Anaconda to work on PyScript. We
|
||||||
|
would like to acknowledge and celebrate Anaconda's continued support of this
|
||||||
|
project. Thank you [Anaconda Inc](https://anaconda.com/)!
|
||||||
|
|||||||
57
bridge/README.md
Normal file
57
bridge/README.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# @pyscript/bridge
|
||||||
|
|
||||||
|
Import Python utilities directly in JS
|
||||||
|
|
||||||
|
```js
|
||||||
|
// main thread
|
||||||
|
const { ffi: { func_a, func_b } } = await import('./test.js');
|
||||||
|
|
||||||
|
// test.js
|
||||||
|
import bridge from 'https://esm.run/@pyscript/bridge';
|
||||||
|
export const ffi = bridge(import.meta.url, { type: 'mpy', worker: false });
|
||||||
|
|
||||||
|
// test.py
|
||||||
|
def func_a(value):
|
||||||
|
print(f"hello {value}")
|
||||||
|
|
||||||
|
def func_b():
|
||||||
|
import sys
|
||||||
|
return sys.version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
* **type**: `py` by default to bootstrap *Pyodide*.
|
||||||
|
* **worker**: `true` by default to bootstrap in a *Web Worker*.
|
||||||
|
* **config**: either a *string* or a PyScript compatible config *JS literal* to make it possible to bootstrap files and whatnot. If specified, the `worker` becomes implicitly `true` to avoid multiple configs conflicting on the main thread.
|
||||||
|
* **env**: to share the same environment across multiple modules loaded at different times.
|
||||||
|
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Run `npx mini-coi .` within this folder to then reach out `http://localhost:8080/test/` that will show:
|
||||||
|
|
||||||
|
```
|
||||||
|
PyScript Bridge
|
||||||
|
------------------
|
||||||
|
no config
|
||||||
|
```
|
||||||
|
|
||||||
|
The [test.js](./test/test.js) files uses the following defaults:
|
||||||
|
|
||||||
|
* `type` as `"mpy"`
|
||||||
|
* `worker` as `false`
|
||||||
|
* `config` as `undefined`
|
||||||
|
* `env` as `undefined`
|
||||||
|
|
||||||
|
To test any variant use query string parameters so that `?type=py` will use `py` instead, `worker` will use a worker and `config` will use a basic *config* that brings in another file from the same folder which exposes the version.
|
||||||
|
|
||||||
|
To recap: `http://localhost:8080/test/?type=py&worker&config` will show this instead:
|
||||||
|
|
||||||
|
```
|
||||||
|
PyScript Bridge
|
||||||
|
------------------
|
||||||
|
3.12.7 (main, May 15 2025, 18:47:24) ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note when a *config* is used, the `worker` attribute is always `true`.
|
||||||
150
bridge/index.js
Normal file
150
bridge/index.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
/*! (c) PyScript Development Team */
|
||||||
|
|
||||||
|
const { stringify } = JSON;
|
||||||
|
const { create, entries } = Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a list of keys into a Python dictionary.
|
||||||
|
* `['a', 'b']` => `{ "a": a, "b": b }`
|
||||||
|
* @param {Iterable<string>} keys
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const dictionary = keys => {
|
||||||
|
const fields = [];
|
||||||
|
for (const key of keys)
|
||||||
|
fields.push(`${stringify(key)}: ${key}`);
|
||||||
|
return `{ ${fields.join(',')} }`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve properly config files relative URLs.
|
||||||
|
* @param {string|Object} config - The configuration to normalize.
|
||||||
|
* @param {string} base - The base URL to resolve relative URLs against.
|
||||||
|
* @returns {string} - The JSON serialized config.
|
||||||
|
*/
|
||||||
|
const normalize = async (config, base) => {
|
||||||
|
if (typeof config === 'string') {
|
||||||
|
base = config;
|
||||||
|
config = await fetch(config).then(res => res.json());
|
||||||
|
}
|
||||||
|
if (typeof config.files === 'object') {
|
||||||
|
const files = {};
|
||||||
|
for (const [key, value] of entries(config.files)) {
|
||||||
|
files[key.startsWith('{') ? key : new URL(key, base)] = value;
|
||||||
|
}
|
||||||
|
config.files = files;
|
||||||
|
}
|
||||||
|
return stringify(config);
|
||||||
|
};
|
||||||
|
|
||||||
|
// this logic is based on a 3 levels cache ...
|
||||||
|
const cache = new Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a bridge to a Python module via a `.js` file that has a `.py` alter ego.
|
||||||
|
* @param {string} url - The URL of the JS module that has a Python counterpart.
|
||||||
|
* @param {Object} options - The options for the bridge.
|
||||||
|
* @param {string} [options.type='py'] - The `py` or `mpy` interpreter type, `py` by default.
|
||||||
|
* @param {boolean} [options.worker=true] - Whether to use a worker, `true` by default.
|
||||||
|
* @param {string|Object} [options.config=null] - The configuration for the bridge, `null` by default.
|
||||||
|
* @param {string} [options.env=null] - The optional shared environment to use.
|
||||||
|
* @param {string} [options.serviceWorker=null] - The optional service worker to use as fallback.
|
||||||
|
* @returns {Object} - The bridge to the Python module.
|
||||||
|
*/
|
||||||
|
export default (url, {
|
||||||
|
type = 'py',
|
||||||
|
worker = true,
|
||||||
|
config = null,
|
||||||
|
env = null,
|
||||||
|
serviceWorker = null,
|
||||||
|
} = {}) => {
|
||||||
|
const { protocol, host, pathname } = new URL(url);
|
||||||
|
const py = pathname.replace(/\.m?js(?:\/\+\w+)?$/, '.py');
|
||||||
|
const file = `${protocol}//${host}${py}`;
|
||||||
|
|
||||||
|
// the first cache is about the desired file in the wild ...
|
||||||
|
if (!cache.has(file)) {
|
||||||
|
// the second cache is about all fields one needs to access out there
|
||||||
|
const exports = new Map;
|
||||||
|
let python;
|
||||||
|
|
||||||
|
cache.set(file, new Proxy(create(null), {
|
||||||
|
get(_, field) {
|
||||||
|
if (!exports.has(field)) {
|
||||||
|
// create an async callback once and always return the same later on
|
||||||
|
exports.set(field, async (...args) => {
|
||||||
|
// the third cache is about reaching lazily the code only once
|
||||||
|
// augmenting its content with exports once and drop it on done
|
||||||
|
if (!python) {
|
||||||
|
// do not await or multiple calls will fetch multiple times
|
||||||
|
// just assign the fetch `Promise` once and return it
|
||||||
|
python = fetch(file).then(async response => {
|
||||||
|
const code = await response.text();
|
||||||
|
// create a unique identifier for the Python context
|
||||||
|
const identifier = pathname.replace(/[^a-zA-Z0-9_]/g, '');
|
||||||
|
const name = `__pyscript_${identifier}${Date.now()}`;
|
||||||
|
// create a Python dictionary with all accessed fields
|
||||||
|
const detail = `{"detail":${dictionary(exports.keys())}}`;
|
||||||
|
// create the arguments for the `dispatchEvent` call
|
||||||
|
const eventArgs = `${stringify(name)},${name}to_ts(${detail})`;
|
||||||
|
// bootstrap the script element type and its attributes
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.type = type;
|
||||||
|
|
||||||
|
// if config is provided it needs to be a worker to avoid
|
||||||
|
// conflicting with main config on the main thread (just like always)
|
||||||
|
script.toggleAttribute('worker', !!config || !!worker);
|
||||||
|
if (config) {
|
||||||
|
const attribute = await normalize(config, file);
|
||||||
|
script.setAttribute('config', attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env) script.setAttribute('env', env);
|
||||||
|
if (serviceWorker) script.setAttribute('service-worker', serviceWorker);
|
||||||
|
|
||||||
|
// augment the code with the previously accessed fields at the end
|
||||||
|
script.textContent = [
|
||||||
|
'\n', code, '\n',
|
||||||
|
// this is to avoid local scope name clashing
|
||||||
|
`from pyscript import window as ${name}`,
|
||||||
|
`from pyscript.ffi import to_js as ${name}to_ts`,
|
||||||
|
`${name}.dispatchEvent(${name}.CustomEvent.new(${eventArgs}))`,
|
||||||
|
// remove these references even if non-clashing to keep
|
||||||
|
// the local scope clean from undesired entries
|
||||||
|
`del ${name}`,
|
||||||
|
`del ${name}to_ts`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// let PyScript resolve and execute this script
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
// intercept once the unique event identifier with all exports
|
||||||
|
globalThis.addEventListener(
|
||||||
|
name,
|
||||||
|
event => {
|
||||||
|
resolve(event.detail);
|
||||||
|
script.remove();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// return a promise that will resolve only once the event
|
||||||
|
// has been emitted and the interpreter evaluated the code
|
||||||
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the `Promise` that will after invoke the exported field
|
||||||
|
return python.then(foreign => foreign[field](...args));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the lazily to be resolved once callback to invoke
|
||||||
|
return exports.get(field);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache.get(file);
|
||||||
|
};
|
||||||
27
bridge/package.json
Normal file
27
bridge/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@pyscript/bridge",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "A JS based way to use PyScript modules",
|
||||||
|
"type": "module",
|
||||||
|
"module": "./index.js",
|
||||||
|
"unpkg": "./index.js",
|
||||||
|
"jsdelivr": "./jsdelivr.js",
|
||||||
|
"browser": "./index.js",
|
||||||
|
"main": "./index.js",
|
||||||
|
"keywords": [
|
||||||
|
"PyScript",
|
||||||
|
"JS",
|
||||||
|
"Python",
|
||||||
|
"bridge"
|
||||||
|
],
|
||||||
|
"author": "Anaconda Inc.",
|
||||||
|
"license": "APACHE-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/pyscript/pyscript.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/pyscript/pyscript/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/pyscript/pyscript#readme"
|
||||||
|
}
|
||||||
33
bridge/test/index.html
Normal file
33
bridge/test/index.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>PyScript Bridge</title>
|
||||||
|
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
|
||||||
|
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css" />
|
||||||
|
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
|
||||||
|
<!-- for local testing purpose only-->
|
||||||
|
<script type="importmap">{"imports":{"https://esm.run/@pyscript/bridge":"../index.js"}}</script>
|
||||||
|
<script type="module">
|
||||||
|
const { ffi: { test_func, test_other, version } } = await import('./test.js');
|
||||||
|
|
||||||
|
console.time("⏱️ first invoke");
|
||||||
|
const result = await test_func("PyScript Bridge");
|
||||||
|
console.timeEnd("⏱️ first invoke");
|
||||||
|
|
||||||
|
document.body.append(
|
||||||
|
Object.assign(
|
||||||
|
document.createElement("h3"),
|
||||||
|
{ textContent: result },
|
||||||
|
),
|
||||||
|
document.createElement("hr"),
|
||||||
|
await version(),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.time("⏱️ other invokes");
|
||||||
|
await test_other("🐍");
|
||||||
|
console.timeEnd("⏱️ other invokes");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
40
bridge/test/remote/index.html
Normal file
40
bridge/test/remote/index.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>PyScript Bridge</title>
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"https://esm.run/@pyscript/bridge": "https://esm.run/@pyscript/bridge@latest",
|
||||||
|
"https://esm.run/@pyscript/bridge/test/test.js": "https://esm.run/@pyscript/bridge@latest/test/test.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
|
||||||
|
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css" />
|
||||||
|
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
const cdn_test = 'https://esm.run/@pyscript/bridge/test/test.js';
|
||||||
|
const { ffi: { test_func, test_other, version } } = await import(cdn_test);
|
||||||
|
|
||||||
|
console.time("⏱️ first invoke");
|
||||||
|
const result = await test_func("PyScript Bridge");
|
||||||
|
console.timeEnd("⏱️ first invoke");
|
||||||
|
|
||||||
|
document.body.append(
|
||||||
|
Object.assign(
|
||||||
|
document.createElement("h3"),
|
||||||
|
{ textContent: result },
|
||||||
|
),
|
||||||
|
document.createElement("hr"),
|
||||||
|
await version(),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.time("⏱️ other invokes");
|
||||||
|
await test_other("🐍");
|
||||||
|
console.timeEnd("⏱️ other invokes");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
5
bridge/test/sys_version.py
Normal file
5
bridge/test/sys_version.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def version():
|
||||||
|
return sys.version
|
||||||
17
bridge/test/test.js
Normal file
17
bridge/test/test.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import bridge from "https://esm.run/@pyscript/bridge";
|
||||||
|
|
||||||
|
// for local testing purpose only
|
||||||
|
const { searchParams } = new URL(location.href);
|
||||||
|
|
||||||
|
// the named (or default) export for test.py
|
||||||
|
export const ffi = bridge(import.meta.url, {
|
||||||
|
env: searchParams.get("env"),
|
||||||
|
type: searchParams.get("type") || "mpy",
|
||||||
|
worker: searchParams.has("worker"),
|
||||||
|
config: searchParams.has("config") ?
|
||||||
|
({
|
||||||
|
files: {
|
||||||
|
"./sys_version.py": "./sys_version.py",
|
||||||
|
},
|
||||||
|
}) : undefined,
|
||||||
|
});
|
||||||
22
bridge/test/test.py
Normal file
22
bridge/test/test.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from pyscript import config, RUNNING_IN_WORKER
|
||||||
|
|
||||||
|
type = config["type"]
|
||||||
|
print(f"{type}-script", RUNNING_IN_WORKER and "worker" or "main")
|
||||||
|
|
||||||
|
|
||||||
|
def test_func(message):
|
||||||
|
print("Python", message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def test_other(message):
|
||||||
|
print("Python", message)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def version():
|
||||||
|
try:
|
||||||
|
from sys_version import version
|
||||||
|
except ImportError:
|
||||||
|
version = lambda: "no config"
|
||||||
|
return version()
|
||||||
205
core/LICENSE
Normal file
205
core/LICENSE
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
|
||||||
|
Copyright (c) 2022-present, PyScript Development Team
|
||||||
|
|
||||||
|
Originated at Anaconda, Inc. in 2022
|
||||||
|
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
168
core/README.md
Normal file
168
core/README.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# @pyscript/core
|
||||||
|
|
||||||
|
PyScript brings two Python interpreters to the browser:
|
||||||
|
|
||||||
|
- [MicroPython](https://micropython.org/) - a lean and efficient implementation
|
||||||
|
of the Python 3 programming language that includes a small subset of the
|
||||||
|
Python standard library and is optimised to run on microcontrollers and in
|
||||||
|
constrained environments (like the browser).
|
||||||
|
- [Pyodide](https://pyodide.org)) - a port of all CPython to WebAssembly.
|
||||||
|
|
||||||
|
These interpreters are compiled to [WebAssembly](https://webassembly.org/)
|
||||||
|
(shortened to WASM). The browser provides a secure WASM computing sandbox. Both
|
||||||
|
interpreters are compiled to web assembly with
|
||||||
|
[Emscripten](https://emscripten.org/). PyScript core maintainers work closely
|
||||||
|
with the core maintainers of both MicroPython and Pyodide (and CPython). We
|
||||||
|
work hard to ensure PyScript works efficiently in browsers on all platforms:
|
||||||
|
desktop, mobile, or elsewhere.
|
||||||
|
|
||||||
|
Our technical documentation for using this project can be
|
||||||
|
[found here](https://docs.pyscript.net/).
|
||||||
|
|
||||||
|
PyScript sits on two further projects (both written in JavaScript):
|
||||||
|
|
||||||
|
1. [polyscript](https://github.com/pyscript/polyscript/#readme) - used to
|
||||||
|
bootstrap WASM compiled interpreters in a browser.
|
||||||
|
2. [coincident](https://github.com/WebReflection/coincident) - used to simplify
|
||||||
|
worker based tasks.
|
||||||
|
|
||||||
|
PyScript itself is mostly written in JavaScript. The test suite for JavaScript
|
||||||
|
is in two parts: automated tests run in [playwright](https://playwright.dev/),
|
||||||
|
and manual tests you have to run in a browser and check yourself. PyScript also
|
||||||
|
has a plugin system so third parties can extend its capabilities with
|
||||||
|
JavaScript. Our built-in core plugins can be found in the `src/plugins`
|
||||||
|
directory. We describe how to write third party plugins in our
|
||||||
|
[developer documentation](https://docs.pyscript.net/latest/user-guide/plugins/).
|
||||||
|
|
||||||
|
We provide a `pyscript` namespace containing Python modules for common browser
|
||||||
|
based APIs and features (i.e. you can `import pyscript` in Python code running
|
||||||
|
inside PyScript, to access these features). The Python code for the `pyscript`
|
||||||
|
namespace is in `src/stdlib/pyscript` with the associated test suite in
|
||||||
|
`tests/python`. The tests use the browser friendly
|
||||||
|
[uPyTest](https://github.com/ntoll/upytest) test framework for checking Python
|
||||||
|
code running _within_ PyScript. All the Python tests are run in each each
|
||||||
|
available interpreter in both the main thread and a web worker (i.e. the
|
||||||
|
test suite is run four times, accounting for each combination of interpreter
|
||||||
|
and main/worker context).
|
||||||
|
|
||||||
|
When you create a local build all the automated tests (JavaScript and Python)
|
||||||
|
are run.
|
||||||
|
|
||||||
|
## Developer Guide
|
||||||
|
|
||||||
|
Full instructions for setting up a working development environment, how to
|
||||||
|
build PyScript and how to test it can be
|
||||||
|
[found in our official docs](https://docs.pyscript.net/latest/developers/).
|
||||||
|
|
||||||
|
The short version is:
|
||||||
|
|
||||||
|
- Ensure you have Python, node and npm installed.
|
||||||
|
- Create a Python virtual environment.
|
||||||
|
- In the root of this repository `make setup`.
|
||||||
|
- `make build` to build PyScript.
|
||||||
|
- As dependencies change over time, `make update` to keep in sync.
|
||||||
|
|
||||||
|
To start using the locally built version of PyScript, you'll need an HTML
|
||||||
|
page something like this (note the relative paths to assets in the `dist`
|
||||||
|
directory, in the `<head>` of the document):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Pure Python PyScript tests</title>
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css" />
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="mpy" src="./main.py" config="./conf.toml"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
Once set up, you should be able to run the most common activities via the
|
||||||
|
`make` command:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ make
|
||||||
|
|
||||||
|
There is no default Makefile target right now. Try:
|
||||||
|
|
||||||
|
make setup - check your environment and install the dependencies.
|
||||||
|
make update - update dependencies.
|
||||||
|
make clean - clean up auto-generated assets.
|
||||||
|
make build - build PyScript.
|
||||||
|
make precommit-check - run the precommit checks (run eslint).
|
||||||
|
make test - run all automated tests in playwright.
|
||||||
|
make fmt - format the code.
|
||||||
|
make fmt-check - check the code formatting.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
There are two main artifacts in this project:
|
||||||
|
|
||||||
|
- **stdlib** and its content: `src/stdlib/pyscript.js` exposes, as a
|
||||||
|
JavaScript object literal, all the _Python_ content within the folder
|
||||||
|
(recursively).
|
||||||
|
- **plugins** and its content: `src/plugins.js` exposes all available
|
||||||
|
_dynamic imports_, and is able to instrument the bundler to create files
|
||||||
|
apart from the `_dist/_` folder, so that by default _core_ remains as small
|
||||||
|
as possible.
|
||||||
|
|
||||||
|
Accordingly, whenever a file contains this warning at its first line, **please
|
||||||
|
do not change such file directly before submitting a merge request**, as that
|
||||||
|
file will be overwritten at the next `npm run build` command, either here or
|
||||||
|
in _CI_:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
While community or third party plugins don't need to be part of this repository
|
||||||
|
and can be added just importing `@pyscript/core` as module, there are a few
|
||||||
|
plugins that we would like to make available by default and these are
|
||||||
|
considered _core plugins_.
|
||||||
|
|
||||||
|
To add a _core plugin_ to this project define the plugin entry-point and name
|
||||||
|
in the `src/plugins` folder (see the `error.js` example) and create, if
|
||||||
|
necessary, a folder with the same name where extra files or dependencies can be
|
||||||
|
added.
|
||||||
|
|
||||||
|
The _build_ command will include plugins by name as artifacts so that the
|
||||||
|
bundler can create ad-hoc files within the `dist/` folder.
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
The `pyscript` package available in _Python_ lives in the folder
|
||||||
|
`src/stdlib/pyscript/`.
|
||||||
|
|
||||||
|
All _Python_ files will be embedded automatically whenever `npm run build`
|
||||||
|
happens and reflected into the `src/stdlib/pyscript.js` file.
|
||||||
|
|
||||||
|
Its _core_ responsibility is to ensure those files will be available through
|
||||||
|
the filesystem in either the _main_ thread, or any _worker_.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
To cut a new release of PyScript simply
|
||||||
|
[add a new release](https://github.com/pyscript/pyscript/releases) while
|
||||||
|
remembering to write a comprehensive changelog. A
|
||||||
|
[GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
|
||||||
|
will kick in and ensure the release is described and deployed to a URL with the
|
||||||
|
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
|
||||||
|
our [CalVer](https://calver.org/) versioning scheme).
|
||||||
|
|
||||||
|
Then, the following three separate repositories need updating:
|
||||||
|
|
||||||
|
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
|
||||||
|
file in the root of the directory and then `node version-update.js`.
|
||||||
|
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
|
||||||
|
referenced in `index.html` is the latest version.
|
||||||
|
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
|
||||||
|
Anaconda) to let the PSDC team know there's a new version, so they can update
|
||||||
|
their project templates.
|
||||||
31
core/dev.cjs
Normal file
31
core/dev.cjs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
const { exec } = require("node:child_process");
|
||||||
|
|
||||||
|
const build = (fileName) => {
|
||||||
|
if (fileName) console.log(fileName, "changed");
|
||||||
|
else console.log("building without optimizations");
|
||||||
|
queue = queue.then(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
exec(
|
||||||
|
"npm run build:stdlib && npm run build:plugins && npm run build:core",
|
||||||
|
{ cwd: __dirname, env: { ...process.env, NO_MIN: true } },
|
||||||
|
(error) => {
|
||||||
|
if (error) console.error(error);
|
||||||
|
else console.log(fileName || "", "build completed");
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
ignored: /\/(?:toml|plugins|pyscript)\.[mc]?js$/,
|
||||||
|
persistent: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
require("chokidar").watch("./src", options).on("change", build);
|
||||||
|
|
||||||
|
build();
|
||||||
22
core/eslint.config.mjs
Normal file
22
core/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
ignores: ["**/3rd-party/"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2021,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-implicit-globals": ["error"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
1
core/index.js
Normal file
1
core/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./dist/core.js";
|
||||||
2
core/jsdelivr.js
Normal file
2
core/jsdelivr.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// @see https://github.com/jsdelivr/jsdelivr/issues/18528
|
||||||
|
export * from "./core/dist/core.js";
|
||||||
4069
core/package-lock.json
generated
Normal file
4069
core/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
114
core/package.json
Normal file
114
core/package.json
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"name": "@pyscript/core",
|
||||||
|
"version": "0.6.62",
|
||||||
|
"type": "module",
|
||||||
|
"description": "PyScript",
|
||||||
|
"module": "./index.js",
|
||||||
|
"unpkg": "./index.js",
|
||||||
|
"jsdelivr": "./jsdelivr.js",
|
||||||
|
"browser": "./index.js",
|
||||||
|
"main": "./index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"./dist/",
|
||||||
|
"./src/",
|
||||||
|
"./types/",
|
||||||
|
"./index.js",
|
||||||
|
"./jsdelivr.js",
|
||||||
|
"LICENSE",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./types/core.d.ts",
|
||||||
|
"import": "./src/core.js"
|
||||||
|
},
|
||||||
|
"./js": {
|
||||||
|
"types": "./types/core.d.ts",
|
||||||
|
"import": "./dist/core.js"
|
||||||
|
},
|
||||||
|
"./css": {
|
||||||
|
"import": "./dist/core.css"
|
||||||
|
},
|
||||||
|
"./storage": {
|
||||||
|
"import": "./dist/storage.js"
|
||||||
|
},
|
||||||
|
"./service-worker": {
|
||||||
|
"import": "./dist/service-worker.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
|
||||||
|
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:integration; fi",
|
||||||
|
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||||
|
"build:flatted": "node rollup/flatted.cjs",
|
||||||
|
"build:plugins": "node rollup/plugins.cjs",
|
||||||
|
"build:stdlib": "node rollup/stdlib.cjs",
|
||||||
|
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||||
|
"build:tests-index": "node rollup/build_test_index.cjs",
|
||||||
|
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||||
|
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && playwright test tests/py_tests.main.spec.js && playwright test tests/py_tests.worker.spec.js) || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||||
|
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
|
||||||
|
"dev": "node dev.cjs",
|
||||||
|
"release": "npm run build && npm run zip",
|
||||||
|
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||||
|
"ts": "rm -rf types && tsc -p .",
|
||||||
|
"zip": "zip -r dist.zip ./dist"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"pyscript",
|
||||||
|
"core"
|
||||||
|
],
|
||||||
|
"author": "Anaconda Inc.",
|
||||||
|
"license": "APACHE-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
|
"@webreflection/idb-map": "^0.3.2",
|
||||||
|
"@webreflection/utils": "^0.1.1",
|
||||||
|
"add-promise-listener": "^0.1.3",
|
||||||
|
"basic-devtools": "^0.1.6",
|
||||||
|
"polyscript": "^0.17.33",
|
||||||
|
"sticky-module": "^0.1.1",
|
||||||
|
"to-json-callback": "^0.1.1",
|
||||||
|
"type-checked-collections": "^0.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@codemirror/commands": "^6.8.1",
|
||||||
|
"@codemirror/lang-python": "^6.2.1",
|
||||||
|
"@codemirror/language": "^6.11.2",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/view": "^6.38.0",
|
||||||
|
"@playwright/test": "^1.53.2",
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.6",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
|
"@webreflection/toml-j0.4": "^1.1.4",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"bun": "^1.2.17",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"codedent": "^0.1.2",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"eslint": "^9.30.0",
|
||||||
|
"flatted": "^3.3.3",
|
||||||
|
"rollup": "^4.44.1",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"rollup-plugin-string": "^3.0.0",
|
||||||
|
"static-handler": "^0.5.3",
|
||||||
|
"string-width": "^7.2.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"xterm-readline": "^1.1.2"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/pyscript/pyscript.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/pyscript/pyscript/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/pyscript/pyscript#readme"
|
||||||
|
}
|
||||||
77
core/rollup/3rd-party.cjs
Normal file
77
core/rollup/3rd-party.cjs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const { copyFileSync, writeFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
||||||
|
|
||||||
|
const targets = join(__dirname, "..", "src", "3rd-party");
|
||||||
|
const node_modules = join(__dirname, "..", "node_modules");
|
||||||
|
|
||||||
|
const { devDependencies } = require(join(__dirname, "..", "package.json"));
|
||||||
|
|
||||||
|
const v = (name) => devDependencies[name].replace(/[^\d.]/g, "");
|
||||||
|
|
||||||
|
const dropSourceMap = (str) =>
|
||||||
|
str.replace(/^\/.+? sourceMappingURL=\/.+$/m, "");
|
||||||
|
|
||||||
|
// Fetch a module via jsdelivr CDN `/+esm` orchestration
|
||||||
|
// then sanitize the resulting outcome to avoid importing
|
||||||
|
// anything via `/npm/...` through Rollup
|
||||||
|
const resolve = (name) => {
|
||||||
|
const cdn = `${CDN}/${name}@${v(name)}/+esm`;
|
||||||
|
console.debug("fetching", cdn);
|
||||||
|
return fetch(cdn)
|
||||||
|
.then((b) => b.text())
|
||||||
|
.then((text) =>
|
||||||
|
text.replace(
|
||||||
|
/("|')\/npm\/(.+)?\+esm\1/g,
|
||||||
|
// normalize `/npm/module@version/+esm` as
|
||||||
|
// just `module` so that rollup can do the rest
|
||||||
|
(_, quote, module) => {
|
||||||
|
const i = module.lastIndexOf("@");
|
||||||
|
return `${quote}${module.slice(0, i)}${quote}`;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// create a file rollup can then process and understand
|
||||||
|
const reBundle = (name) => Promise.resolve(`export * from "${name}";\n`);
|
||||||
|
|
||||||
|
// key/value pairs as:
|
||||||
|
// "3rd-party/file-name.js"
|
||||||
|
// string as content or
|
||||||
|
// Promise<string> as resolved content
|
||||||
|
const modules = {
|
||||||
|
// toml
|
||||||
|
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||||
|
|
||||||
|
// xterm
|
||||||
|
"xterm.js": resolve("@xterm/xterm"),
|
||||||
|
"xterm-readline.js": resolve("xterm-readline"),
|
||||||
|
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||||
|
b.text(),
|
||||||
|
),
|
||||||
|
"xterm_addon-web-links.js": fetch(
|
||||||
|
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||||
|
).then((b) => b.text()),
|
||||||
|
"xterm.css": fetch(
|
||||||
|
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
|
||||||
|
).then((b) => b.text()),
|
||||||
|
|
||||||
|
// codemirror
|
||||||
|
"codemirror.js": reBundle("codemirror"),
|
||||||
|
"codemirror_state.js": reBundle("@codemirror/state"),
|
||||||
|
"codemirror_lang-python.js": reBundle("@codemirror/lang-python"),
|
||||||
|
"codemirror_language.js": reBundle("@codemirror/language"),
|
||||||
|
"codemirror_view.js": reBundle("@codemirror/view"),
|
||||||
|
"codemirror_commands.js": reBundle("@codemirror/commands"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [target, source] of Object.entries(modules)) {
|
||||||
|
if (typeof source === "string") copyFileSync(source, join(targets, target));
|
||||||
|
else {
|
||||||
|
source.then((text) =>
|
||||||
|
writeFileSync(join(targets, target), dropSourceMap(text)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
core/rollup/build_test_index.cjs
Normal file
73
core/rollup/build_test_index.cjs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
const { join } = require("node:path");
|
||||||
|
const { lstatSync, readdirSync, writeFileSync } = require("node:fs");
|
||||||
|
|
||||||
|
// folders to not consider while crawling
|
||||||
|
const EXCLUDE_DIR = new Set(["ws"]);
|
||||||
|
|
||||||
|
const TEST_DIR = join(__dirname, "..", "tests");
|
||||||
|
|
||||||
|
const TEST_INDEX = join(TEST_DIR, "index.html");
|
||||||
|
|
||||||
|
const crawl = (path, tree = {}) => {
|
||||||
|
for (const file of readdirSync(path)) {
|
||||||
|
const current = join(path, file);
|
||||||
|
if (current === TEST_INDEX) continue;
|
||||||
|
if (lstatSync(current).isDirectory()) {
|
||||||
|
if (EXCLUDE_DIR.has(file)) continue;
|
||||||
|
const sub = {};
|
||||||
|
tree[file] = sub;
|
||||||
|
crawl(current, sub);
|
||||||
|
if (!Reflect.ownKeys(sub).length) {
|
||||||
|
delete tree[file];
|
||||||
|
}
|
||||||
|
} else if (file.endsWith(".html")) {
|
||||||
|
const name = file === "index.html" ? "." : file.slice(0, -5);
|
||||||
|
tree[name] = current.replace(TEST_DIR, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createList = (tree) => {
|
||||||
|
const ul = ["<ul>"];
|
||||||
|
for (const [key, value] of Object.entries(tree)) {
|
||||||
|
ul.push("<li>");
|
||||||
|
if (typeof value === "string") {
|
||||||
|
ul.push(`<a href=".${value}">${key}<small>.html</small></a>`);
|
||||||
|
} else {
|
||||||
|
if ("." in value) {
|
||||||
|
ul.push(`<strong><a href=".${value["."]}">${key}</a></strong>`);
|
||||||
|
delete value["."];
|
||||||
|
} else {
|
||||||
|
ul.push(`<strong><span>${key}</span></strong>`);
|
||||||
|
}
|
||||||
|
if (Reflect.ownKeys(value).length) ul.push(createList(value));
|
||||||
|
}
|
||||||
|
ul.push("</li>");
|
||||||
|
}
|
||||||
|
ul.push("</ul>");
|
||||||
|
return ul.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
TEST_INDEX,
|
||||||
|
`<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PyScript tests</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; }
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
transition: opacity .3s;
|
||||||
|
}
|
||||||
|
a, span { opacity: .7; }
|
||||||
|
a:hover { opacity: 1; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>${createList(crawl(TEST_DIR))}</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
);
|
||||||
67
core/rollup/core.config.js
Normal file
67
core/rollup/core.config.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// This file generates /core.js minified version of the module, which is
|
||||||
|
// the default exported as npm entry.
|
||||||
|
|
||||||
|
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import terser from "@rollup/plugin-terser";
|
||||||
|
import postcss from "rollup-plugin-postcss";
|
||||||
|
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
input: "./src/core.js",
|
||||||
|
plugins: plugins.concat(
|
||||||
|
process.env.NO_MIN
|
||||||
|
? [nodeResolve(), commonjs()]
|
||||||
|
: [nodeResolve(), commonjs(), terser()],
|
||||||
|
),
|
||||||
|
output: {
|
||||||
|
esModule: true,
|
||||||
|
dir: "./dist",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "./src/core.css",
|
||||||
|
plugins: [
|
||||||
|
postcss({
|
||||||
|
extract: true,
|
||||||
|
sourceMap: false,
|
||||||
|
minimize: !process.env.NO_MIN,
|
||||||
|
plugins: [],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
file: "./dist/core.css",
|
||||||
|
},
|
||||||
|
onwarn(warning, warn) {
|
||||||
|
if (warning.code === "FILE_NAME_CONFLICT") return;
|
||||||
|
warn(warning);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "./src/storage.js",
|
||||||
|
plugins: plugins.concat(
|
||||||
|
process.env.NO_MIN
|
||||||
|
? [nodeResolve(), commonjs()]
|
||||||
|
: [nodeResolve(), commonjs(), terser()],
|
||||||
|
),
|
||||||
|
output: {
|
||||||
|
esModule: true,
|
||||||
|
dir: "./dist",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "./src/service-worker.js",
|
||||||
|
plugins: plugins.concat(
|
||||||
|
process.env.NO_MIN
|
||||||
|
? [nodeResolve(), commonjs()]
|
||||||
|
: [nodeResolve(), commonjs(), terser()],
|
||||||
|
),
|
||||||
|
output: {
|
||||||
|
file: "./dist/service-worker.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
17
core/rollup/flatted.cjs
Normal file
17
core/rollup/flatted.cjs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const { writeFileSync, readFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const flatted = "# https://www.npmjs.com/package/flatted\n\n";
|
||||||
|
const source = join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"node_modules",
|
||||||
|
"flatted",
|
||||||
|
"python",
|
||||||
|
"flatted.py",
|
||||||
|
);
|
||||||
|
const dest = join(__dirname, "..", "src", "stdlib", "pyscript", "flatted.py");
|
||||||
|
|
||||||
|
const clear = (str) => String(str).replace(/^#.*/gm, "").trimStart();
|
||||||
|
|
||||||
|
writeFileSync(dest, flatted + clear(readFileSync(source)));
|
||||||
33
core/rollup/plugins.cjs
Normal file
33
core/rollup/plugins.cjs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const { readdirSync, writeFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const plugins = [""];
|
||||||
|
|
||||||
|
for (const file of readdirSync(join(__dirname, "..", "src", "plugins"))) {
|
||||||
|
if (/\.js$/.test(file)) {
|
||||||
|
const name = file.slice(0, -3);
|
||||||
|
const key = /^[a-zA-Z0-9$_]+$/.test(name)
|
||||||
|
? name
|
||||||
|
: `[${JSON.stringify(name)}]`;
|
||||||
|
const value = JSON.stringify(`./plugins/${file}`);
|
||||||
|
plugins.push(
|
||||||
|
// this comment is needed to avoid bundlers eagerly embedding lazy
|
||||||
|
// dependencies, causing all sort of issues once in production
|
||||||
|
// ⚠️ THIS HAS TO BE LIKE THIS or prettier changes it every single time
|
||||||
|
` ${key}: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
${value}
|
||||||
|
),`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.push("");
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(__dirname, "..", "src", "plugins.js"),
|
||||||
|
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default {${plugins.join(
|
||||||
|
"\n",
|
||||||
|
)}};\n`,
|
||||||
|
);
|
||||||
66
core/rollup/stdlib.cjs
Normal file
66
core/rollup/stdlib.cjs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const {
|
||||||
|
readdirSync,
|
||||||
|
readFileSync,
|
||||||
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
|
} = require("node:fs");
|
||||||
|
|
||||||
|
const { spawnSync } = require("node:child_process");
|
||||||
|
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const dedent = require("codedent");
|
||||||
|
|
||||||
|
const crawl = (path, json) => {
|
||||||
|
for (const file of readdirSync(path)) {
|
||||||
|
const full = join(path, file);
|
||||||
|
if (/\.py$/.test(file)) {
|
||||||
|
if (process.env.NO_MIN) json[file] = readFileSync(full).toString();
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
output: [error, result],
|
||||||
|
} = spawnSync("pyminify", [
|
||||||
|
"--remove-literal-statements",
|
||||||
|
full,
|
||||||
|
]);
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
json[file] = result.toString();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.log(
|
||||||
|
dedent(`
|
||||||
|
\x1b[1m⚠️ is your env activated?\x1b[0m
|
||||||
|
\x1b[2mYou need a Python env to run \x1b[0mpyminify\x1b[2m.\x1b[0m
|
||||||
|
\x1b[2mTo do so, you can try the following:\x1b[0m
|
||||||
|
python -m venv env
|
||||||
|
source env/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install --ignore-requires-python python-minifier
|
||||||
|
pip install setuptools
|
||||||
|
\x1b[2mand you can then try \x1b[0mnpm run build\x1b[2m again.\x1b[0m
|
||||||
|
`),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (statSync(full).isDirectory() && !file.endsWith("_"))
|
||||||
|
crawl(full, (json[file] = {}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const json = {};
|
||||||
|
|
||||||
|
crawl(join(__dirname, "..", "src", "stdlib"), json);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(__dirname, "..", "src", "stdlib", "pyscript.js"),
|
||||||
|
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default ${JSON.stringify(
|
||||||
|
json,
|
||||||
|
null,
|
||||||
|
" ",
|
||||||
|
)};\n`,
|
||||||
|
);
|
||||||
21
core/src/3rd-party-licenses/codemirror.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_commands.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_commands.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_language.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_language.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_state.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_state.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/codemirror_view.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_view.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/toml.license.txt
Normal file
21
core/src/3rd-party-licenses/toml.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Jak Wings
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
25
core/src/3rd-party-licenses/xterm-readline.license.txt
Normal file
25
core/src/3rd-party-licenses/xterm-readline.license.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Copyright 2021 Erik Bremen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
21
core/src/3rd-party-licenses/xterm.license.txt
Normal file
21
core/src/3rd-party-licenses/xterm.license.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
|
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||||
|
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
19
core/src/3rd-party-licenses/xterm_addon-fit.license.txt
Normal file
19
core/src/3rd-party-licenses/xterm_addon-fit.license.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
11
core/src/3rd-party/README.md
vendored
Normal file
11
core/src/3rd-party/README.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# PyScript 3rd Party
|
||||||
|
|
||||||
|
This folder contains artifacts created via [3rd-party.cjs](../../rollup/3rd-party.cjs).
|
||||||
|
|
||||||
|
As we would like to offer a way to run PyScript offline, and we already offer a `dist` folder with all the necessary scripts, we have created a foreign dependencies resolver that allow to lazy-load CDN dependencies out of the box.
|
||||||
|
|
||||||
|
Please **note** these dependencies are **not interpreters**, because interpreters have their own mechanism, folders structure, WASM files, and whatnot, to work locally, but at least XTerm or the TOML parser, among other lazy dependencies, should be available within the dist folder.
|
||||||
|
|
||||||
|
## Licenses
|
||||||
|
|
||||||
|
All licenses provided by 3rd-party authors can be found in [3rd-party-licenses](../3rd-party-licenses/) folder.
|
||||||
1
core/src/3rd-party/codemirror.js
vendored
Normal file
1
core/src/3rd-party/codemirror.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "codemirror";
|
||||||
1
core/src/3rd-party/codemirror_commands.js
vendored
Normal file
1
core/src/3rd-party/codemirror_commands.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@codemirror/commands";
|
||||||
1
core/src/3rd-party/codemirror_lang-python.js
vendored
Normal file
1
core/src/3rd-party/codemirror_lang-python.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@codemirror/lang-python";
|
||||||
1
core/src/3rd-party/codemirror_language.js
vendored
Normal file
1
core/src/3rd-party/codemirror_language.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@codemirror/language";
|
||||||
1
core/src/3rd-party/codemirror_state.js
vendored
Normal file
1
core/src/3rd-party/codemirror_state.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@codemirror/state";
|
||||||
1
core/src/3rd-party/codemirror_view.js
vendored
Normal file
1
core/src/3rd-party/codemirror_view.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "@codemirror/view";
|
||||||
3
core/src/3rd-party/toml.js
vendored
Normal file
3
core/src/3rd-party/toml.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
core/src/3rd-party/xterm-readline.js
vendored
Normal file
7
core/src/3rd-party/xterm-readline.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
core/src/3rd-party/xterm.css
vendored
Normal file
7
core/src/3rd-party/xterm.css
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Minified by jsDelivr using clean-css v5.3.2.
|
||||||
|
* Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
|
||||||
7
core/src/3rd-party/xterm.js
vendored
Normal file
7
core/src/3rd-party/xterm.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
core/src/3rd-party/xterm_addon-fit.js
vendored
Normal file
7
core/src/3rd-party/xterm_addon-fit.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
|
||||||
|
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
var e,t,r={exports:{}};self;var s=r.exports=(e=t={},Object.defineProperty(e,"__esModule",{value:!0}),e.FitAddon=void 0,e.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(s.getPropertyValue("height")),o=Math.max(0,parseInt(s.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=i-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=o-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}},t),i=r.exports.FitAddon,o=r.exports.__esModule;export{i as FitAddon,o as __esModule,s as default};
|
||||||
7
core/src/3rd-party/xterm_addon-web-links.js
vendored
Normal file
7
core/src/3rd-party/xterm_addon-web-links.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
|
||||||
|
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
var e={exports:{}};self;var t=e.exports=(()=>{var e={6:(e,t)=>{function r(e){try{const t=new URL(e),r=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(r.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,r,n={}){this._terminal=e,this._regex=t,this._handler=r,this._options=n}provideLinks(e,t){const r=n.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(r))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,r)=>{if(this._options.hover){const{range:n}=e;this._options.hover(t,r,n)}},e)))}};class n{static computeLink(e,t,o,s){const i=new RegExp(t.source,(t.flags||"")+"g"),[a,l]=n._getWindowedLineStrings(e-1,o),c=a.join("");let d;const p=[];for(;d=i.exec(c);){const e=d[0];if(!r(e))continue;const[t,i]=n._mapStrIdx(o,l,0,d.index),[a,c]=n._mapStrIdx(o,t,i,e.length);if(-1===t||-1===i||-1===a||-1===c)continue;const h={start:{x:i+1,y:t+1},end:{x:c,y:a+1}};p.push({range:h,text:e,activate:s})}return p}static _getWindowedLineStrings(e,t){let r,n=e,o=e,s=0,i="";const a=[];if(r=t.buffer.active.getLine(e)){const e=r.translateToString(!0);if(r.isWrapped&&" "!==e[0]){for(s=0;(r=t.buffer.active.getLine(--n))&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),r.isWrapped&&-1===i.indexOf(" ")););a.reverse()}for(a.push(e),s=0;(r=t.buffer.active.getLine(++o))&&r.isWrapped&&s<2048&&(i=r.translateToString(!0),s+=i.length,a.push(i),-1===i.indexOf(" ")););}return[a,n]}static _mapStrIdx(e,t,r,n){const o=e.buffer.active,s=o.getNullCell();let i=r;for(;n;){const e=o.getLine(t);if(!e)return[-1,-1];for(let r=i;r<e.length;++r){e.getCell(r,s);const i=s.getChars();if(s.getWidth()&&(n-=i.length||1,r===e.length-1&&""===i)){const e=o.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,s),2===s.getWidth()&&(n+=1))}if(n<0)return[t,r]}t++,i=0}return[t,i]}}t.LinkComputer=n}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var s=t[n]={exports:{}};return e[n](s,s.exports,r),s.exports}var n={};return(()=>{var e=n;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=r(6),o=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function s(e,t){const r=window.open();if(r){try{r.opener=null}catch{}r.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=s,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const r=this._options,n=r.urlRegex||o;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,n,this._handler,r))}dispose(){this._linkProvider?.dispose()}}})(),n})(),r=e.exports.WebLinksAddon,n=e.exports.__esModule;export{r as WebLinksAddon,n as __esModule,t as default};
|
||||||
18
core/src/all-done.js
Normal file
18
core/src/all-done.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
import TYPES from "./types.js";
|
||||||
|
|
||||||
|
const waitForIt = [];
|
||||||
|
|
||||||
|
for (const [TYPE] of TYPES) {
|
||||||
|
const selectors = [`script[type="${TYPE}"]`, `${TYPE}-script`];
|
||||||
|
for (const element of document.querySelectorAll(selectors.join(","))) {
|
||||||
|
const { promise, resolve } = withResolvers();
|
||||||
|
waitForIt.push(promise);
|
||||||
|
element.addEventListener(`${TYPE}:done`, resolve, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for all the things then cleanup
|
||||||
|
Promise.all(waitForIt).then(() => {
|
||||||
|
dispatchEvent(new Event("py:all-done"));
|
||||||
|
});
|
||||||
163
core/src/config.js
Normal file
163
core/src/config.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* This file parses a generic <py-config> or config attribute
|
||||||
|
* to use as base config for all py-script elements, importing
|
||||||
|
* also a queue of plugins *before* the interpreter (if any) resolves.
|
||||||
|
*/
|
||||||
|
import { $$ } from "basic-devtools";
|
||||||
|
|
||||||
|
import TYPES from "./types.js";
|
||||||
|
import allPlugins from "./plugins.js";
|
||||||
|
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||||
|
import { ErrorCode } from "./exceptions.js";
|
||||||
|
|
||||||
|
const { BAD_CONFIG, CONFLICTING_CODE } = ErrorCode;
|
||||||
|
|
||||||
|
const badURL = (url, expected = "") => {
|
||||||
|
let message = `(${BAD_CONFIG}): Invalid URL: ${url}`;
|
||||||
|
if (expected) message += `\nexpected ${expected} content`;
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string, returns its trimmed content as text,
|
||||||
|
* fetching it from a file if the content is a URL.
|
||||||
|
* @param {string} config either JSON, TOML, or a file to fetch
|
||||||
|
* @param {string?} type the optional type to enforce
|
||||||
|
* @returns {{json: boolean, toml: boolean, text: string}}
|
||||||
|
*/
|
||||||
|
export const configDetails = async (config, type) => {
|
||||||
|
let text = config?.trim();
|
||||||
|
// we only support an object as root config
|
||||||
|
let url = "",
|
||||||
|
toml = false,
|
||||||
|
json = /^{/.test(text) && /}$/.test(text);
|
||||||
|
// handle files by extension (relaxing urls parts after)
|
||||||
|
if (!json && /\.(\w+)(?:\?\S*)?$/.test(text)) {
|
||||||
|
const ext = RegExp.$1;
|
||||||
|
if (ext === "json" && type !== "toml") json = true;
|
||||||
|
else if (ext === "toml" && type !== "json") toml = true;
|
||||||
|
else badURL(text, type);
|
||||||
|
url = text;
|
||||||
|
text = (await fetch(url).then(getText)).trim();
|
||||||
|
}
|
||||||
|
return { json, toml: toml || (!json && !!text), text, url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
|
||||||
|
|
||||||
|
const relative_url = (url, base = location.href) => new URL(url, base).href;
|
||||||
|
|
||||||
|
const syntaxError = (type, url, { message }) => {
|
||||||
|
let str = `(${BAD_CONFIG}): Invalid ${type}`;
|
||||||
|
if (url) str += ` @ ${url}`;
|
||||||
|
return new SyntaxError(`${str}\n${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configs = new Map();
|
||||||
|
|
||||||
|
for (const [TYPE] of TYPES) {
|
||||||
|
/** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
||||||
|
let plugins;
|
||||||
|
|
||||||
|
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
||||||
|
let parsed;
|
||||||
|
|
||||||
|
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
|
||||||
|
let error;
|
||||||
|
|
||||||
|
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
|
||||||
|
let configURL;
|
||||||
|
|
||||||
|
let config,
|
||||||
|
type,
|
||||||
|
pyElement,
|
||||||
|
pyConfigs = $$(`${TYPE}-config`),
|
||||||
|
attrConfigs = $$(
|
||||||
|
[
|
||||||
|
`script[type="${TYPE}"][config]:not([worker])`,
|
||||||
|
`${TYPE}-script[config]:not([worker])`,
|
||||||
|
].join(","),
|
||||||
|
);
|
||||||
|
|
||||||
|
// throw an error if there are multiple <py-config> or <mpy-config>
|
||||||
|
if (pyConfigs.length > 1) {
|
||||||
|
error = conflictError(`Too many ${TYPE}-config`);
|
||||||
|
} else {
|
||||||
|
// throw an error if there are <x-config> and config="x" attributes
|
||||||
|
if (pyConfigs.length && attrConfigs.length) {
|
||||||
|
error = conflictError(
|
||||||
|
`Ambiguous ${TYPE}-config VS config attribute`,
|
||||||
|
);
|
||||||
|
} else if (pyConfigs.length) {
|
||||||
|
[pyElement] = pyConfigs;
|
||||||
|
config = pyElement.getAttribute("src") || pyElement.textContent;
|
||||||
|
type = pyElement.getAttribute("type");
|
||||||
|
} else if (attrConfigs.length) {
|
||||||
|
[pyElement, ...attrConfigs] = attrConfigs;
|
||||||
|
config = pyElement.getAttribute("config");
|
||||||
|
// throw an error if dirrent scripts use different configs
|
||||||
|
if (
|
||||||
|
attrConfigs.some((el) => el.getAttribute("config") !== config)
|
||||||
|
) {
|
||||||
|
error = conflictError(
|
||||||
|
"Unable to use different configs on main",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// catch possible fetch errors
|
||||||
|
if (!error && config) {
|
||||||
|
try {
|
||||||
|
const { json, toml, text, url } = await configDetails(config, type);
|
||||||
|
if (url) configURL = relative_url(url);
|
||||||
|
config = text;
|
||||||
|
if (json || type === "json") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
error = syntaxError("JSON", url, e);
|
||||||
|
}
|
||||||
|
} else if (toml || type === "toml") {
|
||||||
|
try {
|
||||||
|
const { parse } = await import(
|
||||||
|
/* webpackIgnore: true */ "./3rd-party/toml.js"
|
||||||
|
);
|
||||||
|
parsed = parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
error = syntaxError("TOML", url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse all plugins and optionally ignore only
|
||||||
|
// those flagged as "undesired" via `!` prefix
|
||||||
|
plugins = async () => {
|
||||||
|
const toBeAwaited = [];
|
||||||
|
for (const [key, value] of Object.entries(allPlugins)) {
|
||||||
|
if (error) {
|
||||||
|
if (key === "error") {
|
||||||
|
// show on page the config is broken, meaning that
|
||||||
|
// it was not possible to disable error plugin neither
|
||||||
|
// as that part wasn't correctly parsed anyway
|
||||||
|
value().then(({ notify }) => notify(error.message));
|
||||||
|
}
|
||||||
|
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||||
|
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||||
|
} else if (key === "error") {
|
||||||
|
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await Promise.all(toBeAwaited);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Number.isSafeInteger(parsed?.experimental_ffi_timeout))
|
||||||
|
globalThis.reflected_ffi_timeout = parsed?.experimental_ffi_timeout;
|
||||||
|
|
||||||
|
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { configs, relative_url };
|
||||||
61
core/src/core.css
Normal file
61
core/src/core.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
py-script,
|
||||||
|
py-config,
|
||||||
|
mpy-script,
|
||||||
|
mpy-config {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PyEditor */
|
||||||
|
.py-editor-box,
|
||||||
|
.mpy-editor-box {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.py-editor-input,
|
||||||
|
.mpy-editor-input {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.py-editor-box::before,
|
||||||
|
.mpy-editor-box::before {
|
||||||
|
content: attr(data-env);
|
||||||
|
display: block;
|
||||||
|
font-size: x-small;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
.py-editor-output,
|
||||||
|
.mpy-editor-output {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.py-editor-run-button,
|
||||||
|
.mpy-editor-run-button {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.py-editor-box:hover .py-editor-run-button,
|
||||||
|
.mpy-editor-box:hover .mpy-editor-run-button,
|
||||||
|
.py-editor-run-button:focus,
|
||||||
|
.py-editor-run-button.running,
|
||||||
|
.mpy-editor-run-button:focus,
|
||||||
|
.mpy-editor-run-button.running {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
py-terminal span,
|
||||||
|
mpy-terminal span {
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.pyscript-fs {
|
||||||
|
border-radius: 8px;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.pyscript-fs > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
370
core/src/core.js
Normal file
370
core/src/core.js
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
/*! (c) PyScript Development Team */
|
||||||
|
|
||||||
|
import "./zero-redirect.js";
|
||||||
|
import stickyModule from "sticky-module";
|
||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
|
||||||
|
import {
|
||||||
|
INVALID_CONTENT,
|
||||||
|
Hook,
|
||||||
|
XWorker,
|
||||||
|
assign,
|
||||||
|
dedent,
|
||||||
|
define,
|
||||||
|
defineProperty,
|
||||||
|
dispatch,
|
||||||
|
isSync,
|
||||||
|
queryTarget,
|
||||||
|
unescape,
|
||||||
|
whenDefined,
|
||||||
|
} from "polyscript/exports";
|
||||||
|
|
||||||
|
import "./all-done.js";
|
||||||
|
import TYPES from "./types.js";
|
||||||
|
import { configs, relative_url } from "./config.js";
|
||||||
|
import sync from "./sync.js";
|
||||||
|
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
||||||
|
import { ErrorCode } from "./exceptions.js";
|
||||||
|
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||||
|
import {
|
||||||
|
hooks,
|
||||||
|
main,
|
||||||
|
worker,
|
||||||
|
codeFor,
|
||||||
|
createFunction,
|
||||||
|
inputFailure,
|
||||||
|
} from "./hooks.js";
|
||||||
|
import * as fs from "./fs.js";
|
||||||
|
|
||||||
|
import codemirror from "./plugins/codemirror.js";
|
||||||
|
export { codemirror };
|
||||||
|
|
||||||
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
export { stdlib, optional, inputFailure };
|
||||||
|
|
||||||
|
export const donkey = (options) =>
|
||||||
|
import(/* webpackIgnore: true */ "./plugins/donkey.js").then((module) =>
|
||||||
|
module.default(options),
|
||||||
|
);
|
||||||
|
|
||||||
|
// generic helper to disambiguate between custom element and script
|
||||||
|
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||||
|
|
||||||
|
// Used to create either Pyodide or MicroPython workers
|
||||||
|
// with the PyScript module available within the code
|
||||||
|
const [PyWorker, MPWorker] = [...TYPES.entries()].map(
|
||||||
|
([TYPE, interpreter]) =>
|
||||||
|
/**
|
||||||
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||||
|
* @param {string} file the python file to run ina worker.
|
||||||
|
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||||
|
* @returns {Promise<Worker & {sync: object}>}
|
||||||
|
*/
|
||||||
|
async function PyScriptWorker(file, options) {
|
||||||
|
await configs.get(TYPE).plugins;
|
||||||
|
const xworker = XWorker.call(
|
||||||
|
new Hook(null, hooked.get(TYPE)),
|
||||||
|
file,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
type: interpreter,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assign(xworker.sync, sync);
|
||||||
|
return xworker.ready;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// avoid multiple initialization of the same library
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
PyWorker: exportedPyWorker,
|
||||||
|
MPWorker: exportedMPWorker,
|
||||||
|
hooks: exportedHooks,
|
||||||
|
config: exportedConfig,
|
||||||
|
whenDefined: exportedWhenDefined,
|
||||||
|
},
|
||||||
|
alreadyLive,
|
||||||
|
] = stickyModule("@pyscript/core", {
|
||||||
|
PyWorker,
|
||||||
|
MPWorker,
|
||||||
|
hooks,
|
||||||
|
config: {},
|
||||||
|
whenDefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
TYPES,
|
||||||
|
relative_url,
|
||||||
|
exportedPyWorker as PyWorker,
|
||||||
|
exportedMPWorker as MPWorker,
|
||||||
|
exportedHooks as hooks,
|
||||||
|
exportedConfig as config,
|
||||||
|
exportedWhenDefined as whenDefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const offline_interpreter = (config) =>
|
||||||
|
config?.interpreter && relative_url(config.interpreter);
|
||||||
|
|
||||||
|
const hooked = new Map();
|
||||||
|
|
||||||
|
for (const [TYPE, interpreter] of TYPES) {
|
||||||
|
// avoid any dance if the module already landed
|
||||||
|
if (alreadyLive) break;
|
||||||
|
|
||||||
|
const dispatchDone = (element, isAsync, result) => {
|
||||||
|
if (isAsync) result.then(() => dispatch(element, TYPE, "done"));
|
||||||
|
else dispatch(element, TYPE, "done");
|
||||||
|
};
|
||||||
|
|
||||||
|
const { config, configURL, plugins, error } = configs.get(TYPE);
|
||||||
|
|
||||||
|
// create a unique identifier when/if needed
|
||||||
|
let id = 0;
|
||||||
|
const getID = (prefix = TYPE) => `${prefix}-${id++}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
|
||||||
|
* It either throws an error if the 'src' can't be fetched or it returns a fallback
|
||||||
|
* content as source.
|
||||||
|
*/
|
||||||
|
const fetchSource = async (tag, io, asText) => {
|
||||||
|
if (tag.hasAttribute("src")) {
|
||||||
|
try {
|
||||||
|
return await fetch(tag.getAttribute("src")).then(getText);
|
||||||
|
} catch (error) {
|
||||||
|
io.stderr(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asText) return dedent(tag.textContent);
|
||||||
|
|
||||||
|
const code = dedent(unescape(tag.innerHTML));
|
||||||
|
console.warn(
|
||||||
|
`Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
|
||||||
|
code,
|
||||||
|
);
|
||||||
|
return code;
|
||||||
|
};
|
||||||
|
|
||||||
|
// register once any interpreter
|
||||||
|
let alreadyRegistered = false;
|
||||||
|
|
||||||
|
// allows lazy element features on code evaluation
|
||||||
|
let currentElement;
|
||||||
|
|
||||||
|
const registerModule = ({ XWorker, interpreter, io }) => {
|
||||||
|
// avoid multiple registration of the same interpreter
|
||||||
|
if (alreadyRegistered) return;
|
||||||
|
alreadyRegistered = true;
|
||||||
|
|
||||||
|
// automatically use the pyscript stderr (when/if defined)
|
||||||
|
// this defaults to console.error
|
||||||
|
function PyWorker(...args) {
|
||||||
|
const worker = XWorker(...args);
|
||||||
|
worker.onerror = ({ error }) => io.stderr(error);
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enrich the Python env with some JS utility for main
|
||||||
|
interpreter.registerJsModule("_pyscript", {
|
||||||
|
PyWorker,
|
||||||
|
fs,
|
||||||
|
interpreter,
|
||||||
|
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||||
|
get target() {
|
||||||
|
return isScript(currentElement)
|
||||||
|
? currentElement.target.id
|
||||||
|
: currentElement.id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// define the module as both `<script type="py">` and `<py-script>`
|
||||||
|
// but only if the config didn't throw an error
|
||||||
|
if (!error) {
|
||||||
|
// ensure plugins are bootstrapped already before custom type definition
|
||||||
|
// NOTE: we cannot top-level await in here as plugins import other utilities
|
||||||
|
// from core.js itself so that custom definition should not be blocking.
|
||||||
|
plugins().then(() => {
|
||||||
|
// possible early errors sent by polyscript
|
||||||
|
const errors = new Map();
|
||||||
|
|
||||||
|
// specific main and worker hooks
|
||||||
|
const hooks = {
|
||||||
|
main: {
|
||||||
|
...codeFor(main, TYPE),
|
||||||
|
async onReady(wrap, element) {
|
||||||
|
registerModule(wrap);
|
||||||
|
|
||||||
|
// allows plugins to do whatever they want with the element
|
||||||
|
// before regular stuff happens in here
|
||||||
|
for (const callback of main("onReady"))
|
||||||
|
await callback(wrap, element);
|
||||||
|
|
||||||
|
// now that all possible plugins are configured,
|
||||||
|
// bail out if polyscript encountered an error
|
||||||
|
if (errors.has(element)) {
|
||||||
|
let { message } = errors.get(element);
|
||||||
|
errors.delete(element);
|
||||||
|
const clone = message === INVALID_CONTENT;
|
||||||
|
message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
|
||||||
|
message += element.cloneNode(clone).outerHTML;
|
||||||
|
wrap.io.stderr(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScript(element)) {
|
||||||
|
const isAsync = !isSync(element);
|
||||||
|
const target = element.getAttribute("target");
|
||||||
|
const show = target
|
||||||
|
? queryTarget(element, target)
|
||||||
|
: document.createElement("script-py");
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
const { head, body } = document;
|
||||||
|
if (head.contains(element)) body.append(show);
|
||||||
|
else element.after(show);
|
||||||
|
}
|
||||||
|
if (!show.id) show.id = getID();
|
||||||
|
|
||||||
|
// allows the code to retrieve the target element via
|
||||||
|
// document.currentScript.target if needed
|
||||||
|
defineProperty(element, "target", { value: show });
|
||||||
|
|
||||||
|
// notify before the code runs
|
||||||
|
dispatch(element, TYPE, "ready");
|
||||||
|
dispatchDone(
|
||||||
|
element,
|
||||||
|
isAsync,
|
||||||
|
wrap[`run${isAsync ? "Async" : ""}`](
|
||||||
|
await fetchSource(element, wrap.io, true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// resolve PyScriptElement to allow connectedCallback
|
||||||
|
element._wrap.resolve(wrap);
|
||||||
|
}
|
||||||
|
console.debug("[pyscript/main] PyScript Ready");
|
||||||
|
},
|
||||||
|
onWorker(_, xworker) {
|
||||||
|
assign(xworker.sync, sync);
|
||||||
|
for (const callback of main("onWorker"))
|
||||||
|
callback(_, xworker);
|
||||||
|
},
|
||||||
|
onBeforeRun(wrap, element) {
|
||||||
|
currentElement = element;
|
||||||
|
bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onBeforeRun",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onBeforeRunAsync(wrap, element) {
|
||||||
|
currentElement = element;
|
||||||
|
return bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onBeforeRunAsync",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onAfterRun(wrap, element) {
|
||||||
|
bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onAfterRun",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onAfterRunAsync(wrap, element) {
|
||||||
|
return bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onAfterRunAsync",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
...codeFor(worker, TYPE),
|
||||||
|
// these are lazy getters that returns a composition
|
||||||
|
// of the current hooks or undefined, if no hook is present
|
||||||
|
get onReady() {
|
||||||
|
return createFunction(this, "onReady", true);
|
||||||
|
},
|
||||||
|
get onBeforeRun() {
|
||||||
|
return createFunction(this, "onBeforeRun", false);
|
||||||
|
},
|
||||||
|
get onBeforeRunAsync() {
|
||||||
|
return createFunction(this, "onBeforeRunAsync", true);
|
||||||
|
},
|
||||||
|
get onAfterRun() {
|
||||||
|
return createFunction(this, "onAfterRun", false);
|
||||||
|
},
|
||||||
|
get onAfterRunAsync() {
|
||||||
|
return createFunction(this, "onAfterRunAsync", true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
hooked.set(TYPE, hooks);
|
||||||
|
|
||||||
|
define(TYPE, {
|
||||||
|
config,
|
||||||
|
configURL,
|
||||||
|
interpreter,
|
||||||
|
hooks,
|
||||||
|
env: `${TYPE}-script`,
|
||||||
|
version: offline_interpreter(config),
|
||||||
|
onerror(error, element) {
|
||||||
|
errors.set(element, error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define(
|
||||||
|
`${TYPE}-script`,
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
assign(super(), {
|
||||||
|
_wrap: withResolvers(),
|
||||||
|
srcCode: "",
|
||||||
|
executed: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
get id() {
|
||||||
|
return super.id || (super.id = getID());
|
||||||
|
}
|
||||||
|
set id(value) {
|
||||||
|
super.id = value;
|
||||||
|
}
|
||||||
|
async connectedCallback() {
|
||||||
|
if (!this.executed) {
|
||||||
|
this.executed = true;
|
||||||
|
const isAsync = !isSync(this);
|
||||||
|
const { io, run, runAsync } = await this._wrap
|
||||||
|
.promise;
|
||||||
|
this.srcCode = await fetchSource(
|
||||||
|
this,
|
||||||
|
io,
|
||||||
|
!this.childElementCount,
|
||||||
|
);
|
||||||
|
this.replaceChildren();
|
||||||
|
this.style.display = "block";
|
||||||
|
dispatch(this, TYPE, "ready");
|
||||||
|
dispatchDone(
|
||||||
|
this,
|
||||||
|
isAsync,
|
||||||
|
(isAsync ? runAsync : run)(this.srcCode),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// export the used config without allowing leaks through it
|
||||||
|
exportedConfig[TYPE] = structuredClone(config);
|
||||||
|
}
|
||||||
109
core/src/exceptions.js
Normal file
109
core/src/exceptions.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { assign } from "polyscript/exports";
|
||||||
|
|
||||||
|
const CLOSEBUTTON =
|
||||||
|
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor' width='12px'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These error codes are used to identify the type of error that occurred.
|
||||||
|
* @see https://pyscript.github.io/docs/latest/reference/exceptions.html?highlight=errors
|
||||||
|
*/
|
||||||
|
export const ErrorCode = {
|
||||||
|
GENERIC: "PY0000", // Use this only for development then change to a more specific error code
|
||||||
|
CONFLICTING_CODE: "PY0409",
|
||||||
|
BAD_CONFIG: "PY1000",
|
||||||
|
MICROPIP_INSTALL_ERROR: "PY1001",
|
||||||
|
BAD_PLUGIN_FILE_EXTENSION: "PY2000",
|
||||||
|
NO_DEFAULT_EXPORT: "PY2001",
|
||||||
|
TOP_LEVEL_AWAIT: "PY9000",
|
||||||
|
// Currently these are created depending on error code received from fetching
|
||||||
|
FETCH_ERROR: "PY0001",
|
||||||
|
FETCH_NAME_ERROR: "PY0002",
|
||||||
|
FETCH_UNAUTHORIZED_ERROR: "PY0401",
|
||||||
|
FETCH_FORBIDDEN_ERROR: "PY0403",
|
||||||
|
FETCH_NOT_FOUND_ERROR: "PY0404",
|
||||||
|
FETCH_SERVER_ERROR: "PY0500",
|
||||||
|
FETCH_UNAVAILABLE_ERROR: "PY0503",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys of the ErrorCode object
|
||||||
|
* @typedef {keyof ErrorCode} ErrorCodes
|
||||||
|
* */
|
||||||
|
|
||||||
|
export class UserError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {ErrorCodes} errorCode
|
||||||
|
* @param {string} message
|
||||||
|
* @param {string} messageType
|
||||||
|
* */
|
||||||
|
constructor(errorCode, message = "", messageType = "text") {
|
||||||
|
super(`(${errorCode}): ${message}`);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.messageType = messageType;
|
||||||
|
this.name = "UserError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FetchError extends UserError {
|
||||||
|
/**
|
||||||
|
* @param {ErrorCodes} errorCode
|
||||||
|
* @param {string} message
|
||||||
|
* */
|
||||||
|
constructor(errorCode, message) {
|
||||||
|
super(errorCode, message);
|
||||||
|
this.name = "FetchError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstallError extends UserError {
|
||||||
|
/**
|
||||||
|
* @param {ErrorCodes} errorCode
|
||||||
|
* @param {string} message
|
||||||
|
* */
|
||||||
|
constructor(errorCode, message) {
|
||||||
|
super(errorCode, message);
|
||||||
|
this.name = "InstallError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal function for creating alert banners on the page
|
||||||
|
* @param {string} message The message to be displayed to the user
|
||||||
|
* @param {string} level The alert level of the message. Can be any string; 'error' or 'warning' cause matching messages to be emitted to the console
|
||||||
|
* @param {string} [messageType="text"] If set to "html", the message content will be assigned to the banner's innerHTML directly, instead of its textContent
|
||||||
|
* @param {any} [logMessage=true] An additional flag for whether the message should be sent to the console log.
|
||||||
|
*/
|
||||||
|
export function _createAlertBanner(
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
messageType = "text",
|
||||||
|
logMessage = true,
|
||||||
|
) {
|
||||||
|
switch (`log-${level}-${logMessage}`) {
|
||||||
|
case "log-error-true":
|
||||||
|
console.error(message);
|
||||||
|
break;
|
||||||
|
case "log-warning-true":
|
||||||
|
console.warn(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = messageType === "html" ? "innerHTML" : "textContent";
|
||||||
|
const banner = assign(document.createElement("div"), {
|
||||||
|
className: `alert-banner py-${level}`,
|
||||||
|
[content]: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (level === "warning") {
|
||||||
|
const closeButton = assign(document.createElement("button"), {
|
||||||
|
id: "alert-close-button",
|
||||||
|
innerHTML: CLOSEBUTTON,
|
||||||
|
});
|
||||||
|
|
||||||
|
banner.appendChild(closeButton).addEventListener("click", () => {
|
||||||
|
banner.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.prepend(banner);
|
||||||
|
}
|
||||||
69
core/src/fetch.js
Normal file
69
core/src/fetch.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { FetchError, ErrorCode } from "./exceptions.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Response} response
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getText = (response) => response.text();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a fetch wrapper that handles any non 200 responses and throws a
|
||||||
|
* FetchError with the right ErrorCode. This is useful because our FetchError
|
||||||
|
* will automatically create an alert banner.
|
||||||
|
*
|
||||||
|
* @param {string} url - URL to fetch
|
||||||
|
* @param {Request} [options] - options to pass to fetch
|
||||||
|
* @returns {Promise<Response>}
|
||||||
|
*/
|
||||||
|
export async function robustFetch(url, options) {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
// Note: We need to wrap fetch into a try/catch block because fetch
|
||||||
|
// throws a TypeError if the URL is invalid such as http://blah.blah
|
||||||
|
try {
|
||||||
|
response = await fetch(url, options);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err;
|
||||||
|
let errMsg;
|
||||||
|
if (url.startsWith("http")) {
|
||||||
|
errMsg =
|
||||||
|
`Fetching from URL ${url} failed with error ` +
|
||||||
|
`'${error.message}'. Are your filename and path correct?`;
|
||||||
|
} else {
|
||||||
|
errMsg = `Polyscript: Access to local files
|
||||||
|
(using [[fetch]] configurations in <py-config>)
|
||||||
|
is not available when directly opening a HTML file;
|
||||||
|
you must use a webserver to serve the additional files.
|
||||||
|
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
|
||||||
|
on starting a simple webserver with Python.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
throw new FetchError(ErrorCode.FETCH_ERROR, errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that response.ok is true for 200-299 responses
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMsg = `Fetching from URL ${url} failed with error ${response.status} (${response.statusText}). Are your filename and path correct?`;
|
||||||
|
switch (response.status) {
|
||||||
|
case 404:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_NOT_FOUND_ERROR, errorMsg);
|
||||||
|
case 401:
|
||||||
|
throw new FetchError(
|
||||||
|
ErrorCode.FETCH_UNAUTHORIZED_ERROR,
|
||||||
|
errorMsg,
|
||||||
|
);
|
||||||
|
case 403:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_FORBIDDEN_ERROR, errorMsg);
|
||||||
|
case 500:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_SERVER_ERROR, errorMsg);
|
||||||
|
case 503:
|
||||||
|
throw new FetchError(
|
||||||
|
ErrorCode.FETCH_UNAVAILABLE_ERROR,
|
||||||
|
errorMsg,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_ERROR, errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
82
core/src/fs.js
Normal file
82
core/src/fs.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import IDBMap from "@webreflection/idb-map";
|
||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
import { assign } from "polyscript/exports";
|
||||||
|
import { $$ } from "basic-devtools";
|
||||||
|
|
||||||
|
const stop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⚠️ these two constants MUST be passed as `fs`
|
||||||
|
// within the worker onBeforeRunAsync hook!
|
||||||
|
export const NAMESPACE = "@pyscript.fs";
|
||||||
|
export const ERROR = "storage permissions not granted";
|
||||||
|
|
||||||
|
export const idb = new IDBMap(NAMESPACE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask a user action via dialog and returns the directory handler once granted.
|
||||||
|
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
|
||||||
|
* @returns {Promise<FileSystemDirectoryHandle>}
|
||||||
|
*/
|
||||||
|
export const getFileSystemDirectoryHandle = async (options) => {
|
||||||
|
if (!("showDirectoryPicker" in globalThis)) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error("showDirectoryPicker is not supported"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promise, resolve, reject } = withResolvers();
|
||||||
|
|
||||||
|
const how = { id: "pyscript", mode: "readwrite", ...options };
|
||||||
|
if (options.hint) how.startIn = options.hint;
|
||||||
|
|
||||||
|
const transient = async () => {
|
||||||
|
try {
|
||||||
|
/* eslint-disable */
|
||||||
|
const handler = await showDirectoryPicker(how);
|
||||||
|
/* eslint-enable */
|
||||||
|
if ((await handler.requestPermission(how)) === "granted") {
|
||||||
|
resolve(handler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch ({ message }) {
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// in case the user decided to attach the event itself
|
||||||
|
// as opposite of relying our dialog walkthrough
|
||||||
|
if (navigator.userActivation?.isActive) {
|
||||||
|
if (!(await transient())) reject(new Error(ERROR));
|
||||||
|
} else {
|
||||||
|
const dialog = assign(document.createElement("dialog"), {
|
||||||
|
className: "pyscript-fs",
|
||||||
|
innerHTML: [
|
||||||
|
"<strong>ℹ️ Persistent FileSystem</strong><hr>",
|
||||||
|
"<p><small>PyScript would like to access a local folder.</small></p>",
|
||||||
|
"<div><button title='ok'>✅ Authorize</button>",
|
||||||
|
"<button title='cancel'>❌</button></div>",
|
||||||
|
].join(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ok, cancel] = $$("button", dialog);
|
||||||
|
|
||||||
|
ok.addEventListener("click", async (event) => {
|
||||||
|
stop(event);
|
||||||
|
if (await transient()) dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
cancel.addEventListener("click", async (event) => {
|
||||||
|
stop(event);
|
||||||
|
reject(new Error(ERROR));
|
||||||
|
dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(dialog).showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
117
core/src/hooks.js
Normal file
117
core/src/hooks.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { typedSet } from "type-checked-collections";
|
||||||
|
import { dedent } from "polyscript/exports";
|
||||||
|
import toJSONCallback from "to-json-callback";
|
||||||
|
|
||||||
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
|
||||||
|
export const main = (name) => hooks.main[name];
|
||||||
|
export const worker = (name) => hooks.worker[name];
|
||||||
|
|
||||||
|
const code = (hooks, branch, key, lib) => {
|
||||||
|
hooks[key] = () => {
|
||||||
|
const arr = lib ? [lib] : [];
|
||||||
|
arr.push(...branch(key));
|
||||||
|
return arr.map(dedent).join("\n");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codeFor = (branch, type) => {
|
||||||
|
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
|
||||||
|
const hooks = {};
|
||||||
|
code(hooks, branch, `codeBeforeRun`, pylib);
|
||||||
|
code(hooks, branch, `codeBeforeRunAsync`, pylib);
|
||||||
|
code(hooks, branch, `codeAfterRun`);
|
||||||
|
code(hooks, branch, `codeAfterRunAsync`);
|
||||||
|
return hooks;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFunction = (self, name) => {
|
||||||
|
const cbs = [...worker(name)];
|
||||||
|
if (cbs.length) {
|
||||||
|
const cb = toJSONCallback(
|
||||||
|
self[`_${name}`] ||
|
||||||
|
(name.endsWith("Async")
|
||||||
|
? async (wrap, xworker, ...cbs) => {
|
||||||
|
for (const cb of cbs) await cb(wrap, xworker);
|
||||||
|
}
|
||||||
|
: (wrap, xworker, ...cbs) => {
|
||||||
|
for (const cb of cbs) cb(wrap, xworker);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const a = cbs.map(toJSONCallback).join(", ");
|
||||||
|
return Function(`return(w,x)=>(${cb})(w,x,...[${a}])`)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetFunction = typedSet({ typeof: "function" });
|
||||||
|
const SetString = typedSet({ typeof: "string" });
|
||||||
|
|
||||||
|
export const inputFailure = `
|
||||||
|
import builtins
|
||||||
|
def input(prompt=""):
|
||||||
|
raise Exception("\\n ".join([
|
||||||
|
"input() doesn't work when PyScript runs in the main thread.",
|
||||||
|
"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/"
|
||||||
|
]))
|
||||||
|
|
||||||
|
builtins.input = input
|
||||||
|
del builtins
|
||||||
|
del input
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const hooks = {
|
||||||
|
main: {
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onWorker: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onReady: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRunAsync: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRunAsync: new SetFunction(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRun: new SetString([inputFailure]),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRunAsync: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRun: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRunAsync: new SetString(),
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onReady: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRunAsync: new SetFunction([
|
||||||
|
({ interpreter }) => {
|
||||||
|
interpreter.registerJsModule("_pyscript", {
|
||||||
|
// cannot be imported from fs.js
|
||||||
|
// because this code is stringified
|
||||||
|
fs: {
|
||||||
|
ERROR: "storage permissions not granted",
|
||||||
|
NAMESPACE: "@pyscript.fs",
|
||||||
|
},
|
||||||
|
interpreter,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRunAsync: new SetFunction(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRun: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRunAsync: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRun: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRunAsync: new SetString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
26
core/src/plugins-helper.js
Normal file
26
core/src/plugins-helper.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineProperty } from "polyscript/exports";
|
||||||
|
|
||||||
|
// helper for all script[type="py"] out there
|
||||||
|
const before = (script) => {
|
||||||
|
defineProperty(document, "currentScript", {
|
||||||
|
configurable: true,
|
||||||
|
get: () => script,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const after = () => {
|
||||||
|
delete document.currentScript;
|
||||||
|
};
|
||||||
|
|
||||||
|
// common life-cycle handlers for any node
|
||||||
|
export default async (main, wrap, element, hook) => {
|
||||||
|
const isAsync = hook.endsWith("Async");
|
||||||
|
const isBefore = hook.startsWith("onBefore");
|
||||||
|
// make it possible to reach the current target node via Python
|
||||||
|
// or clean up for other scripts executing around this one
|
||||||
|
(isBefore ? before : after)(element);
|
||||||
|
for (const fn of main(hook)) {
|
||||||
|
if (isAsync) await fn(wrap, element);
|
||||||
|
else fn(wrap, element);
|
||||||
|
}
|
||||||
|
};
|
||||||
38
core/src/plugins.js
Normal file
38
core/src/plugins.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||||
|
export default {
|
||||||
|
codemirror: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/codemirror.js"
|
||||||
|
),
|
||||||
|
["deprecations-manager"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/deprecations-manager.js"
|
||||||
|
),
|
||||||
|
donkey: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/donkey.js"
|
||||||
|
),
|
||||||
|
error: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/error.js"
|
||||||
|
),
|
||||||
|
["py-editor"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-editor.js"
|
||||||
|
),
|
||||||
|
["py-game"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-game.js"
|
||||||
|
),
|
||||||
|
["py-terminal"]: () =>
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */
|
||||||
|
"./plugins/py-terminal.js"
|
||||||
|
),
|
||||||
|
};
|
||||||
31
core/src/plugins/codemirror.js
Normal file
31
core/src/plugins/codemirror.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// lazy loaded on-demand codemirror related files
|
||||||
|
export default {
|
||||||
|
get core() {
|
||||||
|
return import(/* webpackIgnore: true */ "../3rd-party/codemirror.js");
|
||||||
|
},
|
||||||
|
get state() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get python() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get language() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get view() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
get commands() {
|
||||||
|
return import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
27
core/src/plugins/deprecations-manager.js
Normal file
27
core/src/plugins/deprecations-manager.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// PyScript Derepcations Plugin
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
import { hooks } from "../core.js";
|
||||||
|
|
||||||
|
// react lazily on PyScript bootstrap
|
||||||
|
hooks.main.onReady.add(checkDeprecations);
|
||||||
|
hooks.main.onWorker.add(checkDeprecations);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that there are no scripts loading from pyscript.net/latest
|
||||||
|
*/
|
||||||
|
function checkDeprecations() {
|
||||||
|
const scripts = document.querySelectorAll("script");
|
||||||
|
for (const script of scripts) checkLoadingScriptsFromLatest(script.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if src being loaded from pyscript.net/latest and display a notification if true
|
||||||
|
* * @param {string} src
|
||||||
|
*/
|
||||||
|
function checkLoadingScriptsFromLatest(src) {
|
||||||
|
if (/\/pyscript\.net\/latest/.test(src)) {
|
||||||
|
notify(
|
||||||
|
"Loading scripts from latest is deprecated and will be removed soon. Please use a specific version instead.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
core/src/plugins/donkey.js
Normal file
121
core/src/plugins/donkey.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import addPromiseListener from "add-promise-listener";
|
||||||
|
import { assign, dedent } from "polyscript/exports";
|
||||||
|
|
||||||
|
const { stringify } = JSON;
|
||||||
|
|
||||||
|
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||||
|
|
||||||
|
const donkey = ({
|
||||||
|
type = "py",
|
||||||
|
persistent,
|
||||||
|
terminal,
|
||||||
|
config,
|
||||||
|
serviceWorker,
|
||||||
|
}) => {
|
||||||
|
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
|
||||||
|
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
|
||||||
|
|
||||||
|
const src = URL.createObjectURL(
|
||||||
|
new Blob([
|
||||||
|
[
|
||||||
|
// this array is to better minify this code once in production
|
||||||
|
"from pyscript import sync, config",
|
||||||
|
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
|
||||||
|
"__locals__ = {}",
|
||||||
|
'if config["type"] == "py":',
|
||||||
|
" import sys",
|
||||||
|
" def __error__(_):",
|
||||||
|
" info = sys.exc_info()",
|
||||||
|
" return __message__(info[0], info[1])",
|
||||||
|
"else:",
|
||||||
|
" __error__ = lambda e: __message__(e.__class__, e.value)",
|
||||||
|
"def execute(code):",
|
||||||
|
` try: return ${invoke("exec", args)};`,
|
||||||
|
" except Exception as e: print(__error__(e));",
|
||||||
|
"def evaluate(code):",
|
||||||
|
` try: return ${invoke("eval", args)};`,
|
||||||
|
" except Exception as e: print(__error__(e));",
|
||||||
|
"sync.execute = execute",
|
||||||
|
"sync.evaluate = evaluate",
|
||||||
|
].join("\n"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// create the script that exposes the code to execute or evaluate
|
||||||
|
const script = assign(document.createElement("script"), { type, src });
|
||||||
|
script.toggleAttribute("worker", true);
|
||||||
|
script.toggleAttribute("terminal", true);
|
||||||
|
if (terminal) script.setAttribute("target", terminal);
|
||||||
|
if (config) {
|
||||||
|
script.setAttribute(
|
||||||
|
"config",
|
||||||
|
typeof config === "string" ? config : stringify(config),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
|
||||||
|
|
||||||
|
return addPromiseListener(
|
||||||
|
document.body.appendChild(script),
|
||||||
|
`${type}:done`,
|
||||||
|
{ stopPropagation: true },
|
||||||
|
).then(() => {
|
||||||
|
URL.revokeObjectURL(src);
|
||||||
|
return script;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const utils = async (options) => {
|
||||||
|
const script = await donkey(options);
|
||||||
|
const { xworker, process, terminal } = script;
|
||||||
|
const { execute, evaluate } = xworker.sync;
|
||||||
|
script.remove();
|
||||||
|
return {
|
||||||
|
xworker,
|
||||||
|
process,
|
||||||
|
terminal,
|
||||||
|
execute,
|
||||||
|
evaluate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (options = {}) => {
|
||||||
|
let farmer = await utils(options);
|
||||||
|
let working = false;
|
||||||
|
const kill = () => {
|
||||||
|
if (farmer) {
|
||||||
|
farmer.xworker.terminate();
|
||||||
|
farmer.terminal.dispose();
|
||||||
|
farmer = null;
|
||||||
|
}
|
||||||
|
working = false;
|
||||||
|
};
|
||||||
|
const reload = async () => {
|
||||||
|
kill();
|
||||||
|
farmer = await utils(options);
|
||||||
|
};
|
||||||
|
const asyncTask = (method) => async (code) => {
|
||||||
|
// race condition ... a new task has been
|
||||||
|
// assigned while the previous one didn't finish
|
||||||
|
if (working) await reload();
|
||||||
|
working = true;
|
||||||
|
try {
|
||||||
|
return await farmer[method](dedent(code));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
working = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const asyncMethod = (method) => async () => {
|
||||||
|
if (working) await reload();
|
||||||
|
else farmer?.terminal[method]();
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
process: asyncTask("process"),
|
||||||
|
execute: asyncTask("execute"),
|
||||||
|
evaluate: asyncTask("evaluate"),
|
||||||
|
clear: asyncMethod("clear"),
|
||||||
|
reset: asyncMethod("reset"),
|
||||||
|
kill,
|
||||||
|
};
|
||||||
|
};
|
||||||
56
core/src/plugins/error.js
Normal file
56
core/src/plugins/error.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// PyScript Error Plugin
|
||||||
|
import { buffered } from "polyscript/exports";
|
||||||
|
import { hooks } from "../core.js";
|
||||||
|
|
||||||
|
let dontBotherDOM = false;
|
||||||
|
export function notOnDOM() {
|
||||||
|
dontBotherDOM = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks.main.onReady.add(function override(pyScript) {
|
||||||
|
// be sure this override happens only once
|
||||||
|
hooks.main.onReady.delete(override);
|
||||||
|
|
||||||
|
// trap generic `stderr` to propagate to it regardless
|
||||||
|
const { stderr } = pyScript.io;
|
||||||
|
|
||||||
|
const cb = (error, ...rest) => {
|
||||||
|
notify(error.message || error);
|
||||||
|
// let other plugins or stderr hook, if any, do the rest
|
||||||
|
return stderr(error, ...rest);
|
||||||
|
};
|
||||||
|
|
||||||
|
// override it with our own logic
|
||||||
|
pyScript.io.stderr = pyScript.type === "py" ? cb : buffered(cb);
|
||||||
|
|
||||||
|
// be sure uncaught Python errors are also visible
|
||||||
|
addEventListener("error", ({ message }) => {
|
||||||
|
if (message.startsWith("Uncaught PythonError")) notify(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error hook utilities
|
||||||
|
|
||||||
|
// Custom function to show notifications
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a banner to the top of the page, notifying the user of an error
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
export function notify(message) {
|
||||||
|
if (dontBotherDOM) return;
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "py-error";
|
||||||
|
div.textContent = message;
|
||||||
|
div.style.cssText = `
|
||||||
|
border: 1px solid red;
|
||||||
|
background: #ffdddd;
|
||||||
|
color: black;
|
||||||
|
font-family: courier, monospace;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
`;
|
||||||
|
document.body.append(div);
|
||||||
|
}
|
||||||
467
core/src/plugins/py-editor.js
Normal file
467
core/src/plugins/py-editor.js
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
// PyScript py-editor plugin
|
||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||||
|
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
import codemirror from "./codemirror.js";
|
||||||
|
|
||||||
|
const RUN_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,12a1,1,0,0,1-.55.89l-10,5A1,1,0,0,1,8,18a1,1,0,0,1-.53-.15A1,1,0,0,1,7,17V7a1,1,0,0,1,1.45-.89l10,5A1,1,0,0,1,19,12Z" fill="#464646"/></svg>`;
|
||||||
|
const STOP_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7h10v10H7z" style="fill:#464646;stroke:#464646;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"/></svg>`;
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
const getID = (type) => `${type}-editor-${id++}`;
|
||||||
|
|
||||||
|
const envs = new Map();
|
||||||
|
const configs = new Map();
|
||||||
|
const editors = new WeakMap();
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
worker: {
|
||||||
|
codeBeforeRun: () => stdlib,
|
||||||
|
// works on both Pyodide and MicroPython
|
||||||
|
onReady: ({ runAsync, io }, { sync }) => {
|
||||||
|
io.stdout = io.buffered(sync.write);
|
||||||
|
io.stderr = io.buffered(sync.writeErr);
|
||||||
|
sync.revoke();
|
||||||
|
sync.runAsync = runAsync;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (config, result) => {
|
||||||
|
if (typeof result === "boolean") throw `Invalid source: ${config}`;
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelatedScript = (target, type) => {
|
||||||
|
const editor = target.closest(`.${type}-editor-box`);
|
||||||
|
return editor?.parentNode?.previousElementSibling;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function execute({ currentTarget }) {
|
||||||
|
const { env, pySrc, outDiv } = this;
|
||||||
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
|
if (hasRunButton) {
|
||||||
|
currentTarget.classList.add("running");
|
||||||
|
currentTarget.innerHTML = STOP_BUTTON;
|
||||||
|
outDiv.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envs.has(env)) {
|
||||||
|
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||||
|
const details = {
|
||||||
|
type: this.interpreter,
|
||||||
|
serviceWorker: this.serviceWorker,
|
||||||
|
};
|
||||||
|
const { config } = this;
|
||||||
|
if (config) {
|
||||||
|
// verify that config can be parsed and used
|
||||||
|
try {
|
||||||
|
details.configURL = relative_url(config);
|
||||||
|
if (config.endsWith(".toml")) {
|
||||||
|
const [{ parse }, toml] = await Promise.all([
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||||
|
),
|
||||||
|
fetch(config).then((r) => r.ok && r.text()),
|
||||||
|
]);
|
||||||
|
details.config = parse(validate(config, toml));
|
||||||
|
} else if (config.endsWith(".json")) {
|
||||||
|
const json = await fetch(config).then(
|
||||||
|
(r) => r.ok && r.json(),
|
||||||
|
);
|
||||||
|
details.config = validate(config, json);
|
||||||
|
} else {
|
||||||
|
details.configURL = relative_url("./config.txt");
|
||||||
|
details.config = JSON.parse(config);
|
||||||
|
}
|
||||||
|
details.version = offline_interpreter(details.config);
|
||||||
|
} catch (error) {
|
||||||
|
notify(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
details.config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||||
|
|
||||||
|
// expose xworker like in terminal or other workers to allow
|
||||||
|
// creation and destruction of editors on the fly
|
||||||
|
if (hasRunButton) {
|
||||||
|
for (const type of TYPES.keys()) {
|
||||||
|
const script = getRelatedScript(currentTarget, type);
|
||||||
|
if (script) {
|
||||||
|
defineProperties(script, { xworker: { value: xworker } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sync } = xworker;
|
||||||
|
const { promise, resolve } = withResolvers();
|
||||||
|
envs.set(env, promise);
|
||||||
|
sync.revoke = () => {
|
||||||
|
URL.revokeObjectURL(srcLink);
|
||||||
|
resolve(xworker);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the env then set the target div
|
||||||
|
// before executing the current code
|
||||||
|
return envs.get(env).then((xworker) => {
|
||||||
|
xworker.onerror = ({ error }) => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
outDiv.insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
`<span style='color:red'>${
|
||||||
|
error.message || error
|
||||||
|
}</span>\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enable = () => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
currentTarget.classList.remove("running");
|
||||||
|
currentTarget.innerHTML = RUN_BUTTON;
|
||||||
|
const { previousElementSibling } =
|
||||||
|
currentTarget.closest("[data-env]").parentElement;
|
||||||
|
previousElementSibling?.dispatchEvent(
|
||||||
|
new Event("py-editor:done", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const { sync } = xworker;
|
||||||
|
sync.write = (str) => {
|
||||||
|
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||||
|
else console.log(str);
|
||||||
|
};
|
||||||
|
sync.writeErr = (str) => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
outDiv.insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
`<span style='color:red'>${str}</span>\n`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notify(str);
|
||||||
|
console.error(str);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sync.runAsync(pySrc).then(enable, enable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeRunButton = (handler, type) => {
|
||||||
|
const runButton = document.createElement("button");
|
||||||
|
runButton.className = `absolute ${type}-editor-run-button`;
|
||||||
|
runButton.innerHTML = RUN_BUTTON;
|
||||||
|
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||||
|
runButton.addEventListener("click", async (event) => {
|
||||||
|
if (
|
||||||
|
runButton.classList.contains("running") &&
|
||||||
|
confirm("Stop evaluating this code?")
|
||||||
|
) {
|
||||||
|
const script = getRelatedScript(runButton, type);
|
||||||
|
if (script) {
|
||||||
|
const editor = editors.get(script);
|
||||||
|
const content = editor.state.doc.toString();
|
||||||
|
const clone = script.cloneNode(true);
|
||||||
|
clone.type = `${type}-editor`;
|
||||||
|
clone.textContent = content;
|
||||||
|
script.xworker.terminate();
|
||||||
|
script.nextElementSibling.remove();
|
||||||
|
script.replaceWith(clone);
|
||||||
|
editors.delete(script);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runButton.blur();
|
||||||
|
await handler.handleEvent(event);
|
||||||
|
});
|
||||||
|
return runButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeEditorDiv = (handler, type) => {
|
||||||
|
const editorDiv = document.createElement("div");
|
||||||
|
editorDiv.className = `${type}-editor-input`;
|
||||||
|
editorDiv.setAttribute("aria-label", "Python Script Area");
|
||||||
|
|
||||||
|
const runButton = makeRunButton(handler, type);
|
||||||
|
const editorShadowContainer = document.createElement("div");
|
||||||
|
|
||||||
|
// avoid outer elements intercepting key events (reveal as example)
|
||||||
|
editorShadowContainer.addEventListener("keydown", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
editorDiv.append(runButton, editorShadowContainer);
|
||||||
|
|
||||||
|
return editorDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeOutDiv = (type) => {
|
||||||
|
const outDiv = document.createElement("div");
|
||||||
|
outDiv.className = `${type}-editor-output`;
|
||||||
|
outDiv.id = `${getID(type)}-output`;
|
||||||
|
return outDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeBoxDiv = (handler, type) => {
|
||||||
|
const boxDiv = document.createElement("div");
|
||||||
|
boxDiv.className = `${type}-editor-box`;
|
||||||
|
|
||||||
|
const editorDiv = makeEditorDiv(handler, type);
|
||||||
|
const outDiv = makeOutDiv(type);
|
||||||
|
boxDiv.append(editorDiv, outDiv);
|
||||||
|
|
||||||
|
return [boxDiv, outDiv, editorDiv.querySelector("button")];
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async (script, type, interpreter) => {
|
||||||
|
const [
|
||||||
|
{ basicSetup, EditorView },
|
||||||
|
{ Compartment },
|
||||||
|
{ python },
|
||||||
|
{ indentUnit },
|
||||||
|
{ keymap },
|
||||||
|
{ defaultKeymap, indentWithTab },
|
||||||
|
] = await Promise.all([
|
||||||
|
codemirror.core,
|
||||||
|
codemirror.state,
|
||||||
|
codemirror.python,
|
||||||
|
codemirror.language,
|
||||||
|
codemirror.view,
|
||||||
|
codemirror.commands,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let isSetup = script.hasAttribute("setup");
|
||||||
|
const hasConfig = script.hasAttribute("config");
|
||||||
|
const serviceWorker = script.getAttribute("service-worker");
|
||||||
|
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||||
|
|
||||||
|
// helps preventing too lazy ServiceWorker initialization on button run
|
||||||
|
if (serviceWorker) {
|
||||||
|
new XWorker("data:application/javascript,postMessage(0)", {
|
||||||
|
type: "dummy",
|
||||||
|
serviceWorker,
|
||||||
|
}).onmessage = ({ target }) => target.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasConfig && configs.has(env)) {
|
||||||
|
throw new SyntaxError(
|
||||||
|
configs.get(env)
|
||||||
|
? `duplicated config for env: ${env}`
|
||||||
|
: `unable to add a config to the env: ${env}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
configs.set(env, hasConfig);
|
||||||
|
|
||||||
|
let source = script.textContent;
|
||||||
|
|
||||||
|
// verify the src points to a valid file that can be parsed
|
||||||
|
const { src } = script;
|
||||||
|
if (src) {
|
||||||
|
try {
|
||||||
|
source = validate(
|
||||||
|
src,
|
||||||
|
await fetch(src).then((b) => b.ok && b.text()),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
notify(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
// allow the listener to be overridden at distance
|
||||||
|
handleEvent: execute,
|
||||||
|
serviceWorker,
|
||||||
|
interpreter,
|
||||||
|
env,
|
||||||
|
config: hasConfig && script.getAttribute("config"),
|
||||||
|
get pySrc() {
|
||||||
|
return isSetup ? source : editor.state.doc.toString();
|
||||||
|
},
|
||||||
|
get outDiv() {
|
||||||
|
return isSetup ? null : outDiv;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let target;
|
||||||
|
defineProperties(script, {
|
||||||
|
target: { get: () => target },
|
||||||
|
handleEvent: {
|
||||||
|
get: () => context.handleEvent,
|
||||||
|
set: (callback) => {
|
||||||
|
// do not bother with logic if it was set back as its original handler
|
||||||
|
if (callback === execute) context.handleEvent = execute;
|
||||||
|
// in every other case be sure that if the listener override returned
|
||||||
|
// `false` nothing happens, otherwise keep doing what it always did
|
||||||
|
else {
|
||||||
|
context.handleEvent = async (event) => {
|
||||||
|
// trap the currentTarget ASAP (if any)
|
||||||
|
// otherwise it gets lost asynchronously
|
||||||
|
const { currentTarget } = event;
|
||||||
|
// augment a code snapshot before invoking the override
|
||||||
|
defineProperties(event, {
|
||||||
|
code: { value: context.pySrc },
|
||||||
|
});
|
||||||
|
// avoid executing the default handler if the override returned `false`
|
||||||
|
if ((await callback(event)) !== false)
|
||||||
|
await execute.call(context, { currentTarget });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
get: () => context.pySrc,
|
||||||
|
set: (insert) => {
|
||||||
|
if (isSetup) return;
|
||||||
|
editor.update([
|
||||||
|
editor.state.update({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editor.state.doc.length,
|
||||||
|
insert,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
/**
|
||||||
|
* Simulate a setup node overriding the source to evaluate.
|
||||||
|
* @param {string} code the Python code to evaluate.
|
||||||
|
* @param {boolean} asRunButtonAction invoke the `Run` button handler.
|
||||||
|
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||||
|
*/
|
||||||
|
value(code, asRunButtonAction = false) {
|
||||||
|
if (asRunButtonAction) return listener();
|
||||||
|
const wasSetup = isSetup;
|
||||||
|
const wasSource = source;
|
||||||
|
isSetup = true;
|
||||||
|
source = code;
|
||||||
|
const restore = () => {
|
||||||
|
isSetup = wasSetup;
|
||||||
|
source = wasSource;
|
||||||
|
};
|
||||||
|
return context
|
||||||
|
.handleEvent({ currentTarget: null })
|
||||||
|
.then(restore, restore);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifyEditor = () => {
|
||||||
|
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSetup) {
|
||||||
|
await context.handleEvent({ currentTarget: null });
|
||||||
|
notifyEditor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = script.getAttribute("target");
|
||||||
|
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement(`${type}-editor`);
|
||||||
|
target.style.display = "block";
|
||||||
|
script.after(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.id) target.id = getID(type);
|
||||||
|
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
||||||
|
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
||||||
|
|
||||||
|
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
||||||
|
const [boxDiv, outDiv, runButton] = makeBoxDiv(context, type);
|
||||||
|
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
|
||||||
|
|
||||||
|
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
|
||||||
|
const parent = inputChild.attachShadow({ mode: "open" });
|
||||||
|
// avoid inheriting styles from the outer component
|
||||||
|
parent.innerHTML = `<style> :host { all: initial; }</style>`;
|
||||||
|
|
||||||
|
target.appendChild(boxDiv);
|
||||||
|
|
||||||
|
const doc = dedent(script.textContent).trim();
|
||||||
|
|
||||||
|
// preserve user indentation, if any
|
||||||
|
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
|
||||||
|
|
||||||
|
const listener = () => runButton.click();
|
||||||
|
const editor = new EditorView({
|
||||||
|
extensions: [
|
||||||
|
indentUnit.of(indentation),
|
||||||
|
new Compartment().of(python()),
|
||||||
|
keymap.of([
|
||||||
|
...defaultKeymap,
|
||||||
|
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
||||||
|
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
||||||
|
{ key: "Shift-Enter", run: listener, preventDefault: true },
|
||||||
|
// @see https://codemirror.net/examples/tab/
|
||||||
|
indentWithTab,
|
||||||
|
]),
|
||||||
|
basicSetup,
|
||||||
|
],
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||||
|
parent,
|
||||||
|
doc,
|
||||||
|
});
|
||||||
|
|
||||||
|
editors.set(script, editor);
|
||||||
|
editor.focus();
|
||||||
|
notifyEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// avoid too greedy MutationObserver operations at distance
|
||||||
|
let timeout = 0;
|
||||||
|
|
||||||
|
// avoid delayed initialization
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
// reset interval value then check for new scripts
|
||||||
|
const resetTimeout = () => {
|
||||||
|
timeout = 0;
|
||||||
|
pyEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||||
|
const pyEditor = () => {
|
||||||
|
if (timeout) return;
|
||||||
|
timeout = setTimeout(resetTimeout, 250);
|
||||||
|
for (const [type, interpreter] of TYPES) {
|
||||||
|
const selector = `script[type="${type}-editor"]`;
|
||||||
|
for (const script of document.querySelectorAll(selector)) {
|
||||||
|
// avoid any further bootstrap by changing the type as active
|
||||||
|
script.type += "-active";
|
||||||
|
// don't await in here or multiple calls might happen
|
||||||
|
// while the first script is being initialized
|
||||||
|
queue = queue.then(() => init(script, type, interpreter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queue;
|
||||||
|
};
|
||||||
|
|
||||||
|
new MutationObserver(pyEditor).observe(document, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// try to check the current document ASAP
|
||||||
|
export default pyEditor();
|
||||||
112
core/src/plugins/py-game.js
Normal file
112
core/src/plugins/py-game.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
dedent,
|
||||||
|
define,
|
||||||
|
createProgress,
|
||||||
|
loadProgress,
|
||||||
|
} from "polyscript/exports";
|
||||||
|
|
||||||
|
import { stdlib } from "../core.js";
|
||||||
|
import { configDetails } from "../config.js";
|
||||||
|
import { getText } from "../fetch.js";
|
||||||
|
|
||||||
|
const progress = createProgress("py-game");
|
||||||
|
|
||||||
|
const inputPatch = `
|
||||||
|
import builtins
|
||||||
|
def input(prompt=""):
|
||||||
|
import js
|
||||||
|
return js.prompt(prompt)
|
||||||
|
|
||||||
|
builtins.input = input
|
||||||
|
del builtins
|
||||||
|
del input
|
||||||
|
`;
|
||||||
|
|
||||||
|
let toBeWarned = true;
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
main: {
|
||||||
|
onReady: async (wrap, script) => {
|
||||||
|
if (toBeWarned) {
|
||||||
|
toBeWarned = false;
|
||||||
|
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
if (script.hasAttribute("config")) {
|
||||||
|
const value = script.getAttribute("config");
|
||||||
|
const { json, toml, text, url } = await configDetails(value);
|
||||||
|
if (json) config = JSON.parse(text);
|
||||||
|
else if (toml) {
|
||||||
|
const { parse } = await import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/toml.js"
|
||||||
|
);
|
||||||
|
config = parse(text);
|
||||||
|
}
|
||||||
|
if (config.packages) {
|
||||||
|
await wrap.interpreter.loadPackage("micropip");
|
||||||
|
const micropip = wrap.interpreter.pyimport("micropip");
|
||||||
|
await micropip.install(config.packages, {
|
||||||
|
keep_going: true,
|
||||||
|
});
|
||||||
|
micropip.destroy();
|
||||||
|
}
|
||||||
|
await loadProgress(
|
||||||
|
"py-game",
|
||||||
|
progress,
|
||||||
|
wrap.interpreter,
|
||||||
|
config,
|
||||||
|
url ? new URL(url, location.href).href : location.href,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap.interpreter.registerJsModule("_pyscript", {
|
||||||
|
PyWorker() {
|
||||||
|
throw new Error(
|
||||||
|
"Unable to use PyWorker in py-game scripts",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
js_import: (...urls) =>
|
||||||
|
Promise.all(urls.map((url) => import(url))),
|
||||||
|
get target() {
|
||||||
|
return script.id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrap.interpreter.runPythonAsync(stdlib);
|
||||||
|
wrap.interpreter.runPython(inputPatch);
|
||||||
|
|
||||||
|
let code = dedent(script.textContent);
|
||||||
|
if (script.src) code = await fetch(script.src).then(getText);
|
||||||
|
|
||||||
|
const target = script.getAttribute("target") || "canvas";
|
||||||
|
const canvas = document.getElementById(target);
|
||||||
|
wrap.interpreter.canvas.setCanvas2D(canvas);
|
||||||
|
|
||||||
|
// allow 3rd party to hook themselves right before
|
||||||
|
// the code gets executed
|
||||||
|
const event = new CustomEvent("py-game", {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
detail: {
|
||||||
|
canvas,
|
||||||
|
code,
|
||||||
|
config,
|
||||||
|
wrap,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
// run only if the default was not prevented
|
||||||
|
if (!event.defaultPrevented)
|
||||||
|
await wrap.interpreter.runPythonAsync(code);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
define("py-game", {
|
||||||
|
config: { packages: ["pygame-ce"] },
|
||||||
|
configURL: new URL("./config.txt", location.href).href,
|
||||||
|
interpreter: "pyodide",
|
||||||
|
env: "py-game",
|
||||||
|
hooks,
|
||||||
|
});
|
||||||
60
core/src/plugins/py-terminal.js
Normal file
60
core/src/plugins/py-terminal.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// PyScript py-terminal plugin
|
||||||
|
import { TYPES, relative_url } from "../core.js";
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
import { customObserver } from "polyscript/exports";
|
||||||
|
|
||||||
|
// will contain all valid selectors
|
||||||
|
const SELECTORS = [];
|
||||||
|
|
||||||
|
// avoid processing same elements twice
|
||||||
|
const processed = new WeakSet();
|
||||||
|
|
||||||
|
// show the error on main and
|
||||||
|
// stops the module from keep executing
|
||||||
|
const notifyAndThrow = (message) => {
|
||||||
|
notify(message);
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||||
|
|
||||||
|
let addStyle = true;
|
||||||
|
|
||||||
|
for (const type of TYPES.keys()) {
|
||||||
|
const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
|
||||||
|
SELECTORS.push(selector);
|
||||||
|
customObserver.set(selector, async (element) => {
|
||||||
|
// we currently support only one terminal on main as in "classic"
|
||||||
|
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
||||||
|
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||||
|
notifyAndThrow("You can use at most 1 main terminal");
|
||||||
|
|
||||||
|
// import styles lazily
|
||||||
|
if (addStyle) {
|
||||||
|
addStyle = false;
|
||||||
|
document.head.append(
|
||||||
|
Object.assign(document.createElement("link"), {
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: relative_url("./xterm.css", import.meta.url),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processed.has(element)) return;
|
||||||
|
processed.add(element);
|
||||||
|
|
||||||
|
const bootstrap = (module) => module.default(element);
|
||||||
|
|
||||||
|
// we can't be smart with template literals for the dynamic import
|
||||||
|
// or bundlers are incapable of producing multiple files around
|
||||||
|
if (type === "mpy") {
|
||||||
|
await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
|
||||||
|
bootstrap,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
|
||||||
|
bootstrap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
256
core/src/plugins/py-terminal/mpy.js
Normal file
256
core/src/plugins/py-terminal/mpy.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
// PyScript pyodide terminal plugin
|
||||||
|
import withResolvers from "@webreflection/utils/with-resolvers";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
import { hooks, inputFailure } from "../../core.js";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "mpy" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
|
||||||
|
|
||||||
|
interpreter.registerJsModule("_pyscript_input", {
|
||||||
|
input: pyterminal_read,
|
||||||
|
});
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from _pyscript_input import input",
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) pyterminal_write(missingReturn);
|
||||||
|
pyterminal_write(buffer);
|
||||||
|
};
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
sync.pyterminal_stream_write = () => {};
|
||||||
|
|
||||||
|
// tiny shim of the code module with only interact
|
||||||
|
// to bootstrap a REPL like environment
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer) interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const writer = encoder.writable.getWriter();
|
||||||
|
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pyterminal_ready();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const terminalOptions = {
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = () => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...terminalOptions,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
await stream.write(`${line}\r`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const { sync } = xworker;
|
||||||
|
|
||||||
|
// handle the read mode on input
|
||||||
|
let promisedChunks = null;
|
||||||
|
let readChunks = "";
|
||||||
|
|
||||||
|
sync.is_pyterminal = () => true;
|
||||||
|
|
||||||
|
// put the terminal in a read-only state
|
||||||
|
// frees the worker on \r
|
||||||
|
sync.pyterminal_read = (buffer) => {
|
||||||
|
terminal.write(buffer);
|
||||||
|
promisedChunks = withResolvers();
|
||||||
|
return promisedChunks.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// write if not reading input
|
||||||
|
sync.pyterminal_write = (buffer) => {
|
||||||
|
if (!promisedChunks) terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// add the onData terminal listener which forwards to the worker
|
||||||
|
// everything typed in a queued char-by-char way
|
||||||
|
sync.pyterminal_ready = () => {
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
stream = {
|
||||||
|
write: (buffer) =>
|
||||||
|
(queue = queue.then(() =>
|
||||||
|
sync.pyterminal_stream_write(buffer),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
terminal.onData((buffer) => {
|
||||||
|
if (promisedChunks) {
|
||||||
|
// handle backspace on input
|
||||||
|
if (buffer === "\x7f") {
|
||||||
|
// avoid over-greedy backspace
|
||||||
|
if (readChunks.length) {
|
||||||
|
readChunks = readChunks.slice(0, -1);
|
||||||
|
// override previous char position
|
||||||
|
// put an empty space to clear the char
|
||||||
|
// move back position again
|
||||||
|
buffer = "\b \b";
|
||||||
|
} else buffer = "";
|
||||||
|
} else readChunks += buffer;
|
||||||
|
if (buffer) {
|
||||||
|
terminal.write(buffer);
|
||||||
|
if (readChunks.endsWith("\r")) {
|
||||||
|
terminal.write("\n");
|
||||||
|
promisedChunks.resolve(readChunks.slice(0, -1));
|
||||||
|
promisedChunks = null;
|
||||||
|
readChunks = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stream.write(buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
} else {
|
||||||
|
// ⚠️ In an ideal world the inputFailure should never be used on main.
|
||||||
|
// However, Pyodide still can't compete with MicroPython REPL mode
|
||||||
|
// so while it's OK to keep that entry on main as default, we need
|
||||||
|
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
|
||||||
|
// also throw whenever an `input(...)` is required / digited.
|
||||||
|
hooks.main.codeBeforeRun.delete(inputFailure);
|
||||||
|
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "mpy") return;
|
||||||
|
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) terminal.write(missingReturn);
|
||||||
|
terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// expose the __terminal__ one-off reference
|
||||||
|
globalThis.__py_terminal__ = terminal;
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from js import prompt as input",
|
||||||
|
"from js import __py_terminal__ as __terminal__",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
// NOTE: this is NOT the same as the one within
|
||||||
|
// the onWorkerReady callback!
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer)
|
||||||
|
interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
stream = encoder.writable.getWriter();
|
||||||
|
terminal.onData((buffer) => stream.write(buffer));
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
192
core/src/plugins/py-terminal/py.js
Normal file
192
core/src/plugins/py-terminal/py.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// PyScript py-terminal plugin
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
import { hooks } from "../../core.js";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "py" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const { pyterminal_read, pyterminal_write } = sync;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
pyterminal_write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => pyterminal_read(data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
|
||||||
|
await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const readline = new Readline();
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = (options) => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(readline);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
terminal.paste(`${line}`);
|
||||||
|
terminal.write("\r\n");
|
||||||
|
do {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 0),
|
||||||
|
);
|
||||||
|
} while (!readline.activeRead?.resolve);
|
||||||
|
readline.activeRead.resolve(line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
init({
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
xworker.sync.is_pyterminal = () => true;
|
||||||
|
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||||
|
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
|
||||||
|
// @see https://github.com/pyscript/pyscript/issues/2246
|
||||||
|
const patchInput = [
|
||||||
|
"import builtins as _b",
|
||||||
|
"from pyscript import sync as _s",
|
||||||
|
"_b.input = _s.pyterminal_read",
|
||||||
|
"del _b",
|
||||||
|
"del _s",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
hooks.worker.codeBeforeRun.add(patchInput);
|
||||||
|
hooks.worker.codeBeforeRunAsync.add(patchInput);
|
||||||
|
} else {
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "py") return;
|
||||||
|
|
||||||
|
console.warn("py-terminal is read only on main thread");
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
// on main, it's easy to trash and clean the current terminal
|
||||||
|
globalThis.__py_terminal__ = init({
|
||||||
|
disableStdin: true,
|
||||||
|
cursorBlink: false,
|
||||||
|
cursorStyle: "underline",
|
||||||
|
});
|
||||||
|
run("from js import __py_terminal__ as __terminal__");
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
readline.write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
readline.write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => readline.read(data),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
1
core/src/service-worker.js
Normal file
1
core/src/service-worker.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "polyscript/service-worker";
|
||||||
70
core/src/stdlib.js
Normal file
70
core/src/stdlib.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Create through Python the pyscript module through
|
||||||
|
* the artifact generated at build time.
|
||||||
|
* This the returned value is a string that must be used
|
||||||
|
* either before a worker execute code or when the module
|
||||||
|
* is registered on the main thread.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import pyscript from "./stdlib/pyscript.js";
|
||||||
|
|
||||||
|
class Ignore extends Array {
|
||||||
|
#add = false;
|
||||||
|
#paths;
|
||||||
|
#array;
|
||||||
|
constructor(array, ...paths) {
|
||||||
|
super();
|
||||||
|
this.#array = array;
|
||||||
|
this.#paths = paths;
|
||||||
|
}
|
||||||
|
push(...values) {
|
||||||
|
if (this.#add) super.push(...values);
|
||||||
|
return this.#array.push(...values);
|
||||||
|
}
|
||||||
|
path(path) {
|
||||||
|
for (const _path of this.#paths) {
|
||||||
|
// bails out at the first `true` value
|
||||||
|
if ((this.#add = path.startsWith(_path))) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entries } = Object;
|
||||||
|
|
||||||
|
const python = [
|
||||||
|
"import os as _os",
|
||||||
|
"from pathlib import Path as _Path",
|
||||||
|
"_path = None",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ignore = new Ignore(python, "-");
|
||||||
|
|
||||||
|
const write = (base, literal) => {
|
||||||
|
for (const [key, value] of entries(literal)) {
|
||||||
|
ignore.path(`${base}/${key}`);
|
||||||
|
ignore.push(`_path = _Path("${base}/${key}")`);
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const code = JSON.stringify(value);
|
||||||
|
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
|
||||||
|
} else {
|
||||||
|
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
||||||
|
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
|
||||||
|
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
|
||||||
|
write(`${base}/${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
write(".", pyscript);
|
||||||
|
|
||||||
|
// in order to fix js.document in the Worker case
|
||||||
|
// we need to bootstrap pyscript module ASAP
|
||||||
|
python.push("import pyscript as _pyscript");
|
||||||
|
|
||||||
|
python.push(
|
||||||
|
...["_Path", "_path", "_os", "_pyscript"].map((ref) => `del ${ref}`),
|
||||||
|
);
|
||||||
|
python.push("\n");
|
||||||
|
|
||||||
|
export const stdlib = python.join("\n");
|
||||||
|
export const optional = ignore.join("\n");
|
||||||
19
core/src/stdlib/pyscript.js
Normal file
19
core/src/stdlib/pyscript.js
Normal file
File diff suppressed because one or more lines are too long
51
core/src/stdlib/pyscript/__init__.py
Normal file
51
core/src/stdlib/pyscript/__init__.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Some notes about the naming conventions and the relationship between various
|
||||||
|
# similar-but-different names.
|
||||||
|
#
|
||||||
|
# import pyscript
|
||||||
|
# this package contains the main user-facing API offered by pyscript. All
|
||||||
|
# the names which are supposed be used by end users should be made
|
||||||
|
# available in pyscript/__init__.py (i.e., this file)
|
||||||
|
#
|
||||||
|
# import _pyscript
|
||||||
|
# this is an internal module implemented in JS. It is used internally by
|
||||||
|
# the pyscript package, end users should not use it directly. For its
|
||||||
|
# implementation, grep for `interpreter.registerJsModule("_pyscript",
|
||||||
|
# ...)` in core.js
|
||||||
|
#
|
||||||
|
# import js
|
||||||
|
# this is the JS globalThis, as exported by pyodide and/or micropython's
|
||||||
|
# FFIs. As such, it contains different things in the main thread or in a
|
||||||
|
# worker.
|
||||||
|
#
|
||||||
|
# import pyscript.magic_js
|
||||||
|
# this submodule abstracts away some of the differences between the main
|
||||||
|
# thread and the worker. In particular, it defines `window` and `document`
|
||||||
|
# in such a way that these names work in both cases: in the main thread,
|
||||||
|
# they are the "real" objects, in the worker they are proxies which work
|
||||||
|
# thanks to coincident.
|
||||||
|
#
|
||||||
|
# from pyscript import window, document
|
||||||
|
# these are just the window and document objects as defined by
|
||||||
|
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
||||||
|
# as it works transparently in both the main thread and worker cases.
|
||||||
|
|
||||||
|
from polyscript import lazy_py_modules as py_import
|
||||||
|
from pyscript.magic_js import (
|
||||||
|
RUNNING_IN_WORKER,
|
||||||
|
PyWorker,
|
||||||
|
config,
|
||||||
|
current_target,
|
||||||
|
document,
|
||||||
|
js_import,
|
||||||
|
js_modules,
|
||||||
|
sync,
|
||||||
|
window,
|
||||||
|
)
|
||||||
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
|
from pyscript.storage import Storage, storage
|
||||||
|
from pyscript.websocket import WebSocket
|
||||||
|
from pyscript.events import when, Event
|
||||||
|
|
||||||
|
if not RUNNING_IN_WORKER:
|
||||||
|
from pyscript.workers import create_named_worker, workers
|
||||||
178
core/src/stdlib/pyscript/display.py
Normal file
178
core/src/stdlib/pyscript/display.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import base64
|
||||||
|
import html
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
|
||||||
|
from pyscript.magic_js import current_target, document, window
|
||||||
|
|
||||||
|
_MIME_METHODS = {
|
||||||
|
"savefig": "image/png",
|
||||||
|
"_repr_javascript_": "application/javascript",
|
||||||
|
"_repr_json_": "application/json",
|
||||||
|
"_repr_latex": "text/latex",
|
||||||
|
"_repr_png_": "image/png",
|
||||||
|
"_repr_jpeg_": "image/jpeg",
|
||||||
|
"_repr_pdf_": "application/pdf",
|
||||||
|
"_repr_svg_": "image/svg+xml",
|
||||||
|
"_repr_markdown_": "text/markdown",
|
||||||
|
"_repr_html_": "text/html",
|
||||||
|
"__repr__": "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _render_image(mime, value, meta):
|
||||||
|
# If the image value is using bytes we should convert it to base64
|
||||||
|
# otherwise it will return raw bytes and the browser will not be able to
|
||||||
|
# render it.
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = base64.b64encode(value).decode("utf-8")
|
||||||
|
|
||||||
|
# This is the pattern of base64 strings
|
||||||
|
base64_pattern = re.compile(
|
||||||
|
r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
|
||||||
|
)
|
||||||
|
# If value doesn't match the base64 pattern we should encode it to base64
|
||||||
|
if len(value) > 0 and not base64_pattern.match(value):
|
||||||
|
value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
data = f"data:{mime};charset=utf-8;base64,{value}"
|
||||||
|
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
|
||||||
|
return f'<img src="{data}" {attrs}></img>'
|
||||||
|
|
||||||
|
|
||||||
|
def _identity(value, meta):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
_MIME_RENDERERS = {
|
||||||
|
"text/plain": html.escape,
|
||||||
|
"text/html": _identity,
|
||||||
|
"image/png": lambda value, meta: _render_image("image/png", value, meta),
|
||||||
|
"image/jpeg": lambda value, meta: _render_image("image/jpeg", value, meta),
|
||||||
|
"image/svg+xml": _identity,
|
||||||
|
"application/json": _identity,
|
||||||
|
"application/javascript": lambda value, meta: f"<script>{value}<\\/script>",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HTML:
|
||||||
|
"""
|
||||||
|
Wrap a string so that display() can render it as plain HTML
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, html):
|
||||||
|
self._html = html
|
||||||
|
|
||||||
|
def _repr_html_(self):
|
||||||
|
return self._html
|
||||||
|
|
||||||
|
|
||||||
|
def _eval_formatter(obj, print_method):
|
||||||
|
"""
|
||||||
|
Evaluates a formatter method.
|
||||||
|
"""
|
||||||
|
if print_method == "__repr__":
|
||||||
|
return repr(obj)
|
||||||
|
if hasattr(obj, print_method):
|
||||||
|
if print_method == "savefig":
|
||||||
|
buf = io.BytesIO()
|
||||||
|
obj.savefig(buf, format="png")
|
||||||
|
buf.seek(0)
|
||||||
|
return base64.b64encode(buf.read()).decode("utf-8")
|
||||||
|
return getattr(obj, print_method)()
|
||||||
|
if print_method == "_repr_mimebundle_":
|
||||||
|
return {}, {}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_mime(obj):
|
||||||
|
"""
|
||||||
|
Formats object using _repr_x_ methods.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return html.escape(obj), "text/plain"
|
||||||
|
|
||||||
|
mimebundle = _eval_formatter(obj, "_repr_mimebundle_")
|
||||||
|
if isinstance(mimebundle, tuple):
|
||||||
|
format_dict, _ = mimebundle
|
||||||
|
else:
|
||||||
|
format_dict = mimebundle
|
||||||
|
|
||||||
|
output, not_available = None, []
|
||||||
|
for method, mime_type in _MIME_METHODS.items():
|
||||||
|
if mime_type in format_dict:
|
||||||
|
output = format_dict[mime_type]
|
||||||
|
else:
|
||||||
|
output = _eval_formatter(obj, method)
|
||||||
|
|
||||||
|
if output is None:
|
||||||
|
continue
|
||||||
|
if mime_type not in _MIME_RENDERERS:
|
||||||
|
not_available.append(mime_type)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if output is None:
|
||||||
|
if not_available:
|
||||||
|
window.console.warn(
|
||||||
|
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||||
|
)
|
||||||
|
output = repr(output)
|
||||||
|
mime_type = "text/plain"
|
||||||
|
elif isinstance(output, tuple):
|
||||||
|
output, meta = output
|
||||||
|
else:
|
||||||
|
meta = {}
|
||||||
|
return _MIME_RENDERERS[mime_type](output, meta), mime_type
|
||||||
|
|
||||||
|
|
||||||
|
def _write(element, value, append=False):
|
||||||
|
html, mime_type = _format_mime(value)
|
||||||
|
if html == "\\n":
|
||||||
|
return
|
||||||
|
|
||||||
|
if append:
|
||||||
|
out_element = document.createElement("div")
|
||||||
|
element.append(out_element)
|
||||||
|
else:
|
||||||
|
out_element = element.lastElementChild
|
||||||
|
if out_element is None:
|
||||||
|
out_element = element
|
||||||
|
|
||||||
|
if mime_type in ("application/javascript", "text/html"):
|
||||||
|
script_element = document.createRange().createContextualFragment(html)
|
||||||
|
out_element.append(script_element)
|
||||||
|
else:
|
||||||
|
out_element.innerHTML = html
|
||||||
|
|
||||||
|
|
||||||
|
def display(*values, target=None, append=True):
|
||||||
|
if target is None:
|
||||||
|
target = current_target()
|
||||||
|
elif not isinstance(target, str):
|
||||||
|
msg = f"target must be str or None, not {target.__class__.__name__}"
|
||||||
|
raise TypeError(msg)
|
||||||
|
elif target == "":
|
||||||
|
msg = "Cannot have an empty target"
|
||||||
|
raise ValueError(msg)
|
||||||
|
elif target.startswith("#"):
|
||||||
|
# note: here target is str and not None!
|
||||||
|
# align with @when behavior
|
||||||
|
target = target[1:]
|
||||||
|
|
||||||
|
element = document.getElementById(target)
|
||||||
|
|
||||||
|
# If target cannot be found on the page, a ValueError is raised
|
||||||
|
if element is None:
|
||||||
|
msg = f"Invalid selector with id={target}. Cannot be found in the page."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
# if element is a <script type="py">, it has a 'target' attribute which
|
||||||
|
# points to the visual element holding the displayed values. In that case,
|
||||||
|
# use that.
|
||||||
|
if element.tagName == "SCRIPT" and hasattr(element, "target"):
|
||||||
|
element = element.target
|
||||||
|
|
||||||
|
for v in values:
|
||||||
|
if not append:
|
||||||
|
element.replaceChildren()
|
||||||
|
_write(element, v, append=append)
|
||||||
168
core/src/stdlib/pyscript/events.py
Normal file
168
core/src/stdlib/pyscript/events.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
from pyscript.magic_js import document
|
||||||
|
from pyscript.ffi import create_proxy
|
||||||
|
from pyscript.util import is_awaitable
|
||||||
|
from pyscript import config
|
||||||
|
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
"""
|
||||||
|
Represents something that may happen at some point in the future.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
|
def trigger(self, result):
|
||||||
|
"""
|
||||||
|
Trigger the event with a result to pass into the handlers.
|
||||||
|
"""
|
||||||
|
for listener in self._listeners:
|
||||||
|
if is_awaitable(listener):
|
||||||
|
# Use create task to avoid making this an async function.
|
||||||
|
asyncio.create_task(listener(result))
|
||||||
|
else:
|
||||||
|
listener(result)
|
||||||
|
|
||||||
|
def add_listener(self, listener):
|
||||||
|
"""
|
||||||
|
Add a callable/awaitable to listen to when this event is triggered.
|
||||||
|
"""
|
||||||
|
if is_awaitable(listener) or callable(listener):
|
||||||
|
if listener not in self._listeners:
|
||||||
|
self._listeners.append(listener)
|
||||||
|
else:
|
||||||
|
msg = "Listener must be callable or awaitable."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
def remove_listener(self, *args):
|
||||||
|
"""
|
||||||
|
Clear the specified handler functions in *args. If no handlers
|
||||||
|
provided, clear all handlers.
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
for listener in args:
|
||||||
|
self._listeners.remove(listener)
|
||||||
|
else:
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
|
|
||||||
|
def when(target, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add an event listener to the target element(s) for the specified event type.
|
||||||
|
|
||||||
|
The target can be a string representing the event type, or an Event object.
|
||||||
|
If the target is an Event object, the event listener will be added to that
|
||||||
|
object. If the target is a string, the event listener will be added to the
|
||||||
|
element(s) that match the (second) selector argument.
|
||||||
|
|
||||||
|
If a (third) handler argument is provided, it will be called when the event
|
||||||
|
is triggered; thus allowing this to be used as both a function and a
|
||||||
|
decorator.
|
||||||
|
"""
|
||||||
|
# If "when" is called as a function, try to grab the handler from the
|
||||||
|
# arguments. If there's no handler, this must be a decorator based call.
|
||||||
|
handler = None
|
||||||
|
if args and (callable(args[0]) or is_awaitable(args[0])):
|
||||||
|
handler = args[0]
|
||||||
|
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
|
||||||
|
handler = kwargs.pop("handler")
|
||||||
|
# If the target is a string, it is the "older" use of `when` where it
|
||||||
|
# represents the name of a DOM event.
|
||||||
|
if isinstance(target, str):
|
||||||
|
# Extract the selector from the arguments or keyword arguments.
|
||||||
|
selector = args[0] if args else kwargs.pop("selector")
|
||||||
|
if not selector:
|
||||||
|
msg = "No selector provided."
|
||||||
|
raise ValueError(msg)
|
||||||
|
# Grab the DOM elements to which the target event will be attached.
|
||||||
|
from pyscript.web import Element, ElementCollection
|
||||||
|
|
||||||
|
if isinstance(selector, str):
|
||||||
|
elements = document.querySelectorAll(selector)
|
||||||
|
elif isinstance(selector, Element):
|
||||||
|
elements = [selector._dom_element]
|
||||||
|
elif isinstance(selector, ElementCollection):
|
||||||
|
elements = [el._dom_element for el in selector]
|
||||||
|
else:
|
||||||
|
elements = selector if isinstance(selector, list) else [selector]
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
if config["type"] == "mpy": # Is MicroPython?
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a very ugly hack to get micropython working because
|
||||||
|
`inspect.signature` doesn't exist. It may be actually better
|
||||||
|
to not try any magic for now and raise the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
|
return await func()
|
||||||
|
raise
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
This is a very ugly hack to get micropython working because
|
||||||
|
`inspect.signature` doesn't exist. It may be actually better
|
||||||
|
to not try any magic for now and raise the error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
|
return func()
|
||||||
|
raise
|
||||||
|
|
||||||
|
else:
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
if sig.parameters:
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(event):
|
||||||
|
return await func(event)
|
||||||
|
|
||||||
|
else:
|
||||||
|
wrapper = func
|
||||||
|
else:
|
||||||
|
# Function doesn't receive events.
|
||||||
|
if is_awaitable(func):
|
||||||
|
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await func()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return func()
|
||||||
|
|
||||||
|
wrapper = wraps(func)(wrapper)
|
||||||
|
if isinstance(target, Event):
|
||||||
|
# The target is a single Event object.
|
||||||
|
target.add_listener(wrapper)
|
||||||
|
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
|
||||||
|
# The target is a list of Event objects.
|
||||||
|
for evt in target:
|
||||||
|
evt.add_listener(wrapper)
|
||||||
|
else:
|
||||||
|
# The target is a string representing an event type, and so a
|
||||||
|
# DOM element or collection of elements is found in "elements".
|
||||||
|
for el in elements:
|
||||||
|
el.addEventListener(target, create_proxy(wrapper))
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
# If "when" was called as a decorator, return the decorator function,
|
||||||
|
# otherwise just call the internal decorator function with the supplied
|
||||||
|
# handler.
|
||||||
|
return decorator(handler) if handler else decorator
|
||||||
87
core/src/stdlib/pyscript/fetch.py
Normal file
87
core/src/stdlib/pyscript/fetch.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
|
||||||
|
### wrap the response to grant Pythonic results
|
||||||
|
class _Response:
|
||||||
|
def __init__(self, response):
|
||||||
|
self._response = response
|
||||||
|
|
||||||
|
# grant access to response.ok and other fields
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._response, attr)
|
||||||
|
|
||||||
|
# exposed methods with Pythonic results
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
# works in Pyodide
|
||||||
|
if hasattr(buffer, "to_py"):
|
||||||
|
return buffer.to_py()
|
||||||
|
# shims in MicroPython
|
||||||
|
return memoryview(as_bytearray(buffer))
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
return await self._response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
return as_bytearray(buffer)
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return json.loads(await self.text())
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return await self._response.text()
|
||||||
|
|
||||||
|
|
||||||
|
### allow direct await to _Response methods
|
||||||
|
class _DirectResponse:
|
||||||
|
@staticmethod
|
||||||
|
def setup(promise, response):
|
||||||
|
promise._response = _Response(response)
|
||||||
|
return promise._response
|
||||||
|
|
||||||
|
def __init__(self, promise):
|
||||||
|
self._promise = promise
|
||||||
|
promise._response = None
|
||||||
|
promise.arrayBuffer = self.arrayBuffer
|
||||||
|
promise.blob = self.blob
|
||||||
|
promise.bytearray = self.bytearray
|
||||||
|
promise.json = self.json
|
||||||
|
promise.text = self.text
|
||||||
|
|
||||||
|
async def _response(self):
|
||||||
|
if not self._promise._response:
|
||||||
|
await self._promise
|
||||||
|
return self._promise._response
|
||||||
|
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.arrayBuffer()
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.bytearray()
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.text()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, **kw):
|
||||||
|
# workaround Pyodide / MicroPython dict <-> js conversion
|
||||||
|
options = js.JSON.parse(json.dumps(kw))
|
||||||
|
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
|
||||||
|
promise = js.fetch(url, options).then(awaited)
|
||||||
|
_DirectResponse(promise)
|
||||||
|
return promise
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user