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

Replace optionalDependencies with script for installing swc #33496

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ test-timings.json

# Cache
*.tsbuildinfo
packages/next/native
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"prettier-fix": "prettier --write .",
"types": "lerna run types --stream",
"typescript": "lerna run typescript",
"prepublish": "lerna run prepublish",
"prepublish": "lerna run prepublish && node packages/next/install-swc.js",
"publish-canary": "git checkout canary && git pull && lerna version prerelease --preid canary --force-publish && release --pre --skip-questions",
"publish-stable": "lerna version --force-publish",
"lint-staged": "lint-staged",
Expand All @@ -40,7 +40,7 @@
"next-no-sourcemaps": "node --trace-deprecation packages/next/dist/bin/next",
"clean-trace-jaeger": "rm -rf test/integration/basic/.next && TRACE_TARGET=JAEGER node --trace-deprecation --enable-source-maps packages/next/dist/bin/next build test/integration/basic",
"debug": "node --inspect packages/next/dist/bin/next",
"postinstall": "git config feature.manyFiles true && node scripts/install-native.mjs"
"postinstall": "git config feature.manyFiles true"
},
"pre-commit": "lint-staged",
"devDependencies": {
Expand Down
7 changes: 7 additions & 0 deletions packages/next/build/swc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ function loadNative() {
} catch (e) {}
}

for (const triple of triples) {
try {
bindings = require(`next/native/next-swc.${triple.platformArchABI}.node`)
break
} catch (e) {}
}

if (!bindings) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this block any more?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we should for most cases although we could leave it in case users want to manually install the package and skip our custom install script πŸ€”

for (const triple of triples) {
let pkg = `@next/swc-${triple.platformArchABI}`
Expand Down
15 changes: 15 additions & 0 deletions packages/next/compiled/tar/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
The ISC License

Copyright (c) Isaac Z. Schlueter and Contributors

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1 change: 1 addition & 0 deletions packages/next/compiled/tar/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/next/compiled/tar/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"tar","main":"index.js","author":"Isaac Z. Schlueter <[email protected]> (http://blog.izs.me/)","license":"ISC"}
256 changes: 256 additions & 0 deletions packages/next/install-swc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
const fs = require('fs')
const os = require('os')
const path = require('path')
const tar = require('next/dist/compiled/tar')

if (process.env.NEXT_SWC_SKIP_INSTALL) {
process.exit(0)
}

const MAX_VERSIONS_TO_CACHE = 5
const { version } = require('next/package.json')

// @ts-ignore https://nodejs.org/api/report.html
const { glibcVersionRuntime } = process.report.getReport().header
const isGlibc = !!glibcVersionRuntime

/**
@type {
[platform: string]: {
[arch: string]: {
// function to check if the arch/platform fully matches
// e.g. check if linux is glibc/musl
check?: () => {}
packageName: string
}[]
}
}
*/
const packageMap = {
win32: {
x64: [
{
packageName: '@next/swc-win32-x64-msvc',
},
],
ia32: [
{
packageName: '@next/swc-win32-ia32-msvc',
},
],
arm64: [
{
packageName: '@next/swc-win32-arm64-msvc',
},
],
},
linux: {
x64: [
{
packageName: '@next/swc-linux-x64-gnu',
check: () => isGlibc,
},
{
packageName: '@next/swc-linux-x64-musl',
check: () => !isGlibc,
},
],
arm64: [
{
packageName: '@next/swc-linux-arm64-gnu',
check: () => isGlibc,
},
{
packageName: '@next/swc-linux-arm64-musl',
check: () => !isGlibc,
},
],
arm: [
{
packageName: '@next/swc-linux-arm-gnueabihf',
},
],
},
darwin: {
x64: [
{
packageName: '@next/swc-darwin-x64',
},
],
arm64: [
{
packageName: '@next/swc-darwin-arm64',
},
],
},
android: {
arm64: [
{
packageName: '@next/swc-android-arm64',
},
],
},
}

let activePackage = packageMap[process.platform]?.[process.arch]?.find(
(item) => {
return typeof item.check === 'undefined' ? true : item.check()
ijjk marked this conversation as resolved.
Show resolved Hide resolved
}
)

