Skip to content

Commit 03435e5

Browse files
feat: begin integrating @netlify/dev (#7950)
* feat: begin integrating `@netlify/dev` * chore: add deps * fix(deps): bump netlify packages to dedupe with @netlify/dev * chore: update deps * chore: fix lint issue * chore: add debug logging * chore: add comment Co-authored-by: Philippe Serhal <philippe.serhal@netlify.com> * chore: cap site name length in tests * chore: update snapshot --------- Co-authored-by: Philippe Serhal <philippe.serhal@netlify.com>
1 parent f876f57 commit 03435e5

File tree

8 files changed

+436
-206
lines changed

8 files changed

+436
-206
lines changed

package-lock.json

Lines changed: 259 additions & 196 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,19 @@
5858
},
5959
"dependencies": {
6060
"@fastify/static": "9.0.0",
61-
"@netlify/ai": "0.3.4",
61+
"@netlify/ai": "0.3.8",
6262
"@netlify/api": "14.0.14",
63-
"@netlify/blobs": "10.1.0",
63+
"@netlify/blobs": "10.7.0",
6464
"@netlify/build": "35.7.1",
6565
"@netlify/build-info": "10.3.0",
6666
"@netlify/config": "24.4.0",
67-
"@netlify/dev-utils": "4.3.2",
67+
"@netlify/dev": "4.11.2",
68+
"@netlify/dev-utils": "4.3.3",
6869
"@netlify/edge-bundler": "14.9.8",
6970
"@netlify/edge-functions": "3.0.3",
7071
"@netlify/edge-functions-bootstrap": "2.17.1",
7172
"@netlify/headers-parser": "9.0.2",
72-
"@netlify/images": "1.2.5",
73+
"@netlify/images": "1.3.3",
7374
"@netlify/local-functions-proxy": "2.0.3",
7475
"@netlify/redirect-parser": "15.0.3",
7576
"@netlify/zip-it-and-ship-it": "14.3.2",
@@ -158,8 +159,8 @@
158159
"@bugsnag/js": "8.6.0",
159160
"@eslint/compat": "1.4.1",
160161
"@eslint/js": "9.36.0",
161-
"@netlify/functions": "5.1.0",
162-
"@netlify/types": "2.2.0",
162+
"@netlify/functions": "5.1.2",
163+
"@netlify/types": "2.3.0",
163164
"@sindresorhus/slugify": "3.0.0",
164165
"@tsconfig/node18": "18.2.4",
165166
"@tsconfig/recommended": "1.0.13",

src/commands/blobs/blobs-set.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const blobsSet = async (
6060
if (force === undefined) {
6161
const existingValue = await store.get(key)
6262

63+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
6364
if (existingValue) {
6465
await promptBlobSetOverwrite(key, storeName)
6566
}

src/commands/dev/dev.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ import {
2121
import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.js'
2222
import { parseAIGatewayContext, setupAIGateway } from '@netlify/ai/bootstrap'
2323

24-
import { UNLINKED_SITE_MOCK_ID, getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.js'
24+
import {
25+
UNLINKED_SITE_MOCK_ID,
26+
getDotEnvVariables,
27+
getSiteInformation,
28+
injectEnvVariables,
29+
processOnExit,
30+
} from '../../utils/dev.js'
2531
import { getEnvelopeEnv } from '../../utils/env/index.js'
2632
import { ensureNetlifyIgnore } from '../../utils/gitignore.js'
2733
import { getLiveTunnelSlug, startLiveTunnel } from '../../utils/live-tunnel.js'
@@ -35,6 +41,7 @@ import { getBaseOptionValues } from '../base-command.js'
3541
import type { NetlifySite } from '../types.js'
3642

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

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

175182
injectEnvVariables(env)
176183

184+
const programmaticNetlifyDev = await startProgrammaticNetlifyDev({
185+
projectRoot: command.workingDir,
186+
apiToken: api.accessToken ?? undefined,
187+
env,
188+
})
189+
190+
if (programmaticNetlifyDev) {
191+
processOnExit(() => programmaticNetlifyDev.stop())
192+
}
193+
177194
await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state })
178195

179196
let settings: ServerSettings
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import process from 'process'
2+
3+
import { NetlifyDev } from '@netlify/dev'
4+
5+
import { NETLIFYDEVWARN, log } from '../../utils/command-helpers.js'
6+
import type { EnvironmentVariables } from '../../utils/types.js'
7+
8+
interface StartNetlifyDevOptions {
9+
projectRoot: string
10+
apiToken: string | undefined
11+
env: EnvironmentVariables
12+
}
13+
14+
/**
15+
* Much of the core of local dev emulation of the Netlify platform was extracted
16+
* (duplicated) to https://github.com/netlify/primitives. This is a shim that
17+
* gradually enables *some* of this extracted functionality while falling back
18+
* to the legacy copy in this codebase for the rest.
19+
*
20+
* TODO: Hook this up to the request chain and fall through to the existing handler.
21+
* TODO: `@netlify/images` follows a different pattern (it is used directly).
22+
* Move that here.
23+
*/
24+
export const startNetlifyDev = async ({
25+
apiToken,
26+
env,
27+
projectRoot,
28+
}: StartNetlifyDevOptions): Promise<NetlifyDev | undefined> => {
29+
if (process.env.EXPERIMENTAL_NETLIFY_DB_ENABLED !== '1') {
30+
return
31+
}
32+
33+
const netlifyDev = new NetlifyDev({
34+
projectRoot,
35+
apiToken,
36+
...(process.env.NETLIFY_API_URL && { apiURL: process.env.NETLIFY_API_URL }),
37+
38+
aiGateway: { enabled: false },
39+
blobs: { enabled: false },
40+
edgeFunctions: { enabled: false },
41+
environmentVariables: { enabled: false },
42+
functions: { enabled: false },
43+
geolocation: { enabled: false },
44+
headers: { enabled: false },
45+
images: { enabled: false },
46+
redirects: { enabled: false },
47+
staticFiles: { enabled: false },
48+
serverAddress: null,
49+
})
50+
51+
try {
52+
await netlifyDev.start()
53+
} catch (error) {
54+
log(`${NETLIFYDEVWARN} Failed to start @netlify/dev: ${error instanceof Error ? error.message : String(error)}`)
55+
}
56+
57+
if (process.env.NETLIFY_DB_URL) {
58+
env.NETLIFY_DB_URL = { sources: ['internal'], value: process.env.NETLIFY_DB_URL }
59+
}
60+
61+
return netlifyDev
62+
}

tests/integration/__snapshots__/framework-detection.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ exports[`frameworks/framework-detection > should use static server when framewor
152152
⬥ Unable to determine public folder to serve files from. Using current working directory
153153
⬥ Setup a netlify.toml file with a [dev] section to specify your dev server settings.
154154
⬥ See docs at: https://docs.netlify.com/cli/local-development/#project-detection
155-
⬥ Running static server from \\"should-use-static-server-when-framework-is-set-to-static\\"
155+
⬥ Running static server from \\"should-use-static-server-when-framework-i-cabde4ea\\"
156156
⬥ Setting up local dev server
157157
158158
⬥ Static server listening to <SNAPSHOT_PORT_NORMALIZED>
@@ -168,7 +168,7 @@ exports[`frameworks/framework-detection > should warn if using static server and
168168
"⬥ Using simple static server because '--dir' flag was specified
169169
⬥ Ignoring 'targetPort' setting since using a simple static server.
170170
⬥ Use --staticServerPort or [dev.staticServerPort] to configure the static server port
171-
⬥ Running static server from \\"should-warn-if-using-static-server-and-target-port-is-configured/public\\"
171+
⬥ Running static server from \\"should-warn-if-using-static-server-and-ta-45f6af30/public\\"
172172
⬥ Setting up local dev server
173173
174174
⬥ Static server listening to <SNAPSHOT_PORT_NORMALIZED>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import fetch from 'node-fetch'
2+
import { describe, test } from 'vitest'
3+
4+
import { withDevServer } from '../../utils/dev-server.js'
5+
import { withSiteBuilder } from '../../utils/site-builder.js'
6+
7+
describe('@netlify/dev integration', () => {
8+
test('Makes DB available to functions when EXPERIMENTAL_NETLIFY_DB_ENABLED is set', async (t) => {
9+
await withSiteBuilder(t, async (builder) => {
10+
builder
11+
.withPackageJson({
12+
packageJson: {
13+
dependencies: { '@netlify/db': '0.1.0', '@netlify/db-dev': '0.2.0' },
14+
},
15+
})
16+
.withCommand({ command: ['npm', 'install'] })
17+
.withContentFile({
18+
path: 'netlify/functions/db-test.mjs',
19+
content: `
20+
import { getDatabase } from "@netlify/db";
21+
22+
export default async () => {
23+
try {
24+
const { sql } = getDatabase();
25+
const rows = await sql\`SELECT 1 + 1 AS sum\`;
26+
return Response.json({ sum: rows[0].sum });
27+
} catch (error) {
28+
return Response.json({ error: error.message }, { status: 500 });
29+
}
30+
};
31+
32+
export const config = { path: "/db-test" };
33+
`,
34+
})
35+
36+
await builder.build()
37+
38+
await withDevServer({ cwd: builder.directory, env: { EXPERIMENTAL_NETLIFY_DB_ENABLED: '1' } }, async (server) => {
39+
const response = await fetch(`${server.url}/db-test`)
40+
const body = await response.text()
41+
console.log(body)
42+
t.expect(body).toEqual(JSON.stringify({ sum: 2 }))
43+
})
44+
})
45+
})
46+
47+
test('Does not set NETLIFY_DB_URL when EXPERIMENTAL_NETLIFY_DB_ENABLED is not set', async (t) => {
48+
await withSiteBuilder(t, async (builder) => {
49+
builder.withFunction({
50+
path: 'db-url.mjs',
51+
pathPrefix: 'netlify/functions',
52+
runtimeAPIVersion: 2,
53+
config: { path: '/db-url' },
54+
handler: () => Response.json({ url: process.env.NETLIFY_DB_URL ?? '' }),
55+
})
56+
57+
await builder.build()
58+
59+
await withDevServer({ cwd: builder.directory }, async (server) => {
60+
const response = await fetch(`${server.url}/db-url`)
61+
const body = await response.text()
62+
console.log(body)
63+
64+
t.expect(response.status).toBe(200)
65+
t.expect(body).toEqual(JSON.stringify({ url: '' }))
66+
})
67+
})
68+
})
69+
})

tests/integration/utils/site-builder.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from 'crypto'
12
import { copyFile, mkdir, rm, unlink, writeFile } from 'fs/promises'
23
import os from 'os'
34
import path from 'path'
@@ -317,13 +318,29 @@ export class SiteBuilder {
317318
}
318319
}
319320

321+
// Windows has a MAX_PATH limit of 260 characters. Since test directories
322+
// include the temp dir, process version, PID, a UUID, and the site name,
323+
// long test names can push nested file paths over this limit. We cap the
324+
// site name and append a hash to avoid collisions.
325+
const MAX_SITE_NAME_LENGTH = 50
326+
327+
const truncateSiteName = (siteName: string): string => {
328+
if (siteName.length <= MAX_SITE_NAME_LENGTH) {
329+
return siteName
330+
}
331+
332+
const hash = createHash('sha256').update(siteName).digest('hex').slice(0, 8)
333+
334+
return `${siteName.slice(0, MAX_SITE_NAME_LENGTH - 9)}-${hash}`
335+
}
336+
320337
export const createSiteBuilder = ({ siteName }: { siteName: string }) => {
321338
const directory = path.join(
322339
tempDirectory,
323340
`netlify-cli-tests-${process.version}`,
324341
`${process.pid}`,
325342
uuidv4(),
326-
siteName,
343+
truncateSiteName(siteName),
327344
)
328345

329346
return new SiteBuilder(directory).ensureDirectoryExists(directory)

0 commit comments

Comments
 (0)