catalyst-core 1.0.1 → 1.0.2-canary.2
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/bin/catalyst.js +105 -1
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +50 -0
- package/dist/cli/buildErrors.js +170 -0
- package/dist/cli/buildWarnings.js +28 -0
- package/dist/cli/devOutput.js +524 -0
- package/dist/cli/diagnostic.js +388 -0
- package/dist/cli/hints.js +69 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/spinner.js +141 -0
- package/dist/cli/theme.js +155 -0
- package/dist/errors/index.js +7 -3
- package/dist/errors/registry.js +8 -0
- package/dist/errors-index.json +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.jsx +1 -1
- package/dist/logger.js +61 -6
- package/dist/native/androidProject/app/build.gradle.kts +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
- package/dist/native/androidSetup.js +46 -57
- package/dist/native/buildAndroid/assets.js +22 -13
- package/dist/native/buildAndroid/build.js +98 -26
- package/dist/native/buildAndroid/config.js +43 -12
- package/dist/native/buildAndroid/index.js +49 -191
- package/dist/native/buildAppAndroid.js +19 -8
- package/dist/native/buildAppIos.js +35 -16
- package/dist/native/buildIos/assets.js +1 -6
- package/dist/native/buildIos/build.js +61 -94
- package/dist/native/buildIos/config.js +26 -8
- package/dist/native/buildIos/index.js +143 -64
- package/dist/native/buildIos/plugins.js +12 -15
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
- package/dist/native/pluginComposerAndroid.js +2 -3
- package/dist/native/renameAndroidProject.js +42 -20
- package/dist/native/setupEmulatorIos.js +28 -49
- package/dist/native/setupServer.js +12 -15
- package/dist/native/terminalProgress.js +186 -147
- package/dist/native/utils.js +335 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +314 -42
- package/dist/scripts/generateOfflineManifest.js +11 -1
- package/dist/scripts/loadEnvironmentVariables.js +24 -2
- package/dist/scripts/preServerInit.js +32 -10
- package/dist/scripts/preflight.js +115 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +65 -9
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/document/Body.d.ts +0 -11
- package/dist/server/renderer/document/Body.jsx +0 -10
- package/dist/server/renderer/document/Head.d.ts +0 -13
- package/dist/server/renderer/document/Head.jsx +0 -12
- package/dist/server/renderer/handler.jsx +87 -11
- package/dist/server/utils/validator.js +14 -11
- package/dist/vite/manifest-categorization-plugin.js +7 -5
- package/dist/vite/resolveDevServerConfig.js +9 -2
- package/dist/vite/vite.config.client.js +19 -1
- package/dist/vite/vite.config.js +96 -19
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +20 -14
- package/dist/web-router/components/MetaTag.test.jsx +108 -32
- package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
- package/dist/web-router/components/RouterDataProvider.jsx +35 -1
- package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
- package/dist/web-router/context.d.ts +15 -1
- package/dist/web-router/context.jsx +11 -0
- package/dist/webmcp/WebMcpProvider.jsx +234 -0
- package/dist/webmcp/declarative.js +207 -0
- package/dist/webmcp/declarative.test.js +122 -0
- package/dist/webmcp/errors.js +59 -0
- package/dist/webmcp/errors.test.js +47 -0
- package/dist/webmcp/index.js +40 -0
- package/dist/webmcp/native.js +37 -0
- package/dist/webmcp/native.test.js +41 -0
- package/dist/webmcp/pageInfo.js +119 -0
- package/dist/webmcp/pageInfo.test.js +85 -0
- package/dist/webmcp/registry.dom.test.js +175 -0
- package/dist/webmcp/registry.js +540 -0
- package/dist/webmcp/registry.test.js +228 -0
- package/dist/webmcp/routeContext.js +11 -0
- package/dist/webmcp/shim-entry.js +8 -0
- package/dist/webmcp/shim.js +149 -0
- package/dist/webmcp/useTool.js +124 -0
- package/package.json +4 -7
- package/test/cli/buildErrors.test.ts +173 -0
- package/test/cli/buildWarnings.test.cjs +62 -0
- package/test/cli/devOutput.test.ts +549 -0
- package/test/cli/diagnostic.test.ts +434 -0
- package/test/cli/output.test.cjs +90 -0
- package/test/cli/spinner.test.ts +177 -0
- package/test/cli/theme.test.ts +163 -0
- package/test/native/redactSecrets.test.ts +72 -0
- package/test/native/utils.test.ts +532 -0
- package/test/scripts/build.test.ts +275 -0
- package/test/scripts/preflight.test.ts +100 -2
- package/test/scripts/serve.test.ts +102 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
- package/test/server/handler.test.ts +17 -0
- package/test/server/logger.test.ts +185 -0
- package/test/server/manifestCache.test.ts +13 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/vitest.config.ts +4 -0
- package/dist/scripts/checkVersion.js +0 -3
- package/dist/scripts/cleanCache.js +0 -30
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const os = require("os")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
const pc = require("ansis")
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The one visual language shared by every Catalyst command.
|
|
9
|
+
*
|
|
10
|
+
* Rules, so anything added later still looks like it belongs:
|
|
11
|
+
* - One accent (cyan) for identity and links. Green, red and yellow carry
|
|
12
|
+
* state only, never decoration.
|
|
13
|
+
* - Single-width glyphs, never emoji. Emoji render double-width in some
|
|
14
|
+
* terminals, which breaks every column that follows them.
|
|
15
|
+
* - Dim is structure: rules, labels and paths recede so the one thing that
|
|
16
|
+
* matters on a line is the bright one.
|
|
17
|
+
* - Every line starts at the same gutter, so output reads as a column.
|
|
18
|
+
*
|
|
19
|
+
* Authored as CommonJS because src/native/ ships as CJS (see
|
|
20
|
+
* src/native/package.json) while src/scripts/ is ESM -- CJS is the only form
|
|
21
|
+
* both halves can consume.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Colour codes sit in front of text, so anything that matches against output
|
|
25
|
+
// must strip them first. Defined once here rather than per-module.
|
|
26
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g")
|
|
27
|
+
|
|
28
|
+
const GUTTER = " "
|
|
29
|
+
|
|
30
|
+
const glyph = {
|
|
31
|
+
pending: "○",
|
|
32
|
+
running: "◐",
|
|
33
|
+
done: "✓",
|
|
34
|
+
fail: "✗",
|
|
35
|
+
warn: "▲",
|
|
36
|
+
info: "•",
|
|
37
|
+
arrow: "›",
|
|
38
|
+
branch: "├",
|
|
39
|
+
last: "└",
|
|
40
|
+
pipe: "│",
|
|
41
|
+
rule: "─",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Frames for the running indicator. Braille spinners look busier than they
|
|
45
|
+
// read; these four are calm and legible at any width.
|
|
46
|
+
const SPINNER_FRAMES = ["◐", "◓", "◑", "◒"]
|
|
47
|
+
|
|
48
|
+
const t = {
|
|
49
|
+
accent: pc.cyan,
|
|
50
|
+
dim: pc.gray,
|
|
51
|
+
ok: pc.green,
|
|
52
|
+
bad: pc.red,
|
|
53
|
+
warn: pc.yellow,
|
|
54
|
+
bold: pc.bold,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* True when we may draw in place: a real terminal, not a pipe, not CI.
|
|
59
|
+
* Vite's gate, so the web and native commands agree on what "interactive"
|
|
60
|
+
* means. ansis independently handles NO_COLOR/FORCE_COLOR/TERM=dumb.
|
|
61
|
+
*/
|
|
62
|
+
function isInteractive(stream = process.stdout) {
|
|
63
|
+
return Boolean(stream.isTTY && !process.env.CI)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* True when the user asked for more detail: `--verbose`/`--debug` on this
|
|
68
|
+
* process, or CATALYST_OUTPUT_MODE forwarded by a parent that saw them. The
|
|
69
|
+
* default view keeps only what someone acts on; the rest is here.
|
|
70
|
+
*/
|
|
71
|
+
function isVerbose(argv = process.argv, env = process.env) {
|
|
72
|
+
if (argv.includes("--verbose") || argv.includes("--debug")) return true
|
|
73
|
+
return env.CATALYST_OUTPUT_MODE === "verbose" || env.CATALYST_OUTPUT_MODE === "debug"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let cachedVersion
|
|
77
|
+
/** catalyst-core's own version: ../../package.json from both src/cli and dist/cli. */
|
|
78
|
+
function version() {
|
|
79
|
+
if (cachedVersion === undefined) {
|
|
80
|
+
try {
|
|
81
|
+
cachedVersion = require("../../package.json").version || null
|
|
82
|
+
} catch (error) {
|
|
83
|
+
cachedVersion = null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return cachedVersion
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The titled rule every command opens with: `catalyst build v1.0.1 › production`. */
|
|
90
|
+
function header(title, subtitle) {
|
|
91
|
+
const v = version()
|
|
92
|
+
const line =
|
|
93
|
+
`${t.bold(t.accent(title))}` +
|
|
94
|
+
`${v ? t.dim(` v${v}`) : ""}` +
|
|
95
|
+
`${subtitle ? t.dim(` ${glyph.arrow} ${subtitle}`) : ""}`
|
|
96
|
+
return `\n${GUTTER}${line}\n${GUTTER}${t.dim(glyph.rule.repeat(46))}\n`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A dim-key / bright-value row, values column-aligned. */
|
|
100
|
+
function row(key, value, width = 9) {
|
|
101
|
+
return `${GUTTER}${t.dim(key.padEnd(width))}${value}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function duration(ms) {
|
|
105
|
+
if (ms < 1000) return `${ms}ms`
|
|
106
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
|
|
107
|
+
return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A path as a person would type it: relative to the app when inside it,
|
|
112
|
+
* `~/...` when under home, absolute otherwise. Absolute paths into DerivedData
|
|
113
|
+
* or node_modules pushed the useful tail off the edge of the terminal.
|
|
114
|
+
*/
|
|
115
|
+
function displayPath(file, root = process.cwd()) {
|
|
116
|
+
if (!file) return file
|
|
117
|
+
const relative = path.relative(root, file)
|
|
118
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) return relative
|
|
119
|
+
const home = os.homedir()
|
|
120
|
+
return file.startsWith(home) ? `~${file.slice(home.length)}` : file
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bytes(n) {
|
|
124
|
+
if (n < 1024) return `${n} B`
|
|
125
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`
|
|
126
|
+
return `${(n / 1024 / 1024).toFixed(2)} MB`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Below this a timing is noise: "0ms" next to a step says nothing a reader
|
|
130
|
+
// can use, and a column of them hides the one step that was actually slow.
|
|
131
|
+
const MIN_SHOWN_MS = 100
|
|
132
|
+
|
|
133
|
+
/** A completed step: `✓ Label 1.2s` */
|
|
134
|
+
function step(label, ms, state = "done") {
|
|
135
|
+
const mark =
|
|
136
|
+
state === "fail" ? t.bad(glyph.fail) : state === "warn" ? t.warn(glyph.warn) : t.ok(glyph.done)
|
|
137
|
+
return `${GUTTER}${mark} ${label}${ms == null || ms < MIN_SHOWN_MS ? "" : ` ${t.dim(duration(ms))}`}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
ANSI,
|
|
142
|
+
GUTTER,
|
|
143
|
+
glyph,
|
|
144
|
+
SPINNER_FRAMES,
|
|
145
|
+
t,
|
|
146
|
+
isInteractive,
|
|
147
|
+
isVerbose,
|
|
148
|
+
version,
|
|
149
|
+
header,
|
|
150
|
+
row,
|
|
151
|
+
duration,
|
|
152
|
+
bytes,
|
|
153
|
+
displayPath,
|
|
154
|
+
step,
|
|
155
|
+
}
|
package/dist/errors/index.js
CHANGED
|
@@ -138,7 +138,11 @@ const BOX_WIDTH = 70
|
|
|
138
138
|
function box(bodyLines) {
|
|
139
139
|
const top = pc.red(`┌${"─".repeat(BOX_WIDTH)}`)
|
|
140
140
|
const bottom = pc.red(`└${"─".repeat(BOX_WIDTH)}`)
|
|
141
|
-
|
|
141
|
+
// A multi-line entry (details listing several values) keeps the border on
|
|
142
|
+
// every line instead of spilling out of the box.
|
|
143
|
+
const body = bodyLines
|
|
144
|
+
.flatMap((line) => String(line).split("\n"))
|
|
145
|
+
.map((line) => (line === "" ? pc.red("│") : `${pc.red("│")} ${line}`))
|
|
142
146
|
return [top, ...body, bottom].join("\n")
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -163,7 +167,7 @@ function formatCauseChain(cause) {
|
|
|
163
167
|
*/
|
|
164
168
|
function formatVerbose(err) {
|
|
165
169
|
const lines = [
|
|
166
|
-
pc.bold(
|
|
170
|
+
pc.bold(err.message),
|
|
167
171
|
`Code: ${err.code}`,
|
|
168
172
|
`Category: ${err.category || "UNKNOWN"}`,
|
|
169
173
|
`Time: ${new Date().toISOString()}`,
|
|
@@ -193,7 +197,7 @@ function formatVerbose(err) {
|
|
|
193
197
|
*/
|
|
194
198
|
function formatDebug(err, env) {
|
|
195
199
|
const lines = [
|
|
196
|
-
pc.bold(
|
|
200
|
+
pc.bold(err.message),
|
|
197
201
|
`Code: ${err.code}`,
|
|
198
202
|
`Category: ${err.category || "UNKNOWN"}`,
|
|
199
203
|
`Time: ${new Date().toISOString()}`,
|
package/dist/errors/registry.js
CHANGED
|
@@ -22,6 +22,7 @@ export const ERROR_CODES = {
|
|
|
22
22
|
PREFLIGHT_GET_ROUTES_NOT_FUNCTION: "PREFLIGHT-019",
|
|
23
23
|
PREFLIGHT_CUSTOM_DOCUMENT_MISSING: "PREFLIGHT-020",
|
|
24
24
|
PREFLIGHT_CUSTOM_DOCUMENT_NOT_FUNCTION: "PREFLIGHT-021",
|
|
25
|
+
PREFLIGHT_BUILD_STALE: "PREFLIGHT-022",
|
|
25
26
|
|
|
26
27
|
PROCESS_SERVER_INIT_FAILED: "PROCESS-001",
|
|
27
28
|
PROCESS_USER_HOOK_FAILED: "PROCESS-002",
|
|
@@ -204,6 +205,13 @@ export const ERROR_DEFINITIONS = {
|
|
|
204
205
|
defaultDetails: "document was found but is not a function/component.",
|
|
205
206
|
suggestedAction: "Ensure document is exported as a React component (function).",
|
|
206
207
|
},
|
|
208
|
+
[ERROR_CODES.PREFLIGHT_BUILD_STALE]: {
|
|
209
|
+
category: "PREFLIGHT",
|
|
210
|
+
defaultMessage: "The build is stale: config/config.json changed since it was built",
|
|
211
|
+
defaultDetails:
|
|
212
|
+
"PUBLIC_STATIC_ASSET_URL, PUBLIC_STATIC_ASSET_PATH and the CLIENT_ENV_VARIABLES values are inlined into the bundles at build time, so serving this build would use the old values.",
|
|
213
|
+
suggestedAction: "Run `npm run build` again, then `npm run serve`.",
|
|
214
|
+
},
|
|
207
215
|
|
|
208
216
|
[ERROR_CODES.PROCESS_SERVER_INIT_FAILED]: {
|
|
209
217
|
category: "PROCESS",
|
package/dist/errors-index.json
CHANGED
|
@@ -216,6 +216,13 @@
|
|
|
216
216
|
"suggestedAction": "Ensure document is exported as a React component (function).",
|
|
217
217
|
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/PREFLIGHT/PREFLIGHT-021.md"
|
|
218
218
|
},
|
|
219
|
+
"PREFLIGHT-022": {
|
|
220
|
+
"category": "PREFLIGHT",
|
|
221
|
+
"message": "The build is stale: config/config.json changed since it was built",
|
|
222
|
+
"details": "PUBLIC_STATIC_ASSET_URL, PUBLIC_STATIC_ASSET_PATH and the CLIENT_ENV_VARIABLES values are inlined into the bundles at build time, so serving this build would use the old values.",
|
|
223
|
+
"suggestedAction": "Run `npm run build` again, then `npm run serve`.",
|
|
224
|
+
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/PREFLIGHT/PREFLIGHT-022.md"
|
|
225
|
+
},
|
|
219
226
|
"PROCESS-001": {
|
|
220
227
|
"category": "PROCESS",
|
|
221
228
|
"message": "preServerInit threw an error during server startup",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { RouterDataProvider, useCurrentRouteData, useRouterData, } from "./web-router/components/RouterDataProvider.jsx";
|
|
1
|
+
export { RouterDataProvider, useCurrentRouteData, useRouterData, useRouterState, } from "./web-router/components/RouterDataProvider.jsx";
|
|
2
2
|
export { MetaTag } from "./web-router/components/MetaTag.jsx";
|
|
3
3
|
export { split, hydrationReady } from "./web-router/components/Split.jsx";
|
|
4
4
|
export { Head } from "./server/renderer/document/Head.jsx";
|
package/dist/index.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { RouterDataProvider, useCurrentRouteData, useRouterData } from "./web-router/components/RouterDataProvider.jsx";
|
|
1
|
+
export { RouterDataProvider, useCurrentRouteData, useRouterData, useRouterState } from "./web-router/components/RouterDataProvider.jsx";
|
|
2
2
|
export { MetaTag } from "./web-router/components/MetaTag.jsx";
|
|
3
3
|
export { split, hydrationReady } from "./web-router/components/Split.jsx";
|
|
4
4
|
export { Head } from "./server/renderer/document/Head.jsx";
|
package/dist/logger.js
CHANGED
|
@@ -13,6 +13,55 @@ const { createLogger, format, transports } = winston
|
|
|
13
13
|
* @returns loggerInstance
|
|
14
14
|
*
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Render a log argument as text.
|
|
18
|
+
*
|
|
19
|
+
* JSON.stringify was wrong in two ways that mattered: an Error serializes to
|
|
20
|
+
* "{}" (message and stack are non-enumerable), so error logs said nothing at
|
|
21
|
+
* all, and a circular reference made the logger itself throw -- turning a
|
|
22
|
+
* logged problem into a crash.
|
|
23
|
+
*/
|
|
24
|
+
function formatLogMessage(msg) {
|
|
25
|
+
if (typeof msg === "string") return msg
|
|
26
|
+
if (msg instanceof Error) return msg.stack || `${msg.name}: ${msg.message}`
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const seen = new WeakSet()
|
|
30
|
+
const text = JSON.stringify(msg, (key, value) => {
|
|
31
|
+
if (value instanceof Error) return value.stack || `${value.name}: ${value.message}`
|
|
32
|
+
if (typeof value === "bigint") return value.toString()
|
|
33
|
+
if (typeof value === "object" && value !== null) {
|
|
34
|
+
if (seen.has(value)) return "[Circular]"
|
|
35
|
+
seen.add(value)
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
})
|
|
39
|
+
// stringify returns undefined for functions and symbols.
|
|
40
|
+
return text === undefined ? String(msg) : text
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return String(msg)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What winston receives for a log argument.
|
|
48
|
+
*
|
|
49
|
+
* A plain object goes in as an object, so file logs keep its fields
|
|
50
|
+
* queryable rather than nesting it as one escaped string under "message".
|
|
51
|
+
* It is a clone built from the console text -- cycle-safe, Errors as their
|
|
52
|
+
* stack -- which also keeps winston from writing level and timestamp onto the
|
|
53
|
+
* caller's own object. Strings, Errors and anything else go in as text.
|
|
54
|
+
*/
|
|
55
|
+
function toLogRecord(msg, text) {
|
|
56
|
+
if (msg === null || typeof msg !== "object" || Array.isArray(msg) || msg instanceof Error) return text
|
|
57
|
+
try {
|
|
58
|
+
const record = JSON.parse(text)
|
|
59
|
+
return record !== null && typeof record === "object" && !Array.isArray(record) ? record : text
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return text
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
16
65
|
const configureLogger = (config = {}) => {
|
|
17
66
|
const { enableDebugLogs = true, enableFileLogging = true, enableConsoleLogging = true } = config
|
|
18
67
|
|
|
@@ -55,22 +104,28 @@ const configureLogger = (config = {}) => {
|
|
|
55
104
|
errorLogger.add(fileTransport("error"))
|
|
56
105
|
}
|
|
57
106
|
|
|
107
|
+
// Winston serializes with format.json(), which renders an Error as {} for
|
|
108
|
+
// the same reason JSON.stringify does, so it gets the formatted text for
|
|
109
|
+
// those -- and a safe structured copy of a plain object (toLogRecord).
|
|
58
110
|
const Logger = {
|
|
59
111
|
debug: () => {},
|
|
60
112
|
error: (msg) => {
|
|
61
|
-
|
|
62
|
-
|
|
113
|
+
const text = formatLogMessage(msg)
|
|
114
|
+
console.log(pc.red(pc.bold("ERROR: " + text)))
|
|
115
|
+
errorLogger.error(toLogRecord(msg, text))
|
|
63
116
|
},
|
|
64
117
|
info: (msg) => {
|
|
65
|
-
|
|
66
|
-
|
|
118
|
+
const text = formatLogMessage(msg)
|
|
119
|
+
console.log(pc.green(pc.bold("INFO: " + text)))
|
|
120
|
+
infoLogger.info(toLogRecord(msg, text))
|
|
67
121
|
},
|
|
68
122
|
}
|
|
69
123
|
|
|
70
124
|
if (enableDebugLogs && JSON.parse(enableDebugLogs)) {
|
|
71
125
|
Logger.debug = (msg) => {
|
|
72
|
-
|
|
73
|
-
|
|
126
|
+
const text = formatLogMessage(msg)
|
|
127
|
+
console.log(pc.yellow(pc.bold("DEBUG: " + text)))
|
|
128
|
+
debugLogger.debug(toLogRecord(msg, text))
|
|
74
129
|
}
|
|
75
130
|
}
|
|
76
131
|
|
|
@@ -379,7 +379,8 @@ tasks.register<JacocoReport>("jacocoTestReport") {
|
|
|
379
379
|
// so a plain "$*" is safe here.
|
|
380
380
|
"**/CustomWebView.class", "**/CustomWebView\$*.class", "**/CustomWebViewKt.class",
|
|
381
381
|
"**/MainActivity.class", "**/MainActivity\$*.class",
|
|
382
|
-
"**/SplashActivity.class", "**/SplashActivity\$*.class",
|
|
382
|
+
"**/SplashActivity.class", "**/SplashActivity\$*.class", "**/SplashActivityKt.class",
|
|
383
|
+
"**/SplashOverlay.class", "**/SplashOverlay\$*.class",
|
|
383
384
|
"**/NativeCameraManager.class", "**/NativeCameraManager\$*.class",
|
|
384
385
|
"**/camera/CameraSessionManager.class", "**/camera/CameraSessionManager\$*.class",
|
|
385
386
|
"**/TransitionManager.class", "**/TransitionManager\$*.class",
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -68,6 +68,7 @@ class CustomWebView(
|
|
|
68
68
|
private var visibleOfflineSnapshotUrl: String? = null
|
|
69
69
|
@Volatile private var defaultRequestHeaders: Map<String, String> = emptyMap()
|
|
70
70
|
var onPageStarted: (() -> Unit)? = null
|
|
71
|
+
var onInitialPageLoaded: (() -> Unit)? = null
|
|
71
72
|
|
|
72
73
|
// Counters for asset loading statistics
|
|
73
74
|
private var assetLoadAttempts = 0
|
|
@@ -1142,6 +1143,7 @@ class CustomWebView(
|
|
|
1142
1143
|
if(!isInitialPageLoaded){
|
|
1143
1144
|
isInitialPageLoaded = true
|
|
1144
1145
|
metricsMonitor.markAppStartComplete()
|
|
1146
|
+
onInitialPageLoaded?.invoke()
|
|
1145
1147
|
}
|
|
1146
1148
|
url?.let { metricsMonitor.trackPageLoadEnd(it) }
|
|
1147
1149
|
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt
CHANGED
|
@@ -52,6 +52,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
52
52
|
private var currentUrl: String = ""
|
|
53
53
|
private var edgeToEdgeEnabled = false
|
|
54
54
|
private var latestSafeAreaInsets: SafeAreaInsets = SafeAreaInsets.ZERO
|
|
55
|
+
private var splashOverlay: SplashOverlay? = null
|
|
55
56
|
|
|
56
57
|
private fun enableHardwareAcceleration() {
|
|
57
58
|
if (!isHardwareAccelerationEnabled) {
|
|
@@ -319,6 +320,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
319
320
|
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
320
321
|
supportActionBar?.hide()
|
|
321
322
|
setContentView(binding.root)
|
|
323
|
+
|
|
324
|
+
// HTML splash handed off from SplashActivity (fresh launch only)
|
|
325
|
+
if (savedInstanceState == null && intent.getBooleanExtra(SplashActivity.EXTRA_HTML_SPLASH, false)) {
|
|
326
|
+
splashOverlay = SplashOverlay(this, properties).also { it.show() }
|
|
327
|
+
}
|
|
322
328
|
|
|
323
329
|
// Initialize keyboard utility (pass webView so keyboard events reach WebPerfCollector)
|
|
324
330
|
keyboardUtil = KeyboardUtil(this, binding.webviewContainer, binding.webview, edgeToEdgeEnabled)
|
|
@@ -334,6 +340,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
334
340
|
progressBar = binding.progress,
|
|
335
341
|
properties = properties
|
|
336
342
|
)
|
|
343
|
+
customWebView.onInitialPageLoaded = { splashOverlay?.onMainPageLoaded() }
|
|
337
344
|
|
|
338
345
|
// Handle state restoration
|
|
339
346
|
if (savedInstanceState == null) {
|
|
@@ -482,6 +489,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
|
|
|
482
489
|
}
|
|
483
490
|
|
|
484
491
|
override fun onDestroy() {
|
|
492
|
+
splashOverlay?.release()
|
|
493
|
+
splashOverlay = null
|
|
485
494
|
io.yourname.androidproject.utils.PerfEventBuffer.reset()
|
|
486
495
|
// Log all performance metrics before destroying
|
|
487
496
|
if (BuildConfig.DEBUG) {
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package io.yourname.androidproject
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
4
5
|
import android.content.Intent
|
|
5
6
|
import android.graphics.Color
|
|
6
7
|
import android.os.Build
|
|
@@ -18,6 +19,11 @@ import java.util.Properties
|
|
|
18
19
|
|
|
19
20
|
class SplashActivity : AppCompatActivity() {
|
|
20
21
|
|
|
22
|
+
companion object {
|
|
23
|
+
const val EXTRA_HTML_SPLASH = "io.yourname.androidproject.EXTRA_HTML_SPLASH"
|
|
24
|
+
const val HTML_SPLASH_ASSET = "splash/splashscreen.html"
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
private lateinit var binding: ActivitySplashBinding
|
|
22
28
|
private lateinit var properties: Properties
|
|
23
29
|
private var isFinishing = false
|
|
@@ -38,50 +44,40 @@ class SplashActivity : AppCompatActivity() {
|
|
|
38
44
|
return
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
// HTML splash: forward straight to MainActivity without drawing, so the system starting
|
|
48
|
+
// window stays up until MainActivity draws the image + HTML overlay (no gap between two
|
|
49
|
+
// drawn activities)
|
|
50
|
+
if (hasHtmlSplash()) {
|
|
51
|
+
startHtmlSplashMainActivity()
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
if (EdgeToEdgeUtils.isEnabled(properties)) {
|
|
42
56
|
EdgeToEdgeUtils.apply(this, properties)
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
binding = ActivitySplashBinding.inflate(layoutInflater)
|
|
46
60
|
setContentView(binding.root)
|
|
47
|
-
|
|
48
|
-
configureSplashScreen()
|
|
61
|
+
|
|
62
|
+
configureSplashScreen(this, binding, properties)
|
|
49
63
|
startSplashTimer()
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
private fun
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
binding.splashContainer.setBackgroundColor(Color.parseColor(backgroundColor))
|
|
60
|
-
} catch (e: Exception) {
|
|
61
|
-
binding.splashContainer.setBackgroundColor(Color.WHITE)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
val widthPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, imageWidth.toFloat(), resources.displayMetrics).toInt()
|
|
65
|
-
val heightPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, imageHeight.toFloat(), resources.displayMetrics).toInt()
|
|
66
|
-
|
|
67
|
-
binding.splashImage.layoutParams.apply {
|
|
68
|
-
width = widthPx
|
|
69
|
-
height = heightPx
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
val drawable = ContextCompat.getDrawable(this,
|
|
73
|
-
resources.getIdentifier("splashscreen", "drawable", packageName).takeIf { it != 0 }
|
|
74
|
-
?: R.mipmap.ic_launcher
|
|
75
|
-
) ?: return
|
|
66
|
+
private fun hasHtmlSplash(): Boolean = try {
|
|
67
|
+
assets.open(HTML_SPLASH_ASSET).close()
|
|
68
|
+
true
|
|
69
|
+
} catch (e: Exception) {
|
|
70
|
+
false
|
|
71
|
+
}
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
private fun startHtmlSplashMainActivity() {
|
|
74
|
+
isFinishing = true
|
|
75
|
+
startActivity(
|
|
76
|
+
Intent(this, MainActivity::class.java)
|
|
77
|
+
.putExtra(EXTRA_HTML_SPLASH, true)
|
|
78
|
+
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
|
79
|
+
)
|
|
80
|
+
finish()
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
private fun startSplashTimer() {
|
|
@@ -114,3 +110,38 @@ class SplashActivity : AppCompatActivity() {
|
|
|
114
110
|
finish()
|
|
115
111
|
}
|
|
116
112
|
}
|
|
113
|
+
|
|
114
|
+
internal fun configureSplashScreen(context: Context, binding: ActivitySplashBinding, properties: Properties) {
|
|
115
|
+
val backgroundColor = properties.getProperty("splashScreen.backgroundColor", "#ffffff")
|
|
116
|
+
val imageWidth = properties.getProperty("splashScreen.imageWidth", "120").toIntOrNull() ?: 120
|
|
117
|
+
val imageHeight = properties.getProperty("splashScreen.imageHeight", "120").toIntOrNull() ?: 120
|
|
118
|
+
val cornerRadius = properties.getProperty("splashScreen.cornerRadius", "20").toFloatOrNull() ?: 20f
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
binding.splashContainer.setBackgroundColor(Color.parseColor(backgroundColor))
|
|
122
|
+
} catch (e: Exception) {
|
|
123
|
+
binding.splashContainer.setBackgroundColor(Color.WHITE)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
val widthPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, imageWidth.toFloat(), context.resources.displayMetrics).toInt()
|
|
127
|
+
val heightPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, imageHeight.toFloat(), context.resources.displayMetrics).toInt()
|
|
128
|
+
|
|
129
|
+
binding.splashImage.layoutParams.apply {
|
|
130
|
+
width = widthPx
|
|
131
|
+
height = heightPx
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
val drawable = ContextCompat.getDrawable(context,
|
|
135
|
+
context.resources.getIdentifier("splashscreen", "drawable", context.packageName).takeIf { it != 0 }
|
|
136
|
+
?: R.mipmap.ic_launcher
|
|
137
|
+
) ?: return
|
|
138
|
+
|
|
139
|
+
if (cornerRadius > 0) {
|
|
140
|
+
val radiusPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, cornerRadius, context.resources.displayMetrics)
|
|
141
|
+
val roundedDrawable = RoundedBitmapDrawableFactory.create(context.resources, drawable.toBitmap(widthPx, heightPx))
|
|
142
|
+
roundedDrawable.cornerRadius = radiusPx
|
|
143
|
+
binding.splashImage.setImageDrawable(roundedDrawable)
|
|
144
|
+
} else {
|
|
145
|
+
binding.splashImage.setImageDrawable(drawable)
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
package io.yourname.androidproject
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
8
|
+
import android.os.SystemClock
|
|
9
|
+
import android.view.ViewGroup
|
|
10
|
+
import android.webkit.WebView
|
|
11
|
+
import android.webkit.WebViewClient
|
|
12
|
+
import android.widget.FrameLayout
|
|
13
|
+
import io.yourname.androidproject.databinding.ActivitySplashBinding
|
|
14
|
+
import java.util.Properties
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* HTML splash shown above the main WebView until its first page finishes,
|
|
18
|
+
* held for at least splashScreen.minDuration ms and capped at MAX_DURATION_MS.
|
|
19
|
+
*/
|
|
20
|
+
class SplashOverlay(private val activity: Activity, private val properties: Properties) {
|
|
21
|
+
|
|
22
|
+
companion object {
|
|
23
|
+
private const val MAX_DURATION_MS = 10_000L
|
|
24
|
+
private const val FADE_DURATION_MS = 300L
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
28
|
+
private val minDuration = properties.getProperty("splashScreen.minDuration")?.toLongOrNull() ?: 0
|
|
29
|
+
private var root: FrameLayout? = null
|
|
30
|
+
private var webView: WebView? = null
|
|
31
|
+
private var shownAt = 0L
|
|
32
|
+
private var dismissed = false
|
|
33
|
+
|
|
34
|
+
@SuppressLint("SetJavaScriptEnabled")
|
|
35
|
+
fun show() {
|
|
36
|
+
val splashBinding = ActivitySplashBinding.inflate(activity.layoutInflater)
|
|
37
|
+
configureSplashScreen(activity, splashBinding, properties)
|
|
38
|
+
|
|
39
|
+
// No JS interfaces; only the bundled asset may load
|
|
40
|
+
val splashWebView = WebView(activity).apply {
|
|
41
|
+
settings.javaScriptEnabled = true
|
|
42
|
+
settings.blockNetworkLoads = true
|
|
43
|
+
settings.allowFileAccess = false
|
|
44
|
+
settings.allowContentAccess = false
|
|
45
|
+
setBackgroundColor(Color.TRANSPARENT)
|
|
46
|
+
// Chromium paints the page background before its content, so keep the native image
|
|
47
|
+
// in view until the HTML's first frame with content is on screen
|
|
48
|
+
alpha = 0f
|
|
49
|
+
webViewClient = object : WebViewClient() {
|
|
50
|
+
override fun onPageFinished(view: WebView, url: String?) {
|
|
51
|
+
view.postVisualStateCallback(0, object : WebView.VisualStateCallback() {
|
|
52
|
+
override fun onComplete(requestId: Long) {
|
|
53
|
+
view.alpha = 1f
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
loadUrl("file:///android_asset/${SplashActivity.HTML_SPLASH_ASSET}")
|
|
59
|
+
}
|
|
60
|
+
val matchParent = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
61
|
+
val overlayRoot = FrameLayout(activity).apply {
|
|
62
|
+
addView(splashBinding.root, matchParent)
|
|
63
|
+
addView(splashWebView, matchParent)
|
|
64
|
+
// Swallow touches so they don't reach the main WebView underneath
|
|
65
|
+
isClickable = true
|
|
66
|
+
isFocusable = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
activity.addContentView(overlayRoot, matchParent)
|
|
70
|
+
|
|
71
|
+
root = overlayRoot
|
|
72
|
+
webView = splashWebView
|
|
73
|
+
shownAt = SystemClock.elapsedRealtime()
|
|
74
|
+
handler.postDelayed({ dismiss() }, MAX_DURATION_MS)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fun onMainPageLoaded() {
|
|
78
|
+
if (dismissed) return
|
|
79
|
+
val remaining = minDuration - (SystemClock.elapsedRealtime() - shownAt)
|
|
80
|
+
handler.postDelayed({ dismiss() }, remaining.coerceAtLeast(0))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun dismiss() {
|
|
84
|
+
if (dismissed) return
|
|
85
|
+
dismissed = true
|
|
86
|
+
handler.removeCallbacksAndMessages(null)
|
|
87
|
+
val overlayRoot = root ?: return
|
|
88
|
+
overlayRoot.animate()
|
|
89
|
+
.alpha(0f)
|
|
90
|
+
.setDuration(FADE_DURATION_MS)
|
|
91
|
+
.withEndAction { destroy() }
|
|
92
|
+
.start()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun release() {
|
|
96
|
+
dismissed = true
|
|
97
|
+
handler.removeCallbacksAndMessages(null)
|
|
98
|
+
root?.animate()?.cancel()
|
|
99
|
+
destroy()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun destroy() {
|
|
103
|
+
val overlayRoot = root ?: return
|
|
104
|
+
(overlayRoot.parent as? ViewGroup)?.removeView(overlayRoot)
|
|
105
|
+
webView?.destroy()
|
|
106
|
+
webView = null
|
|
107
|
+
root = null
|
|
108
|
+
}
|
|
109
|
+
}
|