@netlify/build 29.31.5 → 29.32.1

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.
@@ -1,8 +1,10 @@
1
- export function logPluginsFetchError(logs: any, message: any): void;
2
- export function logPluginsList({ pluginsList, debug, logs }: {
3
- pluginsList: any;
4
- debug: any;
5
- logs: any;
6
- }): void;
7
- export function logFailPluginWarning(methodName: any, event: any): void;
8
- export function logDeploySuccess(logs: any): void;
1
+ import type { PluginList } from '../../plugins/list.js';
2
+ import { BufferedLogs } from '../logger.js';
3
+ export declare const logPluginsFetchError: (logs: BufferedLogs | undefined, message: string) => void;
4
+ export declare const logPluginsList: ({ pluginsList, debug, logs, }: {
5
+ pluginsList: PluginList;
6
+ logs: BufferedLogs | undefined;
7
+ debug?: boolean | undefined;
8
+ }) => void;
9
+ export declare const logFailPluginWarning: (methodName: any, event: any) => void;
10
+ export declare const logDeploySuccess: (logs?: BufferedLogs) => void;
@@ -1,20 +1,19 @@
1
- import { log, logArray, logWarning, logSubHeader } from '../logger.js';
1
+ import { log, logArray, logSubHeader, logWarning } from '../logger.js';
2
2
  export const logPluginsFetchError = function (logs, message) {
3
3
  logWarning(logs, `
4
4
  Warning: could not fetch latest plugins list. Plugins versions might not be the latest.
5
5
  ${message}`);
6
6
  };
