publish method
Publish a previous preparation. A retry never repacks or overwrites a registry version. Already published packages keep their existing tags.
Implementation
Future<void> publish({Uri? registry, String? tag, NpmAccess? access}) async {
final destination = output.absolute;
await checkOutputPath(destination.path);
final record = await _readJson(
File('${destination.path}/.napi-release.json'),
);
if (record['format'] != 1 ||
record['name'] != name ||
record['version'] != version) {
throw const FormatException(
'Prepared release name or version does not match this declaration.',
);
}
packageMetadata({'name': name, 'version': version});
final rows = record['packages'];
if (rows is! List || rows.isEmpty) {
throw const FormatException('Prepared release has no packages.');
}
final packages = <_PreparedPackage>[];
final names = <String>{};
for (final row in rows) {
if (row is! Map ||
row['version'] != version ||
row['name'] is! String ||
row['tarball'] is! String ||
row['integrity'] is! String) {
throw const FormatException('Invalid prepared package.');
}
final packageName = row['name'] as String;
packageMetadata({'name': packageName, 'version': version});
final relative = row['tarball'] as String;
if (!names.add(packageName) ||
(packageName != name && !packageName.startsWith('$name-')) ||
path.posix.dirname(relative) != 'tarballs' ||
path.posix.basename(relative).contains('\\') ||
!relative.endsWith('.tgz')) {
throw const FormatException(
'Invalid prepared package name or tarball path.',
);
}
final file = File('${destination.path}/$relative');
if (await _integrity(file) != row['integrity']) {
throw StateError('Prepared tarball changed: $relative');
}
packages.add(
_PreparedPackage(packageName, file, row['integrity'] as String),
);
}
if (packages.last.name != name) {
throw const FormatException('The entry package must be last.');
}
final config = record['publishConfig'];
if (config != null && config is! Map) {
throw const FormatException('publishConfig must be a map.');
}
final scope = name.startsWith('@') ? name.split('/').first : null;
final configured =
registry?.toString() ??
(scope == null ? null : (config as Map?)?['$scope:registry']) ??
(config as Map?)?['registry'] ??
(scope == null ? null : await _npmConfig('$scope:registry')) ??
await _npmConfig('registry');
final selected = configured is String ? Uri.tryParse(configured) : null;
if (selected == null ||
!{'https', 'http'}.contains(selected.scheme) ||
selected.host.isEmpty ||
selected.userInfo.isNotEmpty ||
selected.hasQuery ||
selected.hasFragment) {
throw ArgumentError('npm registry must be an absolute HTTP(S) URL.');
}
final registryOptions = [
'--registry=${selected.toString()}',
if (scope != null) '--$scope:registry=${selected.toString()}',
];
final done = <String>{};
try {
// Discover all conflicts before writing any new registry versions.
for (final package in packages) {
if (await _published(package, registryOptions)) done.add(package.name);
}
for (final package in packages) {
if (done.contains(package.name)) {
stdout.writeln('Already published ${package.name}@$version');
continue;
}
// Verify again immediately before npm opens the prepared tarball.
if (await _integrity(package.tarball) != package.integrity) {
throw StateError('Prepared tarball changed: ${package.tarball.path}');
}
final command = await _npmCommand();
final process = await Process.start(command.first, [
...command.skip(1),
'publish',
package.tarball.path,
'--ignore-scripts',
...registryOptions,
if (tag != null) '--tag=$tag',
if (access != null) '--access=${access.name}',
], mode: ProcessStartMode.inheritStdio);
final code = await process.exitCode;
// A network failure can occur after the registry accepted the upload.
// Read back the digest before deciding whether this package is pending.
if (!await _published(package, registryOptions)) {
throw StateError(
'npm publish did not confirm ${package.name}@$version (exit $code).',
);
}
done.add(package.name);
stdout.writeln('Published ${package.name}@$version');
}
} catch (error) {
throw StateError(
'$error\nConfirmed: ${done.isEmpty ? 'none' : done.join(', ')}.\n'
'Pending: ${packages.where((p) => !done.contains(p.name)).map((p) => p.name).join(', ')}. '
'Retry publish with the same prepared release.',
);
}
}