@akanjs/devkit 3.2.0-rc.3 → 3.2.0-rc.5

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.
@@ -77,20 +77,12 @@ export class PreviewView {
77
77
  }: PreviewProbeOptions = {},
78
78
  ): Promise<PreviewProbeResult> {
79
79
  const logs: string[] = [];
80
- const view = PreviewView.#open({ width, height, engine, onConsole: (line) => logs.push(line) });
80
+ const view = await PreviewView.#start(
81
+ () => PreviewView.#open({ width, height, engine, onConsole: (line) => logs.push(line) }),
82
+ engine,
83
+ { startTimeoutMs, timeoutMs },
84
+ );
81
85
  try {
82
- //? A browser that missed its start bound may still come up, so a later page retries it within only its own bound.
83
- //? One that died or was closed instead is spawned afresh by the next view, which gets the full bound.
84
- const startMs = PreviewView.#stalled.has(engine) ? Math.min(startTimeoutMs, timeoutMs) : startTimeoutMs;
85
- const stalled = new Error(`Timed out starting the ${engine} browser after ${startMs / 1_000} s.`);
86
- await PreviewView.#within(view.navigate("about:blank"), Date.now() + startMs, stalled).then(
87
- () => PreviewView.#stalled.delete(engine),
88
- (error: unknown) => {
89
- if (error === stalled) PreviewView.#stalled.add(engine);
90
- else PreviewView.#stalled.delete(engine);
91
- throw error;
92
- },
93
- );
94
86
  const deadline = Date.now() + timeoutMs;
95
87
  const within = <T>(work: Promise<T>, what: string) =>
96
88
  PreviewView.#within(
@@ -122,6 +114,33 @@ export class PreviewView {
122
114
  }
123
115
  }
124
116
 
117
+ static async #start(
118
+ open: () => WebViewLike,
119
+ engine: PreviewEngine,
120
+ { startTimeoutMs, timeoutMs }: { startTimeoutMs: number; timeoutMs: number },
121
+ ) {
122
+ for (let attempt = 1; ; attempt++) {
123
+ //? A browser that missed its start bound may still come up, so a later page retries it within only its own bound.
124
+ //? One that died or was closed instead is spawned afresh by the next view, which gets the full bound.
125
+ const startMs = PreviewView.#stalled.has(engine) ? Math.min(startTimeoutMs, timeoutMs) : startTimeoutMs;
126
+ const stalled = new Error(`Timed out starting the ${engine} browser after ${startMs / 1_000} s.`);
127
+ let view: WebViewLike | undefined;
128
+ try {
129
+ view = open();
130
+ await PreviewView.#within(view.navigate("about:blank"), Date.now() + startMs, stalled);
131
+ PreviewView.#stalled.delete(engine);
132
+ return view;
133
+ } catch (error) {
134
+ await view?.close();
135
+ if (error === stalled) PreviewView.#stalled.add(engine);
136
+ else PreviewView.#stalled.delete(engine);
137
+ if (error === stalled || attempt > 1) throw error;
138
+ //? Bun reaps a browser that died only on a later timer turn; until then `new Bun.WebView()` throws "Failed to spawn Chrome".
139
+ await Bun.sleep(100);
140
+ }
141
+ }
142
+ }
143
+
125
144
  static async #load(
126
145
  view: WebViewLike,
127
146
  url: string,
@@ -178,9 +178,7 @@ Bun.WebView.closeAll();`,
178
178
  console.log(await open());
179
179
  const killed = open();
180
180
  Bun.WebView.closeAll();
181
- console.error(await killed);
182
- await Bun.sleep(500);
183
- console.log(await open());
181
+ console.log(await killed);
184
182
  Bun.WebView.closeAll();`,
185
183
  6,
186
184
  );