7
- export const logPluginsList = function ({ pluginsList, debug, logs }) {
7
+ export const logPluginsList = function ({ pluginsList, debug, logs, }) {
8
8
  if (!debug) {
9
9
  return;
10
10
  }
11
- const pluginsListArray = Object.entries(pluginsList).map(getPluginsListItem).sort();
11
+ const pluginsListArray = Object.entries(pluginsList)
12
+ .map(([packageName, versions]) => `${packageName}@${versions[0].version}`)
13
+ .sort();
12
14
  logSubHeader(logs, 'Available plugins');
13
15
  logArray(logs, pluginsListArray);
14
16
  };
15
- const getPluginsListItem = function ([packageName, versions]) {
16
- return `${packageName}@${versions[0].version}`;
17
- };
18
17
  export const logFailPluginWarning = function (methodName, event) {
19
18
  logWarning(undefined, `Plugin error: since "${event}" happens after deploy, the build has already succeeded and cannot fail anymore. This plugin should either:
20
19
  - use utils.build.failPlugin() instead of utils.build.${methodName}() to clarify that the plugin failed, but not the build.
@@ -1,33 +1,25 @@
1
- export function getExpectedVersion({ versions, nodeVersion, packageJson, buildDir, pinnedVersion }: {
2
- versions: any;
3
- nodeVersion: any;
4
- packageJson: any;
5
- buildDir: any;
6
- pinnedVersion: any;
7
- }): Promise<{
8
- version: any;
1
+ import { PackageJson } from 'read-pkg-up';
2
+ import { PluginVersion } from './list.js';
3
+ /**
4
+ * Retrieve the `expectedVersion` of a plugin:
5
+ * - This is the version which should be run
6
+ * - This takes version pinning into account
7
+ * - If this does not match the currently cached version, it is installed first
8
+ * This is also used to retrieve the `compatibleVersion` of a plugin
9
+ * - This is the most recent version compatible with this site
10
+ * - This is the same logic except it does not use version pinning
11
+ * - This is only used to print a warning message when the `compatibleVersion`
12
+ * is older than the currently used version.
13
+ */
14
+ export declare const getExpectedVersion: ({ versions, nodeVersion, packageJson, packagePath, buildDir, pinnedVersion, }: {
15
+ versions: PluginVersion[];
16
+ /** The package.json of the repository */
17
+ packageJson: PackageJson;
18
+ packagePath?: string | undefined;
19
+ buildDir: string;
20
+ nodeVersion: string;
21
+ pinnedVersion?: string | undefined;
22
+ }) => Promise<{
23
+ version: string;
9
24
  compatWarning: string;
10
25
  }>;
11
- export namespace CONDITIONS {
12
- namespace nodeVersion {
13
- export { nodeVersionTest as test };
14
- export { nodeVersionWarning as warning };
15
- }
16
- namespace siteDependencies {
17
- export { siteDependenciesTest as test };
18
- export { siteDependenciesWarning as warning };
19
- }
20
- }
21
- declare function nodeVersionTest(allowedNodeVersion: any, { nodeVersion }: {
22
- nodeVersion: any;
23
- }): boolean;
24
- declare function nodeVersionWarning(allowedNodeVersion: any): string;
25
- declare function siteDependenciesTest(allowedSiteDependencies: any, { packageJson: { devDependencies, dependencies }, buildDir }: {
26
- packageJson: {
27
- devDependencies: any;
28
- dependencies: any;
29
- };
30
- buildDir: any;
31
- }): Promise<any>;
32
- declare function siteDependenciesWarning(allowedSiteDependencies: any): string;
33
- export {};
@@ -1,104 +1,63 @@
1
- import pEvery from 'p-every';
1
+ import _pEvery from 'p-every';
2
2
  import pLocate from 'p-locate';
3
3
  import semver from 'semver';
4
- import { importJsonFile } from '../utils/json.js';
5
- import { resolvePath } from '../utils/resolve.js';
6
- // Retrieve the `expectedVersion` of a plugin:
7
- // - This is the version which should be run
8
- // - This takes version pinning into account
9
- // - If this does not match the currently cached version, it is installed first
10
- // This is also used to retrieve the `compatibleVersion` of a plugin
11
- // - This is the most recent version compatible with this site
12
- // - This is the same logic except it does not use version pinning
13
- // - This is only used to print a warning message when the `compatibleVersion`
14
- // is older than the currently used version.
15
- export const getExpectedVersion = async function ({ versions, nodeVersion, packageJson, buildDir, pinnedVersion }) {
16
- const { version, conditions } = await getCompatibleEntry({
4
+ import { CONDITIONS } from './plugin_conditions.js';
5
+ // the types of that package seem to be not correct and demand a `pEvery.default()` usage which is wrong
6
+ const pEvery = _pEvery;
7
+ /**
8
+ * Retrieve the `expectedVersion` of a plugin:
9
+ * - This is the version which should be run
10
+ * - This takes version pinning into account
11
+ * - If this does not match the currently cached version, it is installed first
12
+ * This is also used to retrieve the `compatibleVersion` of a plugin
13
+ * - This is the most recent version compatible with this site
14
+ * - This is the same logic except it does not use version pinning
15
+ * - This is only used to print a warning message when the `compatibleVersion`
16
+ * is older than the currently used version.
17
+ */
18
+ export const getExpectedVersion = async function ({ versions, nodeVersion, packageJson, packagePath, buildDir, pinnedVersion, }) {
19
+ const { version, conditions = [] } = await getCompatibleEntry({
17
20
  versions,
18
21
  nodeVersion,
19
22
  packageJson,
23
+ packagePath,
20
24
  buildDir,
21
25
  pinnedVersion,
22
26
  });
23
- const compatWarning = getCompatWarning(conditions);
27
+ // Retrieve warning message shown when using an older version with `compatibility`
28
+ const compatWarning = conditions.map(({ type, condition }) => CONDITIONS[type].warning(condition)).join(', ');
24
29
  return { version, compatWarning };
25
30
  };
26
- // This function finds the right `compatibility` entry to use with the plugin.
27
- // - `compatibitlity` entries are meant for backward compatibility
28
- // Plugins should define each major version in `compatibility`.
29
- // - The entries are sorted from most to least recent version.
30
- // - After their first successful run, plugins are pinned by their major
31
- // version which is passed as `pinnedVersion` to the next builds.
32
- // When the plugin does not have a `pinnedVersion`, we use the most recent
33
- // `compatibility` entry with a successful condition.
34
- // When the plugin has a `pinnedVersion`, we do not use the `compatibility`
35
- // conditions. Instead, we just use the most recent entry with a `version`
36
- // matching `pinnedVersion`.
37
- // When no `compatibility` entry matches, we use:
38
- // - If there is a `pinnedVersion`, use it unless `latestVersion` matches it
39
- // - Otherwise, use `latestVersion`
40
- const getCompatibleEntry = async function ({ versions, nodeVersion, packageJson, buildDir, pinnedVersion }) {
41
- if (pinnedVersion !== undefined) {
42
- const matchingVersion = versions.find(({ version }) => semver.satisfies(version, pinnedVersion, { includePrerelease: true }));
43
- return matchingVersion || { version: pinnedVersion };
44
- }
45
- const versionsWithConditions = versions.filter(hasConditions);
46
- const compatibleEntry = await pLocate(versionsWithConditions, ({ conditions }) => matchesCompatField({ conditions, nodeVersion, packageJson, buildDir }));
47
- return compatibleEntry || { version: versions[0].version };
48
- };
49
- // Ignore entries without conditions. Those are used to specify breaking
50
- // changes, i.e. meant to be used for version pinning instead.
51
- const hasConditions = function ({ conditions }) {
52
- return conditions.length !== 0;
53
- };
54
- const matchesCompatField = async function ({ conditions, nodeVersion, packageJson, buildDir }) {
55
- return await pEvery(conditions, ({ type, condition }) => CONDITIONS[type].test(condition, { nodeVersion, packageJson, buildDir }));
56
- };
57
- // Retrieve warning message shown when using an older version with `compatibility`
58
- const getCompatWarning = function (conditions = []) {
59
- return conditions.map(getConditionWarning).join(', ');
60
- };
61
- const getConditionWarning = function ({ type, condition }) {
62
- return CONDITIONS[type].warning(condition);
63
- };
64
- // Plugins can use `compatibility.{version}.nodeVersion: 'allowedNodeVersion'`
65
- // to deliver different plugin versions based on the Node.js version
66
- const nodeVersionTest = function (allowedNodeVersion, { nodeVersion }) {
67
- return semver.satisfies(nodeVersion, allowedNodeVersion);
68
- };
69
- const nodeVersionWarning = function (allowedNodeVersion) {
70
- return `Node.js ${allowedNodeVersion}`;
71
- };
72
- const siteDependenciesTest = async function (allowedSiteDependencies, { packageJson: { devDependencies, dependencies }, buildDir }) {
73
- const siteDependencies = { ...devDependencies, ...dependencies };
74
- return await pEvery(Object.entries(allowedSiteDependencies), ([dependencyName, allowedVersion]) => siteDependencyTest({ dependencyName, allowedVersion, siteDependencies, buildDir }));
75
- };
76
- const siteDependencyTest = async function ({ dependencyName, allowedVersion, siteDependencies, buildDir }) {
77
- const siteDependency = siteDependencies[dependencyName];
78
- if (typeof siteDependency !== 'string') {
79
- return false;
80
- }
81
- // if this is a valid version we can apply the rule directly
82
- if (semver.clean(siteDependency) !== null) {
83
- return semver.satisfies(siteDependency, allowedVersion);
84
- }
85
- try {
86
- // if this is a range we need to get the exact version
87
- const packageJsonPath = await resolvePath(`${dependencyName}/package.json`, buildDir);
88
- const { version } = await importJsonFile(packageJsonPath);
89
- return semver.satisfies(version, allowedVersion);
90
- }
91
- catch {
92
- return false;
93
- }
94
- };
95
- const siteDependenciesWarning = function (allowedSiteDependencies) {
96
- return Object.entries(allowedSiteDependencies).map(siteDependencyWarning).join(',');
97
- };
98
- const siteDependencyWarning = function ([dependencyName, allowedVersion]) {
99
- return `${dependencyName}@${allowedVersion}`;
100
- };
101
- export const CONDITIONS = {
102
- nodeVersion: { test: nodeVersionTest, warning: nodeVersionWarning },
103
- siteDependencies: { test: siteDependenciesTest, warning: siteDependenciesWarning },
31
+ /**
32
+ * This function finds the right `compatibility` entry to use with the plugin.
33
+ * - `compatibility` entries are meant for backward compatibility
34
+ * Plugins should define each major version in `compatibility`.
35
+ * - The entries are sorted from most to least recent version.
36
+ * - After their first successful run, plugins are pinned by their major
37
+ * version which is passed as `pinnedVersion` to the next builds.
38
+ * When the plugin does not have a `pinnedVersion`, we use the most recent
39
+ * `compatibility` entry with a successful condition.
40
+ * When the plugin has a `pinnedVersion`, we do not use the `compatibility`
41
+ * conditions. Instead, we just use the most recent entry with a `version`
42
+ * matching `pinnedVersion`.
43
+ * When no `compatibility` entry matches, we use:
44
+ * - If there is a `pinnedVersion`, use it unless `latestVersion` matches it
45
+ * - Otherwise, use `latestVersion`
46
+ */
47
+ const getCompatibleEntry = async function ({ versions, nodeVersion, packageJson, packagePath, buildDir, pinnedVersion, }) {
48
+ const compatibleEntry = await pLocate(versions, async ({ version, overridePinnedVersion, conditions }) => {
49
+ // Detect if the overridePinnedVersion intersects with the pinned version in this case we don't care about filtering
50
+ const overridesPin = Boolean(pinnedVersion && overridePinnedVersion && semver.intersects(overridePinnedVersion, pinnedVersion));
51
+ // ignore versions that don't satisfy the pinned version here if a pinned version is set
52
+ if (!overridesPin && pinnedVersion && !semver.satisfies(version, pinnedVersion, { includePrerelease: true })) {
53
+ return false;
54
+ }
55
+ // no conditions means nothing to filter
56
+ if (conditions.length === 0 && pinnedVersion === undefined) {
57
+ return false;
58
+ }
59
+ return await pEvery(conditions, async ({ type, condition }) => CONDITIONS[type].test(condition, { nodeVersion, packageJson, packagePath, buildDir }));
60
+ });
61
+ return (compatibleEntry ||
62
+ (pinnedVersion ? { version: pinnedVersion, conditions: [] } : { version: versions[0].version, conditions: [] }));
104
63
  };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,148 @@
1
+ import { expect, test } from 'vitest';
2
+ import { getExpectedVersion } from './compatibility.js';
3
+ test('`getExpectedVersion` should ignore the new major version if the version is pinned', async () => {
4
+ const versions = [
5
+ { version: '5.0.0', conditions: [] },
6
+ { version: '4.41.2', conditions: [] },
7
+ ];
8
+ const { version } = await getExpectedVersion({
9
+ versions,
10
+ nodeVersion: '18.19.0',
11
+ packageJson: {},
12
+ buildDir: '/some/path',
13
+ pinnedVersion: '4',
14
+ });
15
+ expect(version).toBe('4.41.2');
16
+ });
17
+ test('`getExpectedVersion` matches prerelease versions', async () => {
18
+ const versions = [
19
+ { version: '5.0.0', conditions: [] },
20
+ { version: '4.42.0-alpha.1', conditions: [] },
21
+ { version: '4.41.2', conditions: [] },
22
+ ];
23
+ const { version: version1 } = await getExpectedVersion({
24
+ versions,
25
+ nodeVersion: '18.19.0',
26
+ packageJson: {},
27
+ buildDir: '/some/path',
28
+ });
29
+ const { version: version2 } = await getExpectedVersion({
30
+ versions,
31
+ nodeVersion: '18.19.0',
32
+ packageJson: {},
33
+ buildDir: '/some/path',
34
+ pinnedVersion: '4',
35
+ });
36
+ expect(version1).toBe('5.0.0');
37
+ expect(version2).toBe('4.42.0-alpha.1');
38
+ });
39
+ test('`getExpectedVersion`should retrieve a new major version if the overridePinnedVersion is specified', async () => {
40
+ const versions = [
41
+ { version: '5.0.0', conditions: [], overridePinnedVersion: '>=4.0.0' },
42
+ { version: '4.41.2', conditions: [] },
43
+ ];
44
+ const { version } = await getExpectedVersion({
45
+ versions,
46
+ nodeVersion: '18.19.0',
47
+ packageJson: {},
48
+ buildDir: '/some/path',
49
+ pinnedVersion: '4',
50
+ });
51
+ expect(version).toBe('5.0.0');
52
+ });
53
+ test('`getExpectedVersion`should retrieve the plugin based on the condition of a nodeVersion', async () => {
54
+ const versions = [
55
+ {
56
+ version: '4.42.0',
57
+ conditions: [{ type: 'nodeVersion', condition: '>=18.0.0' }],
58
+ },
59
+ { version: '4.41.2', conditions: [] },
60
+ ];
61
+ const { version } = await getExpectedVersion({
62
+ versions,
63
+ nodeVersion: '17.19.0',
64
+ packageJson: {},
65
+ buildDir: '/some/path',
66
+ pinnedVersion: '4',
67
+ });
68
+ expect(version).toBe('4.41.2');
69
+ });
70
+ test('`getExpectedVersion` should retrieve the plugin based on conditions and feature flag due to pinned version', async () => {
71
+ const versions = [
72
+ {
73
+ version: '5.0.0-beta.1',
74
+ conditions: [
75
+ { type: 'nodeVersion', condition: '>= 18.0.0' },
76
+ { type: 'siteDependencies', condition: { next: '>=13.5.0' } },
77
+ ],
78
+ overridePinnedVersion: '>=4.0.0',
79
+ },
80
+ {
81
+ version: '4.42.0',
82
+ conditions: [{ type: 'siteDependencies', condition: { next: '>=10.0.9' } }],
83
+ },
84
+ { version: '4.41.2', conditions: [] },
85
+ {
86
+ version: '3.9.2',
87
+ conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
88
+ },
89
+ ];
90
+ const { version: version1 } = await getExpectedVersion({
91
+ versions,
92
+ nodeVersion: '17.19.0',
93
+ packageJson: { dependencies: { next: '10.0.8' } },
94
+ buildDir: '/some/path',
95
+ pinnedVersion: '3',
96
+ });
97
+ expect(version1).toBe('3.9.2');
98
+ const { version: version2 } = await getExpectedVersion({
99
+ versions,
100
+ nodeVersion: '17.19.0',
101
+ packageJson: { dependencies: { next: '11.0.0' } },
102
+ buildDir: '/some/path',
103
+ pinnedVersion: '4',
104
+ });
105
+ expect(version2).toBe('4.42.0');
106
+ const { version: version3 } = await getExpectedVersion({
107
+ versions,
108
+ nodeVersion: '18.19.0',
109
+ packageJson: { dependencies: { next: '13.5.0' } },
110
+ buildDir: '/some/path',
111
+ pinnedVersion: '4',
112
+ });
113
+ expect(version3).toBe('5.0.0-beta.1');
114
+ });
115
+ test('`getExpectedVersion` should retrieve the plugin based on conditions and feature flag due to pinned version', async () => {
116
+ const versions = [
117
+ {
118
+ version: '5.0.0-beta.1',
119
+ conditions: [
120
+ { type: 'nodeVersion', condition: '>= 18.0.0' },
121
+ { type: 'siteDependencies', condition: { next: '>=13.5.0' } },
122
+ ],
123
+ overridePinnedVersion: '>=4.0.0',
124
+ },
125
+ { version: '4.41.2', conditions: [] },
126
+ {
127
+ version: '3.9.2',
128
+ conditions: [{ type: 'siteDependencies', condition: { next: '<10.0.9' } }],
129
+ },
130
+ ];
131
+ const { version: version1 } = await getExpectedVersion({
132
+ versions,
133
+ nodeVersion: '20.0.0',
134
+ packageJson: { dependencies: { next: '14.0.0' } },
135
+ buildDir: '/some/path',
136
+ pinnedVersion: '4',
137
+ });
138
+ expect(version1).toBe('5.0.0-beta.1');
139
+ // out of range
140
+ const { version: version2 } = await getExpectedVersion({
141
+ versions,
142
+ nodeVersion: '20.0.0',
143
+ packageJson: { dependencies: { next: '13.0.0' } },
144
+ buildDir: '/some/path',
145
+ pinnedVersion: '4',
146
+ });
147
+ expect(version2).toBe('4.41.2');
148
+ });
@@ -1,10 +1,11 @@
1
- export function addExpectedVersions({ pluginsOptions, autoPluginsDir, packageJson, debug, logs, buildDir, testOpts, featureFlags, }: {
1
+ export declare const addExpectedVersions: ({ pluginsOptions, autoPluginsDir, packageJson, packagePath, debug, logs, buildDir, testOpts, featureFlags, }: {
2
2
  pluginsOptions: any;
3
3
  autoPluginsDir: any;
4
4
  packageJson: any;
5
+ packagePath: any;
5
6
  debug: any;
6
7
  logs: any;
7
8
  buildDir: any;
8
9
  testOpts: any;
9
10
  featureFlags: any;
10
- }): Promise<any>;
11
+ }) => Promise<any>;
@@ -7,15 +7,24 @@ import { getPluginsList } from './list.js';
7
7
  // When using plugins in our official list, those are installed in .netlify/plugins/
8
8
  // We ensure that the last version that's been approved is always the one being used.
9
9
  // We also ensure that the plugin is our official list.
10
- export const addExpectedVersions = async function ({ pluginsOptions, autoPluginsDir, packageJson, debug, logs, buildDir, testOpts, featureFlags, }) {
10
+ export const addExpectedVersions = async function ({ pluginsOptions, autoPluginsDir, packageJson, packagePath, debug, logs, buildDir, testOpts, featureFlags, }) {
11
11
  if (!pluginsOptions.some(needsExpectedVersion)) {
12
12
  return pluginsOptions;
13
13
  }
14
14
  const pluginsList = await getPluginsList({ debug, logs, testOpts });
15
- return await Promise.all(pluginsOptions.map((pluginOptions) => addExpectedVersion({ pluginsList, autoPluginsDir, packageJson, pluginOptions, buildDir, featureFlags, testOpts })));
15
+ return await Promise.all(pluginsOptions.map((pluginOptions) => addExpectedVersion({
16
+ pluginsList,
17
+ autoPluginsDir,
18
+ packageJson,
19
+ packagePath,
20
+ pluginOptions,
21
+ buildDir,
22
+ featureFlags,
23
+ testOpts,
24
+ })));
16
25
  };
17
- // Any `pluginOptions` with `expectedVersion` set will be automatically installed
18
- const addExpectedVersion = async function ({ pluginsList, autoPluginsDir, packageJson, pluginOptions, pluginOptions: { packageName, pluginPath, loadedFrom, nodeVersion, pinnedVersion }, buildDir, featureFlags, testOpts, }) {
26
+ /** Any `pluginOptions` with `expectedVersion` set will be automatically installed */
27
+ const addExpectedVersion = async function ({ pluginsList, autoPluginsDir, packageJson, packagePath, pluginOptions, pluginOptions: { packageName, pluginPath, loadedFrom, nodeVersion, pinnedVersion }, buildDir, featureFlags, testOpts, }) {
19
28
  if (!needsExpectedVersion(pluginOptions)) {
20
29
  return pluginOptions;
21
30
  }
@@ -27,8 +36,8 @@ const addExpectedVersion = async function ({ pluginsList, autoPluginsDir, packag
27
36
  const versions = filterVersions(unfilteredVersions, featureFlags);
28
37
  const [{ version: latestVersion, migrationGuide }] = versions;
29
38
  const [{ version: expectedVersion }, { version: compatibleVersion, compatWarning }] = await Promise.all([
30
- getExpectedVersion({ versions, nodeVersion, packageJson, buildDir, pinnedVersion }),
31
- getExpectedVersion({ versions, nodeVersion, packageJson, buildDir }),
39
+ getExpectedVersion({ versions, nodeVersion, packageJson, packagePath, buildDir, pinnedVersion }),
40
+ getExpectedVersion({ versions, nodeVersion, packageJson, packagePath, buildDir }),
32
41
  ]);
33
42
  const isMissing = await isMissingVersion({ autoPluginsDir, packageName, pluginPath, loadedFrom, expectedVersion });
34
43
  return {
@@ -47,8 +56,10 @@ const addExpectedVersion = async function ({ pluginsList, autoPluginsDir, packag
47
56
  const filterVersions = function (unfilteredVersions, featureFlags) {
48
57
  return unfilteredVersions.filter(({ featureFlag }) => featureFlag === undefined || featureFlags[featureFlag]);
49
58
  };
50
- // Checks whether plugin should be installed due to the wrong version being used
51
- // (either outdated, or mismatching compatibility requirements)
59
+ /**
60
+ * Checks whether plugin should be installed due to the wrong version being used
61
+ * (either outdated, or mismatching compatibility requirements)
62
+ */
52
63
  const isMissingVersion = async function ({ autoPluginsDir, packageName, pluginPath, loadedFrom, expectedVersion }) {
53
64
  return (
54
65
  // We always respect the versions specified in `package.json`, as opposed
@@ -62,6 +73,10 @@ const isMissingVersion = async function ({ autoPluginsDir, packageName, pluginPa
62
73
  const getAutoPluginVersion = async function (packageName, autoPluginsDir) {
63
74
  const packageJsonPath = await resolvePath(`${packageName}/package.json`, autoPluginsDir);
64
75
  const { version } = await importJsonFile(packageJsonPath);
76
+ if (!version) {
77
+ // this should never happen
78
+ throw new Error('No version specified for the plugin!');
79
+ }
65
80
  return version;
66
81
  };
67
82
  const needsExpectedVersion = function ({ loadedFrom }) {
@@ -1,9 +1,32 @@
1
- export function getPluginsList({ debug, logs, testOpts: { pluginsListUrl } }: {
2
- debug: any;
3
- logs: any;
4
- testOpts: {
5
- pluginsListUrl: any;
6
- };
7
- }): Promise<never[] | {
8
- [k: string]: any;
9
- }>;
1
+ import { BufferedLogs } from '../log/logger.js';
2
+ import { CONDITIONS } from './plugin_conditions.js';
3
+ /**
4
+ * The resolved and normalized Plugin list
5
+ */
6
+ export type PluginList = Record<string, PluginVersion[]>;
7
+ export type PluginVersion = {
8
+ version: string;
9
+ migrationGuide?: string;
10
+ featureFlag?: string;
11
+ overridePinnedVersion?: string;
12
+ conditions: {
13
+ type: keyof typeof CONDITIONS;
14
+ condition: string | Record<string, string>;
15
+ }[];
16
+ };
17
+ /**
18
+ * Retrieve the list of plugins officially vetted by us and displayed in our
19
+ * plugins directory UI.
20
+ * We fetch this list during each build (no caching) because we want new
21
+ * versions of plugins to be available instantly to all users. The time to
22
+ * make this request is somewhat ok (in the 100ms range).
23
+ * We only fetch this plugins list when needed, i.e. we defer it as much as
24
+ * possible.
25
+ */
26
+ export declare const getPluginsList: ({ debug, logs, testOpts: { pluginsListUrl }, }: {
27
+ testOpts?: {
28
+ pluginsListUrl?: string | undefined;
29
+ } | undefined;
30
+ debug?: boolean | undefined;
31
+ logs?: BufferedLogs | undefined;
32
+ }) => Promise<PluginList>;
@@ -2,21 +2,23 @@ import { pluginsUrl, pluginsList as oldPluginsList } from '@netlify/plugins-list
2
2
  import got from 'got';
3
3
  import isPlainObj from 'is-plain-obj';
4
4
  import { logPluginsList, logPluginsFetchError } from '../log/messages/plugins.js';
5
- import { CONDITIONS } from './compatibility.js';
6
- // 1 minute HTTP request timeout
5
+ import { CONDITIONS } from './plugin_conditions.js';
6
+ /** 1 minute HTTP request timeout */
7
7
  const PLUGINS_LIST_TIMEOUT = 6e4;
8
- // Retrieve the list of plugins officially vetted by us and displayed in our
9
- // plugins directory UI.
10
- // We fetch this list during each build (no caching) because we want new
11
- // versions of plugins to be available instantly to all users. The time to
12
- // make this request is somewhat ok (in the 100ms range).
13
- // We only fetch this plugins list when needed, i.e. we defer it as much as
14
- // possible.
15
- export const getPluginsList = async function ({ debug, logs, testOpts: { pluginsListUrl } }) {
8
+ /**
9
+ * Retrieve the list of plugins officially vetted by us and displayed in our
10
+ * plugins directory UI.
11
+ * We fetch this list during each build (no caching) because we want new
12
+ * versions of plugins to be available instantly to all users. The time to
13
+ * make this request is somewhat ok (in the 100ms range).
14
+ * We only fetch this plugins list when needed, i.e. we defer it as much as
15
+ * possible.
16
+ */
17
+ export const getPluginsList = async function ({ debug, logs, testOpts: { pluginsListUrl } = {}, }) {
16
18
  // We try not to mock in integration tests. However, sending a request for
17
19
  // each test would be too slow and make tests unreliable.
18
20
  if (pluginsListUrl === 'test') {
19
- return [];
21
+ return {};
20
22
  }
21
23
  const pluginsListUrlA = pluginsListUrl === undefined ? pluginsUrl : pluginsListUrl;
22
24
  const pluginsList = await fetchPluginsList({ logs, pluginsListUrl: pluginsListUrlA });
@@ -24,7 +26,7 @@ export const getPluginsList = async function ({ debug, logs, testOpts: { plugins
24
26
  logPluginsList({ pluginsList: pluginsListA, debug, logs });
25
27
  return pluginsListA;
26
28
  };
27
- const fetchPluginsList = async function ({ logs, pluginsListUrl }) {
29
+ const fetchPluginsList = async function ({ logs, pluginsListUrl, }) {
28
30
  try {
29
31
  const { body } = await got(pluginsListUrl, { responseType: 'json', timeout: { request: PLUGINS_LIST_TIMEOUT } });
30
32
  if (!isValidPluginsList(body)) {
@@ -49,21 +51,23 @@ const isValidPluginsList = function (pluginsList) {
49
51
  const normalizePluginsList = function (pluginsList) {
50
52
  return Object.fromEntries(pluginsList.map(normalizePluginItem));
51
53
  };
52
- // `version` in `plugins.json` is the latest version.
53
- // A `compatibility` array of objects can be added to specify conditions to
54
- // apply different versions.
55
- // `netlify/plugins` ensures that `compatibility`:
56
- // - Has the proper shape.
57
- // - Is sorted from the highest to lowest version.
58
- // - Does not include the latest `version`.
54
+ /**
55
+ * `version` in `plugins.json` is the latest version.
56
+ * A `compatibility` array of objects can be added to specify conditions to
57
+ * apply different versions.
58
+ * `netlify/plugins` ensures that `compatibility`:
59
+ * - Has the proper shape.
60
+ * - Is sorted from the highest to lowest version.
61
+ * - Does not include the latest `version`.
62
+ */
59
63
  const normalizePluginItem = function ({ package: packageName, version, compatibility = [] }) {
60
64
  const versions = compatibility.length === 0 ? [{ version }] : compatibility;
61
65
  const versionsA = versions.map(normalizeCompatVersion);
62
66
  return [packageName, versionsA];
63
67
  };
64
- const normalizeCompatVersion = function ({ version, migrationGuide, featureFlag, ...otherProperties }) {
68
+ const normalizeCompatVersion = function ({ version, migrationGuide, featureFlag, overridePinnedVersion, ...otherProperties }) {
65
69
  const conditions = Object.entries(otherProperties).filter(isCondition).map(normalizeCondition);
66
- return { version, migrationGuide, featureFlag, conditions };
70
+ return { version, migrationGuide, overridePinnedVersion, featureFlag, conditions };
67
71
  };
68
72
  const isCondition = function ([type]) {
69
73
  return type in CONDITIONS;
@@ -0,0 +1,24 @@
1
+ import { PackageJson } from 'read-pkg-up';
2
+ import { type PluginVersion } from './list.js';
3
+ type ConditionContext = {
4
+ nodeVersion: string;
5
+ packageJson: PackageJson;
6
+ buildDir: string;
7
+ packagePath?: string;
8
+ };
9
+ type PluginCondition = PluginVersion['conditions'][number]['condition'];
10
+ export type Condition = {
11
+ test: (condition: PluginCondition, ctx: ConditionContext) => boolean | Promise<boolean>;
12
+ warning: (condition: PluginCondition) => void;
13
+ };
14
+ export declare const CONDITIONS: {
15
+ nodeVersion: {
16
+ test: (allowedNodeVersion: string, { nodeVersion }: ConditionContext) => boolean;
17
+ warning: (allowedNodeVersion: string) => string;
18
+ };
19
+ siteDependencies: {
20
+ test: (allowedSiteDependencies: Record<string, string>, { packageJson: { devDependencies, dependencies }, packagePath, buildDir }: ConditionContext) => Promise<boolean>;
21
+ warning: (allowedSiteDependencies: any) => string;
22
+ };
23
+ };
24
+ export {};
@@ -0,0 +1,61 @@
1
+ import { join } from 'path';
2
+ import _pEvery from 'p-every';
3
+ import semver from 'semver';
4
+ import { importJsonFile } from '../utils/json.js';
5
+ import { resolvePath } from '../utils/resolve.js';
6
+ // the types of that package seem to be not correct and demand a `pEvery.default()` usage which is wrong
7
+ const pEvery = _pEvery;
8
+ /**
9
+ * Plugins can use `compatibility.{version}.nodeVersion: 'allowedNodeVersion'`
10
+ * to deliver different plugin versions based on the Node.js version
11
+ */
12
+ const nodeVersionTest = (allowedNodeVersion, { nodeVersion }) => semver.satisfies(nodeVersion, allowedNodeVersion);
13
+ const nodeVersionWarning = (allowedNodeVersion) => `Node.js ${allowedNodeVersion}`;
14
+ const siteDependenciesTest = async function (allowedSiteDependencies, { packageJson: { devDependencies = {}, dependencies = {} }, packagePath, buildDir }) {
15
+ let siteDependencies = { ...devDependencies, ...dependencies };
16
+ // If there is a packagePath in a mono repository add the dependencies from the package as well
17
+ // the packageJson is in this case only the top root package json which does not contain all the dependencies to test for
18
+ const pkgJsonPath = packagePath && join(buildDir, packagePath, 'package.json');
19
+ if (pkgJsonPath) {
20
+ try {
21
+ const { devDependencies: devDepsPgk = {}, dependencies: depsPkg = {} } = await importJsonFile(pkgJsonPath);
22
+ siteDependencies = { ...siteDependencies, ...devDepsPgk, ...depsPkg };
23
+ }
24
+ catch {
25
+ // noop
26
+ }
27
+ }
28
+ return await pEvery(Object.entries(allowedSiteDependencies), ([dependencyName, allowedVersion]) => siteDependencyTest({ dependencyName, allowedVersion, siteDependencies, buildDir }));
29
+ };
30
+ const siteDependencyTest = async function ({ dependencyName, allowedVersion, siteDependencies, buildDir, }) {
31
+ const siteDependency = siteDependencies[dependencyName];
32
+ if (typeof siteDependency !== 'string') {
33
+ return false;
34
+ }
35
+ // if this is a valid version we can apply the rule directly
36
+ if (semver.clean(siteDependency) !== null) {
37
+ return semver.satisfies(siteDependency, allowedVersion);
38
+ }
39
+ try {
40
+ // if this is a range we need to get the exact version
41
+ const packageJsonPath = await resolvePath(`${dependencyName}/package.json`, buildDir);
42
+ const { version } = await importJsonFile(packageJsonPath);
43
+ if (!version) {
44
+ return false;
45
+ }
46
+ return semver.satisfies(version, allowedVersion);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ };
52
+ const siteDependenciesWarning = function (allowedSiteDependencies) {
53
+ return Object.entries(allowedSiteDependencies).map(siteDependencyWarning).join(',');
54
+ };
55
+ const siteDependencyWarning = function ([dependencyName, allowedVersion]) {
56
+ return `${dependencyName}@${allowedVersion}`;
57
+ };
58
+ export const CONDITIONS = {
59
+ nodeVersion: { test: nodeVersionTest, warning: nodeVersionWarning },
60
+ siteDependencies: { test: siteDependenciesTest, warning: siteDependenciesWarning },
61
+ };
@@ -26,6 +26,7 @@ export const resolvePluginsPath = async function ({ pluginsOptions, siteInfo, bu
26
26
  pluginsOptions: pluginsOptionsC,
27
27
  autoPluginsDir,
28
28
  packageJson,
29
+ packagePath,
29
30
  debug,
30
31
  logs,
31
32
  buildDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/build",
3
- "version": "29.31.5",
3
+ "version": "29.32.1",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -26,9 +26,12 @@
26
26
  "postbuild": "npx copyfiles \"src/**/*.yml\" lib/ -u 1",
27
27
  "build": "tsc",
28
28
  "test:types": "tsd",
29
- "test": "ava && tsd",
29
+ "test": "ava && tsd && vitest run",
30
30
  "test:dev": "ava -w",
31
- "test:ci": "c8 -r lcovonly -r text -r json ava && tsd",
31
+ "test:ci": "run-p test:ci:*",
32
+ "test:ci:types": "tsd",
33
+ "test:ci:ava": "c8 -r lcovonly -r text -r json ava",
34
+ "test:ci:vitest": "vitest run",
32
35
  "test:measure": "../../tools/tests-duration.js"
33
36
  },
34
37
  "keywords": [
@@ -65,10 +68,10 @@
65
68
  "license": "MIT",
66
69
  "dependencies": {
67
70
  "@bugsnag/js": "^7.0.0",
68
- "@netlify/blobs": "^6.3.1",
71
+ "@netlify/blobs": "^6.4.2",
69
72
  "@netlify/cache-utils": "^5.1.5",
70
- "@netlify/config": "^20.10.1",
71
- "@netlify/edge-bundler": "10.1.3",
73
+ "@netlify/config": "^20.10.2",
74
+ "@netlify/edge-bundler": "11.2.0",
72
75
  "@netlify/framework-info": "^9.8.10",
73
76
  "@netlify/functions-utils": "^5.2.46",
74
77
  "@netlify/git-utils": "^5.1.1",
@@ -127,6 +130,7 @@
127
130
  "@netlify/nock-udp": "^3.1.2",
128
131
  "@opentelemetry/sdk-trace-base": "^1.18.1",
129
132
  "@types/node": "^14.18.53",
133
+ "@vitest/coverage-c8": "^0.30.1",
130
134
  "atob": "^2.1.2",
131
135
  "ava": "^4.0.0",
132
136
  "c8": "^7.12.0",
@@ -139,11 +143,13 @@
139
143
  "get-stream": "^6.0.0",
140
144
  "has-ansi": "^5.0.0",
141
145
  "moize": "^6.0.0",
146
+ "npm-run-all": "^4.1.5",
142
147
  "path-key": "^4.0.0",
143
148
  "process-exists": "^5.0.0",
144
149
  "sinon": "^13.0.0",
145
150
  "tmp-promise": "^3.0.2",
146
151
  "tsd": "^0.29.0",
152
+ "vitest": "^0.30.1",
147
153
  "yarn": "^1.22.4"
148
154
  },
149
155
  "peerDependencies": {
@@ -157,5 +163,5 @@
157
163
  "engines": {
158
164
  "node": "^14.16.0 || >=16.0.0"
159
165
  },
160
- "gitHead": "f8462d09c2cba21e9ffa52296ccc42f50a3c2495"
166
+ "gitHead": "be80dc14bac4c67333c36dfe5a056bbf50ac4288"
161
167
  }