diff --git a/docs/en/apis/index.mdx b/docs/en/apis/index.mdx index e551bcdb..59620a2e 100644 --- a/docs/en/apis/index.mdx +++ b/docs/en/apis/index.mdx @@ -10,13 +10,13 @@ APIs for Lynx bundles running inside Sparkling containers. -| API | Description | -| --- | --- | -| [GlobalProps](/apis/global-props/Interface.GlobalProps) | Runtime globals injected by the native SDK (`lynx.__globalProps`) | -| [Scheme](/apis/scheme) | The unified `hybrid://...` URL format for opening pages/containers | -| [Navigation](/apis/sparkling-methods/sparkling-navigation) | Router helpers for opening/closing pages from Lynx/JS | -| [Storage](/apis/sparkling-methods/sparkling-storage) | Key-value storage helpers for Lynx/JS | -| [Media](/apis/sparkling-methods/sparkling-media) | Media helpers for choosing, uploading, downloading, and saving files | +| API | Description | Web | +| --- | --- | --- | +| [GlobalProps](/apis/global-props/Interface.GlobalProps) | Runtime globals injected by the native SDK (`lynx.__globalProps`) | - | +| [Scheme](/apis/scheme) | The unified `hybrid://...` URL format for opening pages/containers | - | +| [Navigation](/apis/sparkling-methods/sparkling-navigation) | Router helpers for opening/closing pages from Lynx/JS | Yes | +| [Storage](/apis/sparkling-methods/sparkling-storage) | Key-value storage helpers for Lynx/JS | Yes | +| [Media](/apis/sparkling-methods/sparkling-media) | Media helpers for choosing, uploading, downloading, and saving files | Yes | ## Sparkling SDK diff --git a/docs/en/guide/cli.md b/docs/en/guide/cli.md index 11d815ab..8ff0fe06 100644 --- a/docs/en/guide/cli.md +++ b/docs/en/guide/cli.md @@ -25,11 +25,14 @@ npx sparkling build | Option | Description | | --- | --- | | `--config ` | Path to `app.config.ts` (default: `app.config.ts`) | +| `--platform ` | Target platform: `android`, `ios`, `web`, or `all` (default: `all`) | | `--copy` | Copy built assets to Android and iOS native shells | | `--skip-copy` | Skip copying assets (default) | By default, asset copying is skipped for faster iteration during development. Use `--copy` when you need the bundles inside the native projects (e.g. for a release build). +When `--platform web` is specified, only web bundles (`*.web.bundle`) are produced. This is faster than building all environments. + ### `sparkling dev` Start the Rspeedy dev server for hot-reload development. Instead of rebuilding and copying bundles manually, the dev server serves bundles over HTTP so changes are reflected instantly. @@ -41,6 +44,7 @@ npx sparkling dev | Option | Description | | --- | --- | | `--config ` | Path to `app.config.ts` (default: `app.config.ts`) | +| `--platform ` | Target platform: `web`, `native`, or `all` (default: `all`) | | `--port ` | Dev server port (default: `app.config.ts -> dev.server.port`, fallback `5969`) | | `--host ` | Dev server host (default: `app.config.ts -> dev.server.host`, then Rspeedy default) | @@ -81,12 +85,13 @@ npx sparkling autolink | Option | Description | | --- | --- | -| `--platform ` | Platform to autolink: `android`, `ios`, or `all` (default: `all`) | +| `--platform ` | Platform to autolink: `android`, `ios`, `web`, or `all` (default: `all`) | **What it does:** - **Android** — Links Sparkling method Gradle projects and generates `SparklingAutolink.kt`. Debug-tool packages are linked as `debugImplementation`. - **iOS** — Links Sparkling method pods and generates `SparklingAutolink.swift`. Debug-tool packages are linked in the debug target. +- **Web** — Reads `web` entries from each `module.config.json` and generates a `web-autolink.ts` file that imports all `*/web` method handlers. ### `sparkling run:android` @@ -140,6 +145,24 @@ This command will: You can also set the `SPARKLING_IOS_SIMULATOR` environment variable to specify a default simulator. +### `sparkling run:web` + +Build web bundles and launch a browser preview. + +```bash +npx sparkling run:web +``` + +This command will: + +1. Build web bundles (`*.web.bundle`) +2. Start the `sparkling-web-shell` dev server +3. Open the app in your default browser at `http://localhost:3000` + +Use `?page=` query parameters to navigate to different entry points (e.g. `http://localhost:3000?page=second`). + +For more details, see the [Web Platform Guide](/guide/web-platform). + ### `sparkling doctor` Verify that your development environment is properly set up. @@ -196,6 +219,9 @@ npx sparkling run:android # 5. Run on iOS npx sparkling run:ios -# 6. Build bundles for release +# 6. Preview in browser +npx sparkling run:web + +# 7. Build bundles for release npx sparkling build --copy ``` diff --git a/docs/en/guide/get-started/create-custom-method.mdx b/docs/en/guide/get-started/create-custom-method.mdx index c10e0458..b013a4bd 100644 --- a/docs/en/guide/get-started/create-custom-method.mdx +++ b/docs/en/guide/get-started/create-custom-method.mdx @@ -256,6 +256,17 @@ npx sparkling autolink +## Add web support (optional) + +You can add a web implementation so your method works in the browser. See the full guide at [Web Method Implementations](/guide/web-method-implementations). + +In brief: + +1. Create `src/web/index.ts` with `registerWebMethod()` calls +2. Add `"./web"` subpath export to `package.json` +3. Add `"web"` to `platforms` in `module.config.json` +4. Run `npx sparkling autolink` + ## Best practices - **Naming convention**: package name should follow `sparkling-` format. diff --git a/docs/en/guide/get-started/create-new-app.mdx b/docs/en/guide/get-started/create-new-app.mdx index a8122fe9..6ce99e6c 100644 --- a/docs/en/guide/get-started/create-new-app.mdx +++ b/docs/en/guide/get-started/create-new-app.mdx @@ -18,7 +18,7 @@ npm create sparkling-app@latest my-app cd my-app ``` -### Run native targets +### Run on any platform ```bash # Android @@ -26,6 +26,9 @@ npm run run:android # iOS npm run run:ios + +# Web (browser preview) +npm run run:web ``` ### Add Sparkling methods as needed @@ -48,6 +51,7 @@ npx sparkling build - A Lynx app project (based on the default template) - Android and iOS native shells already wired for Sparkling +- Web platform support for browser previews (enabled by default) - A working JS ↔ native pipe method example (router) - Developer workflow commands (build / autolink / run) @@ -56,7 +60,7 @@ Key folders/files created by the default template: - `src/`: Lynx/React entry points and assets - `android/`, `ios/`: native shells wired to Sparkling SDK - `app.config.ts`: build + routing config consumed by `sparkling-app-cli` -- `package.json`: scripts (`dev`, `build`, `run:android`, `run:ios`) +- `package.json`: scripts (`dev`, `build`, `run:android`, `run:ios`, `run:web`) ### Prerequisites diff --git a/docs/en/guide/web-limitations.md b/docs/en/guide/web-limitations.md new file mode 100644 index 00000000..ee1338ae --- /dev/null +++ b/docs/en/guide/web-limitations.md @@ -0,0 +1,62 @@ +# Web Limitations + +The web platform provides a convenient development and preview environment, but has differences from the native Android/iOS platforms. This page documents known limitations. + +## Rendering + +- **DOM-based rendering**: `@lynx-js/web-core` renders Lynx components as custom HTML elements in the DOM, rather than using a native view hierarchy. CSS behavior follows browser standards, which may differ from Lynx's native layout engine in edge cases. +- **Performance**: For complex UIs with many elements or animations, native rendering will generally be faster. The web platform is best suited for development previews, not performance-critical production use. + +## Native Bridge + +- **`NativeModules` is unavailable**: On web, the Lynx `NativeModules` global does not exist. All method calls must go through the [web handler registry](/guide/web-method-implementations). +- **Unregistered methods**: Methods without a web handler return error code `-3` (module not registered). Check which methods have web handlers in the [built-in methods table](/guide/web-method-implementations#built-in-web-methods). + +## Storage + +- **Not encrypted**: `localStorage` is not encrypted, unlike native secure storage options. Do not store sensitive data (tokens, credentials) via `storage.*` methods on web in production. +- **Size limit**: `localStorage` has a ~5 MB per-origin limit in most browsers. Native storage does not have this restriction. +- **Synchronous**: `localStorage` operations are synchronous and block the main thread. For large datasets, this can cause jank. + +## Media + +- **Camera access**: `media.chooseMedia` with a camera source requires HTTPS. On `http://localhost` during development, camera access may be allowed by the browser, but in production, HTTPS is required. +- **File downloads**: `media.downloadFile` uses `fetch()` and is subject to CORS restrictions. The target URL must include appropriate CORS headers, or the download will fail. +- **File type filtering**: Browser `` accept filters are hints — users can override them and select any file type. + +## Network + +- **CORS**: All `fetch()` calls from the browser are subject to Cross-Origin Resource Sharing (CORS) policies. APIs that work from native (which has no CORS) may fail from web without server-side CORS headers. +- **Cookies**: Cookie handling differs between browser and native HTTP clients. Session management may behave differently on web. + +## Device APIs + +The following native capabilities are not available on web unless the browser provides equivalent APIs with user permission: + +- Geolocation (available via browser Geolocation API, requires HTTPS) +- Push notifications (available via browser Push API, requires service worker) +- Biometric authentication (not available) +- App-level deep linking (not applicable in browser context) +- Background processing (limited to service workers) + +## Feature Detection + +Write platform-aware code using error handling: + +```ts +import { callAsync } from 'sparkling-method'; + +try { + const result = await callAsync('nativeOnly.feature', params); + // Use native result +} catch (e) { + if (e.code === -2 || e.code === -3) { + // Not available on this platform + // Show alternative UI or skip the feature + } +} +``` + +Error codes: +- `-2`: `NativeModules` not available (running on web without a handler) +- `-3`: Module not registered (no web handler for this method) diff --git a/docs/en/guide/web-method-implementations.md b/docs/en/guide/web-method-implementations.md new file mode 100644 index 00000000..2ce7aaa5 --- /dev/null +++ b/docs/en/guide/web-method-implementations.md @@ -0,0 +1,147 @@ +# Web Method Implementations + +Sparkling method SDKs (navigation, storage, media) communicate with the native layer via `NativeModules` on Android/iOS. On web, `NativeModules` is unavailable — instead, a **web handler registry** dispatches method calls to browser-native implementations. + +## How It Works + +``` +App code → LynxPipe.call('router.open', params) + ↓ +sparkling-method detects web environment + ↓ +Web handler registry lookup by method name + ↓ +Browser-native implementation (History API, localStorage, etc.) +``` + +When `LynxPipe.call()` runs on the web: + +1. It checks if `NativeModules` is available — on web, it isn't. +2. Before returning an error, it looks up the method name in the **web handler registry**. +3. If a handler is registered, it's invoked with the same params and callback. +4. If no handler is found, it returns error code `-3` (module not registered). + +## Built-in Web Methods + +### Navigation (`sparkling-navigation`) + +| Method | Web Implementation | Notes | +|--------|-------------------|-------| +| `router.open` | Parses the `hybrid://` scheme, extracts the bundle name, uses the History API and fires a `CustomEvent` to swap the `` URL | Full scheme parameter support | +| `router.close` | `window.history.back()` | | + +### Storage (`sparkling-storage`) + +| Method | Web Implementation | Notes | +|--------|-------------------|-------| +| `storage.getItem` | `localStorage.getItem()` | 5 MB per-origin limit | +| `storage.setItem` | `localStorage.setItem()` | | +| `storage.removeItem` | `localStorage.removeItem()` | | + +### Media (`sparkling-media`) + +| Method | Web Implementation | Notes | +|--------|-------------------|-------| +| `media.chooseMedia` | `` with accept filters | Camera source requires HTTPS | +| `media.downloadFile` | `fetch()` + `URL.createObjectURL()` + download link | Subject to CORS restrictions | + +## Creating a Web Handler for a Custom Method + +If you've created a custom method SDK (see [Create a Custom Method](/guide/get-started/create-custom-method)), you can add web support by following these steps: + +### 1. Create the web handler + +Create `src/web/index.ts` in your method package: + +```ts +import { registerWebMethod } from 'sparkling-method'; + +registerWebMethod('myModule.myAction', (params, callback) => { + // Implement using browser APIs + const result = { /* ... */ }; + callback({ code: 0, data: result }); +}); + +registerWebMethod('myModule.anotherAction', (params, callback) => { + // ... + callback({ code: 0, data: {} }); +}); +``` + +### 2. Add subpath export + +In your method package's `package.json`, add a `./web` export: + +```json +{ + "exports": { + ".": "./dist/index.js", + "./web": "./dist/web/index.js" + } +} +``` + +### 3. Update module.config.json + +Add `"web"` to the `platforms` array: + +```json +{ + "name": "my-method", + "platforms": ["android", "ios", "web"], + "web": { + "entryPoint": "./src/web/index.ts", + "subpath": "./web" + } +} +``` + +### 4. Run autolink + +```bash +pnpm autolink +``` + +The CLI discovers the `web` platform entry and generates the appropriate imports so your web handler is loaded automatically. + +## Testing Web Handlers + +Web handlers are plain functions — you can test them in isolation: + +```ts +import { getWebMethodHandler } from 'sparkling-method'; + +// After importing your web handler module +import 'my-method/web'; + +test('myModule.myAction returns expected data', (done) => { + const handler = getWebMethodHandler('myModule.myAction'); + handler({ input: 'test' }, (response) => { + expect(response.code).toBe(0); + expect(response.data).toEqual({ /* expected */ }); + done(); + }); +}); +``` + +## Error Handling + +If a method is called on web without a registered handler, `LynxPipe` returns: + +```json +{ "code": -3, "message": "Module not registered" } +``` + +You can use this to implement graceful degradation: + +```ts +import { callAsync } from 'sparkling-method'; + +try { + const result = await callAsync('myModule.doThing', params); +} catch (e) { + if (e.code === -3) { + // Method not available on this platform — show fallback UI + } +} +``` diff --git a/docs/en/guide/web-platform.md b/docs/en/guide/web-platform.md new file mode 100644 index 00000000..e9e04fd8 --- /dev/null +++ b/docs/en/guide/web-platform.md @@ -0,0 +1,140 @@ +# Web Platform Guide + +Sparkling supports rendering Lynx bundles in the browser via the [Lynx web platform](https://lynxjs.org). This lets you preview and test your app in a browser without a native simulator — useful for rapid iteration, CI testing, and potentially web deployment. + +## Architecture + +``` +App TSX → rspeedy (multi-env) → *.lynx.bundle → Android/iOS native LynxView + → *.web.bundle → in browser +``` + +The same source code produces two outputs: +- **`*.lynx.bundle`** — consumed by native LynxView on Android/iOS +- **`*.web.bundle`** — consumed by `` custom element in the browser + +## New Projects + +When you run `npm create sparkling-app@latest`, the scaffolder asks: + +``` +? Enable web platform support? (preview in browser) (Y/n) +``` + +Answering **Yes** (the default) sets up the project with: +- `environments` config in `app.config.ts` for dual-output builds +- `dev:web`, `build:web`, and `run:web` scripts +- `sparkling-web-shell` devDependency + +You can also pass `--web` or `--no-web` as CLI flags: +```bash +npm create sparkling-app@latest my-app -- --web # enable web +npm create sparkling-app@latest my-app -- --no-web # disable web +``` + +## Existing Projects + +To add web support to an existing Sparkling project: + +### 1. Update `app.config.ts` + +Add the `environments` block and move `assetPrefix` into the `lynx` environment: + +```ts +const lynxConfig = defineConfig({ + source: { + entry: { + main: './src/pages/main/index.tsx', + }, + }, + output: { + filename: { + bundle: '[name].lynx.bundle', + }, + }, + environments: { + web: { + output: { + assetPrefix: '/', + distPath: { + root: 'dist/web', + }, + }, + }, + lynx: { + output: { + assetPrefix: 'asset:///', + }, + }, + }, + plugins: [/* ... */], +}) +``` + +### 2. Add dependencies + +```bash +pnpm add -D sparkling-web-shell @lynx-js/web-core @lynx-js/web-elements +``` + +### 3. Add scripts to `package.json` + +```json +{ + "scripts": { + "dev:web": "sparkling-app-cli dev --platform web", + "build:web": "sparkling-app-cli build --platform web", + "run:web": "sparkling-app-cli run:web" + } +} +``` + +### 4. Run autolink + +```bash +pnpm autolink +``` + +This generates web method imports so that method SDKs (navigation, storage, etc.) work in the browser. + +## Development Workflow + +### Start the web dev server + +```bash +pnpm run:web +``` + +This builds your web bundles and starts the web shell server at `http://localhost:3000`. + +### Navigate between pages + +For multi-page apps, use the `?page=` query parameter: + +- `http://localhost:3000` — loads `main.web.bundle` (default) +- `http://localhost:3000?page=second` — loads `second.web.bundle` + +When using `sparkling-navigation`'s `router.open()` in your app code, page transitions are handled automatically via the History API. + +### Hot Module Replacement + +The rspeedy dev server provides HMR for web bundles. Changes to your source files are reflected in the browser without a full reload. + +### Debugging + +Use your browser's built-in DevTools (Chrome DevTools, Firefox DevTools, etc.) to inspect the DOM, debug JavaScript, profile performance, and monitor network requests. + +## Build for Web + +To produce web bundles without starting a dev server: + +```bash +pnpm build:web +``` + +Output goes to `dist/web/`. These bundles can be served by any static file server. + +## Next Steps + +- [Web Method Implementations](/guide/web-method-implementations) — how method SDKs work on web +- [Web Limitations](/guide/web-limitations) — what native features aren't available on web diff --git a/packages/create-sparkling-app/src/create-app.ts b/packages/create-sparkling-app/src/create-app.ts index 44589595..903b76c7 100644 --- a/packages/create-sparkling-app/src/create-app.ts +++ b/packages/create-sparkling-app/src/create-app.ts @@ -47,6 +47,7 @@ import { askNamespace, askProjectName, askTemplate, + askWebPlatform, confirmInitGit, confirmInstall, confirmRemoveExistingDir, @@ -75,6 +76,45 @@ function ensureExecutable(filePath: string): void { } } +function removeWebSupport(projectDir: string): void { + // Remove web-related scripts and dependencies from package.json + const packageJsonPath = path.join(projectDir, "package.json"); + if (fs.existsSync(packageJsonPath)) { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as Record; + + const scripts = (pkg.scripts ?? {}) as Record; + delete scripts["dev:web"]; + delete scripts["build:web"]; + delete scripts["run:web"]; + + const devDeps = (pkg.devDependencies ?? {}) as Record; + delete devDeps["sparkling-web-shell"]; + delete devDeps["@lynx-js/web-core"]; + delete devDeps["@lynx-js/web-elements"]; + + fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`); + } + + // Remove environments config from app.config.ts, restoring flat assetPrefix + const appConfigPath = path.join(projectDir, "app.config.ts"); + if (fs.existsSync(appConfigPath)) { + let content = fs.readFileSync(appConfigPath, "utf8"); + // Remove the environments block and restore assetPrefix at top level + content = content.replace( + /\s*environments:\s*\{[\s\S]*?\n\s*\},\n/, + "\n" + ); + // Add assetPrefix back to output if not present + if (!content.includes("assetPrefix")) { + content = content.replace( + /output:\s*\{/, + "output: {\n assetPrefix: 'asset:///',", + ); + } + fs.writeFileSync(appConfigPath, content); + } +} + function toPascalCase(input: string): string { const base = input.includes("/") ? (input.split("/").pop() ?? input) : input; return base @@ -191,6 +231,8 @@ export async function createSparklingApp( const additionalTools = await askAdditionalTools(flags); + const enableWeb = await askWebPlatform(flags); + const defaultNamespace = deriveDefaultNamespace(packageName); const packageNamespace = await askNamespace(defaultNamespace, flags); @@ -275,6 +317,9 @@ export async function createSparklingApp( }); applyPackageNamespace(config.targetDir, packageNamespace); ensureExecutable(path.join(config.targetDir, "android", "gradlew")); + if (!enableWeb) { + removeWebSupport(config.targetDir); + } }, }); @@ -329,7 +374,7 @@ export async function createSparklingApp( await initializeGitRepo(distFolder); } - showCompletionNotes(targetDir, packageManager, didInstall); + showCompletionNotes(targetDir, packageManager, didInstall, enableWeb); p.outro(ui.success('Happy hacking!')); } diff --git a/packages/create-sparkling-app/src/create-app/post-create.ts b/packages/create-sparkling-app/src/create-app/post-create.ts index 90a7be82..fa57e1f2 100644 --- a/packages/create-sparkling-app/src/create-app/post-create.ts +++ b/packages/create-sparkling-app/src/create-app/post-create.ts @@ -112,7 +112,7 @@ export function detectPackageManager(): string { } } -export function showCompletionNotes(targetDir: string, packageManager?: string, didInstall = false): void { +export function showCompletionNotes(targetDir: string, packageManager?: string, didInstall = false, enableWeb = true): void { const formatScriptCommand = (script: string) => { const pm = packageManager ?? 'npm'; return pm === 'npm' ? `${pm} run ${script}` : `${pm} ${script}`; @@ -128,11 +128,17 @@ export function showCompletionNotes(targetDir: string, packageManager?: string, nextSteps.push(formatScriptCommand('run:ios')); nextSteps.push(formatScriptCommand('run:android')); + if (enableWeb) { + nextSteps.push(formatScriptCommand('run:web')); + } const tips = [ 'iOS: ensure Xcode Command Line Tools are installed.', 'Android: ensure ANDROID_HOME and SDK platforms are set.', ]; + if (enableWeb) { + tips.push('Web: run `run:web` to preview your app in the browser.'); + } p.note( [...nextSteps, '', ...tips.map(t => ui.tip(t))].join('\n'), diff --git a/packages/create-sparkling-app/src/create-app/types.ts b/packages/create-sparkling-app/src/create-app/types.ts index 45dd98b3..a15eedef 100644 --- a/packages/create-sparkling-app/src/create-app/types.ts +++ b/packages/create-sparkling-app/src/create-app/types.ts @@ -18,6 +18,7 @@ export interface CreateAppFlags { namespace?: string; 'app-id'?: string; verbose?: boolean; + web?: boolean; } export interface CreateSparklingAppOptions { diff --git a/packages/create-sparkling-app/src/create-app/user-prompts.ts b/packages/create-sparkling-app/src/create-app/user-prompts.ts index 5b3cf31b..ff9464a7 100644 --- a/packages/create-sparkling-app/src/create-app/user-prompts.ts +++ b/packages/create-sparkling-app/src/create-app/user-prompts.ts @@ -92,6 +92,18 @@ export async function askAdditionalTools(flags: { yes?: boolean }): Promise { + if (flags.web !== undefined) return flags.web; + if (!flags.yes) { + const enableWeb = await p.confirm({ + message: 'Enable web platform support? (preview in browser)', + initialValue: true, + }); + return checkCancel(enableWeb); + } + return true; +} + export async function askNamespace(defaultNamespace: string, flags: { yes?: boolean; namespace?: string; ['app-id']?: string }): Promise { const provided = flags.namespace ?? flags['app-id']; if (provided) return provided; diff --git a/packages/create-sparkling-app/src/init.ts b/packages/create-sparkling-app/src/init.ts index 64151c4d..154c22d3 100644 --- a/packages/create-sparkling-app/src/init.ts +++ b/packages/create-sparkling-app/src/init.ts @@ -22,7 +22,9 @@ function parseFlags(argv: string[]): { name?: string; flags: CreateAppFlags } { .option('--no-git', 'Skip git initialization') .option('--namespace ', 'Android package / iOS bundle id') .option('--app-id ', 'Alias for namespace') - .option('-v, --verbose', 'Enable verbose logging'); + .option('-v, --verbose', 'Enable verbose logging') + .option('--web', 'Include web platform support (default: true)') + .option('--no-web', 'Exclude web platform support'); const parsed = program.parse(argv, { from: 'user' }); const opts = parsed.opts<{ @@ -36,6 +38,7 @@ function parseFlags(argv: string[]): { name?: string; flags: CreateAppFlags } { namespace?: string; appId?: string; verbose?: boolean; + web?: boolean; }>(); const [name] = parsed.args as string[]; @@ -51,6 +54,7 @@ function parseFlags(argv: string[]): { name?: string; flags: CreateAppFlags } { 'app-id': opts.appId, templateVersion: opts.templateVersion, verbose: opts.verbose, + web: opts.web, }; return { name, flags }; diff --git a/packages/methods/sparkling-media/module.config.json b/packages/methods/sparkling-media/module.config.json index b8854497..53f31934 100644 --- a/packages/methods/sparkling-media/module.config.json +++ b/packages/methods/sparkling-media/module.config.json @@ -1,10 +1,37 @@ { "name": "sparkling-media", - "packageName": "com.tiktok.sparkling.method", - "moduleName": "Media", - "androidDsl": "kts", - "projectName": "sparkling-media", + "platforms": ["android", "ios", "web"], + "version": "1.0.0", + "description": "Media methods for choosing, uploading, and downloading media files in Sparkling apps", + "methods": { + "chooseMedia": { + "description": "Choose media from album or camera" + }, + "downloadFile": { + "description": "Download a file from server" + }, + "uploadFile": { + "description": "Upload a file to server" + }, + "uploadImage": { + "description": "Upload an image to server" + }, + "saveDataURL": { + "description": "Save base64 data URL to local file" + } + }, + "android": { + "packageName": "com.tiktok.sparkling.method.media", + "className": "MediaMethod", + "buildGradle": "android/build.gradle.kts" + }, "ios": { + "moduleName": "Media", + "className": "MediaMethod", "podspecPath": "ios/Sparkling-Media.podspec" + }, + "web": { + "entryPoint": "index.ts", + "bundleName": "media.bundle" } } diff --git a/packages/playground/package.json b/packages/playground/package.json index ccfda4e0..be68025c 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -13,6 +13,7 @@ "build": "pnpm -C ../.. --filter sparkling-method --filter sparkling-navigation --filter sparkling-storage --filter sparkling-media build && rspeedy build", "build:web": "pnpm -C ../.. --filter sparkling-method --filter sparkling-navigation --filter sparkling-storage --filter sparkling-media build && rspeedy build --config lynx.web.config.ts", "dev": "rspeedy dev", + "dev:web": "pnpm build:web && LYNX_BUNDLE_DIR=\"$PWD/dist-web\" pnpm --filter sparkling-web-shell dev", "format": "prettier --write .", "preview": "rspeedy preview", "test": "vitest run --passWithNoTests", diff --git a/packages/sparkling-app-cli/src/commands/autolink.ts b/packages/sparkling-app-cli/src/commands/autolink.ts index 1f174292..02b635b5 100644 --- a/packages/sparkling-app-cli/src/commands/autolink.ts +++ b/packages/sparkling-app-cli/src/commands/autolink.ts @@ -20,7 +20,7 @@ const IOS_AUTOLINK_END = '# END SPARKLING AUTOLINK'; export interface AutolinkOptions { cwd: string; configFile?: string; - platform?: 'android' | 'ios' | 'all'; + platform?: 'android' | 'ios' | 'web' | 'all'; } /** @@ -256,6 +256,8 @@ async function discoverModules(cwd: string): Promise { const androidMavenDependency = isNodeModule ? resolveMavenDependency(androidConfig?.mavenDependency, moduleRoot) : undefined; + const webConfig = config.web as Record | undefined; + const platforms = Array.isArray(config.platforms) ? config.platforms as string[] : undefined; const androidBuild = (androidConfig?.buildGradle && typeof androidConfig.buildGradle === 'string') ? path.resolve(moduleRoot, androidConfig.buildGradle) @@ -276,6 +278,7 @@ async function discoverModules(cwd: string): Promise { name, root: moduleRoot, devtool: config.devtool === true, + platforms, android: { packageName: androidPackageName, className: androidClassName, @@ -293,6 +296,10 @@ async function discoverModules(cwd: string): Promise { className: typeof iosConfig?.className === 'string' ? iosConfig.className : undefined, podspecPath: iosPodspecPath, }, + web: webConfig ? { + entryPoint: typeof webConfig.entryPoint === 'string' ? webConfig.entryPoint : undefined, + subpath: typeof webConfig.subpath === 'string' ? webConfig.subpath : './web', + } : undefined, }); } } @@ -984,14 +991,50 @@ function writeIosRegistry(modules: MethodModuleConfig[], bundleId: string, cwd: fs.writeFileSync(filePath, content); } +function writeWebAutolink(modules: MethodModuleConfig[], cwd: string) { + const tempDir = path.resolve(cwd, '.sparkling'); + fs.ensureDirSync(tempDir); + const filePath = path.join(tempDir, 'web-autolink.ts'); + + const webModules = modules.filter(m => + (m.platforms && m.platforms.includes('web')) || m.web, + ); + const skippedModules = modules.filter(m => + !(m.platforms && m.platforms.includes('web')) && !m.web, + ); + + const lines: string[] = [ + '// Generated by sparkling autolink — do not edit', + ]; + + for (const mod of webModules) { + const subpath = mod.web?.subpath ?? './web'; + const importPath = `${mod.name}/${subpath.replace(/^\.\//, '')}`; + lines.push(`import '${importPath}';`); + } + + for (const mod of skippedModules) { + lines.push(`// ${mod.name}: web platform not supported, skipping`); + } + + lines.push(''); + fs.writeFileSync(filePath, lines.join('\n')); + + if (isVerboseEnabled()) { + verboseLog(`Web autolink: ${webModules.length} module(s) linked, ${skippedModules.length} skipped`); + verboseLog(`Web autolink written to ${filePath}`); + } +} + export async function autolink(options: AutolinkOptions): Promise { const platform = options.platform ?? 'all'; const doAndroid = platform === 'android' || platform === 'all'; const doIos = platform === 'ios' || platform === 'all'; + const doWeb = platform === 'web' || platform === 'all'; let modules = await discoverModules(options.cwd); if (isVerboseEnabled()) { const moduleNames = modules.map(m => m.name).join(', ') || '(none)'; - verboseLog(`Autolink platforms -> android: ${doAndroid}, ios: ${doIos}`); + verboseLog(`Autolink platforms -> android: ${doAndroid}, ios: ${doIos}, web: ${doWeb}`); verboseLog(`Autolink discovered modules: ${moduleNames}`); } // Prefer user-defined IDs but fall back to defaults to stay compatible even if config can't load. @@ -1069,7 +1112,15 @@ export async function autolink(options: AutolinkOptions): Promise { const configPath = path.resolve(options.cwd, options.configFile ?? 'app.config.ts'); const tempConfigPath = createTempLynxConfig(options.cwd, configPath); - const rspeedyBin = 'rspeedy'; + const platform = options.platform ?? 'all'; if (isVerboseEnabled()) { verboseLog(`App config path: ${configPath}`); verboseLog(`Temp Lynx config: ${tempConfigPath}`); - verboseLog(`rspeedy binary: ${rspeedyBin}`); + verboseLog(`Build platform: ${platform}`); + } + + const rspeedyArgs = ['build', '--config', tempConfigPath]; + + if (platform === 'web') { + rspeedyArgs.push('--environment', 'web'); + } else if (platform === 'android' || platform === 'ios' || platform === 'native') { + rspeedyArgs.push('--environment', 'lynx'); } + // platform === 'all' → no --environment flag → builds all environments - console.log(ui.headline(`Building Lynx bundle with config from ${path.relative(options.cwd, configPath)}`)); - await runCommand(rspeedyBin, ['build', '--config', tempConfigPath], { cwd: options.cwd }); + console.log(ui.headline(`Building ${platform} bundle(s) with config from ${path.relative(options.cwd, configPath)}`)); + await runCommand('rspeedy', rspeedyArgs, { cwd: options.cwd }); - const shouldCopy = options.skipCopy !== true; // default to no copy + // Skip asset copy for web-only builds + const shouldCopy = platform !== 'web' && options.skipCopy !== true; if (shouldCopy) { - // Read AppConfig to locate platform asset destinations if provided const { config } = await loadAppConfig(options.cwd, options.configFile ?? 'app.config.ts'); await copyAssets({ cwd: options.cwd, androidDest: config.paths?.androidAssets ?? 'android/app/src/main/assets', iosDest: config.paths?.iosAssets ?? 'ios/LynxResources/Assets', + // When building all environments, exclude the web/ subdirectory from native copies + excludeDirs: platform === 'all' ? ['web'] : undefined, }); } else if (isVerboseEnabled()) { - verboseLog('Skipping asset copy because --skip-copy is in effect.'); + verboseLog(platform === 'web' + ? 'Skipping asset copy for web-only build.' + : 'Skipping asset copy because --skip-copy is in effect.'); } } diff --git a/packages/sparkling-app-cli/src/commands/copy-assets.ts b/packages/sparkling-app-cli/src/commands/copy-assets.ts index f5ef6f97..72ce685a 100644 --- a/packages/sparkling-app-cli/src/commands/copy-assets.ts +++ b/packages/sparkling-app-cli/src/commands/copy-assets.ts @@ -11,16 +11,46 @@ export interface CopyAssetsOptions { source?: string; androidDest?: string; iosDest?: string; + webDest?: string; + /** Directory names to exclude from native (android/ios) copies (e.g., ['web']) */ + excludeDirs?: string[]; cwd: string; } -function copyDir(src: string, dest: string) { +function copyDir(src: string, dest: string, excludeDirs?: string[]) { if (!fs.existsSync(src)) { console.warn(ui.warn(`Skip copy: missing source ${src}`)); return; } fs.ensureDirSync(dest); - fs.cpSync(src, dest, { recursive: true, force: true, dereference: true }); + + if (excludeDirs?.length) { + copyDirFiltered(src, dest, excludeDirs); + } else { + fs.cpSync(src, dest, { recursive: true, force: true, dereference: true }); + } +} + +function copyDirFiltered(src: string, dest: string, excludeDirs: string[]) { + const entries = fs.readdirSync(src, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory() && excludeDirs.includes(entry.name)) { + if (isVerboseEnabled()) { + verboseLog(`Excluding directory ${entry.name} from native copy`); + } + continue; + } + + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + + if (entry.isDirectory()) { + copyDirFiltered(srcPath, destPath, excludeDirs); + } else { + fs.ensureDirSync(path.dirname(destPath)); + fs.copyFileSync(srcPath, destPath); + } + } } export async function copyAssets(options: CopyAssetsOptions): Promise { @@ -31,11 +61,21 @@ export async function copyAssets(options: CopyAssetsOptions): Promise { verboseLog(`Copy assets source: ${source}`); verboseLog(`Android assets destination: ${androidDest}`); verboseLog(`iOS assets destination: ${iosDest}`); + if (options.excludeDirs?.length) { + verboseLog(`Excluding directories: ${options.excludeDirs.join(', ')}`); + } } for (const dest of [androidDest, iosDest]) { console.log(ui.info(`Copying ${relativeTo(options.cwd, source)} -> ${relativeTo(options.cwd, dest)}`)); - copyDir(source, dest); + copyDir(source, dest, options.excludeDirs); + } + + if (options.webDest) { + const webDest = path.resolve(options.cwd, options.webDest); + const webSource = path.resolve(source, 'web'); + console.log(ui.info(`Copying ${relativeTo(options.cwd, webSource)} -> ${relativeTo(options.cwd, webDest)}`)); + copyDir(webSource, webDest); } console.log(ui.success('Assets copied successfully.')); diff --git a/packages/sparkling-app-cli/src/commands/dev.ts b/packages/sparkling-app-cli/src/commands/dev.ts index 9112ec9a..38e44c1a 100644 --- a/packages/sparkling-app-cli/src/commands/dev.ts +++ b/packages/sparkling-app-cli/src/commands/dev.ts @@ -16,11 +16,14 @@ import { ui } from '../utils/ui'; import { isVerboseEnabled, verboseLog } from '../utils/verbose'; import { warnIfWildcardDevServerHost } from '../utils/dev-server-host'; +export type DevPlatform = 'web' | 'native' | 'all'; + export interface DevOptions { cwd: string; configFile?: string; port?: number; host?: string; + platform?: DevPlatform; } function getEntryKeys(config: { lynxConfig: unknown }): string[] { @@ -43,6 +46,7 @@ function clearConfigRequireCache(cwd: string, configPath: string): void { export async function devProject(options: DevOptions): Promise { const configFile = options.configFile ?? 'app.config.ts'; + const platform = options.platform ?? 'all'; const { config, configPath } = await loadAppConfig(options.cwd, configFile); const { devPort, lynxPort } = getConfiguredDevServerPorts(config); if (devPort !== undefined && lynxPort !== undefined && devPort !== lynxPort) { @@ -67,6 +71,7 @@ export async function devProject(options: DevOptions): Promise { if (isVerboseEnabled()) { verboseLog(`App config path: ${configPath}`); verboseLog(`Dev server port: ${port}`); + verboseLog(`Dev platform: ${platform}`); if (host) { verboseLog(`Dev server host: ${host}`); } @@ -76,7 +81,7 @@ export async function devProject(options: DevOptions): Promise { console.log( ui.headline( - `Starting Rspeedy dev server on port ${port} with config from ${path.relative(options.cwd, configPath)}`, + `Starting Rspeedy dev server (${platform}) on port ${port} with config from ${path.relative(options.cwd, configPath)}`, ), ); @@ -124,7 +129,14 @@ export async function devProject(options: DevOptions): Promise { if (isVerboseEnabled()) { verboseLog(`Temp Lynx config: ${tempConfigPath}`); } - child = spawn('rspeedy', ['dev', '--config', tempConfigPath], { + const rspeedyArgs = ['dev', '--config', tempConfigPath]; + if (platform === 'web') { + rspeedyArgs.push('--environment', 'web'); + } else if (platform === 'native') { + rspeedyArgs.push('--environment', 'lynx'); + } + // platform === 'all' → no --environment flag → serves all environments + child = spawn('rspeedy', rspeedyArgs, { cwd: options.cwd, env: process.env, stdio: 'inherit', diff --git a/packages/sparkling-app-cli/src/commands/doctor/checks.ts b/packages/sparkling-app-cli/src/commands/doctor/checks.ts index c8af4330..54dfd30a 100644 --- a/packages/sparkling-app-cli/src/commands/doctor/checks.ts +++ b/packages/sparkling-app-cli/src/commands/doctor/checks.ts @@ -348,6 +348,58 @@ export function checkCocoaPods(): CheckResult { return { ...base, status: 'pass', version }; } +export function checkWebCore(): CheckResult { + const base: Omit = { + name: '@lynx-js/web-core', + category: 'web', + }; + + try { + require.resolve('@lynx-js/web-core', { paths: [process.cwd()] }); + return { ...base, status: 'pass', message: 'Installed' }; + } catch { + return { + ...base, + status: 'fail', + message: '@lynx-js/web-core is not installed', + fixHint: + '@lynx-js/web-core is not installed. Install it with: pnpm add -D @lynx-js/web-core @lynx-js/web-elements', + }; + } +} + +export function checkWebShell(): CheckResult { + const base: Omit = { + name: 'sparkling-web-shell', + category: 'web', + }; + + try { + require.resolve('sparkling-web-shell/package.json', { paths: [process.cwd()] }); + return { ...base, status: 'pass', message: 'Installed' }; + } catch { + // Also check common workspace sibling locations + const cwd = process.cwd(); + const candidates = [ + path.join(cwd, '../sparkling-web-shell/package.json'), + path.join(cwd, '../../packages/sparkling-web-shell/package.json'), + ]; + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return { ...base, status: 'pass', message: 'Found in workspace' }; + } + } + + return { + ...base, + status: 'fail', + message: 'sparkling-web-shell is not installed or found in workspace', + fixHint: + 'sparkling-web-shell is not found. Install it with: pnpm add -D sparkling-web-shell', + }; + } +} + export function checkSimulator(): CheckResult { const base: Omit = { name: 'iOS Simulator', diff --git a/packages/sparkling-app-cli/src/commands/doctor/index.ts b/packages/sparkling-app-cli/src/commands/doctor/index.ts index 4f54fe72..273688d6 100644 --- a/packages/sparkling-app-cli/src/commands/doctor/index.ts +++ b/packages/sparkling-app-cli/src/commands/doctor/index.ts @@ -13,6 +13,8 @@ import { checkXcode, checkCocoaPods, checkSimulator, + checkWebCore, + checkWebShell, } from './checks'; import type { CheckResult } from './types'; @@ -59,7 +61,7 @@ function buildAgentPrompt(failed: CheckResult[]): string { } export interface DoctorOptions { - platform: 'android' | 'ios' | 'all'; + platform: 'android' | 'ios' | 'web' | 'all'; } export async function doctor(opts: DoctorOptions): Promise { @@ -104,7 +106,19 @@ export async function doctor(opts: DoctorOptions): Promise { } } - const allResults = [...generalResults, ...androidResults, ...iosResults]; + const webResults: CheckResult[] = []; + if (platform === 'web' || platform === 'all') { + webResults.push(checkWebCore()); + webResults.push(checkWebShell()); + + console.log(''); + console.log(ui.info('Web:')); + for (const r of webResults) { + console.log(formatCheckLine(r)); + } + } + + const allResults = [...generalResults, ...androidResults, ...iosResults, ...webResults]; const failed = allResults.filter((r) => r.status === 'fail'); const warned = allResults.filter((r) => r.status === 'warn'); diff --git a/packages/sparkling-app-cli/src/commands/doctor/types.ts b/packages/sparkling-app-cli/src/commands/doctor/types.ts index d7c971a8..33d8a62e 100644 --- a/packages/sparkling-app-cli/src/commands/doctor/types.ts +++ b/packages/sparkling-app-cli/src/commands/doctor/types.ts @@ -8,7 +8,7 @@ export interface CheckResult { /** Display name of the check (e.g. "Node.js", "JDK") */ name: string; /** Category for grouping in output */ - category: 'general' | 'android' | 'ios'; + category: 'general' | 'android' | 'ios' | 'web'; /** Whether the check passed */ status: CheckStatus; /** Detected version string, if applicable */ diff --git a/packages/sparkling-app-cli/src/commands/run-web.ts b/packages/sparkling-app-cli/src/commands/run-web.ts new file mode 100644 index 00000000..d6311a2f --- /dev/null +++ b/packages/sparkling-app-cli/src/commands/run-web.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2025 TikTok Pte. Ltd. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. +import path from 'node:path'; +import fs from 'fs-extra'; +import { WEB_SHELL_PORT } from '../constants'; +import { buildProject } from './build'; +import { autolink } from './autolink'; +import { runCommand } from '../utils/exec'; +import { ui } from '../utils/ui'; +import { isVerboseEnabled, verboseLog } from '../utils/verbose'; + +export interface RunWebOptions { + cwd: string; + configFile?: string; + port?: number; + open?: boolean; +} + +/** + * Locate the sparkling-web-shell package. + * Checks node_modules resolution first, then common workspace locations. + */ +function resolveWebShellDir(cwd: string): string | null { + // Try require.resolve to find the package entry + try { + const pkgJsonPath = require.resolve('sparkling-web-shell/package.json', { + paths: [cwd], + }); + return path.dirname(pkgJsonPath); + } catch { + // Not found via node resolution + } + + // Fallback: check common workspace sibling locations + const candidates = [ + path.resolve(cwd, '../sparkling-web-shell'), + path.resolve(cwd, '../../packages/sparkling-web-shell'), + ]; + for (const candidate of candidates) { + if (fs.existsSync(path.join(candidate, 'package.json'))) { + return candidate; + } + } + + return null; +} + +export async function runWeb(options: RunWebOptions): Promise { + const cwd = options.cwd; + const port = options.port ?? WEB_SHELL_PORT; + const shouldOpen = options.open !== false; + + // Step 1: Run web autolink to generate web method imports + console.log(ui.headline('Autolinking web method modules...')); + await autolink({ cwd, platform: 'web', configFile: options.configFile }); + + // Step 2: Build web bundles only + console.log(ui.headline('Building web bundles...')); + await buildProject({ + cwd, + configFile: options.configFile, + platform: 'web', + skipCopy: true, + }); + + // Step 3: Resolve sparkling-web-shell location + const webShellDir = resolveWebShellDir(cwd); + if (!webShellDir) { + throw new Error( + 'Could not find sparkling-web-shell. Install it with:\n' + + ' pnpm add -D sparkling-web-shell', + ); + } + + if (isVerboseEnabled()) { + verboseLog(`Web shell resolved at: ${webShellDir}`); + } + + // Step 4: Verify build output exists + const distDir = path.resolve(cwd, 'dist/web'); + if (!fs.existsSync(distDir)) { + throw new Error(`Web build output not found at ${distDir}. Did the build succeed?`); + } + + // Step 5: Start the web shell dev server pointing at app's web dist + console.log(ui.headline(`Starting web preview at http://localhost:${port}`)); + + await runCommand('npx', ['rsbuild', 'dev'], { + cwd: webShellDir, + env: { + LYNX_BUNDLE_DIR: distDir, + PORT: String(port), + BROWSER: shouldOpen ? 'true' : 'none', + }, + }); +} diff --git a/packages/sparkling-app-cli/src/constants.ts b/packages/sparkling-app-cli/src/constants.ts index 73687cc4..f10da475 100644 --- a/packages/sparkling-app-cli/src/constants.ts +++ b/packages/sparkling-app-cli/src/constants.ts @@ -7,3 +7,9 @@ * 5969 = "LYNX" on a phone keypad (L=5, Y=9, N=6, X=9). */ export const DEV_SERVER_PORT = 5969; + +/** + * Default port for the web shell preview server. + * Matches the default in sparkling-web-shell/rsbuild.config.ts. + */ +export const WEB_SHELL_PORT = 4200; diff --git a/packages/sparkling-app-cli/src/index.ts b/packages/sparkling-app-cli/src/index.ts index d2660f78..2cada061 100644 --- a/packages/sparkling-app-cli/src/index.ts +++ b/packages/sparkling-app-cli/src/index.ts @@ -6,12 +6,15 @@ import { Command } from 'commander'; import path from 'node:path'; import { autolink } from './commands/autolink'; import { buildProject } from './commands/build'; +import type { BuildPlatform } from './commands/build'; import { copyAssets } from './commands/copy-assets'; import { devProject } from './commands/dev'; +import type { DevPlatform } from './commands/dev'; import { doctor } from './commands/doctor'; import { runAndroid } from './commands/run-android'; import { runIos } from './commands/run-ios'; import { DEV_SERVER_PORT } from './constants'; +import { runWeb } from './commands/run-web'; import { ui } from './utils/ui'; import { enableVerboseLogging, isVerboseEnabled, verboseLog } from './utils/verbose'; import type { AppConfig } from './types'; @@ -41,16 +44,27 @@ function resolveSkipCopy(opts: { copy?: boolean; skipCopy?: boolean }): boolean return true; } +const BUILD_PLATFORMS = ['android', 'ios', 'web', 'native', 'all']; +const DEV_PLATFORMS = ['web', 'native', 'all']; +const AUTOLINK_PLATFORMS = ['android', 'ios', 'web', 'all']; +const DOCTOR_PLATFORMS = ['android', 'ios', 'web', 'all']; + program .command('build') .description('Build Lynx bundle using app.config.ts (no-copy by default)') .option('--config ', 'Path to app.config.ts', 'app.config.ts') + .option('--platform ', 'Target platform: android|ios|web|native|all', 'all') .option('--copy', 'Copy assets to native shells') .option('--skip-copy', 'Skip copying assets to native shells') .action(async opts => { const cwd = process.cwd(); const skipCopy = resolveSkipCopy(opts); - await buildProject({ cwd, configFile: opts.config, skipCopy }); + const raw = String(opts.platform ?? 'all').toLowerCase(); + const platform = (BUILD_PLATFORMS.includes(raw) ? raw : 'all') as BuildPlatform; + if (!BUILD_PLATFORMS.includes(raw)) { + console.warn(ui.warn(`Unknown platform "${opts.platform}", defaulting to 'all'.`)); + } + await buildProject({ cwd, configFile: opts.config, skipCopy, platform }); }); program @@ -59,17 +73,24 @@ program .option('--config ', 'Path to app.config.ts', 'app.config.ts') .option('--port ', `Dev server port (default: app.config.ts dev.server.port or ${DEV_SERVER_PORT})`) .option('--host ', 'Dev server host (e.g. 127.0.0.1)') + .option('--platform ', 'Target platform: web|native|all', 'all') .action(async opts => { const cwd = process.cwd(); const rawPort = opts.port === undefined ? undefined : Number.parseInt(String(opts.port), 10); if (opts.port !== undefined && !Number.isFinite(rawPort)) { throw new Error(`Invalid --port value: ${opts.port}`); } + const raw = String(opts.platform ?? 'all').toLowerCase(); + const platform = (DEV_PLATFORMS.includes(raw) ? raw : 'all') as DevPlatform; + if (!DEV_PLATFORMS.includes(raw)) { + console.warn(ui.warn(`Unknown platform "${opts.platform}", defaulting to 'all'.`)); + } await devProject({ cwd, configFile: opts.config, port: rawPort, host: opts.host, + platform, }); }); @@ -79,6 +100,7 @@ program .option('--source ', 'Path to compiled assets', 'dist') .option('--android-dest ', 'Android asset destination', 'android/app/src/main/assets') .option('--ios-dest ', 'iOS asset destination', 'ios/LynxResources/Assets') + .option('--exclude-web', 'Exclude web/ subdirectory from native copies') .action(async opts => { const cwd = process.cwd(); await copyAssets({ @@ -86,19 +108,19 @@ program source: opts.source, androidDest: opts.androidDest, iosDest: opts.iosDest, + excludeDirs: opts.excludeWeb ? ['web'] : undefined, }); }); program .command('autolink') - .description('Autolink Sparkling method modules for Android and iOS') - .option('--platform ', 'Platform to autolink: android|ios|all', 'all') + .description('Autolink Sparkling method modules for Android, iOS, and Web') + .option('--platform ', 'Platform to autolink: android|ios|web|all', 'all') .action(async (opts) => { const cwd = process.cwd(); const raw = String(opts.platform ?? 'all').toLowerCase(); - const allowed = ['android', 'ios', 'all']; - const platform = (allowed.includes(raw) ? raw : 'all') as 'android' | 'ios' | 'all'; - if (!allowed.includes(raw)) { + const platform = (AUTOLINK_PLATFORMS.includes(raw) ? raw : 'all') as 'android' | 'ios' | 'web' | 'all'; + if (!AUTOLINK_PLATFORMS.includes(raw)) { console.warn(ui.warn(`Unknown platform "${opts.platform}", defaulting to 'all'.`)); } await autolink({ cwd, platform }); @@ -136,15 +158,30 @@ program }); }); +program + .command('run:web') + .description('Build web bundles and launch in browser') + .option('--config ', 'Path to app.config.ts', 'app.config.ts') + .option('--port ', 'Web server port (default: 4200)', '4200') + .option('--no-open', 'Do not auto-open browser') + .action(async (opts) => { + const cwd = process.cwd(); + await runWeb({ + cwd, + configFile: opts.config, + port: Number(opts.port), + open: opts.open, + }); + }); + program .command('doctor') .description('Check if your environment is ready to build a Sparkling app') - .option('--platform ', 'Platform to check: android|ios|all', 'all') + .option('--platform ', 'Platform to check: android|ios|web|all', 'all') .action(async (opts) => { const raw = String(opts.platform ?? 'all').toLowerCase(); - const allowed = ['android', 'ios', 'all']; - const platform = (allowed.includes(raw) ? raw : 'all') as 'android' | 'ios' | 'all'; - if (!allowed.includes(raw)) { + const platform = (DOCTOR_PLATFORMS.includes(raw) ? raw : 'all') as 'android' | 'ios' | 'web' | 'all'; + if (!DOCTOR_PLATFORMS.includes(raw)) { console.warn(ui.warn(`Unknown platform "${opts.platform}", defaulting to 'all'.`)); } await doctor({ platform }); diff --git a/packages/sparkling-app-cli/src/types.ts b/packages/sparkling-app-cli/src/types.ts index 9ed6fd1b..1e1c97ac 100644 --- a/packages/sparkling-app-cli/src/types.ts +++ b/packages/sparkling-app-cli/src/types.ts @@ -12,6 +12,9 @@ export interface PlatformConfig { bundleIdentifier?: string; simulator?: string; }; + web?: { + port?: number; + }; } export type LynxConfig = unknown; @@ -55,6 +58,7 @@ export interface AppConfig { paths?: { androidAssets?: string; iosAssets?: string; + webAssets?: string; }; appIcon?: string; router?: RouterConfig; @@ -68,6 +72,7 @@ export interface MethodModuleConfig { root: string; /** When true, the module is a devtool module: linked with debugImplementation on Android and excluded from release on iOS. */ devtool?: boolean; + platforms?: string[]; android?: { packageName?: string; className?: string; @@ -81,4 +86,8 @@ export interface MethodModuleConfig { className?: string; podspecPath?: string; }; + web?: { + entryPoint?: string; + subpath?: string; + }; } diff --git a/packages/website/rspress.config.ts b/packages/website/rspress.config.ts index f0346ee1..60b985a2 100644 --- a/packages/website/rspress.config.ts +++ b/packages/website/rspress.config.ts @@ -76,6 +76,11 @@ const sidebarEn = { { text: 'Scheme', link: '/guide/scheme' }, { text: 'Navigation', link: '/guide/multi-page-navigation' }, { dividerType: 'solid' }, + { sectionHeaderText: 'Web Platform' }, + { text: 'Web Platform Guide', link: '/guide/web-platform' }, + { text: 'Web Methods', link: '/guide/web-method-implementations' }, + { text: 'Limitations', link: '/guide/web-limitations' }, + { dividerType: 'solid' }, { sectionHeaderText: 'Native Module' }, { text: 'Custom Methods', link: '/guide/get-started/create-custom-method' }, ], @@ -155,6 +160,11 @@ const sidebarZhBase = { { text: 'Scheme', link: '/guide/scheme' }, { text: '导航', link: '/guide/multi-page-navigation' }, { dividerType: 'solid' }, + { sectionHeaderText: 'Web 平台' }, + { text: 'Web 平台指南', link: '/guide/web-platform' }, + { text: 'Web Methods', link: '/guide/web-method-implementations' }, + { text: '限制', link: '/guide/web-limitations' }, + { dividerType: 'solid' }, { sectionHeaderText: '原生模块' }, { text: '自定义 Method', link: '/guide/get-started/create-custom-method' }, ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c310393..45e32ac4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -481,6 +481,12 @@ importers: '@lynx-js/types': specifier: ^3.7.0 version: 3.7.0 + '@lynx-js/web-core': + specifier: ^0.7.0 + version: 0.7.1(@lynx-js/web-elements@0.7.7(tslib@2.8.1)) + '@lynx-js/web-elements': + specifier: ^0.7.0 + version: 0.7.7(tslib@2.8.1) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -502,6 +508,9 @@ importers: sparkling-types: specifier: ~2.1.0-rc.12 version: 2.1.0-rc.20(@lynx-js/types@3.7.0) + sparkling-web-shell: + specifier: workspace:* + version: link:../../packages/sparkling-web-shell typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1274,6 +1283,9 @@ packages: '@lynx-js/web-constants@0.19.8': resolution: {integrity: sha512-aOk2wmwNu0jJ4ERqU+FBva20uZsQi3Uw1uRwOw8pv3fnetNhW5BlExuGdUzv6XTHbOEd1Jfm4jiOwjrzgXr0PQ==} + '@lynx-js/web-constants@0.7.1': + resolution: {integrity: sha512-W8JmHKBwb2BGNnY8ZJBLKbKqP6cd9oUbo6ElezwFUuznDcIasxf43HSccSBpHxwghD1sdF5WrP1PFF5giIBazg==} + '@lynx-js/web-core-wasm@0.0.5': resolution: {integrity: sha512-ICvkMf9Myx8/eShv3oCXp9/u+u7yLMC7WTDtHDEZXGyLVkT4dQqtevw7AfOgihu8PQKncOfeMPzO2csFKDejzw==} peerDependencies: @@ -1308,6 +1320,18 @@ packages: tslib: optional: true + '@lynx-js/web-core@0.7.1': + resolution: {integrity: sha512-Vn6rmp0EWetm2mQMThcs9yQFRd1SxPy3+AmaapgYQLS+RIfRlQqHQmQBxWIJlUGCcq9iLFM03xw0H/YVYN3kHg==} + peerDependencies: + '@lynx-js/lynx-core': 0.1.0 + '@lynx-js/web-elements': '>=0.1.0' + + '@lynx-js/web-elements-reactive@0.2.2': + resolution: {integrity: sha512-IVZJaD4IDtVMaFG14vwhavPH6RJPU+VgxN3NLgcRB+2DO+0Ks1/0hYAJuk/QskGF6A7haGsG9TW/EQ7mk1s/sg==} + + '@lynx-js/web-elements-template@0.7.7': + resolution: {integrity: sha512-i/Aavm6Ijk8hs7BiBxu4fjatwzkKU0s1Vbj5wdH7lo18FvCbOJZsWUuzNTwXR41D49F8oPI4hQvs0OkKv8ZUJQ==} + '@lynx-js/web-elements@0.11.3': resolution: {integrity: sha512-GmfJ1pyvIyR2BpFaWYlH/zlVujVMuCUj+rK20vxlza1wQdSsc5wOeBHbbY3DGq73DvtkhPEczKqIncRUQGZaMA==} peerDependencies: @@ -1323,23 +1347,42 @@ packages: peerDependencies: tslib: ^2.5.0 + '@lynx-js/web-elements@0.7.7': + resolution: {integrity: sha512-8DjHNinJqQNJ2Ues7VAaf2HHGxmg7EQEiKsZ0f/u/avy0X+RNrACt42sY7VUI0+r9asC7VcRkD/QILUd9VCkNQ==} + peerDependencies: + tslib: ^2.5.0 + '@lynx-js/web-mainthread-apis@0.19.8': resolution: {integrity: sha512-Ze6u1wg5VZO1htXG6AVRB6CSf1qDcMrxtyl22BMmQhRvm9ut4Ck8k1+e5MIaUCTST1q1j0s59bsKsSstZ/Xlng==} + '@lynx-js/web-mainthread-apis@0.7.1': + resolution: {integrity: sha512-xeBYM8DoVCiAN70gsq4EQs4all++zdkIWSflIkbJHMdiCWNWFeb+UNBjDLTYBfP4NwNZosnKhLArofMSne85cw==} + '@lynx-js/web-rsbuild-server-middleware@0.19.9': resolution: {integrity: sha512-iOqUxrXUD6L6ot01AMb9VXdR/Twh/e6t4SRKFC1NafIw99gUHumVsQHUGh5oZQLtP7mZHJMkeBcavzqvfcW58w==} + '@lynx-js/web-style-transformer@0.2.2': + resolution: {integrity: sha512-b/6HCg7pHzmBapr4FFZqzchZwyw9kfjCOuwKFJZp2Xn4ABCUHEbPfBfO3EPEO+mm1CjiN48/YcXG1IxHME7b7w==} + '@lynx-js/web-worker-rpc@0.19.8': resolution: {integrity: sha512-POkpPZQjU+a0V/LGWNkaqy+/E6B2WEDYnbh+0FtRNLXcNOBOcgvZNg0fo6VVbw3UInmTwO0v+2g/kMjotizwkg==} '@lynx-js/web-worker-rpc@0.20.4': resolution: {integrity: sha512-i4RV8f397nl02zB1wjFfEJDo/bM0c+dvzd9nIPY9gksf2lX8aSOpkXn6Kyou+05qdJAXm2EZIl8SKYiTMi/P6w==} + '@lynx-js/web-worker-rpc@0.7.1': + resolution: {integrity: sha512-5aKeVfElE+duqBFWiOaiBBZiC2B7aTDJI88jY7ROrikD5suftmANbMRme8YC46UmJBOo2a5SyLaoZcW54SjL5Q==} + '@lynx-js/web-worker-runtime@0.19.8': resolution: {integrity: sha512-lZHnEWQ0QrNIHRGUzYBS22d4ckwyUM+1Gee8D2pCYSAdyqr4aRaNJrUEp8sO8YTh91kohN3c09YQcgRZ8sIstQ==} peerDependencies: '@lynx-js/lynx-core': 0.1.3 + '@lynx-js/web-worker-runtime@0.7.1': + resolution: {integrity: sha512-zqxmqBb77E54VHTo92wMmenRyqIuM9mUON8F+sJKPq0eBa8F2GG4y/fewaXvFU+6pfQUMawGU8wsAUqEiTJwrA==} + peerDependencies: + '@lynx-js/lynx-core': 0.1.0 + '@lynx-js/webpack-dev-transport@0.2.0': resolution: {integrity: sha512-RSy02FSoMsavEn2wna4khSJwT2uGW4XeLduKH8UDT3aCsBNM/rXndUyG6PbwMcywXCeyb8UofkhaQDtJvNJklA==} engines: {node: '>=18'} @@ -2339,7 +2382,6 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unhead/react@2.1.13': resolution: {integrity: sha512-gC48tNJ0UtbithkiKCc2WUlxbVVk5o171EtruS2w2hQUblfYFHzCPu2hljjT1e0tUHXXqN8EMv7mpxHddMB2sg==} @@ -4441,13 +4483,8 @@ packages: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -4812,10 +4849,6 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.10: - resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.17: resolution: {integrity: sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==} engines: {node: ^10 || ^12 || >=14} @@ -6232,7 +6265,7 @@ snapshots: '@babel/code-frame@7.26.2': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -6989,7 +7022,7 @@ snapshots: dependencies: '@lynx-js/webpack-runtime-globals': 0.0.6 - '@lynx-js/css-extract-webpack-plugin@0.7.0(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1))(webpack@5.105.0)': + '@lynx-js/css-extract-webpack-plugin@0.7.0(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1))(webpack@5.105.0)': dependencies: '@lynx-js/template-webpack-plugin': 0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1) mini-css-extract-plugin: 2.10.2(webpack@5.105.0) @@ -7023,16 +7056,16 @@ snapshots: '@lynx-js/react-alias-rsbuild-plugin@0.12.10': {} - '@lynx-js/react-refresh-webpack-plugin@0.3.4(@lynx-js/react-webpack-plugin@0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1)))': + '@lynx-js/react-refresh-webpack-plugin@0.3.4(@lynx-js/react-webpack-plugin@0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1)))': dependencies: - '@lynx-js/react-webpack-plugin': 0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1)) + '@lynx-js/react-webpack-plugin': 0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1)) '@lynx-js/react-rsbuild-plugin@0.12.10(@lynx-js/lynx-core@0.1.3)(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(tslib@2.8.1)(webpack@5.105.0)': dependencies: - '@lynx-js/css-extract-webpack-plugin': 0.7.0(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1))(webpack@5.105.0) + '@lynx-js/css-extract-webpack-plugin': 0.7.0(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1))(webpack@5.105.0) '@lynx-js/react-alias-rsbuild-plugin': 0.12.10 - '@lynx-js/react-refresh-webpack-plugin': 0.3.4(@lynx-js/react-webpack-plugin@0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1))) - '@lynx-js/react-webpack-plugin': 0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1)) + '@lynx-js/react-refresh-webpack-plugin': 0.3.4(@lynx-js/react-webpack-plugin@0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1))) + '@lynx-js/react-webpack-plugin': 0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1)) '@lynx-js/runtime-wrapper-webpack-plugin': 0.1.3 '@lynx-js/template-webpack-plugin': 0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1) '@lynx-js/use-sync-external-store': 1.5.0(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28)) @@ -7044,7 +7077,7 @@ snapshots: - tslib - webpack - '@lynx-js/react-webpack-plugin@0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1))': + '@lynx-js/react-webpack-plugin@0.7.4(@lynx-js/react@0.116.5(@lynx-js/types@3.7.0)(@types/react@18.3.28))(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1))': dependencies: '@lynx-js/template-webpack-plugin': 0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1) '@lynx-js/webpack-runtime-globals': 0.0.6 @@ -7123,6 +7156,10 @@ snapshots: dependencies: '@lynx-js/web-worker-rpc': 0.19.8 + '@lynx-js/web-constants@0.7.1': + dependencies: + '@lynx-js/web-worker-rpc': 0.7.1 + '@lynx-js/web-core-wasm@0.0.5(@lynx-js/css-serializer@0.1.4)(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1)': dependencies: '@lynx-js/web-elements': 0.12.0(tslib@2.8.1) @@ -7151,6 +7188,17 @@ snapshots: '@lynx-js/lynx-core': 0.1.3 tslib: 2.8.1 + '@lynx-js/web-core@0.7.1(@lynx-js/web-elements@0.7.7(tslib@2.8.1))': + dependencies: + '@lynx-js/web-constants': 0.7.1 + '@lynx-js/web-elements': 0.7.7(tslib@2.8.1) + '@lynx-js/web-worker-rpc': 0.7.1 + '@lynx-js/web-worker-runtime': 0.7.1 + + '@lynx-js/web-elements-reactive@0.2.2': {} + + '@lynx-js/web-elements-template@0.7.7': {} + '@lynx-js/web-elements@0.11.3(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -7165,18 +7213,35 @@ snapshots: markdown-it: 14.1.1 tslib: 2.8.1 + '@lynx-js/web-elements@0.7.7(tslib@2.8.1)': + dependencies: + '@lynx-js/web-elements-reactive': 0.2.2 + '@lynx-js/web-elements-template': 0.7.7 + tslib: 2.8.1 + '@lynx-js/web-mainthread-apis@0.19.8': dependencies: '@lynx-js/web-constants': 0.19.8 hyphenate-style-name: 1.1.0 wasm-feature-detect: 1.8.0 + '@lynx-js/web-mainthread-apis@0.7.1': + dependencies: + '@lynx-js/web-constants': 0.7.1 + '@lynx-js/web-style-transformer': 0.2.2 + css-tree: 3.2.1 + hyphenate-style-name: 1.1.0 + '@lynx-js/web-rsbuild-server-middleware@0.19.9': {} + '@lynx-js/web-style-transformer@0.2.2': {} + '@lynx-js/web-worker-rpc@0.19.8': {} '@lynx-js/web-worker-rpc@0.20.4': {} + '@lynx-js/web-worker-rpc@0.7.1': {} + '@lynx-js/web-worker-runtime@0.19.8(@lynx-js/lynx-core@0.1.3)': dependencies: '@lynx-js/lynx-core': 0.1.3 @@ -7185,6 +7250,12 @@ snapshots: '@lynx-js/web-mainthread-apis': 0.19.8 '@lynx-js/web-worker-rpc': 0.19.8 + '@lynx-js/web-worker-runtime@0.7.1': + dependencies: + '@lynx-js/web-constants': 0.7.1 + '@lynx-js/web-mainthread-apis': 0.7.1 + '@lynx-js/web-worker-rpc': 0.7.1 + '@lynx-js/webpack-dev-transport@0.2.0': {} '@lynx-js/webpack-runtime-globals@0.0.6': {} @@ -9070,16 +9141,16 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@7.4.0(postcss@8.5.10): + css-declaration-sorter@7.4.0(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 css-minimizer-webpack-plugin@7.0.2(webpack@5.105.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 7.1.5(postcss@8.5.10) + cssnano: 7.1.5(postcss@8.5.17) jest-worker: 29.7.0 - postcss: 8.5.10 + postcss: 8.5.17 schema-utils: 4.3.3 serialize-javascript: 6.0.2 webpack: 5.105.0 @@ -9108,49 +9179,49 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.13(postcss@8.5.10): + cssnano-preset-default@7.0.13(postcss@8.5.17): dependencies: browserslist: 4.28.2 - css-declaration-sorter: 7.4.0(postcss@8.5.10) - cssnano-utils: 5.0.1(postcss@8.5.10) - postcss: 8.5.10 - postcss-calc: 10.1.1(postcss@8.5.10) - postcss-colormin: 7.0.8(postcss@8.5.10) - postcss-convert-values: 7.0.10(postcss@8.5.10) - postcss-discard-comments: 7.0.6(postcss@8.5.10) - postcss-discard-duplicates: 7.0.2(postcss@8.5.10) - postcss-discard-empty: 7.0.1(postcss@8.5.10) - postcss-discard-overridden: 7.0.1(postcss@8.5.10) - postcss-merge-longhand: 7.0.5(postcss@8.5.10) - postcss-merge-rules: 7.0.9(postcss@8.5.10) - postcss-minify-font-values: 7.0.1(postcss@8.5.10) - postcss-minify-gradients: 7.0.3(postcss@8.5.10) - postcss-minify-params: 7.0.7(postcss@8.5.10) - postcss-minify-selectors: 7.0.6(postcss@8.5.10) - postcss-normalize-charset: 7.0.1(postcss@8.5.10) - postcss-normalize-display-values: 7.0.1(postcss@8.5.10) - postcss-normalize-positions: 7.0.1(postcss@8.5.10) - postcss-normalize-repeat-style: 7.0.1(postcss@8.5.10) - postcss-normalize-string: 7.0.1(postcss@8.5.10) - postcss-normalize-timing-functions: 7.0.1(postcss@8.5.10) - postcss-normalize-unicode: 7.0.7(postcss@8.5.10) - postcss-normalize-url: 7.0.1(postcss@8.5.10) - postcss-normalize-whitespace: 7.0.1(postcss@8.5.10) - postcss-ordered-values: 7.0.2(postcss@8.5.10) - postcss-reduce-initial: 7.0.7(postcss@8.5.10) - postcss-reduce-transforms: 7.0.1(postcss@8.5.10) - postcss-svgo: 7.1.1(postcss@8.5.10) - postcss-unique-selectors: 7.0.5(postcss@8.5.10) - - cssnano-utils@5.0.1(postcss@8.5.10): - dependencies: - postcss: 8.5.10 - - cssnano@7.1.5(postcss@8.5.10): - dependencies: - cssnano-preset-default: 7.0.13(postcss@8.5.10) + css-declaration-sorter: 7.4.0(postcss@8.5.17) + cssnano-utils: 5.0.1(postcss@8.5.17) + postcss: 8.5.17 + postcss-calc: 10.1.1(postcss@8.5.17) + postcss-colormin: 7.0.8(postcss@8.5.17) + postcss-convert-values: 7.0.10(postcss@8.5.17) + postcss-discard-comments: 7.0.6(postcss@8.5.17) + postcss-discard-duplicates: 7.0.2(postcss@8.5.17) + postcss-discard-empty: 7.0.1(postcss@8.5.17) + postcss-discard-overridden: 7.0.1(postcss@8.5.17) + postcss-merge-longhand: 7.0.5(postcss@8.5.17) + postcss-merge-rules: 7.0.9(postcss@8.5.17) + postcss-minify-font-values: 7.0.1(postcss@8.5.17) + postcss-minify-gradients: 7.0.3(postcss@8.5.17) + postcss-minify-params: 7.0.7(postcss@8.5.17) + postcss-minify-selectors: 7.0.6(postcss@8.5.17) + postcss-normalize-charset: 7.0.1(postcss@8.5.17) + postcss-normalize-display-values: 7.0.1(postcss@8.5.17) + postcss-normalize-positions: 7.0.1(postcss@8.5.17) + postcss-normalize-repeat-style: 7.0.1(postcss@8.5.17) + postcss-normalize-string: 7.0.1(postcss@8.5.17) + postcss-normalize-timing-functions: 7.0.1(postcss@8.5.17) + postcss-normalize-unicode: 7.0.7(postcss@8.5.17) + postcss-normalize-url: 7.0.1(postcss@8.5.17) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.17) + postcss-ordered-values: 7.0.2(postcss@8.5.17) + postcss-reduce-initial: 7.0.7(postcss@8.5.17) + postcss-reduce-transforms: 7.0.1(postcss@8.5.17) + postcss-svgo: 7.1.1(postcss@8.5.17) + postcss-unique-selectors: 7.0.5(postcss@8.5.17) + + cssnano-utils@5.0.1(postcss@8.5.17): + dependencies: + postcss: 8.5.17 + + cssnano@7.1.5(postcss@8.5.17): + dependencies: + cssnano-preset-default: 7.0.13(postcss@8.5.17) lilconfig: 3.1.3 - postcss: 8.5.10 + postcss: 8.5.17 csso@5.0.5: dependencies: @@ -11297,9 +11368,7 @@ snapshots: mustache@4.2.0: {} - nanoid@3.3.11: {} - - nanoid@3.3.16: {} + nanoid@3.3.15: {} natural-compare@1.4.0: {} @@ -11479,142 +11548,142 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-calc@10.1.1(postcss@8.5.10): + postcss-calc@10.1.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.8(postcss@8.5.10): + postcss-colormin@7.0.8(postcss@8.5.17): dependencies: '@colordx/core': 5.0.3 browserslist: 4.28.2 caniuse-api: 3.0.0 - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.10(postcss@8.5.10): + postcss-convert-values@7.0.10(postcss@8.5.17): dependencies: browserslist: 4.28.2 - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.6(postcss@8.5.10): + postcss-discard-comments@7.0.6(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-selector-parser: 7.1.1 - postcss-discard-duplicates@7.0.2(postcss@8.5.10): + postcss-discard-duplicates@7.0.2(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 - postcss-discard-empty@7.0.1(postcss@8.5.10): + postcss-discard-empty@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 - postcss-discard-overridden@7.0.1(postcss@8.5.10): + postcss-discard-overridden@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 - postcss-merge-longhand@7.0.5(postcss@8.5.10): + postcss-merge-longhand@7.0.5(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - stylehacks: 7.0.9(postcss@8.5.10) + stylehacks: 7.0.9(postcss@8.5.17) - postcss-merge-rules@7.0.9(postcss@8.5.10): + postcss-merge-rules@7.0.9(postcss@8.5.17): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 - cssnano-utils: 5.0.1(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 5.0.1(postcss@8.5.17) + postcss: 8.5.17 postcss-selector-parser: 7.1.1 - postcss-minify-font-values@7.0.1(postcss@8.5.10): + postcss-minify-font-values@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.3(postcss@8.5.10): + postcss-minify-gradients@7.0.3(postcss@8.5.17): dependencies: '@colordx/core': 5.0.3 - cssnano-utils: 5.0.1(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 5.0.1(postcss@8.5.17) + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.7(postcss@8.5.10): + postcss-minify-params@7.0.7(postcss@8.5.17): dependencies: browserslist: 4.28.2 - cssnano-utils: 5.0.1(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 5.0.1(postcss@8.5.17) + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.6(postcss@8.5.10): + postcss-minify-selectors@7.0.6(postcss@8.5.17): dependencies: cssesc: 3.0.0 - postcss: 8.5.10 + postcss: 8.5.17 postcss-selector-parser: 7.1.1 - postcss-normalize-charset@7.0.1(postcss@8.5.10): + postcss-normalize-charset@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 - postcss-normalize-display-values@7.0.1(postcss@8.5.10): + postcss-normalize-display-values@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.1(postcss@8.5.10): + postcss-normalize-positions@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.1(postcss@8.5.10): + postcss-normalize-repeat-style@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.1(postcss@8.5.10): + postcss-normalize-string@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.1(postcss@8.5.10): + postcss-normalize-timing-functions@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.7(postcss@8.5.10): + postcss-normalize-unicode@7.0.7(postcss@8.5.17): dependencies: browserslist: 4.28.2 - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.1(postcss@8.5.10): + postcss-normalize-url@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.1(postcss@8.5.10): + postcss-normalize-whitespace@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.2(postcss@8.5.10): + postcss-ordered-values@7.0.2(postcss@8.5.17): dependencies: - cssnano-utils: 5.0.1(postcss@8.5.10) - postcss: 8.5.10 + cssnano-utils: 5.0.1(postcss@8.5.17) + postcss: 8.5.17 postcss-value-parser: 4.2.0 - postcss-reduce-initial@7.0.7(postcss@8.5.10): + postcss-reduce-initial@7.0.7(postcss@8.5.17): dependencies: browserslist: 4.28.2 caniuse-api: 3.0.0 - postcss: 8.5.10 + postcss: 8.5.17 - postcss-reduce-transforms@7.0.1(postcss@8.5.10): + postcss-reduce-transforms@7.0.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 postcss-selector-parser@7.1.1: @@ -11622,28 +11691,22 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.1.1(postcss@8.5.10): + postcss-svgo@7.1.1(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-value-parser: 4.2.0 svgo: 4.0.1 - postcss-unique-selectors@7.0.5(postcss@8.5.10): + postcss-unique-selectors@7.0.5(postcss@8.5.17): dependencies: - postcss: 8.5.10 + postcss: 8.5.17 postcss-selector-parser: 7.1.1 postcss-value-parser@4.2.0: {} - postcss@8.5.10: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.17: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -12506,10 +12569,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylehacks@7.0.9(postcss@8.5.10): + stylehacks@7.0.9(postcss@8.5.17): dependencies: browserslist: 4.28.2 - postcss: 8.5.10 + postcss: 8.5.17 postcss-selector-parser: 7.1.1 supports-color@7.2.0: @@ -12944,7 +13007,7 @@ snapshots: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.10 + postcss: 8.5.17 rollup: 4.60.1 tinyglobby: 0.2.16 optionalDependencies: @@ -12961,7 +13024,7 @@ snapshots: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.10 + postcss: 8.5.17 rollup: 4.60.1 tinyglobby: 0.2.16 optionalDependencies: @@ -13094,7 +13157,7 @@ snapshots: vue-lynx@0.4.0(@lynx-js/lynx-core@0.1.3)(@lynx-js/types@3.7.0)(@rsbuild/core@2.0.0-rc.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(core-js@3.47.0))(@rsbuild/plugin-vue@1.2.9(@rsbuild/core@2.0.0-rc.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(core-js@3.47.0))(@rspack/core@1.7.11(@swc/helpers@0.5.21))(vue@3.5.39(typescript@5.9.3)))(@types/react@19.2.14)(tslib@2.8.1)(webpack@5.105.0): dependencies: - '@lynx-js/css-extract-webpack-plugin': 0.7.0(@lynx-js/template-webpack-plugin@0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1))(webpack@5.105.0) + '@lynx-js/css-extract-webpack-plugin': 0.7.0(@lynx-js/template-webpack-plugin@0.10.5(tslib@2.8.1))(webpack@5.105.0) '@lynx-js/react': 0.116.5(@lynx-js/types@3.7.0)(@types/react@19.2.14) '@lynx-js/runtime-wrapper-webpack-plugin': 0.1.3 '@lynx-js/template-webpack-plugin': 0.10.5(@lynx-js/lynx-core@0.1.3)(tslib@2.8.1) diff --git a/template/sparkling-app-template/app.config.ts b/template/sparkling-app-template/app.config.ts index 03eedf13..38c128dd 100644 --- a/template/sparkling-app-template/app.config.ts +++ b/template/sparkling-app-template/app.config.ts @@ -12,9 +12,23 @@ const lynxConfig = defineConfig({ }, }, output: { - assetPrefix: 'asset:///', filename: { - bundle: '[name].lynx.bundle' + bundle: '[name].lynx.bundle', + }, + }, + environments: { + web: { + output: { + assetPrefix: '/', + distPath: { + root: 'dist/web', + }, + }, + }, + lynx: { + output: { + assetPrefix: 'asset:///', + }, }, }, plugins: [ diff --git a/template/sparkling-app-template/package.json b/template/sparkling-app-template/package.json index 4079c5b3..5a1bebbd 100644 --- a/template/sparkling-app-template/package.json +++ b/template/sparkling-app-template/package.json @@ -12,6 +12,9 @@ "scripts": { "dev": "sparkling-app-cli dev", "build": "sparkling-app-cli build --copy", + "build:web": "sparkling-app-cli build --platform web", + "dev:web": "sparkling-app-cli dev --platform web", + "run:web": "sparkling-app-cli run:web", "autolink": "sparkling-app-cli autolink", "test": "vitest run --passWithNoTests", "run:android": "sparkling-app-cli run:android", @@ -27,7 +30,10 @@ "@lynx-js/react-rsbuild-plugin": "^0.12.7", "@lynx-js/rspeedy": "^0.13.3", "@lynx-js/types": "^3.7.0", + "@lynx-js/web-core": "^0.7.0", + "@lynx-js/web-elements": "^0.7.0", "sparkling-types": "~2.1.0-rc.12", + "sparkling-web-shell": "workspace:*", "@testing-library/jest-dom": "^6.9.1", "@types/react": "^18.3.20", "@vitest/coverage-v8": "^4.0.18",