@@ -194,4 +192,36 @@ Bun.WebView.closeAll();`,
194
192
  },
195
193
  browserTestMs,
196
194
  );
195
+
196
+ test.skipIf(!slowChromeRuns)(
197
+ "starts a new browser for the page right after one that the browser died on",
198
+ async () => {
199
+ const { code, out, log } = await withSlowChrome(
200
+ `const { promise: loading, resolve } = Promise.withResolvers();
201
+ const server = Bun.serve({
202
+ port: 0,
203
+ fetch: (req) =>
204
+ new URL(req.url).pathname === "/hang"
205
+ ? (resolve(), new Promise(() => undefined))
206
+ : new Response("<!doctype html><body>up</body>", { headers: { "content-type": "text/html" } }),
207
+ });
208
+ const open = (path) =>
209
+ PreviewView.probe(\`http://localhost:\${server.port}\${path}\`, { engine: "chrome" }).then(
210
+ (probe) => probe.text,
211
+ (error) => error.message,
212
+ );
213
+ const killed = open("/hang");
214
+ await loading;
215
+ Bun.WebView.closeAll();
216
+ console.error(await killed);
217
+ console.log(await open("/"));
218
+ server.stop(true);
219
+ Bun.WebView.closeAll();`,
220
+ 0,
221
+ );
222
+ expect(out, log).toBe("up");
223
+ expect(code, log).toBe(0);
224
+ },
225
+ browserTestMs,
226
+ );
197
227
  });
@@ -1,15 +1,28 @@
1
1
  export const sysNamePattern = /^[a-z0-9][a-zA-Z0-9_-]*$/;
2
2
  export const identifierPattern = /^_{0,2}[a-zA-Z][a-zA-Z0-9_-]*$/;
3
+ const packageNamePattern = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
4
+
5
+ export type PlainNameKind = "app" | "lib" | "module" | "service" | "scalar";
3
6
 
4
7
  export const isWorkflowSysName = (value: unknown): value is string =>
5
8
  typeof value === "string" && sysNamePattern.test(value);
6
9
  export const isWorkflowModuleName = (value: unknown): value is string =>
7
10
  typeof value === "string" && identifierPattern.test(value);
8
11
 
9
- export const assertPlainName = (kind: "app" | "lib" | "module" | "service" | "scalar", name: string) => {
12
+ export const plainNameProblem = (kind: PlainNameKind, name: string) => {
10
13
  const isSys = kind === "app" || kind === "lib";
11
- if (isSys ? isWorkflowSysName(name) : isWorkflowModuleName(name)) return;
14
+ if (isSys ? isWorkflowSysName(name) : isWorkflowModuleName(name)) return null;
15
+ return `${JSON.stringify(name)} is no ${kind} name; use letters, digits, - and _, starting with ${isSys ? "a lowercase letter or digit" : "a letter"}`;
16
+ };
17
+
18
+ export const assertPlainName = (kind: PlainNameKind, name: string) => {
19
+ const problem = plainNameProblem(kind, name);
20
+ if (problem) throw new Error(problem);
21
+ };
22
+
23
+ export const assertPackageName = (name: string) => {
24
+ if (packageNamePattern.test(name)) return;
12
25
  throw new Error(
13
- `${JSON.stringify(name)} is no ${kind} name; use letters, digits, - and _, starting with ${isSys ? "a lowercase letter or digit" : "a letter"}`,
26
+ `${JSON.stringify(name)} is no package name; use name or @scope/name, each of lowercase letters, digits, -, . and _, starting with a letter or digit`,
14
27
  );
15
28
  };
@@ -1,18 +0,0 @@
1
- import ts from "typescript";
2
-
3
- const isFieldCall = (node: ts.Expression): boolean =>
4
- ts.isCallExpression(node) &&
5
- (ts.isIdentifier(node.expression)
6
- ? node.expression.text === "field"
7
- : ts.isPropertyAccessExpression(node.expression) && isFieldCall(node.expression.expression));
8
-
9
- export const constantFieldNames = (content: string) => {
10
- const names: string[] = [];
11
- const visit = (node: ts.Node) => {
12
- if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && isFieldCall(node.initializer))
13
- names.push(node.name.text);
14
- ts.forEachChild(node, visit);
15
- };
16
- visit(ts.createSourceFile("constant.ts", content, ts.ScriptTarget.Latest));
17
- return names;
18
- };