diff --git a/packages/font/src/google/font-data.json b/packages/font/src/google/font-data.json index 02d12e223181..bfa950f0f704 100644 --- a/packages/font/src/google/font-data.json +++ b/packages/font/src/google/font-data.json @@ -4,6 +4,11 @@ "styles": ["normal", "italic"], "subsets": ["latin", "latin-ext"] }, + "ADLaM Display": { + "weights": ["400"], + "styles": ["normal"], + "subsets": ["adlam", "latin", "latin-ext"] + }, "Abel": { "weights": ["400"], "styles": ["normal"], @@ -7769,9 +7774,28 @@ "subsets": ["gurmukhi", "latin", "latin-ext"] }, "Noto Sans HK": { - "weights": ["100", "300", "400", "500", "700", "900"], + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], "styles": ["normal"], - "subsets": ["latin"] + "axes": [ + { + "tag": "wght", + "min": 100, + "max": 900, + "defaultValue": 400 + } + ], + "subsets": ["cyrillic", "latin", "latin-ext", "vietnamese"] }, "Noto Sans Hanifi Rohingya": { "weights": ["400", "500", "600", "700", "variable"], @@ -7834,12 +7858,12 @@ "Noto Sans Indic Siyaq Numbers": { "weights": ["400"], "styles": ["normal"], - "subsets": ["indic-siyaq-numbers"] + "subsets": ["indic-siyaq-numbers", "latin", "latin-ext"] }, "Noto Sans Inscriptional Pahlavi": { "weights": ["400"], "styles": ["normal"], - "subsets": ["inscriptional-pahlavi"] + "subsets": ["inscriptional-pahlavi", "latin", "latin-ext"] }, "Noto Sans Inscriptional Parthian": { "weights": ["400"], @@ -7884,9 +7908,28 @@ "subsets": ["javanese", "latin", "latin-ext"] }, "Noto Sans KR": { - "weights": ["100", "300", "400", "500", "700", "900"], + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], "styles": ["normal"], - "subsets": ["latin"] + "axes": [ + { + "tag": "wght", + "min": 100, + "max": 900, + "defaultValue": 400 + } + ], + "subsets": ["cyrillic", "latin", "latin-ext", "vietnamese"] }, "Noto Sans Kaithi": { "weights": ["400"], @@ -8147,7 +8190,7 @@ "Noto Sans Mayan Numerals": { "weights": ["400"], "styles": ["normal"], - "subsets": ["mayan-numerals"] + "subsets": ["latin", "latin-ext", "mayan-numerals"] }, "Noto Sans Medefaidrin": { "weights": ["400", "500", "600", "700", "variable"], @@ -8454,9 +8497,28 @@ "subsets": ["latin", "latin-ext", "runic"] }, "Noto Sans SC": { - "weights": ["100", "300", "400", "500", "700", "900"], + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], "styles": ["normal"], - "subsets": ["latin"] + "axes": [ + { + "tag": "wght", + "min": 100, + "max": 900, + "defaultValue": 400 + } + ], + "subsets": ["cyrillic", "latin", "latin-ext", "vietnamese"] }, "Noto Sans Samaritan": { "weights": ["400"], @@ -8637,9 +8699,28 @@ "subsets": ["latin", "latin-ext", "syriac"] }, "Noto Sans TC": { - "weights": ["100", "300", "400", "500", "700", "900"], + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], "styles": ["normal"], - "subsets": ["latin"] + "axes": [ + { + "tag": "wght", + "min": 100, + "max": 900, + "defaultValue": 400 + } + ], + "subsets": ["cyrillic", "latin", "latin-ext", "vietnamese"] }, "Noto Sans Tagalog": { "weights": ["400"], @@ -12478,8 +12559,16 @@ "subsets": ["latin", "latin-ext", "thai", "vietnamese"] }, "Teko": { - "weights": ["300", "400", "500", "600", "700"], + "weights": ["300", "400", "500", "600", "700", "variable"], "styles": ["normal"], + "axes": [ + { + "tag": "wght", + "min": 300, + "max": 700, + "defaultValue": 400 + } + ], "subsets": ["devanagari", "latin", "latin-ext"] }, "Tektur": { @@ -13189,6 +13278,42 @@ "styles": ["normal"], "subsets": ["latin", "latin-ext", "vietnamese"] }, + "Wavefont": { + "weights": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", + "variable" + ], + "styles": ["normal"], + "axes": [ + { + "tag": "ROND", + "min": 0, + "max": 100, + "defaultValue": 100 + }, + { + "tag": "YELA", + "min": -100, + "max": 100, + "defaultValue": -100 + }, + { + "tag": "wght", + "min": 100, + "max": 900, + "defaultValue": 400 + } + ], + "subsets": ["latin", "latin-ext"] + }, "Wellfleet": { "weights": ["400"], "styles": ["normal"], diff --git a/packages/font/src/google/index.ts b/packages/font/src/google/index.ts index 1cb4e5276023..2bd917b6e734 100644 --- a/packages/font/src/google/index.ts +++ b/packages/font/src/google/index.ts @@ -19,6 +19,18 @@ export declare function ABeeZee< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function ADLaM_Display< + T extends CssVariable | undefined = undefined +>(options: { + weight: '400' | Array<'400'> + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'adlam' | 'latin' | 'latin-ext'> +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Abel< T extends CssVariable | undefined = undefined >(options: { @@ -13928,22 +13940,28 @@ export declare function Noto_Sans_Gurmukhi< }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_HK< T extends CssVariable | undefined = undefined ->(options: { - weight: +>(options?: { + weight?: | '100' + | '200' | '300' | '400' | '500' + | '600' | '700' + | '800' | '900' - | Array<'100' | '300' | '400' | '500' | '700' | '900'> + | 'variable' + | Array< + '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' + > style?: 'normal' | Array<'normal'> display?: Display variable?: T preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'latin'> + subsets?: Array<'cyrillic' | 'latin' | 'latin-ext' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Hanifi_Rohingya< T extends CssVariable | undefined = undefined @@ -14035,7 +14053,7 @@ export declare function Noto_Sans_Indic_Siyaq_Numbers< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'indic-siyaq-numbers'> + subsets?: Array<'indic-siyaq-numbers' | 'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Inscriptional_Pahlavi< T extends CssVariable | undefined = undefined @@ -14047,7 +14065,7 @@ export declare function Noto_Sans_Inscriptional_Pahlavi< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'inscriptional-pahlavi'> + subsets?: Array<'inscriptional-pahlavi' | 'latin' | 'latin-ext'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Inscriptional_Parthian< T extends CssVariable | undefined = undefined @@ -14106,22 +14124,28 @@ export declare function Noto_Sans_Javanese< }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_KR< T extends CssVariable | undefined = undefined ->(options: { - weight: +>(options?: { + weight?: | '100' + | '200' | '300' | '400' | '500' + | '600' | '700' + | '800' | '900' - | Array<'100' | '300' | '400' | '500' | '700' | '900'> + | 'variable' + | Array< + '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' + > style?: 'normal' | Array<'normal'> display?: Display variable?: T preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'latin'> + subsets?: Array<'cyrillic' | 'latin' | 'latin-ext' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Kaithi< T extends CssVariable | undefined = undefined @@ -14491,7 +14515,7 @@ export declare function Noto_Sans_Mayan_Numerals< preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'mayan-numerals'> + subsets?: Array<'latin' | 'latin-ext' | 'mayan-numerals'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Medefaidrin< T extends CssVariable | undefined = undefined @@ -15048,22 +15072,28 @@ export declare function Noto_Sans_Runic< }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_SC< T extends CssVariable | undefined = undefined ->(options: { - weight: +>(options?: { + weight?: | '100' + | '200' | '300' | '400' | '500' + | '600' | '700' + | '800' | '900' - | Array<'100' | '300' | '400' | '500' | '700' | '900'> + | 'variable' + | Array< + '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' + > style?: 'normal' | Array<'normal'> display?: Display variable?: T preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'latin'> + subsets?: Array<'cyrillic' | 'latin' | 'latin-ext' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Samaritan< T extends CssVariable | undefined = undefined @@ -15324,22 +15354,28 @@ export declare function Noto_Sans_Syriac_Eastern< }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_TC< T extends CssVariable | undefined = undefined ->(options: { - weight: +>(options?: { + weight?: | '100' + | '200' | '300' | '400' | '500' + | '600' | '700' + | '800' | '900' - | Array<'100' | '300' | '400' | '500' | '700' | '900'> + | 'variable' + | Array< + '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' + > style?: 'normal' | Array<'normal'> display?: Display variable?: T preload?: boolean fallback?: string[] adjustFontFallback?: boolean - subsets?: Array<'latin'> + subsets?: Array<'cyrillic' | 'latin' | 'latin-ext' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable export declare function Noto_Sans_Tagalog< T extends CssVariable | undefined = undefined @@ -21783,13 +21819,14 @@ export declare function Taviraj< }): T extends undefined ? NextFont : NextFontWithVariable export declare function Teko< T extends CssVariable | undefined = undefined ->(options: { - weight: +>(options?: { + weight?: | '300' | '400' | '500' | '600' | '700' + | 'variable' | Array<'300' | '400' | '500' | '600' | '700'> style?: 'normal' | Array<'normal'> display?: Display @@ -22997,6 +23034,32 @@ export declare function Waterfall< adjustFontFallback?: boolean subsets?: Array<'latin' | 'latin-ext' | 'vietnamese'> }): T extends undefined ? NextFont : NextFontWithVariable +export declare function Wavefont< + T extends CssVariable | undefined = undefined +>(options?: { + weight?: + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900' + | 'variable' + | Array< + '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' + > + style?: 'normal' | Array<'normal'> + display?: Display + variable?: T + preload?: boolean + fallback?: string[] + adjustFontFallback?: boolean + subsets?: Array<'latin' | 'latin-ext'> + axes?: ('ROND' | 'YELA')[] +}): T extends undefined ? NextFont : NextFontWithVariable export declare function Wellfleet< T extends CssVariable | undefined = undefined >(options: { diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts index 36c1b826d6f1..a2bd730c9b50 100644 --- a/packages/next/src/cli/next-dev.ts +++ b/packages/next/src/cli/next-dev.ts @@ -25,6 +25,7 @@ import { getValidatedArgs } from '../lib/get-validated-args' import { Worker } from 'next/dist/compiled/jest-worker' import type { ChildProcess } from 'child_process' import { checkIsNodeDebugging } from '../server/lib/is-node-debugging' +import { createSelfSignedCertificate } from '../lib/mkcert' import uploadTrace from '../trace/upload-trace' let dir: string @@ -201,6 +202,9 @@ const nextDev: CliCommand = async (argv) => { '--hostname': String, '--turbo': Boolean, '--experimental-turbo': Boolean, + '--experimental-https': Boolean, + '--experimental-https-key': String, + '--experimental-https-cert': String, '--experimental-test-proxy': Boolean, '--experimental-upload-trace': String, @@ -391,7 +395,33 @@ const nextDev: CliCommand = async (argv) => { const runDevServer = async (reboot: boolean) => { try { const workerInit = await createRouterWorker() - await workerInit.worker.startServer(devServerOptions) + if (!!args['--experimental-https']) { + Log.warn( + 'Self-signed certificates are currently an experimental feature, use at your own risk.' + ) + + let certificate: { key: string; cert: string } | undefined + + if ( + args['--experimental-https-key'] && + args['--experimental-https-cert'] + ) { + certificate = { + key: path.resolve(args['--experimental-https-key']), + cert: path.resolve(args['--experimental-https-cert']), + } + } else { + certificate = await createSelfSignedCertificate(host) + } + + await workerInit.worker.startServer({ + ...devServerOptions, + selfSignedCertificate: certificate, + }) + } else { + await workerInit.worker.startServer(devServerOptions) + } + await preflight(reboot) return { cleanup: workerInit.cleanup, diff --git a/packages/next/src/lib/download-swc.ts b/packages/next/src/lib/download-swc.ts index ff148dcb0393..2a7565302727 100644 --- a/packages/next/src/lib/download-swc.ts +++ b/packages/next/src/lib/download-swc.ts @@ -1,4 +1,3 @@ -import os from 'os' import fs from 'fs' import path from 'path' import * as Log from '../build/output/log' @@ -11,66 +10,19 @@ const { WritableStream } = require('node:stream/web') as { } import { fileExists } from './file-exists' import { getRegistry } from './helpers/get-registry' +import { getCacheDirectory } from './helpers/get-cache-directory' const MAX_VERSIONS_TO_CACHE = 8 -// 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 -async function getCacheDirectory() { - 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 { - /// Attempt to use generic tmp location for un-handled platform - if (!systemCacheDirectory) { - for (const dir of [ - path.join(os.homedir(), '.cache'), - path.join(os.tmpdir()), - ]) { - if (await fileExists(dir)) { - systemCacheDirectory = dir - break - } - } - } - - if (!systemCacheDirectory) { - 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 -} - async function extractBinary( outputDirectory: string, pkgName: string, tarFileName: string ) { - const cacheDirectory = await getCacheDirectory() + const cacheDirectory = await getCacheDirectory( + 'next-swc', + process.env['NEXT_SWC_PATH'] + ) const extractFromTar = async () => { await tar.x({ diff --git a/packages/next/src/lib/helpers/get-cache-directory.ts b/packages/next/src/lib/helpers/get-cache-directory.ts new file mode 100644 index 000000000000..fc15bb1a70dc --- /dev/null +++ b/packages/next/src/lib/helpers/get-cache-directory.ts @@ -0,0 +1,56 @@ +import os from 'os' +import path from 'path' +import { fileExists } from '../file-exists' + +// 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 +export async function getCacheDirectory( + fileDirectory: string, + envPath?: string +) { + let result + + if (envPath) { + result = envPath + } 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 { + /// Attempt to use generic tmp location for un-handled platform + if (!systemCacheDirectory) { + for (const dir of [ + path.join(os.homedir(), '.cache'), + path.join(os.tmpdir()), + ]) { + if (await fileExists(dir)) { + systemCacheDirectory = dir + break + } + } + } + + if (!systemCacheDirectory) { + console.error(new Error('Unsupported platform: ' + process.platform)) + process.exit(0) + } + } + result = path.join(systemCacheDirectory, fileDirectory) + } + + 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 +} diff --git a/packages/next/src/lib/mkcert.ts b/packages/next/src/lib/mkcert.ts new file mode 100644 index 000000000000..53f6b34fa3b8 --- /dev/null +++ b/packages/next/src/lib/mkcert.ts @@ -0,0 +1,131 @@ +import fs from 'fs' +import path from 'path' +import { getCacheDirectory } from './helpers/get-cache-directory' +import * as Log from '../build/output/log' +import { execSync } from 'child_process' + +const { fetch } = require('next/dist/compiled/undici') as { + fetch: typeof global.fetch +} + +const MKCERT_VERSION = 'v1.4.4' + +function getBinaryName() { + const platform = process.platform + const arch = process.arch === 'x64' ? 'amd64' : process.arch + + if (platform === 'win32') { + return `mkcert-${MKCERT_VERSION}-windows-${arch}.exe` + } + if (platform === 'darwin') { + return `mkcert-${MKCERT_VERSION}-darwin-${arch}` + } + if (platform === 'linux') { + return `mkcert-${MKCERT_VERSION}-linux-${arch}` + } + + throw new Error(`Unsupported platform: ${platform}`) +} + +async function downloadBinary() { + try { + const binaryName = getBinaryName() + const cacheDirectory = await getCacheDirectory('mkcert') + const binaryPath = path.join(cacheDirectory, binaryName) + + if (fs.existsSync(binaryPath)) { + return binaryPath + } + + const downloadUrl = `https://github.com/FiloSottile/mkcert/releases/download/${MKCERT_VERSION}/${binaryName}` + + await fs.promises.mkdir(cacheDirectory, { recursive: true }) + + Log.info(`Downloading mkcert package...`) + + const response = await fetch(downloadUrl) + + if (!response.ok || !response.body) { + throw new Error(`request failed with status ${response.status}`) + } + + Log.info(`Download response was successful, writing to disk`) + + const arrayBuffer = await response.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + + await fs.promises.writeFile(binaryPath, buffer) + await fs.promises.chmod(binaryPath, 0o755) + + return binaryPath + } catch (err) { + Log.error('Error downloading mkcert:', err) + } +} + +export async function createSelfSignedCertificate( + host?: string, + certDir: string = 'certificates' +) { + try { + const binaryPath = await downloadBinary() + if (!binaryPath) throw new Error('missing mkcert binary') + + const resolvedCertDir = path.resolve(process.cwd(), `./${certDir}`) + + await fs.promises.mkdir(resolvedCertDir, { + recursive: true, + }) + + const keyPath = path.resolve(resolvedCertDir, 'localhost-key.pem') + const certPath = path.resolve(resolvedCertDir, 'localhost.pem') + + Log.info( + 'Attempting to generate self signed certificate. This may prompt for your password' + ) + + const defaultHosts = ['localhost', '127.0.0.1', '::1'] + + const hosts = + host && !defaultHosts.includes(host) + ? [...defaultHosts, host] + : defaultHosts + + execSync( + `${binaryPath} -install -key-file ${keyPath} -cert-file ${certPath} ${hosts.join( + ' ' + )}`, + { stdio: 'ignore' } + ) + + const caLocation = execSync(`${binaryPath} -CAROOT`).toString() + + if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) { + throw new Error('Certificate files not found') + } + + Log.info(`CA Root certificate created in ${caLocation}`) + Log.info(`Certificates created in ${resolvedCertDir}`) + + const gitignorePath = path.resolve(process.cwd(), './.gitignore') + + if (fs.existsSync(gitignorePath)) { + const gitignore = await fs.promises.readFile(gitignorePath, 'utf8') + if (!gitignore.includes(certDir)) { + Log.info('Adding certificates to .gitignore') + + await fs.promises.appendFile(gitignorePath, `\n${certDir}`) + } + } + + return { + key: keyPath, + cert: certPath, + } + } catch (err) { + Log.error( + 'Failed to generate self-signed certificate. Falling back to http.', + err + ) + } +} diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts index 2bc3aa28b66e..9a24ef00a668 100644 --- a/packages/next/src/server/lib/start-server.ts +++ b/packages/next/src/server/lib/start-server.ts @@ -3,11 +3,13 @@ import '../node-polyfill-fetch' import type { IncomingMessage, ServerResponse } from 'http' import http from 'http' +import https from 'https' import * as Log from '../../build/output/log' import setupDebug from 'next/dist/compiled/debug' import { getDebugPort } from './utils' import { formatHostname } from './format-hostname' import { initialize } from './router-server' +import fs from 'fs' import { WorkerRequestHandler, WorkerUpgradeHandler, @@ -25,6 +27,11 @@ export interface StartServerOptions { customServer?: boolean minimalMode?: boolean keepAliveTimeout?: number + // this is dev-server only + selfSignedCertificate?: { + key: string + cert: string + } isExperimentalTestProxy?: boolean } @@ -70,6 +77,7 @@ export async function startServer({ keepAliveTimeout, isExperimentalTestProxy, logReady = true, + selfSignedCertificate, }: StartServerOptions): Promise { let handlersReady = () => {} let handlersError = () => {} @@ -103,7 +111,13 @@ export async function startServer({ } // setup server listener as fast as possible - const server = http.createServer(async (req, res) => { + if (selfSignedCertificate && !isDev) { + throw new Error( + 'Using a self signed certificate is only supported with `next dev`.' + ) + } + + async function requestListener(req: IncomingMessage, res: ServerResponse) { try { if (handlersPromise) { await handlersPromise @@ -116,7 +130,17 @@ export async function startServer({ Log.error(`Failed to handle request for ${req.url}`) console.error(err) } - }) + } + + const server = selfSignedCertificate + ? https.createServer( + { + key: fs.readFileSync(selfSignedCertificate.key), + cert: fs.readFileSync(selfSignedCertificate.cert), + }, + requestListener + ) + : http.createServer(requestListener) if (keepAliveTimeout) { server.keepAliveTimeout = keepAliveTimeout @@ -171,7 +195,9 @@ export async function startServer({ : actualHostname port = typeof addr === 'object' ? addr?.port || port : port - const appUrl = `http://${formattedHostname}:${port}` + const appUrl = `${ + selfSignedCertificate ? 'https' : 'http' + }://${formattedHostname}:${port}` if (isNodeDebugging) { const debugPort = getDebugPort() diff --git a/test/development/experimental-https-server/app/1/page.js b/test/development/experimental-https-server/app/1/page.js new file mode 100644 index 000000000000..db022cd6865d --- /dev/null +++ b/test/development/experimental-https-server/app/1/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return
Hello from App
+} diff --git a/test/development/experimental-https-server/app/layout.js b/test/development/experimental-https-server/app/layout.js new file mode 100644 index 000000000000..8525f5f8c0b2 --- /dev/null +++ b/test/development/experimental-https-server/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/development/experimental-https-server/certificates/localhost-key.pem b/test/development/experimental-https-server/certificates/localhost-key.pem new file mode 100644 index 000000000000..2306990ac62c --- /dev/null +++ b/test/development/experimental-https-server/certificates/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC2skb3RwQBKtJL +kLfxmPMxAZG4o6R4m9ipcMJtpB6im6di7g+fZzRP+j0AtOnnIaM6ZuT61nCTZKd7 +RB/wlrhgEBkIqEGJtJh93Z0An3J2tt7YnBKsJNBP5BqqLD2TDyFGgTjfKdXJObSr +t67IiRtrKdT6VgRtM2gTM+d1lvL5IVc9JiTOeK9QjzVmDI2MIprzkFrQPI7G+UDq +P80IlrRX2mj30g+rIg6ma6GcnG0Wrk0csaghuAQSiLUqoGU3TzG7QK95sMS48xrv +0tpIyIWlWTTr17ntOIWA1cAGKUQadeVc2No/VRJbMxIcjmtDUc1e66c46z/KTgHo +2vSzjWVNAgMBAAECggEACuIT2Cci1e73GAlG691wnzq4s4cMBSNDhNRywJVGPemH +zxzfUV+Ufi8p8yDTzjDyyEfY3BhqHF2inHUyceKImTBcTWe4f7uCWf0ZnS/iYbAD +FmQ1uIt43Ul5TSnVgS0ljk2kVaboVVRaruACSW/hckDLrx3wpZCqYnp1D0wurSh1 +hQJsb2Opy5wve+hEUGr8AYlolB2SyQLtdWkwyNTmtDeb/cIM/NKXuWDECBrgEN8y +v6fluRe4YkLqzTOJvMr4xNtA9wshO8qcNWy6T5YaiNRJJmzBq2V3d41UuYRcih2K +jSRfn9xZvGPWZ92+Jce15pZY5+ec9A1PuT6J8304AQKBgQDbyWoPZxE6yEXDWz4+ +eFEvPGlVR8KiZhJl78iC1b9gW+FNlmd/LlamRB21IGfpoJgoRyjFP6UJ/tqGx79x +c+7krcBFCOfckYhRGCLxFdK2O0FTQ8ltfqaSYG4jeTSPVDaxhMYh/H3xjls9i7Y7 +90sWDjE/uSFuT+ExmNZvvVtjdQKBgQDUzGToK5gqFpVO43pcJbM6odBTVbZSxDMF +cq+KEOT/c7CzKOOQj7UTqiQgXM0odiJ/I4ZvaIlv+s7l/Cr9LyII3gZUQdPnszsM +/N8kMeJC5Oy8O5pboo+p4SOosRTVZhxGB1tTQxqyECZV5Y5ZCH6bGV4cEqO6rjYC +Pkunc6B3eQKBgCLmtg/iFwtVmDZwe87hvkqY9kUTkyXEvbEwRY/5L1222W0/sAmz +KxFWCb2kervPw7nJqwC/nY6byMnUWGNEvK/Vo42S33bYKWRvR8Uu6PoFKNd3ETpw +/TSLWZIKgj0sa07/PZNSDBHawERitjqJh4PmFw3+cP+acbE1iv/NewCtAoGAWOzt +QiRtmzECxgvDp1xN0LOsNhb8cQvyclVhy+WRfLrg3Y25w0B6oDQakreVOFJdyhmT +ZV0fCf+alHtTj6gxpdj6dh1oK0w34g6ORTbfYar+zw5tS9vcA1bFKwqNNTxNlmoe +nOXO8xhSnNSoLsag+bmZHUwgxbNleHyF6v0j0qkCgYAklO2RDc2RDccXYAe6MbT7 +EYExvS+K09CF4PVPoFk1QZxENuXfewDli9UCwt+uJvVacJOnUyLjMkkooibTTk+A +xf/dAp5ECrTHny/cMSerFJEgVZYsH06m+0a+RP+zL45skm/EsidOvIS80OkRr+Vz +rgIUqg2F14h2H2vbBlqrqQ== +-----END PRIVATE KEY----- diff --git a/test/development/experimental-https-server/certificates/localhost.pem b/test/development/experimental-https-server/certificates/localhost.pem new file mode 100644 index 000000000000..3d68b4263530 --- /dev/null +++ b/test/development/experimental-https-server/certificates/localhost.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKDCCApCgAwIBAgIRAOCKMC740uI5i7wASQ65kkgwDQYJKoZIhvcNAQELBQAw +bTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSEwHwYDVQQLDBhqakBz +bGF0ZS5sYW4gKEpKIEthc3BlcikxKDAmBgNVBAMMH21rY2VydCBqakBzbGF0ZS5s +YW4gKEpKIEthc3BlcikwHhcNMjMwMTI2MDQyMjA2WhcNMjUwNDI2MDMyMjA2WjBM +MScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxITAfBgNV +BAsMGGpqQHNsYXRlLmxhbiAoSkogS2FzcGVyKTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALayRvdHBAEq0kuQt/GY8zEBkbijpHib2Klwwm2kHqKbp2Lu +D59nNE/6PQC06echozpm5PrWcJNkp3tEH/CWuGAQGQioQYm0mH3dnQCfcna23tic +Eqwk0E/kGqosPZMPIUaBON8p1ck5tKu3rsiJG2sp1PpWBG0zaBMz53WW8vkhVz0m +JM54r1CPNWYMjYwimvOQWtA8jsb5QOo/zQiWtFfaaPfSD6siDqZroZycbRauTRyx +qCG4BBKItSqgZTdPMbtAr3mwxLjzGu/S2kjIhaVZNOvXue04hYDVwAYpRBp15VzY +2j9VElszEhyOa0NRzV7rpzjrP8pOAeja9LONZU0CAwEAAaNkMGIwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFOCz+XaB1lpu +T6P2UIfjbq7pUX9GMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG +9w0BAQsFAAOCAYEAaIPJdegZqcKys25yF5MYV10OyZ4wpP4gkONaA5oLfE/SeasH +cs1Af85oeGh6fCuszAoDCJHYvSsUqGhg/243Dkl993vGeLVikvC4R/LC/IkiwNKd +91byKnQBDghzeynyJiHO9yEiY4VFxUTThbttfDstDePOxHYxt8bLbIpGNZb0fdeh +opDwD9hhfL90A7sN7MdakBfQajpquyQgWvKGedaQx363WVXkssi4xRAIdm77ZzqA +Cy4WVrnRtvSbJNqYtNT0Ibx3gF7WC+ACh13lnniDjQlfcDJWxOVjYyvPx0k4M0KQ ++Qz/sx4RXy5jI9OO/hxJNNc3HAxU/7fLtnO/VtEb/c9A1m5gPUFU0W/EHL5VPJJ6 +A6+/T8wK8hDEY4j+bMrysbOeTrbTyLmFXMGFkkSI7OjX0RHkgYBJclgBZfQYGrh1 +PDmdZ26GSgt39k6VCY6ur7dXQuMPvQmRM6IqiPQpmd9SYP+FEiJh5TAOUBinI/Cu +hvseiJ2JQx+4YqxB +-----END CERTIFICATE----- diff --git a/test/development/experimental-https-server/https-server.generated-key.test.ts b/test/development/experimental-https-server/https-server.generated-key.test.ts new file mode 100644 index 000000000000..0dc713da155c --- /dev/null +++ b/test/development/experimental-https-server/https-server.generated-key.test.ts @@ -0,0 +1,35 @@ +import { createNextDescribe } from 'e2e-utils' +import https from 'https' +import { renderViaHTTP } from 'next-test-utils' + +createNextDescribe( + 'experimental-https-server (generated certificate)', + { + files: __dirname, + startCommand: 'yarn next dev --experimental-https', + skipStart: !process.env.CI, + }, + ({ next }) => { + if (!process.env.CI) { + console.warn('only runs on CI as it requires administrator privileges') + it('only runs on CI as it requires administrator privileges', () => {}) + return + } + + const agent = new https.Agent({ + rejectUnauthorized: false, + }) + + it('should successfully load the app in app dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/1', undefined, { agent }) + expect(html).toContain('Hello from App') + }) + + it('should successfully load the app in pages dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/2', undefined, { agent }) + expect(html).toContain('Hello from Pages') + }) + } +) diff --git a/test/development/experimental-https-server/https-server.provided-key.test.ts b/test/development/experimental-https-server/https-server.provided-key.test.ts new file mode 100644 index 000000000000..5f2d878e0ae3 --- /dev/null +++ b/test/development/experimental-https-server/https-server.provided-key.test.ts @@ -0,0 +1,29 @@ +import { createNextDescribe } from 'e2e-utils' +import https from 'https' +import { renderViaHTTP } from 'next-test-utils' + +createNextDescribe( + 'experimental-https-server (provided certificate)', + { + files: __dirname, + startCommand: + 'yarn next dev --experimental-https --experimental-https-key ./certificates/localhost-key.pem --experimental-https-cert ./certificates/localhost.pem', + }, + ({ next }) => { + const agent = new https.Agent({ + rejectUnauthorized: false, + }) + + it('should successfully load the app in app dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/1', undefined, { agent }) + expect(html).toContain('Hello from App') + }) + + it('should successfully load the app in pages dir', async () => { + expect(next.url).toInclude('https://') + const html = await renderViaHTTP(next.url, '/2', undefined, { agent }) + expect(html).toContain('Hello from Pages') + }) + } +) diff --git a/test/development/experimental-https-server/pages/2.js b/test/development/experimental-https-server/pages/2.js new file mode 100644 index 000000000000..ca29f3f4dc89 --- /dev/null +++ b/test/development/experimental-https-server/pages/2.js @@ -0,0 +1,3 @@ +export default function Page() { + return
Hello from Pages
+} diff --git a/test/integration/cli/certificates/localhost-key.pem b/test/integration/cli/certificates/localhost-key.pem new file mode 100644 index 000000000000..2306990ac62c --- /dev/null +++ b/test/integration/cli/certificates/localhost-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC2skb3RwQBKtJL +kLfxmPMxAZG4o6R4m9ipcMJtpB6im6di7g+fZzRP+j0AtOnnIaM6ZuT61nCTZKd7 +RB/wlrhgEBkIqEGJtJh93Z0An3J2tt7YnBKsJNBP5BqqLD2TDyFGgTjfKdXJObSr +t67IiRtrKdT6VgRtM2gTM+d1lvL5IVc9JiTOeK9QjzVmDI2MIprzkFrQPI7G+UDq +P80IlrRX2mj30g+rIg6ma6GcnG0Wrk0csaghuAQSiLUqoGU3TzG7QK95sMS48xrv +0tpIyIWlWTTr17ntOIWA1cAGKUQadeVc2No/VRJbMxIcjmtDUc1e66c46z/KTgHo +2vSzjWVNAgMBAAECggEACuIT2Cci1e73GAlG691wnzq4s4cMBSNDhNRywJVGPemH +zxzfUV+Ufi8p8yDTzjDyyEfY3BhqHF2inHUyceKImTBcTWe4f7uCWf0ZnS/iYbAD +FmQ1uIt43Ul5TSnVgS0ljk2kVaboVVRaruACSW/hckDLrx3wpZCqYnp1D0wurSh1 +hQJsb2Opy5wve+hEUGr8AYlolB2SyQLtdWkwyNTmtDeb/cIM/NKXuWDECBrgEN8y +v6fluRe4YkLqzTOJvMr4xNtA9wshO8qcNWy6T5YaiNRJJmzBq2V3d41UuYRcih2K +jSRfn9xZvGPWZ92+Jce15pZY5+ec9A1PuT6J8304AQKBgQDbyWoPZxE6yEXDWz4+ +eFEvPGlVR8KiZhJl78iC1b9gW+FNlmd/LlamRB21IGfpoJgoRyjFP6UJ/tqGx79x +c+7krcBFCOfckYhRGCLxFdK2O0FTQ8ltfqaSYG4jeTSPVDaxhMYh/H3xjls9i7Y7 +90sWDjE/uSFuT+ExmNZvvVtjdQKBgQDUzGToK5gqFpVO43pcJbM6odBTVbZSxDMF +cq+KEOT/c7CzKOOQj7UTqiQgXM0odiJ/I4ZvaIlv+s7l/Cr9LyII3gZUQdPnszsM +/N8kMeJC5Oy8O5pboo+p4SOosRTVZhxGB1tTQxqyECZV5Y5ZCH6bGV4cEqO6rjYC +Pkunc6B3eQKBgCLmtg/iFwtVmDZwe87hvkqY9kUTkyXEvbEwRY/5L1222W0/sAmz +KxFWCb2kervPw7nJqwC/nY6byMnUWGNEvK/Vo42S33bYKWRvR8Uu6PoFKNd3ETpw +/TSLWZIKgj0sa07/PZNSDBHawERitjqJh4PmFw3+cP+acbE1iv/NewCtAoGAWOzt +QiRtmzECxgvDp1xN0LOsNhb8cQvyclVhy+WRfLrg3Y25w0B6oDQakreVOFJdyhmT +ZV0fCf+alHtTj6gxpdj6dh1oK0w34g6ORTbfYar+zw5tS9vcA1bFKwqNNTxNlmoe +nOXO8xhSnNSoLsag+bmZHUwgxbNleHyF6v0j0qkCgYAklO2RDc2RDccXYAe6MbT7 +EYExvS+K09CF4PVPoFk1QZxENuXfewDli9UCwt+uJvVacJOnUyLjMkkooibTTk+A +xf/dAp5ECrTHny/cMSerFJEgVZYsH06m+0a+RP+zL45skm/EsidOvIS80OkRr+Vz +rgIUqg2F14h2H2vbBlqrqQ== +-----END PRIVATE KEY----- diff --git a/test/integration/cli/certificates/localhost.pem b/test/integration/cli/certificates/localhost.pem new file mode 100644 index 000000000000..3d68b4263530 --- /dev/null +++ b/test/integration/cli/certificates/localhost.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKDCCApCgAwIBAgIRAOCKMC740uI5i7wASQ65kkgwDQYJKoZIhvcNAQELBQAw +bTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSEwHwYDVQQLDBhqakBz +bGF0ZS5sYW4gKEpKIEthc3BlcikxKDAmBgNVBAMMH21rY2VydCBqakBzbGF0ZS5s +YW4gKEpKIEthc3BlcikwHhcNMjMwMTI2MDQyMjA2WhcNMjUwNDI2MDMyMjA2WjBM +MScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxITAfBgNV +BAsMGGpqQHNsYXRlLmxhbiAoSkogS2FzcGVyKTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALayRvdHBAEq0kuQt/GY8zEBkbijpHib2Klwwm2kHqKbp2Lu +D59nNE/6PQC06echozpm5PrWcJNkp3tEH/CWuGAQGQioQYm0mH3dnQCfcna23tic +Eqwk0E/kGqosPZMPIUaBON8p1ck5tKu3rsiJG2sp1PpWBG0zaBMz53WW8vkhVz0m +JM54r1CPNWYMjYwimvOQWtA8jsb5QOo/zQiWtFfaaPfSD6siDqZroZycbRauTRyx +qCG4BBKItSqgZTdPMbtAr3mwxLjzGu/S2kjIhaVZNOvXue04hYDVwAYpRBp15VzY +2j9VElszEhyOa0NRzV7rpzjrP8pOAeja9LONZU0CAwEAAaNkMGIwDgYDVR0PAQH/ +BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFOCz+XaB1lpu +T6P2UIfjbq7pUX9GMBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG +9w0BAQsFAAOCAYEAaIPJdegZqcKys25yF5MYV10OyZ4wpP4gkONaA5oLfE/SeasH +cs1Af85oeGh6fCuszAoDCJHYvSsUqGhg/243Dkl993vGeLVikvC4R/LC/IkiwNKd +91byKnQBDghzeynyJiHO9yEiY4VFxUTThbttfDstDePOxHYxt8bLbIpGNZb0fdeh +opDwD9hhfL90A7sN7MdakBfQajpquyQgWvKGedaQx363WVXkssi4xRAIdm77ZzqA +Cy4WVrnRtvSbJNqYtNT0Ibx3gF7WC+ACh13lnniDjQlfcDJWxOVjYyvPx0k4M0KQ ++Qz/sx4RXy5jI9OO/hxJNNc3HAxU/7fLtnO/VtEb/c9A1m5gPUFU0W/EHL5VPJJ6 +A6+/T8wK8hDEY4j+bMrysbOeTrbTyLmFXMGFkkSI7OjX0RHkgYBJclgBZfQYGrh1 +PDmdZ26GSgt39k6VCY6ur7dXQuMPvQmRM6IqiPQpmd9SYP+FEiJh5TAOUBinI/Cu +hvseiJ2JQx+4YqxB +-----END CERTIFICATE----- diff --git a/test/integration/cli/test/index.test.js b/test/integration/cli/test/index.test.js index 9d19da817314..160bc39a0cfe 100644 --- a/test/integration/cli/test/index.test.js +++ b/test/integration/cli/test/index.test.js @@ -10,7 +10,7 @@ import { runNextCommandDev, } from 'next-test-utils' import fs from 'fs-extra' -import { join } from 'path' +import path, { join } from 'path' import pkg from 'next/package' import http from 'http' import stripAnsi from 'strip-ansi' @@ -531,6 +531,70 @@ describe('CLI Usage', () => { } }) + // only runs on CI as it requires administrator privileges + test('--experimental-https', async () => { + if (!process.env.CI) { + console.warn( + '--experimental-https only runs on CI as it requires administrator privileges' + ) + + return + } + + const port = await findPort() + let output = '' + const app = await runNextCommandDev( + [dirBasic, '--experimental-https', '--port', port], + undefined, + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await check(() => output, /on \[::\]:(\d+)/) + await check(() => output, /https:\/\/localhost:(\d+)/) + await check(() => output, /Certificates created in/) + } finally { + await killApp(app) + } + }) + + test('--experimental-https with provided key/cert', async () => { + const keyFile = path.resolve( + __dirname, + '../certificates/localhost-key.pem' + ) + const certFile = path.resolve(__dirname, '../certificates/localhost.pem') + const port = await findPort() + let output = '' + const app = await runNextCommandDev( + [ + dirBasic, + '--experimental-https', + '--experimental-https-key', + keyFile, + '--experimental-https-cert', + certFile, + '--port', + port, + ], + undefined, + { + onStdout(msg) { + output += stripAnsi(msg) + }, + } + ) + try { + await check(() => output, /on \[::\]:(\d+)/) + await check(() => output, /https:\/\/localhost:(\d+)/) + } finally { + await killApp(app) + } + }) + test('should format IPv6 addresses correctly', async () => { const port = await findPort() let output = ''