@netlify/build 35.10.0 → 35.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,16 @@ export function logFunctionsToBundle({ logs, userFunctions, userFunctionsSrc, us
14
14
  generatedFunctions: any;
15
15
  type?: string | undefined;
16
16
  }): void;
17
+ export function logDbProvisioning({ logs, branch, context }: {
18
+ logs: any;
19
+ branch: any;
20
+ context: any;
21
+ }): void;
22
+ export function logDbMigrations({ logs, migrations, srcDir }: {
23
+ logs: any;
24
+ migrations: any;
25
+ srcDir: any;
26
+ }): void;
17
27
  export function logSecretsScanSkipMessage(logs: any, msg: any): void;
18
28
  export function logSecretsScanSuccessMessage(logs: any, msg: any): void;
19
29
  export function logSecretsScanFailBuildMessage({ logs, scanResults, groupedResults, enhancedScanShouldRunInActiveMode, }: {
@@ -90,6 +90,22 @@ export const logFunctionsToBundle = function ({ logs, userFunctions, userFunctio
90
90
  log(logs, `Packaging ${type} from ${THEME.highlightWords(userFunctionsSrc)} directory:`);
91
91
  logArray(logs, userFunctions, { indent: false });
92
92
  };
93
+ // Print the database provisioning message
94
+ export const logDbProvisioning = function ({ logs, branch, context }) {
95
+ log(logs, `Provisioning database`);
96
+ if (context !== 'production') {
97
+ log(logs, `Creating database branch for ${THEME.highlightWords(branch)}`);
98
+ }
99
+ };
100
+ // Print the list of database migrations about to be copied
101
+ export const logDbMigrations = function ({ logs, migrations, srcDir }) {
102
+ if (migrations.length === 0) {
103
+ log(logs, `No migrations found in ${THEME.highlightWords(srcDir)} directory`);
104
+ return;
105
+ }
106
+ log(logs, `Loading migrations from ${THEME.highlightWords(srcDir)} directory:`);
107
+ logArray(logs, migrations, { indent: false });
108
+ };
93
109
  const logModulesWithDynamicImports = ({ logs, modulesWithDynamicImports }) => {
94
110
  const externalNodeModules = modulesWithDynamicImports.map((moduleName) => `"${moduleName}"`).join(', ');
95
111
  logWarningSubHeader(logs, `The following Node.js modules use dynamic expressions to include files:`);
@@ -1,5 +1,7 @@
1
1
  import { join } from 'node:path';
2
+ import { logDbProvisioning, logDbMigrations } from '../../log/messages/core_steps.js';
2
3
  import { getPackageJson } from '../../utils/package.js';
4
+ import { readMigrationEntries, getMigrationNames } from './utils.js';
3
5
  const NPM_PACKAGE_NAME = '@netlify/db';
4
6
  const condition = async ({ buildDir, packagePath, featureFlags }) => {
5
7
  if (!featureFlags?.netlify_build_db_setup) {
@@ -17,8 +19,14 @@ const condition = async ({ buildDir, packagePath, featureFlags }) => {
17
19
  }
18
20
  return false;
19
21
  };
20
- const coreStep = async ({ api, branch, constants, context }) => {
22
+ const coreStep = async ({ api, branch, buildDir, constants, context, logs }) => {
21
23
  const siteId = constants.SITE_ID;
24
+ logDbProvisioning({ logs, branch, context });
25
+ const entries = await readMigrationEntries(buildDir, constants.DB_MIGRATIONS_SRC);
26
+ const migrationNames = getMigrationNames(entries);
27
+ if (migrationNames.length > 0) {
28
+ logDbMigrations({ logs, migrations: migrationNames, srcDir: constants.DB_MIGRATIONS_SRC });
29
+ }
22
30
  // @ts-expect-error This is an internal method for now so it isn't typed yet.
23
31
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
24
32
  const database = (await api.createSiteDatabase({ site_id: siteId }));
@@ -44,6 +52,6 @@ export const dbSetup = {
44
52
  coreStep,
45
53
  coreStepId: 'db_provision',
46
54
  coreStepName: 'Netlify DB setup',
47
- coreStepDescription: () => 'Setup Netlify DB database',
55
+ coreStepDescription: () => 'Netlify DB setup',
48
56
  condition,
49
57
  };
@@ -1,7 +1,8 @@
1
- import { copyFile, mkdir, readdir, stat } from 'node:fs/promises';
1
+ import { copyFile, mkdir } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { pathExists } from 'path-exists';
4
- import { validateMigrationDirs, formatValidationErrors } from './validation.js';
4
+ import { readMigrationEntries } from './utils.js';
5
+ import { validateMigrations, formatValidationErrors } from './validation.js';
5
6
  const condition = async ({ featureFlags, constants, buildDir }) => {
6
7
  if (!featureFlags?.netlify_build_db_setup) {
7
8
  return false;
@@ -15,15 +16,8 @@ const condition = async ({ featureFlags, constants, buildDir }) => {
15
16
  const coreStep = async ({ constants, buildDir, systemLog }) => {
16
17
  const srcDir = resolve(buildDir, constants.DB_MIGRATIONS_SRC);
17
18
  const destDir = resolve(buildDir, constants.DB_MIGRATIONS_DIST);
18
- const entries = await readdir(srcDir);
19
- const dirNames = [];
20
- for (const entry of entries) {
21
- const entryStat = await stat(join(srcDir, entry));
22
- if (entryStat.isDirectory()) {
23
- dirNames.push(entry);
24
- }
25
- }
26
- if (dirNames.length === 0) {
19
+ const { dirNames, fileNames } = await readMigrationEntries(buildDir, constants.DB_MIGRATIONS_SRC);
20
+ if (dirNames.length === 0 && fileNames.length === 0) {
27
21
  systemLog('No migration directories found, skipping copy.');
28
22
  return {};
29
23
  }
@@ -34,7 +28,7 @@ const coreStep = async ({ constants, buildDir, systemLog }) => {
34
28
  existingSqlFiles.add(dirName);
35
29
  }
36
30
  }
37
- const result = validateMigrationDirs(dirNames, existingSqlFiles);
31
+ const result = validateMigrations(dirNames, fileNames, existingSqlFiles);
38
32
  if (!result.valid) {
39
33
  const message = formatValidationErrors(result.errors);
40
34
  throw new Error(message);
@@ -44,7 +38,14 @@ const coreStep = async ({ constants, buildDir, systemLog }) => {
44
38
  await mkdir(migrationDestDir, { recursive: true });
45
39
  await copyFile(join(srcDir, dirName, 'migration.sql'), join(migrationDestDir, 'migration.sql'));
46
40
  }
47
- systemLog(`Copied ${String(result.dirs.length)} migration(s) to ${destDir}`);
41
+ for (const fileName of result.files) {
42
+ const stem = fileName.replace(/\.sql$/, '');
43
+ const migrationDestDir = join(destDir, stem);
44
+ await mkdir(migrationDestDir, { recursive: true });
45
+ await copyFile(join(srcDir, fileName), join(migrationDestDir, 'migration.sql'));
46
+ }
47
+ const totalCount = result.dirs.length + result.files.length;
48
+ systemLog(`Copied ${String(totalCount)} migration(s) to ${destDir}`);
48
49
  return {};
49
50
  };
50
51
  export const copyDbMigrations = {
@@ -54,4 +55,5 @@ export const copyDbMigrations = {
54
55
  coreStepName: 'Netlify DB migrations',
55
56
  coreStepDescription: () => 'Copy database migrations to internal directory',
56
57
  condition,
58
+ quiet: true,
57
59
  };
@@ -0,0 +1,6 @@
1
+ export interface MigrationEntries {
2
+ dirNames: string[];
3
+ fileNames: string[];
4
+ }
5
+ export declare const readMigrationEntries: (buildDir: string, migrationsSrc?: string) => Promise<MigrationEntries>;
6
+ export declare const getMigrationNames: ({ dirNames, fileNames }: MigrationEntries) => string[];
@@ -0,0 +1,30 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { pathExists } from 'path-exists';
4
+ export const readMigrationEntries = async (buildDir, migrationsSrc) => {
5
+ const empty = { dirNames: [], fileNames: [] };
6
+ if (!migrationsSrc) {
7
+ return empty;
8
+ }
9
+ const srcDir = resolve(buildDir, migrationsSrc);
10
+ if (!(await pathExists(srcDir))) {
11
+ return empty;
12
+ }
13
+ const entries = await readdir(srcDir);
14
+ const dirNames = [];
15
+ const fileNames = [];
16
+ for (const entry of entries) {
17
+ const entryStat = await stat(join(srcDir, entry));
18
+ if (entryStat.isDirectory()) {
19
+ dirNames.push(entry);
20
+ }
21
+ else if (entry.endsWith('.sql')) {
22
+ fileNames.push(entry);
23
+ }
24
+ }
25
+ return { dirNames, fileNames };
26
+ };
27
+ export const getMigrationNames = ({ dirNames, fileNames }) => [
28
+ ...dirNames,
29
+ ...fileNames.map((f) => f.replace(/\.sql$/, '')),
30
+ ];
@@ -1,16 +1,18 @@
1
1
  export declare const MIGRATION_DIR_PATTERN: RegExp;
2
+ export declare const MIGRATION_FILE_PATTERN: RegExp;
2
3
  interface ValidationError {
3
- type: 'invalid_dir_name' | 'missing_sql_file';
4
+ type: 'missing_sql_file';
4
5
  dirName: string;
5
6
  }
6
7
  interface ValidationResult {
7
8
  valid: true;
8
9
  dirs: string[];
10
+ files: string[];
9
11
  }
10
12
  interface ValidationFailure {
11
13
  valid: false;
12
14
  errors: ValidationError[];
13
15
  }
14
- export declare const validateMigrationDirs: (dirNames: string[], existingSqlFiles: Set<string>) => ValidationResult | ValidationFailure;
16
+ export declare const validateMigrations: (dirNames: string[], fileNames: string[], existingSqlFiles: Set<string>) => ValidationResult | ValidationFailure;
15
17
  export declare const formatValidationErrors: (errors: ValidationError[]) => string;
16
18
  export {};
@@ -1,11 +1,13 @@
1
- export const MIGRATION_DIR_PATTERN = /^\d+_[a-z0-9-]+$/;
2
- export const validateMigrationDirs = (dirNames, existingSqlFiles) => {
1
+ export const MIGRATION_DIR_PATTERN = /^\d+_[a-z0-9_-]+$/;
2
+ export const MIGRATION_FILE_PATTERN = /^\d+_[a-z0-9_-]+\.sql$/;
3
+ export const validateMigrations = (dirNames, fileNames, existingSqlFiles) => {
3
4
  const errors = [];
5
+ const matchingDirs = [];
4
6
  for (const dirName of dirNames) {
5
7
  if (!MIGRATION_DIR_PATTERN.test(dirName)) {
6
- errors.push({ type: 'invalid_dir_name', dirName });
7
8
  continue;
8
9
  }
10
+ matchingDirs.push(dirName);
9
11
  if (!existingSqlFiles.has(dirName)) {
10
12
  errors.push({ type: 'missing_sql_file', dirName });
11
13
  }
@@ -13,19 +15,21 @@ export const validateMigrationDirs = (dirNames, existingSqlFiles) => {
13
15
  if (errors.length > 0) {
14
16
  return { valid: false, errors };
15
17
  }
16
- const sorted = [...dirNames].sort();
17
- return { valid: true, dirs: sorted };
18
+ const matchingFiles = [];
19
+ for (const fileName of fileNames) {
20
+ if (MIGRATION_FILE_PATTERN.test(fileName)) {
21
+ matchingFiles.push(fileName);
22
+ }
23
+ }
24
+ return {
25
+ valid: true,
26
+ dirs: [...matchingDirs].sort(),
27
+ files: [...matchingFiles].sort(),
28
+ };
18
29
  };
19
30
  export const formatValidationErrors = (errors) => {
20
31
  const lines = errors.map((error) => {
21
- if (error.type === 'invalid_dir_name') {
22
- return ` - "${error.dirName}" does not match the required pattern "<number>_<slug>" (e.g. "1700000000_create-users" or "001_create-users"). Slugs must be lowercase alphanumeric with hyphens.`;
23
- }
24
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
25
- if (error.type === 'missing_sql_file') {
26
- return ` - "${error.dirName}/migration.sql" is missing.`;
27
- }
28
- return ` - "${error.dirName}": unknown validation error.`;
32
+ return ` - "${error.dirName}/migration.sql" is missing.`;
29
33
  });
30
34
  return `Database migration validation failed:\n${lines.join('\n')}`;
31
35
  };
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import { MIGRATION_DIR_PATTERN, validateMigrationDirs, formatValidationErrors } from './validation.js';
2
+ import { MIGRATION_DIR_PATTERN, MIGRATION_FILE_PATTERN, validateMigrations, formatValidationErrors, } from './validation.js';
3
3
  describe('MIGRATION_DIR_PATTERN', () => {
4
4
  const validNames = [
5
5
  '1700000000_create-users',
@@ -12,13 +12,15 @@ describe('MIGRATION_DIR_PATTERN', () => {
12
12
  '1_init',
13
13
  '0001_add-posts',
14
14
  '42_z',
15
+ '1700000000_under_score',
16
+ '001_create_users_table',
17
+ '1_a_b_c',
15
18
  ];
16
19
  test.each(validNames)('matches valid name: %s', (name) => {
17
20
  expect(MIGRATION_DIR_PATTERN.test(name)).toBe(true);
18
21
  });
19
22
  const invalidNames = [
20
23
  { name: '1700000000_CAPS', reason: 'uppercase letters' },
21
- { name: '1700000000_under_score', reason: 'underscores in slug' },
22
24
  { name: 'no-timestamp', reason: 'no numeric prefix' },
23
25
  { name: '1700000000_', reason: 'empty slug' },
24
26
  { name: '1700000000', reason: 'missing underscore and slug' },
@@ -30,60 +32,108 @@ describe('MIGRATION_DIR_PATTERN', () => {
30
32
  expect(MIGRATION_DIR_PATTERN.test(name)).toBe(false);
31
33
  });
32
34
  });
33
- describe('validateMigrationDirs', () => {
35
+ describe('MIGRATION_FILE_PATTERN', () => {
36
+ const validNames = [
37
+ '1700000000_create-users.sql',
38
+ '001_create_users_table.sql',
39
+ '1_init.sql',
40
+ '42_a-b-c.sql',
41
+ '1700000000_under_score.sql',
42
+ ];
43
+ test.each(validNames)('matches valid name: %s', (name) => {
44
+ expect(MIGRATION_FILE_PATTERN.test(name)).toBe(true);
45
+ });
46
+ const invalidNames = [
47
+ { name: '1700000000_CAPS.sql', reason: 'uppercase letters' },
48
+ { name: 'no-timestamp.sql', reason: 'no numeric prefix' },
49
+ { name: '1700000000_.sql', reason: 'empty slug' },
50
+ { name: '1700000000_create-users.txt', reason: 'wrong extension' },
51
+ { name: '1700000000_create-users', reason: 'no extension' },
52
+ { name: '1700000000_hello world.sql', reason: 'spaces in slug' },
53
+ ];
54
+ test.each(invalidNames)('rejects invalid name: $name ($reason)', ({ name }) => {
55
+ expect(MIGRATION_FILE_PATTERN.test(name)).toBe(false);
56
+ });
57
+ });
58
+ describe('validateMigrations', () => {
34
59
  test('returns valid result with sorted dirs when all dirs are valid', () => {
35
60
  const dirNames = ['1700000001_add-posts', '1700000000_create-users'];
36
61
  const existingSqlFiles = new Set(['1700000000_create-users', '1700000001_add-posts']);
37
- const result = validateMigrationDirs(dirNames, existingSqlFiles);
38
- expect(result.valid).toBe(true);
39
- if (result.valid) {
40
- expect(result.dirs).toEqual(['1700000000_create-users', '1700000001_add-posts']);
41
- }
62
+ const result = validateMigrations(dirNames, [], existingSqlFiles);
63
+ expect(result).toEqual({
64
+ valid: true,
65
+ dirs: ['1700000000_create-users', '1700000001_add-posts'],
66
+ files: [],
67
+ });
42
68
  });
43
- test('returns error for invalid directory names', () => {
69
+ test('silently skips non-matching directory names', () => {
44
70
  const dirNames = ['bad-name', '1700000000_create-users'];
45
71
  const existingSqlFiles = new Set(['1700000000_create-users']);
46
- const result = validateMigrationDirs(dirNames, existingSqlFiles);
47
- expect(result.valid).toBe(false);
48
- if (!result.valid) {
49
- expect(result.errors).toEqual([{ type: 'invalid_dir_name', dirName: 'bad-name' }]);
50
- }
72
+ const result = validateMigrations(dirNames, [], existingSqlFiles);
73
+ expect(result).toEqual({
74
+ valid: true,
75
+ dirs: ['1700000000_create-users'],
76
+ files: [],
77
+ });
51
78
  });
52
79
  test('returns error for missing migration.sql files', () => {
53
80
  const dirNames = ['1700000000_create-users', '1700000001_add-posts'];
54
81
  const existingSqlFiles = new Set(['1700000000_create-users']);
55
- const result = validateMigrationDirs(dirNames, existingSqlFiles);
56
- expect(result.valid).toBe(false);
57
- if (!result.valid) {
58
- expect(result.errors).toEqual([{ type: 'missing_sql_file', dirName: '1700000001_add-posts' }]);
59
- }
82
+ const result = validateMigrations(dirNames, [], existingSqlFiles);
83
+ expect(result).toEqual({
84
+ valid: false,
85
+ errors: [{ type: 'missing_sql_file', dirName: '1700000001_add-posts' }],
86
+ });
87
+ });
88
+ test('returns valid result with sorted files for loose .sql files', () => {
89
+ const fileNames = ['002_add-posts.sql', '001_create-users.sql'];
90
+ const result = validateMigrations([], fileNames, new Set());
91
+ expect(result).toEqual({
92
+ valid: true,
93
+ dirs: [],
94
+ files: ['001_create-users.sql', '002_add-posts.sql'],
95
+ });
60
96
  });
61
- test('reports both invalid names and missing files', () => {
62
- const dirNames = ['bad-name', '1700000001_add-posts'];
63
- const existingSqlFiles = new Set();
64
- const result = validateMigrationDirs(dirNames, existingSqlFiles);
65
- expect(result.valid).toBe(false);
66
- if (!result.valid) {
67
- expect(result.errors).toHaveLength(2);
68
- expect(result.errors[0]).toEqual({ type: 'invalid_dir_name', dirName: 'bad-name' });
69
- expect(result.errors[1]).toEqual({ type: 'missing_sql_file', dirName: '1700000001_add-posts' });
70
- }
97
+ test('silently skips non-matching file names', () => {
98
+ const fileNames = ['README.sql', '001_create-users.sql', 'bad-name.sql'];
99
+ const result = validateMigrations([], fileNames, new Set());
100
+ expect(result).toEqual({
101
+ valid: true,
102
+ dirs: [],
103
+ files: ['001_create-users.sql'],
104
+ });
105
+ });
106
+ test('handles mixed dirs and files', () => {
107
+ const dirNames = ['1700000000_create-users'];
108
+ const fileNames = ['1700000001_add-posts.sql'];
109
+ const existingSqlFiles = new Set(['1700000000_create-users']);
110
+ const result = validateMigrations(dirNames, fileNames, existingSqlFiles);
111
+ expect(result).toEqual({
112
+ valid: true,
113
+ dirs: ['1700000000_create-users'],
114
+ files: ['1700000001_add-posts.sql'],
115
+ });
71
116
  });
72
117
  test('returns valid result for empty input', () => {
73
- const result = validateMigrationDirs([], new Set());
74
- expect(result.valid).toBe(true);
75
- if (result.valid) {
76
- expect(result.dirs).toEqual([]);
77
- }
118
+ const result = validateMigrations([], [], new Set());
119
+ expect(result).toEqual({
120
+ valid: true,
121
+ dirs: [],
122
+ files: [],
123
+ });
124
+ });
125
+ test('allows underscores in dir names', () => {
126
+ const dirNames = ['001_create_users_table'];
127
+ const existingSqlFiles = new Set(['001_create_users_table']);
128
+ const result = validateMigrations(dirNames, [], existingSqlFiles);
129
+ expect(result).toEqual({
130
+ valid: true,
131
+ dirs: ['001_create_users_table'],
132
+ files: [],
133
+ });
78
134
  });
79
135
  });
80
136
  describe('formatValidationErrors', () => {
81
- test('formats invalid_dir_name errors', () => {
82
- const message = formatValidationErrors([{ type: 'invalid_dir_name', dirName: 'bad-name' }]);
83
- expect(message).toContain('Database migration validation failed');
84
- expect(message).toContain('"bad-name"');
85
- expect(message).toContain('<number>_<slug>');
86
- });
87
137
  test('formats missing_sql_file errors', () => {
88
138
  const message = formatValidationErrors([{ type: 'missing_sql_file', dirName: '1700000000_create-users' }]);
89
139
  expect(message).toContain('Database migration validation failed');
@@ -91,10 +141,10 @@ describe('formatValidationErrors', () => {
91
141
  });
92
142
  test('formats multiple errors', () => {
93
143
  const message = formatValidationErrors([
94
- { type: 'invalid_dir_name', dirName: 'bad-name' },
144
+ { type: 'missing_sql_file', dirName: '1700000000_create-users' },
95
145
  { type: 'missing_sql_file', dirName: '1700000001_add-posts' },
96
146
  ]);
97
- expect(message).toContain('"bad-name"');
147
+ expect(message).toContain('"1700000000_create-users/migration.sql" is missing');
98
148
  expect(message).toContain('"1700000001_add-posts/migration.sql" is missing');
99
149
  });
100
150
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/build",
3
- "version": "35.10.0",
3
+ "version": "35.11.0",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -69,14 +69,14 @@
69
69
  "@bugsnag/js": "^8.0.0",
70
70
  "@netlify/blobs": "^10.4.4",
71
71
  "@netlify/cache-utils": "^6.0.5",
72
- "@netlify/config": "^24.4.3",
73
- "@netlify/edge-bundler": "14.9.17",
74
- "@netlify/functions-utils": "^6.2.26",
72
+ "@netlify/config": "^24.4.4",
73
+ "@netlify/edge-bundler": "14.9.19",
74
+ "@netlify/functions-utils": "^6.2.27",
75
75
  "@netlify/git-utils": "^6.0.4",
76
76
  "@netlify/opentelemetry-utils": "^2.0.2",
77
77
  "@netlify/plugins-list": "^6.81.3",
78
78
  "@netlify/run-utils": "^6.0.3",
79
- "@netlify/zip-it-and-ship-it": "14.5.0",
79
+ "@netlify/zip-it-and-ship-it": "14.5.1",
80
80
  "@sindresorhus/slugify": "^2.0.0",
81
81
  "ansi-escapes": "^7.0.0",
82
82
  "ansis": "^4.1.0",
@@ -152,5 +152,5 @@
152
152
  "engines": {
153
153
  "node": ">=18.14.0"
154
154
  },
155
- "gitHead": "4c1f6b11490fca02a044a8f0e3bed51ef6b7c85a"
155
+ "gitHead": "3694352be4959c56e96a6c15dd04ed4d7aa31344"
156
156
  }