@multiplatform.one/cli 7.11.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.
Files changed (163) hide show
  1. package/README.md +121 -111
  2. package/bin/createMultiplatformApp.mjs +39 -37
  3. package/lib/bin/multiplatformOne.mjs +65 -75
  4. package/lib/commands/addApp.mjs +78 -0
  5. package/lib/commands/adoptApp.mjs +11 -7
  6. package/lib/commands/doctypeTypes.mjs +148 -0
  7. package/lib/commands/e2e.mjs +2 -2
  8. package/lib/commands/init.mjs +142 -459
  9. package/lib/commands/merge.mjs +359 -0
  10. package/lib/commands/pieces.mjs +236 -0
  11. package/lib/commands/provenance.mjs +108 -0
  12. package/lib/commands/scaffold.mjs +105 -0
  13. package/lib/commands/updateApp.mjs +211 -528
  14. package/lib/source.json +5 -0
  15. package/lib/source.mjs +63 -0
  16. package/package.json +6 -6
  17. package/scripts/frappe-bootstrap.sh +29 -3
  18. package/scripts/frappe-dev.sh +1 -2
  19. package/scripts/frappe-helpers.sh +50 -2
  20. package/scripts/frappe.sh +5 -0
  21. package/scripts/scaffold.sh +937 -0
  22. package/scripts/stamp-source.mjs +51 -0
  23. package/scripts/stamp-source.spec.ts +86 -0
  24. package/src/bin/multiplatformOne.ts +116 -226
  25. package/src/commands/addApp.ts +102 -0
  26. package/src/commands/adoptApp.spec.ts +5 -1
  27. package/src/commands/adoptApp.ts +15 -10
  28. package/src/commands/doctypeTypes.spec.ts +155 -0
  29. package/src/commands/doctypeTypes.ts +214 -0
  30. package/src/commands/init.spec.ts +66 -0
  31. package/src/commands/init.ts +201 -643
  32. package/src/commands/merge.spec.ts +81 -0
  33. package/src/commands/merge.ts +411 -0
  34. package/src/commands/pieces.spec.ts +62 -0
  35. package/src/commands/pieces.ts +340 -0
  36. package/src/commands/provenance.spec.ts +92 -0
  37. package/src/commands/provenance.ts +169 -0
  38. package/src/commands/scaffold.ts +131 -0
  39. package/src/commands/updateApp.spec.ts +540 -331
  40. package/src/commands/updateApp.ts +255 -668
  41. package/src/source.spec.ts +45 -0
  42. package/src/source.ts +88 -0
  43. package/templates/AGENTS.md +161 -0
  44. package/templates/README.md +62 -0
  45. package/templates/pieces/frappe/universal/README.md.partial +7 -32
  46. package/templates/pieces/frappe/universal/env.example.partial +3 -3
  47. package/templates/pieces/frappe-bench/universal/README.md.partial +28 -0
  48. package/templates/pieces/{frappe → frappe-bench}/universal/docker/compose.frappe.yaml +2 -2
  49. package/templates/pieces/tauri/universal/README.md.partial +7 -5
  50. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/dev.rs +463 -0
  51. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/src/lib.rs +6 -0
  52. package/templates/pieces/tauri/universal/apps/__NAME__/src-tauri/tauri.conf.json +2 -1
  53. package/templates/pieces/webext/universal/apps/__NAME__/package.json.partial +1 -0
  54. package/templates/pieces/webext/universal/apps/__NAME__/vite.config.webext.ts +6 -2
  55. package/types/bin/multiplatformOne.d.ts.map +1 -1
  56. package/types/commands/addApp.d.ts +20 -0
  57. package/types/commands/addApp.d.ts.map +1 -0
  58. package/types/commands/adoptApp.d.ts +4 -3
  59. package/types/commands/adoptApp.d.ts.map +1 -1
  60. package/types/commands/doctypeTypes.d.ts +40 -0
  61. package/types/commands/doctypeTypes.d.ts.map +1 -0
  62. package/types/commands/init.d.ts +30 -13
  63. package/types/commands/init.d.ts.map +1 -1
  64. package/types/commands/merge.d.ts +54 -0
  65. package/types/commands/merge.d.ts.map +1 -0
  66. package/types/commands/pieces.d.ts +54 -0
  67. package/types/commands/pieces.d.ts.map +1 -0
  68. package/types/commands/provenance.d.ts +52 -0
  69. package/types/commands/provenance.d.ts.map +1 -0
  70. package/types/commands/scaffold.d.ts +43 -0
  71. package/types/commands/scaffold.d.ts.map +1 -0
  72. package/types/commands/updateApp.d.ts +19 -27
  73. package/types/commands/updateApp.d.ts.map +1 -1
  74. package/types/source.d.ts +35 -0
  75. package/types/source.d.ts.map +1 -0
  76. package/lib/commands/initApp.mjs +0 -431
  77. package/lib/generateVscode.mjs +0 -184
  78. package/scripts/clone.sh +0 -17
  79. package/scripts/update.sh +0 -46
  80. package/src/commands/initApp.spec.ts +0 -720
  81. package/src/commands/initApp.ts +0 -706
  82. package/src/generateVscode.ts +0 -238
  83. package/templates/app/AGENTS.md +0 -142
  84. package/templates/app/README.md +0 -62
  85. package/templates/app/apps/__NAME__/index.html +0 -36
  86. package/templates/app/apps/__NAME__/package.json +0 -31
  87. package/templates/app/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  88. package/templates/app/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  89. package/templates/app/apps/__NAME__/src/App.tsx +0 -9
  90. package/templates/app/apps/__NAME__/src/main.tsx +0 -14
  91. package/templates/app/apps/__NAME__/tsconfig.json +0 -8
  92. package/templates/app/apps/__NAME__/vite.config.ts +0 -17
  93. package/templates/app/features/__NAME__/package.json +0 -34
  94. package/templates/app/features/__NAME__/src/HomeScreen.tsx +0 -70
  95. package/templates/app/features/__NAME__/src/index.ts +0 -1
  96. package/templates/app/features/__NAME__/tsconfig.json +0 -5
  97. package/templates/app/gitignore +0 -9
  98. package/templates/app/npmrc +0 -2
  99. package/templates/app/oxlintrc.json +0 -39
  100. package/templates/app/package.json +0 -38
  101. package/templates/app/packages/config/config.json +0 -4
  102. package/templates/app/packages/config/index.ts +0 -5
  103. package/templates/app/packages/config/package.json +0 -17
  104. package/templates/app/packages/config/tsconfig.json +0 -5
  105. package/templates/app/pnpm-workspace.yaml +0 -4
  106. package/templates/app/tsconfig.base.json +0 -16
  107. package/templates/universal/AGENTS.md +0 -142
  108. package/templates/universal/README.md +0 -97
  109. package/templates/universal/apps/__NAME__/app.config.js +0 -31
  110. package/templates/universal/apps/__NAME__/config/tamagui.config.ts +0 -4
  111. package/templates/universal/apps/__NAME__/package.json +0 -62
  112. package/templates/universal/apps/__NAME__/polyfills.native.ts +0 -23
  113. package/templates/universal/apps/__NAME__/polyfills.ts +0 -3
  114. package/templates/universal/apps/__NAME__/public/fonts/inter-400.woff2 +0 -0
  115. package/templates/universal/apps/__NAME__/public/fonts/inter-700.woff2 +0 -0
  116. package/templates/universal/apps/__NAME__/routes/_layout.tsx +0 -22
  117. package/templates/universal/apps/__NAME__/routes/_layout.web.tsx +0 -21
  118. package/templates/universal/apps/__NAME__/routes/about.tsx +0 -3
  119. package/templates/universal/apps/__NAME__/routes/index.tsx +0 -3
  120. package/templates/universal/apps/__NAME__/src/app.ts +0 -10
  121. package/templates/universal/apps/__NAME__/tamagui.css +0 -3
  122. package/templates/universal/apps/__NAME__/tsconfig.json +0 -8
  123. package/templates/universal/apps/__NAME__/types.d.ts +0 -9
  124. package/templates/universal/apps/__NAME__/vite.config.ts +0 -20
  125. package/templates/universal/features/__NAME__/package.json +0 -35
  126. package/templates/universal/features/__NAME__/src/AboutScreen.tsx +0 -27
  127. package/templates/universal/features/__NAME__/src/AppLayout.tsx +0 -50
  128. package/templates/universal/features/__NAME__/src/ContactForm.spec.tsx +0 -22
  129. package/templates/universal/features/__NAME__/src/ContactForm.tsx +0 -55
  130. package/templates/universal/features/__NAME__/src/HomeScreen.tsx +0 -40
  131. package/templates/universal/features/__NAME__/src/index.ts +0 -4
  132. package/templates/universal/features/__NAME__/src/screens.spec.ts +0 -12
  133. package/templates/universal/features/__NAME__/tsconfig.json +0 -5
  134. package/templates/universal/features/__NAME__/vitest.config.mjs +0 -7
  135. package/templates/universal/gitignore +0 -14
  136. package/templates/universal/npmrc +0 -3
  137. package/templates/universal/oxlintrc.json +0 -39
  138. package/templates/universal/package.json +0 -51
  139. package/templates/universal/packages/config/config.json +0 -4
  140. package/templates/universal/packages/config/index.ts +0 -5
  141. package/templates/universal/packages/config/package.json +0 -17
  142. package/templates/universal/packages/config/tsconfig.json +0 -5
  143. package/templates/universal/packages/i18n/en/common.json +0 -30
  144. package/templates/universal/packages/i18n/es/common.json +0 -30
  145. package/templates/universal/packages/i18n/i18n.d.ts +0 -16
  146. package/templates/universal/packages/i18n/index.ts +0 -26
  147. package/templates/universal/packages/i18n/package.json +0 -20
  148. package/templates/universal/packages/i18n/resources.ts +0 -13
  149. package/templates/universal/packages/i18n/tsconfig.json +0 -5
  150. package/templates/universal/packages/themes/accent.ts +0 -32
  151. package/templates/universal/packages/themes/base.ts +0 -50
  152. package/templates/universal/packages/themes/fonts.ts +0 -8
  153. package/templates/universal/packages/themes/index.ts +0 -33
  154. package/templates/universal/packages/themes/package.json +0 -23
  155. package/templates/universal/packages/themes/tamagui.config.ts +0 -14
  156. package/templates/universal/packages/themes/tsconfig.json +0 -5
  157. package/templates/universal/pnpm-workspace.yaml +0 -4
  158. package/templates/universal/tsconfig.base.json +0 -21
  159. package/templates/universal/types/env.d.ts +0 -9
  160. package/types/commands/initApp.d.ts +0 -114
  161. package/types/commands/initApp.d.ts.map +0 -1
  162. package/types/generateVscode.d.ts +0 -7
  163. 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
  }
@@ -22,7 +22,8 @@
22
22
  "fullscreen": false,
23
23
  "center": true,
24
24
  "minWidth": 800,
25
- "minHeight": 600
25
+ "minHeight": 600,
26
+ "devtools": true
26
27
  }
27
28
  ]
28
29
  },
@@ -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(resolveConfig(publicConfigKeys));
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":"AA4FA,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"}
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 the mpo templates and consume
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 "./initApp";
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;;;;;;;;;;;;;GAaG;AAMH,OAAO,EAUL,KAAK,YAAY,EAClB,MAAM,WAAW,CAAC;AAEnB,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,CA0F3E"}
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"}