if (!activePackage) {
// TODO: should this be a hard error even though it fails install?
// should we fallback to wasm in this case?
Copy link
Member

Choose a reason for hiding this comment

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

Falling back to wasm makes sense to me.

Thats the advantage of this custom install script πŸ‘

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, falling back to wasm sounds good to me.

console.error(
`Error: unsupported next-swc platform: ` +
`${process.platform} ${process.arch} (glibc: ${isGlibc})\n` +
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`${process.platform} ${process.arch} (glibc: ${isGlibc})\n` +
`${process.platform} ${process.arch} (glibcVersionRuntime: ${glibcVersionRuntime})\n` +

`Please report this on the feedback thread here: https://github.com/vercel/next.js/discussions/30468`
)
process.exit(0)
}

const localFileName = `next-swc.${activePackage.packageName.substr(10)}.node`
const tarFileName = `${activePackage.packageName.substr(6)}-${version}.tgz`
const outputDirectory = path.join(__dirname, 'native')

const exists = (filePath) => {
return fs.promises
.access(filePath, fs.constants.F_OK)
.then(() => true)
.catch(() => false)
}
const rmFile = (filePath) => {
return fs.promises.unlink(filePath).catch(() => {})
}

;(async () => {
const outputFilepath = path.join(outputDirectory, localFileName)
const versionFilepath = path.join(outputDirectory, 'version.txt')

// check if native folder already has an extracted copy of swc,
// nothing further is needed if so
if (
(await exists(outputFilepath)) &&
(await fs.promises.readFile(versionFilepath, 'utf8')) === version
) {
return
}

// get platform specific cache directory adapted from playwright's handling
// https://github.com/microsoft/playwright/blob/7d924470d397975a74a19184c136b3573a974e13/packages/playwright-core/src/utils/registry.ts#L141
const cacheDirectory = (() => {
let result
const envDefined = process.env['NEXT_SWC_PATH']

if (envDefined) {
result = envDefined
} else {
let systemCacheDirectory
if (process.platform === 'linux') {
systemCacheDirectory =
process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache')
} else if (process.platform === 'darwin') {
systemCacheDirectory = path.join(os.homedir(), 'Library', 'Caches')
} else if (process.platform === 'win32') {
systemCacheDirectory =
process.env.LOCALAPPDATA ||
path.join(os.homedir(), 'AppData', 'Local')
} else {
console.error(new Error('Unsupported platform: ' + process.platform))
process.exit(0)
}
result = path.join(systemCacheDirectory, 'next-swc')
}

if (!path.isAbsolute(result)) {
// It is important to resolve to the absolute path:
// - for unzipping to work correctly;
// - so that registry directory matches between installation and execution.
// INIT_CWD points to the root of `npm/yarn install` and is probably what
// the user meant when typing the relative path.
result = path.resolve(process.env['INIT_CWD'] || process.cwd(), result)
}
return result
})()

const extractFromTar = async () => {
await fs.promises.mkdir(outputDirectory).catch((err) => {
if (err.code !== 'EEXIST') throw err
})
const tarFilepath = path.join(cacheDirectory, tarFileName)
const readStream = fs.createReadStream(tarFilepath)
const writeStream = fs.createWriteStream(outputFilepath)
let foundEntry = false

await new Promise((resolve, reject) => {
readStream
.pipe(tar.t())
.on('entry', (entry) => {
if (entry.path.endsWith('.node')) {
foundEntry = true
entry
.pipe(writeStream)
.on('error', (err) => reject(err))
.on('finish', () => resolve())
}
})
.on('error', (err) => reject(err))
.on('finish', () => {
if (!foundEntry) {
reject(new Error(`Failed to find entry in ${tarFilepath}`))
}
})
})
await rmFile(versionFilepath)
await fs.promises.writeFile(versionFilepath, version)
}

// check cache first if it exists copy from there
if (await exists(path.join(cacheDirectory, tarFileName))) {
await extractFromTar()
return
}

// download fresh copy and populate cache
const fetch = require('next/dist/compiled/node-fetch')

const tempFile = path.join(
cacheDirectory,
`${tarFileName}.temp-${Date.now()}`
)
await fs.promises.mkdir(cacheDirectory, { recursive: true })

try {
await fetch(
`https://registry.npmjs.org/${activePackage.packageName}/-/${tarFileName}`
Copy link
Contributor

@merceyz merceyz Jan 20, 2022

Choose a reason for hiding this comment

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

Some setups don't have access to the npm registry so this will be problematic
Ref evanw/esbuild#1621

Copy link
Member

@styfle styfle Jan 21, 2022

Choose a reason for hiding this comment

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

Yeah this is problematic because package config like .npmrc or .yarnrc is no longer respected evanw/esbuild#286

Perhaps we need to double down on getting this feature into npm now that yarn just landed it in yarnpkg/berry#3981

Copy link
Member Author

Choose a reason for hiding this comment

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

We can definitely handle loading the registry from the package config. I think it's gonna be a while before this is sorted out/performant in relevant package managers so the custom install script will still be needed for a bit.

).then((res) => {
if (!res.ok) {
throw new Error(`request failed with status ${res.status}`)
}
const cacheWriteStream = fs.createWriteStream(tempFile)

return new Promise((resolve, reject) => {
res.body
.pipe(cacheWriteStream)
.on('error', (err) => reject(err))
.on('finish', () => resolve())
}).finally(() => cacheWriteStream.close())
})
await fs.promises.rename(tempFile, path.join(cacheDirectory, tarFileName))
await extractFromTar()

const cacheFiles = await fs.promises.readdir(cacheDirectory)

if (cacheFiles.length > MAX_VERSIONS_TO_CACHE) {
cacheFiles.sort()

for (let i = MAX_VERSIONS_TO_CACHE - 1; i++; i < cacheFiles.length) {
await rmFile(path.join(cacheDirectory, cacheFiles[i]))
}
}
} catch (err) {
await rmFile(tempFile)
console.error(`Failed to download swc binary from npm`, err)
process.exit(1)
}
})()
4 changes: 3 additions & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"index.d.ts",
"types/index.d.ts",
"types/global.d.ts",
"image-types/global.d.ts"
"image-types/global.d.ts",
"install-swc.js"
],
"bin": {
"next": "./dist/bin/next"
Expand Down Expand Up @@ -253,6 +254,7 @@
"string-hash": "1.1.3",
"string_decoder": "1.3.0",
"strip-ansi": "6.0.0",
"tar": "6.1.11",
"taskr": "1.1.0",
"terser": "5.7.1",
"text-table": "0.2.0",
Expand Down
10 changes: 10 additions & 0 deletions packages/next/taskfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,15 @@ export async function ncc_browserslist(task, opts) {
.target('compiled/browserslist')
}

// eslint-disable-next-line camelcase
externals['tar'] = 'next/dist/compiled/tar'
export async function ncc_tar(task, opts) {
await task
.source(opts.src || relative(__dirname, require.resolve('tar')))
.ncc({ packageName: 'tar', externals })
.target('compiled/tar')
}

// eslint-disable-next-line camelcase
externals['@napi-rs/triples'] = 'next/dist/compiled/@napi-rs/triples'
export async function ncc_napirs_triples(task, opts) {
Expand Down Expand Up @@ -1579,6 +1588,7 @@ export async function ncc(task, opts) {
'ncc_chalk',
'ncc_browserslist',
'ncc_napirs_triples',
'ncc_tar',
'ncc_etag',
'ncc_p_limit',
'ncc_raw_body',
Expand Down
6 changes: 6 additions & 0 deletions packages/next/types/misc.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ declare module 'next/dist/compiled/node-fetch' {
export * from 'node-fetch'
}

declare module 'next/dist/compiled/tar' {
import m from 'tar'
export default m
export * from 'tar'
}

declare module 'next/dist/compiled/jest-worker' {
export * from 'jest-worker'
}
Expand Down
9 changes: 3 additions & 6 deletions scripts/publish-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,12 @@ const cwd = process.cwd()
)
}

// Update optional dependencies versions
// Add postinstall script for downloading correct
// swc binary before publish
let nextPkg = JSON.parse(
await readFile(path.join(cwd, 'packages/next/package.json'))
)
for (let platform of platforms) {
let optionalDependencies = nextPkg.optionalDependencies || {}
optionalDependencies['@next/swc-' + platform] = version
nextPkg.optionalDependencies = optionalDependencies
}
nextPkg.scripts.postinstall = 'node install-swc.js'
await writeFile(
path.join(path.join(cwd, 'packages/next/package.json')),
JSON.stringify(nextPkg, null, 2)
Expand Down
Loading