one 2.11.4 → 2.12.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/android/src/main/java/com/margelo/nitro/one/HybridOneAudio.kt +309 -124
- package/dist/cjs/cli/build.cjs +31 -33
- package/dist/cjs/cli/build.native.cjs +31 -33
- package/dist/cjs/cli/build.native.js +31 -33
- package/dist/cjs/cli/build.native.js.map +1 -1
- package/dist/cjs/cli/buildPage.cjs +11 -7
- package/dist/cjs/cli/buildPage.native.cjs +11 -7
- package/dist/cjs/cli/buildPage.native.js +11 -7
- package/dist/cjs/cli/buildPage.native.js.map +1 -1
- package/dist/cjs/cli/buildPageWorker.cjs +1 -1
- package/dist/cjs/cli/buildPageWorker.native.cjs +1 -1
- package/dist/cjs/cli/buildPageWorker.native.js +1 -1
- package/dist/cjs/cli/buildPageWorker.native.js.map +1 -1
- package/dist/cjs/headless-server.cjs +2 -5
- package/dist/cjs/headless-server.native.cjs +2 -5
- package/dist/cjs/headless-server.native.js +2 -5
- package/dist/cjs/headless-server.native.js.map +1 -1
- package/dist/cjs/platform/audio/index.cjs +83 -76
- package/dist/cjs/platform/audio/index.native.cjs +2 -2
- package/dist/cjs/platform/audio/index.native.js +2 -2
- package/dist/cjs/platform/audio/index.native.js.map +1 -1
- package/dist/cjs/platform/audio/nowPlaying.cjs +126 -0
- package/dist/cjs/platform/audio/nowPlaying.native.cjs +160 -0
- package/dist/cjs/platform/audio/nowPlaying.native.js +162 -0
- package/dist/cjs/platform/audio/nowPlaying.native.js.map +1 -0
- package/dist/cjs/platform/audio/player.cjs +15 -0
- package/dist/cjs/platform/audio/player.native.cjs +14 -0
- package/dist/cjs/platform/audio/player.native.js +14 -0
- package/dist/cjs/platform/audio/player.native.js.map +1 -1
- package/dist/cjs/platform/audio/unavailable.cjs +1 -1
- package/dist/cjs/platform/audio/unavailable.native.cjs +1 -1
- package/dist/cjs/platform/audio/unavailable.native.js +1 -1
- package/dist/cjs/platform/audio/unavailable.native.js.map +1 -1
- package/dist/cjs/platform/location/index.native.cjs +9 -2
- package/dist/cjs/platform/location/index.native.js +9 -2
- package/dist/cjs/platform/location/index.native.js.map +1 -1
- package/dist/cjs/serve-worker.cjs +0 -3
- package/dist/cjs/serve-worker.native.cjs +0 -3
- package/dist/cjs/serve-worker.native.js +0 -3
- package/dist/cjs/serve-worker.native.js.map +1 -1
- package/dist/cjs/server/oneServe.cjs +0 -6
- package/dist/cjs/server/oneServe.native.cjs +0 -6
- package/dist/cjs/server/oneServe.native.js +0 -6
- package/dist/cjs/server/oneServe.native.js.map +1 -1
- package/dist/cjs/server/workerHandler.cjs +12 -10
- package/dist/cjs/server/workerHandler.native.cjs +14 -10
- package/dist/cjs/server/workerHandler.native.js +14 -10
- package/dist/cjs/server/workerHandler.native.js.map +1 -1
- package/dist/cjs/vite/plugins/workerdDevPlugin.cjs +33 -37
- package/dist/cjs/vite/plugins/workerdDevPlugin.native.cjs +33 -37
- package/dist/cjs/vite/plugins/workerdDevPlugin.native.js +33 -37
- package/dist/cjs/vite/plugins/workerdDevPlugin.native.js.map +1 -1
- package/dist/esm/cli/build.mjs +31 -33
- package/dist/esm/cli/build.mjs.map +1 -1
- package/dist/esm/cli/build.native.js +31 -33
- package/dist/esm/cli/build.native.js.map +1 -1
- package/dist/esm/cli/buildPage.mjs +11 -7
- package/dist/esm/cli/buildPage.mjs.map +1 -1
- package/dist/esm/cli/buildPage.native.js +11 -7
- package/dist/esm/cli/buildPage.native.js.map +1 -1
- package/dist/esm/cli/buildPageWorker.mjs +1 -1
- package/dist/esm/cli/buildPageWorker.mjs.map +1 -1
- package/dist/esm/cli/buildPageWorker.native.js +1 -1
- package/dist/esm/cli/buildPageWorker.native.js.map +1 -1
- package/dist/esm/headless-server.mjs +2 -3
- package/dist/esm/headless-server.mjs.map +1 -1
- package/dist/esm/headless-server.native.js +2 -3
- package/dist/esm/headless-server.native.js.map +1 -1
- package/dist/esm/platform/audio/index.mjs +83 -76
- package/dist/esm/platform/audio/index.mjs.map +1 -1
- package/dist/esm/platform/audio/index.native.js +2 -2
- package/dist/esm/platform/audio/index.native.js.map +1 -1
- package/dist/esm/platform/audio/nowPlaying.mjs +102 -0
- package/dist/esm/platform/audio/nowPlaying.mjs.map +1 -0
- package/dist/esm/platform/audio/nowPlaying.native.js +134 -0
- package/dist/esm/platform/audio/nowPlaying.native.js.map +1 -0
- package/dist/esm/platform/audio/player.mjs +15 -0
- package/dist/esm/platform/audio/player.mjs.map +1 -1
- package/dist/esm/platform/audio/player.native.js +14 -0
- package/dist/esm/platform/audio/player.native.js.map +1 -1
- package/dist/esm/platform/audio/unavailable.mjs +1 -1
- package/dist/esm/platform/audio/unavailable.mjs.map +1 -1
- package/dist/esm/platform/audio/unavailable.native.js +1 -1
- package/dist/esm/platform/audio/unavailable.native.js.map +1 -1
- package/dist/esm/platform/location/index.native.js +9 -2
- package/dist/esm/platform/location/index.native.js.map +1 -1
- package/dist/esm/serve-worker.mjs +1 -2
- package/dist/esm/serve-worker.mjs.map +1 -1
- package/dist/esm/serve-worker.native.js +1 -2
- package/dist/esm/serve-worker.native.js.map +1 -1
- package/dist/esm/server/oneServe.mjs +0 -6
- package/dist/esm/server/oneServe.mjs.map +1 -1
- package/dist/esm/server/oneServe.native.js +0 -6
- package/dist/esm/server/oneServe.native.js.map +1 -1
- package/dist/esm/server/workerHandler.mjs +12 -10
- package/dist/esm/server/workerHandler.mjs.map +1 -1
- package/dist/esm/server/workerHandler.native.js +14 -10
- package/dist/esm/server/workerHandler.native.js.map +1 -1
- package/dist/esm/vite/plugins/workerdDevPlugin.mjs +33 -37
- package/dist/esm/vite/plugins/workerdDevPlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/workerdDevPlugin.native.js +33 -37
- package/dist/esm/vite/plugins/workerdDevPlugin.native.js.map +1 -1
- package/ios/Nitro/HybridOneAudio.swift +309 -96
- package/nitrogen/generated/android/c++/JAudioPlayOptions.hpp +61 -0
- package/nitrogen/generated/android/c++/JAudioRemoteCommandEvent.hpp +8 -3
- package/nitrogen/generated/android/c++/JFunc_void_AudioRemoteCommandEvent.hpp +1 -0
- package/nitrogen/generated/android/c++/JHybridOneAudioSpec.cpp +37 -3
- package/nitrogen/generated/android/c++/JHybridOneAudioSpec.hpp +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/one/AudioPlayOptions.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/one/AudioRemoteCommandEvent.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/one/HybridOneAudioSpec.kt +40 -32
- package/nitrogen/generated/ios/One-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridOneAudioSpecSwift.hpp +21 -2
- package/nitrogen/generated/ios/swift/AudioPlayOptions.swift +60 -0
- package/nitrogen/generated/ios/swift/AudioRemoteCommandEvent.swift +19 -1
- package/nitrogen/generated/ios/swift/HybridOneAudioSpec.swift +3 -1
- package/nitrogen/generated/ios/swift/HybridOneAudioSpec_cxx.swift +70 -32
- package/nitrogen/generated/shared/c++/AudioPlayOptions.hpp +87 -0
- package/nitrogen/generated/shared/c++/AudioRemoteCommandEvent.hpp +7 -2
- package/nitrogen/generated/shared/c++/HybridOneAudioSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridOneAudioSpec.hpp +6 -1
- package/package.json +16 -16
- package/src/cli/build.ts +35 -32
- package/src/cli/buildPage.test.ts +152 -0
- package/src/cli/buildPage.ts +14 -9
- package/src/cli/buildPageWorker.ts +2 -1
- package/src/cli/workerPool.ts +1 -0
- package/src/headless-server.ts +2 -2
- package/src/index.ts +1 -0
- package/src/platform/audio/index.native.ts +4 -2
- package/src/platform/audio/index.ts +102 -74
- package/src/platform/audio/nowPlaying.ts +122 -0
- package/src/platform/audio/player.native.ts +17 -1
- package/src/platform/audio/player.ts +23 -1
- package/src/platform/audio/types.ts +6 -1
- package/src/platform/audio/unavailable.ts +3 -1
- package/src/platform/index.native.ts +1 -0
- package/src/platform/index.ts +1 -0
- package/src/platform/location/index.native.ts +14 -3
- package/src/platform/specs/OneAudio.nitro.ts +11 -1
- package/src/serve-worker.ts +2 -3
- package/src/server/oneServe.ts +0 -6
- package/src/server/workerHandler.ts +34 -18
- package/src/vite/plugins/workerdDevPlugin.ts +33 -37
- package/src/vite/resolveResponse.ts +2 -2
- package/types/cli/build.d.ts.map +1 -1
- package/types/cli/buildPage.d.ts +1 -1
- package/types/cli/buildPage.d.ts.map +1 -1
- package/types/cli/workerPool.d.ts +1 -0
- package/types/cli/workerPool.d.ts.map +1 -1
- package/types/headless-server.d.ts +3 -3
- package/types/headless-server.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +1 -1
- package/types/platform/audio/index.d.ts +2 -2
- package/types/platform/audio/index.d.ts.map +1 -1
- package/types/platform/audio/index.native.d.ts +3 -3
- package/types/platform/audio/index.native.d.ts.map +1 -1
- package/types/platform/audio/nowPlaying.d.ts +12 -0
- package/types/platform/audio/nowPlaying.d.ts.map +1 -0
- package/types/platform/audio/player.d.ts +3 -1
- package/types/platform/audio/player.d.ts.map +1 -1
- package/types/platform/audio/player.native.d.ts +3 -1
- package/types/platform/audio/player.native.d.ts.map +1 -1
- package/types/platform/audio/types.d.ts +3 -1
- package/types/platform/audio/types.d.ts.map +1 -1
- package/types/platform/audio/unavailable.d.ts +3 -3
- package/types/platform/audio/unavailable.d.ts.map +1 -1
- package/types/platform/index.d.ts +1 -1
- package/types/platform/index.d.ts.map +1 -1
- package/types/platform/index.native.d.ts +1 -1
- package/types/platform/index.native.d.ts.map +1 -1
- package/types/platform/location/index.native.d.ts.map +1 -1
- package/types/platform/specs/OneAudio.nitro.d.ts +8 -1
- package/types/platform/specs/OneAudio.nitro.d.ts.map +1 -1
- package/types/serve-worker.d.ts +3 -3
- package/types/serve-worker.d.ts.map +1 -1
- package/types/server/oneServe.d.ts.map +1 -1
- package/types/server/workerHandler.d.ts +1 -1
- package/types/server/workerHandler.d.ts.map +1 -1
- package/types/vite/plugins/workerdDevPlugin.d.ts.map +1 -1
- package/types/vite/resolveResponse.d.ts +1 -1
- package/types/vite/resolveResponse.d.ts.map +1 -1
- package/dist/cjs/server/staticHtmlFetcher.cjs +0 -33
- package/dist/cjs/server/staticHtmlFetcher.native.cjs +0 -35
- package/dist/cjs/server/staticHtmlFetcher.native.js +0 -37
- package/dist/cjs/server/staticHtmlFetcher.native.js.map +0 -1
- package/dist/esm/server/staticHtmlFetcher.mjs +0 -10
- package/dist/esm/server/staticHtmlFetcher.mjs.map +0 -1
- package/dist/esm/server/staticHtmlFetcher.native.js +0 -10
- package/dist/esm/server/staticHtmlFetcher.native.js.map +0 -1
- package/src/server/staticHtmlFetcher.ts +0 -12
- package/types/server/staticHtmlFetcher.d.ts +0 -3
- package/types/server/staticHtmlFetcher.d.ts.map +0 -1
package/src/cli/build.ts
CHANGED
|
@@ -548,6 +548,11 @@ export async function build(args: {
|
|
|
548
548
|
// concurrency limiter for parallel page builds
|
|
549
549
|
const limit = pLimit(buildConcurrency)
|
|
550
550
|
|
|
551
|
+
// native:false runs One as a pure web framework: no native bundle exists to
|
|
552
|
+
// read the per-loader .native.js twins, so buildPage omits them. default and
|
|
553
|
+
// native-enabled builds keep writing both twins.
|
|
554
|
+
const skipNativeLoaders = oneOptions.native === false
|
|
555
|
+
|
|
551
556
|
// initialize worker pool if enabled (default: true)
|
|
552
557
|
const useWorkers = shouldUseWorkers(oneOptions)
|
|
553
558
|
const workerPool = useWorkers ? getWorkerPool(buildConcurrency) : null
|
|
@@ -1081,6 +1086,7 @@ export async function build(args: {
|
|
|
1081
1086
|
deferredPreloads,
|
|
1082
1087
|
useAfterLCP,
|
|
1083
1088
|
useAfterLCPAggressive,
|
|
1089
|
+
skipNativeLoaders,
|
|
1084
1090
|
})
|
|
1085
1091
|
.then((built) => ({ built, path }))
|
|
1086
1092
|
}
|
|
@@ -1110,7 +1116,8 @@ export async function build(args: {
|
|
|
1110
1116
|
criticalPreloads,
|
|
1111
1117
|
deferredPreloads,
|
|
1112
1118
|
useAfterLCP,
|
|
1113
|
-
useAfterLCPAggressive
|
|
1119
|
+
useAfterLCPAggressive,
|
|
1120
|
+
skipNativeLoaders
|
|
1114
1121
|
)
|
|
1115
1122
|
})
|
|
1116
1123
|
|
|
@@ -1351,7 +1358,7 @@ if (typeof MessageChannel === 'undefined') {
|
|
|
1351
1358
|
}
|
|
1352
1359
|
}
|
|
1353
1360
|
|
|
1354
|
-
import { serve,
|
|
1361
|
+
import { serve, withRange } from 'one/serve-worker'
|
|
1355
1362
|
|
|
1356
1363
|
// Lazy import map - modules load on-demand when route is matched
|
|
1357
1364
|
const lazyRoutes = {
|
|
@@ -1374,44 +1381,40 @@ export default {
|
|
|
1374
1381
|
server = await serve(buildInfo, lazyRoutes)
|
|
1375
1382
|
}
|
|
1376
1383
|
|
|
1377
|
-
//
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1384
|
+
// keep the asset binding and origin scoped to this request
|
|
1385
|
+
const fetchStaticHtml = env.ASSETS
|
|
1386
|
+
? async (path) => {
|
|
1387
|
+
try {
|
|
1388
|
+
const url = new URL(request.url)
|
|
1389
|
+
url.pathname = path
|
|
1390
|
+
const assetResponse = await env.ASSETS.fetch(new Request(url))
|
|
1391
|
+
if (assetResponse && assetResponse.ok) {
|
|
1392
|
+
return await assetResponse.text()
|
|
1393
|
+
}
|
|
1394
|
+
} catch (e) {
|
|
1395
|
+
// asset not found
|
|
1386
1396
|
}
|
|
1387
|
-
|
|
1388
|
-
// asset not found
|
|
1397
|
+
return null
|
|
1389
1398
|
}
|
|
1390
|
-
|
|
1391
|
-
})
|
|
1392
|
-
}
|
|
1399
|
+
: undefined
|
|
1393
1400
|
|
|
1394
|
-
|
|
1395
|
-
const response = await server.fetch(request, env, ctx)
|
|
1401
|
+
const response = await server.fetch(request, env, ctx, fetchStaticHtml)
|
|
1396
1402
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
}
|
|
1405
|
-
} catch (e) {
|
|
1406
|
-
// asset not found, continue with original response
|
|
1403
|
+
// no route matched or 404 → try static assets
|
|
1404
|
+
if (!response || response.status === 404) {
|
|
1405
|
+
if (env.ASSETS) {
|
|
1406
|
+
try {
|
|
1407
|
+
const assetResponse = await env.ASSETS.fetch(request)
|
|
1408
|
+
if (assetResponse && assetResponse.status !== 404) {
|
|
1409
|
+
return await withRange(request, assetResponse)
|
|
1407
1410
|
}
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
// asset not found, continue with original response
|
|
1408
1413
|
}
|
|
1409
1414
|
}
|
|
1410
|
-
|
|
1411
|
-
return response
|
|
1412
|
-
} finally {
|
|
1413
|
-
setFetchStaticHtml(null)
|
|
1414
1415
|
}
|
|
1416
|
+
|
|
1417
|
+
return response
|
|
1415
1418
|
}
|
|
1416
1419
|
}
|
|
1417
1420
|
`
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, expect, it } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
LOADER_JS_POSTFIX,
|
|
7
|
+
NATIVE_LOADER_JS_POSTFIX_UNCACHED,
|
|
8
|
+
} from '../constants'
|
|
9
|
+
import { buildPage } from './buildPage'
|
|
10
|
+
|
|
11
|
+
let root: string | undefined
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
if (root) await rm(root, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
async function fixture() {
|
|
18
|
+
root = await mkdtemp(join(tmpdir(), 'one-build-page-'))
|
|
19
|
+
const serverEntry = join(root, 'entry.mjs')
|
|
20
|
+
const { writeFile, mkdir } = await import('node:fs/promises')
|
|
21
|
+
await writeFile(
|
|
22
|
+
serverEntry,
|
|
23
|
+
`export default {
|
|
24
|
+
options: { routes: {
|
|
25
|
+
'/app/probe.tsx': async () => ({
|
|
26
|
+
loader: ({ params }) => ({ slug: params.slug }),
|
|
27
|
+
}),
|
|
28
|
+
'/app/gated.tsx': async () => ({
|
|
29
|
+
loader: () => { throw Response.redirect('http://example.com/login', 302) },
|
|
30
|
+
}),
|
|
31
|
+
} },
|
|
32
|
+
render: async ({ loaderData }) => '<html><head></head><body>' + JSON.stringify(loaderData) + '</body></html>'
|
|
33
|
+
}`
|
|
34
|
+
)
|
|
35
|
+
return serverEntry
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function buildRoute(
|
|
39
|
+
serverEntry: string,
|
|
40
|
+
out: string,
|
|
41
|
+
route: { path: string; file: string; params: any },
|
|
42
|
+
skipNativeLoaders?: boolean
|
|
43
|
+
) {
|
|
44
|
+
const clientDir = join(out, 'client')
|
|
45
|
+
const staticDir = join(out, 'static')
|
|
46
|
+
const { mkdir } = await import('node:fs/promises')
|
|
47
|
+
await mkdir(join(clientDir, 'assets'), { recursive: true })
|
|
48
|
+
await mkdir(staticDir, { recursive: true })
|
|
49
|
+
const built = await buildPage(
|
|
50
|
+
serverEntry,
|
|
51
|
+
'app',
|
|
52
|
+
route.path,
|
|
53
|
+
'/probe.tsx',
|
|
54
|
+
route.params,
|
|
55
|
+
{ type: 'ssg', file: route.file, layouts: [], middlewares: [] } as any,
|
|
56
|
+
{ file: 'assets/probe.js' },
|
|
57
|
+
staticDir,
|
|
58
|
+
clientDir,
|
|
59
|
+
{},
|
|
60
|
+
'server/probe.js',
|
|
61
|
+
[],
|
|
62
|
+
[],
|
|
63
|
+
[],
|
|
64
|
+
[],
|
|
65
|
+
undefined,
|
|
66
|
+
undefined,
|
|
67
|
+
undefined,
|
|
68
|
+
undefined,
|
|
69
|
+
undefined,
|
|
70
|
+
skipNativeLoaders
|
|
71
|
+
)
|
|
72
|
+
return { built, clientDir, staticDir }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function nativeTwins(clientDir: string, loaderPath: string) {
|
|
76
|
+
const cached = join(clientDir, loaderPath.replace(/\.js$/, '.native.js'))
|
|
77
|
+
const uncached = join(
|
|
78
|
+
clientDir,
|
|
79
|
+
loaderPath.replace(LOADER_JS_POSTFIX, NATIVE_LOADER_JS_POSTFIX_UNCACHED)
|
|
80
|
+
)
|
|
81
|
+
return { cached, uncached }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function exists(path: string) {
|
|
85
|
+
try {
|
|
86
|
+
await readFile(path)
|
|
87
|
+
return true
|
|
88
|
+
} catch {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it('omits native loader twins when skipped, keeping web output identical', async () => {
|
|
94
|
+
const serverEntry = await fixture()
|
|
95
|
+
for (const route of [
|
|
96
|
+
{ path: '/probe', file: './probe.tsx', params: { slug: 'probe' } },
|
|
97
|
+
{ path: '/gated', file: './gated.tsx', params: {} },
|
|
98
|
+
]) {
|
|
99
|
+
const enabled = await buildRoute(serverEntry, join(root!, 'enabled', route.path), route)
|
|
100
|
+
const skipped = await buildRoute(
|
|
101
|
+
serverEntry,
|
|
102
|
+
join(root!, 'skipped', route.path),
|
|
103
|
+
route,
|
|
104
|
+
true
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// web loader and html are byte-identical either way
|
|
108
|
+
const [enabledLoader, skippedLoader] = await Promise.all([
|
|
109
|
+
readFile(join(enabled.clientDir, enabled.built.loaderPath), 'utf8'),
|
|
110
|
+
readFile(join(skipped.clientDir, skipped.built.loaderPath), 'utf8'),
|
|
111
|
+
])
|
|
112
|
+
expect(skippedLoader, route.path).toBe(enabledLoader)
|
|
113
|
+
const [enabledHtml, skippedHtml] = await Promise.all([
|
|
114
|
+
readFile(join(enabled.staticDir, `${route.path.slice(1)}.html`), 'utf8'),
|
|
115
|
+
readFile(join(skipped.staticDir, `${route.path.slice(1)}.html`), 'utf8'),
|
|
116
|
+
])
|
|
117
|
+
expect(skippedHtml, route.path).toBe(enabledHtml)
|
|
118
|
+
|
|
119
|
+
// native twins exist by default, absent when skipped
|
|
120
|
+
const enabledTwins = nativeTwins(enabled.clientDir, enabled.built.loaderPath)
|
|
121
|
+
expect(await exists(enabledTwins.cached), `${route.path} cached`).toBe(true)
|
|
122
|
+
expect(await exists(enabledTwins.uncached), `${route.path} uncached`).toBe(true)
|
|
123
|
+
const skippedTwins = nativeTwins(skipped.clientDir, skipped.built.loaderPath)
|
|
124
|
+
expect(await exists(skippedTwins.cached), `${route.path} cached`).toBe(false)
|
|
125
|
+
expect(await exists(skippedTwins.uncached), `${route.path} uncached`).toBe(false)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// the redirect loader still carries its signal on the web file
|
|
129
|
+
const skipped = await buildRoute(
|
|
130
|
+
serverEntry,
|
|
131
|
+
join(root!, 'redirect'),
|
|
132
|
+
{ path: '/gated', file: './gated.tsx', params: {} },
|
|
133
|
+
true
|
|
134
|
+
)
|
|
135
|
+
const webLoader = await readFile(
|
|
136
|
+
join(skipped.clientDir, skipped.built.loaderPath),
|
|
137
|
+
'utf8'
|
|
138
|
+
)
|
|
139
|
+
expect(webLoader).toContain('__oneRedirect')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('writes native loader twins by default', async () => {
|
|
143
|
+
const serverEntry = await fixture()
|
|
144
|
+
const { built, clientDir } = await buildRoute(
|
|
145
|
+
serverEntry,
|
|
146
|
+
join(root!, 'default'),
|
|
147
|
+
{ path: '/probe', file: './probe.tsx', params: { slug: 'probe' } }
|
|
148
|
+
)
|
|
149
|
+
const twins = nativeTwins(clientDir, built.loaderPath)
|
|
150
|
+
expect(await exists(twins.cached)).toBe(true)
|
|
151
|
+
expect(await exists(twins.uncached)).toBe(true)
|
|
152
|
+
})
|
package/src/cli/buildPage.ts
CHANGED
|
@@ -63,7 +63,8 @@ export async function buildPage(
|
|
|
63
63
|
criticalPreloads?: string[],
|
|
64
64
|
deferredPreloads?: string[],
|
|
65
65
|
useAfterLCP?: boolean,
|
|
66
|
-
useAfterLCPAggressive?: boolean
|
|
66
|
+
useAfterLCPAggressive?: boolean,
|
|
67
|
+
skipNativeLoaders?: boolean
|
|
67
68
|
): Promise<One.RouteBuildInfo> {
|
|
68
69
|
let t0 = performance.now()
|
|
69
70
|
|
|
@@ -275,10 +276,12 @@ prefetchCSS()
|
|
|
275
276
|
loaderPartialPath,
|
|
276
277
|
`export function loader(){return ${redirectData}}`
|
|
277
278
|
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
if (!skipNativeLoaders) {
|
|
280
|
+
// native-friendly CJS version
|
|
281
|
+
const nativeCjs = `exports.loader = function(){return ${redirectData}}`
|
|
282
|
+
await outputFile(loaderPartialPath.replace(/\.js$/, '.native.js'), nativeCjs)
|
|
283
|
+
await outputFile(uncachedNativePath, nativeCjs)
|
|
284
|
+
}
|
|
282
285
|
loaderPath = getLoaderPath(path)
|
|
283
286
|
loaderData = {}
|
|
284
287
|
} else {
|
|
@@ -289,10 +292,12 @@ prefetchCSS()
|
|
|
289
292
|
// stub of its own to replace)
|
|
290
293
|
const data = JSON.stringify(loaderData)
|
|
291
294
|
await outputFile(loaderPartialPath, `export function loader(){return ${data}}`)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
295
|
+
if (!skipNativeLoaders) {
|
|
296
|
+
// native-friendly CJS version
|
|
297
|
+
const nativeCjs = `exports.loader = function(){return ${data}}`
|
|
298
|
+
await outputFile(loaderPartialPath.replace(/\.js$/, '.native.js'), nativeCjs)
|
|
299
|
+
await outputFile(uncachedNativePath, nativeCjs)
|
|
300
|
+
}
|
|
296
301
|
loaderPath = getLoaderPath(path)
|
|
297
302
|
}
|
|
298
303
|
}
|
|
@@ -86,7 +86,8 @@ parentPort.on('message', async (msg: any) => {
|
|
|
86
86
|
msg.args.criticalPreloads,
|
|
87
87
|
msg.args.deferredPreloads,
|
|
88
88
|
msg.args.useAfterLCP,
|
|
89
|
-
msg.args.useAfterLCPAggressive
|
|
89
|
+
msg.args.useAfterLCPAggressive,
|
|
90
|
+
msg.args.skipNativeLoaders
|
|
90
91
|
)
|
|
91
92
|
})
|
|
92
93
|
// loader data is already in the artifacts and is discarded by the parent.
|
package/src/cli/workerPool.ts
CHANGED
|
@@ -127,6 +127,7 @@ export class BuildWorkerPool {
|
|
|
127
127
|
deferredPreloads?: string[]
|
|
128
128
|
useAfterLCP?: boolean
|
|
129
129
|
useAfterLCPAggressive?: boolean
|
|
130
|
+
skipNativeLoaders?: boolean
|
|
130
131
|
}): Promise<any> {
|
|
131
132
|
if (this._terminated) {
|
|
132
133
|
throw new Error('Worker pool has been terminated')
|
package/src/headless-server.ts
CHANGED
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
* })
|
|
28
28
|
*
|
|
29
29
|
* const response = await server.fetch(request)
|
|
30
|
+
* // pass a request-scoped static HTML reader as the fourth argument for SPA or SSG
|
|
30
31
|
* server.updateRoutes(newManifest, newRoutes)
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
export { createWorkerHandler, type LazyRoutes } from './server/workerHandler'
|
|
34
35
|
export { compileManifest } from './createHandleRequest'
|
|
35
36
|
export type { RequestHandlers } from './createHandleRequest'
|
|
36
|
-
export { getFetchStaticHtml, setFetchStaticHtml } from './server/staticHtmlFetcher'
|
|
37
37
|
export type { One } from './vite/types'
|
|
38
38
|
|
|
39
39
|
import { setServerGlobals } from './server/setServerGlobals'
|
|
@@ -92,7 +92,7 @@ export async function createServer(options: HeadlessServerOptions) {
|
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
return {
|
|
95
|
-
/** handle
|
|
95
|
+
/** handle a request with an optional request-scoped static HTML reader */
|
|
96
96
|
fetch: handler.handleRequest,
|
|
97
97
|
|
|
98
98
|
/** hot-swap the route table without recreating the server */
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { validateCallback } from '../validateCallback'
|
|
|
2
2
|
import { rethrowNativeError } from '../nativeError'
|
|
3
3
|
import type {
|
|
4
4
|
AudioPlaybackStatus,
|
|
5
|
+
AudioPlayOptions,
|
|
5
6
|
AudioRecordingPermission,
|
|
6
7
|
AudioRecordingResult,
|
|
7
8
|
AudioRecordingStatus,
|
|
@@ -15,6 +16,7 @@ import { AudioPlayer, createPlayer } from './player.native'
|
|
|
15
16
|
export type {
|
|
16
17
|
AudioPlaybackState,
|
|
17
18
|
AudioPlaybackStatus,
|
|
19
|
+
AudioPlayOptions,
|
|
18
20
|
AudioRecordingPermission,
|
|
19
21
|
AudioRecordingResult,
|
|
20
22
|
AudioRecordingState,
|
|
@@ -33,8 +35,8 @@ export const Audio = Object.freeze({
|
|
|
33
35
|
native().getRecordingPermissionStatus().catch(rethrowNativeError),
|
|
34
36
|
requestRecordingPermission: (): Promise<AudioRecordingPermission> =>
|
|
35
37
|
native().requestRecordingPermission().catch(rethrowNativeError),
|
|
36
|
-
play: (uri: string): Promise<AudioPlaybackStatus> =>
|
|
37
|
-
native().play(uri).catch(rethrowNativeError),
|
|
38
|
+
play: (uri: string, options?: AudioPlayOptions): Promise<AudioPlaybackStatus> =>
|
|
39
|
+
native().play(uri, options ?? {}).catch(rethrowNativeError),
|
|
38
40
|
getPlaybackStatus: (): Promise<AudioPlaybackStatus> =>
|
|
39
41
|
native().getPlaybackStatus().catch(rethrowNativeError),
|
|
40
42
|
pause: (): Promise<AudioPlaybackStatus> => native().pause().catch(rethrowNativeError),
|
|
@@ -2,17 +2,24 @@ import { Audio as unavailable } from './unavailable'
|
|
|
2
2
|
import { validateCallback } from '../validateCallback'
|
|
3
3
|
import type {
|
|
4
4
|
AudioPlaybackStatus,
|
|
5
|
+
AudioPlayOptions,
|
|
5
6
|
AudioRecordingPermission,
|
|
6
7
|
AudioRecordingResult,
|
|
7
8
|
AudioRecordingStatus,
|
|
8
9
|
AudioNowPlayingInfo,
|
|
9
10
|
AudioRemoteCommandEvent,
|
|
10
11
|
} from '../specs/OneAudio.nitro'
|
|
12
|
+
import { addRemoteListener, claimNowPlaying, releaseNowPlaying } from './nowPlaying'
|
|
11
13
|
import { AudioPlayer, createPlayer } from './player'
|
|
12
14
|
export type { PitchCorrectionQuality } from '../specs/OneAudio.nitro'
|
|
13
15
|
export type * from './types'
|
|
14
16
|
export type * from './unavailable'
|
|
15
17
|
let player: HTMLAudioElement | undefined
|
|
18
|
+
// a play with a position or paused flag, between creation and the landed
|
|
19
|
+
// seek. paused is the intent pause and resume update while it loads.
|
|
20
|
+
let pendingLoad:
|
|
21
|
+
| { audio: HTMLAudioElement; paused: boolean; cancel?: () => void }
|
|
22
|
+
| undefined
|
|
16
23
|
let playbackError: string | undefined
|
|
17
24
|
let permission: AudioRecordingPermission = 'undetermined'
|
|
18
25
|
let openingRecording = false
|
|
@@ -27,8 +34,6 @@ let recording:
|
|
|
27
34
|
error?: Error
|
|
28
35
|
}
|
|
29
36
|
| undefined
|
|
30
|
-
const remoteListeners = new Set<(event: AudioRemoteCommandEvent) => void>()
|
|
31
|
-
let remoteInstalled = false
|
|
32
37
|
|
|
33
38
|
async function getRecordingPermissionStatus(): Promise<AudioRecordingPermission> {
|
|
34
39
|
if (typeof window === 'undefined' || !navigator.mediaDevices?.getUserMedia)
|
|
@@ -74,7 +79,7 @@ function playbackStatus(): AudioPlaybackStatus {
|
|
|
74
79
|
? 'failed'
|
|
75
80
|
: player.ended
|
|
76
81
|
? 'ended'
|
|
77
|
-
: player.readyState < 2
|
|
82
|
+
: pendingLoad || player.readyState < 2
|
|
78
83
|
? 'loading'
|
|
79
84
|
: player.paused
|
|
80
85
|
? 'paused'
|
|
@@ -90,7 +95,9 @@ function requirePlayer(): HTMLAudioElement {
|
|
|
90
95
|
return player
|
|
91
96
|
}
|
|
92
97
|
function clearPlayer(): void {
|
|
93
|
-
|
|
98
|
+
releaseNowPlaying('global')
|
|
99
|
+
pendingLoad?.cancel?.()
|
|
100
|
+
pendingLoad = undefined
|
|
94
101
|
if (player) {
|
|
95
102
|
player.pause()
|
|
96
103
|
player.removeAttribute('src')
|
|
@@ -152,69 +159,88 @@ function recorderChange(event: 'pause' | 'resume'): Promise<AudioRecordingStatus
|
|
|
152
159
|
}
|
|
153
160
|
})
|
|
154
161
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
remoteInstalled = true
|
|
162
|
+
// resolves once the element has metadata and sits at the position, without
|
|
163
|
+
// playing. replacement cancels it through the load's cancel hook.
|
|
164
|
+
function prepareAt(load: NonNullable<typeof pendingLoad>, positionMs: number): Promise<void> {
|
|
165
|
+
const { audio } = load
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const cleanup = () => {
|
|
168
|
+
audio.removeEventListener('loadedmetadata', ready)
|
|
169
|
+
audio.removeEventListener('seeked', done)
|
|
170
|
+
audio.removeEventListener('error', failed)
|
|
171
|
+
load.cancel = undefined
|
|
172
|
+
}
|
|
173
|
+
let seconds = 0
|
|
174
|
+
// a source without range support lands somewhere else, often 0; fail
|
|
175
|
+
// rather than let a later resume play the opening.
|
|
176
|
+
const done = () => {
|
|
177
|
+
cleanup()
|
|
178
|
+
if (Math.abs(audio.currentTime - seconds) > 0.25)
|
|
179
|
+
reject(new Error('Audio.play: the source cannot seek to the position'))
|
|
180
|
+
else resolve()
|
|
181
|
+
}
|
|
182
|
+
const failed = () => {
|
|
183
|
+
cleanup()
|
|
184
|
+
reject(new Error(`Audio.play: ${audio.error?.message || 'decoding failed'}`))
|
|
185
|
+
}
|
|
186
|
+
function ready() {
|
|
187
|
+
audio.removeEventListener('loadedmetadata', ready)
|
|
188
|
+
seconds = Math.min(
|
|
189
|
+
positionMs / 1000,
|
|
190
|
+
Number.isFinite(audio.duration) ? audio.duration : Infinity
|
|
191
|
+
)
|
|
192
|
+
if (audio.currentTime === seconds) return done()
|
|
193
|
+
audio.addEventListener('seeked', done, { once: true })
|
|
194
|
+
audio.currentTime = seconds
|
|
195
|
+
}
|
|
196
|
+
load.cancel = () => {
|
|
197
|
+
cleanup()
|
|
198
|
+
reject(new Error('Audio.play: playback was replaced'))
|
|
199
|
+
}
|
|
200
|
+
audio.addEventListener('error', failed, { once: true })
|
|
201
|
+
if (audio.readyState >= 1) ready()
|
|
202
|
+
else audio.addEventListener('loadedmetadata', ready, { once: true })
|
|
203
|
+
})
|
|
198
204
|
}
|
|
199
205
|
export const Audio = Object.freeze({
|
|
200
206
|
getRecordingPermissionStatus,
|
|
201
207
|
requestRecordingPermission,
|
|
202
|
-
play: async (uri: string): Promise<AudioPlaybackStatus> => {
|
|
203
|
-
if (typeof window === 'undefined') return unavailable.play(uri)
|
|
208
|
+
play: async (uri: string, options?: AudioPlayOptions): Promise<AudioPlaybackStatus> => {
|
|
209
|
+
if (typeof window === 'undefined') return unavailable.play(uri, options)
|
|
204
210
|
if (openingRecording || recording)
|
|
205
211
|
throw new Error('Audio.play: stop the current recording first')
|
|
206
212
|
const url = new URL(uri, document.baseURI)
|
|
207
213
|
if (!['https:', 'http:', 'blob:', 'data:'].includes(url.protocol))
|
|
208
214
|
throw new Error('Audio.play: invalid browser audio URI')
|
|
215
|
+
const prepare = options?.positionMs !== undefined || options?.paused !== undefined
|
|
216
|
+
const positionMs = options?.positionMs ?? 0
|
|
217
|
+
if (typeof positionMs !== 'number' || !Number.isFinite(positionMs) || positionMs < 0)
|
|
218
|
+
throw new Error('Audio.play: position must be finite and nonnegative')
|
|
209
219
|
clearPlayer()
|
|
210
220
|
const audio = new window.Audio(url.href)
|
|
211
221
|
player = audio
|
|
212
|
-
for (const event of ['play', 'pause', 'ended'])
|
|
213
|
-
audio.addEventListener(event, syncNowPlaying)
|
|
214
222
|
audio.addEventListener('error', () => {
|
|
215
223
|
if (player === audio)
|
|
216
224
|
playbackError = audio.error?.message ?? 'Audio.play: decoding failed'
|
|
217
225
|
})
|
|
226
|
+
if (prepare) {
|
|
227
|
+
const load: NonNullable<typeof pendingLoad> = { audio, paused: options?.paused ?? false }
|
|
228
|
+
pendingLoad = load
|
|
229
|
+
audio.preload = 'auto'
|
|
230
|
+
try {
|
|
231
|
+
await prepareAt(load, positionMs)
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (pendingLoad === load) {
|
|
234
|
+
pendingLoad = undefined
|
|
235
|
+
playbackError = (error as Error).message
|
|
236
|
+
}
|
|
237
|
+
throw error
|
|
238
|
+
}
|
|
239
|
+
if (player !== audio || pendingLoad !== load)
|
|
240
|
+
throw new Error('Audio.play: playback was replaced')
|
|
241
|
+
pendingLoad = undefined
|
|
242
|
+
if (load.paused) return playbackStatus()
|
|
243
|
+
}
|
|
218
244
|
try {
|
|
219
245
|
await audio.play()
|
|
220
246
|
} catch (error) {
|
|
@@ -228,6 +254,10 @@ export const Audio = Object.freeze({
|
|
|
228
254
|
pause: async (): Promise<AudioPlaybackStatus> => {
|
|
229
255
|
if (typeof window === 'undefined') return unavailable.pause()
|
|
230
256
|
const audio = requirePlayer()
|
|
257
|
+
if (pendingLoad) {
|
|
258
|
+
pendingLoad.paused = true
|
|
259
|
+
return playbackStatus()
|
|
260
|
+
}
|
|
231
261
|
if (!audio.paused)
|
|
232
262
|
await new Promise<void>((resolve) => {
|
|
233
263
|
audio.addEventListener('pause', () => resolve(), { once: true })
|
|
@@ -238,6 +268,11 @@ export const Audio = Object.freeze({
|
|
|
238
268
|
resume: async (): Promise<AudioPlaybackStatus> => {
|
|
239
269
|
if (typeof window === 'undefined') return unavailable.resume()
|
|
240
270
|
const audio = requirePlayer()
|
|
271
|
+
// a prepared load plays itself once its seek lands.
|
|
272
|
+
if (pendingLoad) {
|
|
273
|
+
pendingLoad.paused = false
|
|
274
|
+
return playbackStatus()
|
|
275
|
+
}
|
|
241
276
|
if (audio.ended) audio.currentTime = 0
|
|
242
277
|
await audio.play()
|
|
243
278
|
if (player !== audio) throw new Error('Audio.resume: playback was replaced')
|
|
@@ -248,7 +283,8 @@ export const Audio = Object.freeze({
|
|
|
248
283
|
if (!Number.isFinite(positionMs) || positionMs < 0)
|
|
249
284
|
throw new Error('Audio.seek: position must be finite and nonnegative')
|
|
250
285
|
const audio = requirePlayer()
|
|
251
|
-
if (audio.readyState < 1
|
|
286
|
+
if (audio.readyState < 1 || pendingLoad)
|
|
287
|
+
throw new Error('Audio.seek: playback is not ready')
|
|
252
288
|
const seconds = Math.min(
|
|
253
289
|
positionMs / 1000,
|
|
254
290
|
Number.isFinite(audio.duration) ? audio.duration : Infinity
|
|
@@ -402,27 +438,23 @@ export const Audio = Object.freeze({
|
|
|
402
438
|
watchInterruptions: unavailable.watchInterruptions,
|
|
403
439
|
setNowPlayingInfo: async (info: AudioNowPlayingInfo): Promise<void> => {
|
|
404
440
|
if (typeof window === 'undefined') return unavailable.setNowPlayingInfo(info)
|
|
405
|
-
if (!info.title.trim())
|
|
406
|
-
throw new Error('Audio.setNowPlayingInfo: title must not be empty')
|
|
407
441
|
const audio = requirePlayer()
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
installRemote()
|
|
421
|
-
syncNowPlaying()
|
|
442
|
+
await claimNowPlaying(
|
|
443
|
+
{
|
|
444
|
+
key: 'global',
|
|
445
|
+
element: audio,
|
|
446
|
+
play: Audio.resume,
|
|
447
|
+
pause: Audio.pause,
|
|
448
|
+
seek: Audio.seek,
|
|
449
|
+
},
|
|
450
|
+
info,
|
|
451
|
+
'Audio.setNowPlayingInfo',
|
|
452
|
+
() => player === audio
|
|
453
|
+
)
|
|
422
454
|
},
|
|
423
455
|
clearNowPlayingInfo: async (): Promise<void> => {
|
|
424
456
|
if (typeof window === 'undefined') return unavailable.clearNowPlayingInfo()
|
|
425
|
-
|
|
457
|
+
releaseNowPlaying('global')
|
|
426
458
|
},
|
|
427
459
|
watchRemoteCommands: (
|
|
428
460
|
onEvent: (event: AudioRemoteCommandEvent) => void
|
|
@@ -430,11 +462,7 @@ export const Audio = Object.freeze({
|
|
|
430
462
|
validateCallback(onEvent, 'Audio.watchRemoteCommands: onEvent must be a function')
|
|
431
463
|
if (typeof window === 'undefined' || !navigator.mediaSession)
|
|
432
464
|
return unavailable.watchRemoteCommands(onEvent)
|
|
433
|
-
|
|
434
|
-
return () => {
|
|
435
|
-
remoteListeners.delete(onEvent)
|
|
436
|
-
|
|
437
|
-
}
|
|
465
|
+
return addRemoteListener(onEvent)
|
|
438
466
|
},
|
|
439
467
|
AudioPlayer,
|
|
440
468
|
createPlayer,
|