Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Turbopack: Implement HMR in next-api #54772

Merged
merged 31 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dd650bf
Setup basic pages websocket
jridgewell Aug 19, 2023
64c5959
Setup basic app websocket
jridgewell Aug 19, 2023
07c3a1c
Keep react-dev-overlay remap in turbo-dev dev server
jridgewell Aug 21, 2023
0cdd01a
Unify page bootstrapping
jridgewell Aug 23, 2023
6ba827e
Make turbopack HMR agnostic to websocket
jridgewell Aug 23, 2023
4897c28
Unify addMessageListener with turbopack
jridgewell Aug 23, 2023
b7663a8
Allow subscriptions to be broken mid iteration
jridgewell Aug 23, 2023
f46d016
Wire up pre-compiled turbopack dev runtime
jridgewell Aug 23, 2023
22ef3a6
tmp
jridgewell Aug 25, 2023
d03a933
Implement hotReloader.send
jridgewell Aug 25, 2023
cffc1a4
Remove HRM setup from app pages
jridgewell Aug 25, 2023
30e3077
Implement middleware changed
jridgewell Aug 25, 2023
3dddb0d
Implement serverOnlyChanges and serverComponentChanges
jridgewell Aug 26, 2023
d9b906e
Implement reloadPage
jridgewell Aug 26, 2023
ba5d601
Pin to latest turbopack
jridgewell Aug 29, 2023
44354eb
Fix changed subscriptions
jridgewell Aug 29, 2023
a1c3327
Don't compile turbopack-ecmascript-runtime
jridgewell Aug 29, 2023
2d8787d
Revert "Don't compile turbopack-ecmascript-runtime"
jridgewell Aug 29, 2023
d847936
Compile dev runtime TS into JS
jridgewell Aug 29, 2023
e374402
Lint
jridgewell Aug 30, 2023
8e099a0
Update sass loader for some reason
jridgewell Aug 30, 2023
b478dfe
Our Eslint setup sucks
jridgewell Aug 30, 2023
80864bd
update turbopack
sokra Aug 30, 2023
f6d2e25
update changed APIs
sokra Aug 30, 2023
9857ae6
Merge remote-tracking branch 'origin/canary' into jrl-hmr
sokra Aug 30, 2023
7e5e581
Merge branch 'sokra/update-turbopack9' into jrl-hmr
sokra Aug 30, 2023
5a19faf
remove turbopack runtime from compiled
sokra Aug 30, 2023
36578f7
Merge remote-tracking branch 'origin/canary' into sokra-jrl/hmr
sokra Aug 30, 2023
e339cc6
fix test case
sokra Aug 30, 2023
d5b5bb6
fix dependencies
sokra Aug 30, 2023
b3c2c63
fix import
sokra Aug 30, 2023
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
2 changes: 1 addition & 1 deletion packages/next-swc/crates/next-api/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,6 @@ impl Endpoint for MiddlewareEndpoint {

#[turbo_tasks::function]
fn client_changed(self: Vc<Self>) -> Vc<Completion> {
Completion::new()
Completion::immutable()
}
}
4 changes: 2 additions & 2 deletions packages/next-swc/crates/next-api/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ impl Project {
Ok(self
.await?
.versioned_content_map
.get(self.client_root().join(identifier)))
.get(self.client_relative_path().join(identifier)))
}

