Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/tricky-lamps-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@lynx-js/qrcode-rsbuild-plugin": patch
---

feat: support web platform preview

use rspeedy settings to enable Lynx Web Platform preview

```js
// rspeedy configurations
environments:{
web:{},
lynx:{}
}
```
4 changes: 4 additions & 0 deletions examples/react/lynx.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ export default defineConfig({
performance: {
profile: enableBundleAnalysis,
},
environments: {
web: {},
lynx: {},
},
});
3 changes: 3 additions & 0 deletions packages/rspeedy/plugin-qrcode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
"build": "rslib build",
"test": "pnpm -w run test --project rspeedy/qrcode"
},
"dependencies": {
"@lynx-js/web-rsbuild-server-middleware": "workspace:*"
},
"devDependencies": {
"@clack/prompts": "1.0.0-alpha.5",
"@lynx-js/rspeedy": "workspace:*",
Expand Down
45 changes: 39 additions & 6 deletions packages/rspeedy/plugin-qrcode/src/generateDevUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import type { ExposedAPI } from '@lynx-js/rspeedy'

import type { CustomizedSchemaFn } from './index.js'

export default function generateDevUrls(
function generateFileNameBase(
api: RsbuildPluginAPI,
entry: string,
schemaFn: CustomizedSchemaFn,
port: number,
): Record<string, string> {
): { name: string, assetPublicPath: string } {
const { dev: { assetPrefix } } = api.getNormalizedConfig()
const { config } = api.useExposed<ExposedAPI>(
Symbol.for('rspeedy.api'),
Expand All @@ -34,16 +32,51 @@ export default function generateDevUrls(
} else {
name = filename
}
// <port> is supported in `dev.assetPrefix`, we should replace it with the real port
const assetPublicPath = assetPrefix.replaceAll('<port>', String(port))
return { name, assetPublicPath }
}

export function generateExplorerDevUrls(
api: RsbuildPluginAPI,
entry: string,
schemaFn: CustomizedSchemaFn,
port: number,
): Record<string, string> {
const { name, assetPublicPath } = generateFileNameBase(api, port)

const customSchema = schemaFn(
new URL(
name.replace('[name]', entry).replace('[platform]', 'lynx'),
// <port> is supported in `dev.assetPrefix`, we should replace it with the real port
assetPrefix.replaceAll('<port>', String(port)),
assetPublicPath,
).toString(),
)

return typeof customSchema === 'string'
? { default: customSchema }
: customSchema
}

export function generateWebDevUrls(
api: RsbuildPluginAPI,
webEntries: string[],
port: number,
): Record<string, string> {
const { name, assetPublicPath } = generateFileNameBase(api, port)
return Object.fromEntries(
webEntries.map(entry => {
const base = URL.parse(assetPublicPath)
? assetPublicPath
: `http://localhost:${port}/`
const pathname = new URL(
name.replace('[name]', entry).replace('[platform]', 'web'),
base,
).pathname
const url = new URL(`/web?casename=${pathname}`, base).toString()
return [
entry,
url,
]
}),
)
}
28 changes: 16 additions & 12 deletions packages/rspeedy/plugin-qrcode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import type { EnvironmentContext, RsbuildPlugin } from '@rsbuild/core'

import { createWebVirtualFilesMiddleware } from '@lynx-js/web-rsbuild-server-middleware'

Comment on lines +13 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Fix missing dependency/exports for web middleware import

Type-check fails: Cannot find module '@lynx-js/web-rsbuild-server-middleware'. Ensure the package is part of the workspace, exported correctly, and added as a dependency of this plugin. If the package exposes a node entry, import it explicitly.

Apply one of:

- import { createWebVirtualFilesMiddleware } from '@lynx-js/web-rsbuild-server-middleware'
+ import { createWebVirtualFilesMiddleware } from '@lynx-js/web-rsbuild-server-middleware/node'

Or add dependency and proper exports/types to the workspace package.

Run this script to verify dependency wiring and exports:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Check plugin dependency..."
PLUGIN_PKG=$(fd -a -t f '^package\.json$' | rg -n 'packages/rspeedy/plugin-qrcode/package.json' -N | cut -d: -f1 || true)
WEB_PKG=$(fd -a -t f '^package\.json$' | rg -n 'packages/web-platform/web-rsbuild-server-middleware/package.json' -N | cut -d: -f1 || true)

if [[ -z "$PLUGIN_PKG" || -z "$WEB_PKG" ]]; then
  echo "Missing package.json files. Found:"
  echo "plugin: $PLUGIN_PKG"
  echo "web   : $WEB_PKG"
  exit 1
fi

echo "Plugin depends on web middleware?"
jq -r '.dependencies["@lynx-js/web-rsbuild-server-middleware"], .devDependencies["@lynx-js/web-rsbuild-server-middleware"]' "$PLUGIN_PKG"

echo "Web middleware package name and exports:"
jq -r '.name, .exports, .main, .types' "$WEB_PKG"

echo "Look for a 'node' export subpath:"
jq -r '.exports["./node"], .exports["node"]' "$WEB_PKG" 2>/dev/null || true

Length of output: 225


Expose a node entry and type declarations from @lynx-js/web-rsbuild-server-middleware

  • packages/rspeedy/plugin-qrcode/package.json depends on @lynx-js/web-rsbuild-server-middleware (workspace:*).
  • packages/web-platform/web-rsbuild-server-middleware/package.json has no "exports" and no "types" (main: dist/index.js) and does not expose a "./node" subpath — add an "exports" mapping (include "./node" if you intend to import '/node') and publish TypeScript declarations or set "types" so the module can be resolved by type-checking.
🧰 Tools
🪛 GitHub Check: code-style-check

[failure] 13-13:
Cannot find module '@lynx-js/web-rsbuild-server-middleware' or its corresponding type declarations.

🤖 Prompt for AI Agents
In packages/rspeedy/plugin-qrcode/src/index.ts around lines 13 to 14, the import
from '@lynx-js/web-rsbuild-server-middleware' fails type-resolution because the
source package (packages/web-platform/web-rsbuild-server-middleware) does not
declare "exports" (including a "./node" subpath if you intend to import a
node-specific entry) nor does it publish TypeScript declarations or set "types".
Fix package.json of web-rsbuild-server-middleware by adding an "exports" map
that exposes the default entry and a "./node" subpath (pointing to the built JS
files), and add a "types" field or publish .d.ts files from the dist so
TypeScript and consumers can resolve imports and typings correctly; then
rebuild/publish the package and update workspace deps if needed.

import { registerConsoleShortcuts } from './shortcuts.js'

/**
Expand Down Expand Up @@ -101,8 +103,13 @@ export function pluginQRCode(
name: 'lynx:rsbuild:qrcode',
pre: ['lynx:rsbuild:api'],
setup(api) {
api.onBeforeStartDevServer(({ environments, server }) => {
if (environments['web']) {
server.middlewares.use(createWebVirtualFilesMiddleware('/web'))
}
})
api.onAfterStartProdServer(async ({ environments, port }) => {
await main(environments['lynx'], port)
await main(environments, port)
})

let printedQRCode = false
Expand All @@ -122,7 +129,7 @@ export function pluginQRCode(

printedQRCode = true

await main(environments['lynx'], api.context.devServer.port)
await main(environments, api.context.devServer.port)
})

api.modifyRsbuildConfig((config) => {
Expand All @@ -138,23 +145,20 @@ export function pluginQRCode(
})

async function main(
environmentContext: EnvironmentContext | undefined,
environments: Record<string, EnvironmentContext>,
port: number,
) {
if (!environmentContext) {
// Not lynx environment, skip print QRCode
return
}

const entries = Object.keys(environmentContext.entry)

if (entries.length === 0) {
const lynxEntries = Object.keys(environments['lynx']?.entry ?? {})
const webEntries = Object.keys(environments['web']?.entry ?? {})
if (lynxEntries.length === 0 && webEntries.length === 0) {
// Not lynx or web environment, skip print
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: the comment does not match the condition.

return
}

const unregister = await registerConsoleShortcuts(
{
entries,
entries: lynxEntries,
webEntries,
api,
port,
schema,
Expand Down
78 changes: 50 additions & 28 deletions packages/rspeedy/plugin-qrcode/src/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import type { RsbuildPluginAPI } from '@rsbuild/core'

import type { ExposedAPI } from '@lynx-js/rspeedy'

import generateDevUrls from './generateDevUrls.js'
import {
generateExplorerDevUrls,
generateWebDevUrls,
} from './generateDevUrls.js'

import type { CustomizedSchemaFn } from './index.js'

Expand All @@ -14,13 +17,16 @@ const gExistingShortcuts = new WeakSet<Options>()
interface Options {
api: RsbuildPluginAPI
entries: string[]
webEntries: string[]
schema: CustomizedSchemaFn
port: number
customShortcuts?: Record<
string,
{ value: string, label: string, hint?: string, action?(): Promise<void> }
>
onPrint?: ((url: string) => Promise<void>) | undefined
onPrint?:
| ((lynx?: string, web?: Record<string, string>) => Promise<void>)
| undefined
}

export async function registerConsoleShortcuts(
Expand All @@ -32,22 +38,35 @@ export async function registerConsoleShortcuts(
import('./showQRCode.js'),
])

const currentEntry = options.entries[0]!
const devUrls = generateDevUrls(
// keep the default entry to be lynx explorer entry if exists
const currentEntry = options.entries[0]
const devUrls = currentEntry
? generateExplorerDevUrls(
options.api,
currentEntry,
options.schema,
options.port,
)
: undefined

const value: string | symbol | undefined = devUrls
? Object.values(devUrls)[0]
: undefined
const webUrls = generateWebDevUrls(
options.api,
currentEntry,
options.schema,
options.webEntries,
options.port,
)

const value: string | symbol = Object.values(devUrls)[0]!
await options.onPrint?.(value)
showQRCode(value)
await options.onPrint?.(
value,
webUrls,
)
showQRCode(value, webUrls)

gExistingShortcuts.add(options)

// We should not `await` on this since it would block the NodeJS main thread.
void loop(options, value, devUrls)
void loop(options, value, devUrls, webUrls)

function off() {
gExistingShortcuts.delete(options)
Expand All @@ -57,8 +76,9 @@ export async function registerConsoleShortcuts(

async function loop(
options: Options,
value: string | symbol,
devUrls: Record<string, string>,
value: string | symbol | undefined,
devUrls: Record<string, string> | undefined,
webUrls: Record<string, string>,
) {
const [
{ autocomplete, select, selectKey, isCancel, cancel },
Expand All @@ -70,8 +90,8 @@ async function loop(

const selectFn = (length: number) => length > 5 ? autocomplete : select

let currentEntry = options.entries[0]!
let currentSchema = Object.keys(devUrls)[0]!
let currentEntry = options.entries[0]
let currentSchema = devUrls ? Object.keys(devUrls)[0]! : undefined

while (!isCancel(value)) {
const name = await selectKey({
Expand All @@ -94,18 +114,18 @@ async function loop(
) {
break
}
if (name === 'r') {
if (name === 'r' && currentSchema) {
const selection = await selectFn(options.entries.length)({
message: 'Select entry',
options: options.entries.map(entry => ({
value: entry,
label: entry,
hint: generateDevUrls(
hint: generateExplorerDevUrls(
options.api,
entry,
options.schema,
options.port,
)[currentSchema]!,
)[currentSchema!]!,
})),
initialValue: currentEntry,
})
Expand All @@ -114,8 +134,8 @@ async function loop(
}
currentEntry = selection
value = getCurrentUrl()
} else if (name === 'a') {
const devUrls = generateDevUrls(
} else if (name === 'a' && currentEntry) {
const devUrls = generateExplorerDevUrls(
options.api,
currentEntry,
options.schema,
Expand All @@ -138,8 +158,8 @@ async function loop(
} else if (options.customShortcuts?.[name]) {
await options.customShortcuts[name].action?.()
}
await options.onPrint?.(value)
showQRCode(value)
await options.onPrint?.(value, webUrls)
showQRCode(value, webUrls)
}

// If the `options` is not deleted from `gExistingShortcuts`, means that this is an explicitly
Expand All @@ -152,12 +172,14 @@ async function loop(
return

function getCurrentUrl(): string {
return generateDevUrls(
options.api,
currentEntry,
options.schema,
options.port,
)[currentSchema]!
return (currentEntry && currentSchema)
? generateExplorerDevUrls(
options.api,
currentEntry,
options.schema,
options.port,
)[currentSchema]!
: ''
}

function exit(code?: number) {
Expand Down
18 changes: 14 additions & 4 deletions packages/rspeedy/plugin-qrcode/src/showQRCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@ import { log } from '@clack/prompts'
import color from 'picocolors'
import { renderUnicodeCompact } from 'uqr'

export default function showQRCode(url: string): void {
log.info(color.green('Scan with Lynx'))
log.success(renderUnicodeCompact(url))
log.success(url)
export default function showQRCode(
lynxUrl?: string,
webUrls?: Record<string, string>,
): void {
if (lynxUrl) {
log.info(color.green('Scan with Lynx'))
log.success(renderUnicodeCompact(lynxUrl))
log.success('Lynx Explorer: ' + lynxUrl)
}
if (webUrls) {
for (const [name, webUrl] of Object.entries(webUrls)) {
log.success(`Web Preview for ${name}: ` + webUrl)
}
}
}
Loading
Loading