From 4484cbcb2842c128e2e648278d8ea2584a3b4c72 Mon Sep 17 00:00:00 2001 From: Joshua Tang <joshuaystang@gmail.com> Date: Thu, 10 Aug 2023 23:19:17 +1000 Subject: [PATCH] feat(manager/pub): extract Flutter SDK (#23759) --- .../manager/pub/__fixtures__/extract.yaml | 13 --- .../manager/pub/__fixtures__/update.yaml | 14 --- lib/modules/manager/pub/artifacts.spec.ts | 26 ++++-- lib/modules/manager/pub/artifacts.ts | 17 ++-- lib/modules/manager/pub/extract.spec.ts | 87 +++++++++++++----- lib/modules/manager/pub/extract.ts | 92 ++++++++++--------- lib/modules/manager/pub/schema.ts | 17 +++- lib/modules/manager/pub/utils.spec.ts | 46 +++++++++- lib/modules/manager/pub/utils.ts | 22 ++++- 9 files changed, 222 insertions(+), 112 deletions(-) delete mode 100644 lib/modules/manager/pub/__fixtures__/extract.yaml delete mode 100644 lib/modules/manager/pub/__fixtures__/update.yaml diff --git a/lib/modules/manager/pub/__fixtures__/extract.yaml b/lib/modules/manager/pub/__fixtures__/extract.yaml deleted file mode 100644 index 0ed1dd32cd..0000000000 --- a/lib/modules/manager/pub/__fixtures__/extract.yaml +++ /dev/null @@ -1,13 +0,0 @@ -dev_dependencies: - test: ^0.1 - build: - version: 0.1 - -dependencies: - meta: 'something' - foo: 1 - bar: - version: 1 - baz: - non-sense: true - qux: false diff --git a/lib/modules/manager/pub/__fixtures__/update.yaml b/lib/modules/manager/pub/__fixtures__/update.yaml deleted file mode 100644 index ff17976192..0000000000 --- a/lib/modules/manager/pub/__fixtures__/update.yaml +++ /dev/null @@ -1,14 +0,0 @@ -dev_dependencies: - test: 0.1 - build: - version: 0.1 - -dependencies: - - bar: - sdk: flatter - foo: 1 - bar: - version: 1 - baz: - non-sense: true - qux: false diff --git a/lib/modules/manager/pub/artifacts.spec.ts b/lib/modules/manager/pub/artifacts.spec.ts index c7b8aaf903..bf7c292f22 100644 --- a/lib/modules/manager/pub/artifacts.spec.ts +++ b/lib/modules/manager/pub/artifacts.spec.ts @@ -4,6 +4,7 @@ import { env, fs, mocked } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; import * as docker from '../../../util/exec/docker'; +import { range } from '../../../util/range'; import * as _datasource from '../../datasource'; import type { UpdateArtifact, UpdateArtifactsConfig } from '../types'; import * as pub from '.'; @@ -19,7 +20,9 @@ process.env.CONTAINERBASE = 'true'; const lockFile = 'pubspec.lock'; const oldLockFileContent = 'Old pubspec.lock'; const newLockFileContent = 'New pubspec.lock'; -const depName = 'depName'; +const depNames = [...range(0, 3)].map((i) => `depName${i}`); +const depNamesWithSpace = depNames.join(' '); +const depNamesWithFlutter = [...depNames, 'flutter']; const datasource = mocked(_datasource); @@ -33,7 +36,9 @@ const config: UpdateArtifactsConfig = {}; const updateArtifact: UpdateArtifact = { packageFileName: 'pubspec.yaml', - updatedDeps: [{ depName }], + updatedDeps: depNamesWithFlutter.map((depName) => { + return { depName }; + }), newPackageFileContent: '', config, }; @@ -65,6 +70,15 @@ describe('modules/manager/pub/artifacts', () => { ).toBeNull(); }); + it('returns null if updatedDeps only contains flutter', async () => { + expect( + await pub.updateArtifacts({ + ...updateArtifact, + updatedDeps: [{ depName: 'flutter' }], + }) + ).toBeNull(); + }); + describe.each([ { sdk: 'dart', packageFileContent: '' }, { sdk: 'flutter', packageFileContent: 'sdk: flutter' }, @@ -81,7 +95,7 @@ describe('modules/manager/pub/artifacts', () => { ).toBeNull(); expect(execSnapshots).toMatchObject([ { - cmd: `${params.sdk} pub upgrade ${depName}`, + cmd: `${params.sdk} pub upgrade ${depNamesWithSpace}`, }, ]); }); @@ -107,7 +121,7 @@ describe('modules/manager/pub/artifacts', () => { ]); expect(execSnapshots).toMatchObject([ { - cmd: `${params.sdk} pub upgrade ${depName}`, + cmd: `${params.sdk} pub upgrade ${depNamesWithSpace}`, }, ]); }); @@ -181,7 +195,7 @@ describe('modules/manager/pub/artifacts', () => { 'bash -l -c "' + `install-tool ${params.sdk} 3.3.9` + ' && ' + - `${params.sdk} pub upgrade ${depName}` + + `${params.sdk} pub upgrade ${depNamesWithSpace}` + '"', }, ]); @@ -210,7 +224,7 @@ describe('modules/manager/pub/artifacts', () => { ]); expect(execSnapshots).toMatchObject([ { cmd: `install-tool ${params.sdk} 3.3.9` }, - { cmd: `${params.sdk} pub upgrade ${depName}` }, + { cmd: `${params.sdk} pub upgrade ${depNamesWithSpace}` }, ]); }); diff --git a/lib/modules/manager/pub/artifacts.ts b/lib/modules/manager/pub/artifacts.ts index 613cd364ce..a8ebcd9ff0 100644 --- a/lib/modules/manager/pub/artifacts.ts +++ b/lib/modules/manager/pub/artifacts.ts @@ -24,6 +24,9 @@ export async function updateArtifacts({ if (is.emptyArray(updatedDeps) && !isLockFileMaintenance) { logger.debug('No updated pub deps - returning null'); return null; + } else if (updatedDeps.length === 1 && updatedDeps[0].depName === 'flutter') { + logger.debug('Only updated flutter sdk - returning null'); + return null; } const lockFileName = getSiblingFileName(packageFileName, 'pubspec.lock'); @@ -43,13 +46,13 @@ export async function updateArtifacts({ if (isLockFileMaintenance) { cmd.push(`${toolName} pub upgrade`); } else { - cmd.push( - `${toolName} pub upgrade ${updatedDeps - .map((dep) => dep.depName) - .filter(is.string) - .map((dep) => quote(dep)) - .join(' ')}` - ); + const depNames = updatedDeps + .map((dep) => dep.depName) + .filter(is.string) + .filter((depName) => depName !== 'flutter') + .map(quote) + .join(' '); + cmd.push(`${toolName} pub upgrade ${depNames}`); } let constraint = config.constraints?.[toolName]; diff --git a/lib/modules/manager/pub/extract.spec.ts b/lib/modules/manager/pub/extract.spec.ts index 47e76e4d68..b4b0538615 100644 --- a/lib/modules/manager/pub/extract.spec.ts +++ b/lib/modules/manager/pub/extract.spec.ts @@ -1,42 +1,83 @@ -import { Fixtures } from '../../../../test/fixtures'; +import { codeBlock } from 'common-tags'; import { extractPackageFile } from '.'; describe('modules/manager/pub/extract', () => { describe('extractPackageFile', () => { - it('should return null if package does not contain any deps', () => { - const res = extractPackageFile('foo: bar', 'pubspec.yaml'); - expect(res).toBeNull(); + const packageFile = 'pubspec.yaml'; + + it('returns null if package does not contain any deps', () => { + const content = codeBlock` + environment: + sdk: ^3.0.0 + `; + const actual = extractPackageFile(content, packageFile); + expect(actual).toBeNull(); }); - it('should return null if package is invalid', () => { - const res = extractPackageFile( - Fixtures.get('update.yaml'), - 'pubspec.yaml' - ); - expect(res).toBeNull(); + it('returns null for invalid pubspec file', () => { + const content = codeBlock` + clarly: "invalid" "yaml" + `; + const actual = extractPackageFile(content, packageFile); + expect(actual).toBeNull(); }); - it('should return valid dependencies', () => { - const res = extractPackageFile( - Fixtures.get('extract.yaml'), - 'pubspec.yaml' - ); - expect(res).toEqual({ - datasource: 'dart', + it('returns valid dependencies', () => { + const dartDatasource = 'dart'; + const content = codeBlock` + environment: + sdk: ^3.0.0 + flutter: 2.0.0 + dependencies: + meta: 'something' + foo: 1.0.0 + bar: + version: 1.1.0 + baz: + non-sense: true + qux: false + dev_dependencies: + test: ^0.1.0 + build: + version: 0.0.1 + `; + const actual = extractPackageFile(content, packageFile); + expect(actual).toEqual({ deps: [ - { currentValue: '1', depName: 'foo', depType: 'dependencies' }, - { currentValue: '1', depName: 'bar', depType: 'dependencies' }, - { currentValue: null, depName: 'baz', depType: 'dependencies' }, - { currentValue: null, depName: 'qux', depType: 'dependencies' }, { - currentValue: '^0.1', + currentValue: '1.0.0', + depName: 'foo', + depType: 'dependencies', + datasource: dartDatasource, + }, + { + currentValue: '1.1.0', + depName: 'bar', + depType: 'dependencies', + datasource: dartDatasource, + }, + { + currentValue: '', + depName: 'baz', + depType: 'dependencies', + datasource: dartDatasource, + }, + { + currentValue: '^0.1.0', depName: 'test', depType: 'dev_dependencies', + datasource: dartDatasource, }, { - currentValue: '0.1', + currentValue: '0.0.1', depName: 'build', depType: 'dev_dependencies', + datasource: dartDatasource, + }, + { + currentValue: '2.0.0', + depName: 'flutter', + datasource: 'flutter-version', }, ], }); diff --git a/lib/modules/manager/pub/extract.ts b/lib/modules/manager/pub/extract.ts index 5e66b4d8bc..0c098f2495 100644 --- a/lib/modules/manager/pub/extract.ts +++ b/lib/modules/manager/pub/extract.ts @@ -1,64 +1,74 @@ -import { load } from 'js-yaml'; -import { logger } from '../../../logger'; +import is from '@sindresorhus/is'; import { DartDatasource } from '../../datasource/dart'; +import { FlutterVersionDatasource } from '../../datasource/flutter-version'; import type { PackageDependency, PackageFileContent } from '../types'; +import type { PubspecSchema } from './schema'; +import { parsePubspec } from './utils'; -function getDeps( - depsObj: { [x: string]: any }, - preset: { depType: string } +function extractFromSection( + pubspec: PubspecSchema, + sectionKey: keyof Pick<PubspecSchema, 'dependencies' | 'dev_dependencies'> ): PackageDependency[] { - if (!depsObj) { + const sectionContent = pubspec[sectionKey]; + if (!sectionContent) { return []; } - return Object.keys(depsObj).reduce((acc, depName) => { + + const datasource = DartDatasource.id; + const deps: PackageDependency[] = []; + for (const depName of Object.keys(sectionContent)) { if (depName === 'meta') { - return acc; + continue; } - const section = depsObj[depName]; - - let currentValue: string | null = null; - if (section?.version) { - currentValue = section.version.toString(); - } else if (section) { - if (typeof section === 'string') { - currentValue = section; - } - if (typeof section === 'number') { - currentValue = section.toString(); + let currentValue = sectionContent[depName]; + if (!is.string(currentValue)) { + const version = currentValue.version; + if (version) { + currentValue = version; + } else { + currentValue = ''; } } - const dep: PackageDependency = { ...preset, depName, currentValue }; + deps.push({ depName, depType: sectionKey, currentValue, datasource }); + } + + return deps; +} + +function extractFlutter(pubspec: PubspecSchema): PackageDependency[] { + const currentValue = pubspec.environment.flutter; + if (!currentValue) { + return []; + } - return [...acc, dep]; - }, [] as PackageDependency[]); + return [ + { + depName: 'flutter', + currentValue, + datasource: FlutterVersionDatasource.id, + }, + ]; } export function extractPackageFile( content: string, packageFile: string ): PackageFileContent | null { - try { - // TODO: fix me (#9610) - const doc = load(content, { json: true }) as any; - const deps = [ - ...getDeps(doc.dependencies, { - depType: 'dependencies', - }), - ...getDeps(doc.dev_dependencies, { - depType: 'dev_dependencies', - }), - ]; + const pubspec = parsePubspec(packageFile, content); + if (!pubspec) { + return null; + } - if (deps.length) { - return { - datasource: DartDatasource.id, - deps, - }; - } - } catch (err) { - logger.debug({ packageFile, err }, `Could not parse YAML`); + const deps = [ + ...extractFromSection(pubspec, 'dependencies'), + ...extractFromSection(pubspec, 'dev_dependencies'), + ...extractFlutter(pubspec), + ]; + + if (deps.length) { + return { deps }; } return null; } diff --git a/lib/modules/manager/pub/schema.ts b/lib/modules/manager/pub/schema.ts index dbf72be5c7..2d106c4add 100644 --- a/lib/modules/manager/pub/schema.ts +++ b/lib/modules/manager/pub/schema.ts @@ -1,5 +1,20 @@ import { z } from 'zod'; -import { Yaml } from '../../../util/schema-utils'; +import { LooseRecord, Yaml } from '../../../util/schema-utils'; + +const PubspecDependencySchema = LooseRecord( + z.string(), + z.union([z.string(), z.object({ version: z.string().optional() })]) +); + +export const PubspecSchema = z.object({ + environment: z.object({ sdk: z.string(), flutter: z.string().optional() }), + dependencies: PubspecDependencySchema.optional(), + dev_dependencies: PubspecDependencySchema.optional(), +}); + +export type PubspecSchema = z.infer<typeof PubspecSchema>; + +export const PubspecYaml = Yaml.pipe(PubspecSchema); export const PubspecLockSchema = z.object({ sdks: z.object({ diff --git a/lib/modules/manager/pub/utils.spec.ts b/lib/modules/manager/pub/utils.spec.ts index c236d4c0be..44c62ae4f6 100644 --- a/lib/modules/manager/pub/utils.spec.ts +++ b/lib/modules/manager/pub/utils.spec.ts @@ -1,10 +1,46 @@ import { codeBlock } from 'common-tags'; -import { parsePubspecLock } from './utils'; +import { parsePubspec, parsePubspecLock } from './utils'; describe('modules/manager/pub/utils', () => { - describe('parsePubspeckLock', () => { - const fileName = 'pubspec.lock'; + const fileName = 'fileName'; + const invalidYaml = codeBlock` + clearly: "invalid" "yaml" + `; + const invalidSchema = codeBlock` + clearly: invalid + `; + + describe('parsePubspec', () => { + it('load and parse successfully', () => { + const fileContent = codeBlock` + environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" + dependencies: + dep1: 1.0.0 + dev_dependencies: + dep2: 1.0.1 + `; + const actual = parsePubspec(fileName, fileContent); + expect(actual).toMatchObject({ + environment: { sdk: '>=3.0.0 <4.0.0', flutter: '>=3.10.0' }, + dependencies: { dep1: '1.0.0' }, + dev_dependencies: { dep2: '1.0.1' }, + }); + }); + + it('invalid yaml', () => { + const actual = parsePubspec(fileName, invalidYaml); + expect(actual).toBeNull(); + }); + it('invalid schema', () => { + const actual = parsePubspec(fileName, invalidSchema); + expect(actual).toBeNull(); + }); + }); + + describe('parsePubspeckLock', () => { it('load and parse successfully', () => { const pubspecLock = codeBlock` sdks: @@ -18,12 +54,12 @@ describe('modules/manager/pub/utils', () => { }); it('invalid yaml', () => { - const actual = parsePubspecLock(fileName, 'clearly-invalid'); + const actual = parsePubspecLock(fileName, invalidYaml); expect(actual).toBeNull(); }); it('invalid schema', () => { - const actual = parsePubspecLock(fileName, 'clearly:\n\tinvalid: lock'); + const actual = parsePubspecLock(fileName, invalidSchema); expect(actual).toBeNull(); }); }); diff --git a/lib/modules/manager/pub/utils.ts b/lib/modules/manager/pub/utils.ts index 7aa7f05e0e..dda0df348e 100644 --- a/lib/modules/manager/pub/utils.ts +++ b/lib/modules/manager/pub/utils.ts @@ -1,5 +1,23 @@ import { logger } from '../../../logger'; -import { PubspecLockSchema, PubspecLockYaml } from './schema'; +import { + PubspecLockSchema, + PubspecLockYaml, + PubspecSchema, + PubspecYaml, +} from './schema'; + +export function parsePubspec( + fileName: string, + fileContent: string +): PubspecSchema | null { + const res = PubspecYaml.safeParse(fileContent); + if (res.success) { + return res.data; + } else { + logger.debug({ err: res.error, fileName }, 'Error parsing pubspec.'); + } + return null; +} export function parsePubspecLock( fileName: string, @@ -11,7 +29,7 @@ export function parsePubspecLock( } else { logger.debug( { err: res.error, fileName }, - `Error parsing pubspec lockfile.` + 'Error parsing pubspec lockfile.' ); } return null; -- GitLab