#[turbo_tasks::function]
Expand Down Expand Up @@ -635,7 +635,7 @@ impl Project {
Ok(self
.await?
.versioned_content_map
.keys_in_path(self.client_root()))
.keys_in_path(self.client_relative_path()))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ pub async fn get_client_resolve_options_context(
let next_client_import_map =
get_next_client_import_map(project_path, ty, next_config, execution_context);
let next_client_fallback_import_map = get_next_client_fallback_import_map(ty);
let next_client_resolved_map = get_next_client_resolved_map(project_path, project_path);
let next_client_resolved_map = get_next_client_resolved_map(project_path, project_path, mode);
let module_options_context = ResolveOptionsContext {
enable_node_modules: Some(project_path.root().resolve().await?),
custom_conditions: vec![mode.node_env().to_string()],
Expand Down
34 changes: 20 additions & 14 deletions packages/next-swc/crates/next-core/src/next_import_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,22 +309,28 @@ pub async fn get_next_edge_import_map(
pub fn get_next_client_resolved_map(
context: Vc<FileSystemPath>,
root: Vc<FileSystemPath>,
mode: NextMode,
) -> Vc<ResolvedMap> {
let glob_mappings = vec![
// Temporary hack to replace the hot reloader until this is passable by props in next.js
(
context.root(),
Glob::new(
"**/next/dist/client/components/react-dev-overlay/hot-reloader-client.js"
.to_string(),
let glob_mappings = if mode == NextMode::Development {
vec![]
} else {
vec![
// Temporary hack to replace the hot reloader until this is passable by props in
// next.js
(
context.root(),
Glob::new(
"**/next/dist/client/components/react-dev-overlay/hot-reloader-client.js"
.to_string(),
),
ImportMapping::PrimaryAlternative(
"@vercel/turbopack-next/dev/hot-reloader.tsx".to_string(),
Some(root),
)
.into(),
),
ImportMapping::PrimaryAlternative(
"@vercel/turbopack-next/dev/hot-reloader.tsx".to_string(),
Some(root),
)
.into(),
),
];
]
};
ResolvedMap {
by_glob: glob_mappings,
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
"@types/ws": "8.2.0",
"@vercel/ncc": "0.34.0",
"@vercel/nft": "0.22.6",
"@vercel/turbopack-ecmascript-runtime": "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-ecmascript-runtime/js?turbopack-230829.2",
"acorn": "8.5.0",
"ajv": "8.11.0",
"amphtml-validator": "1.0.35",
Expand Down
26 changes: 21 additions & 5 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ export type TurbopackResult<T = {}> = T & {
diagnostics: Diagnostics[]
}

interface Middleware {
export interface Middleware {
endpoint: Endpoint
}

Expand All @@ -500,7 +500,7 @@ export interface UpdateInfo {
tasks: number
}

enum ServerClientChangeType {
export enum ServerClientChangeType {
Server = 'Server',
Client = 'Client',
Both = 'Both',
Expand Down Expand Up @@ -550,7 +550,7 @@ export interface Endpoint {
* After changed() has been awaited it will listen to changes.
* The async iterator will yield for each change.
*/
changed(): Promise<AsyncIterableIterator<TurbopackResult>>
changed(): Promise<AsyncIterableIterator<TurbopackResult<ServerClientChange>>>
}

interface EndpointConfig {
Expand Down Expand Up @@ -593,6 +593,8 @@ function bindingToApi(binding: any, _wasm: boolean) {
callback: (err: Error, value: T) => void
) => Promise<{ __napiType: 'RootTask' }>

const cancel = new (class Cancel extends Error {})()

/**
* Utility function to ensure all variants of an enum are handled.
*/
Expand Down Expand Up @@ -635,6 +637,7 @@ function bindingToApi(binding: any, _wasm: boolean) {
reject: (error: Error) => void
}
| undefined
let canceled = false

// The native function will call this every time it emits a new result. We
// either need to notify a waiting consumer, or buffer the new result until
Expand All @@ -652,10 +655,10 @@ function bindingToApi(binding: any, _wasm: boolean) {
}
}

return (async function* () {
const iterator = (async function* () {
const task = await withErrorCause(() => nativeFunction(emitResult))
try {
while (true) {
while (!canceled) {
if (buffer.length > 0) {
const item = buffer.shift()!
if (item.err) throw item.err
Expand All @@ -667,10 +670,19 @@ function bindingToApi(binding: any, _wasm: boolean) {
})
}
}
} catch (e) {
if (e === cancel) return
throw e
} finally {
binding.rootTaskDispose(task)
}
})()
iterator.return = async () => {
canceled = true
if (waiting) waiting.reject(cancel)
return { value: undefined, done: true } as IteratorReturnResult<never>
}
return iterator
}

/**
Expand Down Expand Up @@ -908,6 +920,10 @@ function bindingToApi(binding: any, _wasm: boolean) {
)
)

// The subscriptions will emit always emit once, which is the initial
// computation. This is not a change, so swallow it.
await Promise.all([serverSubscription.next(), clientSubscription.next()])

return (async function* () {
try {
while (true) {
Expand Down
10 changes: 2 additions & 8 deletions packages/next/src/client/dev/amp-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,8 @@ async function tryApplyUpdates() {
}
}

addMessageListener((event) => {
if (event.data === '\uD83D\uDC93') {
return
}

addMessageListener((message) => {
try {
const message = JSON.parse(event.data)

// actions which are not related to amp-dev
if (
message.action === 'serverError' ||
Expand All @@ -102,7 +96,7 @@ addMessageListener((event) => {
}
} catch (err: any) {
console.warn(
'[HMR] Invalid message: ' + event.data + '\n' + (err?.stack ?? '')
'[HMR] Invalid message: ' + message + '\n' + (err?.stack ?? '')
)
}
})
Expand Down
16 changes: 4 additions & 12 deletions packages/next/src/client/dev/dev-build-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type VerticalPosition = 'top' | 'bottom'
type HorizonalPosition = 'left' | 'right'

export default function initializeBuildWatcher(
toggleCallback: (cb: (event: string | { data: string }) => void) => void,
toggleCallback: (cb: (obj: Record<string, any>) => void) => void,
position = 'bottom-right'
) {
const shadowHost = document.createElement('div')
Expand Down Expand Up @@ -53,21 +53,13 @@ export default function initializeBuildWatcher(

// Handle events

addMessageListener((event) => {
// This is the heartbeat event
if (event.data === '\uD83D\uDC93') {
return
}

addMessageListener((obj) => {
try {
handleMessage(event)
handleMessage(obj)
} catch {}
})

function handleMessage(event: string | { data: string }) {
const obj =
typeof event === 'string' ? { action: event } : JSON.parse(event.data)

function handleMessage(obj: Record<string, any>) {
// eslint-disable-next-line default-case
switch (obj.action) {
case 'building':
Expand Down
11 changes: 5 additions & 6 deletions packages/next/src/client/dev/error-overlay/hot-dev-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,14 @@ let customHmrEventHandler: any
export default function connect() {
register()

addMessageListener((event) => {
addMessageListener((payload) => {
try {
const payload = JSON.parse(event.data)
if (!('action' in payload)) return

processMessage(payload)
if ('action' in payload) {
processMessage(payload)
}
} catch (err: any) {
console.warn(
'[HMR] Invalid message: ' + event.data + '\n' + (err?.stack ?? '')
'[HMR] Invalid message: ' + payload + '\n' + (err?.stack ?? '')
)
}
})
Expand Down
16 changes: 12 additions & 4 deletions packages/next/src/client/dev/error-overlay/websocket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
type WebSocketMessage = Record<string, any>

let source: WebSocket
const eventCallbacks: ((event: any) => void)[] = []
const eventCallbacks: ((msg: WebSocketMessage) => void)[] = []
let lastActivity = Date.now()

function getSocketProtocol(assetPrefix: string): string {
Expand All @@ -13,7 +15,7 @@ function getSocketProtocol(assetPrefix: string): string {
return protocol === 'http:' ? 'ws' : 'wss'
}

export function addMessageListener(cb: (event: any) => void) {
export function addMessageListener(cb: (msg: WebSocketMessage) => void) {
eventCallbacks.push(cb)
}

Expand All @@ -39,11 +41,17 @@ export function connectHMR(options: {
lastActivity = Date.now()
}

function handleMessage(event: any) {
function handleMessage(event: MessageEvent<string>) {
lastActivity = Date.now()

// webpack's heartbeat event.
if (event.data === '\uD83D\uDC93') {
return
}

const msg = JSON.parse(event.data)
eventCallbacks.forEach((cb) => {
cb(event)
cb(msg)
})
}

Expand Down
27 changes: 21 additions & 6 deletions packages/next/src/client/next-dev-turbopack.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// TODO: Remove use of `any` type.
import { initialize, hydrate, version, router, emitter } from './'
import { displayContent } from './dev/fouc'
import { initialize, version, router, emitter } from './'
import initWebpackHMR from './dev/webpack-hot-middleware-client'

import './setup-hydration-warning'
import { pageBootrap } from './page-bootstrap'
import { addMessageListener, sendMessage } from './dev/error-overlay/websocket'
//@ts-expect-error requires "moduleResolution": "node16" in tsconfig.json and not .ts extension
import { connect } from '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client.ts'

window.next = {
version: `${version}-turbo`,
Expand All @@ -17,11 +21,10 @@ window.next = {
// for the page loader
declare let __turbopack_load__: any

const webpackHMR = initWebpackHMR()
initialize({
// TODO the prop name is confusing as related to webpack
webpackHMR: {
onUnrecoverableError() {},
},
webpackHMR,
})
.then(({ assetPrefix }) => {
// for the page loader
Expand Down Expand Up @@ -51,7 +54,19 @@ initialize({
)
}

return hydrate({ beforeRender: displayContent }).then(() => {})
connect({
addMessageListener(cb: (msg: Record<string, string>) => void) {
addMessageListener((msg) => {
// Only call Turbopack's message listener for turbopack messages
if (msg.type?.startsWith('turbopack-')) {
cb(msg)
}
})
},
sendMessage,
})

return pageBootrap(assetPrefix)
})
.catch((err) => {
console.error('Error was not caught', err)
Expand Down
Loading