diff --git a/content/docs/changelog/index.ko.mdx b/content/docs/changelog/index.ko.mdx new file mode 100644 index 0000000..1728b53 --- /dev/null +++ b/content/docs/changelog/index.ko.mdx @@ -0,0 +1,41 @@ +--- +title: 변경 이력 +description: Webview Bundle 릴리스를 추적하는 곳이며, 지금까지 배포된 버전을 확인할 수 있습니다. +--- + +릴리스는 패키지별로 발행됩니다. 따라서 공식 릴리스 노트는 각 레지스트리와 GitHub 릴리스 페이지에 있습니다. + + + + + + +## 배포된 버전 + +패키지별 현재 배포 버전은 다음과 같습니다. + +| Package | Registry | Version | +| ------------------------ | ----------------------------------------------------------- | ------- | +| `wvb` (Rust 코어) | [crates.io](https://crates.io/crates/wvb) | 0.2.0 | +| `wvb-tauri` | [crates.io](https://crates.io/crates/wvb-tauri) | 0.1.0 | +| `@wvb/cli` | [npm](https://www.npmjs.com/package/@wvb/cli) | 0.1.0 | +| `@wvb/config` | [npm](https://www.npmjs.com/package/@wvb/config) | 0.1.0 | +| `@wvb/node` | [npm](https://www.npmjs.com/package/@wvb/node) | 0.1.0 | +| `@wvb/bridge` | [npm](https://www.npmjs.com/package/@wvb/bridge) | 0.1.0 | +| `@wvb/electron` | [npm](https://www.npmjs.com/package/@wvb/electron) | 0.1.0 | +| `@wvb/remote-aws` | [npm](https://www.npmjs.com/package/@wvb/remote-aws) | 0.1.0 | +| `@wvb/remote-cloudflare` | [npm](https://www.npmjs.com/package/@wvb/remote-cloudflare) | 0.1.0 | + +## 버전 관리 + +패키지는 [유의적 버전](https://semver.org)을 따릅니다. 재현 가능한 빌드를 위해 정확한 버전을 고정하세요. `.wvb` +번들 포맷은 패키지 버전과 별개로 자체 포맷 버전(`v1`)을 가집니다 — +[번들 포맷](/docs/guide/bundle-format)을 참고하세요. diff --git a/content/docs/changelog/meta.ko.json b/content/docs/changelog/meta.ko.json new file mode 100644 index 0000000..0cca8eb --- /dev/null +++ b/content/docs/changelog/meta.ko.json @@ -0,0 +1,5 @@ +{ + "root": true, + "title": "변경 이력", + "pages": ["index"] +} diff --git a/content/docs/config/index.ko.mdx b/content/docs/config/index.ko.mdx new file mode 100644 index 0000000..f3d2133 --- /dev/null +++ b/content/docs/config/index.ko.mdx @@ -0,0 +1,307 @@ +--- +title: 설정 +description: CLI와 프로그래밍 API가 pack, remote, serve, builtin 옵션을 읽어 오는 wvb.config 파일입니다. +--- + +단 하나의 `wvb.config` 파일이 모든 Webview Bundle 워크플로를 이끕니다. `wvb` CLI와 +프로그래밍 API(`@wvb/cli/api`)가 모두 이 파일을 불러옵니다. 따라서 설정 하나로 +패킹, 제공, 원격 배포, 빌트인 설치를 일관되게 유지합니다. `@wvb/config`의 +`defineConfig`로 작성하면 완전한 타입 체크와 에디터 자동 완성을 얻습니다. + +이 페이지에서는 설정 파일 자체와 최상위 `pack`, `serve`, `builtin` 섹션을 다룹니다. +원격 배포, 무결성, 서명 옵션은 별도 페이지에서 설명합니다. + + + + + + + +## 설정 파일 + +설정 파일을 프로젝트 루트에 두세요. CLI는 작업 디렉터리에서 다음 순서로 처음 일치하는 +파일을 자동으로 찾습니다. + +```text +wvb.config.js +wvb.config.cjs +wvb.config.mjs +wvb.config.ts +wvb.config.cts +wvb.config.mts +webview-bundle.config.js +webview-bundle.config.cjs +webview-bundle.config.mjs +webview-bundle.config.ts +webview-bundle.config.cts +webview-bundle.config.mts +wvb.config.json +wvb.config.jsonc +``` + +`wvb.config.*`와 `webview-bundle.config.*` 두 기본 이름 모두 `.js`, `.cjs`, `.mjs`, +`.ts`, `.cts`, `.mts` 확장자를 지원합니다. `.json`과 `.jsonc` 형식은 `wvb.config` +기본 이름에서만 인식됩니다. 자동 탐색에 의존하는 대신 특정 파일을 지정하려면 어떤 +명령어에든 `--config`(별칭 `-C`)를 전달하세요. + +```sh +wvb pack --config ./configs/wvb.prod.ts +``` + +### defineConfig + +설정을 `@wvb/config`의 `defineConfig`로 감싸세요. 이 함수는 런타임에서는 항등 함수이며, +타입을 붙이는 것이 유일한 역할입니다. 객체, 설정으로 해석되는 프로미스, 또는 설정을 +반환하는 동기 혹은 비동기 함수를 받습니다. 따라서 불러오는 시점에 값을 계산할 수 있습니다. + + + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig({ + root: process.cwd(), + pack: { + srcDir: './dist', + }, + serve: { + port: 4312, + }, +}); +``` + + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig(async () => { + const endpoint = process.env.WVB_ENDPOINT; + return { + remote: { + endpoint, + }, + }; +}); +``` + + + +이 패키지를 개발 의존성으로 설치하세요: `npm install -D @wvb/config`. + +## 최상위 필드 + +설정 객체는 다섯 개의 선택 필드를 가집니다. 최상위에서는 그 외에 아무것도 읽지 않습니다. + +| Field | Type | Default | +| --------- | --------------- | --------------- | +| `root` | `string` | `process.cwd()` | +| `pack` | `PackConfig` | — | +| `remote` | `RemoteConfig` | — | +| `serve` | `ServeConfig` | — | +| `builtin` | `BuiltinConfig` | — | + +`root`는 상대 경로를 해석하는 데 사용하는 프로젝트 루트를 설정합니다. 절대 경로일 수도 +있고 설정 파일 기준 상대 경로일 수도 있습니다. `remote` 필드는 +[원격, 무결성 및 서명](/docs/config/remote) 페이지에서 설명합니다. + +## pack + +`pack`(`PackConfig`)은 `wvb pack`의 기본값을 설정합니다. CLI 플래그는 호출할 때마다 +이 값을 재정의합니다. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig({ + pack: { + srcDir: './dist', + outFile: '.wvb/app', + overwrite: true, + ignore: ['*.map', /\.DS_Store$/], + headers: { + '*.html': { 'cache-control': 'max-age=0' }, + '*.js': { 'cache-control': 'max-age=31536000' }, + }, + }, +}); +``` + +| Field | Type | Default | +| ----------- | ------------------------------------------------------------------------------------ | ------------- | +| `srcDir` | `string` | `./dist` | +| `outFile` | `string` | `.wvb/` | +| `overwrite` | `boolean` | `true` | +| `ignore` | `Array \| ((file: string) => boolean \| Promise)` | — | +| `headers` | `Record \| Array<[glob, HeadersInit]> \| ((file) => HeadersInit)` | — | + +`outFile`은 단일 출력 경로입니다. 확장자를 생략하면 `.wvb` 확장자가 자동으로 붙습니다. +경로가 절대 경로가 아니면 `root` 기준 상대 경로로 해석됩니다. 기본값 `.wvb/`은 +`package.json`의 name에서 스코프 접두사를 제거하여 ``을 도출합니다. + + + 이 필드는 완전한 경로인 `outFile`입니다. `pack`에는 `outFileName`이나 `outDir`가 없습니다. 하위 + 디렉터리를 포함한 전체 출력 경로를 `outFile`에 작성하세요. + + +`ignore`는 글로브와 정규식의 배열, 또는 불리언을 반환하는 프레디킷(선택적으로 비동기)을 +받습니다. `headers`는 일치하는 파일에 HTTP 헤더를 붙이며 세 가지 형태를 받습니다. + +```ts +// 1. 글로브를 키로 갖는 Record +headers: { + '*.html': { 'cache-control': 'max-age=3600' }, +} + +// 2. [glob, HeadersInit] 튜플의 배열 +headers: [ + ['*.html', { 'cache-control': 'max-age=3600' }], + ['*.png', [['cache-control', 'max-age=0']]], // HeadersInit는 [name, value] 쌍도 받습니다 +] + +// 3. 파일별로 헤더를 반환하는 함수 +headers: (file) => (file.endsWith('.html') ? { 'cache-control': 'max-age=0' } : undefined) +``` + +## serve + +`serve`(`ServeConfig`)는 `wvb serve`의 기본값을 설정합니다. `wvb serve`는 개발용으로 +패킹된 번들을 제공하는 로컬 서버입니다. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig({ + serve: { + file: './.wvb/app.wvb', + port: 4312, + silent: false, + }, +}); +``` + +| Field | Type | Default | +| -------- | --------- | ----------------------- | +| `file` | `string` | the `pack.outFile` path | +| `port` | `number` | `4312` | +| `silent` | `boolean` | — | + +`file`을 생략하면 해석된 `pack.outFile` 경로로 대체됩니다. 요청 로그 출력을 끄려면 +`silent`를 설정하세요. + + + `serve`에는 `hostname` 필드가 없습니다. 다른 호스트에 바인딩하려면 명령줄에서 `wvb serve`에 + `--hostname`(별칭 `-H`)을 전달하세요. + + +## builtin + +`builtin`(`BuiltinConfig`)은 `wvb builtin`의 기본값을 설정합니다. `wvb builtin`은 앱 +안에 함께 배포되는 번들을 설치합니다. 번들은 원격에서 다운로드하거나 로컬 워크스페이스에서 +수집합니다. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig({ + builtin: { + outDir: '.wvb/builtin/bundles', + target: { type: 'remote' }, + include: ['app*'], + exclude: [/^internal-/], + clean: true, + }, +}); +``` + +| Field | Type | Default | +| --------- | -------------------------------------------------------------------- | ---------------------- | +| `outDir` | `string` | `.wvb/builtin/bundles` | +| `target` | `BuiltinTarget` | `{ type: 'remote' }` | +| `include` | `string \| RegExp \| Array \| ((info) => boolean)` | — | +| `exclude` | `string \| RegExp \| Array \| ((info) => boolean)` | — | +| `clean` | `boolean` | `true` | + +`include`와 `exclude`는 후보 번들을 필터링합니다. 각 필드는 글로브 문자열, 정규식, 둘 중 +하나의 배열, 또는 프레디킷 `(info: { name: string; version: string }) => boolean`(선택적으로 +비동기)을 받습니다. `clean`을 활성화하면 설치 전에 출력 디렉터리를 비웁니다. + +### target + +`target`은 `type`을 기준으로 하는 판별 유니온입니다. 서버에서 번들을 다운로드하려면 +`remote`를, 저장소의 워크스페이스에서 번들을 수집하려면 `local`을 선택하세요. + +```ts +// remote 타겟 — 서버에서 다운로드 +target: { + type: 'remote', + endpoint: 'https://updates.example.com', + download: { + concurrency: 4, + // http: { /* @wvb/node의 HttpOptions */ }, + }, +} + +// local 타겟 — 워크스페이스에서 수집 +target: { + type: 'local', + workspaces: ['packages/*'], + bundleName: { from: 'package.json' }, + version: { from: 'package.json' }, + packBeforeInstall: true, +} +``` + +`remote` 타겟에서 `endpoint`와 `download`는 선택 사항입니다. 유일한 동시성 조절 값은 +`download.concurrency`입니다. `download.http`는 [`@wvb/node`](/docs/references/node)의 +`HttpOptions` 타입을 받습니다. + +| `remote` field | Type | Default | +| ---------------------- | ------------- | ------- | +| `endpoint` | `string` | — | +| `download.concurrency` | `number` | — | +| `download.http` | `HttpOptions` | — | + +`local` 타겟에서 `workspaces`는 필수입니다. 이는 설정 전체에서 유일하게 필수인 필드입니다. +`integrity`와 `signature` 옵션은 remote 섹션과 동일한 형태를 공유합니다. +[원격, 무결성 및 서명](/docs/config/remote)을 참고하세요. + +| `local` field | Type | Default | +| ------------------- | --------------------------------------------------- | -------- | +| `workspaces` | `string[] \| (() => string[] \| Promise)` | required | +| `bundleName` | `BundleNameResolver` | — | +| `version` | `VersionResolver` | — | +| `integrity` | `boolean \| IntegrityMakeConfig` | — | +| `signature` | `SignatureSignConfig` | — | +| `packBeforeInstall` | `boolean` | `true` | + +## 다음 단계 + + + + + + diff --git a/content/docs/config/meta.ko.json b/content/docs/config/meta.ko.json new file mode 100644 index 0000000..71280a9 --- /dev/null +++ b/content/docs/config/meta.ko.json @@ -0,0 +1,5 @@ +{ + "root": true, + "title": "설정", + "pages": ["index", "remote"] +} diff --git a/content/docs/config/remote.ko.mdx b/content/docs/config/remote.ko.mdx new file mode 100644 index 0000000..2decd74 --- /dev/null +++ b/content/docs/config/remote.ko.mdx @@ -0,0 +1,208 @@ +--- +title: 원격, 무결성 및 서명 설정 +description: wvb.config의 remote 블록과 배포 측 무결성 및 서명 옵션을 설정합니다. +--- + +`wvb.config.ts`의 `remote` 블록은 `wvb` CLI가 번들을 배포하는 방법을 정의합니다. 원격 서버의 위치, +각 번들의 이름과 버전을 정하는 방법, 그리고 어떤 프로바이더가 번들을 업로드하고 배포하는지를 지정합니다. +이와 인접한 두 옵션이 결과물을 견고하게 만듭니다. `integrity`는 콘텐츠 해시를 계산하고, `signature`는 +그 해시를 개인 키로 서명하여 클라이언트가 누가 번들을 배포했는지 검증할 수 있게 합니다. 이 페이지는 이 +세 가지 옵션을 설명하고, 마지막에 복사해서 바로 쓸 수 있는 완전한 설정으로 마무리합니다. + +나머지 설정 파일 내용(최상위 필드, `pack`, `serve`, `builtin`)은 [wvb.config 파일](/docs/config)을 +참고하세요. 런타임 측 동작, 즉 클라이언트가 이 번들을 무선(OTA)으로 다운로드하고 검증하고 활성화하는 +방법은 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. + +## remote + +`remote`는 `RemoteConfig` 객체입니다. 모든 필드는 선택 사항이며, 여러분이 제공하는 `uploader`와 +`deployer`는 프로바이더 패키지에서 가져옵니다. + +| Field | Type | Default | Description | +| ------------------ | -------------------------------------- | -------------------------- | ----------------------------------------------------------- | +| `endpoint` | `string` | — | 원격 서버의 기본 URL. | +| `bundleName` | `BundleNameResolver` | `{ from: 'package.json' }` | 번들 이름을 결정하는 방법. | +| `version` | `VersionResolver` | `{ from: 'package.json' }` | 배포할 버전을 결정하는 방법. | +| `packBeforeUpload` | `boolean` | `true` | 업로드 전에 번들을 패킹. | +| `uploader` | `BaseRemoteUploader` | — | `.wvb`를 서버에 업로드(프로바이더에서 제공). | +| `deployer` | `BaseRemoteDeployer` | — | 버전을 배포됨으로 표시(프로바이더에서 제공). | +| `integrity` | `boolean \| IntegrityMakeConfig \| fn` | — | 업로드 시 무결성 해시를 계산. [integrity](#integrity) 참고. | +| `signature` | `SignatureSignConfig \| fn` | — | 업로드 시 무결성 해시에 서명. [signature](#signature) 참고. | + + +`RemoteConfig`에는 `channel`이나 `allowOtherVersions` 필드가 없습니다. 채널은 배포 시점의 인자 +(`wvb deploy --channel `)이지, 설정 객체의 속성이 아닙니다. + + +### Resolvers + +`bundleName`과 `version`은 리터럴 문자열, 리졸버 객체, 또는 함수를 받습니다. 함수 형태는 +`{ packageJson, dir, file }`을 받아 문자열(또는 문자열의 `Promise`)을 반환합니다. + +```ts title="wvb.config.ts" +remote: { + // 번들 이름: 'package.json'(name 필드, 스코프 제거) 또는 문자열 또는 함수. + bundleName: { from: 'package.json' }, + + // 버전: 'package.json'(version 필드), 'git'(HEAD 커밋 해시), 문자열, 또는 함수. + version: { from: 'git' }, +} +``` + +| Resolver | Accepted forms | +| ------------ | ------------------------------------------------------------------------------------------------------ | +| `bundleName` | `{ from: 'package.json' }` \| `string` \| `(params) => string \| Promise` | +| `version` | `{ from: 'package.json' }` \| `{ from: 'git' }` \| `string` \| `(params) => string \| Promise` | + +`{ from: 'package.json' }`은 `name`/`version` 필드를 읽으며, 값이 없으면 오류를 던집니다. +`{ from: 'git' }`(version 전용)은 현재 `HEAD` 커밋 해시를 사용합니다. + +### Providers + +`uploader`와 `deployer`는 `@wvb/config`에 내장되어 있지 않습니다. 프로바이더 패키지에서 가져옵니다. +각 프로바이더는 `remote`에 스프레드로 펼쳐 넣을 수 있는 `{ uploader, deployer }`를 반환하는 팩토리를 +익스포트합니다(AWS는 `signature` 서명자도 함께 반환합니다). + + + + + + + +## integrity + +무결성 기능은 번들 바이트의 암호학적 해시를 계산하여 번들과 함께 저장합니다. 클라이언트는 다운로드 시 +해시를 다시 계산하고, 해시가 일치하지 않는 번들을 거부합니다. 이는 전송 중에 바이트가 손상되거나 +변조되지 않았다는 증거입니다. 해시는 **SHA-2**를 사용합니다. + +`integrity`는 세 가지 형태를 받습니다. + +```ts title="wvb.config.ts" +// 1. Boolean — 기본 알고리즘(sha256)으로 활성화. +integrity: true, + +// 2. 알고리즘 선택. +integrity: { algorithm: 'sha384' }, // 'sha256' | 'sha384' | 'sha512' + +// 3. 커스텀 함수 — 전체 ":" 문자열을 직접 반환. +integrity: async ({ data }) => `sha256:${await myHash(data)}`, +``` + +| Form | Type | Notes | +| ------------- | -------------------------------------------------- | ------------------------------ | +| Boolean | `boolean` | `true`는 기본 알고리즘을 사용. | +| Config object | `{ algorithm?: 'sha256' \| 'sha384' \| 'sha512' }` | 기본 `algorithm`은 `'sha256'`. | +| Function | `(params: { data: Buffer }) => Promise` | 직렬화된 문자열을 반환. | + +직렬화된 출력은 `":"` 형태입니다. 알고리즘 이름, 콜론, 그다음 base64로 인코딩된 다이제스트가 +이어집니다. 예를 들어 `sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=`와 같습니다. + +## signature + +서명은 _누가_ 번들을 배포했는지 증명합니다. 무결성 문자열(위의 `":"` 값)의 바이트를 개인 +키로 서명하고, 클라이언트는 이에 대응하는 공개 키로 그 결과를 검증합니다. 서명되는 메시지가 무결성 +문자열이기 때문에, 서명 검증에는 무결성 값이 반드시 존재해야 합니다. + +`signature`는 `SignatureSignConfig`(`algorithm`을 기준으로 하는 판별 유니언) 또는 커스텀 서명 함수입니다. +이것은 **배포(서명) 측**이며, `@wvb/config`에 포함되어 제공됩니다. + +| `algorithm` | Required fields | Optional fields | +| ------------------ | --------------------------------------------- | --------------- | +| `'ecdsa'` | `curve` (`'p256'` \| `'p384'`), `hash`, `key` | — | +| `'ed25519'` | `key` | — | +| `'rsa-pkcs1-v1.5'` | `hash`, `key` | — | +| `'rsa-pss'` | `hash`, `key` | `saltLength` | + +`hash`는 `'sha256' | 'sha384' | 'sha512'` 중 하나입니다. `key`는 `SignatureSigningKeyConfig`입니다. + +| `format` | `data` type | +| -------------------------------- | ------------ | +| `'jwk'` | `JsonWebKey` | +| `'raw'` \| `'pkcs8'` \| `'spki'` | `Buffer` | + +```ts title="wvb.config.ts" +// ECDSA +signature: { + algorithm: 'ecdsa', + curve: 'p256', // 'p256' | 'p384' + hash: 'sha256', + key: { format: 'pkcs8', data: privateKeyDerBuffer }, +}, + +// Ed25519 +signature: { + algorithm: 'ed25519', + key: { format: 'pkcs8', data: privateKeyDerBuffer }, +}, + +// RSA-PSS (또는 'rsa-pkcs1-v1.5') +signature: { + algorithm: 'rsa-pss', + hash: 'sha256', + saltLength: 32, // rsa-pss 전용; 생략하면 hash 값에서 기본값을 정함 + key: { format: 'pkcs8', data: privateKeyDerBuffer }, +}, + +// 커스텀 서명자 — 무결성 문자열 바이트를 받아 base64 서명을 반환. +signature: async ({ message }) => myExternalSigner(message), +``` + + + 클라이언트는 대응하는 **공개** 키로 검증하며, 검증 측은 서명 측과 다른 키 포맷을 받습니다. SPKI, + PKCS#1(RSA 전용), SEC1(ECDSA 전용), raw 32바이트(Ed25519 전용)를 받지만 JWK는 받지 않습니다. + 검증에 대해서는 [원격 번들](/docs/guide/remote-bundles)을, 클라이언트 측 헬퍼에 대해서는 [Node API + 레퍼런스](/docs/references/node)를 참고하세요. + + +## Complete example + +AWS 프로바이더를 통해 배포하고, SHA-384로 해시하고, Ed25519로 서명하는 완전한 `wvb.config.ts`입니다. +다른 프로바이더를 대상으로 하려면 `awsRemote`를 `localRemote`나 `cloudflareRemote`로 바꾸세요. + +```ts title="wvb.config.ts" +import { readFileSync } from 'node:fs'; +import { defineConfig } from '@wvb/config'; +import { awsRemote } from '@wvb/remote-aws'; + +export default defineConfig(() => { + const provider = awsRemote({ + bucket: 'my-app-bundles', + aws: { region: 'us-east-1' }, + }); + + return { + remote: { + endpoint: 'https://updates.example.com', + bundleName: { from: 'package.json' }, + version: { from: 'git' }, + uploader: provider.uploader, + deployer: provider.deployer, + integrity: { algorithm: 'sha384' }, + signature: { + algorithm: 'ed25519', + key: { + format: 'pkcs8', + data: readFileSync('./keys/signing-key.pkcs8.der'), + }, + }, + }, + }; +}); +``` + +배포하려면 먼저 `wvb upload`를 실행하여 `.wvb`를 푸시하고(`packBeforeUpload`가 `true`이면 먼저 패킹합니다), +그다음 `wvb deploy --version `를 실행하여 버전을 라이브로 표시합니다. 전체 명령어 집합은 +[CLI 레퍼런스](/docs/guide/cli)를, 원격을 처음부터 끝까지 구축하고 테스트하는 방법은 +[원격 가이드](/docs/guide/remote)를 참고하세요. diff --git a/content/docs/guide/bundle-format.ko.mdx b/content/docs/guide/bundle-format.ko.mdx new file mode 100644 index 0000000..5bbe29f --- /dev/null +++ b/content/docs/guide/bundle-format.ko.mdx @@ -0,0 +1,165 @@ +--- +title: 번들 포맷 +description: .wvb 아카이브가 디스크에 섹션별로 어떻게 배치되는지, 그리고 체크섬이 손상으로부터 아카이브를 어떻게 보호하는지 설명합니다. +--- + +Webview Bundle은 빌드된 웹 자산을 압축하고 무결성을 검증한 하나의 아카이브로 묶은 단일 `.wvb` 파일입니다. 세 개의 섹션이 연속으로 기록됩니다. 고정 길이 17바이트 헤더 뒤에 가변 길이의 인덱스와 데이터 영역이 이어집니다. + +| Offset | Section | Size | 담는 내용 | +| ----------- | ------- | ---------- | ----------------------------------------- | +| `0` | Header | `17 bytes` | 매직 넘버, 포맷 버전, 인덱스 크기, 체크섬 | +| `17` | Index | variable | 경로 → 오프셋/길이/헤더 엔트리 맵 | +| after index | Data | variable | 경로순으로 정렬된 LZ4 압축 파일 내용 | + +각 섹션은 자체 체크섬을 가지므로, 잘리거나 손상된 아카이브는 내용을 제공하기 전에 걸러집니다. 번들을 만들려면 [CLI](/docs/guide/cli) 또는 [Node API](/docs/references/node)를 참고하세요. + +```ts title="build.ts" +import { readFile } from 'node:fs/promises'; +import { BundleBuilder, writeBundle } from '@wvb/node'; + +const builder = new BundleBuilder(); +builder.insertEntry('/index.html', await readFile('dist/index.html'), 'text/html'); +builder.insertEntry('/app.js', await readFile('dist/app.js'), 'application/javascript'); + +const bundle = builder.build(); +await writeBundle(bundle, '.wvb/app_1.0.0.wvb'); +``` + +관례상 패킹된 파일은 `_.wvb` 형식으로 이름을 짓습니다. 예를 들어 `app_1.0.0.wvb`입니다. 번들 이름과 버전은 소스 수준의 개념으로, 파일명과 매니페스트에 존재할 뿐 아카이브 내부에는 들어가지 않습니다. [번들 소스](/docs/guide/bundle-sources)를 참고하세요. + +## 헤더 + +헤더는 정확히 17바이트이며 고정 레이아웃을 가집니다. 직렬화기를 거치지 않고 직접 기록하므로, 모든 필드가 알려진 오프셋에 자리합니다. + +| Field | Offset | Length | Encoding | +| ------------ | ------ | ------ | ------------------------------ | +| Magic number | 0 | 8 | raw bytes `0xF09F8C90F09F8E81` | +| Version | 8 | 1 | single byte (포맷 버전) | +| Index size | 9 | 4 | `u32`, big-endian | +| Checksum | 13 | 4 | `u32`, big-endian xxHash-32 | + +인덱스 크기가 `1234`인 `V1` 번들의 경우, 17바이트는 다음과 같습니다. + +```text +F0 9F 8C 90 F0 9F 8E 81 magic (🌐🎁) +01 version (V1) +00 00 04 D2 index size = 1234, big-endian +31 38 03 10 xxHash-32 of the first 13 bytes +``` + +- 매직 넘버 `0xF09F8C90F09F8E81`은 🌐🎁(지구본 + 포장된 선물)의 UTF-8 인코딩입니다. 이 바이트를 찾지 못한 리더는 해당 파일을 번들이 아닌 것으로 판단해 거부합니다. +- 버전 바이트는 번들의 **포맷 버전**을 담습니다. 이 enum은 현재 `V1`이라는 단일 값만 가집니다. 이는 `1.0.0` 같은 번들의 릴리스 버전과는 별개이며, 릴리스 버전은 아카이브 내부에 절대 나타나지 않습니다. +- 인덱스 크기는 뒤따르는 인덱스의 바이트 길이입니다. 인덱스 자체의 후행 체크섬은 여기에 **포함되지 않습니다**. +- 체크섬은 처음 13바이트(체크섬 앞의 모든 바이트)에 대한 xxHash-32이며, big-endian으로 저장됩니다. + +디스크립터를 통해 헤더 필드를 다시 읽어올 수 있습니다. + +```ts +import { readBundle } from '@wvb/node'; + +const header = (await readBundle('app_1.0.0.wvb')).descriptor().header(); +header.version(); // 'v1' +header.indexSize(); // 인덱스의 바이트 길이 +header.indexEndOffset(); // 17 + indexSize + 4 -> 데이터 섹션이 시작되는 위치 +``` + +## 인덱스 + +인덱스는 헤더 바로 뒤에 위치하며, 각 파일 경로(예: `/index.html`)를 해당 파일의 바이트 위치와 제공 방식을 설명하는 엔트리로 매핑합니다. 각 엔트리는 다음을 담습니다. + +| Field | Type | 의미 | +| ---------------- | ------ | ------------------------------------------------- | +| `offset` | `u64` | 데이터 섹션 내에서 이 파일 데이터의 바이트 오프셋 | +| `len` | `u64` | **압축된** 바이트의 길이 | +| `content_type` | string | MIME 타입. 제공 시 `Content-Type` 헤더에 사용됨 | +| `content_length` | `u64` | 원본 **비압축** 크기. `Content-Length`에 사용됨 | +| `headers` | map | 파일 제공 시 재생할 HTTP 헤더 이름/값 쌍 | + +```ts +const index = (await readBundle('app_1.0.0.wvb')).descriptor().index(); + +index.containsPath('/index.html'); // true +index.getEntry('/index.html'); +// { +// offset: 0, +// len: 184, // 압축됨 +// contentType: 'text/html', +// contentLength: 412, // 비압축 +// headers: { 'cache-control': 'no-cache' }, +// isEmpty: false, +// } +``` + +직렬화 세부 사항: + +- big-endian 가변 길이 정수 구성으로 [bincode](https://github.com/bincode-org/bincode)를 사용해 인코딩합니다. 숫자 필드는 고정 폭이 아니라 varint로 인코딩됩니다. +- 맵은 인코딩 전에 경로순으로 정렬됩니다. 따라서 출력 바이트는 결정적이며 삽입 순서와 무관합니다. +- 인덱스 바이트에 대한 4바이트 big-endian xxHash-32 체크섬이 인덱스 바로 뒤에 기록됩니다. + +프로토콜 핸들러가 파일을 제공할 때는 저장된 `headers`를 재생한 다음, `content_type`으로 `Content-Type`을, `content_length`(비압축 크기)로 `Content-Length`를 설정합니다. URL이 엔트리로 어떻게 매핑되는지는 [프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. + +## 데이터 + +데이터 섹션은 인덱스와 동일한 순서로, 경로순으로 정렬된 파일 내용을 담습니다. 각 파일은 LZ4로 압축된 바이트와 그 압축 바이트에 대한 4바이트 big-endian xxHash-32 체크섬으로 구성됩니다. + +```text +[ LZ4 bytes for file A ][ xxHash-32 ][ LZ4 bytes for file B ][ xxHash-32 ] ... +``` + +파일은 크기를 앞에 붙이는 블록 포맷으로 [LZ4](https://github.com/lz4/lz4)를 사용해 압축됩니다. 따라서 비압축 길이가 압축 데이터와 함께 전달됩니다. 엔트리의 `offset`은 압축 바이트의 시작을 가리키고, `len`은 그 바이트의 길이이며, 파일별 체크섬은 `offset + len` 위치에 자리합니다. + +`getData`는 압축 해제된 바이트를 반환하고, `getDataChecksum`은 저장된 xxHash-32를 반환합니다. 호출자는 이를 사용해 정지 상태의 파일을 검증할 수 있습니다. + +```ts +const bundle = await readBundle('app_1.0.0.wvb'); + +bundle.getData('/index.html'); // Buffer (압축 해제됨) | null +bundle.getDataChecksum('/index.html'); // number (xxHash-32) | null +``` + +## 체크섬과 무결성 + +이 포맷은 서로 다른 두 가지 작업을 위해 두 가지 해싱 방식을 사용합니다. + +헤더, 인덱스, 파일별 데이터에 들어가는 **내부 체크섬**은 모두 **xxHash-32**입니다. 이들은 저장 무결성을 지킵니다. 파일이 잘리거나, 일부만 기록되거나, 디스크에서 손상되는 경우를 막습니다. xxHash는 빠르고 비암호학적이며, 이것이 바로 손상 검사에 필요한 특성입니다. + +번들의 **무결성** 값은 이와 다릅니다. 다운로드된 번들의 직렬화된 바이트에 대한 **SHA-2** 해시로, 클라이언트가 서버가 게시한 것을 정확히 받았는지 확인하는 데 쓰입니다. 알고리즘은 `sha256`(기본값), `sha384`, `sha512`이며, `:` 형식으로 직렬화됩니다. + +```text +sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= +sha384:Ws2q... +``` + +```ts +import type { IntegrityAlgorithm, IntegrityPolicy } from '@wvb/node'; +// IntegrityAlgorithm = 'sha256' | 'sha384' | 'sha512' +// IntegrityPolicy = 'strict' | 'optional' | 'none' (기본값: 'optional') +``` + + + 이 포맷은 무결성에 SHA-3가 아니라 **SHA-2**를 사용합니다. 두 작업은 서로 구별됩니다. xxHash-32는 + 정지 상태의 바이트를 보호하고, SHA-2 무결성은 다운로드한 내용을 검증합니다. 업데이트 중에 무결성과 + 서명이 어떻게 검사되는지는 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. + + +## 다음으로 볼 내용 + +정확한 상수와 필드 레이아웃을 포함한 바이트 수준 명세는 [docs.rs/wvb](https://docs.rs/wvb)의 Rust 크레이트 문서에 있습니다. + + + + + + diff --git a/content/docs/guide/bundle-sources.ko.mdx b/content/docs/guide/bundle-sources.ko.mdx new file mode 100644 index 0000000..380ae13 --- /dev/null +++ b/content/docs/guide/bundle-sources.ko.mdx @@ -0,0 +1,141 @@ +--- +title: 번들 소스 +description: Webview Bundle이 builtin 및 remote 소스에 걸쳐 번들을 디스크에 저장하고, 매니페스트로 추적하며, 채널별로 버전을 선택하는 방식을 설명합니다. +--- + +**소스**는 번들이 저장되는 기기 내의 위치입니다. 모든 앱은 두 개의 소스를 가집니다. 하나는 앱 패키지 안에 포함되어 배포되는 읽기 전용 `builtin` 소스이고, 다른 하나는 다운로드된 업데이트를 위한 쓰기 가능한 `remote` 소스입니다. 이 페이지에서는 소스가 어떻게 구성되는지, `manifest.json`이 버전을 어떻게 추적하는지, 그리고 채널이 클라이언트에게 전달할 버전을 어떻게 선택하는지 설명합니다. + +## 번들, 이름, 버전 + +**번들**은 하나의 논리적 웹 앱이며, **번들 이름**(예: `app`)으로 식별됩니다. 번들은 여러 **버전**(`1.0.0`, `1.1.0`, …)을 가지며, 각 버전은 별도의 `.wvb` 파일입니다. 어느 시점이든 하나의 버전이 **현재(current)** 버전으로, 웹뷰에 제공됩니다. + +번들 이름과 버전은 _소스 수준_ 식별자입니다. 이 값들은 디스크에 저장된 파일명과 매니페스트에 나타나지만, `.wvb` 아카이브 자체에는 저장되지 **않습니다**. 아카이브는 바이너리 파일 포맷 버전(`v1`)만 담습니다. 바이트 레이아웃은 [번들 포맷](/docs/guide/bundle-format)을 참고하세요. + +## builtin 소스와 remote 소스 + +앱은 동일한 번들 이름을 서로 다른 저장소에서 제공하는 두 개의 소스로 실행됩니다. + +- **`builtin`** — 앱 패키지 _안에_ 포함되어 배포되는 번들입니다. 읽기 전용이며, 앱을 처음 실행할 때 아직 아무것도 다운로드되지 않은 상태에서 폴백으로 사용됩니다. +- **`remote`** — 업데이트 서버에서 다운로드한 번들입니다. 쓰기 가능하며, 앱스토어 릴리스 없이 무선(OTA)으로 업데이트됩니다. + +번들이 두 소스에 모두 존재하면 **remote가 우선합니다**. 소스는 remote 매니페스트의 현재 버전을 먼저 확인하여 버전을 결정하며, remote에 아무것도 없으면 builtin으로 폴백합니다. + + + 두 소스는 서로 독립적인 디렉터리입니다. 소스 객체는 별도의 `builtinDir`과 `remoteDir`을 가지며, 각 + 디렉터리는 자체 `manifest.json`을 가집니다. 버전을 결정할 때는 remote 매니페스트를 먼저 읽고 + builtin 매니페스트를 나중에 읽습니다. + + +이러한 분리 덕분에 업데이트가 안전해집니다. 폴백할 수 있는 배포된 builtin 버전이 항상 존재하며, 다운로드된 remote 버전은 검증과 설치를 마치면 자연스럽게 builtin 버전을 가립니다. 다운로드 및 검증 흐름은 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. + +## 디스크 레이아웃 + +소스 디렉터리 안에서 각 번들은 자체 폴더를 가지며, 매니페스트는 루트에 위치합니다. + +```text +{source_dir}/ +├── {name}/ +│ └── {name}_{version}.wvb +└── manifest.json +``` + +따라서 `app` 번들의 두 버전을 담고 있는 remote 소스는 다음과 같습니다. + +```text +{remote_dir}/ +├── app/ +│ ├── app_1.0.0.wvb +│ └── app_1.1.0.wvb +└── manifest.json +``` + +파일 경로는 항상 `{source_dir}/{name}/{name}_{version}.wvb`입니다. builtin 소스와 remote 소스는 서로 다른 디렉터리이므로, 동일한 `app_1.1.0.wvb` 파일명이 양쪽에 모두 존재할 수 있으며 각각 별도의 매니페스트로 추적됩니다. + + + 번들 이름과 버전은 경로 구성 요소가 되므로 ASCII `[A-Za-z0-9._-]`로 제한됩니다. 빈 값일 수 없고, + `.`이나 `..`일 수 없으며, `.`으로 끝날 수 없고, Windows 예약 이름(`CON`, `PRN`, `AUX`, `NUL`, + `COM1`–`COM9`, `LPT1`–`LPT9`)일 수 없습니다. 그 외의 값은 파일 시스템에 닿기 전에 거부됩니다. + + +## 매니페스트 + +`manifest.json`은 소스 디렉터리의 루트에 위치합니다. 각 번들에 대해 존재하는 버전, 해당 메타데이터, 그리고 어느 버전이 현재 버전인지를 기록합니다. + +```json title="manifest.json" +{ + "manifestVersion": 1, + "entries": { + "app": { + "versions": { + "1.0.0": {}, + "1.1.0": {} + }, + "currentVersion": "1.0.0" + } + } +} +``` + +각 필드는 다음과 같습니다. + +| Field | Type | Description | +| -------------------------------- | ----------------- | ------------------------------------------- | +| `manifestVersion` | integer | 매니페스트 스키마 버전. 항상 `1`입니다. | +| `entries` | object | 번들 이름 → 엔트리의 맵입니다. | +| `entries.{name}.versions` | object | 버전 문자열 → 버전별 메타데이터의 맵입니다. | +| `entries.{name}.currentVersion` | string (optional) | 이 번들에 대해 제공되는 활성 버전입니다. | +| `entries.{name}.previousVersion` | string (optional) | 현재 버전 직전에 활성이었던 버전입니다. | + +각 버전의 메타데이터는 모든 필드가 선택 사항인 객체입니다. 빈 `{}`도 유효합니다. 메타데이터는 번들을 다운로드할 때 remote 서버의 응답 헤더로부터만 채워지기 때문입니다. + +| Field | Type | Description | +| -------------- | ------ | ------------------------------------------------------------- | +| `etag` | string | 다운로드한 번들에 대한 서버의 `ETag` 값입니다. | +| `integrity` | string | 무결성 해시. `:` 형식입니다(예: `sha256:n4bQ…`). | +| `signature` | string | 무결성 문자열에 대한 Base64 서명입니다. | +| `lastModified` | string | 서버의 `Last-Modified` 값입니다. | + +### 현재 버전과 이전 버전 + +`currentVersion`은 소스가 지금 제공하는 버전입니다. 여러 버전이 스테이징되었지만 아직 활성화된 버전이 없을 때는 이 값이 없을 수 있습니다. 버전을 스테이징하면 `currentVersion`을 바꾸지 않은 채 `versions` 아래에 해당 버전이 기록됩니다. + +`previousVersion`은 가장 최근 활성화 직전에 현재 버전이었던 버전입니다. 소스는 두 가지 이유로 이 버전을 보관합니다. 첫째, 정상 동작이 확인된 빌드로 롤백할 수 있게 해줍니다. 둘째, 이미 해당 파일을 연 진행 중인 요청이 새 버전이 현재 버전이 되는 동안에도 읽기를 깔끔하게 마칠 수 있도록 이전 파일을 디스크에 유지합니다. 현재 버전과 이전 버전만 보관되며, 더 오래된 버전은 정리됩니다. + +## 채널 + +**채널**을 사용하면 동일한 서버에서 서로 다른 대상에게 서로 다른 버전을 전달할 수 있습니다. `stable`, `beta`, `canary` 등이 그 예입니다. 채널은 단계적 롤아웃과 사전 배포 테스트를 가능하게 합니다. + +채널은 순수하게 **remote**와 **updater**의 개념입니다. `.wvb` 포맷의 일부가 _아니며_ `manifest.json`에도 저장되지 _않습니다_. 클라이언트는 remote의 HTTP 호출에 `channel` 쿼리 파라미터를 실어 요청마다 채널을 선택합니다. + +```text +GET /bundles?channel=beta +HEAD /bundles/app?channel=beta +GET /bundles/app?channel=beta +``` + +채널은 선택 사항입니다. **하드코딩된 기본 채널 이름은 없습니다.** 채널이 설정되지 않으면 쿼리 파라미터가 그냥 생략되고, 서버가 무엇을 반환할지 결정합니다. 전체 HTTP 규약과 업데이터 흐름은 [원격 번들](/docs/guide/remote-bundles)을, 업데이터의 채널 설정은 [원격 설정](/docs/config/remote)을 참고하세요. + +## 소스가 제공되는 방식 + +소스는 웹뷰 요청에 응답하는 프로토콜 계층에 데이터를 공급합니다. 프로토콜은 들어오는 URI를 번들 이름과 파일 경로로 매핑하고, 소스에 현재 버전을 결정하도록 요청한 뒤, 일치하는 `.wvb` 안에서 파일을 제공합니다. URI가 번들 조회로 변환되는 방식은 [프로토콜 처리](/docs/guide/protocol-handling)를, 런타임이 builtin 및 remote 디렉터리를 가리키도록 설정하는 방법은 [wvb.config 파일](/docs/config)을 참고하세요. + +## 관련 페이지 + + + + + + + diff --git a/content/docs/guide/cli-programmatic.ko.mdx b/content/docs/guide/cli-programmatic.ko.mdx new file mode 100644 index 0000000..e2db7d4 --- /dev/null +++ b/content/docs/guide/cli-programmatic.ko.mdx @@ -0,0 +1,281 @@ +--- +title: 프로그래밍 방식 사용 +description: '@wvb/cli/api로 스크립트와 CI에 wvb CLI를 내장하고, 직접 작성한 코드에서 wvb 설정 파일을 불러옵니다.' +--- + +모든 `wvb` 명령어는 함수이기도 합니다. `@wvb/cli` 패키지는 `@wvb/cli/api`에서 프로그래밍 API를 제공합니다. +이 API는 CLI가 실행하는 것과 동일한 패킹, 추출, 제공, 업로드, 로컬 원격 로직을 노출합니다. 따라서 별도로 +셸을 띄우지 않고도 빌드 스크립트, 테스트 하네스, CI에서 Webview Bundle을 다룰 수 있습니다. 기본 `@wvb/cli` +엔트리는 설정 헬퍼 `defineConfig`, `loadConfigFile`, `resolveConfig`를 별도로 노출합니다. 이를 통해 +여러분의 도구가 CLI와 동일한 방식으로 `wvb.config` 파일을 읽고 해석할 수 있습니다. + +`@wvb/cli`가 필요한 스크립트나 워크스페이스에 의존성으로 설치하세요. + + + + +```sh +npm install --save-dev @wvb/cli +``` + + + + +```sh +pnpm add -D @wvb/cli +``` + + + + +```sh +yarn add -D @wvb/cli +``` + + + + + + 반환값(패킹된 `Bundle`, 실행 중인 서버 핸들)이 필요하거나 모든 작업을 하나의 Node 프로세스 안에서 + 유지하고 싶다면, `wvb` 바이너리를 실행하는 대신 API를 사용하세요. 명령줄 인터페이스는 [CLI + 레퍼런스](/docs/guide/cli)를 참고하세요. + + +## `/api` 익스포트 + +`@wvb/cli/api`에서 함수를 임포트하세요. 각 함수는 하나의 옵션 객체를 받고 프로미스를 반환합니다. 실패하면 +`ApiError`를 던집니다. + +| Function | Signature | +| -------------- | ---------------------------------------------------------------------- | +| `pack` | `pack(params: PackParams): Promise` | +| `extract` | `extract(params: ExtractParams): Promise` | +| `serve` | `serve(params: ServeParams): Promise` | +| `remoteUpload` | `remoteUpload(params: RemoteUploadParams): Promise` | +| `builtin` | `builtin(params: BuiltinParams): Promise` | +| `localRemote` | `localRemote(params: LocalRemoteParams): Promise` | + + + `/api` 인터페이스는 의도적으로 CLI에 가깝게 설계되었으며, 완전한 클라이언트는 아닙니다. 여기에는 + `deploy`나 `download` 함수가 없습니다. 이 명령어들은 CLI에만 존재하며, + [`@wvb/node`](/docs/references/node)의 `Remote` 클래스를 통해 원격과 통신합니다. + + +### pack + +소스 디렉터리를 `.wvb` 아카이브로 패킹합니다. `outFile`에 확장자를 생략하면 `.wvb` 확장자가 자동으로 +붙습니다. + +```ts +import { pack } from '@wvb/cli/api'; + +const result = await pack({ + srcDir: './dist', + outFile: './.wvb/app', // ./.wvb/app.wvb가 됩니다 + write: true, // 기본값 true; false로 설정하면 메모리에만 패킹합니다 + overwrite: true, // 기본값 true +}); + +console.log(result.outFilePath); // 작성된 .wvb의 절대 경로 +console.log(result.bundle); // 메모리에 있는 Bundle +``` + +`PackParams`는 `srcDir`, `outFile`, 선택적 `ignores`와 `headers`(설정 파일과 동일한 형태), `write` +(기본값 `true`), `overwrite`(기본값 `true`), `cwd`(기본값 `process.cwd()`), `logLevel`(기본값 +`'info'`), 그리고 선택적 `logger`를 받습니다. 반환되는 `PackResult`는 `outFilePath`와 `bundle` +(`@wvb/node`의 `Bundle`)을 가집니다. + +### extract + +`.wvb` 파일을 읽어 그 엔트리를 디렉터리에 씁니다. + +```ts +import { extract } from '@wvb/cli/api'; + +const bundle = await extract({ + file: './.wvb/app.wvb', + outDir: './extracted', // 생략하면 .wvb/이 기본값입니다 + clean: true, // 기본값 false; outDir을 먼저 제거합니다 +}); +``` + +`ExtractParams`는 `file`(필수), 선택적 `outDir`, `cwd`, `write`(기본값 `true`), `clean`(기본값 +`false`), 그리고 `logger`를 받습니다. 파싱된 `Bundle`을 반환합니다. + +### serve + +번들을 웹뷰에 제공하는 로컬호스트 서버를 시작합니다. 디렉터리 경로는 `index.html`로 해석됩니다. + +```ts +import { serve } from '@wvb/cli/api'; + +const instance = await serve({ + file: './.wvb/app.wvb', + port: 4312, // 기본값 4312 + silent: false, // 기본값 false; true로 설정하면 요청 로깅을 끕니다 +}); + +// ... 나중에 +await instance.shutdown(); +``` + +`ServeParams`는 `file`(필수), 선택적 `hostname`, `port`(기본값 `4312`), `silent`(기본값 `false`), +`cwd`, `logger`, `colorEnabled`를 받습니다. 원시 `server`와 `shutdown()` 메서드를 가진 `ServeInstance`를 +반환합니다. + +### remoteUpload + +패킹된 번들을 원격에 업로드합니다. 파일 경로나 메모리에 있는 `Bundle`을 원격 설정의 `uploader`와 함께 +전달하세요. + +```ts +import { pack, remoteUpload } from '@wvb/cli/api'; +import { loadConfigFile } from '@wvb/cli'; + +const config = await loadConfigFile(); +const { bundle } = await pack({ srcDir: './dist', outFile: './.wvb/app' }); + +await remoteUpload({ + file: bundle, + bundleName: 'app', + version: '1.2.0', + uploader: config.remote.uploader, + integrity: true, // 기본값 true +}); +``` + +`RemoteUploadParams`는 `file`(경로 또는 `Bundle`), `bundleName`, `version`, `uploader` +(`BaseRemoteUploader`)를 받습니다. 선택적 필드는 `force`, `integrity`(기본값 `true`; 무결성 설정을 +전달해 커스터마이즈), `signature`, `logger`, `cwd`입니다. + + + 무결성은 SHA-2(기본값 `sha256`)를 사용합니다. 서명을 제공하면 무결성 문자열 바이트에 서명합니다. + 무결성과 서명이 어떻게 함께 동작하는지는 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. + + +### builtin + +원격 또는 로컬 대상에서 빌트인 번들을 앱에 설치합니다. + +```ts +import { builtin } from '@wvb/cli/api'; + +const result = await builtin({ + target: { type: 'remote' }, // 판별 유니언: 'remote' | 'local' + dir: './.wvb/builtin/bundles', + clean: true, // 기본값 true +}); + +console.log(result.manifest); +``` + +`BuiltinParams`는 `target`(`type`으로 구분되는 `BuiltinTarget` 판별 유니언), 선택적 `dir`(기본값 +`'.wvb/builtin/bundles'`), `include`/`exclude` 매치 리스트, `channel`, `clean`(기본값 `true`), +`cwd`, `write`(기본값 `true`), `logLevel`, `logger`, `progress`, 그리고 모바일 프리셋 `android`와 +`ios`를 받습니다. 설치된 번들을 설명하는 `manifest`를 가진 `BuiltinResult`를 반환합니다. + +### localRemote + +개발용 로컬 원격 서버를 시작합니다. 이는 `wvb remote local` 명령어를 그대로 반영하며, 선택적 피어 의존성 +`@wvb/remote-local-provider`를 동적으로 임포트합니다. 따라서 해당 패키지가 설치되어 있는지 확인하세요. + +```ts +import { localRemote } from '@wvb/cli/api'; + +const instance = await localRemote({ + baseDir: '~/.wvb/local', + port: 4313, // 기본값 4313 + allowOtherVersions: false, +}); + +// ... 나중에 +await instance.shutdown(); +``` + +`LocalRemoteParams`는 선택적 `baseDir`(프로바이더가 기본값으로 `~/.wvb/local`을 해석), `hostname`, +`port`(기본값 `4313`), `silent`(기본값 `false`), `allowOtherVersions`, `logger`, `colorEnabled`를 +받습니다. 원시 `server`와 `shutdown()`을 가진 `LocalRemoteInstance`를 반환합니다. + + + `@wvb/remote-local-provider`는 선택적 피어 의존성입니다. 설치되어 있지 않으면 `localRemote`가 이를 + 임포트하려 할 때 예외를 던집니다. 로컬 원격을 사용할 때는 `@wvb/cli`와 함께 설치하세요. + + +## 기본 엔트리의 설정 헬퍼 + +기본 `@wvb/cli` 엔트리(`/api`가 아님)는 설정 도구를 다시 익스포트합니다. [`@wvb/config`](/docs/config)의 +`defineConfig`, 그리고 `loadConfigFile`과 `resolveConfig`, `InlineConfig`와 `ResolvedConfig` 타입을 +노출합니다. + +```ts +import { + defineConfig, + loadConfigFile, + resolveConfig, + type InlineConfig, + type ResolvedConfig, +} from '@wvb/cli'; + +// 현재 디렉터리에서 wvb.config.*를 찾아 불러옵니다. +const config: ResolvedConfig = await loadConfigFile(); + +// 또는 명시적인 파일을 불러오고 인라인 오버라이드를 그 위에 병합합니다. +const inline: InlineConfig = { root: process.cwd() }; +const merged = await resolveConfig(inline); +``` + +`loadConfigFile`은 설정 파일을 찾아 번들링한 다음 해석합니다. `resolveConfig`는 파일 설정을 인라인 설정 +아래에 병합하고, `root`(기본값 `process.cwd()`)를 설정하고, 가장 가까운 `package.json`을 붙인 다음 +읽기 전용 `ResolvedConfig`를 반환합니다. 완전한 타입 추론을 얻으려면 설정 파일 안에서 `defineConfig`를 +사용하세요. + +## 설정 파일 탐색 + +명시적인 경로 없이 `loadConfigFile`을 호출할 때, 그리고 CLI가 `--config` 없이 실행될 때, Webview Bundle은 +작업 디렉터리에서 다음 순서로 처음 일치하는 파일을 찾습니다. + +```text +wvb.config.js +wvb.config.cjs +wvb.config.mjs +wvb.config.ts +wvb.config.cts +wvb.config.mts +webview-bundle.config.js +webview-bundle.config.cjs +webview-bundle.config.mjs +webview-bundle.config.ts +webview-bundle.config.cts +webview-bundle.config.mts +wvb.config.json +wvb.config.jsonc +``` + +두 기본 이름 `wvb.config.*`와 `webview-bundle.config.*`는 모두 `.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, +`.mts` 확장자를 지원합니다. `.json`과 `.jsonc` 확장자는 `wvb.config` 기본 이름에서만 지원됩니다. 탐색을 +건너뛰려면 `--config`, `-C`를 전달하거나 명시적인 경로로 `loadConfigFile`을 호출하세요. + +설정 파일은 실행되기 전에 `rolldown`으로 번들링되므로, 별도의 빌드 단계 없이 TypeScript와 ESM/CJS 파일을 +불러옵니다. 베어 및 `npm:` 지정자와 Node 빌트인은 외부로 유지됩니다. + + + Deno에서는 설정 파일이 항상 ESM으로 취급됩니다. 로더가 Node에서 사용하는 package-type 및 확장자 + 추론을 건너뜁니다. Deno Desktop 지원은 실험적입니다. [Deno 가이드](/docs/guide/platforms/deno)를 + 참고하세요. + + +## 관련 문서 + + + + + + diff --git a/content/docs/guide/cli.ko.mdx b/content/docs/guide/cli.ko.mdx new file mode 100644 index 0000000..6d7cc97 --- /dev/null +++ b/content/docs/guide/cli.ko.mdx @@ -0,0 +1,126 @@ +--- +title: 개요 +description: wvb 명령줄 도구는 웹 자산을 .wvb 번들로 묶고, 업로드부터 배포, 다운로드까지 전체 워크플로를 처리합니다. +--- + +`wvb` 명령줄 도구는 빌드된 웹 자산을 `.wvb` 번들로 묶습니다. 또한 원격 서버에서 번들을 업로드하고, 배포하고, 다운로드하고, 살펴보는 전체 원격 업데이트 워크플로를 처리합니다. 이 도구는 `@wvb/cli` 패키지로 제공되며, 동일하게 동작하는 두 개의 바이너리 `wvb`와 `webview-bundle`을 노출합니다. + +개발 의존성으로 설치하세요. + + + + +```sh +npm install -D @wvb/cli +npx wvb --help +``` + + + + +```sh +pnpm add -D @wvb/cli +pnpm wvb --help +``` + + + + +```sh +yarn add -D @wvb/cli +yarn wvb --help +``` + + + + +대부분의 명령어는 작업 디렉터리에서 찾은 [`wvb.config`](/docs/config) 파일에서 기본값을 읽습니다. 따라서 일반적인 프로젝트에서는 인자 없이 `wvb pack`이나 `wvb upload`를 실행합니다. 각 명령어에는 전체 옵션 레퍼런스를 담은 전용 페이지가 있습니다. JavaScript에서 동일한 로직을 호출하려면 [프로그래밍 방식 API](/docs/guide/cli-programmatic)를 참고하세요. + +## 전역 플래그 + +세 가지 플래그가 모든 명령어에 적용됩니다. 이 플래그는 출력 형식과 로깅만 제어합니다. + +| 플래그 | 값 / 타입 | 기본값 | 환경변수 | 설명 | +| --------------- | ----------------------------------------- | ------- | ----------- | ----------------------------------------------------------------- | +| `--color` | `off` \| `on` \| `auto` | `auto` | `COLOR` | 출력의 색상 모드입니다. `auto`는 TTY나 CI 환경에서 색상을 켭니다. | +| `--log-level` | `debug` \| `info` \| `warning` \| `error` | `info` | `LOG_LEVEL` | 출력할 최소 로그 레벨입니다. | +| `--log-verbose` | boolean | `false` | — | 타임스탬프와 카테고리를 포함한 상세 로깅입니다. | + + +`--config`(`-C`)와 `--cwd`는 전역 플래그가 **아닙니다**. 이 둘은 각 명령어별로 선언되며 대부분의 명령어에 존재합니다. `extract`는 `--cwd`를 받지만 `--config`는 없고, `remote local`은 둘 다 받지 않습니다. 존재하는 경우 `--config `는 특정 설정 파일을 가리키고, `--cwd `는 경로를 해석할 때 사용하는 디렉터리를 바꿉니다. + + +불리언 플래그는 `--flag`, `--flag=true|false`, 그리고 부정형인 `--no-flag`를 받습니다. 예를 들어 `--no-write`와 `--no-pack`은 `--write`와 `--pack`의 기본값을 끕니다. + +## 설정 파일 탐색 + +명령어는 작업 디렉터리에 있는 `wvb.config` 파일에서 기본값을 로드합니다. 특정 파일을 가리키려면 `--config `를 전달하세요. 전달하지 않으면 `wvb`는 다음 순서로 첫 번째로 일치하는 파일을 찾습니다. + +```text +wvb.config.js +wvb.config.cjs +wvb.config.mjs +wvb.config.ts +wvb.config.cts +wvb.config.mts +webview-bundle.config.js +webview-bundle.config.cjs +webview-bundle.config.mjs +webview-bundle.config.ts +webview-bundle.config.cts +webview-bundle.config.mts +wvb.config.json +wvb.config.jsonc +``` + +`wvb.config`와 `webview-bundle.config` 기본 이름은 모두 `.js`, `.cjs`, `.mjs`, `.ts`, `.cts`, `.mts` 확장자를 지원합니다. `.json`과 `.jsonc` 확장자는 `wvb.config` 기본 이름에서만 지원합니다. 전체 스키마는 [설정](/docs/config)을 참고하세요. + +## 명령어 + + + + + + + + + + + + diff --git a/content/docs/guide/commands/builtin.ko.mdx b/content/docs/guide/commands/builtin.ko.mdx new file mode 100644 index 0000000..803fdaa --- /dev/null +++ b/content/docs/guide/commands/builtin.ko.mdx @@ -0,0 +1,79 @@ +--- +title: wvb builtin +description: 원격 또는 로컬 대상에서 내장 웹뷰 번들을 앱에 설치합니다. +--- + +`wvb builtin`은 앱이 내장 폴백으로 함께 배포할 수 있도록 번들을 출력 디렉터리에 설치합니다. 앱이 오프라인으로 시작하거나 무선(OTA)으로 더 새로운 번들을 가져오기 전에는 이 내장 번들을 제공합니다. 이 명령어는 [설정](/docs/config)의 `builtin.target`에 정의된 소스에서 번들을 가져오며, 기본값은 원격 대상입니다. + +이 명령어는 설치된 번들 집합을 설명하는 `manifest.json`과, 번들마다 `/_.wvb` 형태로 배치된 파일을 하나씩 기록합니다. 내장 소스와 원격 소스가 함께 동작하는 방식은 [번들 소스](/docs/guide/bundle-sources)를 참고하세요. + +## Usage + +```sh +# 설정된 대상에서 기본 디렉터리로 설치합니다 +wvb builtin + +# 원격 엔드포인트에서 지정한 디렉터리로 설치합니다 +wvb builtin --endpoint https://updates.example.com --out .wvb/builtin/bundles + +# 설치할 번들을 필터링합니다 +wvb builtin --include 'app*' --exclude 'internal*' + +# 감지된 Android 모듈에 설치합니다 +wvb builtin --android + +# 드라이 런: 실제로 기록하지 않고 설치될 내용만 보고합니다 +wvb builtin --no-write +``` + +## Options + +| Option | Aliases | Default | Description | +| --------------- | ------- | ------------------------- | ------------------------------------------------------------------------------------------------- | +| `--out` | `-O` | `.wvb/builtin/bundles` | 출력 디렉터리. 모바일 프리셋을 사용하면 플랫폼 모듈 디렉터리로 변경됩니다. | +| `--endpoint` | `-E` | `remote.endpoint` | 번들을 가져올 원격 엔드포인트. 원격 대상에만 적용됩니다. | +| `--channel` | — | — | 설치할 릴리스 채널. 원격 대상에만 적용됩니다. | +| `--include` | — | — | 대상 번들에 적용할 glob 포함 패턴. 반복해서 지정할 수 있습니다. | +| `--exclude` | — | — | 대상 번들에 적용할 glob 제외 패턴. 반복해서 지정할 수 있습니다. | +| `--clean` | — | `true` | 설치 전에 출력 디렉터리를 비웁니다. 기존 파일을 유지하려면 `--no-clean`을 전달하세요. | +| `--concurrency` | — | CPU 개수, 최대 8개로 제한 | 병렬 다운로드 개수. 원격 대상에만 적용됩니다. | +| `--android` | — | — | 모바일 프리셋. 인자 없이 쓰면 앱 모듈을 자동 감지하고, `--android=`로 명시적으로 지정합니다. | +| `--ios` | — | — | 모바일 프리셋. 인자 없이 쓰면 프로젝트를 자동 감지하고, `--ios=`로 명시적으로 지정합니다. | +| `--write` | — | `true` | 설치된 파일을 기록합니다. 드라이 런은 `--no-write`를 전달하세요. | +| `--progress` | — | `true` | 다운로드 진행률 표시줄을 보여줍니다. 원격 대상에만 적용됩니다. | +| `--config` | `-C` | (자동 탐색) | 설정 파일 경로. | +| `--cwd` | — | `process.cwd()` | 경로를 해석할 작업 디렉터리. | + +불리언 플래그는 `--flag`, `--flag=true|false`, 그리고 `--no-flag` 부정 형태를 받습니다. 따라서 `--no-write`와 `--no-clean`은 각각 `--write`와 `--clean`의 기본값을 끕니다. + +## Mobile presets + +`--android`와 `--ios`는 일반 디렉터리 대신 플랫폼 프로젝트에 번들을 설치합니다. 각각 인자 없이 전달하면 대상을 자동 감지하고, `--android=` / `--ios=`로 전달하면 명시적인 모듈이나 프로젝트 디렉터리를 가리킵니다. `--ios` 프리셋은 `Project.swift`에 `folderReference`를 추가하며, 이는 Tuist 프로젝트에 적용됩니다. + +한 번의 실행에서 `--android`와 `--ios`를 동시에 전달할 수 없습니다. + +## Notes + +- 대상 유형은 [설정](/docs/config)의 `builtin.target`에서 결정되며, `remote`와 `local`의 구별된 유니온입니다. 기본값은 원격 대상입니다. +- `--endpoint`, `--channel`, `--concurrency`, `--progress`는 원격 대상에만 적용됩니다. +- 실행에 성공하면 `manifest.json` 하나와, 설치된 번들마다 `/_.wvb` 파일 하나를 기록합니다. + +## See also + + + + + + diff --git a/content/docs/guide/commands/deploy.ko.mdx b/content/docs/guide/commands/deploy.ko.mdx new file mode 100644 index 0000000..11effe7 --- /dev/null +++ b/content/docs/guide/commands/deploy.ko.mdx @@ -0,0 +1,59 @@ +--- +title: wvb deploy +description: 이미 업로드된 번들 버전을 클라이언트가 받을 현재 버전으로 지정합니다. +--- + +`wvb deploy`는 이미 원격에 올라와 있는 번들 버전을 현재 버전으로 승격합니다. 그러면 번들을 다운로드하는 클라이언트가 그 버전을 받습니다. 번들을 업로드하면 원격에 저장되지만 현재 버전이 되지는 않습니다. 스위치를 켜는 단계가 바로 deploy입니다. + +[`wvb upload`](/docs/guide/commands/upload)을 실행한 뒤에 사용하세요. 또는 `upload`에 `--deploy`를 넘겨 두 단계를 한 명령어로 합칠 수도 있습니다. + +## Usage + +```sh +wvb deploy app --version 1.2.0 +wvb deploy app --version 1.2.0 --channel beta +wvb deploy # 설정 / package.json 기본값을 사용합니다 +``` + +번들 이름은 위치 인자 `BUNDLE`이며, 생략하면 설정이나 `package.json`에서 해석한 이름으로 대체됩니다. 버전은 `--version`(`-V`) 플래그로 지정합니다. 버전을 위한 위치 인자는 없습니다. + +## Options + +| Option | Aliases | Default | Description | +| ----------- | ------- | ----------------------------- | --------------------------------- | +| `BUNDLE` | — | 설정에서 해석 | 배포할 번들 이름. | +| `--version` | `-V` | 설정 또는 `package.json` 버전 | 현재 버전으로 지정할 버전. | +| `--channel` | — | — | 배포할 릴리스 채널. | +| `--config` | `-C` | 설정 자동 탐색 | 설정 파일 경로. | +| `--cwd` | — | `process.cwd()` | 경로 해석에 사용할 작업 디렉터리. | + +전역 플래그 `--color`, `--log-level`, `--log-verbose`도 함께 적용됩니다. 자세한 내용은 [CLI 개요](/docs/guide/cli)를 참고하세요. + +## Requirements + +`deploy`를 사용하려면 [설정](/docs/config/remote)에 `remote.deployer`가 필요합니다. deployer는 원격에서 어떤 버전이 현재 버전인지 기록하는 구성 요소입니다. 따라서 deployer가 없는 원격에는 배포할 수 없습니다. + + + 버전은 위치 인자가 아니라 `--version`(`-V`) 플래그로 지정합니다. 생략하면 deploy는 설정이나 가장 + 가까운 `package.json`에서 해석한 버전을 사용합니다. + + +## See also + + + + + + diff --git a/content/docs/guide/commands/download.ko.mdx b/content/docs/guide/commands/download.ko.mdx new file mode 100644 index 0000000..e7f4a2b --- /dev/null +++ b/content/docs/guide/commands/download.ko.mdx @@ -0,0 +1,67 @@ +--- +title: wvb download +description: 원격 서버에서 번들을 디스크로 다운로드하거나, 메타데이터를 가져와 출력합니다. +--- + +`wvb download`는 원격 서버에서 번들을 가져오며, 기본적으로 `.wvb` 파일로 디스크에 저장합니다. 번들 이름과 선택적으로 버전을 전달하세요. 버전을 생략하면 현재 배포된 버전을 다운로드합니다. 이 명령어는 `wvb upload`와 `wvb deploy`가 이끄는 배포 워크플로의 읽기 측면에 해당하며, 원격 서버가 실제로 무엇을 제공하는지 확인할 때 유용합니다. + +## Usage + +```sh +wvb download app --endpoint https://updates.example.com +wvb download app 1.2.0 --out ./bundles/app.wvb --overwrite +wvb download app --no-write +``` + +첫 번째 예제는 현재 배포된 `app` 버전을 작업 디렉터리의 `app.wvb`로 다운로드합니다. 두 번째 예제는 특정 버전(`1.2.0`)을 명시한 경로로 다운로드하며, 기존 파일이 있으면 덮어씁니다. 세 번째 예제는 번들을 가져와 정보를 출력하되 디스크에는 아무것도 쓰지 않습니다. + +번들 이름과 엔드포인트는 모두 [`wvb.config`](/docs/config/remote) 파일 값으로 대체되므로, 설정이 갖춰진 프로젝트 안에서는 보통 플래그 없이 `wvb download`만 실행해 현재 버전을 가져올 수 있습니다. + +## Options + +| Option | Aliases | Default | Description | +| ------------- | ------- | ------------------- | ------------------------------------------------------------------------------- | +| `BUNDLE` | — | from config | 번들 이름(위치 인자). 생략하면 설정에서 확인합니다. | +| `VERSION` | — | current deployed | 특정 버전(위치 인자). 생략하면 현재 배포된 버전을 다운로드합니다. | +| `--out` | `-O` | `.wvb` | 출력 파일 경로입니다. | +| `--endpoint` | `-E` | `remote.endpoint` | 다운로드할 원격 엔드포인트입니다. | +| `--channel` | — | — | 릴리스 채널입니다. | +| `--write` | — | `true` | 번들을 디스크에 씁니다. `--no-write`를 전달하면 가져와서 정보만 출력합니다. | +| `--overwrite` | — | `false` | 기존 출력 파일을 덮어씁니다. | +| `--progress` | — | `true` | 다운로드 진행률 표시줄을 보여줍니다. `--no-progress`를 전달하면 비활성화됩니다. | +| `--config` | `-C` | auto-discovery | 설정 파일 경로입니다. | +| `--cwd` | — | `process.cwd()` | 경로를 확인할 작업 디렉터리입니다. | + +불리언 플래그는 `--flag`, `--flag=true|false`, 그리고 `--no-flag` 부정 형태를 받습니다. 세 가지 전역 플래그(`--color`, `--log-level`, `--log-verbose`)도 여기에 적용됩니다. [CLI 개요](/docs/guide/cli)를 참고하세요. + + + 버전 위치 인자를 지정하면 `wvb download`는 해당 버전을 정확히 요청합니다. 지정하지 않으면 해당 + 채널에서 현재 배포된 버전을 다운로드합니다. 현재 버전이 아닌 버전을 제공하려면 원격 서버가 다른 + 버전을 허용하도록 설정되어 있어야 합니다. + + + + `--no-write`는 번들을 가져와 정보를 출력하되 파일을 저장하지 않습니다. 디스크를 건드리지 않고 원격 + 서버가 무엇을 제공하는지 확인할 때 사용하세요. 번들 본문을 전혀 다운로드하지 않고 메타데이터만 + 보려면 대신 [`wvb remote current`](/docs/guide/commands/remote)를 사용하세요. + + +## See also + + + + + + diff --git a/content/docs/guide/commands/extract.ko.mdx b/content/docs/guide/commands/extract.ko.mdx new file mode 100644 index 0000000..427c6af --- /dev/null +++ b/content/docs/guide/commands/extract.ko.mdx @@ -0,0 +1,59 @@ +--- +title: wvb extract +description: .wvb 아카이브의 파일을 디스크로 다시 풀어 번들에 담긴 내용을 확인합니다. +--- + +`wvb extract`는 `.wvb` 아카이브를 읽어 그 안의 파일을 디스크로 다시 씁니다. 번들이 무엇을 담고 있는지 확인하거나, 두 번들을 비교하거나, 번들에 들어간 자산을 복원할 때 사용하세요. + +## Usage + +추출할 번들 파일을 전달하세요. 이 명령어는 풀어낸 파일을 출력 디렉터리에 쓰며, 기본값은 `.wvb/`입니다. + +```sh +wvb extract ./build/app.wvb +wvb extract ./build/app.wvb --outdir ./unpacked +wvb extract ./build/app.wvb --outdir ./unpacked --clean +``` + +`--no-write`를 전달하면 디스크를 건드리지 않고 추출을 실행합니다. 번들이 문제없이 읽히는지 검증할 때 유용합니다. + +```sh +wvb extract ./build/app.wvb --no-write +``` + +## Options + +| Option | Aliases | Default | Description | +| ---------- | ------- | ------------------------------ | ----------------------------------------------------------------- | +| `FILE` | — | required | 추출할 번들 파일. | +| `--outdir` | `-O` | `.wvb/` | 풀어낸 파일을 저장할 대상 디렉터리. | +| `--clean` | — | `false` | 출력 디렉터리가 이미 있으면 먼저 삭제합니다. | +| `--write` | — | `true` | `--no-write`를 전달하면 파일을 쓰지 않고 추출을 시뮬레이션합니다. | +| `--cwd` | — | `process.cwd()` | 경로를 해석하는 데 사용하는 작업 디렉터리. | + + + `wvb extract`에는 `--config` 플래그가 없습니다. 전달한 번들 파일과 위의 플래그만 읽으며, + `wvb.config` 파일은 불러오지 않습니다. 다만 상대 경로를 해석하기 위한 `--cwd`는 받습니다. + + +전역 플래그인 `--color`, `--log-level`, `--log-verbose`는 다른 모든 명령어에서와 마찬가지로 여기서도 적용됩니다. 불리언 플래그는 `--flag`, `--flag=true|false`, 그리고 `--no-flag` 부정 형태를 받습니다. 따라서 `--no-write`는 기본 동작인 `--write`를 끕니다. + +## See also + + + + + + diff --git a/content/docs/guide/commands/meta.ko.json b/content/docs/guide/commands/meta.ko.json new file mode 100644 index 0000000..d40b31d --- /dev/null +++ b/content/docs/guide/commands/meta.ko.json @@ -0,0 +1,4 @@ +{ + "title": "명령어", + "pages": ["pack", "extract", "serve", "upload", "deploy", "download", "builtin", "remote"] +} diff --git a/content/docs/guide/commands/pack.ko.mdx b/content/docs/guide/commands/pack.ko.mdx new file mode 100644 index 0000000..abf9d18 --- /dev/null +++ b/content/docs/guide/commands/pack.ko.mdx @@ -0,0 +1,88 @@ +--- +title: wvb pack +description: 빌드된 웹 자산 디렉터리를 압축되고 무결성이 검증된 단일 .wvb 아카이브로 묶습니다. +--- + +`wvb pack`은 빌드된 웹 자산(HTML, JS, CSS, 미디어)이 담긴 디렉터리를 단일 `.wvb` 아카이브로 묶습니다. 이 아카이브는 압축되고 체크섬이 계산됩니다. 앱은 이 아카이브를 배포하여 웹뷰에 제공하거나, 무선(OTA) 업데이트를 위해 원격 서버에 업로드합니다. + +대부분의 프로젝트는 `wvb pack`을 인자 없이 실행합니다. 소스 디렉터리, 출력 경로를 비롯한 기본값은 작업 디렉터리에서 발견된 [`wvb.config`](/docs/config) 파일에서 가져옵니다. + +## Usage + +```sh +wvb pack [SRC_DIR] +``` + +기본 소스 디렉터리를 묶은 다음, 명시적인 디렉터리를 지정한 출력 경로로 묶습니다. + +```sh +wvb pack # 설정 기본값을 사용합니다 +wvb pack ./dist +wvb pack ./dist --outfile ./build/app.wvb +``` + +반복 가능한 `--ignore` glob으로 파일을 제외합니다. + +```sh +wvb pack ./dist --ignore '*.map' --ignore 'node_modules/**' +``` + +`--header`로 일치하는 파일에 응답 헤더를 붙입니다. 이 플래그는 사용할 때마다 값 세 개를 받습니다. + +```sh +wvb pack ./dist --header '*.html' 'cache-control' 'max-age=3600' +``` + +아카이브를 쓰지 않고 무엇이 묶일지만 보고하는 드라이 런을 실행합니다. + +```sh +wvb pack ./dist --no-write +``` + +## Options + +| Option | Aliases | Default | Description | +| ------------- | ------------------ | ------------------------- | ------------------------------------------------------------------------------------ | +| `SRC_DIR` | — | `pack.srcDir` ?? `./dist` | 묶을 소스 디렉터리입니다. | +| `--outfile` | `--out-file`, `-O` | `.wvb/` | 출력 경로입니다. `.wvb`가 없으면 자동으로 붙습니다. | +| `--ignore` | — | — | 제외할 파일의 glob입니다. 반복 가능합니다. | +| `--header` | `-H` | — | 일치하는 파일에 헤더를 설정합니다: `--header `. 반복 가능합니다. | +| `--no-write` | — | 기본적으로 씀 | 아카이브를 쓰지 않고 패킹을 실행합니다(드라이 런). | +| `--overwrite` | — | `true` | 기존 출력 파일을 덮어씁니다. | + +이 플래그들은 명령어마다 공통으로 쓰이는 `--config`(`-C`)와 `--cwd` 옵션도 받습니다. 설정 탐색과 전역 플래그의 동작 방식은 [CLI 개요](/docs/guide/cli)를 참고하세요. + + +`pack`에는 `--outdir` 플래그가 없습니다. 출력 디렉터리는 `--outfile`로 결정됩니다. 기본값은 `.wvb/`으로 해석되며, 이때 이름은 가장 가까운 `package.json`에서 스코프를 제거한 값입니다. + + +불리언 플래그는 `--flag`, `--flag=true|false`, 그리고 `--no-flag` 형태의 부정을 받습니다. 예를 들어 `--no-write`는 기본 동작인 `--write`를 끄고, `--no-overwrite`는 기존 출력 파일을 그대로 둡니다. + +## Configuration + +[`wvb.config`](/docs/config) 파일에 `pack` 기본값을 설정하면 명령어를 인자 없이 깔끔하게 실행할 수 있습니다. 출력 경로에 해당하는 설정 필드는 `outFile`이며(단일 경로이고, `.wvb`가 자동으로 붙습니다), `overwrite`의 기본값은 `true`입니다. 명령줄에서 전달한 플래그는 항상 설정값보다 우선합니다. + +## See also + + + + + + + diff --git a/content/docs/guide/commands/remote.ko.mdx b/content/docs/guide/commands/remote.ko.mdx new file mode 100644 index 0000000..ec22842 --- /dev/null +++ b/content/docs/guide/commands/remote.ko.mdx @@ -0,0 +1,100 @@ +--- +title: wvb remote +description: 원격을 검사하고 테스트합니다. 현재 배포된 번들을 보여주고, 사용 가능한 번들을 나열하며, 로컬 업데이트 서버를 실행합니다. +--- + +`wvb remote` 명령어 그룹은 원격 업데이트 서버를 검사하고 테스트합니다. 현재 어떤 버전이 배포되어 있는지 확인하고, 원격이 알고 있는 모든 번들을 나열하며, 프로덕션 HTTP 규약을 그대로 재현하는 로컬 서버를 실행할 때 사용하세요. 이렇게 하면 전체 업데이트 루프를 오프라인에서 실행해 볼 수 있습니다. + +세 개의 하위 명령어로 구성됩니다. + +- [`wvb remote current`](#wvb-remote-current-bundle) — 현재 배포된 버전과 그 메타데이터를 보여줍니다. +- [`wvb remote list`](#wvb-remote-list) — 원격에서 사용 가능한 모든 번들을 나열합니다(별칭 `wvb remote ls`). +- [`wvb remote local`](#wvb-remote-local) — 디렉터리를 기반으로 로컬 업데이트 서버를 시작합니다. + +앞의 두 명령어는 기본적으로 [설정](/docs/config/remote)에서 `remote.endpoint`를 읽습니다. 따라서 원격을 한 번 설정해 두면 `--endpoint` 플래그를 생략할 수 있습니다. 배포 모델과 로컬 테스트 과정은 [원격 구축하기](/docs/guide/remote)를 참고하세요. + +## wvb remote current [BUNDLE] + +번들을 직접 다운로드하지 않고, 현재 배포된 버전과 그 메타데이터를 보여줍니다. 출력에는 원격이 보고한 버전, ETag, 무결성, 서명, 마지막 수정 시각 값이 포함됩니다. + +```sh +wvb remote current app --endpoint https://updates.example.com +wvb remote current app --channel beta +wvb remote current # 설정 기본값 사용 +``` + +| Option | Aliases | Default | Description | +| ------------ | ------- | --------------------- | ---------------------------------------------------- | +| `BUNDLE` | — | resolved from config | 번들 이름. 지정하지 않으면 패키지 이름을 사용합니다. | +| `--endpoint` | `-E` | `remote.endpoint` | 조회할 원격 엔드포인트. | +| `--channel` | — | — | 릴리스 채널. | +| `--config` | `-C` | config auto-discovery | 설정 파일 경로. | +| `--cwd` | — | `process.cwd()` | 경로를 확인할 작업 디렉터리. | + +## wvb remote list + +원격에서 사용 가능한 모든 번들을 나열합니다. 이 명령어에는 `wvb remote ls`라는 별칭도 있습니다. 출력이 JSON이므로 다른 도구로 그대로 파이프할 수 있습니다. + +```sh +wvb remote list --endpoint https://updates.example.com +wvb remote ls --channel beta +wvb remote list | jq '.[].name' +``` + +| Option | Aliases | Default | Description | +| ------------ | ------- | --------------------- | ---------------------------- | +| `--endpoint` | `-E` | `remote.endpoint` | 조회할 원격 엔드포인트. | +| `--channel` | — | — | 릴리스 채널. | +| `--config` | `-C` | config auto-discovery | 설정 파일 경로. | +| `--cwd` | — | `process.cwd()` | 경로를 확인할 작업 디렉터리. | + +## wvb remote local + +디렉터리를 기반으로 로컬 업데이트 서버를 시작합니다. 프로덕션 원격과 동일한 HTTP 규약을 구현하므로, 호스팅된 [프로바이더](/docs/guide/providers/local)를 연결하기 전에 전체 업데이트 루프를 오프라인에서 테스트할 수 있습니다. 서버는 기본적으로 `~/.wvb/local`을 `4313` 포트로 제공합니다. + +```sh +wvb remote local # http://localhost:4313, ~/.wvb/local 제공 +wvb remote local --base-dir ./.wvb/local --port 4313 +wvb remote local --allow-other-versions --hostname 0.0.0.0 +``` + +| Option | Aliases | Default | Env | Description | +| ------------------------ | ------- | -------------- | ---------- | ---------------------------------------- | +| `--base-dir` | — | `~/.wvb/local` | — | 제공할 디렉터리. | +| `--allow-other-versions` | — | `false` | — | 현재 버전이 아닌 버전도 제공하도록 허용. | +| `--hostname` | `-H` | `localhost` | `HOSTNAME` | 바인딩할 호스트 이름. | +| `--port` | `-P` | `4313` | `PORT` | 수신 대기할 포트. | +| `--silent` | — | `false` | — | 요청 로깅 비활성화. | + + + `remote local`은 선택적 패키지인 `@wvb/remote-local-provider`가 필요하며, 명령어가 이를 필요할 때 + 임포트합니다. 다른 하위 명령어와 달리 `--config`나 `--cwd`를 읽지 않습니다. 서버는 `SIGINT` 또는 + `SIGTERM`에서 깔끔하게 종료됩니다. + + +로컬 서버는 기본적으로 각 번들에서 현재 버전으로 표시된 버전만 제공합니다. `--allow-other-versions`를 전달하면 특정 이전 버전도 함께 제공합니다. 이는 롤백과 버전 고정을 테스트할 때 유용합니다. + +## 참고 + + + + + + + diff --git a/content/docs/guide/commands/serve.ko.mdx b/content/docs/guide/commands/serve.ko.mdx new file mode 100644 index 0000000..c360315 --- /dev/null +++ b/content/docs/guide/commands/serve.ko.mdx @@ -0,0 +1,65 @@ +--- +title: wvb serve +description: 단일 .wvb 번들의 파일을 HTTP로 제공하여 패킹한 번들을 브라우저에서 미리 봅니다. +--- + +`wvb serve`는 로컬 HTTP 서버를 시작해 하나의 `.wvb` 번들을 풀고 그 파일을 제공합니다. 덕분에 패킹한 번들을 브라우저에서 열어 배포 전에 확인할 수 있습니다. 디렉터리 경로는 `index.html`로 해석되며, 이는 웹뷰가 런타임에 번들을 로드하는 방식과 동일합니다. + +서버는 기본적으로 `http://localhost:4312`에서 요청을 받습니다. `SIGINT`과 `SIGTERM`을 처리해 정상적으로 종료하므로, `Ctrl+C`로 깔끔하게 중지됩니다. + + + `wvb serve`는 단일 번들의 내용을 미리 보여줍니다. HTTP 원격을 상대로 전체 무선(OTA) 업데이트 + 루프를 테스트하려면, [`wvb remote local`](/docs/guide/commands/remote)로 로컬 업데이트 서버를 + 실행하세요. 전체 과정을 처음부터 끝까지 따라가려면 [원격 서버 구축하기](/docs/guide/remote)를 + 참고하세요. + + +## Usage + +```sh +wvb serve # 설정에서 해석한 번들을 제공 +wvb serve ./build/app.wvb # http://localhost:4312 +wvb serve ./build/app.wvb --port 8080 --hostname 0.0.0.0 +wvb serve ./build/app.wvb --silent # 요청 로깅 비활성화 +``` + +`FILE`을 생략하면 `wvb serve`는 [설정](/docs/config)의 `serve.file`을 사용하고, 그다음에는 해석된 패킹 출력 경로를 사용합니다. `wvb pack`을 실행한 일반적인 프로젝트라면 `wvb serve`만으로 미리 볼 수 있습니다. + +## Options + +| Option | Aliases | Default | Description | +| ------------ | ------- | --------------- | --------------------------------------------------------------------------------- | +| `FILE` | — | 설정에서 | 제공할 번들입니다. `serve.file`을 사용하고, 그다음 해석된 패킹 출력을 사용합니다. | +| `--hostname` | `-H` | `localhost` | 바인딩할 호스트명입니다. `HOSTNAME` 환경변수를 읽습니다. | +| `--port` | `-P` | `4312` | 요청을 받을 포트입니다. `PORT` 환경변수를 읽습니다. 1에서 65535 사이여야 합니다. | +| `--silent` | — | `false` | 요청 로깅 미들웨어를 비활성화합니다. | +| `--config` | `-C` | — | 설정 파일 경로입니다. | +| `--cwd` | — | `process.cwd()` | 경로 해석에 사용할 작업 디렉터리입니다. | + +불리언 플래그는 `--silent`, `--silent=true|false`, 그리고 부정형 `--no-silent`을 받습니다. 전역 플래그인 `--color`, `--log-level`, `--log-verbose`도 여기에 적용됩니다. [CLI 개요](/docs/guide/cli)를 참고하세요. + +## Notes + +- 서버는 디렉터리 요청을 `index.html`로 해석합니다. 따라서 디렉터리에 매핑되는 클라이언트 측 라우트는 번들의 엔트리 문서를 로드합니다. +- `--hostname 0.0.0.0`은 모든 인터페이스에 바인딩합니다. 네트워크상의 다른 기기에서 번들을 미리 볼 때 유용합니다. +- JavaScript에서 동일한 로직을 호출하려면 [프로그래매틱 API](/docs/guide/cli-programmatic)의 `serve` 함수를 사용하세요. + +## See also + + + + + + diff --git a/content/docs/guide/commands/upload.ko.mdx b/content/docs/guide/commands/upload.ko.mdx new file mode 100644 index 0000000..e136d9e --- /dev/null +++ b/content/docs/guide/commands/upload.ko.mdx @@ -0,0 +1,75 @@ +--- +title: wvb upload +description: 번들을 패킹하고 해시와 서명을 붙여 설정된 원격 서버에 업로드하며, 선택적으로 배포 단계까지 실행합니다. +--- + +`wvb upload`는 번들을 원격 서버에 게시합니다. 기본적으로 전체 파이프라인을 순서대로 실행합니다. 먼저 패킹하고, 무결성 해시를 계산하고, 서명한 뒤 업로드합니다. 따라서 명령어 하나로 빌드 산출물이 서명과 무결성 검증을 거친 번들이 되어 원격에 올라갑니다. 업로드 단계에는 [설정](/docs/config/remote)의 `remote.uploader`가 필요합니다. 마지막의 선택적 배포 단계에는 `remote.deployer`도 필요합니다. + +```sh +wvb upload # 설정 기본값을 사용합니다 +wvb upload app --version 1.2.0 +wvb upload app --version 1.2.0 --deploy --channel beta +wvb upload --no-pack --file ./build/app.wvb --force +``` + +## 옵션 + +| 옵션 | 별칭 | 기본값 | 설명 | +| ------------------ | ---- | ------------------------------- | ---------------------------------------------------------------------------- | +| `BUNDLE` | — | 설정값 / `--file` 이름 | 번들 이름. 위치 인수입니다. | +| `--version` | `-V` | 설정값 또는 `package.json` 버전 | 게시할 버전. | +| `--file` | `-F` | 확인된 출력 경로 | 업로드할 `.wvb` 파일 경로. | +| `--force` | — | `false` | 원격에 해당 버전이 이미 있으면 덮어씁니다. | +| `--deploy` | — | `false` | 업로드 후 해당 버전을 배포합니다. | +| `--channel` | — | — | 배포할 채널. `--deploy`와 함께 사용합니다. | +| `--pack` | `-P` | `true` | 업로드 전에 `pack.srcDir`을 패킹합니다. 건너뛰려면 `--no-pack`을 전달하세요. | +| `--skip-integrity` | — | `false` | 무결성 해시 계산을 건너뜁니다. | +| `--skip-signature` | — | `false` | 번들 서명을 건너뜁니다. | +| `--config` | `-C` | 설정 자동 탐색 | 설정 파일 경로. | +| `--cwd` | — | `process.cwd()` | 경로를 확인할 작업 디렉터리. | + + + 버전은 위치 인수가 아니라 `--version`(`-V`) 플래그로 지정합니다. 첫 번째 위치 인수는 번들 + 이름입니다. `--deploy`의 기본값은 `false`이므로, 업로드만 하면 버전이 게시되기는 하지만 현재 + 버전으로 설정되지는 않습니다. 이 버전을 배포하기 전까지 클라이언트는 이전에 배포된 버전을 계속 + 받습니다. + + +## 파이프라인 + +`wvb upload`는 다음 단계를 순서대로 실행합니다. + +1. **패킹** — `pack.srcDir`을 `.wvb` 아카이브로 패킹합니다. `--no-pack`으로 건너뛰고 기존 번들을 `--file`로 전달하세요. +2. **무결성** — 무결성 해시를 계산합니다. `--skip-integrity`로 건너뜁니다. +3. **서명** — 번들에 서명합니다. `--skip-signature`로 건너뜁니다. +4. **업로드** — `remote.uploader`를 통해 번들을 원격으로 전송합니다. +5. **배포**(선택) — `--deploy`를 전달할 때만 실행되며, `remote.deployer`를 통해 업로드된 버전을 현재 버전으로 표시합니다. + +성공하면 명령어가 번들 엔드포인트를 출력합니다. + +## 요구 사항 + +- 업로드 단계에는 `remote.uploader`가 설정되어 있어야 합니다. [원격, 무결성 및 서명 설정](/docs/config/remote)을 참고하세요. +- `--deploy`를 전달하면 `remote.deployer`도 추가로 필요합니다. 버전을 나중에 별도로 배포하려면 [`wvb deploy`](/docs/guide/commands/deploy)를 사용하세요. + +게시 모델과 로컬 테스트 과정은 [원격 서버 구축하기](/docs/guide/remote)를 참고하세요. + +## 함께 보기 + + + + + + diff --git a/content/docs/guide/index.ko.mdx b/content/docs/guide/index.ko.mdx new file mode 100644 index 0000000..cb12f70 --- /dev/null +++ b/content/docs/guide/index.ko.mdx @@ -0,0 +1,204 @@ +--- +title: 소개 +description: 웹뷰 기반 프레임워크와 플랫폼을 위한 오프라인 우선 웹 애플리케이션 배포 시스템입니다. +--- + +Webview Bundle(`wvb`)은 빌드한 웹 자산(HTML/JS/CSS/미디어)을 압축되고 무결성이 검증된 단일 +아카이브(`.wvb`)로 묶습니다. 앱은 이 아카이브를 함께 배포하고, 커스텀 URL 스킴을 통해 웹뷰에 +제공합니다. 따라서 첫 화면은 네트워크를 기다리지 않습니다. 원격 서버를 설정하면 앱이 더 새로운 +번들을 무선(OTA, over-the-air)으로 내려받아, 네이티브 앱스토어 배포 없이 갱신된 코드를 +전달합니다. 하나의 `.wvb` 포맷이 공유 Rust 코어를 통해 모든 웹뷰 플랫폼에서 동작합니다. + +![빌드 산출물을 .wvb 아카이브로 묶어 웹뷰에 제공하고, 원격 서버에서 무선으로 갱신합니다.](https://static.wvb.dev/diagrams/pack-serve-update.svg) + +## 빠른 시작 + +CLI를 설치하고, 빌드 산출물을 묶은 뒤, 로컬에서 미리 봅니다. + + + + +```sh +npm install --save-dev @wvb/cli +``` + + + + +```sh +pnpm add -D @wvb/cli +``` + + + + +```sh +yarn add -D @wvb/cli +``` + + + + +```sh +# ./dist를 .wvb/으로 묶습니다 (--outfile,-O로 변경). +npx wvb pack + +# http://localhost:4312에서 로컬 미리보기용으로 번들을 제공합니다. +npx wvb serve +``` + +`wvb.config.ts`에서 `defineConfig`로 빌드를 설정하세요. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig({ + pack: { + // `.wvb`가 자동으로 붙습니다 -> .wvb/app.wvb + outFile: '.wvb/app', + }, + serve: { + port: 4312, + }, +}); +``` + +모든 명령어와 필드는 [CLI 레퍼런스](/docs/guide/cli)와 [`wvb.config` 레퍼런스](/docs/config)를 참고하세요. + +## Webview Bundle을 쓰는 이유 + +- **오프라인 우선.** 리소스가 로컬에 번들로 포함되므로 첫 화면은 네트워크를 기다리지 않습니다. +- **무선 업데이트.** 새 번들 버전을 배포하면 수정 사항이나 기능을 전달합니다. 네이티브 배포가 + 필요 없습니다. +- **무결성과 진위성.** 모든 번들은 내부 체크섬을 담고 있습니다. 다운로드는 SHA-2 무결성 + 해시(`sha256`, `sha384`, `sha512`)와 디지털 서명(ECDSA, Ed25519, RSA)으로 검증할 수 있습니다. +- **하나의 포맷, 모든 플랫폼.** 동일한 `.wvb` 아카이브가 공유 Rust 코어를 통해 Electron, Tauri, + Android, iOS, Deno Desktop에서 동작합니다. + +설정에서 원격의 무결성 및 서명 검증을 활성화하세요. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig({ + remote: { + integrity: { algorithm: 'sha256' }, + signature: { + algorithm: 'ed25519', + keyFormat: 'pkcs8', + // privateKey: ... + }, + }, +}); +``` + +전체 스키마는 [원격, 무결성 및 서명 설정](/docs/config/remote)을 참고하세요. + +## 여기서 시작하세요 + + + + + + + + + + + +## 기능 + + + + + + + + + +## 패키지 + +코어는 Rust 크레이트입니다. 각 플랫폼은 얇은 통합 패키지를 통해 코어를 사용합니다. + +| Package | Version | What it is | +| ---------------------------- | ------- | ----------------------------------------------------------------------------------------- | +| [`wvb`](https://docs.rs/wvb) | 0.2.0 | Rust 코어: 번들 포맷, 소스, 원격, 업데이터, 프로토콜, 무결성, 서명 | +| `@wvb/cli` | 0.1.0 | 커맨드라인 도구(바이너리 `wvb`, `webview-bundle`): pack, serve, upload, deploy, 로컬 원격 | +| `@wvb/config` | 0.1.0 | `wvb.config.ts`를 위한 `defineConfig` | +| `@wvb/node` | 0.1.0 | Node.js용 코어 N-API 바인딩 | +| `@wvb/bridge` | 0.1.0 | 웹 앱이 네이티브 호스트와 통신하기 위한 브리지 | +| `@wvb/electron` | 0.1.0 | Electron 통합(프로토콜, IPC, 업데이터) | +| `@wvb/electron-builder` | Stable | electron-builder 통합 | +| `@wvb/electron-forge` | Stable | Electron Forge 플러그인 | +| `wvb-tauri` | 0.1.0 | Tauri 통합, crates.io에 Rust 크레이트로 배포됨 | +| `@wvb/remote-aws` | 0.1.0 | AWS용 원격 설정 및 프로바이더 | +| `@wvb/remote-cloudflare` | 0.1.0 | Cloudflare용 원격 설정 및 프로바이더 | +| `@wvb/remote-local` | 0.0.0 | 로컬 시뮬레이션을 위한 원격 설정 | + +CLI와 함께 플랫폼 통합을 설치하세요. + +```sh +# Electron +npm install @wvb/electron + +# Tauri (Rust 크레이트) +cargo add wvb-tauri +``` + + + Tauri 통합은 crates.io의 **`wvb-tauri` 크레이트**입니다. `@wvb/tauri` npm 패키지는 없습니다. 전체 + 상태는 [플랫폼 지원](/docs/guide/platform-support)을 참고하세요. + + +## 다음 단계 + +아카이브 안에 무엇이 담겨 배포되는지 이해하려면 [`.wvb` 번들 포맷](/docs/guide/bundle-format)을 +읽으세요. 전체 API는 [Rust 크레이트 문서](https://docs.rs/wvb)를 살펴보세요. diff --git a/content/docs/guide/meta.ko.json b/content/docs/guide/meta.ko.json new file mode 100644 index 0000000..c5a6c51 --- /dev/null +++ b/content/docs/guide/meta.ko.json @@ -0,0 +1,30 @@ +{ + "root": true, + "title": "가이드", + "pages": [ + "---소개---", + "index", + "platform-support", + "why-webview-bundle", + "---기능---", + "bundle-format", + "bundle-sources", + "protocol-handling", + "remote-bundles", + "platform-integration", + "---플랫폼---", + "platforms/electron", + "platforms/tauri", + "platforms/android", + "platforms/ios", + "platforms/deno", + "---CLI---", + "cli", + "commands", + "cli-programmatic", + "---원격---", + "remote", + "providers", + "spec" + ] +} diff --git a/content/docs/guide/platform-integration.ko.mdx b/content/docs/guide/platform-integration.ko.mdx new file mode 100644 index 0000000..498352b --- /dev/null +++ b/content/docs/guide/platform-integration.ko.mdx @@ -0,0 +1,296 @@ +--- +title: 플랫폼 통합 +description: 하나의 Rust 코어가 얇은 바인딩과 @wvb/bridge 웹 레이어를 통해 Electron, Tauri, Android, iOS, Deno에 도달하는 방법. +--- + +Webview Bundle은 다섯 개가 아니라 하나의 시스템입니다. 포맷, 번들 소스, 프로토콜 핸들러, 원격 클라이언트, 업데이터, 무결성 및 서명 검사가 모두 하나의 Rust 크레이트 안에 있습니다. 모든 플랫폼은 얇은 바인딩을 통해 바로 그 크레이트를 사용합니다. 그래서 Electron용으로 빌드한 `.wvb`는 Tauri, Android, iOS, Deno에서도 똑같이 동작합니다. 개별 플랫폼 가이드를 읽기 전에 이 문서를 먼저 읽으세요. 각 가이드는 어떤 패키지가 어디에 속하는지 이미 안다고 가정합니다. + +## 하나의 코어, 여러 호스트 + +Rust 크레이트 `wvb`([crates.io](https://crates.io/crates/wvb), 버전 0.2.0)가 단일 진실 공급원입니다. 이 크레이트는 다음을 구현합니다. + +- `.wvb` 번들 포맷(헤더, 인덱스, LZ4로 압축된 데이터), +- 번들 소스(builtin 및 remote 디렉터리), +- 번들 프로토콜 핸들러와 로컬 프로토콜 핸들러, +- 원격 HTTP 클라이언트, +- 업데이터(확인, 다운로드, 검증, 설치), +- 무결성(SHA-2) 및 서명 검증. + +각 바인딩은 의도적으로 얇습니다. 코어를 호스트 언어에 노출하고, 해당 호스트에 URL 스킴을 등록하며, 요청을 코어로 전달합니다. 코어를 수정하거나 기능을 추가하면 다음 바인딩 릴리스에서 모든 플랫폼에 반영됩니다. 전체 API는 [docs.rs/wvb](https://docs.rs/wvb)에서 확인하세요. + + + Rust 코어는 URL 스킴을 등록하거나 검증하지 않습니다. `app://`이나 `bundle://` 같은 스킴을 선택하고 + OS에 등록하는 일은 바인딩의 몫입니다. 요청이 파일로 매핑되는 방식은 [프로토콜 + 처리](/docs/guide/protocol-handling)를 참고하세요. + + +## 전달 방식 + +각 플랫폼은 서로 다른 배포 채널을 통해 코어에 도달합니다. + +| 플랫폼 | 바인딩 | 배포 방식 | +| ------------------------ | ------------------------- | --------------------------------------------------------------------- | +| Electron / Node.js | `@wvb/node` | N-API 네이티브 애드온(NAPI-RS), npm에 플랫폼별 프리빌드 바이너리 제공 | +| Tauri(데스크톱 + 모바일) | `wvb-tauri` Rust 크레이트 | crates.io의 Tauri v2 플러그인, 버전 0.1.0 | +| Android | `packages/ffi`(UniFFI) | Kotlin 바인딩, `webview-bundle-android` 저장소에서 사용 | +| iOS | `packages/ffi`(UniFFI) | Swift 바인딩, `webview-bundle-ios` 저장소에서 사용 | +| Deno Desktop | `@wvb/deno` | 프리빌드 dylib 위의 Deno FFI(실험적) | + +### Electron과 Node.js + +`@wvb/node`(0.1.0)는 코어를 프리빌드 바이너리가 포함된 N-API 네이티브 애드온으로 감쌉니다. 여느 npm 의존성처럼 설치하세요. + +```sh +npm install @wvb/node +``` + +```ts title="main.ts" +import { readBundle, BundleSource } from '@wvb/node'; + +const source = new BundleSource({ + builtinDir: './bundles', + remoteDir: './remote', +}); +const bundle = await readBundle('./app.wvb'); +``` + +Electron에서는 `@wvb/electron`(0.1.0)을 사용하세요. 이 패키지는 `@wvb/node` 위에 만들어졌으며, 스킴 권한, 프로토콜 등록, IPC 채널을 대신 연결해 줍니다. + +```sh +npm install @wvb/electron +``` + +```js title="main.cjs" +const { app, BrowserWindow } = require('electron'); +const { bundleProtocol, wvb } = require('@wvb/electron'); + +const instance = wvb({ + source: { builtinDir: path.join(__dirname, 'bundles') }, + protocols: [bundleProtocol('app', { onError: e => console.error('[wvb]', e) })], +}); + +app.whenReady().then(async () => { + await instance.whenProtocolRegistered(); + const window = new BrowserWindow({ + webPreferences: { contextIsolation: true, nodeIntegration: false }, + }); + await window.loadURL('app://hacker-news.wvb'); +}); +``` + +electron-builder와 Electron Forge용 패키징 헬퍼인 `@wvb/electron-builder`와 `@wvb/electron-forge`는 번들 패킹을 빌드에 연결합니다. [Electron 가이드](/docs/guide/platforms/electron)와 [Node API 레퍼런스](/docs/references/node)를 참고하세요. + +### Tauri + +Tauri 통합은 Rust 크레이트 `wvb-tauri`(crates.io의 0.1.0)입니다. `@wvb/tauri` npm 패키지는 없습니다. 크레이트를 추가하고 Tauri v2 플러그인으로 등록하세요. + +```toml title="src-tauri/Cargo.toml" +[dependencies] +wvb-tauri = "0.1.0" +``` + +```rust title="src-tauri/src/lib.rs" +use wvb_tauri::{Config, Protocol, Source}; + +tauri::Builder::default() + .plugin(wvb_tauri::init( + Config::new() + .source(Source::new().builtin_dir("bundles")) + .protocol(Protocol::bundle("bundle")), + )) + .run(tauri::generate_context!()) + .unwrap(); +``` + +플러그인 명령어를 호출하는 프런트엔드는 capability 파일에 설정된 ACL 권한을 부여해야 합니다. + +```json title="src-tauri/capabilities/default.json" +{ + "permissions": ["core:default", "wvb-tauri:default"] +} +``` + +같은 크레이트가 데스크톱과 모바일을 모두 처리합니다. Android에서는 builtin 번들이 APK 안에 `asset://` 리소스로 포함됩니다. 그래서 builtin 번들을 함께 배포하는 앱은 `tauri_plugin_fs`도 등록해야 합니다. + +```rust +tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) // Android에서 builtin 번들에 필요함 + .plugin(wvb_tauri::init(config)) +``` + +[Tauri 가이드](/docs/guide/platforms/tauri)를 참고하세요. + +### Android와 iOS + +Android와 iOS는 하나의 바인딩 생성기 `packages/ffi`를 공유합니다. 이는 [UniFFI](https://mozilla.github.io/uniffi-rs/)를 사용해 코어로부터 Kotlin과 Swift 바인딩을 생성하는 Rust 크레이트입니다. 빌드하면 세 개의 GitHub 릴리스 자산이 만들어집니다. + +- `android.zip` — Android용 Kotlin 바인딩과 `jniLibs`, +- `apple.zip` — Apple 플랫폼용 Swift 바인딩과 정적 라이브러리, +- `WebViewBundleFFI.xcframework.zip` — iOS용 xcframework. + +두 개의 전용 저장소가 이 자산들을 사용하며, 릴리스 태그 `ffi/`으로 해석합니다. + +```kotlin title="Android — dev.wvb 바인딩" +import dev.wvb.BundleSource +import dev.wvb.BundleSourceConfig + +val source = BundleSource( + BundleSourceConfig( + builtinDir = "$filesDir/bundles", + remoteDir = "$cacheDir/remote", + builtinManifestFilepath = null, + remoteManifestFilepath = null, + ), +) +val bundle = source.fetchBundle("app") +``` + +```swift title="iOS — WebViewBundleLibrary 바인딩" +import WebViewBundleLibrary + +let source = BundleSource( + config: BundleSourceConfig( + builtinDir: "\(docs)/bundles", + remoteDir: "\(caches)/remote", + builtinManifestFilepath: nil, + remoteManifestFilepath: nil + ) +) +let bundle = try await source.fetchBundle(bundleName: "app") +``` + +```swift title="Package.swift" +.binaryTarget( + name: "WebViewBundleFFI", + url: "https://github.com/webview-bundle/webview-bundle/releases/download/ffi/0.1.0/WebViewBundleFFI.xcframework.zip", + checksum: "" +) +``` + +### Deno + +`@wvb/deno`는 프리빌드 동적 라이브러리 위의 Deno FFI를 통해 코어에 도달합니다. GitHub Releases에서 dylib를 설치한 다음 로드하세요. + +```sh +deno run -A jsr:@wvb/deno/install --out vendor/wvb +``` + +이 통합은 실험적입니다. [Deno 가이드](/docs/guide/platforms/deno)와 [Deno API 레퍼런스](/docs/references/deno)를 참고하세요. + +## 브리지 + +위의 바인딩들은 네이티브 쪽에서 실행됩니다. 브리지 `@wvb/bridge`(0.1.0)는 웹 쪽입니다. 웹뷰 안의 JavaScript가 호스트와 상관없이 하나의 통일된 API로 네이티브 소스, 원격, 업데이터 작업을 호출하게 해 줍니다. + +```sh +npm install @wvb/bridge +``` + +단일 진입점은 `invoke()`입니다. + +```ts +import { invoke } from '@wvb/bridge'; + +const update = await invoke('updaterGetUpdate', { bundleName: 'app' }); +``` + +`invoke()`를 이름으로 직접 호출하는 경우는 드뭅니다. 브리지는 네이티브 명령어를 세 개의 타입이 지정된 객체 `source.*`, `remote.*`, `updater.*`로 묶으며, 각 메서드는 타입이 지정된 `invoke()` 호출입니다. + +```ts +import { source, remote, updater } from '@wvb/bridge'; + +const bundles = await source.listBundles(); +const info = await updater.getUpdate('app'); +if (info.isAvailable) { + await updater.download('app'); + await updater.install('app', info.version); +} +``` + +- `source.*` — 번들 목록 조회, 파일 경로 해석, 메타데이터 읽기, 다운로드한 버전 관리. +- `remote.*` — 원격 서버에서 번들 목록 조회, 검사, 다운로드. +- `updater.*` — 업데이트 확인, 다운로드, 설치. + +브리지는 런타임에 호스트를 감지하고 각 호출을 알맞은 전송 방식으로 라우팅합니다. + +| 플랫폼 | 전송 방식 | +| -------- | ------------------------------------------------------- | +| Electron | `window.wvbElectron.invoke(name, params)` | +| Tauri | Tauri `invoke('plugin:wvb-tauri\|', params)` | +| Android | `window.wvbAndroid.postMessage(...)` | +| iOS | `window.webkit.messageHandlers.wvbIos.postMessage(...)` | + +어디서나 동일한 `source`, `remote`, `updater` 메서드를 사용합니다. `@wvb/bridge` 클라이언트는 electron, tauri, android, ios를 지원합니다. Deno Desktop은 `@wvb/deno-desktop`을 통해 자체 바인딩을 제공합니다. + +`@wvb/bridge/testing`을 임포트하면 네이티브 호스트 없이 웹뷰 코드를 테스트할 수 있습니다. + +```ts title="app.test.ts" +import { mockBridge } from '@wvb/bridge/testing'; +import { updater } from '@wvb/bridge'; + +using bridge = mockBridge({ platform: 'electron' }); +bridge.mockInvoke('updater.getUpdate', () => ({ + name: 'app', + version: '1.2.0', + isAvailable: true, +})); + +const info = await updater.getUpdate('app'); +``` + + + `@wvb/bridge/testing`은 `mockInvoke`, `mockPlatform`, `mockBridge`를 제공하므로, 단위 테스트에서 + 명령어 응답을 스텁으로 대체할 수 있습니다. + + +## 아키텍처 한눈에 보기 + +웹 앱에서 온 요청은 브리지를 지나 네이티브 호스트로 내려가고, 코어로 들어간 다음, 번들 소스나 원격 서버로 나갑니다. + +![웹 앱이 @wvb/bridge를 통해 네이티브 호스트를 호출하고, 호스트는 wvb Rust 코어를 임베드하며, 코어는 소스와 원격 서버에서 읽어 온다.](https://static.wvb.dev/diagrams/architecture.svg) + +웹뷰도 같은 코어를 통해 자산을 로드합니다. 번들 프로토콜이 소스에서 바로 파일을 제공하므로, 페이지 바이트와 `invoke()` 명령어가 하나의 번들 소스를 공유합니다. + +## 다음으로 볼 곳 + + + + + + + + + + + diff --git a/content/docs/guide/platform-support.ko.mdx b/content/docs/guide/platform-support.ko.mdx new file mode 100644 index 0000000..83d868f --- /dev/null +++ b/content/docs/guide/platform-support.ko.mdx @@ -0,0 +1,155 @@ +--- +title: 플랫폼 지원 +description: Webview Bundle을 실행하는 플랫폼과 각 플랫폼에서 설치할 패키지를 정리합니다. +--- + +하나의 공유 Rust 코어가 모든 통합을 구동합니다. 따라서 한 번 패킹한 `.wvb`는 Electron, Tauri, Android, iOS, Deno Desktop에서 동일하게 로드됩니다. 코어가 각 호스트에 도달하는 방식과 웹뷰가 브리지를 통해 응답하는 방식은 [플랫폼 통합](/docs/guide/platform-integration)을 참고하세요. + +이 페이지는 지원 매트릭스입니다. 플랫폼별 패키지 또는 크레이트, 최소 호스트 버전, 상태를 정리합니다. + +## 지원 매트릭스 + +| 플랫폼 | 웹뷰 호스트 | 패키지 / 크레이트 | 최소 버전 | 상태 | +| -------------------------------------------- | -------------- | --------------------------------- | -------------------------------------------------------------------------------- | ------ | +| [Electron](/docs/guide/platforms/electron) | Chromium | `@wvb/electron` 0.1.0 (npm) | `electron >= 15` | 안정 | +| [Tauri desktop](/docs/guide/platforms/tauri) | System WebView | `wvb-tauri` 0.1.0 (crate) | Tauri v2 | 안정 | +| Tauri mobile | System WebView | `wvb-tauri` 0.1.0 (crate) | [Android](/docs/guide/platforms/android) / [iOS](/docs/guide/platforms/ios) 참고 | 안정 | +| [Android](/docs/guide/platforms/android) | System WebView | `webview-bundle-android` (Kotlin) | minSdk 24 / Android 7.0 | 안정 | +| [iOS](/docs/guide/platforms/ios) | WKWebView | `webview-bundle-ios` (Swift) | iOS 16 / macOS 12 | 안정 | +| [Deno Desktop](/docs/guide/platforms/deno) | Deno webview | `@wvb/deno-desktop` (JSR) | — | 실험적 | + +## 데스크톱 + +Electron과 Tauri 데스크톱은 가장 성숙한 통합입니다. 둘 다 배포되어 있으며, Rust 코어를 기반으로 호출자가 제공한 URL 스킴을 통해 번들을 제공합니다. + +### Electron + +npm 패키지를 설치하세요. + +```sh +npm install @wvb/electron +``` + +프로토콜을 등록하고 이름으로 번들을 로드하세요(`://.wvb`). + +```ts title="main.ts" +import { app, BrowserWindow } from 'electron'; +import { bundleProtocol, wvb } from '@wvb/electron'; + +const instance = wvb({ + protocols: [bundleProtocol('app', { onError: e => console.error('[wvb]', e) })], +}); + +app.whenReady().then(async () => { + await instance.whenProtocolRegistered(); + const window = new BrowserWindow({ + webPreferences: { contextIsolation: true, nodeIntegration: false }, + }); + await window.loadURL('app://hacker-news.wvb'); +}); +``` + +[Electron 가이드](/docs/guide/platforms/electron)에서 시작하세요. + +### Tauri + +Tauri 통합은 **Tauri v2**를 대상으로 하는 crates.io의 `wvb-tauri` Rust 크레이트입니다. `@wvb/tauri` npm 패키지는 없습니다. 동일한 크레이트가 Tauri의 모바일 타깃도 구동합니다. + +```toml title="src-tauri/Cargo.toml" +[dependencies] +wvb-tauri = "0.1.0" +``` + +```rust title="src-tauri/src/lib.rs" +use wvb_tauri::{Config, Protocol, Source}; + +tauri::Builder::default() + .plugin(wvb_tauri::init( + Config::new() + .source(Source::new()) + .protocol(Protocol::bundle("bundle")), + )) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +``` + +그러면 윈도우가 `bundle://hacker-news.wvb`를 로드합니다. [Tauri 가이드](/docs/guide/platforms/tauri)에서 시작하세요. + +## 모바일 + +Android와 iOS는 실제로 동작하는 통합입니다. + +### Android + +`webview-bundle-android` Kotlin 라이브러리(네임스페이스 `dev.wvb`)는 **minSdk 24(Android 7.0)**를 요구하며, `WebViewClient`를 통해 번들을 제공합니다. 네이티브 바인딩을 받은 다음 `WebView`에 설치하세요. + +```sh +node scripts/install.mjs +``` + +```kotlin title="MainActivity.kt" +val wvb = WebViewBundle.getInstance( + this, + WebViewBundleConfig(protocols = listOf(WebViewBundleProtocol.bundle())), +) +val webView = WebView(this) +webView.settings.javaScriptEnabled = true +wvb.install(webView) +webView.loadUrl("https://app.wvb/") +``` + +번들은 일반적인 `https://.wvb/...` URL로 제공됩니다(번들 이름이 첫 번째 호스트 레이블입니다). [Android 가이드](/docs/guide/platforms/android)를 참고하세요. + +### iOS + +`webview-bundle-ios` Swift 패키지는 **iOS 16**과 **macOS 12**를 대상으로 하며, `WKURLSchemeHandler`를 통해 번들을 제공합니다. 네이티브 xcframework 릴리스를 설치한 다음 `WKWebViewConfiguration`에 커스텀 스킴을 등록하세요. + +```sh +node scripts/install.mjs ffi/0.1.0 +``` + +```swift title="ContentView.swift" +let instance = try WebViewBundle.configure( + WebViewBundleConfig(protocols: [.bundle(scheme: "app")]) +) +let config = WKWebViewConfiguration() +instance.install(on: config) +let webView = WKWebView(frame: .zero, configuration: config) +webView.load(URLRequest(url: URL(string: "app://app.wvb")!)) +``` + +[iOS 가이드](/docs/guide/platforms/ios)를 참고하세요. + +## Deno Desktop + +Deno Desktop은 가장 최신 통합이며, JSR에 `@wvb/deno-desktop`로 올라와 있습니다. 번들 소스를 빌드하고 `Deno.serve` 호환 핸들러를 노출하며, 윈도우마다 하나의 프로토콜을 사용합니다. + + + Deno Desktop은 실험적입니다. [Deno Desktop 가이드](/docs/guide/platforms/deno)와 [Deno API + 레퍼런스](/docs/references/deno)를 참고하세요. + + +## 다음 단계 + + + + + + + diff --git a/content/docs/guide/platforms/android.ko.mdx b/content/docs/guide/platforms/android.ko.mdx new file mode 100644 index 0000000..40673a9 --- /dev/null +++ b/content/docs/guide/platforms/android.ko.mdx @@ -0,0 +1,337 @@ +--- +title: Android +description: webview-bundle-android Kotlin 라이브러리로 Android WebView 안에서 Webview Bundle을 제공하고 업데이트합니다. +--- + +`webview-bundle-android` 라이브러리는 Webview Bundle Rust 코어를 Android `WebView`에 연결합니다. `WebView`를 넘겨주면 이 라이브러리가 일반적인 `https://.wvb/` URL로 들어오는 요청을 가로챕니다. APK에 함께 배포한 번들의 파일을 제공하고, 앱스토어 출시 없이 원격에서 최신 번들을 무선(OTA)으로 가져옵니다. + +## 요구 사항 + +| 항목 | 값 | +| ----------------------- | ---------------------------------------------------------------- | +| `minSdk` | 24 (Android 7.0) | +| JVM target | 17 (Java와 Kotlin source/target 17) | +| AndroidX | 필수 (`android.useAndroidX=true`, `androidx.webkit`에 필요) | +| System WebView | `WEB_MESSAGE_LISTENER`를 지원해야 함 (최신 WebView / Chrome 88+) | +| 라이브러리 네임스페이스 | `dev.wvb` | + +브리지는 `WebViewCompat.addWebMessageListener`를 통해 연결됩니다. System WebView가 너무 오래되어 `WEB_MESSAGE_LISTENER`를 지원하지 못하는 기기에서는 브리지가 연결되지 않고, 라이브러리는 경고를 기록합니다. 이 경우에도 파일 제공은 정상 동작하며, JavaScript 브리지만 사용할 수 없습니다. + +런타임 의존성은 전이적으로 함께 포함됩니다. JNA(`net.java.dev.jna:jna`, 네이티브 `libwvb_ffi.so`를 로드), `kotlinx-coroutines-core`, `androidx.webkit`가 그 대상입니다. 소비자용 R8/ProGuard 규칙이 라이브러리에 함께 제공되므로, FFI를 위한 keep 규칙을 직접 추가할 필요가 없습니다. + +의존성을 추가합니다. + +```kotlin title="build.gradle.kts" +dependencies { + implementation("dev.wvb:webview-bundle-android:") +} +``` + + + 모든 플랫폼의 지원 현황은 [플랫폼 지원](/docs/guide/platform-support)을 참고하세요. + + +## 빠른 시작 + +`WebViewBundle.getInstance`로 프로세스 전역 `WebViewBundle` 싱글턴을 얻고, 이를 `WebView`에 설치한 다음 번들 URL을 로드합니다. 번들 이름은 호스트의 첫 번째 레이블입니다. 따라서 `https://app.wvb/`는 `app`이라는 이름의 번들을 제공합니다. + +```kotlin title="MainActivity.kt" +import android.os.Bundle +import android.webkit.WebSettings +import android.webkit.WebView +import androidx.appcompat.app.AppCompatActivity +import dev.wvb.WebViewBundle +import dev.wvb.WebViewBundleConfig +import dev.wvb.WebViewBundleProtocol + +class MainActivity : AppCompatActivity() { + private lateinit var webView: WebView + private lateinit var handle: AutoCloseable + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val wvb = WebViewBundle.getInstance( + this, + WebViewBundleConfig( + protocols = listOf(WebViewBundleProtocol.bundle()), + ), + ) + + webView = WebView(this) + webView.settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW + allowFileAccess = false + allowContentAccess = false + } + + handle = wvb.install(webView) { } + webView.loadUrl("https://app.wvb/") + setContentView(webView) + } + + override fun onDestroy() { + handle.close() + webView.destroy() + super.onDestroy() + } +} +``` + +`getInstance`는 프로세스마다 첫 호출에서만 `config`를 반영합니다. 이후 호출은 기존 인스턴스를 반환하고 전달된 config는 무시합니다. `getInstance`에는 짧은 별칭도 있습니다. + +```kotlin +WebViewBundle.getInstance(context, config) // 정식 형태 +WebViewBundle(context, config) // invoke 연산자 +webViewBundle(context, config) // 최상위 함수 +wvb(context, config) // 짧은 별칭 +``` + +매니페스트에는 인터넷 권한이 필요합니다. 평문(cleartext) 개발 서버나 원격을 대상으로 로컬 개발을 할 때는 `usesCleartextTraffic`도 활성화하세요. + +```xml title="AndroidManifest.xml" + + + + + +``` + + + `usesCleartextTraffic="true"`는 로컬 개발 전용입니다. 프로덕션 번들은 앱 내부에서 `https://`로 + 제공되고 원격 엔드포인트는 TLS를 사용해야 하므로, 릴리스 빌드에서는 평문 통신을 비활성화한 채로 + 두세요. + + +## 파일 제공 동작 방식 + +커스텀 URL 스킴은 등록되지 않습니다. `install()`은 `WebViewClient`를 설정하며, 이 클라이언트의 `shouldInterceptRequest`가 일반적인 `http`/`https` 요청을 검사해 일치하는 요청을 번들 소스에서 제공합니다. 각 요청마다 핸들러는 호스트를 소문자로 바꾼 뒤 등록된 프로토콜을 순서대로 살펴봅니다. 호스트를 처음으로 받아들이는 매처가 승리합니다. 번들 프로토콜은 첫 번째 호스트 레이블을 번들 이름으로 삼고, 요청 경로에 해당하는 파일을 반환합니다. 일치하는 것이 없으면 `null`을 반환하고 `WebView`는 네트워크에서 로드합니다. + +핸들러가 예외를 던지면 라이브러리는 `500 text/plain` 응답을 만들고 선택적 `onError` 콜백을 호출합니다. 요청 경로가 `.wvb` 안의 파일로 어떻게 매핑되는지는 [프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. + +`install()`은 메인 프레임에 네이티브 브리지를 `window.wvbAndroid`로 함께 연결합니다. install 옵션을 사용하면 추가 네이티브 핸들러를 등록하거나, 직접 만든 `WebViewClient`를 감싸거나, 브리지를 생략할 수 있습니다. + +```kotlin +val handle = wvb.install(webView) { + delegate = myWebViewClient // 여러분의 콜백이 보존됩니다 + disableBridge = false // window.wvbAndroid를 생략하려면 true로 설정 + bridge = { + handler("greet") { params -> "hello" } + } +} +``` + +브리지 없이 파일 제공 연결부만 원한다면, 저수준 클라이언트를 직접 사용하세요. + +```kotlin +webView.webViewClient = wvb.createWebViewClient(delegate = myWebViewClient) +``` + +## 빌트인 번들 + +앱이 처음 시작할 때 사용하는 번들을 APK의 `assets/bundles/` 아래에 함께 배포하세요. `manifest.json`과 그것이 참조하는 `.wvb` 파일들로 구성됩니다. + +```text +app/src/main/assets/bundles/ +├── manifest.json +└── app/ + └── app_0.1.0.wvb +``` + +네이티브 소스는 에셋 스트림이 아니라 파일을 읽습니다. 그래서 라이브러리는 `assets/bundles/`를 앱의 `filesDir`로 복사합니다. 이 동작은 `SourceOptions`로 제어합니다. + +```kotlin +import dev.wvb.SourceOptions + +WebViewBundleConfig( + protocols = listOf(WebViewBundleProtocol.bundle()), + source = SourceOptions( + builtinAssetsDir = "bundles", // APK의 assets/, null이면 추출 비활성화 + // builtinDir은 기본값이 /wvb/builtin (읽기 전용) + // remoteDir은 기본값이 /wvb/remote (쓰기 가능) + ), +) +``` + +재추출은 APK를 설치하거나 업데이트할 때마다 일어나며 추가 방식으로 동작합니다. 제거된 에셋은 삭제되지 않습니다. `manifest.json`은 번들 엔트리와 현재 버전을 선언하며, `integrity`와 `signature` 값을 담을 수 있습니다. + +```json title="manifest.json" +{ + "manifestVersion": 1, + "bundles": { + "app": { + "currentVersion": "0.1.0", + "versions": { + "0.1.0": { + "integrity": "sha256:n4bQgYhMfWWaL...", + "signature": "..." + } + } + } + } +} +``` + +매니페스트 포맷은 [번들 소스](/docs/guide/bundle-sources)를 참고하세요. + +## 프로토콜 + +프로토콜은 `WebView` 요청을 어떤 호스트에서 제공할지 결정합니다. 프로토콜은 `WebViewBundleConfig.protocols`에 등록하며, 순서대로 평가되어 처음 일치하는 것이 승리합니다. 따라서 `bundle()`은 모든 호스트에 일치하므로 **가장 마지막에 등록하세요**. + + + + +`WebViewBundleProtocol.bundle()`은 모든 호스트에 대해 번들 소스의 엔트리를 제공하며, 첫 번째 호스트 레이블을 번들 이름으로 사용합니다. 특정 이름의 호스트를 대신 네트워크로 보내려면 패스스루 블록을 전달하세요. + +```kotlin +WebViewBundleProtocol.bundle { + passthrough("api.example.com") + passthroughDomain("analytics.example.com") + passthrough { host -> host.endsWith(".cdn.example.com") } +} +``` + + + + +`WebViewBundleProtocol.local(hosts)`는 개발용 프록시입니다. 전체 요청 호스트를 로컬 베이스 URL로 매핑하므로, 개발 중에 번들러의 핫 리로드를 그대로 유지할 수 있습니다. Android 에뮬레이터에서 `10.0.2.2`는 호스트 머신의 루프백에 연결됩니다. + +```kotlin +WebViewBundleProtocol.local( + mapOf("app.wvb" to "http://10.0.2.2:3000"), +) +``` + + + + +일반적인 개발 설정에서는 프록시할 호스트에 `local()`을 등록하고, 폴백으로 `bundle()`을 가장 마지막에 등록합니다. + +```kotlin +WebViewBundleConfig( + protocols = listOf( + WebViewBundleProtocol.local(mapOf("app.wvb" to "http://10.0.2.2:3000")), + WebViewBundleProtocol.bundle(), + ), +) +``` + +## OTA 업데이트 + +`WebViewBundleUpdaterConfig`를 `WebViewBundleConfig.updater`로 전달하세요. 그러면 라이브러리가 원격 클라이언트와 업데이터를 만들어 각각 `wvb.remote`와 `wvb.updater`로 노출합니다(업데이터 config를 제공하지 않으면 둘 다 `null`). + +```kotlin title="MainActivity.kt" +import android.util.Base64 +import dev.wvb.IntegrityPolicy +import dev.wvb.SignatureAlgorithm +import dev.wvb.SignatureVerifierOptions +import dev.wvb.SignatureVerifyingKey +import dev.wvb.VerifyingKeyFormat +import dev.wvb.WebViewBundleConfig +import dev.wvb.WebViewBundleProtocol +import dev.wvb.WebViewBundleRemoteConfig +import dev.wvb.WebViewBundleUpdaterConfig + +val wvb = WebViewBundle.getInstance( + this, + WebViewBundleConfig( + protocols = listOf(WebViewBundleProtocol.bundle()), + updater = WebViewBundleUpdaterConfig( + remote = WebViewBundleRemoteConfig(endpoint = "http://10.0.2.2:4313"), + channel = "stable", + integrityPolicy = IntegrityPolicy.STRICT, + signatureVerifier = SignatureVerifierOptions( + algorithm = SignatureAlgorithm.ED25519, + key = SignatureVerifyingKey( + format = VerifyingKeyFormat.SPKI_DER, + pem = null, + der = Base64.decode("MCowBQYDK2VwAyEA...", Base64.NO_WRAP), + ), + ), + ), + onError = { error -> /* 로그 */ }, + ), +) +``` + +`IntegrityPolicy.STRICT`는 다이제스트가 일치하지 않는 번들을 거부합니다. `OPTIONAL`은 값이 있을 때만 검증하고 없으면 건너뜁니다. `NONE`은 검사를 건너뜁니다. integrity 문자열은 SHA-2(`sha256`, `sha384`, `sha512`)를 사용합니다. 서명 검증기는 누가 번들을 게시했는지 증명합니다. 모든 알고리즘과 키 포맷 조합이 유효한 것은 아닙니다. + +| `SignatureAlgorithm` | 유효한 `VerifyingKeyFormat` | +| ------------------------------------ | ------------------------------------------------------------ | +| `ECDSA_SECP256R1`, `ECDSA_SECP384R1` | `SEC1`, `SPKI_DER`, `SPKI_PEM` | +| `ED25519` | `SPKI_DER`, `SPKI_PEM`, `RAW` (`der`로 전달하는 32바이트 키) | +| `RSA_PKCS1_V15`, `RSA_PSS` | `PKCS1_DER`, `PKCS1_PEM`, `SPKI_DER`, `SPKI_PEM` | + +지원되지 않는 조합은 업데이터를 만들 때 `Exception.Signature`를 던집니다. `*_PEM` 포맷에서는 `pem`을, DER·`SEC1`·`RAW`에서는 `der`을 읽습니다. + + + Android 에뮬레이터에서는 호스트 머신에서 실행 중인 원격에 `http://10.0.2.2:4313`으로 연결할 수 + 있습니다(로컬 원격의 기본 포트). `channel` 값은 쿼리 파라미터로 원격에 전송되어, 원격이 특정 + 릴리스 채널을 제공할 수 있게 합니다. + + +### Kotlin에서 업데이트 구동하기 + +업데이터는 3단계 주기로 실행됩니다. 각 호출은 `suspend` 함수이므로 코루틴에서 호출하세요. + +```kotlin +import kotlinx.coroutines.launch + +lifecycleScope.launch { + val update = wvb.updater?.getUpdate("app") // 원격 확인, 다운로드 없음 + if (update != null && update.isAvailable) { + wvb.updater?.downloadUpdate("app") // 최신 버전 다운로드, remote 디렉터리에 저장 + wvb.updater?.install("app", update.version) // 검증, 활성화, 이전 버전 정리 + } +} +``` + +`getUpdate`는 다운로드 없이 더 새로운 버전이 있는지 알려줍니다. `downloadUpdate`는 번들을 가져와 저장합니다(버전을 넘기지 않으면 최신 버전). `install`은 준비된 번들의 무결성과 서명을 검증하고, 그것을 현재 버전으로 만들며, 오래된 버전을 정리합니다. + +### 웹 JavaScript에서 업데이트 구동하기 + +동일한 주기를 `window.wvbAndroid` 브리지를 통해 웹 앱에서도 사용할 수 있습니다. 빌트인 업데이터 명령어는 `updaterGetUpdate`, `updaterDownload`, `updaterInstall`입니다. 업데이터 config를 제공하지 않은 경우 이들은 `updater_not_initialized`를 던집니다. + +```ts +declare const wvbAndroid: { + postMessage(message: string): void; +}; + +// 브리지는 { name, params, success, error }를 전달하고 콜백으로 응답합니다. +// 명령어마다 Promise를 이행하는 작은 래퍼: +function invoke(name: string, params?: unknown): Promise { + return new Promise((resolve, reject) => { + // ... success/error 콜백을 연결한 다음: + wvbAndroid.postMessage(JSON.stringify({ name, params })); + }); +} + +const update = await invoke('updaterGetUpdate', { name: 'app' }); +await invoke('updaterDownload', { name: 'app' }); +await invoke('updaterInstall', { name: 'app', version: '0.2.0' }); +``` + +플랫폼 전반의 원격 HTTP 규약, 무결성, 서명에 대해서는 [원격 번들](/docs/guide/remote-bundles)과 [원격 구축하기](/docs/guide/remote) 가이드를 참고하세요. + +## 다음 단계 + + + + + + diff --git a/content/docs/guide/platforms/deno.ko.mdx b/content/docs/guide/platforms/deno.ko.mdx new file mode 100644 index 0000000..97e9331 --- /dev/null +++ b/content/docs/guide/platforms/deno.ko.mdx @@ -0,0 +1,208 @@ +--- +title: Deno Desktop +description: Deno.serve 요청 핸들러를 통해 Deno 데스크톱 웹뷰에서 Webview Bundle 아카이브를 제공하고 업데이트합니다. +--- + +실험적 + +Deno Desktop은 `Deno.BrowserWindow`로 네이티브 웹뷰를 렌더링하고 `Deno.serve`로 이를 제공합니다. Webview Bundle이 이 서버의 핸들러가 되므로, 창은 네트워크에서 자산을 가져오는 대신 패킹된 `.wvb` 자산을 오프라인으로 로드합니다. + +```ts title="main.ts" +import { webviewBundle, bundleProtocol } from '@wvb/deno-desktop'; + +const wvb = await webviewBundle({ + protocols: [bundleProtocol({ scheme: 'app' })], +}); + +const server = Deno.serve(wvb.fetch); +const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` }); +await win.closed; +``` + +`@wvb/deno-desktop`는 통합 계층입니다. `@wvb/deno`는 [`@wvb/node`](/docs/references/node)의 FFI 짝으로, 공유 Rust 코어를 구동합니다. + +## 구성 방식 + +창은 `Deno.serve`가 제공하는 단일 로컬 origin을 가리킵니다. 따라서 이 통합은 **단일 origin**이며, 정확히 하나의 프로토콜만 허용합니다. 코어가 각 플랫폼에 도달하는 방식은 [플랫폼 통합](/docs/guide/platform-integration)을, 프로토콜 모델은 [프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. + +## @wvb/deno-desktop + +`webviewBundle(config)`를 호출하세요. 이 함수는 `wvb`로 별칭 지정되며 `WebviewBundle` 클래스로 뒷받침됩니다. `BundleSource`(그리고 선택적으로 `Remote`와 `Updater`)를 만들고 `Deno.serve` 호환 `fetch` 핸들러를 반환합니다. 원격에서 최신 번들을 가져오려면 `updater`를 추가하세요. + +```ts title="main.ts" +import { webviewBundle, bundleProtocol } from '@wvb/deno-desktop'; + +const wvb = await webviewBundle({ + protocols: [bundleProtocol({ scheme: 'app' })], + updater: { + remote: { endpoint: 'https://bundles.example.com' }, + }, +}); + +const server = Deno.serve(wvb.fetch); +const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` }); +await win.closed; +``` + +### 바인딩 + +`registerBindings(win, wvb)`를 호출하면 네이티브 명령어를 웹 앱에 노출합니다. 이 함수는 `wvbInvoke`라는 이름의 `Deno.BrowserWindow` 바인딩 하나를 등록합니다. 페이지에서는 `window.bindings.wvbInvoke`로 접근할 수 있으며, `source.*`, `remote.*`, `updater.*` 그룹의 모든 `@wvb/bridge` 명령어를 디스패치합니다. + +```ts title="main.ts" +import { webviewBundle, bundleProtocol, registerBindings } from '@wvb/deno-desktop'; + +const wvb = await webviewBundle({ + protocols: [bundleProtocol({ scheme: 'app' })], +}); + +const server = Deno.serve(wvb.fetch); +const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` }); + +registerBindings(win, wvb); // window.bindings.wvbInvoke를 연결합니다 + +await win.closed; +``` + +바인딩은 FFI 경계를 넘어 예외를 던지지 않습니다. 각 호출은 `InvokeResult` 봉투(envelope)로 resolve됩니다. + +```ts +type InvokeResult = + | { ok: true; value: unknown } + | { ok: false; error: { code?: string; message: string } }; +``` + +`@wvb/bridge` 클라이언트는 `deno` 플랫폼을 감지하면 이 봉투를 벗겨냅니다. 따라서 웹 코드는 다른 플랫폼과 똑같이 `invoke()`, `source.*`, `remote.*`, `updater.*`를 그대로 호출합니다. + +```ts title="app.ts (in the webview)" +import { updater } from '@wvb/bridge'; + +const update = await updater.getUpdate('my-app'); +if (update.isAvailable) { + await updater.download('my-app'); + await updater.install('my-app', update.version); +} +``` + +## @wvb/deno + +`@wvb/deno`는 [`@wvb/node`](/docs/references/node)의 FFI 짝입니다. `Deno.dlopen`을 통해 사전 빌드된 Rust `cdylib`를 로드하고, 동일한 코어 클래스인 `BundleProtocol`, `LocalProtocol`, `Remote`, `BundleSource`, `Updater`를 노출합니다. + +각 클래스는 `Disposable`입니다. `free()`로 명시적으로 해제하거나, `using` 선언으로 스코프를 벗어날 때 `[Symbol.dispose]`를 호출하게 하세요. + +```ts title="dispose.ts" +import { BundleProtocol } from '@wvb/deno'; + +// `lib`과 `source`는 아래의 로드 + BundleSource 단계에서 가져옵니다. +// 명시적 정리. +const protocol = new BundleProtocol(lib, source); +const res = await protocol.handle('get', 'app://my-app/index.html'); +protocol.free(); + +// 또는 `using`으로 스코프에 묶인 정리. +{ + using scoped = new BundleProtocol(lib, source); + await scoped.handle('get', 'app://my-app/index.html'); +} // 여기서 [Symbol.dispose]()가 실행됩니다 +``` + +### 네이티브 라이브러리 로드 + +명시적 경로에서 `cdylib`를 로드하거나, [`@denosaurs/plug`](https://jsr.io/@denosaurs/plug)를 통해 SHA-256으로 검증된 사전 빌드본을 다운로드하세요. + +```ts title="load.ts" +import { loadLib, loadLibViaPlug } from '@wvb/deno'; + +// 1. 이미 디스크에 있는 라이브러리를 로드합니다. +const lib = loadLib('./vendor/wvb/libwvb_deno.dylib'); + +// 2. sha256으로 검증된 사전 빌드본을 런타임에 다운로드합니다. +const libViaPlug = await loadLibViaPlug(); +``` + +경로를 하드코딩하는 대신 `WVB_DENO_LIB` 환경변수로 `loadLib`가 가리킬 파일을 지정하세요. + +```sh +WVB_DENO_LIB=./vendor/wvb/libwvb_deno.dylib deno run -A main.ts +``` + +또는 설치 서브커맨드로 라이브러리를 미리 벤더링하세요. + +```sh +deno run -A jsr:@wvb/deno/install --out vendor/wvb +``` + +`@wvb/deno/install`은 GitHub Releases에서 cdylib를 다운로드하고 기본적으로 SHA-256 체크섬을 검증합니다. 지원 대상은 다음과 같습니다. + +| Target triple | +| --------------------------- | +| `aarch64-apple-darwin` | +| `x86_64-apple-darwin` | +| `aarch64-unknown-linux-gnu` | +| `x86_64-unknown-linux-gnu` | +| `x86_64-pc-windows-msvc` | + +`--target`으로 특정 대상을 벤더링하세요. + +```sh +deno run -A jsr:@wvb/deno/install --out vendor/wvb --target x86_64-unknown-linux-gnu +``` + +전체 클래스 및 메서드 레퍼런스는 [Deno API 레퍼런스](/docs/references/deno)를 참고하세요. + +## 제한 사항 + +현재 Deno 바인딩에는 두 가지 공백이 있습니다. + +**커스텀 verifier 콜백은 지원하지 않습니다.** `Updater`는 **선언적** `signatureVerifier`만 받습니다. 이는 `algorithm`과 `key`를 가진 `SignatureVerifierOptions`입니다. + +```ts title="updater.ts" +const wvb = await webviewBundle({ + protocols: [bundleProtocol({ scheme: 'app' })], + updater: { + remote: { endpoint: 'https://bundles.example.com' }, + integrityPolicy: 'strict', + signatureVerifier: { + algorithm: 'ed25519', + key: { format: 'raw', data: publicKeyBytes }, + }, + }, +}); +``` + +`@wvb/node`에서 사용할 수 있는 커스텀 `integrityChecker` / `signatureVerifier` 함수 콜백은 FFI 경계를 넘을 수 없습니다. + +**`HttpOptions.defaultHeaders`는 지원하지 않습니다.** Deno `Remote`에서는 이 옵션을 쓸 수 없습니다. 다른 `HttpOptions` 필드는 여전히 적용됩니다. + +```ts title="http.ts" +const wvb = await webviewBundle({ + protocols: [bundleProtocol({ scheme: 'app' })], + updater: { + remote: { + endpoint: 'https://bundles.example.com', + http: { userAgent: 'my-app/1.0', timeout: 120_000 }, + }, + }, +}); +``` + +무결성과 서명이 플랫폼 전반에서 동작하는 방식은 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. + +## 다음 단계 + + + + + + diff --git a/content/docs/guide/platforms/electron/builder.ko.mdx b/content/docs/guide/platforms/electron/builder.ko.mdx new file mode 100644 index 0000000..26027f6 --- /dev/null +++ b/content/docs/guide/platforms/electron/builder.ko.mdx @@ -0,0 +1,109 @@ +--- +title: Electron Builder +description: 패키징 시점에 내장 .wvb 번들을 설치하고, electron-builder 앱에서 네이티브 @wvb/node 애드온을 유지합니다. +--- + +`@wvb/electron-builder`는 Webview Bundle을 [electron-builder](https://www.electron.build/) 패키징과 통합합니다. electron-builder의 `afterPack` 단계에 훅을 걸어, 내장 `.wvb` 번들을 패키징된 앱의 리소스에 설치합니다. 그 결과 `@wvb/electron`이 런타임에 제공하는 번들이 앱과 함께 배포됩니다. 단, 네이티브 `@wvb/node` 애드온을 ASAR 아카이브에서 제외하도록 electron-builder를 직접 설정해야 합니다. 런타임 설정은 [Electron 가이드](/docs/guide/platforms/electron)를 먼저 읽으세요. Forge용 동일 기능은 [Electron Forge](/docs/guide/platforms/electron/forge)를 참고하세요. + +## 하는 일 + +패키징 시점에 이 플러그인은 Webview Bundle 설정을 해석하고 내장 번들을 설치합니다. 내장 번들은 `remote` 타깃에서 다운로드하거나, `@wvb/cli`를 통해 로컬 워크스페이스에서 패킹합니다. 그런 다음 번들을 `/.wvb/builtin/bundles/-` 아래에 준비하고, 패키징된 앱의 `Resources/` 디렉터리로 복사합니다. 이 경로는 `@wvb/electron`이 패키징된 상태에서 내장 번들을 읽는 위치(`process.resourcesPath/bundles`)와 정확히 일치합니다. 따라서 런타임은 추가 설정 없이 번들을 인식합니다. + +이 플러그인은 `electron-builder` 24 이상을 필요로 하며(선택적 peer 의존성으로 선언됨), 번들을 해석하고 패킹하기 위해 `@wvb/cli`와 `@wvb/config`를 가져옵니다. + +## 플러그인 설정 + +electron-builder 설정을 `withWebviewBundle(...)`(별칭 `withWvb`)로 감싸세요. 이 함수는 `afterPack` 훅을 대신 구성하며, 함수 값으로 지정된 기존 `afterPack`이 있으면 그대로 보존합니다. + +```ts title="electron-builder.config.ts" +import { withWebviewBundle } from '@wvb/electron-builder'; + +export default withWebviewBundle({ + appId: 'com.example.app', + asar: true, + mac: { target: 'dmg' }, +}); +``` + +플러그인 옵션은 두 번째 인자로 전달합니다. + +```ts title="electron-builder.config.ts" +import { withWebviewBundle } from '@wvb/electron-builder'; + +export default withWebviewBundle( + { + appId: 'com.example.app', + asar: true, + }, + { + bundlesDir: 'bundles', + channel: 'beta', + } +); +``` + +훅을 직접 연결하고 싶다면, electron-builder의 `afterPack` 함수를 반환하는 로우 레벨 팩토리 `webviewBundleAfterPack(options?)`(별칭 `wvbAfterPack`)를 사용하세요. 이 패키지는 `resolveResourcesPath(ctx)`도 내보냅니다. 이 함수는 주어진 빌드 컨텍스트에 대해 패키징된 앱의 리소스 디렉터리를 반환합니다. + +### 옵션 + +`@wvb/electron-builder`와 `@wvb/electron-forge`는 다음 옵션을 공유합니다. + +| Option | Type | Default | Meaning | +| ------------------------- | ------------------- | ----------- | --------------------------------------------------------------------------------------------- | +| `root` | `string` | (resolved) | 설정과 번들을 찾는 데 사용하는 프로젝트 루트 | +| `builtin` | `BuiltinConfig` | from config | 설정 파일을 덮어쓰는 인라인 내장 설정 | +| `bundlesDir` | `string` | `'bundles'` | 패키징된 `Resources` 아래의 대상 디렉터리 이름 | +| `configFile` | `string \| boolean` | `true` | `true`는 자동으로 찾아 병합하고, 경로를 지정하면 명시적으로 로드하며, `false`는 인라인만 사용 | +| `channel` | `string` | — | 번들을 설치할 릴리스 채널(예: `"beta"`) | +| `throwWhenBuiltinIsEmpty` | `boolean` | `true` | 설치된 번들이 하나도 없으면 예외를 던짐 | + +`builtin.target`을 설정하지 않으면, 플러그인은 해석된 원격 엔드포인트를 사용하는 `remote` 타깃을 기본값으로 씁니다. `bundlesDir`은 `..` 세그먼트가 없는 상대 경로여야 합니다. 내부의 `builtin` 및 `remote` 설정은 [설정](/docs/config)을 참고하고, 설치 소스는 [원격 서버 구축하기](/docs/guide/remote)를 참고하세요. + +## 네이티브 애드온을 ASAR에서 제외하기 + +`@wvb/electron`은 네이티브 N-API 애드온(`.node` 바이너리)인 `@wvb/node`에 의존합니다. `asar: true`일 때 electron-builder는 `node_modules`를 ASAR 아카이브로 묶는데, 네이티브 바이너리는 ASAR 내부에서 로드할 수 없습니다. `asarUnpack`을 사용해 애드온을 압축 해제하면 런타임에 로드됩니다. + +```json title="electron-builder 설정" +{ + "asar": true, + "asarUnpack": ["**/node_modules/@wvb/node/**"] +} +``` + +플러그인이 내장 번들 설치를 처리하므로, 번들을 위해 `extraResources`를 지정할 필요는 없습니다. 플러그인을 쓰지 않고 `.wvb` 파일을 직접 준비한다면, 해당 디렉터리를 추가 리소스로 배포해 `Resources/bundles`에 들어가게 하세요. + +```json title="electron-builder 설정" +{ + "extraResources": [{ "from": "bundles", "to": "bundles" }] +} +``` + + + 개발 환경에서는 앱이 동작하지만 패키징 후 `@wvb/node`에 대한 모듈 누락 오류가 발생한다면, 네이티브 + 애드온이 ASAR로 묶인 것입니다. 위의 `asarUnpack` 글로브를 추가하고 다시 패키징하세요. + + +## 다음 단계 + + + + + + + diff --git a/content/docs/guide/platforms/electron/forge.ko.mdx b/content/docs/guide/platforms/electron/forge.ko.mdx new file mode 100644 index 0000000..7999e19 --- /dev/null +++ b/content/docs/guide/platforms/electron/forge.ko.mdx @@ -0,0 +1,107 @@ +--- +title: Electron Forge +description: 패킹된 .wvb 빌트인을 추가 리소스로 함께 배포하고, @wvb/electron-forge 플러그인으로 네이티브 @wvb/node 애드온을 ASAR에서 언팩합니다. +--- + +`@wvb/electron-forge`는 Webview Bundle을 패키징 단계에 연결하는 Electron Forge 플러그인입니다. 패킹된 `.wvb` 빌트인을 패키징된 앱의 리소스에 설치하고, 네이티브 `@wvb/node` 애드온이 런타임에 항상 사용 가능하도록 도와줍니다. 덕분에 번들 파일을 직접 배치할 필요가 없습니다. 메인 [Electron 가이드](/docs/guide/platforms/electron)와 함께 사용하세요. 이 가이드는 커스텀 프로토콜로 번들을 제공하고 무선(OTA)으로 업데이트하는 방법을 다룹니다. + +## 플러그인이 하는 일 + +이 플러그인은 Forge의 `PluginBase`를 확장하고 `packageAfterCopy`에 훅을 겁니다. 패키징 시점에 Webview Bundle 설정을 확인하고, 빌트인 `.wvb` 번들을 설치합니다(`remote` 타깃에서 다운로드하거나 `@wvb/cli`로 로컬 워크스페이스에서 패킹). 그런 다음 번들을 `/.wvb/builtin/bundles` 아래에 배치하고, 복사된 앱 디렉터리 옆의 패키징된 앱 리소스로 복사합니다. 이 위치가 바로 `@wvb/electron`이 런타임에 빌트인 번들을 읽어 들이는 곳입니다. + +정확한 인터페이스는 패키지 소스를 참고하세요: [`@wvb/electron-forge`](https://github.com/webview-bundle/webview-bundle/tree/main/packages/electron-forge). + +## forge.config.ts에 추가하기 + +다른 플러그인과 함께 Forge 설정에 이 플러그인을 등록하세요. 플러그인 클래스는 `WebviewBundlePlugin`입니다(기본 익스포트로도 제공되며 `WvbPlugin`이라는 별칭도 있습니다). + +```ts title="forge.config.ts" +import type { ForgeConfig } from '@electron-forge/shared-types'; +import { WebviewBundlePlugin } from '@wvb/electron-forge'; + +const config: ForgeConfig = { + plugins: [new WebviewBundlePlugin({ bundlesDir: 'bundles', channel: 'beta' })], +}; + +export default config; +``` + +플러그인은 `WebviewBundlePluginConfig`에서 옵션을 읽습니다: + +| 옵션 | 타입 | 기본값 | 의미 | +| ------------------------- | ------------------- | ----------- | ---------------------------------------------------------------------------------- | +| `bundlesDir` | `string` | `'bundles'` | 패키징된 앱 리소스 아래의 대상 디렉터리 이름 | +| `configFile` | `string \| boolean` | `true` | `true`는 자동 검색 후 병합, 경로를 지정하면 명시적으로 로드, `false`는 인라인 전용 | +| `channel` | `string` | — | 번들을 설치할 릴리스 채널 | +| `throwWhenBuiltinIsEmpty` | `boolean` | `true` | 설치된 번들이 하나도 없으면 예외를 던짐 | + +또한 공유 [Webview Bundle 설정](/docs/config)에서 가져온 인라인 `root`와 `builtin` 설정을 받습니다. `builtin.target`을 지정하지 않으면, 플러그인은 확인된 원격 엔드포인트를 사용하는 `remote` 타깃을 기본값으로 씁니다. + +## 네이티브 바이너리와 추가 리소스 + +플러그인 없이도 두 가지는 반드시 패키징된 앱에 포함되어야 합니다. 빌트인 `bundles` 디렉터리와 네이티브 `@wvb/node` 애드온입니다. 플러그인은 번들을 처리합니다. 네이티브 애드온은 Forge의 `AutoUnpackNativesPlugin`으로 ASAR 아카이브에서 언팩합니다. + +```ts title="forge.config.ts" +import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'; +import { WebviewBundlePlugin } from '@wvb/electron-forge'; + +const config: ForgeConfig = { + packagerConfig: { + asar: true, + extraResource: ['bundles'], // 빌트인 번들을 함께 배포 + }, + plugins: [ + // …vite 플러그인… + new AutoUnpackNativesPlugin({}), // @wvb/node의 .node 바이너리를 ASAR에서 언팩 + new WebviewBundlePlugin({}), + ], +}; +``` + +`extraResource: ['bundles']`는 빌트인 번들을 패키징된 앱의 리소스로 배포합니다. `AutoUnpackNativesPlugin`은 `@wvb/node` 네이티브 `.node` 바이너리를 ASAR에서 언팩하여 런타임에 로드할 수 있게 합니다. [`wvb pack`](/docs/guide/commands/pack)을 실행하면 그 `bundles` 디렉터리에 들어갈 `.wvb` 파일을 생성합니다. + +## node_modules를 유지해 네이티브 모듈을 번들에 포함하기 + +Forge Vite 플러그인은 패키지에서 `node_modules`를 제외할 수 있는데, 이때 네이티브 `@wvb/node` 모듈이 빠집니다. 런타임에 `@wvb/node` 바이너리를 찾지 못하는 문제가 발생하면, `packagerConfig.ignore`를 재정의해 `node_modules`를 유지하세요. + +```ts title="forge.config.ts" +const config: ForgeConfig = { + packagerConfig: { + asar: true, + extraResource: ['bundles'], + // 네이티브 @wvb/node 애드온이 번들에 포함되도록 node_modules를 유지합니다. + ignore: [/^\/(?!node_modules|\.vite|package\.json)/], + }, +}; +``` + + + 앱이 개발 환경에서는 실행되지만 패키징하면 창이 비어 있다면, `bundles` 리소스나 네이티브 + `@wvb/node` 바이너리가 빠졌을 가능성이 큽니다. 위의 `extraResource`, `AutoUnpackNativesPlugin`, + `packagerConfig.ignore` 재정의를 확인하세요. + + +## 다음 단계 + + + + + + + diff --git a/content/docs/guide/platforms/electron/index.ko.mdx b/content/docs/guide/platforms/electron/index.ko.mdx new file mode 100644 index 0000000..1ab5f77 --- /dev/null +++ b/content/docs/guide/platforms/electron/index.ko.mdx @@ -0,0 +1,252 @@ +--- +title: Electron +description: 커스텀 프로토콜을 통해 Electron UI를 .wvb 번들에서 제공하고, 개발 서버 프록시와 무선(OTA) 업데이트를 활용하세요. +--- + +`@wvb/electron`은 세 단계로 Webview Bundle을 Electron에 연결합니다. 커스텀 프로토콜을 통해 UI를 `.wvb` 번들에서 제공하고, 개발 중에는 개발 서버로 프록시하며, 필요하면 번들을 무선(OTA)으로 업데이트합니다. + +## Install + +`@wvb/electron`은 Electron 15 이상을 요구하며 `@wvb/node`(사전 빌드된 N-API 바이너리 — Rust 툴체인 불필요)를 함께 설치합니다. `@wvb/cli`는 빌드 시점에 번들을 패킹합니다. + + + + +```sh +npm install @wvb/electron +npm install -D @wvb/cli +``` + + + + +```sh +pnpm add @wvb/electron +pnpm add -D @wvb/cli +``` + + + + +```sh +yarn add @wvb/electron +yarn add -D @wvb/cli +``` + + + + +## Register the protocol in the main process + +윈도우를 로드하기 전에 `wvb(...)`(`webviewBundle(...)`의 별칭)를 호출하세요. 스킴을 privileged로 등록하고, 소스를 빌드하며, 프로토콜 핸들러와 IPC를 연결합니다. + +```ts title="src/main.ts" +import path from 'node:path'; +import { app, BrowserWindow } from 'electron'; +import { bundleProtocol, localProtocol, wvb } from '@wvb/electron'; + +const instance = wvb({ + source: { + builtinDir: path.join(process.resourcesPath, 'bundles'), + }, + protocols: [ + // 개발: `app-local://app.wvb/...`를 Vite 개발 서버로 프록시하여 핫 리로드를 지원합니다. + localProtocol('app-local', { + hosts: { 'app.wvb': MAIN_WINDOW_VITE_DEV_SERVER_URL }, + }), + // 프로덕션: `app://.wvb/...`를 번들에서 직접 제공합니다. + bundleProtocol('app', { onError: e => console.error('[wvb]', e) }), + ], +}); + +async function createWindow() { + await instance.whenProtocolRegistered(); + const win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + }, + }); + await win.loadURL('app://app.wvb'); +} + +app.whenReady().then(createWindow); +``` + +URL 형태는 `://.wvb/`입니다. `app://app.wvb/index.html`은 번들 `app`의 파일 `/index.html`로 해석됩니다. + +- **`bundleProtocol(scheme, options?)`**는 소스에 있는 번들에서 파일을 직접 제공합니다. +- **`localProtocol(scheme, { hosts })`**는 일치하는 호스트를 개발 서버로 프록시합니다. `hosts`는 필수이며, 호스트를 URL로 매핑하는 `Record`(또는 이를 반환하는 함수)입니다. + +`app.isPackaged`에 따라 로드할 URL을 선택하세요. + +```ts +await win.loadURL(app.isPackaged ? 'app://app.wvb' : 'app-local://app.wvb'); +``` + +`whenProtocolRegistered()`는 모든 프로토콜이 등록되고 `app.whenReady()`가 발생한 후에 resolve됩니다. 화면을 이동하기 전에 항상 await하세요. + +`privileges` 옵션으로 프로토콜별 privileges를 재정의하세요. + +```ts +bundleProtocol('app', { + privileges: { stream: true }, // merged over the defaults below +}); +``` + + + 각 스킴은 다음 기본값으로 privileged로 등록됩니다. `standard`, `secure`, `bypassCSP`, + `allowServiceWorkers`, `supportFetchAPI`, `corsEnabled`, `codeCache`는 모두 `true`이고, `stream`은 + `false`입니다. 전체 모델은 [프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. + + +## Add the preload script + +프리로드는 브리지가 소스, 원격, 업데이터 호출에 사용하는 안전한 전송 계층(`window.wvbElectron.invoke`)을 노출합니다. + +```ts title="src/preload.ts" +import { preload } from '@wvb/electron/preload'; + +preload(); +``` + +`webPreferences.preload`가 컴파일된 프리로드를 가리키게 하고, `contextIsolation: true`와 `nodeIntegration: false`를 유지하세요. + +## Call the API from the renderer + +UI에서 업데이트를 제어하려면 렌더러 코드에서 브리지를 임포트하세요. 예를 들어 "업데이트 확인" 버튼을 만들 수 있습니다. 브리지는 IPC를 통해 메인 프로세스로 전달되므로 프리로드가 로드되었을 때만 동작합니다. + +```ts title="src/renderer.ts" +import { source, remote, updater } from '@wvb/bridge'; + +// 지금 로컬에 설치된 버전은 무엇인가? +const current = await source.loadVersion('app'); + +// 원격에 더 새로운 버전이 배포되었는가? +const update = await updater.getUpdate('app'); +if (update) { + const downloaded = await updater.download('app'); // 다운로드 + 검증 + 스테이징 + await updater.install('app', downloaded.version); // 활성화 + // 새 번들을 반영하려면 윈도우를 리로드하세요 — 앱이 직접 처리해야 합니다. +} +``` + + + `source`, `remote`, `updater`는 `@wvb/bridge`에서 가져오며, `@wvb/bridge`는 + `@wvb/electron/preload`가 설치한 Electron 전송 계층을 자동으로 감지합니다. + `getUpdate`/`download`의 결과 형태는 [Node API 레퍼런스](/docs/references/node)에 있습니다. + + +`wvb(...)`가 반환하는 인스턴스에서 메인 프로세스의 동일한 인터페이스에 접근할 수 있습니다. + +```ts +instance.source; // BundleSource +instance.remote; // Remote | null (null unless `updater` is configured) +instance.updater; // Updater | null (null unless `updater` is configured) +await instance.whenProtocolRegistered(); +``` + +## Configure over-the-air updates + +원격을 가리키는 `updater` 블록을 추가하세요. 이 블록이 있어야 `instance.remote`와 `instance.updater`가 활성화됩니다. 블록을 생략하면 렌더러의 `remote.*` / `updater.*` 호출이 "not initialized" 브리지 오류로 실패합니다. + +```ts +wvb({ + source: { builtinDir: path.join(process.resourcesPath, 'bundles') }, + updater: { + remote: { endpoint: 'https://updates.example.com' }, + channel: 'stable', + // 무결성 및 서명 검증도 여기에 설정합니다: + // integrityPolicy, integrityChecker, signatureVerifier + }, + protocols: [bundleProtocol('app')], +}); +``` + +업데이트를 _누가_ 게시했는지 검증하려면 서명 키를 고정하세요. + +```ts +updater: { + remote: { endpoint: 'https://updates.example.com' }, + signatureVerifier: { + algorithm: 'ed25519', + key: '/* raw 32-byte or PEM public key */', + }, +}, +``` + +`SignatureAlgorithm`은 `ecdsaSecp256R1`, `ecdsaSecp384R1`, `ed25519`, `rsaPkcs1V15`, `rsaPss` 중 하나입니다. 무결성과 서명에 대한 자세한 내용은 [원격 번들](/docs/guide/remote-bundles)을, 서버를 세우는 방법(테스트용 로컬 서버 포함)은 [원격 서버 구축](/docs/guide/remote)을 참고하세요. + +## Where bundles live + +`source`는 `builtinDir`(함께 배포되는 읽기 전용)와 `remoteDir`(다운로드된 업데이트)를 받으며, 둘 다 Electron에 맞는 기본값을 가집니다. + +| Option | Default | +| ------------ | -------------------------------------------------------------------------------- | +| `builtinDir` | 패키징된 경우 `process.resourcesPath/bundles`, 그 외에는 `process.cwd()/bundles` | +| `remoteDir` | `app.getPath('userData')/bundles` | + +다운로드된 버전이 builtin 버전보다 우선하므로, `updater.download(...)`와 `updater.install(...)` 후에는 설치된 업데이트가 자동으로 제공됩니다. 해석 방식에 대한 자세한 내용은 [번들 소스](/docs/guide/bundle-sources)를 참고하세요. + +## Pack and ship bundles + +웹 앱을 빌드한 다음, 산출물을 `builtinDir`로 지정한 디렉터리에 패킹하세요. + +```sh +# 먼저 렌더러를 빌드한 뒤(예: `vite build`), 다음을 실행하세요: +npx wvb pack ./dist --outfile bundles/app/app_1.0.0.wvb +``` + +프로토콜이 런타임에 번들을 제공할 수 있도록 `bundles` 디렉터리를 앱과 함께 배포하세요. 이 파일들을 패키지에 스테이징하는 방법은 패키징 도구에 따라 다릅니다 — [패키징](#packaging)을 참고하세요. + +## Packaging + +`@wvb/electron`을 사용하는 Electron 앱을 패키징하려면 최종 빌드에 두 가지가 포함되어야 합니다. builtin `bundles` 디렉터리가 앱과 함께 배포되어야 하고, 네이티브 `@wvb/node` 바이너리가 런타임에 로드될 수 있도록 ASAR 아카이브 밖에 있어야 합니다. 전용 통합이 두 가지를 모두 처리하고 패키징 시점에 builtin 번들을 설치하므로, `.wvb` 파일을 직접 스테이징할 필요가 없습니다. + + + + + + +## Troubleshooting + +- **빈 윈도우 또는 `ERR_FAILED`** — `wvb(...)`가 실행되고 `whenProtocolRegistered()`가 `loadURL` 전에 resolve되는지, 그리고 URL의 번들 이름이 패킹된 파일과 일치하는지 확인하세요(`app://app.wvb` → 번들 `app`). +- **렌더러가 브리지 API에 접근하지 못함** — 프리로드가 로드되지 않았습니다. `webPreferences.preload`와 그것이 `preload()`를 호출하는지 확인하세요. +- **`remote_not_initialized` / `updater_not_initialized`** — `remote.*` / `updater.*` 호출이 있었지만 `wvb(...)`에 `updater` 블록을 전달하지 않았습니다. [무선(OTA) 업데이트](#configure-over-the-air-updates) 설정을 추가하세요. +- **개발에서는 동작하지만 패키징하면 실패함** — `bundles` 리소스나 네이티브 `@wvb/node` 바이너리가 패키지에 포함되지 않았습니다. [패키징](#packaging)을 참고하세요. + +## Next steps + + + + + + + diff --git a/content/docs/guide/platforms/electron/meta.ko.json b/content/docs/guide/platforms/electron/meta.ko.json new file mode 100644 index 0000000..5c496a3 --- /dev/null +++ b/content/docs/guide/platforms/electron/meta.ko.json @@ -0,0 +1,4 @@ +{ + "title": "Electron", + "pages": ["index", "forge", "builder"] +} diff --git a/content/docs/guide/platforms/ios.ko.mdx b/content/docs/guide/platforms/ios.ko.mdx new file mode 100644 index 0000000..24c63f1 --- /dev/null +++ b/content/docs/guide/platforms/ios.ko.mdx @@ -0,0 +1,247 @@ +--- +title: iOS +description: webview-bundle-ios Swift 패키지로 WKWebView에서 Webview Bundle 아카이브를 제공하고 업데이트합니다. +--- + +`webview-bundle-ios` Swift 패키지는 커스텀 URL 스킴을 통해 `.wvb` 번들을 `WKWebView`에 제공하고, +무선(OTA) 업데이트로 항상 최신 상태를 유지합니다. 스킴을 등록하고 `WKWebView`를 `app://app.wvb`로 +가리키면, 이 패키지가 디스크에 있는 번들에서 모든 요청에 응답합니다. 오프라인 우선으로 동작하며, +업데이터가 백그라운드에서 더 새로운 번들을 받아오므로 앱스토어 출시가 필요 없습니다. + +## Requirements + +| Requirement | Value | +| ---------------- | ---------------------------------------------------------- | +| 최소 OS | iOS 16, macOS 12 (`platforms: [.macOS(.v12), .iOS(.v16)]`) | +| Swift 도구 | 6.1, 언어 모드 6 | +| SwiftPM 프로덕트 | `WebViewBundle` | + +이 패키지는 `WebViewBundleLibrary`라는 UniFFI 생성 모듈을 통해 Rust 코어에 바인딩되며, 둘 다 +`WebViewBundle` 모듈로 노출됩니다. 공유 코어가 각 플랫폼에 어떻게 도달하는지는 +[플랫폼 통합](/docs/guide/platform-integration)을 참고하세요. + +패키지를 의존성으로 추가하세요(네이티브 FFI는 아래 설치 스크립트가 연결합니다). + +```swift title="Package.swift" +dependencies: [ + .package(path: "../webview-bundle-ios"), +], +targets: [ + .target(name: "App", dependencies: [ + .product(name: "WebViewBundle", package: "webview-bundle-ios"), + ]), +] +``` + +## Install the native FFI + +빌드하기 전에 릴리스에서 네이티브 바이너리를 해석하세요. + +```sh +node scripts/install.mjs latest # 가장 높은 ffi/* 릴리스 +node scripts/install.mjs 0.1.0 # -> ffi/0.1.0 태그 +node scripts/install.mjs ffi/0.1.0 # 명시적 릴리스 태그 +``` + +이 스크립트는 해석된 태그의 GitHub 릴리스를 읽어 다음을 수행합니다. + +- `apple.zip` 자산에서 생성된 Swift 바인딩을 추출해 `Sources/WebViewBundle/`에 넣습니다. +- `WebViewBundleFFI.xcframework.zip` 자산의 SHA-256 체크섬을 해석하고(GitHub가 보고하는 다이제스트를 + 우선 사용하며, 없으면 직접 다운로드해 해시함), 그 체크섬과 태그를 `Package.swift`에 기록합니다. + +`PATH`에 `unzip`이 필요합니다. 소스 저장소를 재정의하고, 비공개 저장소는 인증하세요. + +```sh +export GITHUB_TOKEN=ghp_xxx # 비공개 소스 저장소의 경우, 또는 GH_TOKEN +node scripts/install.mjs latest --repo webview-bundle/webview-bundle +``` + +## Quick start + +한 번 설정하고, `WKWebViewConfiguration`에 스킴을 등록한 뒤, 엔트리 URL을 로드하세요. **커스텀 스킴**을 +사용하세요. `http`와 `https`는 예약되어 있어 초기화 시 거부됩니다. + +```swift title="ContentView.swift" +import SwiftUI +import WebKit +import WebViewBundle + +let instance = try WebViewBundle.configure( + WebViewBundleConfig( + protocols: [.bundle(scheme: "app")], + updater: WebViewBundleUpdaterConfig( + remote: WebViewBundleRemoteConfig(endpoint: "https://bundles.example.com"), + integrityPolicy: .strict, + signatureVerifier: SignatureVerifierOptions( + algorithm: .ed25519, + key: SignatureVerifyingKey(format: .spkiDer, pem: nil, der: publicKeyDer) + ) + ) + ) +) + +let config = WKWebViewConfiguration() +instance.install(on: config) // app:// 스킴 핸들러 + JS 브리지 등록 + +let webView = WKWebView(frame: .zero, configuration: config) +webView.load(URLRequest(url: URL(string: "app://app.wvb")!)) +``` + +`configure(_:)`는 인스턴스를 한 번 만들어 프로세스 전역에 캐시합니다. 엔트리 URL `app://app.wvb`는 +`app`이라는 이름의 번들을 선택합니다. 수동 설정을 건너뛰려면 바로 사용할 수 있는 `WKWebView`를 +받으세요. + +```swift +let webView = instance.makeWebView() // 또는: instance.makeConfiguration() +webView.load(URLRequest(url: URL(string: "app://app.wvb")!)) +``` + +설정된 싱글턴은 어디서든 읽을 수 있습니다. `shared`는 `configure(_:)` 이전에 읽으면 트랩을 발생시키고, +`safeShared`는 그렇지 않습니다. + +```swift +let wvb = WebViewBundle.shared // 아직 설정되지 않았으면 precondition 실패 +let maybe = WebViewBundle.safeShared // 아직 설정되지 않았으면 nil +``` + +## How serving works + +각 스킴에는 `setURLSchemeHandler(_:forURLScheme:)`으로 등록된 `WKURLSchemeHandler`가 부여됩니다. 이 +핸들러는 각 요청을 Rust 코어에 매핑하고, Rust 코어는 디스크에 있는 번들에서 응답합니다. + +- **번들 이름 = 요청 호스트의 첫 번째 레이블.** `app://app.wvb/index.html`은 번들 `app`, 경로 + `/index.html`로 해석됩니다. +- **예약된 스킴은 초기화 시 거부됩니다**(예약 스킴에 핸들러를 등록하면 잡을 수 없는 예외가 발생합니다): + `http`, `https`, `file`, `ftp`, `ftps`, `ws`, `wss`, `about`, `blob`, `data`, `javascript`. +- **스킴은 `^[a-z][a-z0-9+.-]*$`와 일치해야 하며** 고유해야 합니다. 비어 있거나, 형식이 잘못되었거나, + 중복되거나, 예약된 스킴은 `WebViewBundleError`를 던집니다. + +```swift +do { + _ = try WebViewBundle.configure( + WebViewBundleConfig(protocols: [.bundle(scheme: "https")])) +} catch WebViewBundleError.reservedScheme(let scheme) { + print("\(scheme) is reserved") // .emptyScheme / .invalidScheme / .duplicateScheme 도 존재 +} +``` + +프로토콜 종류는 두 가지입니다. `.bundle`은 번들 엔트리를 제공하고, `.local`은 로컬 HTTP 서버로 +프록시합니다(전체 요청 호스트를 `hosts` 맵과 대조). 모델은 +[프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. + +```swift +WebViewBundleConfig(protocols: [ + .bundle(scheme: "app"), + .local(scheme: "dev", hosts: ["myapp": "http://localhost:8080"]), +]) +``` + +## The bridge + +`install(on:)`은 JavaScript-네이티브 브리지도 함께 연결합니다. 웹 앱은 +`window.webkit.messageHandlers.wvbIos`로 메시지를 보내며, 브리지는 **메인 프레임 메시지만** 받습니다 +(서브프레임과 iframe 메시지는 버려집니다). 브리지는 소스, 원격, 업데이터 명령어를 노출합니다. + +```ts title="웹 앱" +window.webkit.messageHandlers.wvbIos.postMessage({ + name: 'updaterGetUpdate', + params: { bundleName: 'app' }, +}); +``` + +## Sources + +각 앱은 두 개의 소스에서 읽습니다. 전체 모델은 [번들 소스](/docs/guide/bundle-sources)를 참고하세요. + +- **Builtin** — 앱 안에 함께 배포되는 읽기 전용 번들입니다. 기본 디렉터리는 앱 번들의 `/bundles` + 폴더입니다. 파일이 그 위치에 놓이도록 폴더 레퍼런스로 배포하세요. +- **Remote** — 다운로드된 업데이트를 위한 쓰기 가능 디렉터리입니다. 기본 위치는 Application Support + 아래이며, 앱의 번들 식별자로 네임스페이스가 지정됩니다. + +`WebViewBundleConfig.source`의 `SourceOptions`를 통해 두 경로를 재정의할 수 있습니다. + +```swift +WebViewBundleConfig( + source: SourceOptions( + builtinDir: Foundation.Bundle.main.resourcePath.map { "\($0)/bundles" }, + remoteDir: nil, // nil => 기본 Application Support 디렉터리 + builtinManifestFilepath: nil, + remoteManifestFilepath: nil + ), + protocols: [.bundle(scheme: "app")] +) +``` + + + `WebViewBundle` 모듈 안에서는 정규화되지 않은 `Bundle`이 `Foundation.Bundle`이 아니라 FFI 번들 + 클래스로 해석됩니다. 앱 번들을 의미할 때는 `Foundation.Bundle`을 명시적으로 쓰세요. + + +## OTA updates + +`WebViewBundleConfig.updater`를 설정하면 패키지가 `Remote`와 `Updater`를 만들어 주며, 각각 +`instance.remote`와 `instance.updater`로 접근할 수 있습니다. + +```swift +let updaterConfig = WebViewBundleUpdaterConfig( + remote: WebViewBundleRemoteConfig(endpoint: "https://bundles.example.com"), + channel: "stable", + integrityPolicy: .strict, + signatureVerifier: SignatureVerifierOptions( + algorithm: .ed25519, + key: SignatureVerifyingKey(format: .spkiDer, pem: nil, der: publicKeyDer) + ) +) +``` + +`integrityPolicy`는 `.strict`, `.optional`, `.none` 중 하나입니다. 무결성 값은 `sha256:` 형태로 +직렬화된 SHA-2 다이제스트입니다. `signatureVerifier`는 누가 번들을 게시했는지를 증명합니다. + +| Field | Values | +| --------------------- | --------------------------------------------------------------------------- | +| `algorithm` | `.ecdsaSecp256r1`, `.ecdsaSecp384r1`, `.ed25519`, `.rsaPkcs1V15`, `.rsaPss` | +| `key.format` | `.spkiDer`, `.spkiPem`, `.pkcs1Der`, `.pkcs1Pem`, `.sec1`, `.raw` | +| `key.pem` / `key.der` | 텍스트 키는 `pem`, 바이너리 키는 `der` | + +업데이트 주기는 업데이터를 통해 구동하세요. + +```swift +guard let updater = instance.updater else { return } + +let info = try await updater.getUpdate(bundleName: "app") // 확인만; 다운로드 없음 +if info.isAvailable { + _ = try await updater.downloadUpdate(bundleName: "app", version: info.version) + try await updater.install(bundleName: "app", version: info.version) +} +``` + +`getUpdate`는 원격에서 더 새로운 버전이 있는지 다운로드 없이 확인합니다. `downloadUpdate`는 버전을 +다운로드해 저장합니다(`version`이 `nil`이면 최신 버전). `install`은 스테이징된 버전을 활성화하고, +설정되어 있으면 무결성과 서명을 검증한 뒤, 오래된 버전을 정리합니다. 새 번들을 렌더링하려면 다시 +로드하세요. + +```swift +try await updater.install(bundleName: "app", version: info.version) +await webView.evaluateJavaScript("location.reload()") +``` + +서명 실패는 FFI 오류 `Error.Signature(message:)`로 나타납니다. + +원격 HTTP 규약, 무결성, 서명 세부 사항은 [원격 번들](/docs/guide/remote-bundles)과 +[원격 구축하기](/docs/guide/remote)를 참고하세요. + +## Next steps + + + + + + diff --git a/content/docs/guide/platforms/tauri.ko.mdx b/content/docs/guide/platforms/tauri.ko.mdx new file mode 100644 index 0000000..fe01b48 --- /dev/null +++ b/content/docs/guide/platforms/tauri.ko.mdx @@ -0,0 +1,363 @@ +--- +title: Tauri +description: Tauri v2 앱에 wvb-tauri 플러그인을 추가하면 웹뷰를 .wvb 번들에서 제공하며, 개발 서버 프록시와 무선(OTA) 업데이트를 사용할 수 있습니다. +--- + +`wvb-tauri`는 Tauri v2용 Webview Bundle 통합입니다. 플러그인으로 등록하면 앱은 커스텀 URL 스킴을 통해 로컬 `.wvb` 번들에서 웹뷰를 제공합니다. 개발 중에는 개발 서버를 프록시하고, 더 새로운 번들을 무선(OTA)으로 다운로드합니다. 동일한 플러그인이 데스크톱과 Tauri 모바일(Android 및 iOS)에서 모두 동작합니다. + + + Tauri는 Rust 크레이트 `wvb-tauri`(crates.io)를 사용합니다. `@wvb/tauri` npm 패키지는 없습니다. + 프런트엔드는 Tauri의 표준 커맨드 브리지를 통해 플러그인과 통신하며, `@tauri-apps/api` 외에 추가 + 패키지는 필요하지 않습니다. + + +## 설치 + +```toml title="src-tauri/Cargo.toml" +[dependencies] +wvb-tauri = "0.1" +tauri = { version = "2", features = [] } +``` + +프런트엔드에서 업데이트를 제어하려면 웹 앱에 Tauri의 JS API를 설치하세요. + + + + +```sh +npm install @tauri-apps/api +``` + + + + +```sh +pnpm add @tauri-apps/api +``` + + + + +```sh +yarn add @tauri-apps/api +``` + + + + +## 플러그인 등록 + +`Config::new()`로 `Config`를 만든 다음, 번들 **소스**, 하나 이상의 **프로토콜**, 그리고 선택적으로 업데이트용 **원격**을 체이닝하세요. + +```rust title="src-tauri/src/lib.rs" +use tauri::Manager; +use wvb_tauri::{Config, Protocol, Source}; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(wvb_tauri::init( + Config::new() + .source(Source::new().builtin_dir_fn(|app| { + Ok(app.path().resource_dir()?.join("bundles")) + })) + // `bundle://.wvb/...`를 패킹된 번들에서 바로 제공합니다. + .protocol(Protocol::bundle("bundle")) + // 개발 중에는 `local://example.com/...`를 개발 서버로 프록시합니다. + .protocol(Protocol::local("local").host("example.com", "http://localhost:1420")), + )) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +`Protocol::bundle(scheme)`는 패킹된 번들에서 파일을 제공하며, 요청 호스트에서 번들 이름을 유도합니다. 예를 들어 `bundle://app.wvb/index.html`은 번들 `app`의 파일 `/index.html`로 해석됩니다. + +`Protocol::local(scheme)`는 호스트를 localhost 개발 서버로 프록시하므로 동일한 스킴이 핫 리로드와 함께 동작합니다. `.hosts(...)`로 여러 호스트를 한 번에 매핑하세요. + +```rust +use std::collections::HashMap; + +Protocol::local("local").hosts(HashMap::from([ + ("example.com".to_string(), "http://localhost:1420".to_string()), + ("api.example.com".to_string(), "http://localhost:8080".to_string()), +])); +``` + +`Source`는 번들이 위치한 곳을 해석합니다. 정적 경로 문자열이나 `AppHandle` 클로저를 사용하세요. + +```rust +use wvb_tauri::Source; + +// 정적 경로 문자열(Tauri의 path API로 해석): +Source::new().builtin_dir("$RESOURCE/bundles").remote_dir("$APPLOCALDATA/bundles"); + +// 또는 AppHandle로 런타임에 경로를 계산: +Source::new().builtin_dir_fn(|app| Ok(app.path().resource_dir()?.join("bundles"))); +``` + +값을 설정하지 않으면 builtin 디렉터리는 앱 리소스 디렉터리의 `bundles`로, remote 디렉터리는 앱 로컬 데이터의 `bundles`로 기본 설정됩니다. 각 `.protocol(...)` 호출은 자체 비동기 URI 스킴을 등록합니다. 스킴이 요청을 해석하는 방식은 [프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. + +## 창을 커스텀 스킴으로 지정 + +메인 창을 번들 스킴으로 바로 부팅하세요. + +```json title="src-tauri/tauri.conf.json" +{ + "app": { + "windows": [ + { + "url": "bundle://app.wvb" + } + ] + } +} +``` + +프런트엔드에서 플러그인 커맨드를 호출한다면 capability 파일에서 권한을 부여하세요. 네임스페이스는 `wvb-tauri`이며, `wvb-tauri:default`는 모든 source, remote, updater 커맨드를 허용합니다. + +```json title="src-tauri/capabilities/default.json" +{ + "identifier": "default", + "windows": ["main"], + "permissions": ["core:default", "wvb-tauri:default"] +} +``` + +접근을 제한하려면 `wvb-tauri:default`를 필요한 개별 `wvb-tauri:allow-*` 권한으로 교체하세요. + +```json title="src-tauri/capabilities/default.json" +{ + "identifier": "default", + "windows": ["main"], + "permissions": [ + "core:default", + "wvb-tauri:allow-updater-get-update", + "wvb-tauri:allow-updater-download", + "wvb-tauri:allow-updater-install" + ] +} +``` + +## 번들 패킹 및 배포 + +프런트엔드를 빌드하고 `.wvb`로 패킹한 다음, 결과물을 소스 디렉터리에 배치하세요. + +```sh +# 먼저 프런트엔드를 빌드한 뒤(예: `vite build`): +npx wvb pack ./dist --outfile src-tauri/bundles/app/app_1.0.0.wvb +``` + +resources에 디렉터리를 나열하여 빌드와 함께 배포하세요. + +```json title="src-tauri/tauri.conf.json" +{ + "bundle": { + "resources": ["bundles/**/*"] + } +} +``` + +전체 패킹 워크플로와 플래그는 [CLI 레퍼런스](/docs/guide/cli)와 [번들 소스](/docs/guide/bundle-sources)를 참고하세요. + +## 프런트엔드에서 업데이트 제어 + +`Remote`를 추가하면 OTA 다운로드를 활성화합니다. + +```rust title="src-tauri/src/lib.rs" +use wvb_tauri::{Config, Protocol, Remote, Source}; + +Config::new() + .source(Source::new().builtin_dir_fn(|app| Ok(app.path().resource_dir()?.join("bundles")))) + .protocol(Protocol::bundle("bundle")) + .remote(Remote::new("https://updates.example.com")); +``` + +`Http`로 HTTP 클라이언트를 조정하고(기본값: 요청 타임아웃 `120_000` ms), `.on_download`로 진행 상황을 관찰하세요. + +```rust +use wvb_tauri::{Http, Remote}; + +Remote::new("https://updates.example.com") + .http(Http::new().timeout(30_000).user_agent("my-app/1.0".into())) + .on_download(|downloaded, total, _name| { + if let Some(total) = total { + println!("{downloaded}/{total} bytes"); + } + }); +``` + +플러그인 커맨드는 `plugin:wvb-tauri|`으로 접근합니다. 인자는 JS 쪽에서 camelCase를 사용합니다(`bundle_name`은 `bundleName`이 됩니다). 핵심 OTA 흐름은 세 개의 updater 커맨드를 사용합니다. + +| 커맨드 | 인자 | 반환값 | +| -------------------- | ------------------------ | ------------------------------------------ | +| `updater_get_update` | `bundleName` | `BundleUpdateInfo` (사용 가능 여부 + 버전) | +| `updater_download` | `bundleName`, `version?` | `RemoteBundleInfo` (다운로드된 메타데이터) | +| `updater_install` | `bundleName`, `version` | `null` (다운로드된 버전을 활성화) | + +```ts title="src/update.ts" +import { invoke } from '@tauri-apps/api/core'; + +interface BundleUpdateInfo { + name: string; + version: string; + localVersion?: string; + isAvailable: boolean; +} + +interface RemoteBundleInfo { + name: string; + version: string; + integrity?: string; + signature?: string; +} + +export async function checkAndUpdate(bundleName: string) { + const update = await invoke('plugin:wvb-tauri|updater_get_update', { + bundleName, + }); + if (!update.isAvailable) return; + + const info = await invoke('plugin:wvb-tauri|updater_download', { bundleName }); + await invoke('plugin:wvb-tauri|updater_install', { + bundleName, + version: info.version, + }); + // 새 번들을 반영하도록 웹뷰를 다시 로드합니다 + window.location.reload(); +} +``` + +플러그인은 로컬 번들을 관리하는 `source_*` 커맨드와 원격 번들을 나열하고 스테이징하는 `remote_*` 커맨드도 제공합니다. + +| 커맨드 | 인자 | 반환값 | +| ----------------------- | ------------------------ | --------------------------------------------- | +| `source_list_bundles` | — | 로컬 번들 목록 | +| `source_load_version` | `bundleName` | 활성 로컬 버전(있는 경우) | +| `source_update_version` | `bundleName`, `version` | `null` (스테이징된 버전을 활성화) | +| `remote_list_bundles` | `channel?` | 원격 번들 목록 | +| `remote_get_info` | `bundleName`, `channel?` | 현재 원격 메타데이터 | +| `remote_download` | `bundleName`, `channel?` | `RemoteBundleInfo` (활성화하지 않고 스테이징) | + +`remote_download`는 버전을 활성화하지 않고 스테이징합니다. 이후 `source_update_version`으로 활성화하세요. + +```ts +import { invoke } from '@tauri-apps/api/core'; + +// 지금 원격 번들을 스테이징하고, 다음 실행 시 활성화합니다. +const info = await invoke<{ version: string }>('plugin:wvb-tauri|remote_download', { + bundleName: 'app', +}); +await invoke('plugin:wvb-tauri|source_update_version', { + bundleName: 'app', + version: info.version, +}); +``` + +전체 커맨드 집합(`source_*` 12개, `remote_*` 4개, `updater_*` 4개)은 [`packages/tauri/src/commands.rs`](https://github.com/webview-bundle/webview-bundle/blob/main/packages/tauri/src/commands.rs)에 정의되어 있습니다. 무결성 및 서명 검증이 다운로드 흐름에 어떻게 들어맞는지는 [원격 번들](/docs/guide/remote-bundles)을, 서버 쪽은 [원격 설정](/docs/config/remote)을 참고하세요. + + + `remote_*` 커맨드는 설정에 `Remote`가 필요하고, `updater_*` 커맨드는 추가로 `Updater`가 + 필요합니다. 해당 설정 없이 호출하면 `code` 필드가 `remote_not_initialized` 또는 + `updater_not_initialized`인 오류를 반환합니다. + + +프런트엔드에서 `code` 필드로 분기하세요. + +```ts +import { invoke } from '@tauri-apps/api/core'; + +try { + await invoke('plugin:wvb-tauri|updater_get_update', { bundleName: 'app' }); +} catch (err) { + const { code } = err as { message: string; code?: string }; + if (code === 'updater_not_initialized') { + // Updater가 설정되지 않음 — OTA 확인을 건너뜁니다 + } +} +``` + +## 서명 검증 및 무결성 고정 + +`SignatureVerifier`가 포함된 `Updater`를 추가하면 다운로드된 번들에 게시자 서명을 요구합니다. 검증기는 클로저로부터 지연 방식으로 만들어집니다. + +```rust title="src-tauri/src/lib.rs" +use wvb_tauri::{Config, Ed25519Verifier, IntegrityPolicy, Protocol, Remote, Source, Updater}; + +const PUBLIC_KEY_PEM: &str = include_str!("../keys/public.pem"); + +Config::new() + .source(Source::new()) + .protocol(Protocol::bundle("bundle")) + .remote(Remote::new("https://updates.example.com")) + .updater( + Updater::new() + .channel("stable") + .integrity_policy(IntegrityPolicy::Strict) + .signature_verifier(|| Ok(Ed25519Verifier::from_pem(PUBLIC_KEY_PEM)?.into())), + ); +``` + +`IntegrityPolicy`는 `Strict`(반드시 존재하고 일치해야 함), `Optional`(기본값 — 존재하면 검증), `None`(건너뜀) 중 하나입니다. 검증기 타입은 `EcdsaSecp256r1Verifier`, `EcdsaSecp384r1Verifier`, `Ed25519Verifier`, `RsaPkcs1V15Verifier`, `RsaPssVerifier`입니다. `Updater`에는 `Remote`가 필요하며, 없으면 업데이터가 만들어지지 않습니다. + +## Rust에서 플러그인 접근 + +`App`, `AppHandle`, `Window` 중 무엇에서든 `WebviewBundleExtra` 트레이트가 `webview_bundle()`(별칭 `wvb()`)을 추가하여 관리 상태를 반환합니다. `.source()`는 항상 사용할 수 있고, `.remote()`와 `.updater()`는 `Option`을 반환합니다. + +```rust +use wvb_tauri::WebviewBundleExtra; + +let wvb = app.wvb(); +let _source = wvb.source(); + +if let Some(_updater) = wvb.updater() { + // updater는 `.remote(...)`와 `.updater(...)`가 모두 설정된 경우에만 존재합니다 +} +``` + +이는 프런트엔드 커맨드가 다루는 것과 동일한 상태이므로, Rust 쪽과 프런트엔드 주도 업데이트 로직을 섞어서 사용할 수 있습니다. + +## Tauri 모바일 + +**iOS**에서는 builtin 번들이 실제 파일 시스템 리소스 디렉터리에 위치합니다. 데스크톱 설정 외에 추가 설정은 필요하지 않습니다. **Android**에서는 builtin 번들이 APK 안에 `asset://` 리소스로 배포되는데, 파일 시스템이 이를 직접 읽을 수 없습니다. 따라서 builtin 번들을 배포하는 앱은 Tauri 파일 시스템 플러그인도 등록해야 합니다. + +```rust title="src-tauri/src/lib.rs" +tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) + .plugin(wvb_tauri::init(/* ... */)); +``` + +그러면 플러그인이 첫 요청 시 APK에서 각 번들을 추출하여 앱 로컬 데이터에 캐시합니다. 원격 전용 앱(builtin 번들 없음)은 추가 Android 설정이 필요하지 않습니다. 플랫폼별 세부 사항은 [Android](/docs/guide/platforms/android) 및 [iOS](/docs/guide/platforms/ios) 가이드를, 현재 상태는 [플랫폼 지원](/docs/guide/platform-support)을 참고하세요. + +## 문제 해결 + +- **스킴에서 요청이 HTTP 500을 반환함** — 프로토콜 핸들러가 실패했습니다. 번들이 소스 디렉터리에 존재하는지, 그리고 호스트가 실제 번들 이름에 매핑되는지 확인하세요(`bundle://app.wvb/...` → 번들 `app`). +- **`remote_not_initialized` / `updater_not_initialized`** — `Config`에 `.remote(...)`(및 `.updater(...)`) 없이 `remote_*` 또는 `updater_*` 커맨드를 호출했습니다. `error.code`로 분기하세요. +- **ACL이 커맨드를 거부함** — 호출하는 창을 대상으로 하는 capability에 `wvb-tauri:default`(또는 특정 `wvb-tauri:allow-*` 권한)를 추가하세요. +- **런타임에 번들을 찾을 수 없음** — `bundles` 디렉터리가 `tauri.conf.json`의 resources에 있는지, 그리고 `Source`가 이를 올바르게 해석하는지 확인하세요. Android에서는 builtin 번들을 배포할 때 `tauri_plugin_fs::init()`이 등록되어 있는지 확인하세요. + +## 더 알아보기 + + + + + + + +``` diff --git a/content/docs/guide/protocol-handling.ko.mdx b/content/docs/guide/protocol-handling.ko.mdx new file mode 100644 index 0000000..42865f9 --- /dev/null +++ b/content/docs/guide/protocol-handling.ko.mdx @@ -0,0 +1,243 @@ +--- +title: 프로토콜 처리 +description: 웹뷰 요청이 bundle 프로토콜과 local 프로토콜을 거쳐 .wvb 번들 안의 파일로 매핑되는 방식. +--- + +프로토콜 핸들러는 웹뷰 요청을 네트워크의 바이트가 아니라 번들의 바이트로 바꿉니다. Webview Bundle은 두 가지 프로토콜을 제공합니다. + +- **bundle 프로토콜** — `.wvb` 소스에서 파일을 그대로 제공합니다. +- **local 프로토콜** — 번들러의 개발 서버로 요청을 전달하는 개발용 프록시로, 핫 리로드를 유지합니다. + +Rust 코어는 스킴을 등록하거나 검증하지 않습니다. 코어는 URI의 **호스트**와 **경로**만 읽으며, 스킴 자체는 읽지 않습니다. 스킴을 정하고 OS에 등록하는 일은 플랫폼 통합의 몫입니다. + +## bundle 프로토콜 + +요청은 URI의 두 부분으로 번들 소스에 대해 해석됩니다. 하나는 **번들 이름**(첫 `.`까지의 호스트)이고, 다른 하나는 **경로**(퍼센트 디코딩된 값)입니다. + +```text +app://app.wvb/index.html -> bundle "app", file "/index.html" +app://myapp.wvb/assets/logo.png -> bundle "myapp", file "/assets/logo.png" +``` + +경로 해석은 정적 파일 서버처럼 `index.html`을 채워 넣습니다. + +```text +/ -> /index.html # 끝에 슬래시가 있는 경우 +/about -> /about/index.html # 마지막 세그먼트에 "."이 없는 경우 +/a.js -> /a.js # 마지막 세그먼트에 "."이 있으면 그대로 제공 +``` + +라벨이 없는 호스트는 bundle-not-found 오류로 실패합니다. 번들 인덱스에 없는 경로는 **404 Not Found**를 반환합니다. + +### 메서드, 헤더, 응답 + +`GET`과 `HEAD`만 제공하며, 그 외의 메서드는 **405 Method Not Allowed**를 반환합니다. `HEAD`는 본문이 비어 있는 헤더를 반환합니다. + +제공되는 파일에 대해 핸들러는 인덱스에 저장된 해당 파일의 헤더를 재생한 뒤, `Content-Type`(인덱스 엔트리에서 가져옴)과 `Content-Length`(압축 해제된 크기)를 덮어씁니다. + +```text +GET app://app.wvb/assets/app.js +-> 200 OK + cache-control: public, max-age=31536000 # 인덱스에서 재생됨 + content-type: application/javascript # 인덱스 엔트리에서 가져옴 + content-length: 84213 # 압축 해제된 크기 +``` + + + 헤더는 번들을 빌드할 때 파일별로 저장됩니다. 따라서 빌드 시점에 설정한 캐싱 지시자와 그 밖의 응답 + 헤더가 제공되는 응답에까지 그대로 유지됩니다. + + +### 범위 요청 + +웹뷰는 미디어 탐색과 대용량 다운로드 재개를 위해 범위 요청에 의존합니다. `Range` 헤더가 있으면 `Accept-Ranges: bytes`와 `Content-Range`를 담아 **206 Partial Content**를 반환합니다. + +```text +GET app://app.wvb/media/clip.mp4 +Range: bytes=0-1023 +-> 206 Partial Content + accept-ranges: bytes + content-range: bytes 0-1023/5242880 + content-length: 1024 +``` + +- 반환되는 각 범위는 `1000 * 1024` 바이트(약 1 MB)로 제한되며, 이보다 큰 요청 범위는 잘립니다. +- 여러 범위는 `multipart/byteranges` 응답으로 반환됩니다. +- 충족할 수 없는 범위는 `Content-Range: bytes */`와 함께 **416 Range Not Satisfiable**를 반환합니다. + +```text +Range: bytes=99999999- +-> 416 Range Not Satisfiable + content-range: bytes */5242880 +``` + +범위는 압축 해제된 콘텐츠를 기준으로 계산됩니다. 따라서 오프셋은 빌드가 생성한 파일과 일치합니다. + +## local 프로토콜 + +개발용 프록시입니다. `.wvb` 파일을 읽는 대신, 호스트를 localhost 기준 URL로 매핑하고 요청을 전달합니다. 번들러가 핫 리로드되는 자산을 실시간으로 제공하는 동안, 커스텀 스킴은 프로덕션과 동일하게 유지됩니다. + +```text +app://myapp/api/data?foo=bar -> http://localhost:3000/api/data?foo=bar +``` + +호스트-URL 매핑을 제공하세요. Electron에서는 `localProtocol(scheme, { hosts })`를 전달합니다. + +```ts title="main.ts" +import { localProtocol, wvb } from '@wvb/electron'; + +const instance = wvb({ + protocols: [ + localProtocol('app', { + hosts: { 'myapp.wvb': 'http://localhost:3000' }, + }), + ], +}); +``` + +Tauri에서는 `Protocol::local`에 호스트 맵을 설정합니다. + +```rust title="src-tauri/src/lib.rs" +use wvb_tauri::{Config, Protocol}; + +Config::new().protocol( + Protocol::local("app").host("myapp.wvb", "http://localhost:3000"), +); +``` + +매핑이 없는 호스트로 들어온 요청은 resolve 오류로 실패합니다. 프록시는 응답을 캐시하며, 캐시된 응답을 재생하는 방식으로 업스트림의 `304 Not Modified`를 따릅니다. + + + 개발 중에는 local 프로토콜을 사용하고, 출시 빌드에는 bundle 프로토콜을 사용하세요. 두 프로토콜은 + 같은 스킴을 공유하므로, 웹 앱의 URL은 둘 사이에서 바뀌지 않습니다. + + +## 플랫폼별 스킴 + +Electron, Tauri, iOS는 실제 커스텀 스킴을 웹뷰에 등록합니다. 반면 Android는 `WebViewClient.shouldInterceptRequest`를 통해 일반 `https://.wvb` 요청을 가로채므로, 등록할 커스텀 스킴이 없습니다. 어느 경우든 번들 이름은 호스트 라벨에서, 경로는 URI 경로에서 가져옵니다. 예제는 `app`(Electron, iOS)과 `bundle`(Tauri)을 등록하지만, 이름은 원하는 대로 정할 수 있습니다. + + + + +`@wvb/electron`은 스킴을 privileged로 등록하고 핸들러를 연결합니다. `bundleProtocol(scheme)`을 `webviewBundle`에 전달하고, `whenProtocolRegistered()`를 await한 뒤 `://.wvb`를 로드하세요. + +```js title="main.cjs" +const path = require('path'); +const { app, BrowserWindow } = require('electron'); +const { bundleProtocol, wvb } = require('@wvb/electron'); + +const instance = wvb({ + source: { builtinDir: path.join(__dirname, 'bundles') }, + protocols: [bundleProtocol('app', { onError: e => console.error('[wvb]', e) })], +}); + +app.whenReady().then(async () => { + await instance.whenProtocolRegistered(); + const window = new BrowserWindow(); + await window.loadURL('app://app.wvb/index.html'); +}); +``` + +스킴은 다음의 기본 권한으로 등록됩니다(`bundleProtocol('app', { privileges })`로 프로토콜별로 재정의 가능). + +```ts +{ + standard: true, secure: true, bypassCSP: true, + allowServiceWorkers: true, supportFetchAPI: true, + corsEnabled: true, stream: false, codeCache: true, +} +``` + +권한과 렌더러 브리지에 대해서는 [Electron 가이드](/docs/guide/platforms/electron)를 참고하세요. + + + + +`wvb-tauri` 크레이트는 설정된 `Protocol`마다 비동기 URI 스킴 프로토콜을 등록합니다. `Protocol::bundle(scheme)`을 플러그인 설정에 전달한 뒤, 윈도우가 `://.wvb`를 가리키게 하세요. + +```rust title="src-tauri/src/lib.rs" +use wvb_tauri::{Config, Protocol, Source}; + +tauri::Builder::default() + .plugin(wvb_tauri::init( + Config::new() + .source(Source::new()) + .protocol(Protocol::bundle("bundle")), + )) + .run(tauri::generate_context!()) + .unwrap(); +``` + +`tauri.conf.json`에서 윈도우가 스킴을 가리키게 하세요. + +```json title="src-tauri/tauri.conf.json" +{ + "app": { + "windows": [{ "url": "bundle://app.wvb" }] + } +} +``` + +전체 플러그인 설정은 [Tauri 가이드](/docs/guide/platforms/tauri)를 참고하세요. + + + + +Android는 커스텀 스킴을 등록하지 않습니다. 통합은 `WebViewClient`에서 일반 `https://.wvb` 내비게이션을 가로챕니다. 동일한 호스트-경로 매핑이 적용되므로, `https://app.wvb/index.html`은 번들 `app`의 파일 `/index.html`로 해석됩니다. + +```kotlin +class BundleWebViewClient : WebViewClient() { + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest, + ): WebResourceResponse? { + // 요청을 Webview Bundle 핸들러에 넘깁니다. + return webviewBundle.handle(request) + } +} +``` + +동작하는 설정은 [Android 가이드](/docs/guide/platforms/android)를 참고하세요. + + + + +iOS는 `WKURLSchemeHandler`를 통해 `WKWebView` 설정에 실제 커스텀 스킴을 등록한 뒤, `://.wvb`를 로드합니다. 최소 배포 대상은 iOS 16입니다. + +```swift +let configuration = WKWebViewConfiguration() +configuration.setURLSchemeHandler(bundleSchemeHandler, forURLScheme: "app") + +let webView = WKWebView(frame: .zero, configuration: configuration) +webView.load(URLRequest(url: URL(string: "app://app.wvb/index.html")!)) +``` + +동작하는 설정은 [iOS 가이드](/docs/guide/platforms/ios)를 참고하세요. + + + + +## 관련 문서 + + + + + + + diff --git a/content/docs/guide/providers/aws.ko.mdx b/content/docs/guide/providers/aws.ko.mdx new file mode 100644 index 0000000..c50dd02 --- /dev/null +++ b/content/docs/guide/providers/aws.ko.mdx @@ -0,0 +1,210 @@ +--- +title: AWS 프로바이더 +description: S3, CloudFront, Lambda@Edge와 선택적 KMS 서명을 사용해 AWS에서 원격 Webview Bundle을 호스팅하고, 제공하고, 프로비저닝합니다. +--- + +번들을 직접 관리하는 AWS 계정에 게시하고 글로벌 CDN으로 제공합니다. 번들은 **S3**에 저장되고, **CloudFront**가 이를 캐싱하며, 두 개의 **Lambda@Edge** 함수가 원격 HTTP 계약을 S3 읽기로 변환합니다. 그리고 **KMS**로 각 번들에 선택적으로 서명할 수 있습니다. +이 구성 요소들이 구현하는 계약과 프로바이더를 선택하는 방법은 [원격 구축하기](/docs/guide/remote)를 참고하세요. + +## 지원 서비스 + +| Service | Role | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| S3 | 각 번들 객체와 현재 버전을 기록하는 `deployment.json` 포인터를 저장합니다. | +| CloudFront | S3 앞단의 CDN입니다. 배포기는 배포 후 캐시 무효화를 요청할 수 있습니다. | +| Lambda@Edge | 두 개의 함수입니다. origin-request는 URL을 번들 키로 재작성하고, origin-response는 S3 객체 메타데이터를 계약 응답 헤더로 변환합니다. | +| KMS | 선택 사항입니다. 각 번들의 무결성 문자열에 서명하여 클라이언트가 게시자를 검증할 수 있게 합니다. | + +## 패키지 + +| Package | Version | Role | +| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------- | +| `@wvb/remote-aws` | `0.1.0` | 게시 측입니다. `wvb.config.ts`에 사용할 `uploader`, `deployer`와 선택적 `signature`를 생성합니다. | +| `@wvb/remote-aws-provider` | `0.1.0` | Lambda@Edge 서버(Hono 앱)입니다. `./origin-request`와 `./origin-response` 하위 경로를 노출합니다. | +| `@wvb/remote-aws-provider-pulumi` | `0.1.0` | S3, CloudFront, 두 개의 Lambda@Edge 함수, IAM을 프로비저닝하는 Pulumi 컴포넌트입니다. | + + + + +```sh +npm install @wvb/remote-aws @wvb/remote-aws-provider @wvb/remote-aws-provider-pulumi +``` + + + + +```sh +pnpm add @wvb/remote-aws @wvb/remote-aws-provider @wvb/remote-aws-provider-pulumi +``` + + + + +```sh +yarn add @wvb/remote-aws @wvb/remote-aws-provider @wvb/remote-aws-provider-pulumi +``` + + + + +## 게시 측 설정 + +`awsRemote()`는 `{ uploader, deployer, signature? }`를 반환합니다. 이를 설정의 `remote` 블록에 펼쳐 넣으세요. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; +import { awsRemote } from '@wvb/remote-aws'; + +export default defineConfig({ + remote: { + endpoint: 'https://bundles.example.com', + ...awsRemote({ + bucket: 'my-app-bundles', + aws: { + region: 'us-east-1', + profile: 'my-app-deploy', + }, + }), + }, +}); +``` + +`remote` 스키마 전체는 [원격, 무결성, 서명 설정](/docs/config/remote)을 참고하세요. + +### uploader와 deployer 커스터마이징 + +`uploader`와 `deployer`는 각자의 옵션을 받습니다. uploader는 멀티파트 업로드로 S3에 스트리밍하고, 계약 값을 S3 객체 메타데이터(`webview-bundle-name`, `webview-bundle-version`, 그리고 존재할 경우 `webview-bundle-integrity`와 `webview-bundle-signature`)로 기록합니다. deployer는 `deployment.json`을 GET한 뒤 PUT하며, 배포 후 CloudFront를 무효화할 수 있습니다. + +```ts title="wvb.config.ts" +...awsRemote({ + bucket: 'my-app-bundles', + aws: { region: 'us-east-1' }, + uploader: { + // 기본값: bundles/${name}/${name}_${version}.wvb + key: (name, version) => `bundles/${name}/${version}/${name}_${version}.wvb`, + cacheControl: 'public, max-age=31536000, immutable', + contentType: 'application/webview-bundle', + }, + deployer: { + invalidation: { + distributionId: 'E1ABCDEF2GHIJK', + }, + }, +}), +``` + +번들이 이미 존재하고 `force`를 전달하지 않으면, uploader는 `HeadObject` 사전 확인을 실행하고 `BundleAlreadyUploadedError`를 던집니다. + +```ts +import { isBundleAlreadyUploadedError } from '@wvb/remote-aws'; + +try { + await uploader.upload({ bundle, bundleName, version }); +} catch (err) { + if (isBundleAlreadyUploadedError(err)) { + // 덮어쓰려면 --force로 다시 실행하세요. + } + throw err; +} +``` + +### KMS로 서명하기 + +각 번들의 무결성 문자열에 서명하려면 `signature`에 KMS 서명자 설정을 지정하세요. 서명자는 `MessageType: 'DIGEST'`로 KMS를 호출하고 base64 서명을 반환합니다. + +```ts title="wvb.config.ts" +import { awsRemote } from '@wvb/remote-aws'; + +...awsRemote({ + bucket: 'my-app-bundles', + aws: { region: 'us-east-1' }, + signature: { + keyId: 'arn:aws:kms:us-east-1:111122223333:key/abcd-1234', + algorithm: 'ECDSA_SHA_256', + }, +}), +``` + +서명자를 단독으로 만들려면 `awsKmsSignatureSigner({ keyId, algorithm })`를 사용하세요. 서명 함수를 반환합니다. 서명을 비활성화하려면 `signature: false`를 전달하세요. + + + 기본 uploader 키와 프로바이더가 기대하는 키는 일치하지 않습니다. uploader는 `bundles/{name}/{name} + _{version}.wvb`에 기록하지만, Lambda@Edge 프로바이더는 요청을 `bundles/ + {name}/{version}/{name}_{version}.wvb`로 재작성합니다. 즉 `{version}` 디렉터리가 하나 더 있습니다. + 엔드투엔드 AWS 구성에서는 위 예시처럼 프로바이더의 레이아웃을 만드는 커스텀 uploader `key`를 + 설정하거나, 일치하는 S3 레이아웃을 마련하세요. 기본값에 의존하기 전에 패키지 소스와 대조해 + 확인하세요. + + +## Lambda@Edge에서 제공하기 + +`@wvb/remote-aws-provider`는 계약을 두 개의 CloudFront 이벤트 핸들러로 분리합니다. 각각을 별도의 Lambda에 연결하세요. + +```ts title="origin-request.ts" +import { originRequest } from '@wvb/remote-aws-provider/origin-request'; + +export const handler = originRequest({ + bucketName: 'my-app-bundles', + region: 'us-east-1', + // allowOtherVersions: true, // GET /bundles/{name}/{version} 허용, 그렇지 않으면 403 +}); +``` + +```ts title="origin-response.ts" +import { originResponse } from '@wvb/remote-aws-provider/origin-response'; + +export const handler = originResponse(); +``` + +origin-request 핸들러는 `deployment.json`을 읽고, 버전을 해석하며(채널에서 기본 버전으로 폴백), origin URI를 번들 키로 재작성하고, 요청에 태그를 붙입니다. origin-response 핸들러는 S3의 `x-amz-meta-webview-bundle-*` 메타데이터를 `webview-bundle-*` 응답 헤더로 변환합니다. `allowOtherVersions: true`가 아니면 `GET /bundles/{name}/{version}`은 `403`을 반환합니다. + +## Pulumi로 프로비저닝하기 + +`WebviewBundleRemoteProvider`(토큰 `webview-bundle:aws:RemoteProvider`)는 S3 버킷과 정책, Lambda@Edge용 IAM 역할, `us-east-1`에 두 개의 Lambda@Edge 함수(Lambda@Edge에 필수), 그리고 두 함수가 모두 연결된 CloudFront 배포를 생성합니다. + +```ts title="index.ts" +import { WebviewBundleRemoteProvider } from '@wvb/remote-aws-provider-pulumi'; + +const provider = new WebviewBundleRemoteProvider('webview-bundle', {}); + +export const bucketName = provider.bucketName; +export const distributionDomainName = provider.cloudfrontDistributionDomainName; +export const distributionId = provider.cloudfrontDistributionId; +export const originRequestArn = provider.lambdaOriginRequestArn; +export const originResponseArn = provider.lambdaOriginResponseArn; +``` + +이 컴포넌트는 배포 시점에 Lambda 코드를 번들링하고 각 함수에 `bucketName`, `region`, `allowOtherVersions`를 주입합니다. 함수는 기본적으로 `nodejs22.x` 런타임을 사용합니다. 함수별 설정은 `lambdaOriginRequest` / `lambdaOriginResponse`로 재정의하세요. + +```ts title="index.ts" +new WebviewBundleRemoteProvider('webview-bundle', { + lambdaOriginRequest: { + runtime: 'nodejs22.x', + architecture: 'arm64', + memorySize: 256, + }, +}); +``` + +Lambda@Edge는 `us-east-1`을 요구합니다. Pulumi AWS 프로바이더를 해당 리전으로 설정하세요. + +```ts title="index.ts" +import * as aws from '@pulumi/aws'; + +const usEast1 = new aws.Provider('us-east-1', { region: 'us-east-1' }); +``` + +## 관련 문서 + + + + + diff --git a/content/docs/guide/providers/cloudflare.ko.mdx b/content/docs/guide/providers/cloudflare.ko.mdx new file mode 100644 index 0000000..e02e98e --- /dev/null +++ b/content/docs/guide/providers/cloudflare.ko.mdx @@ -0,0 +1,231 @@ +--- +title: Cloudflare 프로바이더 +description: R2, Workers KV, Cloudflare Workers를 사용해 Cloudflare에서 번들을 저장하고 배포하고 제공합니다. +--- + +Webview Bundle 원격을 Cloudflare 엣지에서 완전히 실행할 수 있습니다. 번들은 **R2**에, 버전 포인터는 **Workers KV**에 저장하고, **Cloudflare Worker**가 업데이터와 통신하는 HTTP 계약을 제공합니다. 관리할 서버 없이 전 세계에 낮은 지연으로 무선(OTA) 전달을 제공합니다. + +세 개의 패키지가 이 워크플로를 담당합니다. + +| Package | 역할 | 실행 위치 | +| ---------------------------------------- | --------------------------------------- | ------------------ | +| `@wvb/remote-cloudflare` | `wvb.config`용 업로더 + 디플로이어 | 로컬 머신 / CI | +| `@wvb/remote-cloudflare-provider` | 번들을 제공하는 Worker | Cloudflare Workers | +| `@wvb/remote-cloudflare-provider-pulumi` | 인프라를 프로비저닝하는 Pulumi 컴포넌트 | `pulumi up` | + +세 패키지 모두 `0.1.0`입니다. `wvb.config`에서 버전을 고정하세요. + +```sh +npm i -D @wvb/remote-cloudflare +``` + + + 원격이 처음이신가요? [원격 만들기](/docs/guide/remote)에서 계약과 로컬 테스트 루프를 먼저 + 살펴보세요. 전체 `wvb.config` 레퍼런스는 [원격, 무결성 및 서명 설정](/docs/config/remote)을 + 참고하세요. + + +## 각 구성 요소가 맞물리는 방식 + +각 역할은 하나의 Cloudflare 서비스에 매핑됩니다. + +- **R2**는 R2의 S3 호환 API를 통해 각 번들을 객체로 저장합니다. `@wvb/remote-cloudflare` 업로더는 `@wvb/remote-aws`의 S3 업로더를 R2로 향하게 하여 재사용합니다. +- **Workers KV**는 배포 포인터를 보관합니다. 번들마다(또는 번들과 채널마다) 키 하나를 두고, 그 값은 배포된 버전입니다. +- `@wvb/remote-cloudflare-provider`를 실행하는 **Cloudflare Worker**는 KV를 읽어 버전을 확인한 뒤, R2에서 일치하는 객체를 스트리밍합니다. + + + Cloudflare에는 기본 서명 기능이 없습니다. KMS에 해당하는 것이 없고, `@wvb/remote-cloudflare`도 + 서명자를 제공하지 않습니다. [서명](/docs/config/remote)이 필요하면 다른 곳에서 생성한 뒤, 결과로 + 나온 `signature` 문자열을 `wvb.config`를 통해 전달하세요. + + +## 업로더와 디플로이어 설정 + +`cloudflareRemote()`를 호출하고 그 결과를 `remote` 블록에 펼쳐 넣으세요. 이 함수는 `{ uploader, deployer }`를 반환합니다. 업로더는 번들을 R2에 쓰고, 디플로이어는 버전을 KV에 씁니다. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; +import { cloudflareRemote } from '@wvb/remote-cloudflare'; + +export default defineConfig({ + remote: { + endpoint: 'https://bundles.example.com', + bundleName: 'my-app', + ...cloudflareRemote({ + bucket: 'webview-bundle', + accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, + kvNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID!, + }), + }, +}); +``` + +| Option | Type | 설명 | +| --------------- | -------- | ------------------------------------------------------------------------------------- | +| `bucket` | `string` | 번들 객체를 저장하는 R2 버킷. | +| `accountId` | `string` | Cloudflare 계정 ID. R2 엔드포인트와 KV 대상을 설정합니다. | +| `kvNamespaceId` | `string` | 배포 포인터를 보관하는 Workers KV 네임스페이스. | +| `uploader` | object | 내부 S3 업로더에 대한 선택적 재정의(`key`, `contentType`, `metadata` 등). | +| `deployer` | object | KV 디플로이어에 대한 선택적 재정의(`key`, `expiration`, `expirationTtl`, `metadata`). | + +기본값: 업로더는 `region: 'auto'`와 엔드포인트 `https://.r2.cloudflarestorage.com`을 설정한 다음, `bundles//_.wvb` 키에 객체를 저장하고 `webview-bundle-*` 커스텀 메타데이터를 붙입니다. 디플로이어는 `` 키(채널 배포일 때는 `/`)에 버전을 KV로 씁니다. + +레이아웃이 다르면 둘 중 어느 쪽이든 재정의하세요. + +```ts title="wvb.config.ts" +...cloudflareRemote({ + bucket: 'webview-bundle', + accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, + kvNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID!, + uploader: { + key: (name, version) => `releases/${name}/${version}.wvb`, + cacheControl: 'public, max-age=31536000, immutable', + }, + deployer: { + key: (name, _version, channel) => (channel ? `${name}/${channel}` : name), + expirationTtl: 60 * 60 * 24, + }, +}), +``` + +R2에는 S3 자격 증명이 필요합니다. AWS와 동일한 방식으로 제공하세요. S3 클라이언트가 읽는 환경변수를 통해 전달합니다. + +```sh +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +``` + +설정을 마쳤으면 번들을 게시하세요. + +```sh +wvb pack ./dist +wvb upload --version 1.2.0 +wvb deploy --version 1.2.0 +``` + +`wvb upload`는 기본적으로 배포하지 않습니다. `wvb deploy`를 실행하거나(또는 `upload`에 `--deploy`를 전달하여) 해당 버전을 라이브 상태로 만드세요. + +```sh +wvb upload --version 1.2.0 --deploy +``` + +기본 포인터 대신 채널로 배포하려면 다음과 같이 합니다. + +```sh +wvb deploy --version 1.2.0 --channel beta +``` + +모든 플래그는 [CLI 레퍼런스](/docs/guide/cli)를 참고하세요. + +## Worker로 번들 제공하기 + +제공 측은 `@wvb/remote-cloudflare-provider`의 Hono 앱입니다. `wvbRemote()`로 생성한 뒤 Worker의 바인딩을 그 `Context`로 전달하세요. `KV` 바인딩은 `kv`가 되고, `BUCKET`(R2) 바인딩은 `r2`가 됩니다. + +```ts title="src/worker.ts" +import { wvbRemote } from '@wvb/remote-cloudflare-provider'; + +const remote = wvbRemote(); + +export interface Env { + KV: KVNamespace; + BUCKET: R2Bucket; +} + +export default { + fetch(request: Request, env: Env): Response | Promise { + return remote.fetch(request, { kv: env.KV, r2: env.BUCKET }); + }, +}; +``` + +`wvbRemote()`는 옵션 하나 `allowOtherVersions`(기본값 `false`)를 받습니다. `false`일 때 `GET /bundles/:name/:version`은 `403`을 반환합니다. 배포된 버전 외의 버전도 클라이언트가 다운로드하게 하려면 `true`로 설정하세요. + +```ts title="src/worker.ts" +const remote = wvbRemote({ allowOtherVersions: true }); +``` + +Worker는 KV에서 버전을 확인하고, R2에서 `bundles//_.wvb`를 읽어, 필수 헤더인 `Webview-Bundle-Name`과 `Webview-Bundle-Version`(객체 메타데이터에 있으면 `Webview-Bundle-Integrity`와 `Webview-Bundle-Signature`도 함께)을 붙여 반환합니다. + +이름이 `Context`와 맞도록 `wrangler.jsonc`에서 `KV`와 `BUCKET`을 바인딩하세요. + +```json title="wrangler.jsonc" +{ + "name": "webview-bundle", + "main": "src/worker.ts", + "compatibility_date": "2025-01-01", + "kv_namespaces": [{ "binding": "KV", "id": "" }], + "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "webview-bundle" }] +} +``` + +Worker를 배포하세요. + +```sh +npx wrangler deploy +``` + +라이브 엔드포인트에 대해 계약을 검증하세요. + +```sh +curl https://bundles.example.com/bundles +# [{ "name": "my-app", "version": "1.2.0" }] + +curl -I https://bundles.example.com/bundles/my-app +# Webview-Bundle-Name: my-app +# Webview-Bundle-Version: 1.2.0 +# Content-Type: application/webview-bundle +``` + +## Pulumi로 프로비저닝하기 + +`@wvb/remote-cloudflare-provider-pulumi`는 전체 스택을 하나의 컴포넌트 리소스로 프로비저닝합니다. R2 버킷, Workers KV 네임스페이스, Worker(버전과 배포 포함)를 만들고, `Context`가 기대하는 바인딩 `BUCKET`(R2)과 `KV`로 서로 연결합니다. + +```ts title="index.ts" +import { WebviewBundleRemoteProvider } from '@wvb/remote-cloudflare-provider-pulumi'; + +const provider = new WebviewBundleRemoteProvider('webview-bundle', { + accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, + // Worker를 점진적으로 롤아웃합니다. 기본값은 100입니다. + workerDeploymentPercentage: 25, +}); + +export const bucketName = provider.bucketName; +export const kvNamespaceId = provider.kvNamespaceId; +export const workerId = provider.workerId; +export const workerVersionId = provider.workerVersionId; +export const workerDeploymentId = provider.workerDeploymentId; +``` + +이 컴포넌트는 Pulumi 리소스 토큰 `webview-bundle:cloudflare:RemoteProvider`를 등록합니다. `workerDeploymentPercentage`는 새 Worker 버전이 받는 트래픽 비율입니다(기본값 `100`). + +```sh +pulumi up +pulumi stack output kvNamespaceId +``` + +내보낸 `bucketName`과 `kvNamespaceId`를 위의 `cloudflareRemote()`에 넣은 다음, CLI로 게시하세요. + +## 다음 단계 + + + + + + + diff --git a/content/docs/guide/providers/local.ko.mdx b/content/docs/guide/providers/local.ko.mdx new file mode 100644 index 0000000..ddb0418 --- /dev/null +++ b/content/docs/guide/providers/local.ko.mdx @@ -0,0 +1,242 @@ +--- +title: 로컬 프로바이더 +description: 클라우드 프로바이더를 도입하기 전에, 내 컴퓨터에서 파일시스템 기반 원격 서버를 실행해 업로드, 배포, 무선(OTA) 업데이트로 이어지는 전체 흐름을 테스트하세요. +--- + +내 컴퓨터에서 실행되는 파일시스템 기반 원격 서버입니다. 클라우드 프로바이더와 동일한 HTTP 규약을 구현하므로, 인프라 없이도 전체 수명 주기(패킹, 업로드, 배포, 무선(OTA) 업데이트)를 실행할 수 있습니다. 이 서버로 개발한 다음, 프로덕션에서는 [클라우드 프로바이더](/docs/guide/remote)로 전환하세요. + + + 개발과 테스트 전용입니다. 번들을 한 컴퓨터에 일반 파일로 저장하며, `wvb remote local`은 + `localhost`에 바인딩됩니다. 프로덕션 트래픽에는 [AWS](/docs/guide/providers/aws) 또는 + [Cloudflare](/docs/guide/providers/cloudflare) 프로바이더를 사용하세요. + + +## 두 개의 패키지 + +둘 다 `0.0.0` 버전입니다. + +| Package | Role | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `@wvb/remote-local` | 게시(publish) 측 클라이언트입니다. `localRemote()`는 `wvb.config`에 연결하는 `uploader`와 `deployer`를 생성합니다. | +| `@wvb/remote-local-provider` | 서버입니다. 저장된 번들을 HTTP로 제공하는 [Hono](https://hono.dev) 앱입니다. CLI가 대신 실행해 줍니다. | + +이 분리 구조는 모든 백엔드에서 동일합니다. 일반 패키지는 번들을 푸시하고, `-provider` 패키지는 번들을 제공합니다. 공유 규약은 [원격 서버 만들기](/docs/guide/remote)를 참고하세요. + +## 게시 측 설정하기 + +`{ uploader, deployer }`를 반환하는 `localRemote()`를 `remote` 블록에 펼쳐 넣으세요. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; +import { localRemote } from '@wvb/remote-local'; + +export default defineConfig({ + remote: { + endpoint: 'http://localhost:4313', + bundleName: 'app', + ...localRemote(), + }, +}); +``` + +`localRemote()`는 선택적 필드 하나를 받습니다. + +| Option | Type | Default | Description | +| --------- | -------- | -------------- | ---------------------------------------------------- | +| `baseDir` | `string` | `~/.wvb/local` | 번들과 배포 상태를 디스크에 저장하는 디렉터리입니다. | + +`baseDir`을 설정하면 프로젝트의 번들을 기본 저장소와 분리할 수 있습니다. + +```ts title="wvb.config.ts" +import os from 'node:os'; +import path from 'node:path'; +import { defineConfig } from '@wvb/config'; +import { localRemote } from '@wvb/remote-local'; + +export default defineConfig({ + remote: { + endpoint: 'http://localhost:4313', + bundleName: 'app', + ...localRemote({ + baseDir: path.join(os.homedir(), '.wvb', 'my-app'), + }), + }, +}); +``` + +`remote` 블록 전체(`endpoint`, `bundleName`, `packBeforeUpload`, 무결성, 서명)는 [원격 설정 레퍼런스](/docs/config/remote)를 참고하세요. + +## 디스크 레이아웃 + +모든 것은 `{baseDir}/bundles` 아래에 기록되며, 번들 이름마다 디렉터리 하나가 만들어집니다. + +```text +{baseDir}/ +└── bundles/ + └── {name}/ + ├── {name}_{version}.wvb # 번들 바이너리 (업로드된 버전마다 하나) + ├── {name}_{version}.json # 버전별 메타데이터: { integrity?, signature? } + └── deployment.json # 채널별로 어떤 버전이 현재 버전인지 기록 +``` + +번들 `app`의 `1.1.0`을 기본 저장소에 업로드하고 배포한 뒤의 모습입니다. + +```text +~/.wvb/local/ +└── bundles/ + └── app/ + ├── app_1.1.0.wvb + ├── app_1.1.0.json + └── deployment.json +``` + +`deployment.json`은 현재 버전을 기록합니다. 채널 없이 배포하면 기본 `version`을 설정하고, 채널과 함께 배포하면 `channels[channel]`을 설정합니다. 따라서 채널마다 서로 다른 버전을 가리킬 수 있습니다. + +```json title="deployment.json" +{ + "name": "app", + "version": "1.1.0", + "channels": { + "beta": "1.2.0" + } +} +``` + +## 서버 실행하기 + +`@wvb/remote-local-provider`는 Hono 앱을 빌드하기만 하고 포트에 바인딩하지는 않습니다. `wvb remote local`이 그 앱을 Node HTTP 서버로 제공합니다. + +```sh +wvb remote local +``` + +기본적으로 `~/.wvb/local`을 `http://localhost:4313`에서 제공합니다. 플래그는 다음과 같습니다. + +| Flag | Default | Description | +| ------------------------ | -------------- | ------------------------------------------------------------------------- | +| `--base-dir` | `~/.wvb/local` | 제공할 디렉터리입니다. `localRemote()`에 설정한 `baseDir`과 일치시키세요. | +| `--port`, `-P` | `4313` | 수신 대기할 포트입니다(`PORT`도 읽습니다). | +| `--hostname`, `-H` | `localhost` | 바인딩할 호스트입니다(`HOSTNAME`도 읽습니다). | +| `--allow-other-versions` | `false` | 배포된 버전 외의 버전 다운로드를 허용합니다. | +| `--silent` | `false` | 서버 로깅을 억제합니다. | + +설정에 지정한 것과 같은 경로를 전달하세요. + +```sh +wvb remote local --base-dir ~/.wvb/my-app --port 4313 +``` + +`remote` 명령어 그룹 전체(`remote list`, 별칭 `remote ls`)는 [CLI 레퍼런스](/docs/guide/cli)를 참고하세요. + +### allowOtherVersions + +기본적으로 서버는 배포된 버전만 제공합니다. `GET /bundles/{name}/{version}`은 `403 Forbidden`을 반환하는데, 이는 클라우드 프로바이더와 동일하며 클라이언트를 특정 버전에 고정된 상태로 유지합니다. + +```sh +curl -i http://localhost:4313/bundles/app/1.0.0 +# HTTP/1.1 403 Forbidden +``` + +이 옵션을 켜면 버전별 경로가 열립니다. 롤백을 테스트하거나 이전 번들을 살펴볼 때 유용합니다. + +```sh +wvb remote local --allow-other-versions +``` + +```sh +curl -i http://localhost:4313/bundles/app/1.0.0 +# HTTP/1.1 200 OK +# Webview-Bundle-Name: app +# Webview-Bundle-Version: 1.0.0 +``` + +프로바이더 앱을 직접 빌드할 때는 `allowOtherVersions: true`를 전달하세요. + +```ts title="server.ts" +import { wvbRemote } from '@wvb/remote-local-provider'; + +const app = wvbRemote({ + baseDir: '~/.wvb/local', + allowOtherVersions: true, +}); +``` + + + `allowOtherVersions`를 비활성화하면 배포된 버전만 `GET /bundles/{name}`으로 접근할 수 있습니다. + 버전별 경로는 이 옵션을 켜기 전까지 `403` 뒤에 숨겨진 상태로 유지됩니다. + + +## 전체 흐름 따라가기 + +로컬 서버를 상대로 OTA 업데이트를 처음부터 끝까지 테스트해 봅니다. + + + `wvb upload`는 기본적으로 배포하지 않습니다. `--deploy`를 전달하지 않으면 `false`입니다. `wvb + upload --deploy`로 업로드와 현재 버전 지정을 한 번에 하거나, `wvb deploy`를 따로 실행하세요. + + +위에서 보여준 대로 `localRemote()`를 연결한 다음, 패킹하고 업로드하고 배포하세요. `packBeforeUpload`가 기본값이면 `wvb upload`가 대신 패킹하므로, 명시적인 `wvb pack`은 선택 사항입니다. + +```sh +wvb pack ./dist +wvb upload --version 1.1.0 --deploy +``` + +같은 저장소를 상대로 서버를 시작하고, 번들이 목록에 나오는지 확인하세요. + +```sh +wvb remote local +``` + +```sh +curl http://localhost:4313/bundles +# [{"name":"app","version":"1.1.0"}] +``` + +```sh +curl -I http://localhost:4313/bundles/app +# Webview-Bundle-Name: app +# Webview-Bundle-Version: 1.1.0 +# Content-Type: application/webview-bundle +``` + +앱의 업데이터가 `http://localhost:4313`을 가리키게 하세요. 업데이터는 `GET /bundles/{name}`을 호출하고, 번들을 검증한 뒤, `remote` 소스에 설치합니다. 원격 대상은 플랫폼 통합에서 설정하세요. [Electron](/docs/guide/platforms/electron), [Tauri](/docs/guide/platforms/tauri), [Android](/docs/guide/platforms/android), [iOS](/docs/guide/platforms/ios) 가이드를 참고하세요. + +버전을 올리고 다시 배포하면 업데이트가 게시됩니다. 다음 업데이터 확인 시점에 이를 가져갑니다. + +```sh +wvb upload --version 1.2.0 --deploy +``` + +`--channel`을 전달하면 기본값 대신 채널로 배포합니다. + +```sh +wvb deploy --version 1.2.0 --channel beta +curl 'http://localhost:4313/bundles?channel=beta' +# [{"name":"app","version":"1.2.0"}] +``` + +## 다음으로 볼 내용 + + + + + + + diff --git a/content/docs/guide/providers/meta.ko.json b/content/docs/guide/providers/meta.ko.json new file mode 100644 index 0000000..087d8ac --- /dev/null +++ b/content/docs/guide/providers/meta.ko.json @@ -0,0 +1,4 @@ +{ + "title": "프로바이더", + "pages": ["local", "aws", "cloudflare"] +} diff --git a/content/docs/guide/remote-bundles.ko.mdx b/content/docs/guide/remote-bundles.ko.mdx new file mode 100644 index 0000000..276bfb9 --- /dev/null +++ b/content/docs/guide/remote-bundles.ko.mdx @@ -0,0 +1,259 @@ +--- +title: 원격 번들과 업데이트 +description: 무선(OTA) 업데이트가 동작하는 방식 — 업데이트 라이프사이클, 업데이터 모델, HTTP 규약, 그리고 무결성과 서명이 각 번들을 보호하는 원리. +--- + +Webview Bundle은 새 번들을 무선(OTA)으로 다운로드합니다. 따라서 네이티브 앱스토어 배포 없이도 업데이트된 앱 코드를 배포할 수 있습니다. 이 페이지에서는 업데이트 라이프사이클, 언어에 종속되지 않는 업데이터 모델, 모든 원격 서버가 구현하는 HTTP 규약, 그리고 무결성과 서명이 각 번들의 신뢰성을 지키는 방법을 다룹니다. 서버를 구축하려면 [원격 서버 구축하기](/docs/guide/remote)를 참고하세요. 언어별 업데이터 API는 [Node](/docs/references/node)와 [Deno](/docs/references/deno) 레퍼런스를 참고하세요. + +## 업데이트 라이프사이클 + +번들은 빌드 산출물에서 기기까지 다섯 단계를 거칩니다. 앞의 세 단계는 사용자의 머신이나 CI에서 실행되고, 마지막 두 단계는 업데이터가 앱 내부에서 구동합니다. + +![개발자 머신이 번들을 원격 서버로 업로드하고 배포하면, 기기가 업데이트를 확인하고 다운로드하고 검증한 뒤 설치합니다.](https://static.wvb.dev/diagrams/update-lifecycle.svg) + +1. 빌드 산출물을 `.wvb` 아카이브로 **패킹**합니다. +2. 해당 아카이브를 **업로드**합니다. 무결성 해시와 서명을 함께 첨부할 수도 있습니다. +3. 업로드한 버전을 **배포**하면 해당 채널의 클라이언트가 이를 보기 시작합니다. +4. 기기에서 업데이터가 새 버전을 **확인**한 뒤 **다운로드**합니다. +5. 업데이터가 다운로드를 **검증**하고 이를 `remote` 소스에 **설치**합니다. + +1~3단계는 CLI 명령어입니다. + +```sh +wvb pack # 빌드 산출물 -> .wvb/ +wvb upload --version 1.2.0 # 서버로 푸시 (배포하지 않음) +wvb deploy --version 1.2.0 # 1.2.0을 현재 버전으로 지정 +``` + +`wvb upload --deploy`(기본값 `false`)는 업로드와 배포를 한 번에 처리합니다. 모든 플래그는 [CLI 레퍼런스](/docs/guide/cli)를 참고하세요. 4~5단계는 업데이터이며, 다음에서 다룹니다. + +## 업데이터 모델 + +업데이터는 [번들 소스](/docs/guide/bundle-sources)를 원격 엔드포인트에 연결하고 세 가지 작업을 제공합니다. 이 작업들은 분리되어 있어, 기기가 언제 바이트를 가져올지와 언제 버전을 전환할지를 정확히 제어할 수 있습니다. + +- **`getUpdate`** — 확인만 합니다. 서버에 현재 배포된 버전을 요청하고 이를 로컬 버전과 비교합니다. 로컬 버전이 없거나 로컬 버전이 배포된 버전과 다르면 `isAvailable`을 `true`로 보고합니다. 아무것도 다운로드하지 않습니다. +- **`download`** — 번들을 가져와 무결성 및 서명 정책에 따라 검증하고 `remote` 소스에 스테이징합니다. 새 버전을 활성화하지는 **않습니다**. +- **`install`** — 스테이징된 번들을 디스크에서 다시 검증하고, 현재 버전으로 활성화하고, 이전 디스크립터를 언로드하고, 더 이상 유지하지 않는 버전을 정리합니다. + +이렇게 분리하면 백그라운드에서 다운로드하고 다음 앱 실행 시 설치할 수 있습니다. + + + `builtin` 소스 — 앱 내부에 포함되어 배포되는 번들 — 는 항상 폴백으로 남아 있습니다. 버전이 + 설치되면 `remote` 소스가 우선하므로, 다운로드된 업데이트가 builtin 번들보다 우선합니다. 원격 + 버전이 없으면 앱은 builtin으로 폴백합니다. [번들 소스](/docs/guide/bundle-sources)를 참고하세요. + + +Rust 코어 흐름입니다. 플랫폼 래퍼는 이를 일대일로 그대로 따릅니다. + +```rust +use std::sync::Arc; +use wvb::updater::Updater; + +let updater = Updater::new(source, remote, None); // None -> UpdaterConfig::default() + +let update = updater.get_update("app").await?; +if update.is_available { + // download는 가져오고, 검증하고, 스테이징하지만 활성화하지는 않음 + let info = updater.download("app", None).await?; + // install은 다시 검증하고, 활성화하고, 이전 버전을 정리함 + updater.install("app", info.version).await?; + // 새 버전을 제공하도록 웹뷰를 리로드 +} +``` + +`get_update`는 `BundleUpdateInfo`를 반환합니다. + +```rust +pub struct BundleUpdateInfo { + pub name: String, + pub version: String, + pub local_version: Option, + pub is_available: bool, + pub etag: Option, + pub integrity: Option, + pub signature: Option, + pub last_modified: Option, +} +``` + +`download`에 `Some(version)`을 전달하면 현재 버전 대신 특정 버전을 가져옵니다. + +```rust +// 현재 배포된 버전을 가져옴 +let info = updater.download("app", None).await?; +// 정확한 버전을 가져옴 (GET /bundles/{name}/{version}) +let info = updater.download("app", Some("1.1.0".into())).await?; +``` + +플랫폼 가이드에는 각 대상에 맞는 관용적인 호출 방법이 나와 있습니다. +[Electron](/docs/guide/platforms/electron), [Tauri](/docs/guide/platforms/tauri), +[Android](/docs/guide/platforms/android), [iOS](/docs/guide/platforms/ios), +[Deno Desktop](/docs/guide/platforms/deno)을 참고하세요. + +## 원격 HTTP 규약 + +Webview Bundle 서버는 네 개의 엔드포인트를 구현합니다. 프로바이더 패키지(local, AWS, Cloudflare)가 이 명세를 구현하며, 이에 맞춰 직접 구현할 수도 있습니다. + +| 메서드와 경로 | 목적 | +| ------------------------------- | ------------------------------------------- | +| `GET /bundles` | 배포된 번들 목록: `[{ "name", "version" }]` | +| `HEAD /bundles/{name}` | 현재 버전의 메타데이터 (헤더만, 본문 없음) | +| `GET /bundles/{name}` | 현재 버전 다운로드 | +| `GET /bundles/{name}/{version}` | 특정 버전 다운로드 | + +`GET /bundles`, `HEAD /bundles/{name}`, `GET /bundles/{name}`은 선택적인 `?channel=` 쿼리를 받습니다. `GET /bundles/{name}/{version}`은 받지 않습니다. 메타데이터는 응답 헤더로 전달됩니다. + +| 헤더 | 필수 여부 | 의미 | +| -------------------------- | --------- | ------------- | +| `Webview-Bundle-Name` | 예 | 번들 이름 | +| `Webview-Bundle-Version` | 예 | 버전 | +| `Webview-Bundle-Integrity` | 아니오 | 무결성 문자열 | +| `Webview-Bundle-Signature` | 아니오 | 서명 | +| `ETag`, `Last-Modified` | 아니오 | 표준 검증자 | + +다운로드 응답은 다음과 같습니다. + +```sh +$ curl -sI http://localhost:4313/bundles/app +HTTP/1.1 200 OK +Content-Type: application/webview-bundle +Webview-Bundle-Name: app +Webview-Bundle-Version: 1.2.0 +Webview-Bundle-Integrity: sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= +ETag: "abc123" +``` + +목록 엔드포인트는 `application/json`을 반환합니다. + +```json +[ + { "name": "app", "version": "1.2.0" }, + { "name": "admin", "version": "0.4.1" } +] +``` + +번들이나 버전이 없으면 `404`를 반환합니다. 서버가 허용하지 않는 경우, 배포되지 않은 특정 버전을 요청하면 `403`을 반환합니다. 필수 헤더가 없거나 2xx가 아닌 응답이 오면 클라이언트는 다운로드를 거부합니다. 기본 요청 타임아웃은 120초입니다. 실제 서버 예제와 로컬 테스트는 [원격 서버 구축하기](/docs/guide/remote)를 참고하세요. + +## 무결성 + +무결성 해시를 사용하면 기기가 다운로드한 바이트가 게시한 바이트와 정확히 일치하는지 확인할 수 있습니다. Webview Bundle은 **SHA-2**를 사용합니다. `sha256`(기본값), `sha384`, `sha512`를 지원하며, `":"` 형식으로 직렬화됩니다. + +```text +sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= +``` + +게시자는 업로드 시점에 이 문자열을 첨부하고([원격 설정](/docs/config/remote)), 서버는 이를 `Webview-Bundle-Integrity`로 반환하며, 업데이터는 다운로드 시 **무결성 정책**에 따라 이를 적용합니다. + +| 정책 | 동작 | +| ---------- | ------------------------------------------------------------- | +| `Strict` | 무결성 값이 반드시 존재하고 일치해야 하며, 그렇지 않으면 거부 | +| `Optional` | **기본값** — 존재하면 검증하고, 첨부되지 않았으면 허용 | +| `None` | 무결성 검사를 완전히 건너뜀 | + +```rust +use wvb::integrity::IntegrityPolicy; +use wvb::updater::UpdaterConfig; + +let config = UpdaterConfig::new() + .integrity_policy(IntegrityPolicy::Strict); +``` + +## 서명 + +무결성이 바이트가 손상되지 않았음을 증명한다면, 서명은 _누가_ 게시했는지를 증명합니다. 서명은 원본 번들이 아니라 **무결성 문자열 바이트**(`":"`)를 대상으로 합니다. 따라서 서명 검증에는 무결성 값이 반드시 존재해야 합니다. 공개 키를 제공하면, 업데이터는 서명이 검증되지 않는 모든 다운로드를 거부합니다. + +검증 알고리즘은 다음과 같습니다. + +- **ECDSA** — 곡선 P-256(secp256r1) 또는 P-384(secp384r1) +- **Ed25519** +- **RSA** — PKCS#1 v1.5 또는 PSS 패딩, SHA-256으로 해싱 + +공개 키 형식은 다음과 같습니다. + +| 형식 | 로더 | 적용 대상 | +| ---------------- | ------------------------- | ------------ | +| SPKI PEM | `from_public_key_pem` | 전체 | +| SPKI DER | `from_public_key_der` | 전체 | +| PKCS#1 PEM/DER | `from_pkcs1_pem` / `_der` | RSA 전용 | +| SEC1 point bytes | `from_sec1_bytes` | ECDSA 전용 | +| Raw 32-byte key | `from_public_key_bytes` | Ed25519 전용 | + +일치하는 무결성 해시와 유효한 Ed25519 서명을 모두 요구하는 업데이터입니다. + +```rust +use wvb::integrity::IntegrityPolicy; +use wvb::signature::{Ed25519Verifier, SignatureVerifier}; +use wvb::updater::{Updater, UpdaterConfig}; +use std::sync::Arc; + +let verifier = SignatureVerifier::Ed25519(Arc::new( + Ed25519Verifier::from_public_key_pem(PUBLIC_KEY_PEM)?, +)); +let config = UpdaterConfig::new() + .integrity_policy(IntegrityPolicy::Strict) + .signature_verifier(verifier); +let updater = Updater::new(source, remote, Some(config)); +``` + +SPKI PEM에서 로드한 ECDSA P-256입니다. + +```rust +use wvb::signature::{EcdsaSecp256r1Verifier, SignatureVerifier}; +use std::sync::Arc; + +let verifier = SignatureVerifier::EcdsaSecp256r1(Arc::new( + EcdsaSecp256r1Verifier::from_public_key_pem(PUBLIC_KEY_PEM)?, +)); +``` + +게시 측 대응 항목 — 어떤 알고리즘으로 서명할지와 개인 키 형식 — 은 [원격 설정](/docs/config/remote)에서 지정합니다. + +## 채널 + +채널은 단계적 롤아웃을 위해 서로 다른 버전을 서로 다른 대상에게 라우팅합니다. 채널에 배포하면 해당 채널을 요청하는 클라이언트만 이를 수신합니다. 채널이 없는 클라이언트는 기본 배포를 받습니다. + +```sh +wvb upload --version 1.2.0 --deploy --channel beta # beta 클라이언트만 1.2.0을 봄 +wvb deploy --version 1.2.0 # 나중에 기본 채널로 승격 +``` + +기기에서 업데이터는 설정에 채널을 담아 `?channel=`로 전달합니다. + +```rust +let config = UpdaterConfig::new().channel("beta"); +let updater = Updater::new(source, remote, Some(config)); +// 이제 get_update / download는 ?channel=beta를 요청함 +``` + +채널은 자유 형식 문자열입니다. 하드코딩된 기본 이름은 없으며, 채널이 설정되지 않으면 쿼리 파라미터가 생략됩니다. + +## 다음으로 볼 내용 + + + + + + + + + diff --git a/content/docs/guide/remote.ko.mdx b/content/docs/guide/remote.ko.mdx new file mode 100644 index 0000000..3e356d3 --- /dev/null +++ b/content/docs/guide/remote.ko.mdx @@ -0,0 +1,235 @@ +--- +title: 원격 서버 구축하기 +description: 무선(OTA) 번들 전송을 위한 업데이트 서버를 세우고, 전체 흐름을 로컬에서 실행한 뒤 프로바이더를 선택하세요. +--- + +원격 서버는 작고 고정된 규약에 따라 앱의 업데이터에 번들을 제공하는 HTTP 서버입니다. [원격 번들](/docs/guide/remote-bundles)의 서버 측 짝에 해당합니다. 원격 서버를 만들고, 클라우드 계정 없이 로컬에서 배포와 업데이트 전체 흐름을 실행한 다음, 프로덕션용 프로바이더를 고르세요. + +Webview Bundle은 `local`, `aws`, `cloudflare` 세 가지 백엔드용 원격 서버를 제공합니다. 각 백엔드는 역할에 따라 세 개의 패키지로 나뉩니다. 작업에 필요한 것만 사용하세요. + +## 패키지 역할 + +모든 백엔드는 최대 세 개의 패키지를 가집니다. + +| 역할 | 패키지 | 하는 일 | +| --------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| 설정 클라이언트 | `@wvb/remote-` | `{ uploader, deployer }`를 만드는 `wvb.config` 플러그인입니다. 퍼블리셔의 머신이나 CI에서 실행되어 번들을 푸시하고 배포합니다. | +| HTTP 서버 | `@wvb/remote--provider` | HTTP 규약을 구현하고 클라이언트에 번들을 제공하는 서버입니다. | +| Pulumi IaC | `@wvb/remote--provider-pulumi` | 클라우드 리소스를 프로비저닝하고 프로바이더를 배포하는 코드형 인프라입니다. | + +구체적으로, 백엔드별로 정리하면 다음과 같습니다. + +| 백엔드 | 설정 클라이언트 | HTTP 서버 | Pulumi IaC | +| ------------ | ------------------------ | --------------------------------- | ---------------------------------------- | +| `local` | `@wvb/remote-local` | `@wvb/remote-local-provider` | — | +| `aws` | `@wvb/remote-aws` | `@wvb/remote-aws-provider` | `@wvb/remote-aws-provider-pulumi` | +| `cloudflare` | `@wvb/remote-cloudflare` | `@wvb/remote-cloudflare-provider` | `@wvb/remote-cloudflare-provider-pulumi` | + +설정 클라이언트는 `wvb.config.ts`의 `remote` 블록에 연결하는 대상입니다. `uploader`는 패킹된 `.wvb`를 스토리지에 쓰고, `deployer`는 클라이언트가 받아야 할 버전을 지정합니다. 그 결과를 `remote`에 펼쳐 넣으세요. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; +import { awsRemote } from '@wvb/remote-aws'; + +export default defineConfig({ + remote: { + endpoint: 'https://cdn.example.com', + bundleName: 'app', + ...awsRemote({ bucket: 'my-bundles', aws: { region: 'us-east-1' } }), + }, +}); +``` + +퍼블리시 측 무결성과 서명 옵션은 `uploader`/`deployer`와 함께 위치합니다. [원격, 무결성 및 서명 설정](/docs/config/remote)을 참고하세요. + +## HTTP 규약 + +모든 프로바이더는 동일한 네 개의 엔드포인트를 구현합니다. 따라서 클라이언트는 뒤에 어떤 백엔드가 있는지 신경 쓰지 않습니다. + +| 메서드와 경로 | 목적 | 특이 사항 | +| ------------------------------- | ------------------------------ | -------------------------------------------------------- | +| `GET /bundles` | 배포된 번들 목록 조회 | `200` JSON `[{ "name", "version" }]`; 선택적 `?channel=` | +| `HEAD /bundles/{name}` | 현재 버전의 메타데이터(헤더만) | 배포되지 않았으면 `404` | +| `GET /bundles/{name}` | 현재 버전 다운로드 | 배포되지 않았으면 `404` | +| `GET /bundles/{name}/{version}` | 특정 버전 다운로드 | 다른 버전 허용이 아니면 `403` | + +필수 응답 헤더는 `Webview-Bundle-Name`과 `Webview-Bundle-Version`입니다. 선택 헤더는 `Webview-Bundle-Integrity`, `Webview-Bundle-Signature`, `ETag`, `Last-Modified`입니다. 다운로드는 `Content-Type: application/webview-bundle`을 사용합니다. 상태 코드와 채널 쿼리 전체 명세는 [원격 번들](/docs/guide/remote-bundles)에 있습니다. + +이 네 개의 라우트에 응답하는 최소한의 핸들러도 유효한 원격 서버입니다. 프로바이더 패키지는 필요하지 않습니다. + +```ts title="minimal-remote.ts" +import { Hono } from 'hono'; + +const app = new Hono(); + +app.get('/bundles', c => c.json([{ name: 'app', version: '1.1.0' }])); + +app.get('/bundles/:name', c => { + const body = readCurrentBundle(c.req.param('name')); // 여러분의 스토리지 + return new Response(body, { + headers: { + 'Content-Type': 'application/webview-bundle', + 'Webview-Bundle-Name': 'app', + 'Webview-Bundle-Version': '1.1.0', + }, + }); +}); + +app.get( + '/bundles/:name/:version', + c => c.body(null, 403) // 기본적으로 특정 버전은 금지 +); + +export default app; +``` + +## 로컬에서 흐름 테스트하기 + +업데이트 전체 흐름을 여러분의 머신에서 실행하세요. `@wvb/remote-local`은 업로드를 로컬 디렉터리(기본값 `~/.wvb/local`)에 쓰고, `wvb remote local`은 프로덕션 서버가 구현하는 것과 동일한 규약으로 그 디렉터리를 제공합니다. + +### 설정을 로컬 프로바이더로 지정하기 + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; +import { localRemote } from '@wvb/remote-local'; + +export default defineConfig({ + pack: { + outFile: 'app', // → app.wvb + }, + remote: { + endpoint: 'http://localhost:4313', + bundleName: 'app', + ...localRemote(), // ~/.wvb/local에 쓰는 { uploader, deployer } + }, +}); +``` + +홈 디렉터리 대신 프로젝트 안에 스토어를 두려면 다음과 같이 하세요. + +```ts +...localRemote({ baseDir: './.wvb/local' }), +``` + +로컬 스토어는 번들과 버전별로 파일을 배치합니다. + +```text +~/.wvb/local/bundles/app/app_1.1.0.wvb # 번들 바이너리 +~/.wvb/local/bundles/app/app_1.1.0.json # { integrity?, signature? } +~/.wvb/local/bundles/app/deployment.json # 배포 포인터 +``` + +### 버전 퍼블리시하기 + +```sh +wvb pack # 설정으로부터 .wvb 빌드 +wvb upload app --version 1.1.0 --deploy # app_1.1.0.wvb를 ~/.wvb/local에 쓰고 배포 +``` + +`wvb upload`는 기본적으로 업로드 전에 패킹하므로, 명시적인 `wvb pack`은 선택 사항입니다. `--deploy`의 기본값은 `false`입니다. 이 플래그를 전달하거나, 배포를 따로 실행하세요. + +```sh +wvb deploy app --version 1.1.0 # 1.1.0을 클라이언트가 받는 버전으로 지정 +``` + +버전은 위치 인자가 아니라 `--version,-V` 플래그입니다. 모든 플래그는 [CLI 레퍼런스](/docs/guide/cli)를 참고하세요. + +### 제공하기 + +```sh +wvb remote local # ~/.wvb/local을 http://localhost:4313에서 제공 +wvb remote local --base-dir ./.wvb/local --port 4313 --allow-other-versions +``` + +`wvb remote local`은 기본적으로 포트 `4313`(베이스 디렉터리 `~/.wvb/local`)에서 대기합니다. `--allow-other-versions`를 지정하면 클라이언트가 배포되지 않은 버전을 `403` 대신 `GET /bundles/{name}/{version}`로 가져올 수 있습니다. + +### 앱을 원격 서버로 지정하기 + +앱의 업데이터 엔드포인트를 `http://localhost:4313`으로 설정하세요. 이제 `getUpdate`, `download`, `install`이 로컬 서버에 요청하며, 프로덕션과 똑같이 번들을 다운로드하고 검증하고 활성화합니다. + +```rust +let remote = Remote::builder() + .endpoint("http://localhost:4313") + .build()?; +let updater = Updater::new(source, Arc::new(remote), None); + +let info = updater.get_update("app").await?; // HEAD /bundles/app +if info.is_available { + updater.download("app", None).await?; // GET /bundles/app + updater.install("app", &info.version).await?; // 다운로드한 버전 활성화 +} +``` + + + Android 에뮬레이터에서는 호스트 머신이 `localhost`가 아닙니다. 업데이터를 + `http://10.0.2.2:4313`으로 지정하세요. + + +### 클라이언트 명령어로 확인하기 + +CLI는 어떤 원격 서버에 대해서도 클라이언트로 동작할 수 있습니다. 앱을 지정하기 전에 서버가 제대로 동작하는지 확인할 때 유용합니다. + +```sh +wvb remote list --endpoint http://localhost:4313 # GET /bundles (별칭: remote ls) +wvb remote current app --endpoint http://localhost:4313 # HEAD /bundles/app +wvb remote download app --endpoint http://localhost:4313 --outfile ./app.wvb # GET /bundles/app +``` + +### 단일 번들 미리 보기 + +서버나 배포 없이 `.wvb`를 살펴보려면, 그 파일들을 직접 제공하세요. + +```sh +wvb serve ./app.wvb # 번들의 파일들을 http://localhost:4312에서 제공 +wvb serve ./app.wvb --port 8080 +``` + +`wvb serve`는 기본적으로 포트 `4312`에서 대기합니다. 업로드하기 전에 패킹이 기대한 파일과 헤더를 만들었는지 확인할 때 사용하세요. + +## 프로바이더 선택하기 + +로컬 테스트를 넘어서면 `localRemote()`를 클라우드 백엔드로 바꾸세요. 각 프로바이더는 `wvb.config.ts`용으로 호환되는 `uploader`와 `deployer`, 위 규약을 구현하는 서버, 그리고 이를 프로비저닝하는 Pulumi 패키지를 제공합니다. + +```ts title="wvb.config.ts — Cloudflare" +import { cloudflareRemote } from '@wvb/remote-cloudflare'; + +export default defineConfig({ + remote: { + endpoint: 'https://bundles.example.com', + bundleName: 'app', + ...cloudflareRemote({ + accountId: process.env.CF_ACCOUNT_ID!, + bucket: 'webview-bundle', + kvNamespaceId: process.env.CF_KV_NAMESPACE_ID!, + }), + }, +}); +``` + +Pulumi로 그에 맞는 인프라를 프로비저닝하세요. + +```ts title="index.ts — pulumi up" +import { WebviewBundleRemoteProvider } from '@wvb/remote-cloudflare-provider-pulumi'; + +export const remote = new WebviewBundleRemoteProvider('app', { + accountId: process.env.CF_ACCOUNT_ID!, +}); +``` + + + + + + diff --git a/content/docs/guide/spec/errors.ko.mdx b/content/docs/guide/spec/errors.ko.mdx new file mode 100644 index 0000000..b143ad5 --- /dev/null +++ b/content/docs/guide/spec/errors.ko.mdx @@ -0,0 +1,93 @@ +--- +title: 오류 +description: Webview Bundle 원격 서버가 HTTP로 실패를 알리는 방식과, 클라이언트가 각 상태 코드를 오류로 매핑하는 방식을 설명합니다. +--- + +원격 서버는 표준 HTTP 상태 코드로 실패를 알립니다. Webview Bundle 클라이언트는 모든 `2xx` 응답을 성공으로 처리하고, 문서에 명시된 `2xx` 외의 코드를 특정 오류로 매핑합니다. 클라이언트와 업데이터가 의도대로 반응하도록 이 응답들을 정확히 구현하세요. 이 오류들이 속한 전체 요청 계약은 [Remote HTTP 명세](/docs/guide/spec)와 [원격 서버 구축하기](/docs/guide/remote)를 참고하세요. + +## 상태 코드 + +| 상태 | 의미 | 시점 | +| ------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `403 Forbidden` | 특정 버전 다운로드 비활성화 | 클라이언트가 `GET /bundles/{name}/{version}`을 요청했지만 서버가 다른 버전의 다운로드를 비활성화한 경우입니다. 클라이언트는 이를 `RemoteForbidden`으로 매핑합니다. | +| `404 Not Found` | 번들 또는 버전이 배포되지 않음 | 지정한 번들 또는 요청한 버전이 배포되지 않은 경우입니다. 클라이언트는 이를 `RemoteBundleNotFound`로 매핑합니다. | +| 그 외 `2xx` 외 코드 | 원격 HTTP 오류 | 그 밖의 모든 실패입니다. 본문은 JSON `{ "message": string }`으로 파싱되어, 상태 코드와 메시지를 담은 원격 HTTP 오류로 표면화됩니다. | + +클라이언트는 모든 `2xx` 상태를 성공으로 처리합니다. 따라서 원격 서버는 실제 실패에만 `2xx` 외의 코드를 사용해야 합니다. + +## Forbidden (403) + +`403`은 특정 버전 다운로드가 비활성화되었음을 알리는 의도적인 신호입니다. 프로바이더는 이를 `allowOtherVersions` 옵션으로 노출하며, 기본값은 `false`입니다. 비활성화된 동안에는 해당 버전이 서버에 존재하더라도 `GET /bundles/{name}/{version}`이 `403`을 반환합니다. 현재 버전 경로(`GET /bundles/{name}`과 `HEAD /bundles/{name}`)는 영향을 받지 않습니다. + +```http +HTTP/1.1 403 Forbidden +``` + +클라이언트는 이 상태에 대해 `RemoteForbidden`을 발생시킵니다. + +## Not Found (404) + +`404`는 지정한 번들 또는 요청한 버전이 배포되지 않았음을 의미합니다. 목록 응답은 배포되지 않은 번들을 제외하므로, 그중 하나를 이어서 요청하면 `404`가 반환됩니다. + +```http +HTTP/1.1 404 Not Found +``` + +클라이언트는 이 상태에 대해 `RemoteBundleNotFound`를 발생시킵니다. + +## 그 밖의 오류 + +그 밖의 `2xx` 외 상태에 대해서는 `{ "message": string }` 형태의 JSON 본문을 반환하세요. 클라이언트는 이 본문을 파싱하여, 상태 코드와 메시지를 모두 담은 원격 HTTP 오류를 발생시킵니다. + +```http +HTTP/1.1 500 Internal Server Error +Content-Type: application/json +``` + +```json +{ "message": "failed to read bundle from storage" } +``` + + + 이 응답들에는 `{ "message": string }` 본문을 보내세요. 클라이언트가 표면화하는 값이 바로 이 `message`이므로, + 실패를 진단할 수 있을 만큼 설명적으로 작성하세요. + + +## 필수 헤더 누락 + +성공적인 다운로드나 메타데이터 응답이라도 필수 헤더는 반드시 담고 있어야 합니다. 클라이언트는 `Webview-Bundle-Name` 또는 `Webview-Bundle-Version`이 빠진 `2xx` 응답을 거부하며, 이를 전송 오류가 아니라 클라이언트 쪽의 잘못된 번들로 처리합니다. 이는 서버가 반환하는 상태가 아니라 클라이언트 쪽 실패이므로, 원격 서버는 다운로드와 메타데이터 응답에 두 헤더를 항상 설정해야 합니다. 전체 헤더 목록은 [GET /bundles/{name}](/docs/guide/spec/get-current)과 [HEAD /bundles/{name}](/docs/guide/spec/head-current)을 참고하세요. + +## 다음으로 볼 내용 + + + + + + + + + diff --git a/content/docs/guide/spec/get-current.ko.mdx b/content/docs/guide/spec/get-current.ko.mdx new file mode 100644 index 0000000..ac6b11f --- /dev/null +++ b/content/docs/guide/spec/get-current.ko.mdx @@ -0,0 +1,99 @@ +--- +title: 현재 번들 다운로드 +description: 배포된 번들의 현재 버전을 원본 .wvb 바이트로 다운로드합니다. 채널을 선택할 수도 있습니다. +--- + +배포된 번들의 현재 버전을 다운로드합니다. 서버는 `.wvb` 아카이브를 응답 본문으로 스트리밍하고, 번들의 메타데이터를 `Webview-Bundle-*` 헤더에 담아 전달합니다. [업데이터](/docs/references/node/updater)는 검사 결과 새 버전이 있다고 확인되면 이 호출로 새 버전을 가져옵니다. + +```http +GET /bundles/{name} +``` + +기본 배포 대신 특정 채널의 현재 버전을 다운로드하려면 `?channel=`을 전달하세요. + +## 매개변수 + +| Name | In | Type | Required | Description | +| --------- | ----- | ------ | -------- | ------------------------------------------------- | +| `name` | path | string | yes | 다운로드할 번들 이름. | +| `channel` | query | string | no | 선택할 채널. 생략하면 기본 배포를 다운로드합니다. | + +## 응답 + +| Status | Description | +| ------ | ------------------------------------------------ | +| `200` | 번들을 찾았습니다. 본문은 `.wvb` 아카이브입니다. | +| `404` | 해당 이름의 번들이 배포되지 않았습니다. | + +`404` 형태와 클라이언트가 그 밖의 non-2xx 응답을 매핑하는 방식은 [오류](/docs/guide/spec/errors)를 참고하세요. + +### 응답 헤더 + +`200` 응답은 [HEAD /bundles/{name}](/docs/guide/spec/head-current)과 동일한 번들 메타데이터 헤더를 담습니다. 클라이언트는 헤더를 대소문자 구분 없이 읽습니다. 여기서는 표준 표기를 보여줍니다. + +| Header | Required | Description | +| -------------------------- | -------- | --------------------------------- | +| `Webview-Bundle-Name` | yes | 번들 이름. | +| `Webview-Bundle-Version` | yes | 번들 버전. | +| `Webview-Bundle-Integrity` | no | 무결성 문자열 `":"`. | +| `Webview-Bundle-Signature` | no | Base64 서명. | +| `ETag` | no | 표준 검증자. | +| `Last-Modified` | no | 표준 검증자. | + +클라이언트는 `Webview-Bundle-Name` 또는 `Webview-Bundle-Version`이 빠진 `200` 응답을 거부합니다. + +응답 본문은 원본 `.wvb` 아카이브 바이트이며, `Content-Type: application/webview-bundle`로 제공됩니다. + +## 예제 + +현재 버전을 다운로드하고 응답 헤더를 출력합니다. + +```sh +curl -s http://localhost:4313/bundles/app -o app.wvb -D - +``` + +```http +HTTP/1.1 200 OK +Content-Type: application/webview-bundle +Webview-Bundle-Name: app +Webview-Bundle-Version: 1.2.0 +Webview-Bundle-Integrity: sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= + + +``` + +`?channel=`으로 채널의 현재 버전을 선택합니다. + +```sh +curl -s 'http://localhost:4313/bundles/app?channel=beta' -o app-beta.wvb +``` + +배포되지 않은 번들은 `404`를 반환합니다. + +```sh +curl -sI http://localhost:4313/bundles/unknown +``` + +```http +HTTP/1.1 404 Not Found +``` + +## 관련 항목 + + + + + + diff --git a/content/docs/guide/spec/get-version.ko.mdx b/content/docs/guide/spec/get-version.ko.mdx new file mode 100644 index 0000000..6a8e904 --- /dev/null +++ b/content/docs/guide/spec/get-version.ko.mdx @@ -0,0 +1,106 @@ +--- +title: '특정 버전 다운로드' +description: 이름이 지정된 번들을 정확한 버전으로 다운로드하며, 서버의 다른 버전 다운로드 정책에 따라 허용 여부가 결정됩니다. +--- + +배포된 현재 버전 대신 정확한 버전으로 번들을 다운로드합니다. 다른 다운로드 경로와 달리 이 작업은 채널을 받지 않습니다. 버전을 경로에 직접 지정합니다. + +```http +GET /bundles/{name}/{version} +``` + +서버는 다른 버전 다운로드를 허용할 때만 이 경로를 제공합니다. 프로바이더는 이를 `allowOtherVersions` 옵션으로 노출하며, 기본값은 `false`입니다. 이 옵션이 비활성화되어 있으면 버전이 존재하더라도 요청은 `403`을 반환합니다. + +## 파라미터 + +| Name | In | Type | Required | Description | +| ----------- | ---- | ------ | -------- | ----------------------- | +| `{name}` | path | string | yes | 다운로드할 번들 이름. | +| `{version}` | path | string | yes | 다운로드할 정확한 버전. | + +이 경로는 `channel` 쿼리 파라미터를 받지 않습니다. + +## 응답 + +| Status | Description | +| ------ | -------------------------------------------------- | +| `200` | 번들 바이트와 메타데이터 헤더. | +| `403` | 서버가 다른 버전 다운로드를 비활성화한 경우. | +| `404` | 이름이 지정된 번들 또는 버전이 배포되지 않은 경우. | + +클라이언트는 `403`을 "forbidden" 오류로, `404`를 "bundle not found" 오류로 매핑합니다. 전체 매핑과 JSON 오류 본문 형태는 [오류](/docs/guide/spec/errors)를 참고하세요. + +### 응답 헤더 + +| Header | Required | Description | +| -------------------------- | -------- | --------------------------------- | +| `Webview-Bundle-Name` | yes | 번들 이름. | +| `Webview-Bundle-Version` | yes | 번들 버전. | +| `Webview-Bundle-Integrity` | no | 무결성 문자열 `":"`. | +| `Webview-Bundle-Signature` | no | Base64 서명. | +| `ETag` | no | 표준 검증자. | +| `Last-Modified` | no | 표준 검증자. | + +클라이언트는 `Webview-Bundle-Name` 또는 `Webview-Bundle-Version`이 없는 응답을 거부합니다. + +`200` 본문은 원시 `.wvb` 바이트이며 `Content-Type: application/webview-bundle`로 제공됩니다. + + + 이 경로는 채널을 선택하지 않습니다. 채널 지정 여부와 관계없이 현재 버전을 다운로드하려면 [GET + /bundles/{name}](/docs/guide/spec/get-current)을 사용하세요. + + +## 예제 + +`app` 번들의 `1.2.0` 버전을 다운로드합니다. + +```sh +$ curl -s http://localhost:4313/bundles/app/1.2.0 -o app-1.2.0.wvb -D - +``` + +```http +HTTP/1.1 200 OK +Content-Type: application/webview-bundle +Webview-Bundle-Name: app +Webview-Bundle-Version: 1.2.0 +Webview-Bundle-Integrity: sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= +``` + +서버가 다른 버전 다운로드를 비활성화한 경우 동일한 요청은 `403`을 반환합니다. + +```sh +$ curl -sI http://localhost:4313/bundles/app/1.2.0 +HTTP/1.1 403 Forbidden +``` + +배포되지 않은 버전은 `404`를 반환합니다. + +```sh +$ curl -sI http://localhost:4313/bundles/app/9.9.9 +HTTP/1.1 404 Not Found +``` + +## 관련 문서 + + + + + + + diff --git a/content/docs/guide/spec/head-current.ko.mdx b/content/docs/guide/spec/head-current.ko.mdx new file mode 100644 index 0000000..e0a6ddc --- /dev/null +++ b/content/docs/guide/spec/head-current.ko.mdx @@ -0,0 +1,90 @@ +--- +title: 현재 메타데이터 조회 +description: HEAD /bundles/{name}은 현재 배포된 버전의 메타데이터를 응답 헤더로 반환하며, 본문은 없습니다. +--- + +`HEAD /bundles/{name}`은 번들의 현재 배포된 버전의 메타데이터를 응답 헤더로 반환하며, 본문은 없습니다. 업데이터는 무언가를 다운로드하기 전에 더 새로운 버전이 있는지 확인하기 위해 이 요청을 호출합니다. + +```http +HEAD /bundles/{name} +``` + +## 매개변수 + +| Name | In | Type | Required | 설명 | +| --------- | ----- | ------ | -------- | -------------------------------------------------------- | +| `name` | path | string | yes | 조회할 번들 이름입니다. | +| `channel` | query | string | no | 채널의 배포를 선택합니다. 생략하면 기본 배포를 읽습니다. | + +## 응답 + +| Status | 설명 | +| ------ | --------------------------------------------------------- | +| `204` | 번들이 배포되어 있습니다. 메타데이터가 헤더로 반환됩니다. | +| `404` | 지정한 번들이 배포되어 있지 않습니다. | + +성공은 `204 No Content`입니다. 응답은 메타데이터 헤더만 담으며 본문은 절대 포함하지 않습니다. `404`는 지정한 번들에 현재 배포된 버전이 없다는 뜻입니다. 오류 본문 형태와 다른 상태 코드는 [오류](/docs/guide/spec/errors)를 참고하세요. + +### 응답 헤더 + +| Header | Required | 설명 | +| -------------------------- | -------- | ---------------------------------------------- | +| `Webview-Bundle-Name` | yes | 번들 이름입니다. | +| `Webview-Bundle-Version` | yes | 현재 배포된 버전입니다. | +| `Webview-Bundle-Integrity` | no | `":"` 형식의 무결성 문자열입니다. | +| `Webview-Bundle-Signature` | no | 무결성 문자열에 대한 Base64 인코딩 서명입니다. | +| `ETag` | no | 표준 검증자입니다. | +| `Last-Modified` | no | 표준 검증자입니다. | + +클라이언트는 이 헤더들을 대소문자 구분 없이 읽으며, `Webview-Bundle-Name` 또는 `Webview-Bundle-Version`이 빠진 응답은 거부합니다. + + + 이 경로는 메타데이터만 반환합니다. 번들 바이트를 가져오려면 [GET /bundles/{name} + ](/docs/guide/spec/get-current)을 사용하세요. + + +## 예시 + +```sh +$ curl -sI 'http://localhost:4313/bundles/app' +``` + +```http +HTTP/1.1 204 No Content +Webview-Bundle-Name: app +Webview-Bundle-Version: 1.2.0 +Webview-Bundle-Integrity: sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= +ETag: "abc123" +Last-Modified: Tue, 01 Jul 2026 00:00:00 GMT +``` + +특정 채널의 현재 버전은 `?channel=`로 읽습니다. + +```sh +$ curl -sI 'http://localhost:4313/bundles/app?channel=beta' +``` + +## 관련 문서 + + + + + + + diff --git a/content/docs/guide/spec/index.ko.mdx b/content/docs/guide/spec/index.ko.mdx new file mode 100644 index 0000000..4a3140a --- /dev/null +++ b/content/docs/guide/spec/index.ko.mdx @@ -0,0 +1,76 @@ +--- +title: 원격 HTTP 명세 +description: Webview Bundle 클라이언트와 업데이터가 번들을 나열하고 조회하고 다운로드할 수 있도록 서버가 구현하는 HTTP 계약입니다. +--- + +여기서 설명하는 엔드포인트를 구현하는 HTTP 서버는 모두 유효한 Webview Bundle 원격입니다. 클라이언트와 업데이터는 이 계약을 통해 배포된 번들을 나열하고, 번들의 현재 메타데이터를 읽고, 번들 바이트를 다운로드합니다. 직접 구현하거나, 이미 이 계약을 구현한 프로바이더를 사용하세요. [Local](/docs/guide/providers/local), [AWS](/docs/guide/providers/aws), [Cloudflare](/docs/guide/providers/cloudflare) 프로바이더는 모두 정확히 이 계약을 제공합니다. 서버를 구축하고 프로바이더를 선택하려면 [원격 구축](/docs/guide/remote)을 참고하세요. 이 호출들을 이끄는 업데이트 흐름의 개념은 [원격 번들 및 업데이트](/docs/guide/remote-bundles)를 참고하세요. + +## Base URL + +원격은 단일 `endpoint` base URL로 설정합니다. 모든 라우트는 이 base 아래의 경로이므로, `endpoint`가 `https://cdn.example.com`이면 `GET https://cdn.example.com/bundles`, `HEAD https://cdn.example.com/bundles/{name}` 등을 제공합니다. 클라이언트는 설정된 `endpoint`에 라우트 경로와 쿼리 문자열을 결합해 각 요청 URL을 만듭니다. + +## 규약 + +이 규약은 모든 엔드포인트에 공통으로 적용됩니다. 각 엔드포인트 페이지는 해당 라우트에만 특정한 내용만 반복합니다. + +### 응답 헤더 + +메타데이터는 다운로드 및 메타데이터 응답의 `Webview-Bundle-*` 응답 헤더에 담겨 전달됩니다. 클라이언트는 대소문자를 구분하지 않고 읽으며, 여기 표시한 것이 표준 표기입니다. + +| 헤더 | 필수 | 의미 | +| -------------------------- | ------ | -------------------------------- | +| `Webview-Bundle-Name` | 예 | 번들 이름 | +| `Webview-Bundle-Version` | 예 | 번들 버전 | +| `Webview-Bundle-Integrity` | 아니오 | 무결성 문자열 `":"` | +| `Webview-Bundle-Signature` | 아니오 | Base64 서명 | +| `ETag` | 아니오 | 표준 검증자 | +| `Last-Modified` | 아니오 | 표준 검증자 | + +클라이언트는 `Webview-Bundle-Name` 또는 `Webview-Bundle-Version`이 없는 응답을 모두 거부합니다. + +### Content type + +다운로드 응답은 `Content-Type: application/webview-bundle`을 설정합니다. 목록 응답은 `Content-Type: application/json`을 설정합니다. + +### 채널 + +목록, 메타데이터, 현재 버전 다운로드 라우트는 선택적 `?channel=` 쿼리를 받아 해당 채널의 배포를 선택합니다. 버전별 다운로드는 채널을 받지 않습니다. 기본 배포를 대상으로 하려면 쿼리를 생략하세요. + +### 요청 타임아웃 + +클라이언트는 모든 호출에 기본 전체 요청 타임아웃 120초를 적용하므로, 서버는 그 안에 여유 있게 응답해야 합니다. + + + 120초 타임아웃은 클라이언트의 HTTP 설정에서 재정의할 수 있지만, 변경하지 않으면 기본값이 모든 + 요청에 적용됩니다. + + +## 엔드포인트 + + + + + + + + diff --git a/content/docs/guide/spec/list-bundles.ko.mdx b/content/docs/guide/spec/list-bundles.ko.mdx new file mode 100644 index 0000000..bce977a --- /dev/null +++ b/content/docs/guide/spec/list-bundles.ko.mdx @@ -0,0 +1,97 @@ +--- +title: 번들 목록 조회 +description: GET /bundles는 원격 서버가 현재 배포한 번들을 반환하며, 선택적으로 채널 범위로 좁힐 수 있습니다. +--- + +원격 서버가 현재 배포한 번들을 반환합니다. 클라이언트와 업데이터는 메타데이터를 읽거나 바이트를 다운로드하기 전에 이 엔드포인트를 먼저 호출합니다. 이를 통해 어떤 번들이 존재하고 각 번들이 어떤 버전으로 배포되었는지 확인합니다. + +```http +GET /bundles +``` + +## 파라미터 + +| Name | In | Type | Required | Description | +| --------- | ----- | ------ | -------- | --------------------------------------------------------------- | +| `channel` | query | string | No | 특정 채널의 배포를 선택합니다. 생략하면 기본 배포가 조회됩니다. | + +## 응답 + +| Status | Description | +| ------ | ----------------------------------- | +| `200` | 현재 배포된 번들의 JSON 배열입니다. | + +4xx 응답에 대해서는 [오류](/docs/guide/spec/errors)를 참고하세요. + +### 응답 헤더 + +| Header | Required | Description | +| -------------- | -------- | ------------------------------------------ | +| `Content-Type` | Yes | 목록 응답의 경우 `application/json`입니다. | + +### 응답 본문 + +본문은 JSON 배열입니다. 각 항목은 배포된 번들 하나를 `name`과 `version`으로 나타냅니다. 배포되지 않은 번들은 제외됩니다. + +```json +[ + { "name": string, "version": string } +] +``` + +| Field | Type | Description | +| --------- | ------ | ----------------------- | +| `name` | string | 번들 이름입니다. | +| `version` | string | 현재 배포된 버전입니다. | + +## 예제 + +```sh +curl -s http://localhost:4313/bundles +``` + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +[ + { "name": "app", "version": "1.2.0" }, + { "name": "admin", "version": "0.4.1" } +] +``` + +`?channel=`로 목록을 특정 채널 범위로 좁힙니다. + +```sh +curl -s 'http://localhost:4313/bundles?channel=beta' +``` + + + 이 작업은 원격 HTTP 계약의 진입점입니다. 번들의 현재 메타데이터를 읽거나 바이트를 다운로드하려면 + [원격 HTTP 명세](/docs/guide/spec) 개요를 참고하세요. + + +## 다음으로 볼 내용 + + + + + + + diff --git a/content/docs/guide/spec/meta.ko.json b/content/docs/guide/spec/meta.ko.json new file mode 100644 index 0000000..f208535 --- /dev/null +++ b/content/docs/guide/spec/meta.ko.json @@ -0,0 +1,4 @@ +{ + "title": "명세", + "pages": ["index", "list-bundles", "head-current", "get-current", "get-version", "errors"] +} diff --git a/content/docs/guide/why-webview-bundle.ko.mdx b/content/docs/guide/why-webview-bundle.ko.mdx new file mode 100644 index 0000000..fbe2e87 --- /dev/null +++ b/content/docs/guide/why-webview-bundle.ko.mdx @@ -0,0 +1,189 @@ +--- +title: Webview Bundle를 선택하는 이유 +description: 웹 앱을 오프라인 우선 .wvb 번들로 출시하고 무선(OTA) 업데이트로 배포하는 방법을 설명합니다. +--- + +Webview Bundle는 빌드된 웹 앱을 하나의 압축된, 무결성이 검증되는 아카이브(`.wvb`)로 묶고, 이를 커스텀 URL 스킴을 통해 웹뷰에 제공합니다. 웹뷰는 HTML, JS, CSS, 미디어를 네트워크가 아닌 로컬 저장소에서 읽습니다. 원격을 설정하면 앱이 더 새로운 번들을 무선으로 다운로드하며, 네이티브 앱스토어 출시 없이도 갱신된 코드를 배포합니다. + +팀이 Webview Bundle를 선택하는 이유는 세 가지입니다. 오프라인 우선 전달, 무선(OTA) 업데이트, 그리고 모든 웹뷰 플랫폼을 아우르는 하나의 워크플로입니다. + +## 기본으로 오프라인 우선 + +원격 URL을 사용하면 첫 페인트가 네트워크를 기다려야 합니다. 캐시가 비어 있거나, 캡티브 포털에 걸리거나, 연결이 끊기면 빈 화면이 나타납니다. + +Webview Bundle는 앱과 함께 출시되는 로컬 `.wvb`에서 자산을 곧바로 제공합니다. 프로토콜은 각 요청을 번들 안의 파일로 매핑하고 그 파일을 반환합니다. + +```text +bundle://app/index.html -> index.html inside the app bundle +bundle://app/assets/x.js -> assets/x.js inside the app bundle +``` + +```ts title="번들을 로컬에서 제공" +import { BundleSource, BundleProtocol } from '@wvb/node'; + +const source = new BundleSource({ + builtinDir: '/path/to/builtin', + remoteDir: '/path/to/remote', +}); + +const protocol = new BundleProtocol(source); + +// 플랫폼 통합이 스킴 요청을 여기로 라우팅합니다. +const res = await protocol.handle('get', 'bundle://app/index.html'); +// res: { status, headers, body } +``` + +첫 페인트는 절대 네트워크를 기다리지 않습니다. 지하철에서든, 비행기에서든, 세션 도중 연결이 끊겨도 앱은 로컬 번들에서 화면을 그립니다. 네트워크는 최적화 수단이 됩니다. + + + 번들 프로토콜은 `GET`과 `HEAD`를 지원하고, 각 파일에 저장된 헤더를 그대로 재현하며, 미디어에 대한 + `Range` 요청에는 `206`으로 응답합니다. 전체 요청 매핑은 [프로토콜 + 처리](/docs/guide/protocol-handling)를 참고하세요. + + +## 무선(OTA) 업데이트 + +앱스토어 출시는 느리고 심사에 막힙니다. 문구 오타 하나, 동작하지 않는 버튼 하나가 며칠에 걸친 심사에 묶일 수 있습니다. 앱 코드가 `.wvb` 안에 있기 때문에, 새 번들 버전을 배포하고 설치된 앱이 이를 무선(OTA)으로 다운로드하게 하면 문제를 고칠 수 있습니다. 네이티브 바이너리는 전혀 바뀌지 않습니다. + +업데이트 모델은 두 개의 소스로 뒷받침됩니다. `builtin` 소스는 출시 시 함께 담은 읽기 전용 번들입니다. `remote` 소스는 다운로드한 번들을 담으며 우선권을 가집니다. 다운로드나 활성화가 실패하면 `builtin`으로 되돌아가므로, OTA가 기기를 동작하는 앱이 없는 상태로 만드는 일은 결코 없습니다. + +```ts title="두 소스: remote가 우선, builtin이 최후의 보루" +new BundleSource({ + builtinDir: '/path/to/builtin', // 앱에 함께 담긴 읽기 전용 + remoteDir: '/path/to/remote', // OTA 다운로드, 우선권을 가짐 +}); +``` + +업데이트는 두 개의 명시적인 단계로 실행되어, 다운로드가 절반만 된 번들이 제공되는 일이 없습니다. `download`는 원격 소스에 스테이징하고 검증합니다. `install`은 스테이징된 버전을 활성화합니다. + +```ts title="확인, 다운로드, 설치" +import { Updater } from '@wvb/node'; + +const updater = new Updater(source, remote); + +const info = await updater.getUpdate('app'); +if (info.isAvailable) { + await updater.download('app'); // 스테이징 + 검증 + await updater.install('app', info.version); // 활성화 +} +``` + +### 채널을 이용한 단계적 롤아웃 + +채널은 목록 및 다운로드 요청에 `?channel=`로 전달되는 선택적 문자열입니다. `UpdaterOptions`를 통해 전달하면 특정 기기 집단을 `beta`로 지정한 뒤 승격할 수 있습니다. 생략하면 이 파라미터는 빠지며, 하드코딩된 채널 이름은 없습니다. + +```ts title="이 기기를 beta 채널에 고정" +const updater = new Updater(source, remote, { channel: 'beta' }); +``` + +```http +GET /bundles/app?channel=beta +``` + +### 무결성과 서명을 통한 진위 확인 + +모든 다운로드는 활성화 전에 검증됩니다. 무결성은 SHA-2(`sha256` 기본값, 그 외 `sha384`, `sha512`)를 사용하며 `":"` 형식으로 직렬화됩니다. + +```text +sha256:n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg= +``` + +기본 정책은 `optional`(값이 있으면 검증)입니다. `strict`로 설정하면 무결성을 필수로 요구합니다. 여기에 선언적 `signatureVerifier`를 추가하면 누가 게시했는지 증명할 수 있습니다. 서명은 무결성 문자열의 바이트를 대상으로 합니다. + +```ts title="무결성 요구, Ed25519 서명 검증" +const updater = new Updater(source, remote, { + integrityPolicy: 'strict', + signatureVerifier: { + algorithm: 'ed25519', + key: { format: 'raw', data: publicKeyBytes }, // 32바이트 Ed25519 키 + }, +}); +``` + +검증은 ECDSA(`ecdsaSecp256R1`, `ecdsaSecp384R1`), `ed25519`, RSA(`rsaPkcs1V15`, `rsaPss`)를 지원합니다. + + + 무결성은 SHA-3가 아니라 SHA-2입니다. 서명은 원시 아카이브 바이트가 아니라 무결성 문자열을 대상으로 + 하며, 무결성 값이 반드시 존재해야 합니다. 검증 흐름은 [원격 번들](/docs/guide/remote-bundles)을, + 스키마는 [원격, 무결성 및 서명 설정](/docs/config/remote)을 참고하세요. + + +## 웹 개발자를 위해 + +이미 알고 있는 방식 그대로 앱을 작성하세요. Webview Bundle는 소스가 아니라 번들러의 출력물을 대상으로 동작하므로 React, Vue, Svelte, 순수 HTML 모두 그대로 동작합니다. 커스텀 프레임워크도, 특별한 빌드 단계도 필요 없습니다. `wvb pack`을 빌드 디렉터리로 향하게 하세요. + +```sh title="빌드 산출물을 하나의 .wvb로 패킹" +npm install --save-dev @wvb/cli +npx wvb pack ./dist +``` + +패킹된 파일은 기본적으로 `.wvb/`에 생성되며 `.wvb` 확장자는 자동으로 붙습니다. `outFile` 필드(단일 경로)로 이를 재정의할 수 있습니다. + +```ts title="wvb.config.ts" +import { defineConfig } from '@wvb/config'; + +export default defineConfig({ + pack: { + outFile: '.wvb/app', + }, +}); +``` + +웹뷰 안에서 코드는 `@wvb/bridge`를 통해 네이티브 호스트와 통신합니다. 하나의 `invoke()` API가 소스, 원격, 업데이터 명령을 노출하고, 그 아래에서 Electron, Tauri, Android, iOS에 맞는 전송 방식을 선택합니다. 같은 업데이트 코드가 어디서나 실행됩니다. + +```ts title="웹 코드에서 업데이트 실행" +import { updater } from '@wvb/bridge'; + +const update = await updater.getUpdate('app'); +if (update.isAvailable) { + await updater.download('app'); + await updater.install('app', update.version); +} +``` + +브리지는 호출 시점에 플랫폼을 확인하므로, 직접 플랫폼별로 분기할 필요가 없습니다. + +```ts title="플랫폼이 자동으로 감지됩니다" +import { platform } from '@wvb/bridge'; + +platform.type; // 'electron' | 'tauri' | 'android' | 'ios' +``` + +### 다른 방식과의 비교 + +흔히 쓰이는 두 가지 대안이 같은 문제의 일부를 해결합니다. + +- **플랫폼별 네이티브 자산 번들링.** 웹 빌드를 각 셸에 임베드하고, 서빙 및 업데이트 로직을 플랫폼마다 한 번씩 작성하는 방식입니다. 동작은 하지만 툴체인, 자산 포맷, 업데이트 코드가 플랫폼마다 다르고 그 모두를 직접 유지보수해야 합니다. Webview Bundle는 공유된 Rust 코어를 기반으로 Electron, Tauri, Android, iOS 전반에 하나의 `.wvb`와 하나의 사고 모델을 제공합니다. +- **CodePush 방식의 OTA.** 호스팅형 OTA 서비스는 스토어 심사 없이 JS 업데이트를 배포합니다. 원격 소스와 같은 이점을 줍니다. 다만 절충점이 다릅니다. Webview Bundle는 관리형 서드파티 서비스가 아니라, builtin 폴백, 채널 롤아웃, 무결성과 선택적 서명을 갖춘, 직접 호스팅하는 검증 가능한 번들 포맷입니다. 서버와 아티팩트를 여러분이 소유합니다. + +솔직한 절충점은 이렇습니다. 번들 포맷과 패킹 단계를 도입하고 원격을 직접 운영해야 합니다. 그 대가로 오프라인 우선 전달, 스토어 심사 없는 OTA, 그리고 플랫폼을 추가해도 변하지 않는 하나의 워크플로를 얻습니다. + + + Android와 iOS는 구현되어 출시된 상태입니다. iOS 최소 버전은 iOS 16입니다. Deno Desktop은 + 실험적입니다. 현재 상태는 [플랫폼 지원](/docs/guide/platform-support)을 참고하세요. + + +## 다음으로 볼 내용 + + + + + + + diff --git a/content/docs/meta.ko.json b/content/docs/meta.ko.json new file mode 100644 index 0000000..5c003f3 --- /dev/null +++ b/content/docs/meta.ko.json @@ -0,0 +1,3 @@ +{ + "pages": ["guide", "references", "config", "changelog"] +} diff --git a/content/docs/references/bridge/errors.ko.mdx b/content/docs/references/bridge/errors.ko.mdx new file mode 100644 index 0000000..4cbe533 --- /dev/null +++ b/content/docs/references/bridge/errors.ko.mdx @@ -0,0 +1,61 @@ +--- +title: 오류 +description: '@wvb/bridge가 BridgeError와 BridgeErrorCode 값을 통해 실패를 표현하는 방식.' +--- + +`@wvb/bridge`에서 발생하는 모든 실패는 `BridgeError`로 표현됩니다. 이 클래스는 `Error`의 서브클래스이며, `name`은 `'BridgeError'`로 설정되고 `BridgeErrorCode`에서 가져온 선택적 `code`를 가집니다. `invoke()`를 직접 호출하든 타입이 지정된 명령어 그룹을 호출하든, 모든 거부(rejection)는 이 단일 오류 타입으로 감싸집니다. 따라서 잡아서 검사할 형태가 하나로 통일됩니다. + +## BridgeError + +`BridgeError`는 내장 `Error`를 확장합니다. 사람이 읽을 수 있는 설명은 `message`에서 확인하고, 특정 실패에 따라 분기하려면 `code`를 확인하세요. 이 패키지는 타입 가드 `isBridgeError`와 `isBridgeErrorData`, 그리고 정적 헬퍼 `BridgeError.of(code, message?)`와 `BridgeError.from(value)`도 내보냅니다. + +## BridgeErrorCode + +`code` 속성은 존재할 경우 다음 값 중 하나입니다. 이 값은 전송 시 실려 가는 문자열입니다. + +| Code | Value | 발생 상황 | +| ----------------------- | ------------------------- | ------------------------------------------------ | +| `InvalidParams` | `invalid_params` | 명령어 파라미터가 거부되었습니다. | +| `RemoteNotInitialized` | `remote_not_initialized` | 호스트에 원격이 설정되지 않았습니다. | +| `UpdaterNotInitialized` | `updater_not_initialized` | 호스트에 업데이터가 설정되지 않았습니다. | +| `HandlerNotFound` | `handler_not_found` | 호스트에 해당 명령어를 처리할 핸들러가 없습니다. | +| `UnencodableResult` | `unencodable_result` | 결과를 반환용으로 인코딩할 수 없습니다. | + +## 오류 처리 + +오류를 잡은 뒤 `isBridgeError`로 타입을 좁히고, `code`를 `BridgeErrorCode`와 비교하세요. + +```ts +import { BridgeError, BridgeErrorCode, isBridgeError, updater } from '@wvb/bridge'; + +try { + const update = await updater.getUpdate('app'); + // update 사용... +} catch (err) { + if (isBridgeError(err) && err.code === BridgeErrorCode.UpdaterNotInitialized) { + // 네이티브 호스트에 업데이터가 설정되지 않았습니다. + return; + } + throw err; +} +``` + + + `err.code`는 선택적입니다. `BridgeError`는 `code` 없이 던져질 수 있습니다(예를 들어 웹뷰가 + 전송(transport)이 없는 환경에서 실행되는 경우). 따라서 분기하기 전에 `undefined`인지 확인하세요. + + +## 함께 보기 + + + + + diff --git a/content/docs/references/bridge/index.ko.mdx b/content/docs/references/bridge/index.ko.mdx new file mode 100644 index 0000000..460c3c0 --- /dev/null +++ b/content/docs/references/bridge/index.ko.mdx @@ -0,0 +1,160 @@ +--- +title: Bridge +description: 웹뷰 안의 JavaScript가 네이티브 호스트를 호출하게 해주는 웹 측 @wvb/bridge 라이브러리입니다. +--- + +`@wvb/bridge`는 Webview Bundle의 웹 측 라이브러리입니다. **웹뷰 안에서** 실행되는 JavaScript가 번들 소스, 원격, 업데이터를 관리하는 네이티브 호스트를 호출하게 해줍니다. 하나의 통일된 `invoke()`가 Electron, Tauri, Android, iOS에서 모두 동작합니다. 플랫폼별 전송 방식은 추상화되어 있으므로 웹 코드는 어떤 플랫폼에서도 동일하게 유지됩니다. + +이 패키지는 순수 TypeScript이며, 타입이 지정된 명령어 그룹(`source`, `remote`, `updater`)을 제공합니다. 따라서 `invoke()`를 직접 호출하는 경우는 드뭅니다. 네이티브 호스트가 이 명령어들을 노출하는 방법은 [플랫폼 연동](/docs/guide/platform-integration)을 참고하세요. 대응하는 네이티브 API는 [Node API 레퍼런스](/docs/references/node)를 참고하세요. + +## Install + +`@wvb/bridge`의 버전은 `0.1.0`입니다. + + + + +```sh +npm install @wvb/bridge +``` + + + + +```sh +pnpm add @wvb/bridge +``` + + + + +```sh +yarn add @wvb/bridge +``` + + + + +## invoke + +`invoke()`는 저수준 진입점입니다. 타입이 지정된 모든 명령어 헬퍼가 이 위에 만들어져 있으므로, 보통은 `source`, `remote`, `updater`를 사용합니다. 타입이 지정된 그룹이 다루지 않는 명령어에만 `invoke()`를 직접 사용하세요. + +```ts +import { invoke } from '@wvb/bridge'; + +const result = await invoke('sourceResolveFilepath', { bundleName: 'app' }); +``` + +시그니처는 `invoke(name: string, params?: InvokeParams): Promise`입니다. 모든 실패는 `BridgeError`로 감싸집니다. `InvokeParams`는 열린 레코드입니다. + +```ts +type InvokeParams = { [key: string | number]: any }; +``` + +## platform + +`platform` 객체는 웹뷰가 어떤 네이티브 호스트에서 실행 중인지 알려줍니다. 모든 속성은 접근할 때마다 실시간으로 계산되므로, 감지 조건이 바뀌어도 정확한 값을 유지합니다. + +```ts +import { platform } from '@wvb/bridge'; + +if (platform.isElectron) { + // Electron 전용 동작 +} + +console.log(platform.type); // 'electron' | 'tauri' | 'android' | 'ios' +``` + +| Member | Type | Description | +| ------------ | --------------------------------------------- | ---------------------------------- | +| `type` | `'electron' \| 'tauri' \| 'android' \| 'ios'` | 감지된 호스트 플랫폼입니다. | +| `isElectron` | `boolean` | Electron에서 실행 중이면 참입니다. | +| `isTauri` | `boolean` | Tauri에서 실행 중이면 참입니다. | +| `isAndroid` | `boolean` | Android에서 실행 중이면 참입니다. | +| `isIos` | `boolean` | iOS에서 실행 중이면 참입니다. | + + + `@wvb/bridge`는 이 네 가지 플랫폼을 감지합니다. Deno Desktop은 실험적이며, `@wvb/bridge`가 아니라 + `@wvb/deno-desktop`을 통해 브리지 지원을 제공합니다. + + +## Transports + +각 플랫폼은 저마다의 전송 방식을 가지며, 모두 `invoke()` 뒤에 숨겨져 있습니다. + +- **Electron**은 `window.wvbElectron.invoke(name, params)`를 호출합니다. +- **Tauri**는 `wvb-tauri` 플러그인 명령어를 호출하며, 명령어 이름을 snake case로 변환합니다. +- **Android**는 `window.wvbAndroid`로 JSON 메시지를 전송하고, 임시 전역 콜백을 통해 결과를 반환합니다. +- **iOS**는 `wvbIos` WebKit 메시지 핸들러로 메시지를 전송하고, 임시 전역 콜백을 통해 결과를 반환합니다. + +이 중 어느 것도 존재하지 않는 곳에서 웹뷰가 실행되면, `invoke()`는 네이티브 웹뷰가 Webview Bundle을 지원해야 한다는 오류를 던집니다. + +## Command groups + +브리지는 타입이 지정된 세 가지 명령어 그룹을 노출합니다. 각 메서드는 타입이 지정된 `invoke()` 호출입니다. 따라서 호출은 타입 검사를 거쳐 대응하는 네이티브 핸들러로 전달됩니다. + + + + + + + + + +## Updater flow + +일반적인 무선(OTA) 확인은 더 새로운 번들을 다운로드하고 설치한 뒤, 웹뷰를 다시 로드하여 이를 반영합니다. + +```ts +import { updater } from '@wvb/bridge'; + +async function checkForUpdate(bundleName: string) { + const update = await updater.getUpdate(bundleName); + if (!update.isAvailable) { + return; + } + + await updater.download(bundleName); + await updater.install(bundleName, update.version); + + // 웹뷰가 새로 설치된 번들을 제공하도록 다시 로드합니다. + window.location.reload(); +} +``` + +업데이터의 전체 기능은 [업데이터 레퍼런스](/docs/references/bridge/updater)를 참고하세요. + +## See also + + + + + diff --git a/content/docs/references/bridge/meta.ko.json b/content/docs/references/bridge/meta.ko.json new file mode 100644 index 0000000..ea18165 --- /dev/null +++ b/content/docs/references/bridge/meta.ko.json @@ -0,0 +1,4 @@ +{ + "title": "Bridge", + "pages": ["index", "source", "remote", "updater", "errors", "testing"] +} diff --git a/content/docs/references/bridge/remote.ko.mdx b/content/docs/references/bridge/remote.ko.mdx new file mode 100644 index 0000000..8127cd1 --- /dev/null +++ b/content/docs/references/bridge/remote.ko.mdx @@ -0,0 +1,58 @@ +--- +title: Remote +description: 네이티브 호스트를 통해 설정된 원격 서버에 질의하는 bridge의 remote.* 명령어 그룹입니다. +--- + +`remote`는 네이티브 호스트를 통해 설정된 원격 서버와 통신하는 bridge 명령어 그룹입니다. 웹뷰 안의 웹 코드에서 이 그룹을 사용하면 원격이 제공하는 항목을 나열하고, 번들의 현재 정보를 조회하고, 다운로드를 실행할 수 있습니다. 웹 쪽에서 번들 바이트를 직접 다루지 않아도 됩니다. + +각 메서드는 대응하는 네이티브 핸들러로 라우팅되는 타입 지정 `invoke()` 호출입니다. 따라서 호출은 타입 검사를 거치며, `invoke()`를 직접 다룰 일은 거의 없습니다. 패키지 전반에 대한 내용은 [Bridge 개요](/docs/references/bridge)를 참고하세요. 이 명령어들이 도달하는 네이티브 쪽에 대한 내용은 [Node remote API](/docs/references/node/remote)를 참고하세요. + +## 메서드 + +| Method | Purpose | +| -------------------------------------- | ---------------------------------- | +| `listBundles()` | 원격이 제공하는 번들을 나열합니다. | +| `getInfo(bundleName)` | 번들의 현재 정보를 가져옵니다. | +| `download(bundleName)` | 현재 버전을 다운로드합니다. | +| `downloadVersion(bundleName, version)` | 특정 버전을 다운로드합니다. | + +```ts +import { remote } from '@wvb/bridge'; + +const bundles = await remote.listBundles(); +const info = await remote.getInfo('app'); + +await remote.download('app'); +await remote.downloadVersion('app', '1.2.0'); +``` + +## 반환값 + +`getInfo`, `download`, `downloadVersion`는 `RemoteBundleInfo`로 resolve됩니다. `download`와 `downloadVersion`는 오직 `RemoteBundleInfo`로만 resolve되며, 번들 바이트는 bridge를 통해 전달되지 않습니다. 네이티브 호스트가 다운로드한 번들을 자신의 소스 저장소에 기록하고, 웹 쪽은 메타데이터만 받습니다. + +`listBundles`는 원격이 현재 제공하는 번들 목록으로 resolve되며, 이는 원격의 `GET /bundles` 응답과 동일합니다. + + + 호스트에 원격이 설정되어 있지 않으면 이 호출들은 `code`가 `RemoteNotInitialized`인 `BridgeError`와 + 함께 실패합니다. 오류 처리에 대한 내용은 [Bridge 개요](/docs/references/bridge)를 참고하세요. + + +## 참고 자료 + + + + + + diff --git a/content/docs/references/bridge/source.ko.mdx b/content/docs/references/bridge/source.ko.mdx new file mode 100644 index 0000000..7b94422 --- /dev/null +++ b/content/docs/references/bridge/source.ko.mdx @@ -0,0 +1,68 @@ +--- +title: Source +description: 네이티브 호스트에서 내장 번들과 원격 번들을 읽고 관리하는 bridge source.* 명령어 그룹입니다. +--- + +`source`는 네이티브 번들 소스에 매핑되는 bridge 명령어 그룹입니다. 네이티브 번들 소스는 호스트가 내장 번들과 원격 번들을 저장하는 공간입니다. 각 메서드는 타입이 지정된 `invoke()` 호출이므로, 웹 측에서 타입 검사를 거쳐 대응하는 네이티브 핸들러로 라우팅됩니다. 이 그룹으로 번들을 나열하고, 버전과 메타데이터를 불러오고, 디스크상의 경로를 확인하고, 다운로드한 원격 번들을 관리하세요. + +이 명령어들이 호출하는 네이티브 측은 [Node API source 레퍼런스](/docs/references/node/source)를 참고하세요. `invoke()`, `platform`, `remote` 및 `updater` 그룹, 오류, 테스트 등 나머지 bridge 영역은 [Bridge 개요](/docs/references/bridge)를 참고하세요. + +```ts +import { source } from '@wvb/bridge'; + +const bundles = await source.listBundles(); +const path = await source.resolveFilepath('app'); +``` + +## Methods + +| Method | Purpose | +| ----------------------------------------------- | ----------------------------------------------- | +| `listBundles()` | 사용 가능한 번들을 나열합니다. | +| `loadVersion(bundleName)` | 번들의 활성 버전을 불러옵니다. | +| `updateVersion(bundleName, version)` | 번들의 활성 원격 버전을 설정합니다. | +| `resolveFilepath(bundleName)` | 소스가 제공하는 디스크상의 경로를 확인합니다. | +| `getBuiltinBundleFilepath(bundleName, version)` | 내장 번들 파일의 경로입니다. | +| `getRemoteBundleFilepath(bundleName, version)` | 원격 번들 파일의 경로입니다. | +| `loadBuiltinMetadata(bundleName, version)` | 내장 번들의 메타데이터를 불러옵니다. | +| `loadRemoteMetadata(bundleName, version)` | 원격 번들의 메타데이터를 불러옵니다. | +| `unloadDescriptor(bundleName)` | 캐시된 번들 디스크립터를 제거합니다. | +| `removeRemoteBundle(bundleName, version)` | 다운로드한 원격 번들을 삭제합니다. | +| `remoteRetainedVersions(bundleName)` | 보관 중인 원격 버전(현재 및 이전)을 나열합니다. | +| `pruneRemoteBundles(bundleName)` | 더 이상 보관하지 않는 원격 번들을 정리합니다. | + + +bridge의 `ListBundleItem`은 `{ type, item }` 형태로 중첩되어 있어, `@wvb/node`의 평면적인 `ListBundleItem`과 다릅니다. 웹 측에서 작업할 때는 bridge 타입을 사용하세요. + + +## Managing remote bundles + +무선(OTA) 업데이트가 적용되면, 호스트는 현재 원격 버전과 이전 버전들을 함께 보관합니다. 보관 중인 항목을 확인한 다음, 더 이상 필요하지 않은 것을 정리하세요. + +```ts +import { source } from '@wvb/bridge'; + +async function tidyRemoteBundles(bundleName: string) { + const retained = await source.remoteRetainedVersions(bundleName); + console.log('retained versions:', retained); + + await source.pruneRemoteBundles(bundleName); +} +``` + +특정 다운로드 버전을 삭제하려면 `removeRemoteBundle(bundleName, version)`을 호출하세요. + +## See also + + + + + diff --git a/content/docs/references/bridge/testing.ko.mdx b/content/docs/references/bridge/testing.ko.mdx new file mode 100644 index 0000000..2dd9f54 --- /dev/null +++ b/content/docs/references/bridge/testing.ko.mdx @@ -0,0 +1,147 @@ +--- +title: 테스트 +description: '@wvb/bridge/testing 헬퍼로 네이티브 호스트 없이 단위 테스트에서 브리지를 구동합니다.' +--- + +`@wvb/bridge/testing` 하위 경로를 사용하면 실제 네이티브 호스트 없이 브리지를 호출하는 웹 코드를 실행할 수 있습니다. 코드가 호출하는 명령어에 핸들러를 등록하고, 감지된 플랫폼을 강제로 지정하며, 테스트 사이에 목(mock)을 정리하세요. 목 명령어 키는 `'source.loadVersion'`이나 `'updater.getUpdate'`처럼 점(dot)으로 구분되고 타입이 지정되어 있으므로, 핸들러 시그니처의 타입 검사가 유지됩니다. + +이 헬퍼가 대신하는 브리지 표면은 [Bridge 개요](/docs/references/bridge)를 참고하세요. + +## Import + +런타임 진입점과 별개인 `testing` 하위 경로에서 헬퍼를 가져오세요. + +```ts +import { mockInvoke, mockPlatform, mockBridge, clearInvokeMocks } from '@wvb/bridge/testing'; +``` + +## 헬퍼 + +| 헬퍼 | 시그니처 | 반환값 | +| ------------------ | ------------------------------ | ---------------- | +| `mockInvoke` | `mockInvoke(command, handler)` | `Disposable` | +| `mockPlatform` | `mockPlatform(type)` | `Disposable` | +| `mockBridge` | `mockBridge(options?)` | 체이닝 가능한 목 | +| `clearInvokeMocks` | `clearInvokeMocks()` | `void` | + +### mockInvoke + +`mockInvoke(command, handler)`는 단일 명령어에 핸들러를 등록하고 `Disposable`을 반환합니다. `command`는 명령어 그룹에서 온, 점으로 구분되고 타입이 지정된 키입니다. `handler`는 일치하는 호출이 resolve할 값을 만듭니다. 반환값을 dispose하면 해당 목만 제거됩니다. + +```ts +import { mockInvoke } from '@wvb/bridge/testing'; + +const mock = mockInvoke('source.loadVersion', () => '1.2.0'); + +// 나중에 이 단일 목만 제거합니다. +mock[Symbol.dispose](); +``` + +### mockPlatform + +`mockPlatform(type)`는 `platform.type`을 지정한 값으로 강제하고 `Disposable`을 반환합니다. 웹 코드의 플랫폼별 분기를 구동할 때 사용하세요. 모든 `platform` 속성은 실시간으로 계산되므로, 반환값을 dispose하면 정상적인 감지가 복원됩니다. + +```ts +import { mockPlatform } from '@wvb/bridge/testing'; + +const mock = mockPlatform('ios'); +// platform.type === 'ios', platform.isIos === true + +mock[Symbol.dispose](); +``` + +### mockBridge + +`mockBridge(options?)`는 플랫폼 목과 명령어 목을 결합한, 체이닝 가능한 목을 반환합니다. 옵션에 `platform`을 넘겨 감지될 호스트를 설정하고, `.mockInvoke(command, handler)`를 체이닝하여 명령어를 등록하며, `.clear()`를 호출하여 목이 등록한 모든 것을 제거하세요. + +```ts +import { mockBridge } from '@wvb/bridge/testing'; + +const bridge = mockBridge({ platform: 'electron' }) + .mockInvoke('updater.getUpdate', () => ({ + name: 'app', + version: '1.2.0', + isAvailable: true, + })) + .mockInvoke('updater.download', () => undefined); + +// 테스트 대상 코드를 실행한 뒤: +bridge.clear(); +``` + +### clearInvokeMocks + +`clearInvokeMocks()`는 등록된 모든 명령어 목을 한 번에 제거합니다. 전역 teardown 훅에서 호출하여 어떤 목도 다음 테스트로 새어 나가지 않게 하세요. + +## 단위 테스트 예시 + +이 예시는 [Vitest](https://vitest.dev)를 사용하여 목으로 만든 브리지에 대해 업데이트 확인 함수를 테스트합니다. 목은 코드가 호출하는 `updater.getUpdate`와 `updater.download` 명령어를 제공하고, `.clear()`는 각 테스트 이후 상태를 초기화합니다. + +```ts title="update.test.ts" +import { afterEach, expect, test, vi } from 'vitest'; +import { updater } from '@wvb/bridge'; +import { mockBridge } from '@wvb/bridge/testing'; + +async function hasUpdate(bundleName: string) { + const update = await updater.getUpdate(bundleName); + if (!update.isAvailable) { + return false; + } + await updater.download(bundleName); + return true; +} + +let bridge: ReturnType; + +afterEach(() => { + bridge.clear(); +}); + +test('downloads when an update is available', async () => { + const download = vi.fn(() => undefined); + bridge = mockBridge({ platform: 'electron' }) + .mockInvoke('updater.getUpdate', () => ({ + name: 'app', + version: '1.2.0', + isAvailable: true, + })) + .mockInvoke('updater.download', download); + + await expect(hasUpdate('app')).resolves.toBe(true); + expect(download).toHaveBeenCalledOnce(); +}); + +test('skips download when no update is available', async () => { + const download = vi.fn(() => undefined); + bridge = mockBridge({ platform: 'electron' }) + .mockInvoke('updater.getUpdate', () => ({ + name: 'app', + version: '1.1.0', + isAvailable: false, + })) + .mockInvoke('updater.download', download); + + await expect(hasUpdate('app')).resolves.toBe(false); + expect(download).not.toHaveBeenCalled(); +}); +``` + + + 테스트 대상 코드가 호출하는 모든 명령어에 핸들러를 등록하세요. 목으로 등록되지 않은 명령어는 + 핸들러가 없으므로, 브리지는 resolve하는 대신 `BridgeError`를 노출합니다. + + +## 함께 보기 + + + + + diff --git a/content/docs/references/bridge/updater.ko.mdx b/content/docs/references/bridge/updater.ko.mdx new file mode 100644 index 0000000..817db5f --- /dev/null +++ b/content/docs/references/bridge/updater.ko.mdx @@ -0,0 +1,87 @@ +--- +title: Updater +description: 웹뷰 내부에서 무선(OTA) 업데이트 흐름을 구동하는 bridge updater.* 명령어 그룹. +--- + +`updater` 명령어 그룹은 웹뷰 내부에서 실행되는 웹 코드로부터 무선(OTA) 업데이트 흐름을 구동합니다. 이 그룹으로 더 새로운 번들이 있는지 확인하고, 스테이징한 뒤, 네이티브 호스트에서 활성화하세요. 각 메서드는 타입이 지정된 `invoke()` 호출이며, 대응하는 네이티브 업데이터 핸들러로 라우팅됩니다. 따라서 `invoke()`를 직접 다룰 일은 거의 없습니다. + +이 명령어들 뒤에 있는 OTA 개념은 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. bridge가 호출하는 네이티브 업데이터는 [Node 업데이터 API](/docs/references/node/updater)를 참고하세요. bridge 전체는 [Bridge 개요](/docs/references/bridge)를 참고하세요. + +```ts +import { updater } from '@wvb/bridge'; +``` + +## Methods + +| Method | Purpose | +| ------------------------------ | -------------------------------------- | +| `listRemotes()` | 원격 서버와 그 번들 정보를 나열합니다. | +| `getUpdate(bundleName)` | 업데이트가 있는지 확인합니다. | +| `download(bundleName)` | 설치할 업데이트를 스테이징합니다. | +| `install(bundleName, version)` | 스테이징된 버전을 활성화합니다. | + +## BundleUpdateInfo + +`getUpdate()`는 `BundleUpdateInfo`로 resolve됩니다. 이 값은 원격 번들과, 로컬에 설치된 버전과의 비교 결과를 설명합니다. + +```ts +type BundleUpdateInfo = { + name: string; + version: string; + localVersion?: string; + isAvailable: boolean; + etag?: string; + integrity?: string; + signature?: string; + lastModified?: string; +}; +``` + +업데이트를 스테이징하기 전에 `isAvailable`을 확인하세요. `version` 필드는 원격 서버가 제공하는 버전이고, `localVersion`은 호스트에 현재 설치된 버전입니다(있는 경우). + +## Update flow + +일반적인 OTA 확인은 더 새로운 번들을 스테이징하고 설치한 뒤, 웹뷰를 다시 로드하여 해당 번들을 제공합니다. + +```ts +import { updater } from '@wvb/bridge'; + +async function checkForUpdate(bundleName: string) { + const update = await updater.getUpdate(bundleName); + if (!update.isAvailable) { + return; + } + + await updater.download(bundleName); + await updater.install(bundleName, update.version); + + // 새로 설치된 번들을 웹뷰가 제공하도록 다시 로드합니다. + window.location.reload(); +} +``` + + + 네이티브 호스트에 업데이터가 설정되어 있지 않으면, 호출은 `code`가 + `BridgeErrorCode.UpdaterNotInitialized`인 `BridgeError`로 reject됩니다. 오류 처리는 [Bridge + 개요](/docs/references/bridge)를 참고하세요. + + +## See also + + + + + + diff --git a/content/docs/references/deno/index.ko.mdx b/content/docs/references/deno/index.ko.mdx new file mode 100644 index 0000000..6cc0d64 --- /dev/null +++ b/content/docs/references/deno/index.ko.mdx @@ -0,0 +1,139 @@ +--- +title: Deno API +description: '@wvb/node의 Deno FFI 대응 패키지인 @wvb/deno 패키지 레퍼런스입니다.' +--- + +실험적 + +Deno 연동은 실험적입니다. + +`@wvb/deno`는 Webview Bundle의 Deno 바인딩입니다. [`@wvb/node`](/docs/references/node)의 FFI 대응 패키지로, +동일한 프로토콜, 소스, 원격, 업데이터 기능을 제공합니다. Deno FFI로 로드한 네이티브 cdylib를 통해 +Deno 데스크톱 웹뷰에 이를 제공합니다. 이 페이지에서는 네이티브 라이브러리를 로드하는 방법을 다룹니다. +클래스, 옵션, 타입은 아래 카테고리 페이지에서 설명합니다. + +## API 표면 + +Deno 바인딩은 `@wvb/node`를 그대로 반영합니다. 각 클래스는 로드된 `lib`를 받고, +[Node API 레퍼런스](/docs/references/node)에서 설명한 것과 동일한 역할을 수행합니다. + + + + + + + + +## 네이티브 라이브러리 로드 + +Deno 바인딩은 Deno FFI로 Rust cdylib와 통신합니다. 클래스를 생성하기 전에 먼저 라이브러리를 로드하세요. +두 가지 방법이 있습니다. + +cdylib가 이미 디스크에 있고 이를 직접 가리키려면 `loadLib(libPath)`를 사용하세요. + +```ts title="main.ts" +import { loadLib } from 'jsr:@wvb/deno'; + +const lib = loadLib('./vendor/wvb/libwvb_deno.dylib'); +``` + +미리 빌드된 cdylib를 필요할 때 다운로드하려면 `loadLibViaPlug(options)`를 사용하세요. +[`@denosaurs/plug`](https://jsr.io/@denosaurs/plug)를 통해 일치하는 아티팩트를 가져오고, +게시된 SHA-256으로 검증한 뒤 로컬에 캐시합니다. + +```ts title="main.ts" +import { loadLibViaPlug } from 'jsr:@wvb/deno'; + +const lib = await loadLibViaPlug(); +``` + +로드 과정에서 해석되는 라이브러리 경로를 재정의하려면 `WVB_DENO_LIB` 환경 변수를 설정하세요. + +### cdylib 설치 + +런타임에 다운로드하는 대신 cdylib를 미리 벤더링할 수 있습니다. 설치 프로그램을 실행하세요. + +```sh +deno run -A jsr:@wvb/deno/install --out vendor/wvb +``` + +호스트가 아닌 특정 플랫폼 빌드를 가져오려면 `--target `을 전달하세요. + +```sh +deno run -A jsr:@wvb/deno/install --out vendor/wvb --target aarch64-unknown-linux-gnu +``` + +설치 프로그램은 GitHub Releases에서 자산을 다운로드하고 기본적으로 SHA-256을 검증합니다. 지원하는 +타깃은 다음과 같습니다. + +| Target triple | Platform | +| --------------------------- | --------------------- | +| `aarch64-apple-darwin` | macOS (Apple Silicon) | +| `x86_64-apple-darwin` | macOS (Intel) | +| `aarch64-unknown-linux-gnu` | Linux (arm64, glibc) | +| `x86_64-unknown-linux-gnu` | Linux (x64, glibc) | +| `x86_64-pc-windows-msvc` | Windows (x64) | + +## 인스턴스 해제 + +FFI 클래스는 네이티브 핸들을 소유합니다. 이들은 `Disposable`이므로, 사용을 마치면 `free()`를 호출하거나 +`using`으로 바인딩하여 스코프를 벗어날 때 `[Symbol.dispose]`를 통해 해제되도록 하세요. + +```ts +using protocol = new BundleProtocol(lib, source); +// 스코프가 끝나면 자동으로 해제됩니다 + +const remote = new Remote(lib, 'https://bundles.example.com'); +try { + // ... +} finally { + remote.free(); +} +``` + +## 제한 사항 + +Deno 바인딩은 FFI 경계를 넘나들기 때문에, 일부 `@wvb/node` 기능은 현재 지원 범위 밖입니다. + +- **선언적 검증만 지원합니다.** `Updater`는 선언적 `signatureVerifier` + (`SignatureVerifierOptions`)를 지원합니다. `@wvb/node`의 커스텀 함수형 `integrityChecker`와 + `signatureVerifier` 콜백은 FFI로 지원되지 않습니다. +- **`HttpOptions.defaultHeaders`는 지원되지 않습니다.** Deno `Remote`에서는 사용할 수 없습니다. + +## Deno Desktop 연동 + +`@wvb/deno-desktop`은 `@wvb/deno`를 Deno의 데스크톱 런타임에 연결합니다. `webviewBundle` 팩토리는 +`BundleSource`(및 선택적 `Remote`/`Updater`)를 빌드하고 `Deno.serve` 호환 핸들러를 노출합니다. +한편 `registerBindings`는 [`@wvb/bridge`](/docs/guide/platform-integration)의 +`source.*`/`remote.*`/`updater.*` 명령어를 `Deno.BrowserWindow`에 연결합니다. + +전체 과정을 처음부터 끝까지 살펴보려면 [Deno Desktop 가이드](/docs/guide/platforms/deno)를 참고하세요. + + + + + diff --git a/content/docs/references/deno/meta.ko.json b/content/docs/references/deno/meta.ko.json new file mode 100644 index 0000000..03d5bbf --- /dev/null +++ b/content/docs/references/deno/meta.ko.json @@ -0,0 +1,4 @@ +{ + "title": "Deno API", + "pages": ["index", "source", "protocol", "remote", "updater"] +} diff --git a/content/docs/references/deno/protocol.ko.mdx b/content/docs/references/deno/protocol.ko.mdx new file mode 100644 index 0000000..5e2bf51 --- /dev/null +++ b/content/docs/references/deno/protocol.ko.mdx @@ -0,0 +1,74 @@ +--- +title: 프로토콜 +description: 번들 파일을 제공하고 로컬 개발 URL을 웹뷰로 프록시하는 Deno 프로토콜 핸들러입니다. +--- + +Deno 바인딩은 두 가지 프로토콜 핸들러를 제공합니다. `BundleProtocol`은 커스텀 스킴을 통해 번들의 파일을 웹뷰에 제공합니다. `LocalProtocol`은 개발용으로 `app://host/...` URL을 로컬호스트 주소로 프록시합니다. 두 핸들러 모두 요청을 FFI 응답으로 해석하며, `toResponse(res)`로 표준 `Response`로 변환합니다. + +기본 개념은 [프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. Deno가 제공하는 전체 API와 네이티브 라이브러리를 로드하는 방법은 [Deno API 개요](/docs/references/deno)를 참고하세요. + +## Handlers + +`BundleProtocol`은 로드된 `lib`과 [`BundleSource`](/docs/references/deno/source)를 받습니다. 들어오는 각 요청을 해석된 번들 안의 파일로 매핑합니다. + +`LocalProtocol`은 `app://host/...` 요청을 로컬호스트 주소로 프록시합니다. 개발 서버를 대상으로 개발할 때 유용합니다. + +각 핸들러는 HTTP 메서드, 요청 URL, 헤더 객체를 받아 FFI 응답으로 해석하는 `handle` 메서드를 제공합니다. 이 응답은 `toResponse(res)`로 웹 `Response`로 변환합니다. + +| Member | Description | +| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | +| `handle(method, url, headers)` | 요청을 FFI 응답으로 해석합니다. `method`는 `HttpMethod`, `url`은 요청 URL 문자열, `headers`는 요청 헤더의 일반 객체입니다. | +| `toResponse(res)` | FFI 응답을 표준 `Response`로 변환합니다. 독립 함수로 내보내집니다. | +| `free()` | 네이티브 핸들을 해제합니다. `using` / `[Symbol.dispose]`로도 사용할 수 있습니다. | + +`HttpMethod`는 허용되는 요청 메서드의 유니온 타입입니다. 예를 들어 `'get'`과 `'head'`가 있습니다. + +## Serving a bundle + +`BundleSource`를 생성하고 `BundleProtocol`에 전달한 뒤, `Deno.serve` 콜백에서 `handle`을 호출하세요. 결과를 반환하기 전에 `toResponse`로 변환하세요. + +```ts title="serve.ts" +import { loadLibViaPlug, BundleProtocol, BundleSource, toResponse } from 'jsr:@wvb/deno'; + +const lib = await loadLibViaPlug(); + +const source = new BundleSource(lib, { + builtinDir: './bundles/builtin', + remoteDir: './bundles/remote', +}); + +using protocol = new BundleProtocol(lib, source); + +Deno.serve(async req => { + const res = await protocol.handle('get', req.url, Object.fromEntries(req.headers)); + return toResponse(res); +}); +``` + +## Disposing handlers + +프로토콜 클래스는 네이티브 핸들을 소유하므로 `Disposable`입니다. `using`으로 바인딩하면 스코프를 벗어날 때 해제되며, 작업이 끝나면 `free()`를 호출해 해제할 수도 있습니다. + +```ts +using protocol = new BundleProtocol(lib, source); +// 스코프 끝에서 자동으로 해제됨 +``` + + + `BundleProtocol`은 [`BundleSource`](/docs/references/deno/source)에서 읽어옵니다. + [Remote](/docs/references/deno/remote), [Updater](/docs/references/deno/updater)와 함께 사용하면 + 더 새로운 번들을 무선(OTA)으로 가져와 설치할 수 있습니다. + + + + + + diff --git a/content/docs/references/deno/remote.ko.mdx b/content/docs/references/deno/remote.ko.mdx new file mode 100644 index 0000000..3fc17ca --- /dev/null +++ b/content/docs/references/deno/remote.ko.mdx @@ -0,0 +1,93 @@ +--- +title: Remote +description: Deno의 Remote 클래스는 Deno FFI를 통해 원격 번들 서버와 통신하여 번들을 조회하고 다운로드합니다. +--- + +`Remote`는 Deno 바인딩에서 원격 번들 서버에 접속하는 클라이언트입니다. 서버가 알리는 번들 목록을 +조회하고 각 번들의 원시 `.wvb` 바이트를 다운로드합니다. 이를 통해 [`Updater`](/docs/references/deno/updater)가 +새 버전을 무선(OTA)으로 설치합니다. `Remote`는 [`@wvb/node`](/docs/references/node/remote)의 `Remote`와 +FFI 상의 대응 구현이며, [`@wvb/deno`](/docs/references/deno)의 나머지 부분과 마찬가지로 네이티브 FFI +경계를 넘나듭니다. + +이 관계의 서버 측 — 원격 서버가 구현하는 HTTP 규약과 실행 방법 — 은 [원격 서버 만들기](/docs/guide/remote)를 +참고하세요. + +## Remote 생성하기 + +먼저 네이티브 라이브러리를 로드한 뒤, 로드한 `lib`과 서버 엔드포인트, 그리고 선택 항목인 `RemoteOptions`로 +`Remote`를 생성합니다. + +```ts title="remote.ts" +import { loadLibViaPlug, Remote } from 'jsr:@wvb/deno'; + +const lib = await loadLibViaPlug(); +using remote = new Remote(lib, 'https://bundles.example.com'); + +const list = await remote.listBundles(); +const { info, data } = await remote.download('app'); +// info: RemoteBundleInfo, data: Uint8Array (원시 .wvb 바이트) +``` + +엔드포인트는 원격 번들 서버의 기본 URL입니다. `RemoteOptions`는 `HttpOptions` 값을 포함한 선택적 요청 +설정을 담습니다. [아래 타입 레퍼런스](#options-and-types)를 참고하세요. + +## 메서드 + +| Method | Description | +| -------------------------------------- | --------------------------------------------------------------------------------- | +| `listBundles()` | 원격 서버가 알리는 번들 목록(이름과 버전 쌍)을 조회합니다. | +| `download(bundleName)` | `bundleName`의 현재 번들을 다운로드합니다. `{ info, data }`로 resolve됩니다. | +| `downloadVersion(bundleName, version)` | `bundleName`의 특정 `version`을 다운로드합니다. `{ info, data }`로 resolve됩니다. | +| `free()` | 네이티브 핸들을 해제합니다. `[Symbol.dispose]`를 통해서도 호출됩니다. | + +`download(bundleName)`과 `downloadVersion(bundleName, version)`은 모두 `{ info, data }`로 resolve됩니다. +여기서 `info`는 `RemoteBundleInfo`이고, `data`는 원시 번들 바이트를 담은 `Uint8Array`입니다. + +```ts title="download.ts" +const { info, data } = await remote.downloadVersion('app', '1.4.0'); +await Deno.writeFile('./bundles/remote/app.wvb', data); +``` + +## 옵션과 타입 + +`Remote`는 다음 관련 타입들과 함께 동작합니다. 이 타입들의 형태는 [`@wvb/node` Remote +레퍼런스](/docs/references/node/remote)와 동일하니, 필드 전체를 자세히 확인하려면 해당 문서를 참고하세요. + +| Type | Role | +| ------------------ | ---------------------------------------------------- | +| `RemoteOptions` | 생성자에 전달하는 선택적 설정입니다. | +| `HttpOptions` | `RemoteOptions`가 담는 HTTP 요청 설정입니다. | +| `RemoteBundleInfo` | 다운로드한 번들 바이트와 함께 반환되는 `info`입니다. | + + + `HttpOptions.defaultHeaders`는 Deno `Remote`에서 지원되지 않습니다. FFI 경계를 넘나들기 때문에 이 + `@wvb/node` 기능은 여기서 사용할 수 없습니다. + + +## Remote 정리하기 + +`Remote`는 네이티브 핸들을 소유하며 `Disposable`입니다. `using`으로 바인딩하면 스코프를 벗어날 때 자동으로 +해제되고, 사용을 마쳤을 때 `free()`를 직접 호출할 수도 있습니다. + +```ts title="dispose.ts" +const remote = new Remote(lib, 'https://bundles.example.com'); +try { + const list = await remote.listBundles(); + // ... +} finally { + remote.free(); +} +``` + + + + + diff --git a/content/docs/references/deno/source.ko.mdx b/content/docs/references/deno/source.ko.mdx new file mode 100644 index 0000000..19a0ec1 --- /dev/null +++ b/content/docs/references/deno/source.ko.mdx @@ -0,0 +1,87 @@ +--- +title: 소스 +description: builtin 디렉터리와 remote 디렉터리에서 번들을 해석하는 Deno의 BundleSource 클래스입니다. +--- + +`BundleSource`는 디스크에서 번들을 읽어 들이는 Deno 바인딩의 진입점입니다. builtin 디렉터리와 remote 디렉터리에서 번들을 해석하며, 이때 remote 디렉터리가 우선합니다. 또한 [`@wvb/node`](/docs/references/node)의 `BundleSource`와 동일한 데이터 API를 제공합니다. Deno 바인딩의 다른 대부분의 클래스 — `BundleProtocol`, `Updater` — 는 `BundleSource`를 받아 이를 통해 데이터를 읽습니다. + +## 소스 생성하기 + +먼저 네이티브 라이브러리를 로드한 뒤, 로드된 `lib`와 옵션 객체를 생성자에 전달하세요. cdylib를 로드하는 두 가지 방법은 [Deno API 개요](/docs/references/deno)를 참고하세요. + +```ts title="source.ts" +import { loadLibViaPlug, BundleSource } from 'jsr:@wvb/deno'; + +const lib = await loadLibViaPlug(); + +const source = new BundleSource(lib, { + builtinDir: './bundles/builtin', + remoteDir: './bundles/remote', +}); +``` + +생성자는 두 개의 인자를 받습니다. + +| 인자 | 타입 | 설명 | +| --------- | ----------------- | ------------------------------------------------------ | +| `lib` | 로드된 라이브러리 | `loadLib`/`loadLibViaPlug`가 반환한 cdylib 핸들입니다. | +| `options` | 객체 | 해석에 사용할 builtin 및 remote 디렉터리입니다. | + +옵션은 다음과 같습니다. + +| 옵션 | 타입 | 설명 | +| ------------ | -------- | ------------------------------------------------------------------------------------- | +| `builtinDir` | `string` | 앱에 기본 포함되어 배포되는 번들을 담는 디렉터리입니다. | +| `remoteDir` | `string` | 원격에서 다운로드한 번들을 담는 디렉터리입니다. `builtinDir`보다 우선하여 해석됩니다. | + +## 소스가 하는 일 + +`BundleSource`는 Node `BundleSource`와 동일한 데이터 API를 제공합니다. + +- 해석할 수 있는 번들 목록을 조회합니다. +- 특정 버전을 로드합니다. +- 번들 또는 그 디스크립터를 가져옵니다. +- 보관 중인 remote 버전을 정리합니다. + +이 공통 API의 정확한 메서드 이름, 시그니처, 반환 타입은 Deno 바인딩이 그대로 미러링하는 [Node API 레퍼런스](/docs/references/node)를 참고하세요. + +## 소스 사용하기 + +`BundleSource`는 프로토콜과 업데이터가 데이터를 읽어 들이는 대상입니다. 번들의 파일을 웹뷰에 제공하려면 `BundleProtocol`에 전달하고, 다운로드한 최신 버전을 remote 디렉터리에 설치하려면 `Updater`에 전달하세요. + +```ts title="serve.ts" +import { loadLibViaPlug, BundleProtocol, BundleSource, toResponse } from 'jsr:@wvb/deno'; + +const lib = await loadLibViaPlug(); + +const source = new BundleSource(lib, { + builtinDir: './bundles/builtin', + remoteDir: './bundles/remote', +}); + +using protocol = new BundleProtocol(lib, source); + +Deno.serve(async req => { + const res = await protocol.handle('get', req.url, Object.fromEntries(req.headers)); + return toResponse(res); +}); +``` + +해석된 파일이 웹뷰에 어떻게 전달되는지는 [프로토콜](/docs/references/deno/protocol)을 참고하세요. + +## Deno 관련 참고 사항 + +Deno 바인딩은 Deno FFI를 통해 Rust cdylib와 통신합니다. 따라서 `BundleSource`는 직접 import하지 않고 로드된 `lib`로부터 생성합니다. FFI 클래스는 네이티브 핸들을 소유하므로, 사용이 끝나면 반드시 해제하세요. `using`으로 바인딩하여 스코프를 벗어날 때 해제하거나, `free()`를 명시적으로 호출하면 됩니다. + + + + + diff --git a/content/docs/references/deno/updater.ko.mdx b/content/docs/references/deno/updater.ko.mdx new file mode 100644 index 0000000..2889050 --- /dev/null +++ b/content/docs/references/deno/updater.ko.mdx @@ -0,0 +1,111 @@ +--- +title: Updater +description: '@wvb/deno의 Updater로 Deno 데스크톱 앱에서 원격에 최신 번들이 있는지 확인하고 설치합니다.' +--- + +`Updater`는 [원격](/docs/references/deno/remote)에서 번들의 최신 버전을 확인하고 로컬 스토어에 +설치합니다. [`BundleSource`](/docs/references/deno/source)를 `Remote`에 연결하고, 무결성 및 서명 +정책을 적용하며, 다운로드한 번들을 기록합니다. 그러면 다음 실행 때 업데이트된 앱 코드가 제공됩니다. +이 방식으로 Deno 데스크톱 앱은 새로운 릴리스 없이 무선(OTA) 업데이트를 전달합니다. 전체 흐름은 +[원격 번들](/docs/guide/remote-bundles)을 참고하세요. 바인딩의 나머지 내용은 +[Deno API 개요](/docs/references/deno)를 참고하세요. + +## Constructor + +`Updater`는 로드된 `lib`, `BundleSource`, `Remote`, 그리고 선택적인 `UpdaterOptions`로 생성합니다. +소스와 원격은 같은 `lib`를 사용해야 합니다. + +```ts title="updater.ts" +import { loadLibViaPlug, BundleSource, Remote, Updater } from 'jsr:@wvb/deno'; + +const lib = await loadLibViaPlug(); + +const source = new BundleSource(lib, { + builtinDir: './bundles/builtin', + remoteDir: './bundles/remote', +}); +using remote = new Remote(lib, 'https://bundles.example.com'); + +using updater = new Updater(lib, source, remote, { + integrityPolicy: 'strict', + signatureVerifier: { + algorithm: 'ed25519', + key: { format: 'raw', data: publicKeyBytes }, + }, +}); +``` + +| Parameter | Type | Description | +| --------- | ---------------- | ------------------------------------------------------------ | +| `lib` | loaded library | `loadLib`/`loadLibViaPlug`로 얻은 네이티브 라이브러리입니다. | +| `source` | `BundleSource` | 업데이트가 설치되는 로컬 스토어입니다. | +| `remote` | `Remote` | 업데이트를 가져오는 원격 서버입니다. | +| `options` | `UpdaterOptions` | 선택 사항. 무결성 정책과 서명 검증기입니다. | + +## Options + +`UpdaterOptions`는 가져온 번들을 설치하기 전에 어떻게 검증할지 설정합니다. + +| Option | Type | Description | +| ------------------- | -------------------------- | ------------------------------------------------------------------------------- | +| `integrityPolicy` | `IntegrityPolicy` | 무결성을 적용하는 방식입니다: `'strict'`, `'optional'`, `'none'`. | +| `signatureVerifier` | `SignatureVerifierOptions` | 선언적 서명 검증입니다. `algorithm`과 `key`(`format`과 키 `data`)로 지정합니다. | + +`IntegrityPolicy`는 `'strict' | 'optional' | 'none'` 유니온입니다. `SignatureVerifierOptions`는 +`SignatureAlgorithm`(예: `'ed25519'`)과 검증에 사용할 공개 키를 지정합니다. + + + Deno 바인딩의 검증은 선언적 방식만 지원합니다. [`@wvb/node`](/docs/references/node)의 사용자 정의 + 함수 콜백인 `integrityChecker`와 `signatureVerifier`는 FFI 경계를 넘지 못합니다. + + +## Methods + +업데이트는 세 단계로 실행합니다. 원격에 무엇이 있는지 확인하고, 다운로드한 다음, 설치합니다. + +```ts title="updater.ts" +const update = await updater.getUpdate('app'); +if (update.isAvailable) { + await updater.download('app'); + await updater.install('app', update.version); +} +``` + +| Method | Returns | Description | +| ------------------------------ | ------------------ | ---------------------------------------------------------------------- | +| `getUpdate(bundleName)` | `BundleUpdateInfo` | 원격에서 최신 버전을 확인합니다. `isAvailable`과 `version`을 읽으세요. | +| `download(bundleName)` | `Promise` | 원격에서 현재 번들을 로컬 스토어로 가져옵니다. | +| `install(bundleName, version)` | `Promise` | 다운로드한 버전을 설치하여 다음 실행 때 제공되게 합니다. | + +`BundleUpdateInfo`는 업데이트가 있는지(`isAvailable`)와 대상 버전(`version`)을 알려줍니다. + +## Disposing instances + +`Updater`, `Remote`, 그리고 다른 FFI 클래스는 네이티브 핸들을 소유합니다. 이들은 `Disposable`입니다. +사용을 마치면 `free()`를 호출하거나, `using`으로 바인딩하여 스코프를 벗어날 때 `[Symbol.dispose]`를 +통해 자동으로 해제되게 하세요. + +```ts +using updater = new Updater(lib, source, remote); +// 스코프가 끝나면 자동으로 해제됨 + +const remote = new Remote(lib, 'https://bundles.example.com'); +try { + // ... +} finally { + remote.free(); +} +``` + + + + + diff --git a/content/docs/references/index.ko.mdx b/content/docs/references/index.ko.mdx new file mode 100644 index 0000000..87863bd --- /dev/null +++ b/content/docs/references/index.ko.mdx @@ -0,0 +1,65 @@ +--- +title: 개요 +description: Rust 코어, Node 및 Deno 바인딩, 웹 측 브리지에 대한 API 레퍼런스입니다. +--- + +대부분의 앱 개발자는 이 API를 직접 다룰 일이 없습니다. 앱에 Webview Bundle을 도입하려면 플랫폼 통합 패키지를 사용하고 플랫폼 가이드를 따르세요. 이 패키지들이 코어를 대신 감싸줍니다. 이 레퍼런스는 고급 사용과 임베딩을 위한 것입니다. 코어를 직접 호스팅하거나, Node 또는 Deno 바인딩을 다루거나, 웹뷰 내부에서 네이티브 호스트를 호출하는 경우에 해당합니다. + + + 앱을 만드시나요? [플랫폼 통합 가이드](/docs/guide/platform-integration)에서 시작해 플랫폼을 + 선택하세요. [Electron](/docs/guide/platforms/electron), [Tauri](/docs/guide/platforms/tauri), + [Android](/docs/guide/platforms/android), [iOS](/docs/guide/platforms/ios), [Deno + Desktop](/docs/guide/platforms/deno) 중에서 고르면 됩니다. + + +## API 레퍼런스 + + + + + + + + +## 웹 측 브리지 + +브리지(`@wvb/bridge`)는 웹뷰 내부에서 실행되며, 웹 앱이 네이티브 호스트를 호출하도록 해줍니다. 단일 `invoke()` 함수와 타입이 지정된 `source`, `remote`, `updater` 헬퍼를 제공합니다. 덕분에 동일한 코드가 Electron, Tauri, Android, iOS에서 그대로 동작하며, 플랫폼별 전송 계층은 추상화되어 있습니다. 전체 API는 [Bridge 레퍼런스](/docs/references/bridge)를 참고하세요. + +```ts +import { invoke, updater } from '@wvb/bridge'; + +// 타입이 지정된 도메인 헬퍼 +const update = await updater.getUpdate('my-app'); +if (update.isAvailable) { + await updater.download('my-app'); + await updater.install('my-app', update.version); +} + +// 또는 명령어를 직접 호출 +const bundles = await invoke('sourceListBundles'); +``` + +네이티브 측은 `@wvb/node`를 호스팅하며 이 호출들에 응답합니다. 브리지가 각 플랫폼에 어떻게 들어맞는지는 [플랫폼 통합 가이드](/docs/guide/platform-integration)를 참고하세요. + +## 그 밖의 플랫폼 + +Tauri와 모바일 통합은 각자의 API를 제공하며, 해당 가이드와 함께 문서화되어 있습니다. + +- Tauri는 Rust 크레이트 `wvb-tauri`로 제공됩니다. [Tauri 가이드](/docs/guide/platforms/tauri)를 참고하세요. +- Android(Kotlin)와 iOS(Swift) 바인딩은 [Android 가이드](/docs/guide/platforms/android)와 [iOS 가이드](/docs/guide/platforms/ios)에서 다룹니다. diff --git a/content/docs/references/meta.ko.json b/content/docs/references/meta.ko.json new file mode 100644 index 0000000..7629f78 --- /dev/null +++ b/content/docs/references/meta.ko.json @@ -0,0 +1,5 @@ +{ + "root": true, + "title": "레퍼런스", + "pages": ["index", "[Rust (docs.rs)](https://docs.rs/wvb)", "node", "deno", "bridge"] +} diff --git a/content/docs/references/node/bundle.ko.mdx b/content/docs/references/node/bundle.ko.mdx new file mode 100644 index 0000000..04f0a89 --- /dev/null +++ b/content/docs/references/node/bundle.ko.mdx @@ -0,0 +1,84 @@ +--- +title: Bundle +description: .wvb 아카이브를 읽고, 빌드하고, 검사하기 위한 @wvb/node 클래스. +--- + +`@wvb/node`는 메모리와 디스크에서 `.wvb` 아카이브를 다루는 소규모 클래스 집합을 제공합니다. `Bundle`과 `BundleBuilder`는 번들 전체를 읽고 씁니다. `BundleDescriptor`, `Header`, `Index`, `LoadedDescriptor`는 번들의 구조를 검사하고, 전체를 메모리에 올리지 않고도 개별 파일을 가져오게 해줍니다. + +소스, 프로토콜, 원격 클라이언트, 업데이터까지 패키지의 전체 표면은 [Node API 개요](/docs/references/node)를 참고하세요. 바이트가 디스크에 배치되는 방식은 [.wvb 번들 포맷](/docs/guide/bundle-format)을 참고하세요. + +## 번들 읽기와 파일 가져오기 + +`readBundle`을 호출해 디스크의 `.wvb` 파일을 파싱한 다음, `getData`로 개별 엔트리를 가져옵니다. `getData`는 `Buffer`를 반환하며, 경로가 번들에 없으면 `null`을 반환합니다. + +```ts +import { readBundle } from '@wvb/node'; + +const bundle = await readBundle('./dist/app.wvb'); + +const html = bundle.getData('index.html'); +if (html) { + console.log(html.toString('utf8')); +} +``` + +## 번들과 빌더 + +`Bundle`은 메모리에 보관된, 파싱이 끝난 아카이브입니다. `readBundle`(또는 `readBundleFromBuffer`)에서 얻거나 `BundleBuilder.build`를 호출해 얻습니다. `BundleBuilder`는 엔트리를 모아 `Bundle`을 만들며, 이렇게 만든 번들을 디스크에 쓸 수 있습니다. + +| 클래스 | 생성자 | 주요 메서드 | +| --------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Bundle` | `readBundle` / `BundleBuilder.build`로 생성됨 | `descriptor(): BundleDescriptor`; `getData(path: string): Buffer \| null`; `getDataChecksum(path: string): number \| null` | +| `BundleBuilder` | `new BundleBuilder(version?: Version)` | `get version: Version`; `entryPaths(): Array`; `insertEntry(path, data: Buffer, contentType?, headers?: Record): boolean`; `removeEntry(path): boolean`; `containsEntry(path): boolean`; `build(options?: BuildOptions): Bundle` | + + + `getDataChecksum`은 파일 바이트의 내부 xxHash-32 체크섬을 반환하며, 이는 아카이브 내부의 손상을 + 감지하는 데 쓰입니다. 이는 번들의 무결성 해시와는 다릅니다. 무결성 해시는 SHA-2 (`sha256`, + `sha384`, `sha512`)를 사용해 다운로드된 내용을 검증합니다. + + +## 디스크립터, 헤더, 인덱스 + +디스크립터는 모든 파일을 메모리에 올리지 않고 번들의 구조를 노출합니다. `bundle.descriptor()`를 호출해 `BundleDescriptor`를 얻은 다음, 그 `Header`와 `Index`를 읽거나 필요할 때 개별 파일을 가져옵니다. + +| 클래스 | 주요 메서드 | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `BundleDescriptor` | `header(): Header`; `index(): Index`; `getData(filepath, path): Buffer \| null`; `getDataChecksum(filepath, path): number \| null`; `asyncGetData(...)`; `asyncGetDataChecksum(...)` | +| `Header` | `version(): Version`; `indexEndOffset(): bigint`; `indexSize(): number` | +| `Index` | `entries(): Record`; `getEntry(path): IndexEntry \| null`; `containsPath(path): boolean` | +| `LoadedDescriptor` | `descriptor(): BundleDescriptor`; `getData(path): Promise` (지연 디스크 읽기); `getDataChecksum(path): Promise` | + +`Header`는 번들의 포맷 `version`과 인덱스가 파일 내 어디에 위치하는지를 설명합니다. `Index`는 각 파일 경로를 `IndexEntry`에 매핑합니다. + +```ts +type IndexEntry = { + offset: number; + len: number; // 압축된 길이 + isEmpty: boolean; + contentType: string; + contentLength: number; // 압축 해제된 길이 + headers: Record; +}; +``` + +`LoadedDescriptor`는 디스크의 파일에 대한 참조 카운트 핸들을 보유하며, 이 핸들은 가비지 컬렉션 시 해제됩니다. 따라서 `getData`와 `getDataChecksum`은 아카이브 전체를 버퍼링하지 않고 지연 방식으로 읽습니다. + + + `Version` 열거형에는 `'v1'`이라는 값 하나만 있습니다. `.wvb` 포맷 버전은 `@wvb/node` 패키지 버전과 + 독립적입니다. + + +## 관련 항목 + + + + + diff --git a/content/docs/references/node/index.ko.mdx b/content/docs/references/node/index.ko.mdx new file mode 100644 index 0000000..500ef60 --- /dev/null +++ b/content/docs/references/node/index.ko.mdx @@ -0,0 +1,117 @@ +--- +title: Node API +description: Node.js에서 Webview Bundle을 읽고, 제공하고, 업데이트하는 N-API 네이티브 애드온인 @wvb/node의 레퍼런스입니다. +--- + +`@wvb/node` 0.1.0은 Webview Bundle의 Node.js 바인딩입니다. N-API 네이티브 애드온(`wvb-node` Rust 크레이트)으로 제공되며, 플랫폼별 사전 빌드 바이너리를 포함합니다. 따라서 Rust 툴체인 없이 설치하고 실행할 수 있습니다. 이 패키지는 번들 리더와 라이터, `BundleSource`, 프로토콜 핸들러, `Remote` 클라이언트, `Updater`를 노출합니다. 모두 `@wvb/electron`과 CLI의 원격 작업을 뒷받침하는 것과 동일한 구성 요소입니다. + +웹뷰 안에서 실행되는 웹 앱은 `@wvb/node`를 직접 호출하지 않습니다. 대신 [`@wvb/bridge`](/docs/references/bridge)를 사용합니다. `@wvb/bridge`의 `source`, `remote`, `updater` 명령어가 여기서 문서화하는 클래스에 대응합니다. 전체 흐름은 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. 실험적인 Deno 대응 구현은 [Deno API](/docs/references/deno)를 참고하세요. + + +애드온은 `@wvb/node-` 선택적 의존성 중 하나에서 사전 빌드 바이너리를 해석합니다(macOS, Linux, Windows, Android에 걸친 12개의 NAPI 타깃). 지원되는 Node 엔진은 `>= 12.22.0 < 13`, `>= 14.17.0 < 15`, `>= 15.12.0 < 16`, `>= 16.0.0`입니다. + + +## Install + + + + +```sh +npm install @wvb/node +``` + + + + +```sh +pnpm add @wvb/node +``` + + + + +```sh +yarn add @wvb/node +``` + + + + +## Top-level functions + +이 함수들은 `.wvb` 아카이브를 읽고 씁니다. + +| Function | Signature | Returns | +| ----------------------- | ---------------------------------------------------------------- | ---------------------------- | +| `readBundle` | `readBundle(filepath: string): Promise` | 디스크에서 읽어 파싱한 번들. | +| `readBundleFromBuffer` | `readBundleFromBuffer(buffer: Buffer): Bundle` | 메모리 버퍼에서 파싱한 번들. | +| `writeBundle` | `writeBundle(bundle: Bundle, filepath: string): Promise` | 기록한 바이트 수. | +| `writeBundleIntoBuffer` | `writeBundleIntoBuffer(bundle: Bundle): Buffer` | 직렬화한 `.wvb` 바이트. | + +## API reference + +클래스, 열거형, 옵션 타입은 주제별 페이지로 나누어 정리했습니다. + + + + + + + + + + +## Example + +디스크에서 `.wvb` 아카이브를 읽고 그 안에서 파일 하나를 꺼냅니다. + +```ts +import { readBundle } from '@wvb/node'; + +const bundle = await readBundle('./dist/app.wvb'); + +const html = bundle.getData('index.html'); +if (html) { + console.log(html.toString('utf8')); +} +``` + +## Related + + + + + + diff --git a/content/docs/references/node/meta.ko.json b/content/docs/references/node/meta.ko.json new file mode 100644 index 0000000..d5ece34 --- /dev/null +++ b/content/docs/references/node/meta.ko.json @@ -0,0 +1,4 @@ +{ + "title": "Node API", + "pages": ["index", "bundle", "source", "protocol", "remote", "updater", "types"] +} diff --git a/content/docs/references/node/protocol.ko.mdx b/content/docs/references/node/protocol.ko.mdx new file mode 100644 index 0000000..87ca928 --- /dev/null +++ b/content/docs/references/node/protocol.ko.mdx @@ -0,0 +1,86 @@ +--- +title: 프로토콜 +description: '@wvb/node 프로토콜 핸들러 레퍼런스 — BundleProtocol은 번들 자산을 제공하고, LocalProtocol은 localhost로 프록시합니다.' +--- + +`@wvb/node`는 웹뷰의 요청을 HTTP 방식의 응답으로 바꾸는 두 가지 프로토콜 핸들러를 제공합니다. `BundleProtocol`은 `BundleSource`에서 자산을 제공하고, `LocalProtocol`은 개발 중에 요청을 localhost 서버로 프록시합니다. 두 핸들러 모두 HTTP 메서드, URI, 그리고 선택적 헤더를 받아 `HttpResponse`로 반환합니다. 이 스킴들이 웹뷰 플랫폼에 어떻게 들어맞는지는 [프로토콜 처리](/docs/guide/protocol-handling)를 참고하세요. + +두 핸들러는 동일한 호출 형태를 공유합니다. + +```ts +handle( + method: HttpMethod, + uri: string, + headers?: Record, +): Promise +``` + +`HttpMethod`와 `HttpResponse`는 [타입](/docs/references/node/types)에 문서화되어 있습니다. `HttpMethod`는 소문자 문자열 유니온(`'get'`, `'head'` 등)입니다. `HttpResponse`는 `{ status, headers, body }`이며, 여기서 `body`는 `Buffer`입니다. + +## BundleProtocol + +`new BundleProtocol(source: BundleSource)`는 `scheme://bundle_name/path` 요청을 [`BundleSource`](/docs/references/node/source)에서 제공합니다. 번들 내부에서 요청한 파일을 찾아 그 바이트를 반환합니다. + +| Member | Signature | +| ----------- | ---------------------------------------------------------------------------------------------- | +| Constructor | `new BundleProtocol(source: BundleSource)` | +| `handle` | `(method: HttpMethod, uri: string, headers?: Record) => Promise` | + +`BundleProtocol`은 `GET`과 `HEAD` 요청에 응답합니다. `GET`은 파일 내용을 `body`에 담아 반환하고, `HEAD`는 본문 없이 응답 메타데이터만 반환합니다. 요청에 `Range` 헤더가 있으면 핸들러는 상태 `206`으로 부분 응답을 제공합니다. + +## LocalProtocol + +`new LocalProtocol(hosts: Record)`는 `app://host/...` 요청을 localhost URL로 프록시합니다. 각 가상 호스트를 전달할 로컬 오리진에 매핑하세요. 이 방식은 웹뷰가 패킹된 `.wvb` 번들 대신 개발 서버를 가리키는 개발 환경에서 유용합니다. + +| Member | Signature | +| ----------- | ---------------------------------------------------------------------------------------------- | +| Constructor | `new LocalProtocol(hosts: Record)` | +| `handle` | `(method: HttpMethod, uri: string, headers?: Record) => Promise` | + +`LocalProtocol`은 응답을 캐시하며, 업스트림 리소스가 변경되지 않았으면 `304`를 반환합니다. + + + 프로덕션에서 패킹된 번들을 제공할 때는 `BundleProtocol`을 사용하고, 개발 중에 개발 서버로 프록시할 + 때는 `LocalProtocol`을 사용하세요. 두 스킴 중 하나를 플랫폼에 연결하는 방법은 [프로토콜 + 처리](/docs/guide/protocol-handling)를 참고하세요. + + +## 프로토콜로 요청 처리하기 + +`BundleSource`를 만들고 `BundleProtocol`로 감싼 뒤, 웹뷰의 요청에 응답하세요. + +```ts +import { BundleSource, BundleProtocol } from '@wvb/node'; + +const source = new BundleSource({ + builtinDir: './bundles/builtin', + remoteDir: './bundles/remote', +}); + +const protocol = new BundleProtocol(source); + +const res = await protocol.handle('get', 'app://my-app/index.html'); +console.log(res.status); // 200 +console.log(res.headers['content-type']); +res.body; // Buffer +``` + +## 관련 문서 + + + + + + diff --git a/content/docs/references/node/remote.ko.mdx b/content/docs/references/node/remote.ko.mdx new file mode 100644 index 0000000..ce35ca2 --- /dev/null +++ b/content/docs/references/node/remote.ko.mdx @@ -0,0 +1,140 @@ +--- +title: Remote +description: 원격에서 번들을 나열하고, 살펴보고, 다운로드하는 @wvb/node HTTP 클라이언트입니다. +--- + +`Remote`는 [번들 원격](/docs/guide/remote)을 위한 `@wvb/node` HTTP 클라이언트입니다. [Remote HTTP Spec](/docs/guide/spec)을 구현한 모든 서버와 통신합니다. 따라서 Node.js에서 사용 가능한 번들을 나열하고, 번들의 메타데이터를 읽고, 번들 바이트를 다운로드할 수 있습니다. [Updater](/docs/references/node/updater)는 `Remote` 위에서 업데이트를 준비하고 활성화합니다. 원격에 더 낮은 수준으로 접근해야 할 때는 `Remote`를 직접 사용하세요. + +## Constructor + +```ts +new Remote(endpoint: string, options?: RemoteOptions) +``` + +원격의 기본 `endpoint`를 전달하세요(예: `https://bundles.example.com`). 클라이언트는 HTTP 규약에 따라 해당 엔드포인트로 요청을 보냅니다. + +```ts +import { Remote } from '@wvb/node'; + +const remote = new Remote('https://bundles.example.com'); +``` + +## Methods + +| Method | Signature | +| ----------------- | --------------------------------------------------------------------- | +| `listBundles` | `(channel?: string): Promise` | +| `getInfo` | `(bundleName: string, channel?: string): Promise` | +| `download` | `(bundleName, channel?): Promise<[RemoteBundleInfo, Bundle, Buffer]>` | +| `downloadVersion` | `(bundleName, version): Promise<[RemoteBundleInfo, Bundle, Buffer]>` | + +`listBundles`는 원격이 제공하는 번들을 나열하며, `channel`로 필터링할 수도 있습니다. `getInfo`는 단일 번들의 현재 메타데이터를 읽습니다. `download`는 번들의 현재 버전을 가져오고, `downloadVersion`은 특정 버전을 가져옵니다. 두 다운로드 메서드 모두 번들 정보, 파싱된 [`Bundle`](/docs/references/node/bundle), 그리고 원본 `.wvb` 바이트를 담은 `Buffer`로 이루어진 튜플로 반환됩니다. + +```ts +const [info, bundle, bytes] = await remote.download('my-app', 'stable'); + +console.log(info.name, info.version); +console.log(bundle.getData('index.html')?.toString('utf8')); +console.log(bytes.byteLength); +``` + +## RemoteOptions + +`RemoteOptions`는 HTTP 전송과 선택적인 다운로드 진행률 콜백을 설정합니다. + +```ts +type RemoteOptions = { + http?: HttpOptions; + onDownload?: (data: RemoteOnDownloadData) => void; +}; + +type RemoteOnDownloadData = { + downloadedBytes: number; + totalBytes?: number; + endpoint: string; +}; +``` + +`onDownload`는 바이트가 도착할 때마다 실행됩니다. `totalBytes`는 원격이 콘텐츠 길이를 알려줄 때 설정됩니다. + +```ts +const remote = new Remote('https://bundles.example.com', { + onDownload: ({ downloadedBytes, totalBytes }) => { + if (totalBytes) { + console.log(`${Math.round((downloadedBytes / totalBytes) * 100)}%`); + } + }, +}); +``` + +## HttpOptions + +`HttpOptions`는 내부 HTTP 클라이언트를 조정합니다. 모든 필드는 선택 사항입니다. + +| Field | Type | Description | +| -------------------- | ------------------------ | ------------------------------------------ | +| `defaultHeaders` | `Record` | 모든 요청에 함께 보내는 헤더입니다. | +| `userAgent` | `string` | `User-Agent` 헤더 값입니다. | +| `timeout` | `number` | 전체 요청 타임아웃(밀리초 단위)입니다. | +| `readTimeout` | `number` | 읽기 타임아웃(밀리초 단위)입니다. | +| `connectTimeout` | `number` | 연결 타임아웃(밀리초 단위)입니다. | +| `poolIdleTimeout` | `number` | 풀링된 연결의 유휴 타임아웃입니다. | +| `poolMaxIdlePerHost` | `number` | 호스트당 유지하는 최대 유휴 연결 수입니다. | +| `referer` | `boolean` | `Referer` 헤더를 보낼지 여부입니다. | +| `tcpNodelay` | `boolean` | 소켓에서 `TCP_NODELAY`를 활성화합니다. | +| `hickoryDns` | `boolean` | Hickory DNS 리졸버를 사용합니다. | + + + 기본 요청 타임아웃은 120초입니다. `HttpOptions.timeout`(밀리초)으로 재정의하세요. + + +```ts +const remote = new Remote('https://bundles.example.com', { + http: { + timeout: 30_000, + userAgent: 'my-app/1.0', + }, +}); +``` + +## Result shapes + +`getInfo`, `download`, `downloadVersion`는 `RemoteBundleInfo`를 반환합니다. `listBundles`는 `ListRemoteBundleInfo` 배열을 반환합니다. + +```ts +type ListRemoteBundleInfo = { + name: string; + version: string; +}; + +type RemoteBundleInfo = { + name: string; + version: string; + etag?: string; + integrity?: string; // ":", 예: "sha256:n4bQ…" + signature?: string; // base64 + lastModified?: string; +}; +``` + +`integrity`와 `signature` 필드는 원격이 `Webview-Bundle-Integrity`와 `Webview-Bundle-Signature` 응답 헤더로 알리는 값을 담습니다. `404`(배포되지 않음)나 `403`(금지됨) 같은 실패 응답을 클라이언트가 어떻게 표면화하는지는 [Errors](/docs/guide/spec/errors)를 참고하세요. + +## Related + + + + + + diff --git a/content/docs/references/node/source.ko.mdx b/content/docs/references/node/source.ko.mdx new file mode 100644 index 0000000..0c518b2 --- /dev/null +++ b/content/docs/references/node/source.ko.mdx @@ -0,0 +1,90 @@ +--- +title: Source +description: BundleSource는 builtin 디렉터리와 remote 디렉터리에 걸쳐 번들을 해석하며, remote 사본이 우선합니다. +--- + +`BundleSource`는 주어진 번들 이름에 대해 어떤 `.wvb` 파일을 제공할지 결정하는 `@wvb/node`의 클래스입니다. 이 클래스는 두 디렉터리에 걸쳐 번들을 관리합니다. 하나는 앱과 함께 배포된 번들을 담는 builtin 디렉터리이고, 다른 하나는 무선(OTA)으로 다운로드한 번들을 담는 remote 디렉터리입니다. 두 디렉터리가 같은 이름의 번들을 모두 가지고 있으면 remote 사본이 우선합니다. 따라서 OTA 업데이트가 앱과 함께 배포된 버전을 자연스럽게 대체합니다. + +`BundleSource`는 [`BundleProtocol`](/docs/references/node/protocol)이 번들을 제공하는 기반이자 [`Updater`](/docs/references/node/updater)가 다운로드를 준비해 두는 기반입니다. 매니페스트, 채널, 디렉터리 구조가 어떻게 맞물리는지는 [Bundle sources](/docs/guide/bundle-sources)를 참고하세요. 패키지 전체 구성은 [Node API 개요](/docs/references/node)를 참고하세요. + +## Constructor + +```ts +new BundleSource(config: BundleSourceConfig) +``` + +`config`는 [`BundleSourceConfig`](/docs/references/node/types)입니다. 이 설정은 소스가 두 디렉터리를 가리키게 하며, 선택적으로 명시적인 매니페스트 파일 경로도 지정합니다: + +```ts +import { BundleSource } from '@wvb/node'; + +const source = new BundleSource({ + builtinDir: './bundles/builtin', + remoteDir: './bundles/remote', +}); +``` + + + 번들 이름이 두 디렉터리에서 모두 해석되면 `BundleSource`는 remote 버전을 반환합니다. 이 덕분에 + 다운로드한 업데이트가 네이티브 앱스토어 릴리스 없이도 builtin 번들을 대체합니다. + + +## Methods + +| Method | Signature | +| -------------------------- | ---------------------------------------------------------------- | +| `listBundles` | `(): Promise` | +| `loadVersion` | `(bundleName): Promise` | +| `updateRemoteVersion` | `(bundleName, version): Promise` | +| `resolveFilepath` | `(bundleName): Promise` | +| `getBuiltinBundleFilepath` | `(bundleName, version): string` | +| `getRemoteBundleFilepath` | `(bundleName, version): string` | +| `fetchBundle` | `(bundleName): Promise` | +| `fetchBuiltinBundle` | `(name, version): Promise` | +| `fetchRemoteBundle` | `(name, version): Promise` | +| `fetchDescriptor` | `(bundleName): Promise` | +| `loadBuiltinMetadata` | `(name, version): Promise` | +| `loadRemoteMetadata` | `(name, version): Promise` | +| `writeRemoteBundle` | `(name, version, bundle, metadata): Promise` | +| `loadDescriptor` | `(bundleName): Promise` (single-flight cached) | +| `unloadDescriptor` | `(bundleName): boolean` | +| `removeRemoteBundle` | `(name, version): Promise` | +| `remoteRetainedVersions` | `(name): Promise` (current + previous) | +| `pruneRemoteBundles` | `(name): Promise` | + +`listBundles`는 두 디렉터리에 걸친 모든 번들을 [`ListBundleItem`](/docs/references/node/types)으로 보고합니다. 각 항목은 `type`(`builtin` 또는 `remote`), 이름, 버전, 현재 버전 여부, 매니페스트 메타데이터를 담습니다. `loadVersion`은 이름에 대한 유효 버전을 해석하고, 그 버전이 어느 디렉터리에서 왔는지 알려줍니다. `resolveFilepath`는 remote 우선 규칙에 따라 선택되는 경로를 반환합니다. 반면 `getBuiltinBundleFilepath`와 `getRemoteBundleFilepath`는 특정 버전에 대해 디렉터리별 경로를 계산합니다. + +`fetch*` 메서드는 파싱된 [`Bundle`](/docs/references/node/bundle)을 읽습니다. `fetchBundle`은 우선 규칙을 따르고, `fetchBuiltinBundle`과 `fetchRemoteBundle`은 특정 디렉터리 하나만 대상으로 합니다. `fetchDescriptor`는 모든 파일을 메모리에 로드하지 않고 [`BundleDescriptor`](/docs/references/node/bundle)를 반환합니다. + +`loadDescriptor`는 single-flight 방식으로 `LoadedDescriptor`를 캐시합니다. 따라서 동시에 호출한 여러 호출자가 하나의 로드를 공유합니다. `unloadDescriptor`는 캐시된 핸들을 해제합니다. remote 디렉터리에 기록하는 경로는 `writeRemoteBundle`입니다. 이 메서드는 다운로드한 번들과 그 [`BundleManifestMetadata`](/docs/references/node/types)를 저장합니다. `remoteRetainedVersions`는 소스가 이름에 대해 보관하는 버전(현재 버전과 이전 버전)을 나열합니다. `removeRemoteBundle`은 특정 버전을 삭제하고, `pruneRemoteBundles`는 더 이상 보관되지 않는 버전을 제거합니다. + + + 상위 수준의 OTA 흐름은 대개 [`Updater`](/docs/references/node/updater)를 통해 이 메서드들을 + 호출합니다. `Updater`는 remote 버전을 준비하고 활성화해 줍니다. 해석, 메타데이터, 정리를 세밀하게 + 제어해야 할 때는 `BundleSource`를 직접 사용하세요. + + +## Related + + + + + + + diff --git a/content/docs/references/node/types.ko.mdx b/content/docs/references/node/types.ko.mdx new file mode 100644 index 0000000..e92b7dc --- /dev/null +++ b/content/docs/references/node/types.ko.mdx @@ -0,0 +1,169 @@ +--- +title: 타입 +description: '@wvb/node API 전반에서 공유되는 열거형, 유니온, 옵션 인터페이스, 결과 타입입니다.' +--- + +[Node API](/docs/references/node)의 클래스와 함수가 반환하고 받아들이는 보조 타입들입니다. reader, [BundleSource](/docs/references/node/source), [프로토콜 핸들러](/docs/references/node/protocol), [Remote](/docs/references/node/remote), [Updater](/docs/references/node/updater) 페이지가 여기서 정의한 형태들을 참조합니다. + +## 열거형과 유니온 + +고정된 값 집합에 이름을 붙이는 문자열 리터럴 유니온입니다. + +```ts +type Version = 'v1'; + +type HttpMethod = + | 'get' + | 'head' + | 'options' + | 'post' + | 'put' + | 'patch' + | 'delete' + | 'trace' + | 'connect'; + +type BundleSourceKind = 'builtin' | 'remote'; +``` + +`Version`은 번들 포맷 버전입니다. 현재는 `v1`만 존재합니다. + +### 무결성 + +```ts +// SHA-2 계열이며, sha384를 권장합니다. +type IntegrityAlgorithm = 'sha256' | 'sha384' | 'sha512'; + +// optional이 기본값입니다. +type IntegrityPolicy = 'strict' | 'optional' | 'none'; +``` + +무결성은 SHA-2를 사용하며, `":"` 형태로 직렬화됩니다(예: `sha256:n4bQ…`). `strict`는 값이 존재하고 일치해야 합니다. `optional`은 값이 존재할 때만 검증합니다. `none`은 검사를 건너뜁니다. [Remote, 무결성 및 서명 설정](/docs/config/remote)을 참고하세요. + +### 서명 + +```ts +type SignatureAlgorithm = + | 'ecdsaSecp256R1' + | 'ecdsaSecp384R1' + | 'ed25519' + | 'rsaPkcs1V15' + | 'rsaPss'; + +type VerifyingKeyFormat = 'spkiDer' | 'spkiPem' | 'pkcs1Der' | 'pkcs1Pem' | 'sec1' | 'raw'; +``` + +서명은 무결성 문자열의 바이트에 서명하여 누가 번들을 게시했는지 증명합니다. 따라서 서명 검증에는 무결성 값이 존재해야 합니다. + + + 키 포맷 제약: `pkcs1Der`와 `pkcs1Pem`은 RSA에만, `sec1`은 ECDSA에만, `raw`는 Ed25519에만(32바이트 + 키) 적용됩니다. + + +## 옵션 인터페이스 + +생성자와 메서드에 전달하는 형태입니다. + +```ts +type BundleSourceConfig = { + builtinDir: string; + remoteDir: string; + builtinManifestFilepath?: string; + remoteManifestFilepath?: string; +}; + +type UpdaterOptions = { + channel?: string; + integrityPolicy?: IntegrityPolicy; + integrityChecker?: (data: Uint8Array, integrity: string) => Promise; + signatureVerifier?: + | SignatureVerifierOptions + | ((data: Uint8Array, signature: string) => Promise); +}; + +type SignatureVerifierOptions = { + algorithm: SignatureAlgorithm; + key: { format: VerifyingKeyFormat; data: string | Uint8Array }; +}; +``` + +`UpdaterOptions`는 선언적 `signatureVerifier`(알고리즘과 키)와 커스텀 콜백을 모두 받습니다. `integrityChecker`도 마찬가지로, 기본 SHA-2 검사를 직접 만든 함수로 대체할 수 있습니다. + +## 결과 타입 + +reader, [BundleSource](/docs/references/node/source), [Updater](/docs/references/node/updater)가 반환하는 형태입니다. + +```ts +type BundleManifestMetadata = { + etag?: string; + integrity?: string; // ":", 예: "sha256:n4bQ…" + signature?: string; // base64 + lastModified?: string; +}; + +// BundleSource.loadVersion() +type BundleSourceVersion = { + type: BundleSourceKind; // 'builtin' | 'remote' + version: string; +}; + +// BundleSource.listBundles() +type ListBundleItem = { + type: BundleSourceKind; + name: string; + version: string; + current: boolean; + metadata: BundleManifestMetadata; +}; + +// Updater.getUpdate() +type BundleUpdateInfo = { + name: string; + version: string; // 배포된 버전 + localVersion?: string; // 설치된 버전이 있다면 그 버전 + isAvailable: boolean; // version !== localVersion일 때 true + etag?: string; + integrity?: string; + signature?: string; + lastModified?: string; +}; + +// BundleBuilder.build() — seed는 테스트용입니다 +type BuildOptions = { + header?: { checksumSeed?: number }; + index?: { checksumSeed?: number }; + dataChecksumSeed?: number; +}; + +// 프로토콜 핸들러의 handle()이 반환합니다 +type HttpResponse = { + status: number; + headers: Record; + body: Buffer; +}; +``` + +## 관련 문서 + + + + + + + diff --git a/content/docs/references/node/updater.ko.mdx b/content/docs/references/node/updater.ko.mdx new file mode 100644 index 0000000..59b2ea4 --- /dev/null +++ b/content/docs/references/node/updater.ko.mdx @@ -0,0 +1,162 @@ +--- +title: Updater +description: '@wvb/node의 Updater 클래스 — 원격 서버에서 최신 번들을 확인하고, 무결성과 서명 검증을 거쳐 스테이징한 뒤 활성화합니다.' +--- + +`Updater`는 번들의 무선(OTA) 업데이트를 처리합니다. [`BundleSource`](/docs/references/node/source)와 [`Remote`](/docs/references/node/remote)를 짝지어 동작합니다. 원격 서버에서 더 새로운 버전을 확인하고, 소스의 원격 디렉터리로 다운로드한 뒤 활성화합니다. 확인, 다운로드, 활성화 단계는 각각 별도의 메서드입니다. 따라서 백그라운드에서 업데이트를 스테이징해 두고 나중에 전환할 수 있습니다. 예를 들어 다음 앱 재시작 시점에 전환하면 됩니다. 전체 흐름은 [원격 번들](/docs/guide/remote-bundles)을 참고하세요. + +```ts +import { BundleSource, Remote, Updater } from '@wvb/node'; + +const updater = new Updater(source, remote, options); +``` + +## Constructor + +`new Updater(source: BundleSource, remote: Remote, options?: UpdaterOptions)` + +| Argument | Type | Description | +| --------- | ---------------------------------------------- | ----------------------------------------------------------- | +| `source` | [`BundleSource`](/docs/references/node/source) | 스테이징된 번들과 활성 번들이 디스크에 저장되는 위치입니다. | +| `remote` | [`Remote`](/docs/references/node/remote) | 원격 서버용 HTTP 클라이언트입니다. | +| `options` | `UpdaterOptions` | 채널과 검증 설정입니다(선택). | + +다운로드와 설치는 번들 단위로 직렬화됩니다. 따라서 같은 번들 이름에 대한 동시 호출은 한 번에 하나씩 실행됩니다. + +## Methods + +| Method | Signature | Behavior | +| ------------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `listRemotes` | `(): Promise` | 원격 서버에서 사용 가능한 번들을 나열합니다(설정된 `channel`을 따릅니다). | +| `getUpdate` | `(bundleName): Promise` | 원격 서버의 현재 버전을 로컬 버전과 비교합니다. 이것이 확인 단계입니다. | +| `download` | `(bundleName, version?): Promise` | 버전을 원격 디렉터리에 스테이징합니다. 설정된 경우 무결성과 서명을 검증합니다. 활성화하지는 않습니다. | +| `install` | `(bundleName, version): Promise` | 스테이징된 버전을 활성화합니다. 다시 검증하고, 현재 버전을 교체하고, 캐시된 디스크립터를 버리고, 오래된 버전을 정리합니다. | + +`getUpdate`가 확인 단계입니다. `check`라는 이름의 메서드는 없습니다. `download`와 `install`을 분리해 두면 업데이트 단계를 재시작할 수 있습니다. 네트워크가 가능할 때 바이트를 스테이징한 뒤, 안전한 시점에 활성화하세요. + +`download`에서 `version` 인자를 생략하면, 업데이터는 설정된 채널의 현재 원격 버전을 가져옵니다. 특정 버전을 스테이징하려면 그 `version`을 직접 전달하세요. + + + `getUpdate`는 `BundleUpdateInfo`를 반환합니다. 원격 서버에 배포된 `version`이 설치된 + `localVersion`과 다르면 `isAvailable`이 `true`입니다. 이 값으로 다운로드와 설치 호출을 제어하세요. + + +## UpdaterOptions + +```ts +type UpdaterOptions = { + channel?: string; + integrityPolicy?: IntegrityPolicy; + integrityChecker?: (data: Uint8Array, integrity: string) => Promise; + signatureVerifier?: + | SignatureVerifierOptions + | ((data: Uint8Array, signature: string) => Promise); +}; + +type SignatureVerifierOptions = { + algorithm: SignatureAlgorithm; + key: { format: VerifyingKeyFormat; data: string | Uint8Array }; +}; +``` + +| Field | Type | Description | +| ------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------ | +| `channel` | `string` | 나열 및 확인 호출 시 원격 서버로 전달되는 릴리스 채널입니다. | +| `integrityPolicy` | `IntegrityPolicy` | `'strict'`, `'optional'`(기본값), 또는 `'none'`입니다. | +| `integrityChecker` | `(data, integrity) => Promise` | 내장 SHA-2 비교를 대체하는 커스텀 무결성 검사입니다. | +| `signatureVerifier` | `SignatureVerifierOptions` or `(data, signature) => Promise` | 선언적인 알고리즘·키 검증기 또는 커스텀 콜백입니다. | + +### Integrity policy + +`integrityPolicy`는 스테이징된 번들의 무결성 해시를 `download` 중에 어떻게 검사하고 `install` 중에 어떻게 다시 검사할지 결정합니다. + +| Policy | Behavior | +| ------------ | ------------------------------------------------------------------------------------ | +| `'strict'` | 무결성 값이 반드시 있어야 하고 일치해야 합니다. 그렇지 않으면 업데이트가 실패합니다. | +| `'optional'` | 원격 서버가 무결성 값을 제공하는 경우 검증합니다(기본값). | +| `'none'` | 무결성 검사를 완전히 건너뜁니다. | + +무결성은 SHA-2(`sha256`, `sha384`, `sha512`)를 사용하며, `":"` 형식으로 직렬화됩니다. 예를 들면 `sha256:n4bQ…`입니다. 자체 로직을 연결하려면, 내장 검사에 의존하는 대신 `integrityChecker` 콜백을 제공하세요. + +### Signature verification + +신뢰하는 키로 업데이트가 서명되도록 요구하려면 `signatureVerifier`를 설정하세요. 알고리즘과 공개 키로 선언적으로 제공하거나, 커스텀 콜백을 제공하세요. + +```ts +type SignatureAlgorithm = + | 'ecdsaSecp256R1' + | 'ecdsaSecp384R1' + | 'ed25519' + | 'rsaPkcs1V15' + | 'rsaPss'; + +type VerifyingKeyFormat = 'spkiDer' | 'spkiPem' | 'pkcs1Der' | 'pkcs1Pem' | 'sec1' | 'raw'; +``` + +키 형식 제약: `pkcs1Der`와 `pkcs1Pem`은 RSA에만, `sec1`은 ECDSA에만, `raw`는 Ed25519(32바이트 키)에만 적용됩니다. + + + 서명은 원본 번들 바이트가 아니라 무결성 문자열의 바이트를 대상으로 합니다. 따라서 서명 검증에는 + 무결성 값이 반드시 있어야 합니다. `signatureVerifier`는 무결성 값을 생성하는 무결성 정책과 함께 + 사용하세요. + + +전체 타입 목록은 [열거형, 옵션, 결과 타입](/docs/references/node/types)을, 배포 측 설정은 [Remote, 무결성 및 서명 설정](/docs/config/remote)을 참고하세요. + +## Update with integrity and signature checks + +엄격한 무결성 정책과 선언적 Ed25519 서명 검증기로 `Updater`를 구성한 뒤, 확인·다운로드·설치 단계를 순서대로 실행하세요. + +```ts +import { BundleSource, Remote, Updater } from '@wvb/node'; + +const source = new BundleSource({ + builtinDir: './bundles/builtin', + remoteDir: './bundles/remote', +}); + +const remote = new Remote('https://bundles.example.com'); + +const updater = new Updater(source, remote, { + channel: 'stable', + integrityPolicy: 'strict', + signatureVerifier: { + algorithm: 'ed25519', + key: { + format: 'spkiPem', + data: process.env.WVB_PUBLIC_KEY!, + }, + }, +}); + +const update = await updater.getUpdate('my-app'); +if (update.isAvailable) { + await updater.download('my-app', update.version); + await updater.install('my-app', update.version); +} +``` + +## Related + + + + + + + diff --git a/src/doc.ts b/src/doc.ts index de9e4e6..acb58c1 100644 --- a/src/doc.ts +++ b/src/doc.ts @@ -1,7 +1,9 @@ import { loader } from 'fumadocs-core/source'; import { docs } from '~source/server'; +import { i18n } from './lib/i18n'; export const docSource = loader({ baseUrl: '/docs', + i18n, source: docs.toFumadocsSource(), }); diff --git a/src/layouts/MobileNav.tsx b/src/layouts/MobileNav.tsx index f64b033..7153240 100644 --- a/src/layouts/MobileNav.tsx +++ b/src/layouts/MobileNav.tsx @@ -1,8 +1,9 @@ import { Dialog } from '@base-ui/react/dialog'; import { usePathname } from 'fumadocs-core/framework'; import { cn } from '../lib/cn'; +import { useUiStrings } from '../lib/ui-strings'; import { LanguageDropdown } from './docs/LanguageDropdown'; -import { CONTROL_BUTTON, SECTIONS } from './header-shared'; +import { CONTROL_BUTTON } from './header-shared'; import { CloseIcon, GitHubIcon, MenuIcon } from './home/components/icons'; import { Logo } from './home/components/Logo'; import { ThemeToggle } from './home/components/ThemeToggle'; @@ -13,10 +14,11 @@ import { GITHUB_URL } from './home/data'; // shared header on small screens (the inline nav + controls take over from `md`). export function MobileNav({ className }: { className?: string }) { const pathname = usePathname(); + const t = useUiStrings(); return ( - + @@ -30,13 +32,13 @@ export function MobileNav({ className }: { className?: string }) { webview-bundle - +