Skip to content
455 changes: 259 additions & 196 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,19 @@
},
"dependencies": {
"@fastify/static": "9.0.0",
"@netlify/ai": "0.3.4",
"@netlify/ai": "0.3.8",
"@netlify/api": "14.0.14",
"@netlify/blobs": "10.1.0",
"@netlify/blobs": "10.7.0",
"@netlify/build": "35.7.1",
"@netlify/build-info": "10.3.0",
"@netlify/config": "24.4.0",
"@netlify/dev-utils": "4.3.2",
"@netlify/dev": "4.11.2",
"@netlify/dev-utils": "4.3.3",
"@netlify/edge-bundler": "14.9.8",
"@netlify/edge-functions": "3.0.3",
"@netlify/edge-functions-bootstrap": "2.17.1",
"@netlify/headers-parser": "9.0.2",
"@netlify/images": "1.2.5",
"@netlify/images": "1.3.3",
"@netlify/local-functions-proxy": "2.0.3",
"@netlify/redirect-parser": "15.0.3",
"@netlify/zip-it-and-ship-it": "14.3.2",
Expand Down Expand Up @@ -158,8 +159,8 @@
"@bugsnag/js": "8.6.0",
"@eslint/compat": "1.4.1",
"@eslint/js": "9.36.0",
"@netlify/functions": "5.1.0",
"@netlify/types": "2.2.0",
"@netlify/functions": "5.1.2",
"@netlify/types": "2.3.0",
"@sindresorhus/slugify": "3.0.0",
"@tsconfig/node18": "18.2.4",
"@tsconfig/recommended": "1.0.13",
Expand Down
1 change: 1 addition & 0 deletions src/commands/blobs/blobs-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const blobsSet = async (
if (force === undefined) {
const existingValue = await store.get(key)

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Copy link
Member

Choose a reason for hiding this comment

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

Is there a wrong type we should be fixing?

Copy link
Member Author

Choose a reason for hiding this comment

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

Linting started failing for this, randomly. I haven't yet had the time to understand why, since I haven't touched this part of the code.

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'll look into this as a follow-up.

if (existingValue) {
await promptBlobSetOverwrite(key, storeName)
}
Expand Down
19 changes: 18 additions & 1 deletion src/commands/dev/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ import {
import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.js'
import { parseAIGatewayContext, setupAIGateway } from '@netlify/ai/bootstrap'

import { UNLINKED_SITE_MOCK_ID, getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.js'
import {
UNLINKED_SITE_MOCK_ID,
getDotEnvVariables,
getSiteInformation,
injectEnvVariables,
processOnExit,
} from '../../utils/dev.js'
import { getEnvelopeEnv } from '../../utils/env/index.js'
import { ensureNetlifyIgnore } from '../../utils/gitignore.js'
import { getLiveTunnelSlug, startLiveTunnel } from '../../utils/live-tunnel.js'
Expand All @@ -35,6 +41,7 @@ import { getBaseOptionValues } from '../base-command.js'
import type { NetlifySite } from '../types.js'

import type { DevConfig } from './types.js'
import { startNetlifyDev as startProgrammaticNetlifyDev } from './programmatic-netlify-dev.js'
import { doesProjectRequireLinkedSite } from '../../lib/extensions.js'

const handleLiveTunnel = async ({
Expand Down Expand Up @@ -174,6 +181,16 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {

injectEnvVariables(env)

const programmaticNetlifyDev = await startProgrammaticNetlifyDev({
Copy link
Member

Choose a reason for hiding this comment

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

You may know this already, but for completeness: this would need to also be hooked up in the dev:exec, serve, and functions:serve commands.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'll do that in a follow up. Having this on dev for now is fine.

projectRoot: command.workingDir,
apiToken: api.accessToken ?? undefined,
env,
})

if (programmaticNetlifyDev) {
processOnExit(() => programmaticNetlifyDev.stop())
}

await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state })

let settings: ServerSettings
Expand Down
62 changes: 62 additions & 0 deletions src/commands/dev/programmatic-netlify-dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import process from 'process'

import { NetlifyDev } from '@netlify/dev'

import { NETLIFYDEVWARN, log } from '../../utils/command-helpers.js'
import type { EnvironmentVariables } from '../../utils/types.js'

interface StartNetlifyDevOptions {
projectRoot: string
apiToken: string | undefined
env: EnvironmentVariables
}

/**
* Much of the core of local dev emulation of the Netlify platform was extracted
* (duplicated) to https://github.com/netlify/primitives. This is a shim that
* gradually enables *some* of this extracted functionality while falling back
* to the legacy copy in this codebase for the rest.
*
* TODO: Hook this up to the request chain and fall through to the existing handler.
* TODO: `@netlify/images` follows a different pattern (it is used directly).
* Move that here.
*/
export const startNetlifyDev = async ({
apiToken,
env,
projectRoot,
}: StartNetlifyDevOptions): Promise<NetlifyDev | undefined> => {
if (process.env.EXPERIMENTAL_NETLIFY_DB_ENABLED !== '1') {
return
}

const netlifyDev = new NetlifyDev({
projectRoot,
apiToken,
...(process.env.NETLIFY_API_URL && { apiURL: process.env.NETLIFY_API_URL }),

aiGateway: { enabled: false },
blobs: { enabled: false },
edgeFunctions: { enabled: false },
environmentVariables: { enabled: false },
functions: { enabled: false },
geolocation: { enabled: false },
headers: { enabled: false },
images: { enabled: false },
redirects: { enabled: false },
staticFiles: { enabled: false },
serverAddress: null,
})

try {
await netlifyDev.start()
} catch (error) {
log(`${NETLIFYDEVWARN} Failed to start @netlify/dev: ${error instanceof Error ? error.message : String(error)}`)
}

if (process.env.NETLIFY_DB_URL) {
env.NETLIFY_DB_URL = { sources: ['internal'], value: process.env.NETLIFY_DB_URL }
}

return netlifyDev
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ exports[`frameworks/framework-detection > should use static server when framewor
⬥ Unable to determine public folder to serve files from. Using current working directory
⬥ Setup a netlify.toml file with a [dev] section to specify your dev server settings.
⬥ See docs at: https://docs.netlify.com/cli/local-development/#project-detection
⬥ Running static server from \\"should-use-static-server-when-framework-is-set-to-static\\"
⬥ Running static server from \\"should-use-static-server-when-framework-i-cabde4ea\\"
⬥ Setting up local dev server
⬥ Static server listening to <SNAPSHOT_PORT_NORMALIZED>
Expand All @@ -168,7 +168,7 @@ exports[`frameworks/framework-detection > should warn if using static server and
"⬥ Using simple static server because '--dir' flag was specified
⬥ Ignoring 'targetPort' setting since using a simple static server.
⬥ Use --staticServerPort or [dev.staticServerPort] to configure the static server port
⬥ Running static server from \\"should-warn-if-using-static-server-and-target-port-is-configured/public\\"
⬥ Running static server from \\"should-warn-if-using-static-server-and-ta-45f6af30/public\\"
⬥ Setting up local dev server
⬥ Static server listening to <SNAPSHOT_PORT_NORMALIZED>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import fetch from 'node-fetch'
import { describe, test } from 'vitest'

import { withDevServer } from '../../utils/dev-server.js'
import { withSiteBuilder } from '../../utils/site-builder.js'

describe('@netlify/dev integration', () => {
test('Makes DB available to functions when EXPERIMENTAL_NETLIFY_DB_ENABLED is set', async (t) => {
await withSiteBuilder(t, async (builder) => {
builder
.withPackageJson({
packageJson: {
dependencies: { '@netlify/db': '0.1.0', '@netlify/db-dev': '0.2.0' },
},
})
.withCommand({ command: ['npm', 'install'] })
.withContentFile({
path: 'netlify/functions/db-test.mjs',
content: `
import { getDatabase } from "@netlify/db";
export default async () => {
try {
const { sql } = getDatabase();
const rows = await sql\`SELECT 1 + 1 AS sum\`;
return Response.json({ sum: rows[0].sum });
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
};
export const config = { path: "/db-test" };
`,
})

await builder.build()

await withDevServer({ cwd: builder.directory, env: { EXPERIMENTAL_NETLIFY_DB_ENABLED: '1' } }, async (server) => {
const response = await fetch(`${server.url}/db-test`)
const body = await response.text()
console.log(body)
t.expect(body).toEqual(JSON.stringify({ sum: 2 }))
})
})
})

test('Does not set NETLIFY_DB_URL when EXPERIMENTAL_NETLIFY_DB_ENABLED is not set', async (t) => {
await withSiteBuilder(t, async (builder) => {
builder.withFunction({
path: 'db-url.mjs',
pathPrefix: 'netlify/functions',
runtimeAPIVersion: 2,
config: { path: '/db-url' },
handler: () => Response.json({ url: process.env.NETLIFY_DB_URL ?? '' }),
})

await builder.build()

await withDevServer({ cwd: builder.directory }, async (server) => {
const response = await fetch(`${server.url}/db-url`)
const body = await response.text()
console.log(body)

t.expect(response.status).toBe(200)
t.expect(body).toEqual(JSON.stringify({ url: '' }))
})
})
})
})
19 changes: 18 additions & 1 deletion tests/integration/utils/site-builder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from 'crypto'
import { copyFile, mkdir, rm, unlink, writeFile } from 'fs/promises'
import os from 'os'
import path from 'path'
Expand Down Expand Up @@ -317,13 +318,29 @@ export class SiteBuilder {
}
}

// Windows has a MAX_PATH limit of 260 characters. Since test directories
// include the temp dir, process version, PID, a UUID, and the site name,
// long test names can push nested file paths over this limit. We cap the
// site name and append a hash to avoid collisions.
const MAX_SITE_NAME_LENGTH = 50

const truncateSiteName = (siteName: string): string => {
if (siteName.length <= MAX_SITE_NAME_LENGTH) {
return siteName
}

const hash = createHash('sha256').update(siteName).digest('hex').slice(0, 8)

return `${siteName.slice(0, MAX_SITE_NAME_LENGTH - 9)}-${hash}`
}

export const createSiteBuilder = ({ siteName }: { siteName: string }) => {
const directory = path.join(
tempDirectory,
`netlify-cli-tests-${process.version}`,
`${process.pid}`,
uuidv4(),
siteName,
truncateSiteName(siteName),
)

return new SiteBuilder(directory).ensureDirectoryExists(directory)
Expand Down
Loading