@multiplatform.one/cli 7.10.0 → 7.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -111
- package/bin/createMultiplatformApp.mjs +39 -37
- package/lib/bin/multiplatformOne.mjs +65 -75
- package/lib/commands/addApp.mjs +78 -0
- package/lib/commands/adoptApp.mjs +11 -7
- package/lib/commands/doctypeTypes.mjs +148 -0
- package/lib/commands/e2e.mjs +2 -2
- package/lib/commands/init.mjs +142 -459
- package/lib/commands/merge.mjs +359 -0
- package/lib/commands/pieces.mjs +236 -0
- package/lib/commands/provenance.mjs +108 -0
- package/lib/commands/scaffold.mjs +105 -0
- package/lib/commands/updateApp.mjs +211 -528
- package/lib/source.json +5 -0
- package/lib/source.mjs +63 -0
- package/package.json +7 -7
- package/scripts/frappe-bootstrap.sh +29 -3
- package/scripts/frappe-dev.sh +1 -2
- package/scripts/frappe-helpers.sh +50 -2
- package/scripts/frappe.sh +5 -0
- package/scripts/scaffold.sh +937 -0
- package/scripts/stamp-source.mjs +51 -0
- package/scripts/stamp-source.spec.ts +86 -0
- package/src/bin/multiplatformOne.ts +116 -226
- package/src/commands/addApp.ts +102 -0
- package/src/commands/adoptApp.spec.ts +5 -1
- package/src/commands/adoptApp.ts +15 -10
- package/src/commands/doctypeTypes.spec.ts +155 -0
- package/src/commands/doctypeTypes.ts +214 -0
- package/src/commands/init.spec.ts +66 -0
- package/src/commands/init.ts +201 -643
- package/src/commands/merge.spec.ts +81 -0
- package/src/commands/merge.ts +411 -0
- package/src/commands/pieces.spec.ts +62 -0
- package/src/commands/pieces.ts +340 -0
- package/src/commands/provenance.spec.ts +92 -0
- package/src/commands/provenance.ts +169 -0
- package/src/commands/scaffold.ts +131 -0
- package/src/commands/updateApp.spec.ts +540 -331
- package/src/commands/updateApp.ts +255 -668
- package/src/source.spec.ts +45 -0
- package/src/source.ts +88 -0
- package/templates/AGENTS.md +161 -0
- package/templates/README.md +62 -0
- package/templates/pieces/frappe/universal/README.md.partial +7 -32
- package/templates/pieces/frappe/universal/env.example.partial +3 -3
- package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
- package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
- package/templates/pieces/tauri/universal/README.md.partial +7 -5
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
- package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
- package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
- package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
- package/types/bin/multiplatformOne.d.ts.map +1 -1
- package/types/commands/addApp.d.ts +20 -0
- package/types/commands/addApp.d.ts.map +1 -0
- package/types/commands/adoptApp.d.ts +4 -3
- package/types/commands/adoptApp.d.ts.map +1 -1
- package/types/commands/doctypeTypes.d.ts +40 -0
- package/types/commands/doctypeTypes.d.ts.map +1 -0
- package/types/commands/init.d.ts +30 -13
- package/types/commands/init.d.ts.map +1 -1
- package/types/commands/merge.d.ts +54 -0
- package/types/commands/merge.d.ts.map +1 -0
- package/types/commands/pieces.d.ts +54 -0
- package/types/commands/pieces.d.ts.map +1 -0
- package/types/commands/provenance.d.ts +52 -0
- package/types/commands/provenance.d.ts.map +1 -0
- package/types/commands/scaffold.d.ts +43 -0
- package/types/commands/scaffold.d.ts.map +1 -0
- package/types/commands/updateApp.d.ts +19 -27
- package/types/commands/updateApp.d.ts.map +1 -1
- package/types/source.d.ts +35 -0
- package/types/source.d.ts.map +1 -0
- package/lib/commands/initApp.mjs +0 -431
- package/lib/generateVscode.mjs +0 -184
- package/scripts/clone.sh +0 -17
- package/scripts/update.sh +0 -46
- package/src/commands/initApp.spec.ts +0 -720
- package/src/commands/initApp.ts +0 -706
- package/src/generateVscode.ts +0 -238
- package/templates/app/AGENTS.md +0 -142
- package/templates/app/README.md +0 -62
- package/templates/app/apps/__NAME__/index.html +0 -36
- package/templates/app/apps/__NAME__/package.json +0 -31
- package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/app/apps/__NAME__/src/App.tsx +0 -9
- package/templates/app/apps/__NAME__/src/main.tsx +0 -14
- package/templates/app/apps/__NAME__/tsconfig.json +0 -8
- package/templates/app/apps/__NAME__/vite.config.ts +0 -17
- package/templates/app/features/__NAME__/package.json +0 -34
- package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
- package/templates/app/features/__NAME__/src/index.ts +0 -1
- package/templates/app/features/__NAME__/tsconfig.json +0 -5
- package/templates/app/gitignore +0 -9
- package/templates/app/npmrc +0 -2
- package/templates/app/oxlintrc.json +0 -39
- package/templates/app/package.json +0 -38
- package/templates/app/packages/config/config.json +0 -4
- package/templates/app/packages/config/index.ts +0 -5
- package/templates/app/packages/config/package.json +0 -17
- package/templates/app/packages/config/tsconfig.json +0 -5
- package/templates/app/pnpm-workspace.yaml +0 -4
- package/templates/app/tsconfig.base.json +0 -16
- package/templates/universal/AGENTS.md +0 -142
- package/templates/universal/README.md +0 -97
- package/templates/universal/apps/__NAME__/app.config.js +0 -31
- package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
- package/templates/universal/apps/__NAME__/package.json +0 -62
- package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
- package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
- package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
- package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
- package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
- package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
- package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
- package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
- package/templates/universal/apps/__NAME__/src/app.ts +0 -10
- package/templates/universal/apps/__NAME__/tamagui.css +0 -3
- package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
- package/templates/universal/apps/__NAME__/types.d.ts +0 -9
- package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
- package/templates/universal/features/__NAME__/package.json +0 -35
- package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
- package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
- package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
- package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
- package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
- package/templates/universal/features/__NAME__/src/index.ts +0 -4
- package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
- package/templates/universal/features/__NAME__/tsconfig.json +0 -5
- package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
- package/templates/universal/gitignore +0 -14
- package/templates/universal/npmrc +0 -3
- package/templates/universal/oxlintrc.json +0 -39
- package/templates/universal/package.json +0 -51
- package/templates/universal/packages/config/config.json +0 -4
- package/templates/universal/packages/config/index.ts +0 -5
- package/templates/universal/packages/config/package.json +0 -17
- package/templates/universal/packages/config/tsconfig.json +0 -5
- package/templates/universal/packages/i18n/en/common.json +0 -30
- package/templates/universal/packages/i18n/es/common.json +0 -30
- package/templates/universal/packages/i18n/i18n.d.ts +0 -16
- package/templates/universal/packages/i18n/index.ts +0 -26
- package/templates/universal/packages/i18n/package.json +0 -20
- package/templates/universal/packages/i18n/resources.ts +0 -13
- package/templates/universal/packages/i18n/tsconfig.json +0 -5
- package/templates/universal/packages/themes/accent.ts +0 -32
- package/templates/universal/packages/themes/base.ts +0 -50
- package/templates/universal/packages/themes/fonts.ts +0 -8
- package/templates/universal/packages/themes/index.ts +0 -33
- package/templates/universal/packages/themes/package.json +0 -23
- package/templates/universal/packages/themes/tamagui.config.ts +0 -14
- package/templates/universal/packages/themes/tsconfig.json +0 -5
- package/templates/universal/pnpm-workspace.yaml +0 -4
- package/templates/universal/tsconfig.base.json +0 -21
- package/templates/universal/types/env.d.ts +0 -9
- package/types/commands/initApp.d.ts +0 -114
- package/types/commands/initApp.d.ts.map +0 -1
- package/types/generateVscode.d.ts +0 -7
- package/types/generateVscode.d.ts.map +0 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
//! The dev shell. A dev build loads the dev server, so the window holds on a
|
|
2
|
+
//! waiting page until the app's client modules answer and a Develop menu
|
|
3
|
+
//! gives it Reload and devtools accelerators. A release build loads its
|
|
4
|
+
//! bundled frontend and needs neither, and a mobile build has no menu API.
|
|
5
|
+
|
|
6
|
+
#[cfg(all(dev, desktop))]
|
|
7
|
+
pub use shell::setup;
|
|
8
|
+
|
|
9
|
+
#[cfg(not(all(dev, desktop)))]
|
|
10
|
+
pub fn setup<R: tauri::Runtime>(_app: &tauri::App<R>) -> tauri::Result<()> {
|
|
11
|
+
Ok(())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[cfg(all(dev, desktop))]
|
|
15
|
+
mod shell {
|
|
16
|
+
use std::collections::{HashSet, VecDeque};
|
|
17
|
+
use std::io::{Read, Write};
|
|
18
|
+
use std::net::{TcpStream, ToSocketAddrs};
|
|
19
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
20
|
+
use std::time::Duration;
|
|
21
|
+
|
|
22
|
+
use tauri::menu::{Menu, MenuItem, Submenu};
|
|
23
|
+
use tauri::{Manager, Runtime, Url, WebviewWindow};
|
|
24
|
+
|
|
25
|
+
const RELOAD: &str = "reload";
|
|
26
|
+
const DEVTOOLS: &str = "devtools";
|
|
27
|
+
|
|
28
|
+
/// True while the main window shows the waiting page. Whoever clears it
|
|
29
|
+
/// (the gate once the client graph settles, or Reload pressed on the
|
|
30
|
+
/// waiting page) is the one that loads the app.
|
|
31
|
+
static WAITING: AtomicBool = AtomicBool::new(false);
|
|
32
|
+
|
|
33
|
+
const WAITING_PAGE: &str = r#"data:text/html,<!DOCTYPE html>
|
|
34
|
+
<html><head><meta charset="utf-8"><style>
|
|
35
|
+
body{font-family:system-ui,-apple-system,sans-serif;background:rgb(26,26,46);color:rgb(224,224,224);
|
|
36
|
+
display:flex;align-items:center;justify-content:center;height:100vh;flex-direction:column;margin:0}
|
|
37
|
+
h1{font-size:1.6rem;margin-bottom:1rem;color:white}
|
|
38
|
+
code{background:rgb(18,18,42);padding:.3rem .6rem;border-radius:6px;font-size:1rem;color:rgb(112,214,255);
|
|
39
|
+
border:1px solid rgb(48,48,90)}
|
|
40
|
+
p{margin:.8rem 0;font-size:.95rem;color:rgb(153,153,153)}
|
|
41
|
+
.dot{display:inline-block;animation:blink .7s infinite alternate}
|
|
42
|
+
.dot:nth-child(2){animation-delay:.2s}
|
|
43
|
+
.dot:nth-child(3){animation-delay:.4s}
|
|
44
|
+
@keyframes blink{from{opacity:.1}to{opacity:1}}
|
|
45
|
+
</style></head>
|
|
46
|
+
<body>
|
|
47
|
+
<h1>Waiting for the dev server<span class="dot">.</span><span class="dot">.</span><span class="dot">.</span></h1>
|
|
48
|
+
<p>Start it if it is not running:</p>
|
|
49
|
+
<code>pnpm dev</code>
|
|
50
|
+
<p>Reload (Cmd/Ctrl+R) loads the app without waiting.</p>
|
|
51
|
+
<p id="tauri-dev-status" style="margin-top:2rem;font-size:.85rem;color:rgb(102,102,102)">waiting for DEV_URL</p>
|
|
52
|
+
</body></html>"#;
|
|
53
|
+
|
|
54
|
+
pub fn setup<R: Runtime>(app: &tauri::App<R>) -> tauri::Result<()> {
|
|
55
|
+
let url = url(app);
|
|
56
|
+
menu(app, url.clone())?;
|
|
57
|
+
if let Some(window) = app.get_webview_window("main") {
|
|
58
|
+
gate(window, url);
|
|
59
|
+
}
|
|
60
|
+
Ok(())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// TAURI_DEV_URL wins outright; ONE_PORT replaces the port of the
|
|
64
|
+
/// configured devUrl, which is how `make one/tauri` follows the repo .env.
|
|
65
|
+
fn url<R: Runtime>(app: &tauri::App<R>) -> Url {
|
|
66
|
+
if let Some(url) = std::env::var("TAURI_DEV_URL")
|
|
67
|
+
.ok()
|
|
68
|
+
.and_then(|value| value.parse().ok())
|
|
69
|
+
{
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
let mut url = app
|
|
73
|
+
.config()
|
|
74
|
+
.build
|
|
75
|
+
.dev_url
|
|
76
|
+
.clone()
|
|
77
|
+
.unwrap_or_else(|| "http://localhost:3456".parse().expect("literal url"));
|
|
78
|
+
if let Some(port) = std::env::var("ONE_PORT")
|
|
79
|
+
.ok()
|
|
80
|
+
.and_then(|value| value.parse().ok())
|
|
81
|
+
{
|
|
82
|
+
let _ = url.set_port(Some(port));
|
|
83
|
+
}
|
|
84
|
+
url
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn menu<R: Runtime>(app: &tauri::App<R>, url: Url) -> tauri::Result<()> {
|
|
88
|
+
let handle = app.handle();
|
|
89
|
+
let reload = MenuItem::with_id(handle, RELOAD, "Reload", true, Some("CmdOrCtrl+R"))?;
|
|
90
|
+
let devtools = MenuItem::with_id(
|
|
91
|
+
handle,
|
|
92
|
+
DEVTOOLS,
|
|
93
|
+
"Toggle Developer Tools",
|
|
94
|
+
true,
|
|
95
|
+
Some("CmdOrCtrl+Alt+I"),
|
|
96
|
+
)?;
|
|
97
|
+
let develop = Submenu::with_items(handle, "Develop", true, &[&reload, &devtools])?;
|
|
98
|
+
let menu = Menu::default(handle)?;
|
|
99
|
+
menu.append(&develop)?;
|
|
100
|
+
app.set_menu(menu)?;
|
|
101
|
+
app.on_menu_event(move |app, event| {
|
|
102
|
+
let Some(window) = app.get_webview_window("main") else {
|
|
103
|
+
return;
|
|
104
|
+
};
|
|
105
|
+
match event.id().as_ref() {
|
|
106
|
+
RELOAD => {
|
|
107
|
+
if WAITING.swap(false, Ordering::SeqCst) {
|
|
108
|
+
let _ = window.navigate(url.clone());
|
|
109
|
+
} else {
|
|
110
|
+
let _ = window.eval("window.location.reload()");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
#[cfg(debug_assertions)]
|
|
114
|
+
DEVTOOLS => {
|
|
115
|
+
if window.is_devtools_open() {
|
|
116
|
+
window.close_devtools();
|
|
117
|
+
} else {
|
|
118
|
+
window.open_devtools();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
_ => {}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
Ok(())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// The window shows the waiting page until two crawls in a row walk the
|
|
128
|
+
/// same client graph with no 504. A 504 means the optimizer re-bundled
|
|
129
|
+
/// under the crawl, and a changed graph means it re-bundled between two,
|
|
130
|
+
/// so either starts over: the app is loaded only once the optimizer has
|
|
131
|
+
/// settled, which is what makes the first paint hydrate on a cold .vite
|
|
132
|
+
/// cache.
|
|
133
|
+
fn gate<R: Runtime>(window: WebviewWindow<R>, url: Url) {
|
|
134
|
+
WAITING.store(true, Ordering::SeqCst);
|
|
135
|
+
let _ = window.navigate(
|
|
136
|
+
WAITING_PAGE
|
|
137
|
+
.replace("DEV_URL", url.as_str())
|
|
138
|
+
.parse()
|
|
139
|
+
.expect("data url"),
|
|
140
|
+
);
|
|
141
|
+
std::thread::spawn(move || {
|
|
142
|
+
let status = |text: String| {
|
|
143
|
+
let text = serde_json::to_string(&text).unwrap_or_default();
|
|
144
|
+
let _ = window.eval(format!(
|
|
145
|
+
"(document.getElementById('tauri-dev-status') || {{}}).textContent = {text}"
|
|
146
|
+
));
|
|
147
|
+
};
|
|
148
|
+
let mut previous: Option<HashSet<String>> = None;
|
|
149
|
+
while WAITING.load(Ordering::SeqCst) {
|
|
150
|
+
match warm(&url, &status) {
|
|
151
|
+
Ok(modules) if previous.as_ref() == Some(&modules) => {
|
|
152
|
+
log::info!(
|
|
153
|
+
"{url}: {} client modules settled, loading the app",
|
|
154
|
+
modules.len()
|
|
155
|
+
);
|
|
156
|
+
if WAITING.swap(false, Ordering::SeqCst) {
|
|
157
|
+
let _ = window.navigate(url);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
Ok(modules) => {
|
|
162
|
+
status(format!(
|
|
163
|
+
"{} client modules answered, checking the optimizer settled",
|
|
164
|
+
modules.len()
|
|
165
|
+
));
|
|
166
|
+
previous = Some(modules);
|
|
167
|
+
}
|
|
168
|
+
Err(reason) => {
|
|
169
|
+
status(reason);
|
|
170
|
+
previous = None;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
std::thread::sleep(Duration::from_secs(1));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn warm(base: &Url, status: &dyn Fn(String)) -> Result<HashSet<String>, String> {
|
|
179
|
+
let mut seen = HashSet::new();
|
|
180
|
+
let Some(document) = document(base)? else {
|
|
181
|
+
return Ok(seen);
|
|
182
|
+
};
|
|
183
|
+
let mut queue: VecDeque<String> = module_scripts(&document.body).into_iter().collect();
|
|
184
|
+
while let Some(path) = queue.pop_front() {
|
|
185
|
+
if !seen.insert(path.clone()) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let response = fetch(base, &path, "*/*")?;
|
|
189
|
+
match response.status {
|
|
190
|
+
200 if response.javascript => queue.extend(imports(&response.body)),
|
|
191
|
+
504 => return Err(format!("{path} is outdated, the optimizer re-bundled")),
|
|
192
|
+
_ => {}
|
|
193
|
+
}
|
|
194
|
+
if seen.len() % 50 == 0 {
|
|
195
|
+
status(format!("warming client modules ({})", seen.len()));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
Ok(seen)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// The document the window will load, following same-origin redirects.
|
|
202
|
+
/// None when it redirects off-origin (an auth server, say): there is
|
|
203
|
+
/// nothing of the app's own to warm, so the window loads it as is.
|
|
204
|
+
fn document(base: &Url) -> Result<Option<Response>, String> {
|
|
205
|
+
let mut path = request_path(base);
|
|
206
|
+
for _ in 0..5 {
|
|
207
|
+
let response = fetch(base, &path, "text/html,*/*;q=0.8")?;
|
|
208
|
+
match (response.status, response.location.as_deref()) {
|
|
209
|
+
(200, _) => return Ok(Some(response)),
|
|
210
|
+
(300..=399, Some(location)) => {
|
|
211
|
+
let target = base
|
|
212
|
+
.join(location)
|
|
213
|
+
.map_err(|error| format!("redirect to {location}: {error}"))?;
|
|
214
|
+
if target.origin() != base.origin() {
|
|
215
|
+
return Ok(None);
|
|
216
|
+
}
|
|
217
|
+
path = request_path(&target);
|
|
218
|
+
}
|
|
219
|
+
(status, _) => return Err(format!("the document answered {status}")),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
Err("the document keeps redirecting".to_string())
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn request_path(url: &Url) -> String {
|
|
226
|
+
match url.query() {
|
|
227
|
+
Some(query) => format!("{}?{query}", url.path()),
|
|
228
|
+
None => url.path().to_string(),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[derive(Debug)]
|
|
233
|
+
struct Response {
|
|
234
|
+
status: u16,
|
|
235
|
+
javascript: bool,
|
|
236
|
+
location: Option<String>,
|
|
237
|
+
body: String,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn fetch(base: &Url, path: &str, accept: &str) -> Result<Response, String> {
|
|
241
|
+
let host = base.host_str().ok_or("the dev url has no host")?;
|
|
242
|
+
let port = base.port_or_known_default().unwrap_or(80);
|
|
243
|
+
let addrs = (host, port)
|
|
244
|
+
.to_socket_addrs()
|
|
245
|
+
.map_err(|error| format!("{host}:{port} does not resolve ({error})"))?;
|
|
246
|
+
let stream = addrs
|
|
247
|
+
.filter_map(|addr| TcpStream::connect_timeout(&addr, Duration::from_secs(2)).ok())
|
|
248
|
+
.next();
|
|
249
|
+
let mut stream = stream.ok_or_else(|| format!("waiting for {host}:{port}"))?;
|
|
250
|
+
let _ = stream.set_read_timeout(Some(Duration::from_secs(120)));
|
|
251
|
+
write!(
|
|
252
|
+
stream,
|
|
253
|
+
"GET {path} HTTP/1.1\r\nHost: {host}:{port}\r\nAccept: {accept}\r\nConnection: close\r\n\r\n"
|
|
254
|
+
)
|
|
255
|
+
.map_err(|error| error.to_string())?;
|
|
256
|
+
let mut raw = Vec::new();
|
|
257
|
+
stream
|
|
258
|
+
.read_to_end(&mut raw)
|
|
259
|
+
.map_err(|error| format!("{path}: {error}"))?;
|
|
260
|
+
parse(&raw).ok_or_else(|| format!("{path}: malformed response"))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fn parse(raw: &[u8]) -> Option<Response> {
|
|
264
|
+
let split = find(raw, b"\r\n\r\n")?;
|
|
265
|
+
let head = String::from_utf8_lossy(&raw[..split]);
|
|
266
|
+
let mut lines = head.lines();
|
|
267
|
+
let status = lines.next()?.split_whitespace().nth(1)?.parse().ok()?;
|
|
268
|
+
let mut chunked = false;
|
|
269
|
+
let mut javascript = false;
|
|
270
|
+
let mut location = None;
|
|
271
|
+
for line in lines {
|
|
272
|
+
let Some((name, value)) = line.split_once(':') else {
|
|
273
|
+
continue;
|
|
274
|
+
};
|
|
275
|
+
let value = value.trim();
|
|
276
|
+
match name.trim().to_ascii_lowercase().as_str() {
|
|
277
|
+
"transfer-encoding" => chunked = value.to_ascii_lowercase().contains("chunked"),
|
|
278
|
+
"content-type" => javascript = value.to_ascii_lowercase().contains("javascript"),
|
|
279
|
+
"location" => location = Some(value.to_string()),
|
|
280
|
+
_ => {}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
let body = &raw[split + 4..];
|
|
284
|
+
let body = if chunked {
|
|
285
|
+
dechunk(body)
|
|
286
|
+
} else {
|
|
287
|
+
body.to_vec()
|
|
288
|
+
};
|
|
289
|
+
Some(Response {
|
|
290
|
+
status,
|
|
291
|
+
javascript,
|
|
292
|
+
location,
|
|
293
|
+
body: String::from_utf8_lossy(&body).into_owned(),
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
|
298
|
+
haystack
|
|
299
|
+
.windows(needle.len())
|
|
300
|
+
.position(|window| window == needle)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn dechunk(body: &[u8]) -> Vec<u8> {
|
|
304
|
+
let mut out = Vec::new();
|
|
305
|
+
let mut rest = body;
|
|
306
|
+
while let Some(line_end) = find(rest, b"\r\n") {
|
|
307
|
+
let size_field = String::from_utf8_lossy(&rest[..line_end]);
|
|
308
|
+
let size = size_field
|
|
309
|
+
.split(';')
|
|
310
|
+
.next()
|
|
311
|
+
.and_then(|hex| usize::from_str_radix(hex.trim(), 16).ok())
|
|
312
|
+
.unwrap_or(0);
|
|
313
|
+
if size == 0 {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
let start = line_end + 2;
|
|
317
|
+
let end = (start + size).min(rest.len());
|
|
318
|
+
out.extend_from_slice(&rest[start..end]);
|
|
319
|
+
rest = &rest[(end + 2).min(rest.len())..];
|
|
320
|
+
}
|
|
321
|
+
out
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn module_scripts(html: &str) -> Vec<String> {
|
|
325
|
+
let mut found = Vec::new();
|
|
326
|
+
for (index, _) in html.match_indices("<script") {
|
|
327
|
+
let Some(end) = html[index..].find('>') else {
|
|
328
|
+
break;
|
|
329
|
+
};
|
|
330
|
+
let tag = &html[index..index + end];
|
|
331
|
+
if !tag.contains("type=\"module\"") && !tag.contains("type='module'") {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if let Some(src) = attribute(tag, "src").filter(|src| same_origin(src)) {
|
|
335
|
+
found.push(src);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
found
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn attribute(tag: &str, name: &str) -> Option<String> {
|
|
342
|
+
let start = tag.find(&format!(" {name}="))? + name.len() + 2;
|
|
343
|
+
let quote = tag[start..].chars().next()?;
|
|
344
|
+
if quote != '"' && quote != '\'' {
|
|
345
|
+
return None;
|
|
346
|
+
}
|
|
347
|
+
let value = &tag[start + 1..];
|
|
348
|
+
Some(value[..value.find(quote)?].to_string())
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
fn imports(source: &str) -> Vec<String> {
|
|
352
|
+
let mut found = Vec::new();
|
|
353
|
+
for keyword in ["from", "import"] {
|
|
354
|
+
for (index, _) in source.match_indices(keyword) {
|
|
355
|
+
let mut rest = source[index + keyword.len()..].trim_start();
|
|
356
|
+
if let Some(inner) = rest.strip_prefix('(') {
|
|
357
|
+
rest = inner.trim_start();
|
|
358
|
+
}
|
|
359
|
+
let Some(quote) = rest.chars().next().filter(|c| matches!(*c, '"' | '\'')) else {
|
|
360
|
+
continue;
|
|
361
|
+
};
|
|
362
|
+
let literal = &rest[1..];
|
|
363
|
+
let Some(end) = literal.find(quote) else {
|
|
364
|
+
continue;
|
|
365
|
+
};
|
|
366
|
+
let specifier = &literal[..end];
|
|
367
|
+
if same_origin(specifier) && !specifier.contains(char::is_whitespace) {
|
|
368
|
+
found.push(specifier.to_string());
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
found
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn same_origin(path: &str) -> bool {
|
|
376
|
+
path.starts_with('/') && !path.starts_with("//")
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[cfg(test)]
|
|
380
|
+
mod tests {
|
|
381
|
+
use super::*;
|
|
382
|
+
|
|
383
|
+
#[test]
|
|
384
|
+
fn module_scripts_reads_the_bootstrap_tags_react_writes() {
|
|
385
|
+
let html = concat!(
|
|
386
|
+
r#"<head><script type="module" src="/@one/dev.js"></script></head><body>"#,
|
|
387
|
+
r#"<script type="module" src="/@vite/client" async=""></script>"#,
|
|
388
|
+
r#"<script type="module" src="/@id/__x00__virtual:one-entry" async=""></script>"#,
|
|
389
|
+
r#"<script src="/classic.js"></script>"#,
|
|
390
|
+
r#"<script type="module">import "/inline.js"</script>"#,
|
|
391
|
+
r#"<script type='module' src='//cdn.example/x.js'></script></body>"#,
|
|
392
|
+
);
|
|
393
|
+
assert_eq!(
|
|
394
|
+
module_scripts(html),
|
|
395
|
+
[
|
|
396
|
+
"/@one/dev.js",
|
|
397
|
+
"/@vite/client",
|
|
398
|
+
"/@id/__x00__virtual:one-entry"
|
|
399
|
+
]
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#[test]
|
|
404
|
+
fn imports_follows_static_dynamic_and_side_effect_imports_on_the_server() {
|
|
405
|
+
let source = concat!(
|
|
406
|
+
"import __vite__cjsImport0_react from \"/node_modules/.vite/deps/react.js?v=1a2b\";\n",
|
|
407
|
+
"import { x } from '/app/features/x.tsx';\n",
|
|
408
|
+
"import \"/app/global.css\";\n",
|
|
409
|
+
"const routes = { './a.tsx': () => import(\"/app/routes/a.tsx\") };\n",
|
|
410
|
+
"const list = Array.from(items);\n",
|
|
411
|
+
"import.meta.hot?.accept();\n",
|
|
412
|
+
"import bare from 'react';\n",
|
|
413
|
+
"import cdn from '//cdn.example/x.js';\n",
|
|
414
|
+
);
|
|
415
|
+
let mut found = imports(source);
|
|
416
|
+
found.sort();
|
|
417
|
+
assert_eq!(
|
|
418
|
+
found,
|
|
419
|
+
[
|
|
420
|
+
"/app/features/x.tsx",
|
|
421
|
+
"/app/global.css",
|
|
422
|
+
"/app/routes/a.tsx",
|
|
423
|
+
"/node_modules/.vite/deps/react.js?v=1a2b",
|
|
424
|
+
]
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[test]
|
|
429
|
+
fn parse_reads_a_sized_javascript_response() {
|
|
430
|
+
let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/javascript\r\nContent-Length: 11\r\n\r\nexport {};\n";
|
|
431
|
+
let response = parse(raw).expect("response");
|
|
432
|
+
assert_eq!(response.status, 200);
|
|
433
|
+
assert!(response.javascript);
|
|
434
|
+
assert_eq!(response.body, "export {};\n");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[test]
|
|
438
|
+
fn parse_dechunks_a_chunked_document() {
|
|
439
|
+
let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nTransfer-Encoding: chunked\r\n\r\n6\r\n<html>\r\n7\r\n</html>\r\n0\r\n\r\n";
|
|
440
|
+
let response = parse(raw).expect("response");
|
|
441
|
+
assert!(!response.javascript);
|
|
442
|
+
assert_eq!(response.body, "<html></html>");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[test]
|
|
446
|
+
fn parse_keeps_the_outdated_optimize_dep_status_and_redirect_location() {
|
|
447
|
+
let outdated = parse(b"HTTP/1.1 504 Outdated Optimize Dep\r\n\r\n").expect("504");
|
|
448
|
+
assert_eq!(outdated.status, 504);
|
|
449
|
+
let redirect =
|
|
450
|
+
parse(b"HTTP/1.1 307 Temporary Redirect\r\nLocation: /home\r\n\r\n").expect("307");
|
|
451
|
+
assert_eq!(redirect.status, 307);
|
|
452
|
+
assert_eq!(redirect.location.as_deref(), Some("/home"));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#[test]
|
|
456
|
+
fn request_path_keeps_the_query() {
|
|
457
|
+
let url: Url = "http://localhost:3456/backoffice?tab=1".parse().unwrap();
|
|
458
|
+
assert_eq!(request_path(&url), "/backoffice?tab=1");
|
|
459
|
+
let root: Url = "http://localhost:3456".parse().unwrap();
|
|
460
|
+
assert_eq!(request_path(&root), "/");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//! __NAME__ Tauri application library. Register #[tauri::command] functions
|
|
2
2
|
//! here (and in the invoke_handler) as the desktop app grows.
|
|
3
3
|
|
|
4
|
+
mod dev;
|
|
5
|
+
|
|
4
6
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
5
7
|
pub fn run() {
|
|
6
8
|
tauri::Builder::default()
|
|
@@ -9,6 +11,10 @@ pub fn run() {
|
|
|
9
11
|
.level(log::LevelFilter::Info)
|
|
10
12
|
.build(),
|
|
11
13
|
)
|
|
14
|
+
.setup(|app| {
|
|
15
|
+
dev::setup(app)?;
|
|
16
|
+
Ok(())
|
|
17
|
+
})
|
|
12
18
|
.run(tauri::generate_context!())
|
|
13
19
|
.expect("error while running tauri application");
|
|
14
20
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"webextension-polyfill": "^0.12.0"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
+
"@multiplatform.one/config": "__MPO_VERSION__",
|
|
13
14
|
"@multiplatform.one/utils": "__MPO_VERSION__",
|
|
14
15
|
"@multiplatform.one/vite-plugin-webext": "__MPO_VERSION__",
|
|
15
16
|
"@tamagui/vite-plugin": "2.7.6",
|
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
// @multiplatform.one/vite-plugin-webext.
|
|
7
7
|
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
|
+
import { readPublicConfigKeys } from "@multiplatform.one/config/vite";
|
|
9
10
|
import { resolveConfig } from "@multiplatform.one/utils/dev";
|
|
10
11
|
import { createWebextViewsConfig } from "@multiplatform.one/vite-plugin-webext";
|
|
11
12
|
import dotenv from "dotenv";
|
|
12
13
|
import { defineConfig } from "vite";
|
|
13
|
-
import { public as publicConfigKeys } from "../../packages/config/config.json";
|
|
14
14
|
import packageJson from "./package.json";
|
|
15
15
|
import { isDev, port } from "./webext/env";
|
|
16
16
|
|
|
17
17
|
dotenv.config({ path: resolve(import.meta.dirname, "../../.env") });
|
|
18
|
-
process.env.VITE_MP_CONFIG = JSON.stringify(
|
|
18
|
+
process.env.VITE_MP_CONFIG = JSON.stringify(
|
|
19
|
+
resolveConfig(
|
|
20
|
+
readPublicConfigKeys(resolve(import.meta.dirname, "../../packages/config/config.json")),
|
|
21
|
+
),
|
|
22
|
+
);
|
|
19
23
|
|
|
20
24
|
export default defineConfig(async () =>
|
|
21
25
|
createWebextViewsConfig({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multiplatformOne.d.ts","sourceRoot":"","sources":["../../src/bin/multiplatformOne.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"multiplatformOne.d.ts","sourceRoot":"","sources":["../../src/bin/multiplatformOne.ts"],"names":[],"mappings":"AA6FA,gHAAgH;AAChH,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,kGAAkG;IAClG,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gGAAgG;IAChG,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mpo add-app <name>`: one more product app from apps/one, merged in at the
|
|
3
|
+
* commit the project was scaffolded from.
|
|
4
|
+
*
|
|
5
|
+
* base = scaffold(recorded sha, recorded apps)
|
|
6
|
+
* theirs = scaffold(recorded sha, recorded apps + name)
|
|
7
|
+
* ours = HEAD
|
|
8
|
+
*
|
|
9
|
+
* Same commit on both sides, so the diff is exactly the new app and the
|
|
10
|
+
* root wiring that names it; the project's own changes are untouched. The
|
|
11
|
+
* result stays uncommitted like `mpo update`.
|
|
12
|
+
*/
|
|
13
|
+
export interface AddAppOptions {
|
|
14
|
+
/** Path or url holding the recorded source commit (default: the recorded url). */
|
|
15
|
+
source?: string;
|
|
16
|
+
skipInstall?: boolean;
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function addApp(nameArg: string, options?: AddAppOptions): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=addApp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addApp.d.ts","sourceRoot":"","sources":["../../src/commands/addApp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoExF"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `mpo adopt` — bring an EXISTING, never-scaffolded project under `mpo
|
|
3
3
|
* update` management. Projects like this hand-copied one or more delivery
|
|
4
|
-
* targets (webext, gnome, …) from
|
|
4
|
+
* targets (webext, gnome, …) from templates/pieces and consume
|
|
5
5
|
* @multiplatform.one/* from npm, but have no .mpo.json provenance, so
|
|
6
|
-
* `mpo update` refuses to run.
|
|
6
|
+
* `mpo update` refuses to run. A project that wants the whole monorepo
|
|
7
|
+
* scaffold instead runs `mpo init --into`.
|
|
7
8
|
*
|
|
8
9
|
* Adoption writes provenance ONLY (template "none" + the chosen pieces +
|
|
9
10
|
* the current CLI version) and seeds .updateignore — it never rewrites a
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
* diffs (and conflicts) against hand-copied files are expected there;
|
|
13
14
|
* every later update is an ordinary three-way merge.
|
|
14
15
|
*/
|
|
15
|
-
import { type InitAppPiece } from "./
|
|
16
|
+
import { type InitAppPiece } from "./pieces";
|
|
16
17
|
export interface AdoptAppOptions {
|
|
17
18
|
/** Pieces the project hand-adopted (at least one is required). */
|
|
18
19
|
pieces?: InitAppPiece[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adoptApp.d.ts","sourceRoot":"","sources":["../../src/commands/adoptApp.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"adoptApp.d.ts","sourceRoot":"","sources":["../../src/commands/adoptApp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,UAAU,CAAC;AASlB,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAuDD,wBAAsB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4F3E"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mpo frappe types`: TypeScript interfaces for DocTypes, generated from the
|
|
3
|
+
* server's own meta (`frappe.desk.form.load.getdoctype`), so a TDoc type
|
|
4
|
+
* parameter is the server schema and not a hand copy of it. `--check`
|
|
5
|
+
* regenerates and diffs against the committed file, so drift fails loudly.
|
|
6
|
+
*/
|
|
7
|
+
export interface DocFieldMeta {
|
|
8
|
+
fieldname: string;
|
|
9
|
+
fieldtype: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
options?: string;
|
|
12
|
+
reqd?: 0 | 1 | boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface DoctypeMeta {
|
|
15
|
+
name: string;
|
|
16
|
+
istable?: 0 | 1 | boolean;
|
|
17
|
+
fields: DocFieldMeta[];
|
|
18
|
+
}
|
|
19
|
+
export interface GetDoctypeOptions {
|
|
20
|
+
baseURL: string;
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
apiSecret?: string;
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
export declare const generatedHeader = "// Generated by `mpo frappe types` from frappe.desk.form.load.getdoctype. Do not edit.";
|
|
26
|
+
/** "Sales Invoice Item" → "SalesInvoiceItem". */
|
|
27
|
+
export declare function interfaceName(doctype: string): string;
|
|
28
|
+
/** The TypeScript type a field's value takes, or undefined for layout/display fields. */
|
|
29
|
+
export declare function fieldTsType(field: DocFieldMeta): string | undefined;
|
|
30
|
+
/** One module with an interface per DocType, sorted by name so output is stable. */
|
|
31
|
+
export declare function doctypesToTypeScript(docs: DoctypeMeta[]): string;
|
|
32
|
+
/** The requested DocType plus its child tables, as getdoctype returns them. */
|
|
33
|
+
export declare function fetchDoctypeMeta(doctype: string, options: GetDoctypeOptions): Promise<DoctypeMeta[]>;
|
|
34
|
+
export declare function generateDoctypeTypes(doctypes: string[], options: GetDoctypeOptions): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Line diff of the committed file against a fresh generation: `-` lines the
|
|
37
|
+
* server no longer has, `+` lines it now has. Empty means no drift.
|
|
38
|
+
*/
|
|
39
|
+
export declare function typesDrift(committed: string, generated: string): string[];
|
|
40
|
+
//# sourceMappingURL=doctypeTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctypeTypes.d.ts","sourceRoot":"","sources":["../../src/commands/doctypeTypes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC;IAC1B,MAAM,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,eAAO,MAAM,eAAe,2FAC8D,CAAC;AA4D3F,iDAAiD;AACjD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOrD;AAeD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAWnE;AAmBD,oFAAoF;AACpF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,MAAM,CAIhE;AAED,+EAA+E;AAC/E,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,WAAW,EAAE,CAAC,CAgBxB;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BzE"}
|