@netlify/build 36.2.4 → 36.3.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/plugins/child/diff.d.ts +8 -2
- package/lib/plugins/child/diff.js +8 -8
- package/lib/plugins/child/run.d.ts +1 -1
- package/lib/plugins_core/frameworks_api/index.js +1 -0
- package/lib/plugins_core/spa_fallback/index.d.ts +2 -0
- package/lib/plugins_core/spa_fallback/index.js +51 -0
- package/lib/steps/get.js +2 -0
- package/lib/types/config/netlify_config.d.ts +4 -0
- package/package.json +6 -6
- package/lib/plugins/compatibility.test.d.ts +0 -1
- package/lib/plugins/compatibility.test.js +0 -298
- package/lib/plugins_core/db_setup/validation.test.d.ts +0 -1
- package/lib/plugins_core/db_setup/validation.test.js +0 -226
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export type ConfigMutation = {
|
|
2
|
+
keys: string[];
|
|
3
|
+
keysString: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
event: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function cloneNetlifyConfig<T>(netlifyConfig: T): T;
|
|
8
|
+
export declare function getConfigMutations(netlifyConfig: object, netlifyConfigCopy: object, event: string): ConfigMutation[];
|
|
@@ -3,9 +3,9 @@ import isPlainObj from 'is-plain-obj';
|
|
|
3
3
|
import rfdc from 'rfdc';
|
|
4
4
|
const clone = rfdc();
|
|
5
5
|
// Copy `netlifyConfig` so we can compare before/after mutating it
|
|
6
|
-
export
|
|
6
|
+
export function cloneNetlifyConfig(netlifyConfig) {
|
|
7
7
|
return clone(netlifyConfig);
|
|
8
|
-
}
|
|
8
|
+
}
|
|
9
9
|
// Diff `netlifyConfig` before and after mutating it to retrieve an array of
|
|
10
10
|
// `configMutations` objects.
|
|
11
11
|
// We need to keep track of the changes on `netlifyConfig` so they can be
|
|
@@ -14,13 +14,13 @@ export const cloneNetlifyConfig = function (netlifyConfig) {
|
|
|
14
14
|
// - Apply the change to `netlifyConfig` in the parent process so it can
|
|
15
15
|
// run `@netlify/config` to normalize and validate the new values
|
|
16
16
|
// `configMutations` is passed to parent process as JSON
|
|
17
|
-
export
|
|
17
|
+
export function getConfigMutations(netlifyConfig, netlifyConfigCopy, event) {
|
|
18
18
|
const configMutations = diffObjects(netlifyConfig, netlifyConfigCopy, []);
|
|
19
19
|
return configMutations.map((configMutation) => getConfigMutation(configMutation, event));
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
// We only recurse over plain objects, not arrays. Which means array properties
|
|
22
22
|
// can only be modified all at once.
|
|
23
|
-
|
|
23
|
+
function diffObjects(objA, objB, parentKeys) {
|
|
24
24
|
const allKeys = [...new Set([...Object.keys(objA), ...Object.keys(objB)])];
|
|
25
25
|
return allKeys.flatMap((key) => {
|
|
26
26
|
const valueA = objA[key];
|
|
@@ -34,8 +34,8 @@ const diffObjects = function (objA, objB, parentKeys) {
|
|
|
34
34
|
}
|
|
35
35
|
return [{ keys, value: valueB }];
|
|
36
36
|
});
|
|
37
|
-
}
|
|
38
|
-
|
|
37
|
+
}
|
|
38
|
+
function getConfigMutation({ keys, value }, event) {
|
|
39
39
|
const serializedKeys = keys.map(String);
|
|
40
40
|
return {
|
|
41
41
|
keys: serializedKeys,
|
|
@@ -43,4 +43,4 @@ const getConfigMutation = function ({ keys, value }, event) {
|
|
|
43
43
|
value,
|
|
44
44
|
event,
|
|
45
45
|
};
|
|
46
|
-
}
|
|
46
|
+
}
|
|
@@ -15,7 +15,7 @@ export function run({ event, error, constants, envChanges, featureFlags, netlify
|
|
|
15
15
|
}): Promise<{
|
|
16
16
|
deployEnvVars: any[];
|
|
17
17
|
newEnvChanges: any;
|
|
18
|
-
configMutations:
|
|
18
|
+
configMutations: import("./diff.js").ConfigMutation[];
|
|
19
19
|
returnValue: {
|
|
20
20
|
generatedFunctions: any[];
|
|
21
21
|
} | undefined;
|
|
@@ -17,6 +17,7 @@ const ALLOWED_PROPERTIES = [
|
|
|
17
17
|
['headers'],
|
|
18
18
|
['images', 'remote_images'],
|
|
19
19
|
['redirects'],
|
|
20
|
+
['spa_fallback'],
|
|
20
21
|
];
|
|
21
22
|
// For array properties, any values set in this API will be merged with the
|
|
22
23
|
// main configuration file in such a way that user-defined values always take
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { logWarning } from '../../log/logger.js';
|
|
2
|
+
import { getConfigMutations } from '../../plugins/child/diff.js';
|
|
3
|
+
// The catch-all redirect that makes a single-page application's client-side
|
|
4
|
+
// router handle every path.
|
|
5
|
+
const SPA_FALLBACK_REDIRECT = {
|
|
6
|
+
from: '/*',
|
|
7
|
+
status: 200,
|
|
8
|
+
to: '/index.html',
|
|
9
|
+
};
|
|
10
|
+
function findCatchAllRedirect(redirects) {
|
|
11
|
+
return redirects.find((r) => r.from === '/*');
|
|
12
|
+
}
|
|
13
|
+
// A catch-all redirect only serves the single-page application the way ours
|
|
14
|
+
// would if it rewrites (rather than redirects) to the same destination.
|
|
15
|
+
function matchesSpaFallback(redirect) {
|
|
16
|
+
return redirect.to === SPA_FALLBACK_REDIRECT.to && redirect.status === SPA_FALLBACK_REDIRECT.status;
|
|
17
|
+
}
|
|
18
|
+
function coreStep(coreStepFunctionArgs) {
|
|
19
|
+
const { netlifyConfig, logs } = coreStepFunctionArgs;
|
|
20
|
+
if (!netlifyConfig.spa_fallback) {
|
|
21
|
+
return Promise.resolve({});
|
|
22
|
+
}
|
|
23
|
+
const existingCatchAll = findCatchAllRedirect(netlifyConfig.redirects);
|
|
24
|
+
if (existingCatchAll) {
|
|
25
|
+
// `spa_fallback` asked us to add a catch-all redirect, but the site
|
|
26
|
+
// already has one. If it doesn't already do what ours would, warn the
|
|
27
|
+
// user instead of silently overriding their explicit configuration.
|
|
28
|
+
if (!matchesSpaFallback(existingCatchAll)) {
|
|
29
|
+
logWarning(logs, `
|
|
30
|
+
Warning: "spa_fallback" is enabled, but a catch-all redirect ("/*") already exists that does not rewrite to "${SPA_FALLBACK_REDIRECT.to}" with a "${String(SPA_FALLBACK_REDIRECT.status)}" status, so Netlify did not add its own.
|
|
31
|
+
Please make sure your existing catch-all redirect correctly serves your single-page application.`);
|
|
32
|
+
}
|
|
33
|
+
return Promise.resolve({});
|
|
34
|
+
}
|
|
35
|
+
const newConfig = {
|
|
36
|
+
redirects: [...netlifyConfig.redirects, SPA_FALLBACK_REDIRECT],
|
|
37
|
+
};
|
|
38
|
+
const configMutations = getConfigMutations(netlifyConfig, {
|
|
39
|
+
...netlifyConfig,
|
|
40
|
+
...newConfig,
|
|
41
|
+
}, applySpaFallback.event);
|
|
42
|
+
return Promise.resolve({ configMutations });
|
|
43
|
+
}
|
|
44
|
+
export const applySpaFallback = {
|
|
45
|
+
coreStep,
|
|
46
|
+
coreStepDescription: () => '',
|
|
47
|
+
coreStepId: 'spa_fallback',
|
|
48
|
+
coreStepName: 'Applying SPA fallback redirect',
|
|
49
|
+
event: 'onPostBuild',
|
|
50
|
+
quiet: true,
|
|
51
|
+
};
|
package/lib/steps/get.js
CHANGED
|
@@ -13,6 +13,7 @@ import { preCleanup } from '../plugins_core/pre_cleanup/index.js';
|
|
|
13
13
|
import { preDevCleanup } from '../plugins_core/pre_dev_cleanup/index.js';
|
|
14
14
|
import { saveArtifacts } from '../plugins_core/save_artifacts/index.js';
|
|
15
15
|
import { scanForSecrets } from '../plugins_core/secrets_scanning/index.js';
|
|
16
|
+
import { applySpaFallback } from '../plugins_core/spa_fallback/index.js';
|
|
16
17
|
// Get all build steps
|
|
17
18
|
export const getSteps = function (steps, eventHandlers) {
|
|
18
19
|
const stepsA = addCoreSteps(steps);
|
|
@@ -72,6 +73,7 @@ const addCoreSteps = function (steps) {
|
|
|
72
73
|
bundleFunctions,
|
|
73
74
|
bundleEdgeFunctions,
|
|
74
75
|
copyDbMigrations,
|
|
76
|
+
applySpaFallback,
|
|
75
77
|
scanForSecrets,
|
|
76
78
|
uploadBlobs,
|
|
77
79
|
deploySite,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/build",
|
|
3
|
-
"version": "36.
|
|
3
|
+
"version": "36.3.0",
|
|
4
4
|
"description": "Netlify build module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./lib/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"prebuild": "rm -rf lib",
|
|
26
26
|
"postbuild": "npx cpy \"src/**/*.yml\" \"lib/\"",
|
|
27
|
-
"build": "tsc",
|
|
27
|
+
"build": "tsc --project tsconfig.build.json",
|
|
28
28
|
"test:types": "tsd",
|
|
29
29
|
"test": "ava && tsd && vitest run",
|
|
30
30
|
"test:dev": "ava -w",
|
|
@@ -69,14 +69,14 @@
|
|
|
69
69
|
"@bugsnag/js": "^8.0.0",
|
|
70
70
|
"@netlify/blobs": "^10.4.4",
|
|
71
71
|
"@netlify/cache-utils": "^7.1.1",
|
|
72
|
-
"@netlify/config": "^25.
|
|
72
|
+
"@netlify/config": "^25.2.0",
|
|
73
73
|
"@netlify/edge-bundler": "16.0.1",
|
|
74
|
-
"@netlify/functions-utils": "^7.1.
|
|
74
|
+
"@netlify/functions-utils": "^7.1.2",
|
|
75
75
|
"@netlify/git-utils": "^7.1.0",
|
|
76
76
|
"@netlify/opentelemetry-utils": "^3.1.0",
|
|
77
77
|
"@netlify/plugins-list": "^6.81.6",
|
|
78
78
|
"@netlify/run-utils": "^7.1.0",
|
|
79
|
-
"@netlify/zip-it-and-ship-it": "15.3.
|
|
79
|
+
"@netlify/zip-it-and-ship-it": "15.3.2",
|
|
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": ">=22.12.0"
|
|
154
154
|
},
|
|
155
|
-
"gitHead": "
|
|
155
|
+
"gitHead": "f34dd81fc3bc8925aabe5cf9c1cdce4e8eebc4e0"
|
|
156
156
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { getExpectedVersion } from './compatibility.js';
|
|
3
|
-
const noopSystemLog = () => {
|
|
4
|
-
// no-op
|
|
5
|
-
};
|
|
6
|
-
describe(`getExpectedVersion`, () => {
|
|
7
|
-
test('should ignore the new major version if the version is pinned', async () => {
|
|
8
|
-
const versions = [
|
|
9
|
-
{ version: '5.0.0', conditions: [] },
|
|
10
|
-
{ version: '4.41.2', conditions: [] },
|
|
11
|
-
];
|
|
12
|
-
const { version } = await getExpectedVersion({
|
|
13
|
-
versions,
|
|
14
|
-
nodeVersion: '18.19.0',
|
|
15
|
-
packageJson: {},
|
|
16
|
-
packageName: '@netlify/cool-plugin',
|
|
17
|
-
buildDir: '/some/path',
|
|
18
|
-
pinnedVersion: '4',
|
|
19
|
-
systemLog: noopSystemLog,
|
|
20
|
-
});
|
|
21
|
-
expect(version).toBe('4.41.2');
|
|
22
|
-
});
|
|
23
|
-
test('matches prerelease versions', async () => {
|
|
24
|
-
const versions = [
|
|
25
|
-
{ version: '5.0.0', conditions: [] },
|
|
26
|
-
{ version: '4.42.0-alpha.1', conditions: [] },
|
|
27
|
-
{ version: '4.41.2', conditions: [] },
|
|
28
|
-
];
|
|
29
|
-
const { version: version1 } = await getExpectedVersion({
|
|
30
|
-
versions,
|
|
31
|
-
nodeVersion: '18.19.0',
|
|
32
|
-
packageJson: {},
|
|
33
|
-
packageName: '@netlify/cool-plugin',
|
|
34
|
-
buildDir: '/some/path',
|
|
35
|
-
systemLog: noopSystemLog,
|
|
36
|
-
});
|
|
37
|
-
const { version: version2 } = await getExpectedVersion({
|
|
38
|
-
versions,
|
|
39
|
-
nodeVersion: '18.19.0',
|
|
40
|
-
packageJson: {},
|
|
41
|
-
packageName: '@netlify/cool-plugin',
|
|
42
|
-
buildDir: '/some/path',
|
|
43
|
-
pinnedVersion: '4',
|
|
44
|
-
systemLog: noopSystemLog,
|
|
45
|
-
});
|
|
46
|
-
expect(version1).toBe('5.0.0');
|
|
47
|
-
expect(version2).toBe('4.42.0-alpha.1');
|
|
48
|
-
});
|
|
49
|
-
test('should retrieve a new major version if the overridePinnedVersion is specified', async () => {
|
|
50
|
-
const versions = [
|
|
51
|
-
{ version: '5.0.0', conditions: [], overridePinnedVersion: '>=4.0.0' },
|
|
52
|
-
{ version: '4.41.2', conditions: [] },
|
|
53
|
-
];
|
|
54
|
-
const { version } = await getExpectedVersion({
|
|
55
|
-
versions,
|
|
56
|
-
nodeVersion: '18.19.0',
|
|
57
|
-
packageJson: {},
|
|
58
|
-
packageName: '@netlify/cool-plugin',
|
|
59
|
-
buildDir: '/some/path',
|
|
60
|
-
pinnedVersion: '4',
|
|
61
|
-
systemLog: noopSystemLog,
|
|
62
|
-
});
|
|
63
|
-
expect(version).toBe('5.0.0');
|
|
64
|
-
});
|
|
65
|
-
test('should retrieve the plugin based on the condition of a nodeVersion', async () => {
|
|
66
|
-
const versions = [
|
|
67
|
-
{
|
|
68
|
-
version: '4.42.0',
|
|
69
|
-
conditions: [{ type: 'nodeVersion', condition: '>=18.0.0' }],
|
|
70
|
-
},
|
|
71
|
-
{ version: '4.41.2', conditions: [] },
|
|
72
|
-
];
|
|
73
|
-
const { version } = await getExpectedVersion({
|
|
74
|
-
versions,
|
|
75
|
-
nodeVersion: '17.19.0',
|
|
76
|
-
packageJson: {},
|
|
77
|
-
packageName: '@netlify/cool-plugin',
|
|
78
|
-
buildDir: '/some/path',
|
|
79
|
-
pinnedVersion: '4',
|
|
80
|
-
systemLog: noopSystemLog,
|
|
81
|
-
});
|
|
82
|
-
expect(version).toBe('4.41.2');
|
|
83
|
-
});
|
|
84
|
-
test('should retrieve the plugin based on conditions and feature flag due to pinned version', async () => {
|
|
85
|
-
const versions = [
|
|
86
|
-
{
|
|
87
|
-
version: '5.0.0-beta.1',
|
|
88
|
-
conditions: [
|
|
89
|
-
{ type: 'nodeVersion', condition: '>= 18.0.0' },
|
|
90
|
-
{ type: 'siteDependencies', condition: { next: '>=13.5.0' } },
|
|
91
|
-
],
|
|
92
|
-
overridePinnedVersion: '>=4.0.0',
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
version: '4.42.0',
|
|
96
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '>=10.0.9' } }],
|
|
97
|
-
},
|
|
98
|
-
{ version: '4.41.2', conditions: [] },
|
|
99
|
-
{
|
|
100
|
-
version: '3.9.2',
|
|
101
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
|
|
102
|
-
},
|
|
103
|
-
];
|
|
104
|
-
const { version: version1 } = await getExpectedVersion({
|
|
105
|
-
versions,
|
|
106
|
-
nodeVersion: '17.19.0',
|
|
107
|
-
packageJson: { dependencies: { next: '10.0.8' } },
|
|
108
|
-
packageName: '@netlify/cool-plugin',
|
|
109
|
-
buildDir: '/some/path',
|
|
110
|
-
pinnedVersion: '3',
|
|
111
|
-
systemLog: noopSystemLog,
|
|
112
|
-
});
|
|
113
|
-
expect(version1).toBe('3.9.2');
|
|
114
|
-
const { version: version2 } = await getExpectedVersion({
|
|
115
|
-
versions,
|
|
116
|
-
nodeVersion: '17.19.0',
|
|
117
|
-
packageJson: { dependencies: { next: '11.0.0' } },
|
|
118
|
-
packageName: '@netlify/cool-plugin',
|
|
119
|
-
buildDir: '/some/path',
|
|
120
|
-
pinnedVersion: '4',
|
|
121
|
-
systemLog: noopSystemLog,
|
|
122
|
-
});
|
|
123
|
-
expect(version2).toBe('4.42.0');
|
|
124
|
-
const { version: version3 } = await getExpectedVersion({
|
|
125
|
-
versions,
|
|
126
|
-
nodeVersion: '18.19.0',
|
|
127
|
-
packageJson: { dependencies: { next: '13.5.0' } },
|
|
128
|
-
packageName: '@netlify/cool-plugin',
|
|
129
|
-
buildDir: '/some/path',
|
|
130
|
-
pinnedVersion: '4',
|
|
131
|
-
systemLog: noopSystemLog,
|
|
132
|
-
});
|
|
133
|
-
expect(version3).toBe('5.0.0-beta.1');
|
|
134
|
-
});
|
|
135
|
-
test('should work with rc versions inside the siteDependencies constraints', async () => {
|
|
136
|
-
const versions = [
|
|
137
|
-
{
|
|
138
|
-
version: '5.0.0-beta.1',
|
|
139
|
-
conditions: [
|
|
140
|
-
{ type: 'nodeVersion', condition: '>= 18.0.0' },
|
|
141
|
-
{ type: 'siteDependencies', condition: { next: '>=13.5.0' } },
|
|
142
|
-
],
|
|
143
|
-
overridePinnedVersion: '>=4.0.0',
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
version: '4.42.0',
|
|
147
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '>=10.0.9' } }],
|
|
148
|
-
},
|
|
149
|
-
{ version: '4.41.2', conditions: [] },
|
|
150
|
-
{
|
|
151
|
-
version: '3.9.2',
|
|
152
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
|
|
153
|
-
},
|
|
154
|
-
];
|
|
155
|
-
const { version } = await getExpectedVersion({
|
|
156
|
-
versions,
|
|
157
|
-
nodeVersion: '18.19.0',
|
|
158
|
-
packageJson: { dependencies: { next: '14.1.1-canary.36' } },
|
|
159
|
-
packageName: '@netlify/cool-plugin',
|
|
160
|
-
buildDir: '/some/path',
|
|
161
|
-
pinnedVersion: '4',
|
|
162
|
-
systemLog: noopSystemLog,
|
|
163
|
-
});
|
|
164
|
-
expect(version).toBe('5.0.0-beta.1');
|
|
165
|
-
});
|
|
166
|
-
test('should retrieve the plugin based on conditions and feature flag due to pinned version', async () => {
|
|
167
|
-
const versions = [
|
|
168
|
-
{
|
|
169
|
-
version: '5.0.0-beta.1',
|
|
170
|
-
conditions: [
|
|
171
|
-
{ type: 'nodeVersion', condition: '>= 18.0.0' },
|
|
172
|
-
{ type: 'siteDependencies', condition: { next: '>=13.5.0' } },
|
|
173
|
-
],
|
|
174
|
-
overridePinnedVersion: '>=4.0.0',
|
|
175
|
-
},
|
|
176
|
-
{ version: '4.41.2', conditions: [] },
|
|
177
|
-
{
|
|
178
|
-
version: '3.9.2',
|
|
179
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
|
|
180
|
-
},
|
|
181
|
-
];
|
|
182
|
-
const { version: version1 } = await getExpectedVersion({
|
|
183
|
-
versions,
|
|
184
|
-
nodeVersion: '20.0.0',
|
|
185
|
-
packageJson: { dependencies: { next: '14.0.0' } },
|
|
186
|
-
packageName: '@netlify/cool-plugin',
|
|
187
|
-
buildDir: '/some/path',
|
|
188
|
-
pinnedVersion: '4',
|
|
189
|
-
systemLog: noopSystemLog,
|
|
190
|
-
});
|
|
191
|
-
expect(version1).toBe('5.0.0-beta.1');
|
|
192
|
-
// out of range
|
|
193
|
-
const { version: version2 } = await getExpectedVersion({
|
|
194
|
-
versions,
|
|
195
|
-
nodeVersion: '20.0.0',
|
|
196
|
-
packageJson: { dependencies: { next: '13.0.0' } },
|
|
197
|
-
packageName: '@netlify/cool-plugin',
|
|
198
|
-
buildDir: '/some/path',
|
|
199
|
-
pinnedVersion: '4',
|
|
200
|
-
systemLog: noopSystemLog,
|
|
201
|
-
});
|
|
202
|
-
expect(version2).toBe('4.41.2');
|
|
203
|
-
});
|
|
204
|
-
test('matches the first entry that satisfies the constraints, even if it also matches another entry further down with more specific constraints', async () => {
|
|
205
|
-
const versions = [
|
|
206
|
-
{ version: '4.41.2', conditions: [] },
|
|
207
|
-
{
|
|
208
|
-
version: '5.0.0-beta.1',
|
|
209
|
-
conditions: [
|
|
210
|
-
{ type: 'nodeVersion', condition: '>= 18.0.0' },
|
|
211
|
-
{ type: 'siteDependencies', condition: { next: '>=13.5.0' } },
|
|
212
|
-
],
|
|
213
|
-
overridePinnedVersion: '>=4.0.0',
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
version: '3.9.2',
|
|
217
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
|
|
218
|
-
},
|
|
219
|
-
];
|
|
220
|
-
const { version } = await getExpectedVersion({
|
|
221
|
-
versions,
|
|
222
|
-
nodeVersion: '20.0.0',
|
|
223
|
-
packageJson: { dependencies: { next: '14.0.0' } },
|
|
224
|
-
packageName: '@netlify/cool-plugin',
|
|
225
|
-
buildDir: '/some/path',
|
|
226
|
-
pinnedVersion: '4',
|
|
227
|
-
systemLog: noopSystemLog,
|
|
228
|
-
});
|
|
229
|
-
expect(version).toBe('4.41.2');
|
|
230
|
-
});
|
|
231
|
-
test('if no pinned version is set, it matches the first version regardless of whether its requirements match the conditions (legacy behavior)', async () => {
|
|
232
|
-
const versions = [
|
|
233
|
-
{
|
|
234
|
-
version: '5.0.0-beta.1',
|
|
235
|
-
conditions: [
|
|
236
|
-
{ type: 'nodeVersion', condition: '>= 18.0.0' },
|
|
237
|
-
{ type: 'siteDependencies', condition: { next: '>=13.5.0' } },
|
|
238
|
-
],
|
|
239
|
-
overridePinnedVersion: '>=4.0.0',
|
|
240
|
-
},
|
|
241
|
-
{ version: '4.41.2', conditions: [] },
|
|
242
|
-
{
|
|
243
|
-
version: '3.9.2',
|
|
244
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
|
|
245
|
-
},
|
|
246
|
-
];
|
|
247
|
-
const logMessages = [];
|
|
248
|
-
const { version } = await getExpectedVersion({
|
|
249
|
-
versions,
|
|
250
|
-
nodeVersion: '20.0.0',
|
|
251
|
-
packageJson: { dependencies: { next: '12.0.0' } },
|
|
252
|
-
packageName: '@netlify/cool-plugin',
|
|
253
|
-
buildDir: '/some/path',
|
|
254
|
-
systemLog: (message) => {
|
|
255
|
-
logMessages.push(message);
|
|
256
|
-
},
|
|
257
|
-
authoritative: true,
|
|
258
|
-
});
|
|
259
|
-
expect(logMessages.length).toBe(1);
|
|
260
|
-
expect(logMessages[0]).toBe(`Detected mismatch in selected version for plugin '@netlify/cool-plugin': used legacy version '5.0.0-beta.1' over new version '4.41.2'`);
|
|
261
|
-
expect(version).toBe('5.0.0-beta.1');
|
|
262
|
-
});
|
|
263
|
-
test('if no pinned version is set, it matches the first version whose requirements match the conditions', async () => {
|
|
264
|
-
const versions = [
|
|
265
|
-
{
|
|
266
|
-
version: '5.0.0-beta.1',
|
|
267
|
-
conditions: [
|
|
268
|
-
{ type: 'nodeVersion', condition: '>= 18.0.0' },
|
|
269
|
-
{ type: 'siteDependencies', condition: { next: '>=13.5.0' } },
|
|
270
|
-
],
|
|
271
|
-
overridePinnedVersion: '>=4.0.0',
|
|
272
|
-
},
|
|
273
|
-
{ version: '4.41.2', conditions: [] },
|
|
274
|
-
{
|
|
275
|
-
version: '3.9.2',
|
|
276
|
-
conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
|
|
277
|
-
},
|
|
278
|
-
];
|
|
279
|
-
const logMessages = [];
|
|
280
|
-
const { version } = await getExpectedVersion({
|
|
281
|
-
versions,
|
|
282
|
-
nodeVersion: '20.0.0',
|
|
283
|
-
packageJson: { dependencies: { next: '12.0.0' } },
|
|
284
|
-
packageName: '@netlify/cool-plugin',
|
|
285
|
-
buildDir: '/some/path',
|
|
286
|
-
systemLog: (message) => {
|
|
287
|
-
logMessages.push(message);
|
|
288
|
-
},
|
|
289
|
-
featureFlags: {
|
|
290
|
-
netlify_build_updated_plugin_compatibility: true,
|
|
291
|
-
},
|
|
292
|
-
authoritative: true,
|
|
293
|
-
});
|
|
294
|
-
expect(logMessages.length).toBe(1);
|
|
295
|
-
expect(logMessages[0]).toBe(`Detected mismatch in selected version for plugin '@netlify/cool-plugin': used new version of '4.41.2' over legacy version '5.0.0-beta.1'`);
|
|
296
|
-
expect(version).toBe('4.41.2');
|
|
297
|
-
});
|
|
298
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { MIGRATION_DIR_PATTERN, MIGRATION_FILE_PATTERN, trackMigrationNumber, validateMigrations, 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
|
-
'001_create-users',
|
|
12
|
-
'1_init',
|
|
13
|
-
'0001_add-posts',
|
|
14
|
-
'42_z',
|
|
15
|
-
'1700000000_under_score',
|
|
16
|
-
'001_create_users_table',
|
|
17
|
-
'1_a_b_c',
|
|
18
|
-
];
|
|
19
|
-
test.each(validNames)('matches valid name: %s', (name) => {
|
|
20
|
-
expect(MIGRATION_DIR_PATTERN.test(name)).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
const invalidNames = [
|
|
23
|
-
{ name: '1700000000_CAPS', reason: 'uppercase letters' },
|
|
24
|
-
{ name: 'no-timestamp', reason: 'no numeric prefix' },
|
|
25
|
-
{ name: '1700000000_', reason: 'empty slug' },
|
|
26
|
-
{ name: '1700000000', reason: 'missing underscore and slug' },
|
|
27
|
-
{ name: '_create-users', reason: 'missing number prefix' },
|
|
28
|
-
{ name: '1700000000_hello world', reason: 'spaces in slug' },
|
|
29
|
-
{ name: '1700000000_special!char', reason: 'special characters in slug' },
|
|
30
|
-
];
|
|
31
|
-
test.each(invalidNames)('rejects invalid name: $name ($reason)', ({ name }) => {
|
|
32
|
-
expect(MIGRATION_DIR_PATTERN.test(name)).toBe(false);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
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('trackMigrationNumber', () => {
|
|
59
|
-
const cases = [
|
|
60
|
-
{ name: '001_create-users', expected: '1' },
|
|
61
|
-
{ name: '1_init', expected: '1' },
|
|
62
|
-
{ name: '0001_add-posts', expected: '1' },
|
|
63
|
-
{ name: '42_z', expected: '42' },
|
|
64
|
-
{ name: '1700000000_create-users', expected: '1700000000' },
|
|
65
|
-
{ name: '1700000000_create-users.sql', expected: '1700000000' },
|
|
66
|
-
{ name: '001_create-users.sql', expected: '1' },
|
|
67
|
-
{ name: '0000000000_init', expected: '0' },
|
|
68
|
-
];
|
|
69
|
-
test.each(cases)('tracks $name under key $expected', ({ name, expected }) => {
|
|
70
|
-
const map = new Map();
|
|
71
|
-
trackMigrationNumber(map, name);
|
|
72
|
-
expect(map.get(expected)).toEqual([name]);
|
|
73
|
-
});
|
|
74
|
-
test('groups names with the same migration number', () => {
|
|
75
|
-
const map = new Map();
|
|
76
|
-
trackMigrationNumber(map, '001_create-users');
|
|
77
|
-
trackMigrationNumber(map, '1_init');
|
|
78
|
-
expect(map.get('1')).toEqual(['001_create-users', '1_init']);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe('validateMigrations', () => {
|
|
82
|
-
test('returns valid result with sorted dirs when all dirs are valid', () => {
|
|
83
|
-
const dirNames = ['1700000001_add-posts', '1700000000_create-users'];
|
|
84
|
-
const existingSqlFiles = new Set(['1700000000_create-users', '1700000001_add-posts']);
|
|
85
|
-
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
86
|
-
expect(result).toEqual({
|
|
87
|
-
valid: true,
|
|
88
|
-
dirs: ['1700000000_create-users', '1700000001_add-posts'],
|
|
89
|
-
files: [],
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
test('silently skips non-matching directory names', () => {
|
|
93
|
-
const dirNames = ['bad-name', '1700000000_create-users'];
|
|
94
|
-
const existingSqlFiles = new Set(['1700000000_create-users']);
|
|
95
|
-
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
96
|
-
expect(result).toEqual({
|
|
97
|
-
valid: true,
|
|
98
|
-
dirs: ['1700000000_create-users'],
|
|
99
|
-
files: [],
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
test('returns error for missing migration.sql files', () => {
|
|
103
|
-
const dirNames = ['1700000000_create-users', '1700000001_add-posts'];
|
|
104
|
-
const existingSqlFiles = new Set(['1700000000_create-users']);
|
|
105
|
-
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
106
|
-
expect(result).toEqual({
|
|
107
|
-
valid: false,
|
|
108
|
-
errors: [{ type: 'missing_sql_file', dirName: '1700000001_add-posts' }],
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
test('returns valid result with sorted files for loose .sql files', () => {
|
|
112
|
-
const fileNames = ['002_add-posts.sql', '001_create-users.sql'];
|
|
113
|
-
const result = validateMigrations([], fileNames, new Set());
|
|
114
|
-
expect(result).toEqual({
|
|
115
|
-
valid: true,
|
|
116
|
-
dirs: [],
|
|
117
|
-
files: ['001_create-users.sql', '002_add-posts.sql'],
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
test('silently skips non-matching file names', () => {
|
|
121
|
-
const fileNames = ['README.sql', '001_create-users.sql', 'bad-name.sql'];
|
|
122
|
-
const result = validateMigrations([], fileNames, new Set());
|
|
123
|
-
expect(result).toEqual({
|
|
124
|
-
valid: true,
|
|
125
|
-
dirs: [],
|
|
126
|
-
files: ['001_create-users.sql'],
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
test('handles mixed dirs and files', () => {
|
|
130
|
-
const dirNames = ['1700000000_create-users'];
|
|
131
|
-
const fileNames = ['1700000001_add-posts.sql'];
|
|
132
|
-
const existingSqlFiles = new Set(['1700000000_create-users']);
|
|
133
|
-
const result = validateMigrations(dirNames, fileNames, existingSqlFiles);
|
|
134
|
-
expect(result).toEqual({
|
|
135
|
-
valid: true,
|
|
136
|
-
dirs: ['1700000000_create-users'],
|
|
137
|
-
files: ['1700000001_add-posts.sql'],
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
test('returns valid result for empty input', () => {
|
|
141
|
-
const result = validateMigrations([], [], new Set());
|
|
142
|
-
expect(result).toEqual({
|
|
143
|
-
valid: true,
|
|
144
|
-
dirs: [],
|
|
145
|
-
files: [],
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
test('allows underscores in dir names', () => {
|
|
149
|
-
const dirNames = ['001_create_users_table'];
|
|
150
|
-
const existingSqlFiles = new Set(['001_create_users_table']);
|
|
151
|
-
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
152
|
-
expect(result).toEqual({
|
|
153
|
-
valid: true,
|
|
154
|
-
dirs: ['001_create_users_table'],
|
|
155
|
-
files: [],
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
test('returns error for duplicate migration numbers in dirs', () => {
|
|
159
|
-
const dirNames = ['001_create-users', '001_add-posts'];
|
|
160
|
-
const existingSqlFiles = new Set(['001_create-users', '001_add-posts']);
|
|
161
|
-
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
162
|
-
expect(result).toEqual({
|
|
163
|
-
valid: false,
|
|
164
|
-
errors: [
|
|
165
|
-
{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_add-posts', '001_create-users'] },
|
|
166
|
-
],
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
test('returns error for duplicate migration numbers in files', () => {
|
|
170
|
-
const fileNames = ['001_create-users.sql', '001_add-posts.sql'];
|
|
171
|
-
const result = validateMigrations([], fileNames, new Set());
|
|
172
|
-
expect(result).toEqual({
|
|
173
|
-
valid: false,
|
|
174
|
-
errors: [
|
|
175
|
-
{
|
|
176
|
-
type: 'duplicate_migration_number',
|
|
177
|
-
migrationNumber: '1',
|
|
178
|
-
names: ['001_add-posts.sql', '001_create-users.sql'],
|
|
179
|
-
},
|
|
180
|
-
],
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
test('returns error for duplicate migration numbers across dirs and files', () => {
|
|
184
|
-
const dirNames = ['001_create-users'];
|
|
185
|
-
const fileNames = ['001_add-posts.sql'];
|
|
186
|
-
const existingSqlFiles = new Set(['001_create-users']);
|
|
187
|
-
const result = validateMigrations(dirNames, fileNames, existingSqlFiles);
|
|
188
|
-
expect(result).toEqual({
|
|
189
|
-
valid: false,
|
|
190
|
-
errors: [
|
|
191
|
-
{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_add-posts.sql', '001_create-users'] },
|
|
192
|
-
],
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
test('treats different zero-padded prefixes as duplicates', () => {
|
|
196
|
-
const dirNames = ['1_init', '001_setup'];
|
|
197
|
-
const existingSqlFiles = new Set(['1_init', '001_setup']);
|
|
198
|
-
const result = validateMigrations(dirNames, [], existingSqlFiles);
|
|
199
|
-
expect(result).toEqual({
|
|
200
|
-
valid: false,
|
|
201
|
-
errors: [{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_setup', '1_init'] }],
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
describe('formatValidationErrors', () => {
|
|
206
|
-
test('formats missing_sql_file errors', () => {
|
|
207
|
-
const message = formatValidationErrors([{ type: 'missing_sql_file', dirName: '1700000000_create-users' }]);
|
|
208
|
-
expect(message).toContain('Database migration validation failed');
|
|
209
|
-
expect(message).toContain('"1700000000_create-users/migration.sql" is missing');
|
|
210
|
-
});
|
|
211
|
-
test('formats multiple errors', () => {
|
|
212
|
-
const message = formatValidationErrors([
|
|
213
|
-
{ type: 'missing_sql_file', dirName: '1700000000_create-users' },
|
|
214
|
-
{ type: 'missing_sql_file', dirName: '1700000001_add-posts' },
|
|
215
|
-
]);
|
|
216
|
-
expect(message).toContain('"1700000000_create-users/migration.sql" is missing');
|
|
217
|
-
expect(message).toContain('"1700000001_add-posts/migration.sql" is missing');
|
|
218
|
-
});
|
|
219
|
-
test('formats duplicate_migration_number errors', () => {
|
|
220
|
-
const message = formatValidationErrors([
|
|
221
|
-
{ type: 'duplicate_migration_number', migrationNumber: '1', names: ['001_add-posts', '001_create-users'] },
|
|
222
|
-
]);
|
|
223
|
-
expect(message).toContain('Database migration validation failed');
|
|
224
|
-
expect(message).toContain('Duplicate migration number 1: "001_add-posts", "001_create-users"');
|
|
225
|
-
});
|
|
226
|
-
});
|