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

Add event for swc load failure and attempt patching lockfile #36527

Merged
merged 6 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 48 additions & 5 deletions packages/next/build/swc/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { platform, arch } from 'os'
import { platformArchTriples } from 'next/dist/compiled/@napi-rs/triples'
import { version as nextVersion, optionalDependencies } from 'next/package.json'
import * as Log from '../output/log'
import { getParserOptions } from './options'
import { eventSwcLoadFailure } from '../../telemetry/events/swc-load-failure'
import { patchIncorrectLockfile } from '../../lib/patch-incorrect-lockfile'

const ArchName = arch()
const PlatformName = platform()
Expand All @@ -18,6 +21,9 @@ async function loadBindings() {
attempts = attempts.concat(a)
}

// TODO: fetch wasm and fallback when loading native fails
// so that users aren't blocked on this, we still want to
// report the native load failure so we can patch though
try {
let bindings = await loadWasm()
return bindings
Expand All @@ -41,13 +47,50 @@ function loadBindingsSync() {

function logLoadFailure(attempts) {
for (let attempt of attempts) {
Log.info(attempt)
Log.warn(attempt)
}
let glibcVersion
let installedSwcPackages

Log.error(
`Failed to load SWC binary for ${PlatformName}/${ArchName}, see more info here: https://nextjs.org/docs/messages/failed-loading-swc`
)
process.exit(1)
try {
glibcVersion = process.report?.getReport().header.glibcVersionRuntime
} catch (_) {}

try {
const pkgNames = Object.keys(optionalDependencies || {}).filter((pkg) =>
pkg.startsWith('@next/swc')
)
const installedPkgs = []

for (const pkg of pkgNames) {
try {
const { version } = require(`${pkg}/package.json`)
installedPkgs.push(`${pkg}@${version}`)
} catch (_) {}
}

if (installedPkgs.length > 0) {
installedSwcPackages = installedPkgs.sort().join(',')
}
} catch (_) {}

patchIncorrectLockfile(process.cwd())
.then(() => {
return eventSwcLoadFailure({
nextVersion,
glibcVersion,
installedSwcPackages,
arch: process.arch,
platform: process.platform,
nodeVersion: process.versions.node,
})
})
.finally(() => {
Log.error(
`Failed to load SWC binary for ${PlatformName}/${ArchName}, see more info here: https://nextjs.org/docs/messages/failed-loading-swc`
)
process.exit(1)
})
}

async function loadWasm() {
Expand Down
94 changes: 94 additions & 0 deletions packages/next/lib/patch-incorrect-lockfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { promises } from 'fs'
import '../server/node-polyfill-fetch'
import * as Log from '../build/output/log'
import findUp from 'next/dist/compiled/find-up'

/**
* Attempts to patch npm package-lock.json when it
* fails to include optionalDependencies for other platforms
* this can occur when the package-lock is rebuilt from a current
* node_modules install instead of pulling fresh package data
*/
export async function patchIncorrectLockfile(dir: string) {
const lockfilePath = await findUp('package-lock.json', { cwd: dir })

if (!lockfilePath) {
// if no lockfile present there is no action to take
return
}
const content = await promises.readFile(lockfilePath, 'utf8')
const lockfileParsed = JSON.parse(content)

const packageKeys = Object.keys(lockfileParsed.dependencies)
const foundSwcPkgs = new Set()
const nextPkg = lockfileParsed.packages['node_modules/next']

if (!nextPkg) {
return console.error('Failed to locate next in', lockfilePath)
}
const nextVersion = nextPkg.version

const expectedSwcPkgs = Object.keys(nextPkg?.optionalDependencies).filter(
(pkg) => pkg.startsWith('@next/swc-')
)

packageKeys.forEach((pkgKey) => {
const swcIndex = pkgKey.indexOf('@next/swc-')
if (swcIndex > -1) {
foundSwcPkgs.add(pkgKey.substring(swcIndex))
}
})

// if swc package keys are missing manually populate them
// so installs on different platforms can succeed
// user will need to run npm i after to ensure it's corrected
if (foundSwcPkgs.size !== expectedSwcPkgs.length) {
Log.warn(`Found lockfile missing swc dependencies, patching..`)

try {
// populate fields for each missing swc pkg
for (const pkg of expectedSwcPkgs) {
if (!foundSwcPkgs.has(pkg)) {
console.log('fetching', pkg)
const res = await fetch(`https://registry.npmjs.org/${pkg}`)

if (!res.ok) {
throw new Error(
`Failed to fetch registry info for ${pkg}, got status ${res.status}`
)
}
const data = await res.json()
const version = data.versions[nextVersion]

if (!version) {
throw new Error(
`Failed to find matching version for ${pkg} at ${nextVersion}`
)
}
lockfileParsed.packages[`node_modules/${pkg}`] = {
version: nextVersion,
resolved: version.dist.tarball,
integrity: version.dist.integrity,
cpu: version.cpu,
optional: true,
os: version.os,
engines: version.engines,
}
}
}

await promises.writeFile(
lockfilePath,
JSON.stringify(lockfileParsed, null, 2)
)
Log.warn(
'Lockfile was successfully patched, please run "npm install" to ensure @next/swc dependencies are downloaded'
)
} catch (err) {
Log.error(
`Failed to patch lockfile, please try uninstalling and reinstalling next in this workspace`
)
console.error(err)
}
}
}
31 changes: 31 additions & 0 deletions packages/next/telemetry/events/swc-load-failure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { traceGlobals } from '../../trace/shared'
import { Telemetry } from '../storage'

const EVENT_PLUGIN_PRESENT = 'NEXT_SWC_LOAD_FAILURE'
export type EventSwcLoadFailure = {
eventName: string
payload: {
platform: string
arch: string
nodeVersion: string
nextVersion: string
wasm?: string
glibcVersion?: string
installedSwcPackages?: string
}
}

export async function eventSwcLoadFailure(
event: EventSwcLoadFailure['payload']
): Promise<void> {
const telemetry: Telemetry = traceGlobals.get('telemetry')
// can't continue if telemetry isn't set
if (!telemetry) return

telemetry.record({
eventName: EVENT_PLUGIN_PRESENT,
payload: event,
})
// ensure this event is flushed before process exits
await telemetry.flush()
}
19 changes: 19 additions & 0 deletions test/integration/telemetry/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,25 @@ describe('Telemetry CLI', () => {
expect(stderr2).toMatch(/isSrcDir.*?true/)
})

it('emits event when swc fails to load', async () => {
await fs.remove(path.join(appDir, '.next'))
const { stderr } = await runNextCommand(['build', appDir], {
stderr: true,
env: {
// block swc from loading
NODE_OPTIONS: '--no-addons',
NEXT_TELEMETRY_DEBUG: 1,
},
})
expect(stderr).toMatch(/NEXT_SWC_LOAD_FAILURE/)
expect(stderr).toContain(
`"nextVersion": "${require('next/package.json').version}"`
)
expect(stderr).toContain(`"arch": "${process.arch}"`)
expect(stderr).toContain(`"platform": "${process.platform}"`)
expect(stderr).toContain(`"nodeVersion": "${process.versions.node}"`)
})

it('logs completed `next build` with warnings', async () => {
await fs.rename(
path.join(appDir, 'pages', 'warning.skip'),
Expand Down
30 changes: 21 additions & 9 deletions test/lib/create-next-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const { linkPackages } =
async function createNextInstall(
dependencies,
installCommand,
packageJson = {}
packageJson = {},
packageLockPath = ''
) {
const tmpDir = await fs.realpath(process.env.NEXT_TEST_DIR || os.tmpdir())
const origRepoDir = path.join(__dirname, '../../')
Expand Down Expand Up @@ -49,14 +50,18 @@ async function createNextInstall(
})
}

const pkgPaths = await linkPackages(tmpRepoDir)
const combinedDependencies = {
...Object.keys(dependencies).reduce((prev, pkg) => {
const pkgPath = pkgPaths.get(pkg)
prev[pkg] = pkgPath || dependencies[pkg]
return prev
}, {}),
next: pkgPaths.get('next'),
let combinedDependencies = dependencies

if (!(packageJson && packageJson.nextPrivateSkipLocalDeps)) {
const pkgPaths = await linkPackages(tmpRepoDir)
combinedDependencies = {
...Object.keys(dependencies).reduce((prev, pkg) => {
const pkgPath = pkgPaths.get(pkg)
prev[pkg] = pkgPath || dependencies[pkg]
return prev
}, {}),
next: pkgPaths.get('next'),
}
}

await fs.ensureDir(installDir)
Expand All @@ -73,6 +78,13 @@ async function createNextInstall(
)
)

if (packageLockPath) {
await fs.copy(
packageLockPath,
path.join(installDir, path.basename(packageLockPath))
)
}

if (installCommand) {
const installString =
typeof installCommand === 'function'
Expand Down
3 changes: 2 additions & 1 deletion test/lib/e2e-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export async function createNext(opts: {
buildCommand?: string
packageJson?: PackageJson
startCommand?: string
packageLockPath?: string
}): Promise<NextInstance> {
try {
if (nextInstance) {
Expand Down Expand Up @@ -149,7 +150,7 @@ export async function createNext(opts: {
} catch (err) {
require('console').error('Failed to create next instance', err)
try {
await nextInstance.destroy()
nextInstance.destroy()
} catch (_) {}
process.exit(1)
}
Expand Down
7 changes: 6 additions & 1 deletion test/lib/next-modes/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class NextInstance {
protected _url: string
protected _parsedUrl: URL
protected packageJson: PackageJson
protected packageLockPath?: string
protected basePath?: string

constructor({
Expand All @@ -42,6 +43,7 @@ export class NextInstance {
buildCommand,
startCommand,
packageJson = {},
packageLockPath,
}: {
files: {
[filename: string]: string | FileRef
Expand All @@ -50,6 +52,7 @@ export class NextInstance {
[name: string]: string
}
packageJson?: PackageJson
packageLockPath?: string
nextConfig?: NextConfig
installCommand?: InstallCommand
buildCommand?: string
Expand All @@ -62,6 +65,7 @@ export class NextInstance {
this.buildCommand = buildCommand
this.startCommand = startCommand
this.packageJson = packageJson
this.packageLockPath = packageLockPath
this.events = {}
this.isDestroyed = false
this.isStopping = false
Expand Down Expand Up @@ -131,7 +135,8 @@ export class NextInstance {
this.testDir = await createNextInstall(
finalDependencies,
this.installCommand,
this.packageJson
this.packageJson,
this.packageLockPath
)
}
console.log('created next.js install, writing test files')
Expand Down