@netlify/build 35.6.2 → 35.7.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.
- package/lib/core/constants.d.ts +11 -1
- package/lib/core/constants.js +9 -1
- package/lib/plugins_core/db_setup/migrations.d.ts +2 -0
- package/lib/plugins_core/db_setup/migrations.js +57 -0
- package/lib/plugins_core/db_setup/validation.d.ts +16 -0
- package/lib/plugins_core/db_setup/validation.js +31 -0
- package/lib/plugins_core/db_setup/validation.test.d.ts +1 -0
- package/lib/plugins_core/db_setup/validation.test.js +98 -0
- package/lib/steps/get.js +2 -0
- package/lib/types/config/netlify_config.d.ts +10 -0
- package/package.json +3 -3
package/lib/core/constants.d.ts
CHANGED
|
@@ -37,11 +37,20 @@ export interface NetlifyPluginConstants {
|
|
|
37
37
|
* the directory where built Edge Functions are placed before deployment. Its value is always defined, but the target might not have been created yet.
|
|
38
38
|
*/
|
|
39
39
|
EDGE_FUNCTIONS_DIST: string;
|
|
40
|
+
/**
|
|
41
|
+
* the directory where database migrations are placed before deployment.
|
|
42
|
+
*/
|
|
43
|
+
DB_MIGRATIONS_DIST?: string;
|
|
40
44
|
/**
|
|
41
45
|
* the directory where Edge Functions source code lives.
|
|
42
46
|
* `undefined` if no `netlify/edge-functions` directory exists.
|
|
43
47
|
*/
|
|
44
48
|
EDGE_FUNCTIONS_SRC?: string;
|
|
49
|
+
/**
|
|
50
|
+
* the directory where database migration source files live.
|
|
51
|
+
* `undefined` if no `netlify/db/migrations` directory exists and if not specified by the user.
|
|
52
|
+
*/
|
|
53
|
+
DB_MIGRATIONS_SRC?: string;
|
|
45
54
|
/**
|
|
46
55
|
* boolean indicating whether the build was [run locally](https://docs.netlify.com/cli/get-started/#run-builds-locally) or on Netlify
|
|
47
56
|
*/
|
|
@@ -91,7 +100,7 @@ export declare const getConstants: ({ configPath, buildDir, packagePath, functio
|
|
|
91
100
|
token: any;
|
|
92
101
|
mode: any;
|
|
93
102
|
}) => Promise<NetlifyPluginConstants>;
|
|
94
|
-
export declare const addMutableConstants: ({ constants, buildDir, netlifyConfig: { build: { publish, edge_functions: edgeFunctions }, functionsDirectory, }, }: {
|
|
103
|
+
export declare const addMutableConstants: ({ constants, buildDir, netlifyConfig: { build: { publish, edge_functions: edgeFunctions }, functionsDirectory, db, }, }: {
|
|
95
104
|
constants: any;
|
|
96
105
|
buildDir: any;
|
|
97
106
|
netlifyConfig: {
|
|
@@ -100,6 +109,7 @@ export declare const addMutableConstants: ({ constants, buildDir, netlifyConfig:
|
|
|
100
109
|
edge_functions: any;
|
|
101
110
|
};
|
|
102
111
|
functionsDirectory: any;
|
|
112
|
+
db: any;
|
|
103
113
|
};
|
|
104
114
|
}) => Promise<{
|
|
105
115
|
[k: string]: string | boolean | undefined;
|
package/lib/core/constants.js
CHANGED
|
@@ -41,15 +41,18 @@ export const getConstants = async function ({ configPath, buildDir, packagePath,
|
|
|
41
41
|
// The directory where internal Edge Functions (i.e. generated programmatically
|
|
42
42
|
// via plugins or others) live
|
|
43
43
|
INTERNAL_EDGE_FUNCTIONS_SRC: join(buildDir, packagePath || '', INTERNAL_EDGE_FUNCTIONS_SRC),
|
|
44
|
+
// The directory where database migrations are placed before deployment
|
|
45
|
+
DB_MIGRATIONS_DIST: join(buildDir, packagePath || '', DB_MIGRATIONS_DIST),
|
|
44
46
|
};
|
|
45
47
|
return (await addMutableConstants({ constants, buildDir, netlifyConfig }));
|
|
46
48
|
};
|
|
47
49
|
const INTERNAL_EDGE_FUNCTIONS_SRC = '.netlify/edge-functions';
|
|
48
50
|
const INTERNAL_FUNCTIONS_SRC = '.netlify/functions-internal';
|
|
51
|
+
const DB_MIGRATIONS_DIST = '.netlify/internal/db/migrations';
|
|
49
52
|
// Retrieve constants which might change during the build if a plugin modifies
|
|
50
53
|
// `netlifyConfig` or creates some default directories.
|
|
51
54
|
// Unlike readonly constants, this is called again before each build step.
|
|
52
|
-
export const addMutableConstants = async function ({ constants, buildDir, netlifyConfig: { build: { publish, edge_functions: edgeFunctions }, functionsDirectory, }, }) {
|
|
55
|
+
export const addMutableConstants = async function ({ constants, buildDir, netlifyConfig: { build: { publish, edge_functions: edgeFunctions }, functionsDirectory, db, }, }) {
|
|
53
56
|
const constantsA = {
|
|
54
57
|
...constants,
|
|
55
58
|
// Directory that contains the deploy-ready HTML files and assets generated by the build
|
|
@@ -58,6 +61,8 @@ export const addMutableConstants = async function ({ constants, buildDir, netlif
|
|
|
58
61
|
FUNCTIONS_SRC: functionsDirectory,
|
|
59
62
|
// The directory where Edge Functions source code lives
|
|
60
63
|
EDGE_FUNCTIONS_SRC: edgeFunctions,
|
|
64
|
+
// The directory where database migration source files live
|
|
65
|
+
DB_MIGRATIONS_SRC: db?.migrations?.path,
|
|
61
66
|
};
|
|
62
67
|
const constantsB = await addDefaultConstants(constantsA, buildDir);
|
|
63
68
|
const constantsC = normalizeConstantsPaths(constantsB, buildDir);
|
|
@@ -79,6 +84,7 @@ const DEFAULT_PATHS = [
|
|
|
79
84
|
{ constantName: 'FUNCTIONS_SRC', defaultPath: 'netlify-automatic-functions' },
|
|
80
85
|
{ constantName: 'FUNCTIONS_SRC', defaultPath: 'netlify/functions' },
|
|
81
86
|
{ constantName: 'EDGE_FUNCTIONS_SRC', defaultPath: 'netlify/edge-functions' },
|
|
87
|
+
{ constantName: 'DB_MIGRATIONS_SRC', defaultPath: 'netlify/db/migrations' },
|
|
82
88
|
];
|
|
83
89
|
const addDefaultConstant = async function ({ constants, constantName, defaultPath, buildDir }) {
|
|
84
90
|
// Configuration paths are relative to the build directory.
|
|
@@ -118,7 +124,9 @@ const CONSTANT_PATHS = new Set([
|
|
|
118
124
|
'FUNCTIONS_DIST',
|
|
119
125
|
'INTERNAL_EDGE_FUNCTIONS_SRC',
|
|
120
126
|
'INTERNAL_FUNCTIONS_SRC',
|
|
127
|
+
'DB_MIGRATIONS_DIST',
|
|
121
128
|
'EDGE_FUNCTIONS_DIST',
|
|
122
129
|
'EDGE_FUNCTIONS_SRC',
|
|
130
|
+
'DB_MIGRATIONS_SRC',
|
|
123
131
|
'CACHE_DIR',
|
|
124
132
|
]);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { copyFile, mkdir, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { pathExists } from 'path-exists';
|
|
4
|
+
import { validateMigrationDirs, formatValidationErrors } from './validation.js';
|
|
5
|
+
const condition = async ({ featureFlags, constants, buildDir }) => {
|
|
6
|
+
if (!featureFlags?.netlify_build_db_setup) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const srcDir = constants.DB_MIGRATIONS_SRC;
|
|
10
|
+
if (!srcDir) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return pathExists(resolve(buildDir, srcDir));
|
|
14
|
+
};
|
|
15
|
+
const coreStep = async ({ constants, buildDir, systemLog }) => {
|
|
16
|
+
const srcDir = resolve(buildDir, constants.DB_MIGRATIONS_SRC);
|
|
17
|
+
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) {
|
|
27
|
+
systemLog('No migration directories found, skipping copy.');
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
const existingSqlFiles = new Set();
|
|
31
|
+
for (const dirName of dirNames) {
|
|
32
|
+
const sqlPath = join(srcDir, dirName, 'migration.sql');
|
|
33
|
+
if (await pathExists(sqlPath)) {
|
|
34
|
+
existingSqlFiles.add(dirName);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const result = validateMigrationDirs(dirNames, existingSqlFiles);
|
|
38
|
+
if (!result.valid) {
|
|
39
|
+
const message = formatValidationErrors(result.errors);
|
|
40
|
+
throw new Error(message);
|
|
41
|
+
}
|
|
42
|
+
for (const dirName of result.dirs) {
|
|
43
|
+
const migrationDestDir = join(destDir, dirName);
|
|
44
|
+
await mkdir(migrationDestDir, { recursive: true });
|
|
45
|
+
await copyFile(join(srcDir, dirName, 'migration.sql'), join(migrationDestDir, 'migration.sql'));
|
|
46
|
+
}
|
|
47
|
+
systemLog(`Copied ${String(result.dirs.length)} migration(s) to ${destDir}`);
|
|
48
|
+
return {};
|
|
49
|
+
};
|
|
50
|
+
export const copyDbMigrations = {
|
|
51
|
+
event: 'onBuild',
|
|
52
|
+
coreStep,
|
|
53
|
+
coreStepId: 'db_migrations_copy',
|
|
54
|
+
coreStepName: 'Netlify DB migrations',
|
|
55
|
+
coreStepDescription: () => 'Copy database migrations to internal directory',
|
|
56
|
+
condition,
|
|
57
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const MIGRATION_DIR_PATTERN: RegExp;
|
|
2
|
+
interface ValidationError {
|
|
3
|
+
type: 'invalid_dir_name' | 'missing_sql_file';
|
|
4
|
+
dirName: string;
|
|
5
|
+
}
|
|
6
|
+
interface ValidationResult {
|
|
7
|
+
valid: true;
|
|
8
|
+
dirs: string[];
|
|
9
|
+
}
|
|
10
|
+
interface ValidationFailure {
|
|
11
|
+
valid: false;
|
|
12
|
+
errors: ValidationError[];
|
|
13
|
+
}
|
|
14
|
+
export declare const validateMigrationDirs: (dirNames: string[], existingSqlFiles: Set<string>) => ValidationResult | ValidationFailure;
|
|
15
|
+
export declare const formatValidationErrors: (errors: ValidationError[]) => string;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const MIGRATION_DIR_PATTERN = /^\d{10}_[a-z0-9-]+$/;
|
|
2
|
+
export const validateMigrationDirs = (dirNames, existingSqlFiles) => {
|
|
3
|
+
const errors = [];
|
|
4
|
+
for (const dirName of dirNames) {
|
|
5
|
+
if (!MIGRATION_DIR_PATTERN.test(dirName)) {
|
|
6
|
+
errors.push({ type: 'invalid_dir_name', dirName });
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
if (!existingSqlFiles.has(dirName)) {
|
|
10
|
+
errors.push({ type: 'missing_sql_file', dirName });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (errors.length > 0) {
|
|
14
|
+
return { valid: false, errors };
|
|
15
|
+
}
|
|
16
|
+
const sorted = [...dirNames].sort();
|
|
17
|
+
return { valid: true, dirs: sorted };
|
|
18
|
+
};
|
|
19
|
+
export const formatValidationErrors = (errors) => {
|
|
20
|
+
const lines = errors.map((error) => {
|
|
21
|
+
if (error.type === 'invalid_dir_name') {
|
|
22
|
+
return ` - "${error.dirName}" does not match the required pattern "<Unix-timestamp>_<slug>" (e.g. "1700000000_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.`;
|
|
29
|
+
});
|
|
30
|
+
return `Database migration validation failed:\n${lines.join('\n')}`;
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { MIGRATION_DIR_PATTERN, validateMigrationDirs, formatValidationErrors } from './validation.js';
|
|
3
|
+
describe('MIGRATION_DIR_PATTERN', () => {
|
|
4
|
+
const validNames = [
|
|
5
|
+
'1700000000_create-users',
|
|
6
|
+
'1700000001_add-posts',
|
|
7
|
+
'0000000000_init',
|
|
8
|
+
'9999999999_z',
|
|
9
|
+
'1700000000_a',
|
|
10
|
+
'1700000000_abc-def-123',
|
|
11
|
+
];
|
|
12
|
+
test.each(validNames)('matches valid name: %s', (name) => {
|
|
13
|
+
expect(MIGRATION_DIR_PATTERN.test(name)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
const invalidNames = [
|
|
16
|
+
{ name: '170000000_short-ts', reason: '9-digit timestamp' },
|
|
17
|
+
{ name: '17000000000_long-ts', reason: '11-digit timestamp' },
|
|
18
|
+
{ name: '1700000000_CAPS', reason: 'uppercase letters' },
|
|
19
|
+
{ name: '1700000000_under_score', reason: 'underscores in slug' },
|
|
20
|
+
{ name: 'no-timestamp', reason: 'no timestamp prefix' },
|
|
21
|
+
{ name: '1700000000_', reason: 'empty slug' },
|
|
22
|
+
{ name: '1700000000', reason: 'missing underscore and slug' },
|
|
23
|
+
{ name: '_create-users', reason: 'missing timestamp' },
|
|
24
|
+
{ name: '1700000000_hello world', reason: 'spaces in slug' },
|
|
25
|
+
{ name: '1700000000_special!char', reason: 'special characters in slug' },
|
|
26
|
+
];
|
|
27
|
+
test.each(invalidNames)('rejects invalid name: $name ($reason)', ({ name }) => {
|
|
28
|
+
expect(MIGRATION_DIR_PATTERN.test(name)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('validateMigrationDirs', () => {
|
|
32
|
+
test('returns valid result with sorted dirs when all dirs are valid', () => {
|
|
33
|
+
const dirNames = ['1700000001_add-posts', '1700000000_create-users'];
|
|
34
|
+
const existingSqlFiles = new Set(['1700000000_create-users', '1700000001_add-posts']);
|
|
35
|
+
const result = validateMigrationDirs(dirNames, existingSqlFiles);
|
|
36
|
+
expect(result.valid).toBe(true);
|
|
37
|
+
if (result.valid) {
|
|
38
|
+
expect(result.dirs).toEqual(['1700000000_create-users', '1700000001_add-posts']);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
test('returns error for invalid directory names', () => {
|
|
42
|
+
const dirNames = ['bad-name', '1700000000_create-users'];
|
|
43
|
+
const existingSqlFiles = new Set(['1700000000_create-users']);
|
|
44
|
+
const result = validateMigrationDirs(dirNames, existingSqlFiles);
|
|
45
|
+
expect(result.valid).toBe(false);
|
|
46
|
+
if (!result.valid) {
|
|
47
|
+
expect(result.errors).toEqual([{ type: 'invalid_dir_name', dirName: 'bad-name' }]);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
test('returns error for missing migration.sql files', () => {
|
|
51
|
+
const dirNames = ['1700000000_create-users', '1700000001_add-posts'];
|
|
52
|
+
const existingSqlFiles = new Set(['1700000000_create-users']);
|
|
53
|
+
const result = validateMigrationDirs(dirNames, existingSqlFiles);
|
|
54
|
+
expect(result.valid).toBe(false);
|
|
55
|
+
if (!result.valid) {
|
|
56
|
+
expect(result.errors).toEqual([{ type: 'missing_sql_file', dirName: '1700000001_add-posts' }]);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
test('reports both invalid names and missing files', () => {
|
|
60
|
+
const dirNames = ['bad-name', '1700000001_add-posts'];
|
|
61
|
+
const existingSqlFiles = new Set();
|
|
62
|
+
const result = validateMigrationDirs(dirNames, existingSqlFiles);
|
|
63
|
+
expect(result.valid).toBe(false);
|
|
64
|
+
if (!result.valid) {
|
|
65
|
+
expect(result.errors).toHaveLength(2);
|
|
66
|
+
expect(result.errors[0]).toEqual({ type: 'invalid_dir_name', dirName: 'bad-name' });
|
|
67
|
+
expect(result.errors[1]).toEqual({ type: 'missing_sql_file', dirName: '1700000001_add-posts' });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
test('returns valid result for empty input', () => {
|
|
71
|
+
const result = validateMigrationDirs([], new Set());
|
|
72
|
+
expect(result.valid).toBe(true);
|
|
73
|
+
if (result.valid) {
|
|
74
|
+
expect(result.dirs).toEqual([]);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('formatValidationErrors', () => {
|
|
79
|
+
test('formats invalid_dir_name errors', () => {
|
|
80
|
+
const message = formatValidationErrors([{ type: 'invalid_dir_name', dirName: 'bad-name' }]);
|
|
81
|
+
expect(message).toContain('Database migration validation failed');
|
|
82
|
+
expect(message).toContain('"bad-name"');
|
|
83
|
+
expect(message).toContain('<Unix-timestamp>_<slug>');
|
|
84
|
+
});
|
|
85
|
+
test('formats missing_sql_file errors', () => {
|
|
86
|
+
const message = formatValidationErrors([{ type: 'missing_sql_file', dirName: '1700000000_create-users' }]);
|
|
87
|
+
expect(message).toContain('Database migration validation failed');
|
|
88
|
+
expect(message).toContain('"1700000000_create-users/migration.sql" is missing');
|
|
89
|
+
});
|
|
90
|
+
test('formats multiple errors', () => {
|
|
91
|
+
const message = formatValidationErrors([
|
|
92
|
+
{ type: 'invalid_dir_name', dirName: 'bad-name' },
|
|
93
|
+
{ type: 'missing_sql_file', dirName: '1700000001_add-posts' },
|
|
94
|
+
]);
|
|
95
|
+
expect(message).toContain('"bad-name"');
|
|
96
|
+
expect(message).toContain('"1700000001_add-posts/migration.sql" is missing');
|
|
97
|
+
});
|
|
98
|
+
});
|
package/lib/steps/get.js
CHANGED
|
@@ -8,6 +8,7 @@ import { bundleEdgeFunctions } from '../plugins_core/edge_functions/index.js';
|
|
|
8
8
|
import { applyDeployConfig } from '../plugins_core/frameworks_api/index.js';
|
|
9
9
|
import { bundleFunctions } from '../plugins_core/functions/index.js';
|
|
10
10
|
import { dbSetup } from '../plugins_core/db_setup/index.js';
|
|
11
|
+
import { copyDbMigrations } from '../plugins_core/db_setup/migrations.js';
|
|
11
12
|
import { preCleanup } from '../plugins_core/pre_cleanup/index.js';
|
|
12
13
|
import { preDevCleanup } from '../plugins_core/pre_dev_cleanup/index.js';
|
|
13
14
|
import { saveArtifacts } from '../plugins_core/save_artifacts/index.js';
|
|
@@ -71,6 +72,7 @@ const addCoreSteps = function (steps) {
|
|
|
71
72
|
...steps,
|
|
72
73
|
bundleFunctions,
|
|
73
74
|
bundleEdgeFunctions,
|
|
75
|
+
copyDbMigrations,
|
|
74
76
|
scanForSecrets,
|
|
75
77
|
uploadBlobs,
|
|
76
78
|
deploySite,
|
|
@@ -25,6 +25,12 @@ interface NetlifyPlugin {
|
|
|
25
25
|
package: string;
|
|
26
26
|
inputs: PluginInputs;
|
|
27
27
|
}
|
|
28
|
+
interface DbMigrationsConfig {
|
|
29
|
+
path?: string;
|
|
30
|
+
}
|
|
31
|
+
interface DbConfig {
|
|
32
|
+
migrations?: DbMigrationsConfig;
|
|
33
|
+
}
|
|
28
34
|
interface ImagesConfig {
|
|
29
35
|
remote_images: string[];
|
|
30
36
|
}
|
|
@@ -51,5 +57,9 @@ export interface NetlifyConfig {
|
|
|
51
57
|
* object with options for image transforms
|
|
52
58
|
*/
|
|
53
59
|
images: ImagesConfig;
|
|
60
|
+
/**
|
|
61
|
+
* object with options for database configuration
|
|
62
|
+
*/
|
|
63
|
+
db?: DbConfig;
|
|
54
64
|
}
|
|
55
65
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/build",
|
|
3
|
-
"version": "35.
|
|
3
|
+
"version": "35.7.0",
|
|
4
4
|
"description": "Netlify build module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./lib/index.js",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@bugsnag/js": "^8.0.0",
|
|
70
70
|
"@netlify/blobs": "^10.4.4",
|
|
71
71
|
"@netlify/cache-utils": "^6.0.4",
|
|
72
|
-
"@netlify/config": "^24.
|
|
72
|
+
"@netlify/config": "^24.4.0",
|
|
73
73
|
"@netlify/edge-bundler": "14.9.8",
|
|
74
74
|
"@netlify/functions-utils": "^6.2.21",
|
|
75
75
|
"@netlify/git-utils": "^6.0.3",
|
|
@@ -152,5 +152,5 @@
|
|
|
152
152
|
"engines": {
|
|
153
153
|
"node": ">=18.14.0"
|
|
154
154
|
},
|
|
155
|
-
"gitHead": "
|
|
155
|
+
"gitHead": "7afb85bef616ee25b81b3bd5728333dbbd076725"
|
|
156
156
|
}
|