diff:
@@ -9,7 +9,6 @@ use function array_key_exists;
use function array_keys;
use function array_unique;
use function current;
-use function get_class;
use function implode;
use function is_array;
use function is_object;
@@ -330,7 +329,7 @@ abstract class AbstractValidator implements
if (is_object($value)) {
$value = method_exists($value, '__toString')
? (string) $value
- : get_class($value) . ' object';
+ : $value::class . ' object';
} elseif (is_array($value)) {
$value = var_export($value, true);
} else {

diff:
@@ -6,7 +6,6 @@ use Traversable;
use function array_key_exists;
use function class_exists;
-use function get_class;
use function gettype;
use function is_array;
use function is_object;
@@ -114,7 +113,7 @@ class Barcode extends AbstractValidator
throw new Exception\InvalidArgumentException(
sprintf(
'Adapter %s does not implement Laminas\\Validator\\Barcode\\AdapterInterface',
- is_object($adapter) ? get_class($adapter) : gettype($adapter)
+ is_object($adapter) ? $adapter::class : gettype($adapter)
)
);
}

diff:
@@ -10,7 +10,6 @@ use function array_unique;
use function array_values;
use function func_get_arg;
use function func_num_args;
-use function get_class;
use function gettype;
use function hash_algos;
use function hash_file;
@@ -129,7 +128,7 @@ class Hash extends AbstractValidator
if (! is_string($value)) {
throw new Exception\InvalidArgumentException(sprintf(
'Hash must be a string, %s received',
- is_object($value) ? get_class($value) : gettype($value)
+ is_object($value) ? $value::class : gettype($value)
));
}
$this->options['hash'][$value] = $algorithm;

diff:
@@ -7,7 +7,6 @@ use Laminas\Stdlib\ArrayUtils;
use Traversable;
use function array_key_exists;
-use function get_class;
use function gettype;
use function is_array;
use function is_int;
@@ -173,7 +172,7 @@ class Identical extends AbstractValidator
throw new Exception\InvalidArgumentException(sprintf(
'Context passed to %s must be array, ArrayObject or null; received "%s"',
__METHOD__,
- is_object($context) ? get_class($context) : gettype($context)
+ is_object($context) ? $context::class : gettype($context)
));
}

diff:
@@ -12,7 +12,6 @@ use function array_intersect_assoc;
use function array_pop;
use function array_unshift;
use function explode;
-use function get_class;
use function gettype;
use function http_build_query;
use function implode;
@@ -182,7 +181,7 @@ class Uri implements UriInterface
} elseif ($uri !== null) {
throw new Exception\InvalidArgumentException(sprintf(
'Expecting a string or a URI object, received "%s"',
- is_object($uri) ? get_class($uri) : gettype($uri)
+ is_object($uri) ? $uri::class : gettype($uri)
));
}
}
@@ -1010,7 +1009,7 @@ class Uri implements UriInterface
if (! is_string($userInfo)) {
throw new Exception\InvalidArgumentException(sprintf(
'Expecting a string, got %s',
- is_object($userInfo) ? get_class($userInfo) : gettype($userInfo)
+ is_object($userInfo) ? $userInfo::class : gettype($userInfo)
));
}
@@ -1036,7 +1035,7 @@ class Uri implements UriInterface
if (! is_string($path)) {
throw new Exception\InvalidArgumentException(sprintf(
'Expecting a string, got %s',
- is_object($path) ? get_class($path) : gettype($path)
+ is_object($path) ? $path::class : gettype($path)
));
}
@@ -1063,7 +1062,7 @@ class Uri implements UriInterface
if (! is_string($input)) {
throw new Exception\InvalidArgumentException(sprintf(
'Expecting a string, got %s',
- is_object($input) ? get_class($input) : gettype($input)
+ is_object($input) ? $input::class : gettype($input)
));
}
@@ -1093,7 +1092,7 @@ class Uri implements UriInterface
if (! is_string($uriString)) {
throw new Exception\InvalidArgumentException(sprintf(
'Expecting a string, got %s',
- is_object($uriString) ? get_class($uriString) : gettype($uriString)
+ is_object($uriString) ? $uriString::class : gettype($uriString)
));
}

diff:
@@ -18,7 +18,6 @@ use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use function assert;
-use function get_class;
use function sprintf;
/**
@@ -95,7 +94,7 @@ final class ValidatorPluginManagerTest extends TestCase
} catch (Exception $e) {
self::fail(sprintf(
'Unexpected exception of type "%s" when testing for invalid validator types',
- get_class($e)
+ $e::class
));
}
}
@@ -111,7 +110,7 @@ final class ValidatorPluginManagerTest extends TestCase
} catch (Exception $e) {
self::fail(sprintf(
'Unexpected exception of type "%s" when testing for invalid validator types',
- get_class($e)
+ $e::class
));
}
}

original_message:
qa: replace `get_class()` with `$instance::class`

Signed-off-by: George Steel <george@net-glue.co.uk>


message: `$type` can never be an object

change_type: MODIFY

diff:
@@ -6,12 +6,10 @@ use DateTimeZone;
use function array_key_exists;
use function array_search;
-use function get_class;
use function gettype;
use function in_array;
use function is_array;
use function is_int;
-use function is_object;
use function is_string;
use function sprintf;
@@ -91,8 +89,7 @@ class Timezone extends AbstractValidator
throw new Exception\InvalidArgumentException(sprintf(
'Unknown type "%s" provided',
is_string($type) || is_int($type)
- ? $type
- : (is_object($type) ? get_class($type) : gettype($type))
+ ? $type : gettype($type)
));
}

license: BSD 3-Clause New or Revised License

repo: laminas/laminas-validator

original_message:
`$type` can never be an object

Signed-off-by: George Steel <george@net-glue.co.uk>


diff:
@@ -42,7 +42,6 @@ use Zend\Validator\Sitemap\Lastmod;
use Zend\Validator\Sitemap\Loc;
use Zend\Validator\Sitemap\Priority;
-use function get_class;
use function gettype;
use function is_object;
use function method_exists;
@@ -572,6 +571,8 @@ class ValidatorPluginManager extends AbstractPluginManager
* Validate plugin instance
*
* {@inheritDoc}
+ * @param mixed $instance
+ * @psalm-assert InstanceType $instance
*/
public function validate($instance)
{
@@ -580,7 +581,7 @@ class ValidatorPluginManager extends AbstractPluginManager
'%s expects only to create instances of %s; %s is invalid',
static::class,
$this->instanceOf,
- is_object($instance) ? get_class($instance) : gettype($instance)
+ is_object($instance) ? $instance::class : gettype($instance)
));
}
}
@@ -601,7 +602,7 @@ class ValidatorPluginManager extends AbstractPluginManager
} catch (InvalidServiceException $e) {
throw new Exception\RuntimeException(sprintf(
'Plugin of type %s is invalid; must implement %s',
- is_object($plugin) ? get_class($plugin) : gettype($plugin),
+ is_object($plugin) ? $plugin::class : gettype($plugin),
ValidatorInterface::class
), $e->getCode(), $e);
}

original_message:
Add psalm annotations for `validate($instance)`

Signed-off-by: George Steel <george@net-glue.co.uk>


message: Regenerate baseline

diff:
(bool) $domain
(bool) $mx
</RedundantCastGivenDocblockType>
- <RedundantConditionGivenDocblockType occurrences="1">
+ <RedundantConditionGivenDocblockType occurrences="2">
+ array_combine($mxHosts, $weight)
is_string($value)
</RedundantConditionGivenDocblockType>
<TypeDoesNotContainType occurrences="1">
</RedundantCastGivenDocblockType>
</file>
<file src="src/Timezone.php">
- <DocblockTypeContradiction occurrences="2">
- is_object($type)
+ <DocblockTypeContradiction occurrences="1">
is_string($type)
</DocblockTypeContradiction>
<InvalidOperand occurrences="1">
array_search($value, $this->constants)
</InvalidOperand>
- <MixedArgument occurrences="2">
- $type
- </MixedArgument>
+ <MixedArgument occurrences="1"/>
<MixedAssignment occurrences="3">
$opts['type']
$type
$type
$type
</MixedOperand>
- <PossiblyFalseArgument occurrences="4">
+ <PossiblyFalseArgument occurrences="2">
$abbrs
$abbrs
- $locations
- $locations
</PossiblyFalseArgument>
<PossiblyFalseOperand occurrences="1">
array_search($value, $this->constants)
<PossiblyUndefinedVariable occurrences="1">
$opts
</PossiblyUndefinedVariable>
- <RedundantConditionGivenDocblockType occurrences="2">
+ <RedundantConditionGivenDocblockType occurrences="1">
gettype($type)
- is_object($type) ? get_class($type) : gettype($type)
</RedundantConditionGivenDocblockType>
</file>
<file src="src/Uri.php">
<MoreSpecificReturnType occurrences="1"/>
</file>
<file src="test/File/FilesSizeTest.php">
- <InvalidArgument occurrences="1">
- [$this, 'errorHandler']
- </InvalidArgument>
<InvalidDocblock occurrences="1">
public function basicDataProvider(): array
</InvalidDocblock>

original_message:
Regenerate baseline

Signed-off-by: George Steel <george@net-glue.co.uk>


message: qa: cs fix

diff:
@@ -571,6 +571,7 @@ class ValidatorPluginManager extends AbstractPluginManager
* Validate plugin instance
*
* {@inheritDoc}
+ *
* @param mixed $instance
* @psalm-assert InstanceType $instance
*/

original_message:
qa: cs fix

Signed-off-by: George Steel <george@net-glue.co.uk>


diff:
<?xml version="1.0" encoding="UTF-8"?>
-<files psalm-version="4.28.0@52e96bea381e6cb07a672aefec791a5817694a26">
+<files psalm-version="4.29.0@7ec5ffbd5f68ae03782d7fd33fff0c45a69f95b3">
<file src="bin/update_hostname_validator.php">
<MissingClosureParamType occurrences="2">
$domain
</RedundantConditionGivenDocblockType>
</file>
<file src="src/File/Upload.php">
- <DocblockTypeContradiction occurrences="2">
+ <DocblockTypeContradiction occurrences="3">
+ $this->options['files'] === null
$this->options['files'] === null
null === $files
</DocblockTypeContradiction>

original_message:
Expand baseline with newly discovered issues

Signed-off-by: George Steel <george@net-glue.co.uk>


message: Remove redundant assertions

diff:
@@ -16,7 +16,6 @@ use function array_combine;
use function array_count_values;
use function array_map;
use function array_shift;
-use function assert;
use function ceil;
use function date_default_timezone_get;
use function explode;
@@ -409,7 +408,6 @@ class DateStep extends Date
} else {
$baseDate = $baseDate->sub($minimumInterval);
}
- assert($baseDate !== false);
}
while (
@@ -422,8 +420,6 @@ class DateStep extends Date
$baseDate = $baseDate->sub($step);
}
- assert($baseDate !== false);
-
// phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
if ($baseDate == $valueDate) {
return true;

original_message:
Remove redundant assertions

Signed-off-by: George Steel <george@net-glue.co.uk>


message: Introduce `IsJsonString` validator

change_type: ADD

diff:
+<?php
+
+declare(strict_types=1);
+
+namespace Laminas\Validator;
+
+use JsonException;
+
+use function gettype;
+use function is_float;
+use function is_int;
+use function is_numeric;
+use function is_string;
+use function json_decode;
+use function str_starts_with;
+
+use const JSON_ERROR_DEPTH;
+use const JSON_THROW_ON_ERROR;
+
+/**
+ * @psalm-type CustomOptions = array{
+ * allow: int-mask-of<self::ALLOW_*>,
+ * maxDepth: positive-int,
+ * }
+ * @psalm-import-type AbstractOptions from AbstractValidator
+ * @psalm-type Options = AbstractOptions|CustomOptions
+ */
+final class IsJsonString extends AbstractValidator
+{
+ public const ERROR_NOT_STRING = 'errorNotString';
+ public const ERROR_TYPE_NOT_ALLOWED = 'errorTypeNotAllowed';
+ public const ERROR_MAX_DEPTH_EXCEEDED = 'errorMaxDepthExceeded';
+ public const ERROR_INVALID_JSON = 'errorInvalidJson';
+
+ public const ALLOW_INT = 0b0000001;
+ public const ALLOW_FLOAT = 0b0000010;
+ public const ALLOW_BOOL = 0b0000100;
+ public const ALLOW_ARRAY = 0b0001000;
+ public const ALLOW_OBJECT = 0b0010000;
+ public const ALLOW_ALL = 0b0011111;
+
+ /** @var array<self::ERROR_*, non-empty-string> */
+ protected $messageTemplates = [
+ self::ERROR_NOT_STRING => 'Expected a string but %type% was received',
+ self::ERROR_TYPE_NOT_ALLOWED => 'Received a JSON %type% but this type is not acceptable',
+ self::ERROR_MAX_DEPTH_EXCEEDED => 'The decoded JSON payload exceeds the allowed depth of %maxDepth%',
+ self::ERROR_INVALID_JSON => 'An invalid JSON payload was received',
+ ];
+
+ /** @var array<string, string> */
+ protected $messageVariables = [
+ 'type' => 'type',
+ 'maxDepth' => 'maxDepth',
+ ];
+
+ protected ?string $type = null;
+ /** @var int-mask-of<self::ALLOW_*> */
+ protected int $allow = self::ALLOW_ALL;
+ /** @var positive-int */
+ protected int $maxDepth = 512;
+
+ /** @param int-mask-of<self::ALLOW_*> $type */
+ public function setAllow(int $type): void
+ {
+ $this->allow = $type;
+ }
+
+ /** @param positive-int $maxDepth */
+ public function setMaxDepth(int $maxDepth): void
+ {
+ $this->maxDepth = $maxDepth;
+ }
+
+ public function isValid(mixed $value): bool
+ {
+ if (! is_string($value)) {
+ $this->error(self::ERROR_NOT_STRING);
+ $this->type = gettype($value);
+
+ return false;
+ }
+
+ if (is_numeric($value)) {
+ /** @psalm-var mixed $value */
+ $value = json_decode($value);
+
+ if (is_int($value) && ! $this->isAllowed(self::ALLOW_INT)) {
+ $this->error(self::ERROR_TYPE_NOT_ALLOWED);
+ $this->type = 'int';
+
+ return false;
+ }
+
+ if (is_float($value) && ! $this->isAllowed(self::ALLOW_FLOAT)) {
+ $this->error(self::ERROR_TYPE_NOT_ALLOWED);
+ $this->type = 'float';
+
+ return false;
+ }
+
+ return true;
+ }
+
+ if ($value === 'true' || $value === 'false') {
+ if (! $this->isAllowed(self::ALLOW_BOOL)) {
+ $this->error(self::ERROR_TYPE_NOT_ALLOWED);
+ $this->type = 'boolean';
+
+ return false;
+ }
+
+ return true;
+ }
+
+ if (str_starts_with($value, '[') && ! $this->isAllowed(self::ALLOW_ARRAY)) {
+ $this->error(self::ERROR_TYPE_NOT_ALLOWED);
+ $this->type = 'array';
+
+ return false;
+ }
+
+ if (str_starts_with($value, '{') && ! $this->isAllowed(self::ALLOW_OBJECT)) {
+ $this->error(self::ERROR_TYPE_NOT_ALLOWED);
+ $this->type = 'object';
+
+ return false;
+ }
+
+ try {
+ /** @psalm-suppress UnusedFunctionCall */
+ json_decode($value, true, $this->maxDepth, JSON_THROW_ON_ERROR);
+
+ return true;
+ } catch (JsonException $e) {
+ if ($e->getCode() === JSON_ERROR_DEPTH) {
+ $this->error(self::ERROR_MAX_DEPTH_EXCEEDED);
+
+ return false;
+ }
+
+ $this->error(self::ERROR_INVALID_JSON);
+
+ return false;
+ }
+ }
+
+ /** @param self::ALLOW_* $flag */
+ private function isAllowed(int $flag): bool
+ {
+ return ($this->allow & $flag) === $flag;
+ }
+}

diff:
@@ -420,6 +420,7 @@ class ValidatorPluginManager extends AbstractPluginManager
Isbn::class => InvokableFactory::class,
IsCountable::class => InvokableFactory::class,
IsInstanceOf::class => InvokableFactory::class,
+ IsJsonString::class => InvokableFactory::class,
LessThan::class => InvokableFactory::class,
NotEmpty::class => InvokableFactory::class,
I18nValidator\PhoneNumber::class => InvokableFactory::class,

diff:
+<?php
+
+declare(strict_types=1);
+
+namespace LaminasTest\Validator;
+
+use Laminas\ServiceManager\ServiceManager;
+use Laminas\Validator\IsJsonString;
+use Laminas\Validator\ValidatorPluginManager;
+use PHPUnit\Framework\TestCase;
+
+use function json_encode;
+
+class IsJsonStringTest extends TestCase
+{
+ /**
+ * @return array<string, array{
+ * 0: int-mask-of<IsJsonString::ALLOW_*>,
+ * 1: string,
+ * 2: bool,
+ * 3: IsJsonString::ERROR_*|null,
+ * }>
+ */
+ public function allowProvider(): array
+ {
+ // phpcs:disable Generic.Files.LineLength
+ return [
+ 'Standalone Integer' => [IsJsonString::ALLOW_INT, '1', true, null],
+ 'Standalone Integer, Not Allowed' => [IsJsonString::ALLOW_ALL ^ IsJsonString::ALLOW_INT, '1', false, IsJsonString::ERROR_TYPE_NOT_ALLOWED],
+ 'Standalone Float' => [IsJsonString::ALLOW_FLOAT, '1.23', true, null],
+ 'Standalone Float, Not Allowed' => [IsJsonString::ALLOW_ALL ^ IsJsonString::ALLOW_FLOAT, '1.23', false, IsJsonString::ERROR_TYPE_NOT_ALLOWED],
+ 'Standalone True' => [IsJsonString::ALLOW_BOOL, 'true', true, null],
+ 'Standalone False' => [IsJsonString::ALLOW_BOOL, 'false', true, null],
+ 'Case Sensitive True' => [IsJsonString::ALLOW_BOOL, 'TRUE', false, IsJsonString::ERROR_INVALID_JSON],
+ 'Case Sensitive False' => [IsJsonString::ALLOW_BOOL, 'FALSE', false, IsJsonString::ERROR_INVALID_JSON],
+ 'Standalone True, Not Allowed' => [IsJsonString::ALLOW_ALL ^ IsJsonString::ALLOW_BOOL, 'true', false, IsJsonString::ERROR_TYPE_NOT_ALLOWED],
+ 'Standalone False, Not Allowed' => [IsJsonString::ALLOW_ALL ^ IsJsonString::ALLOW_BOOL, 'false', false, IsJsonString::ERROR_TYPE_NOT_ALLOWED],
+ 'List Notation' => [IsJsonString::ALLOW_ARRAY, '["Some","List"]', true, null],
+ 'List Notation, Not Allowed' => [IsJsonString::ALLOW_ALL ^ IsJsonString::ALLOW_ARRAY, '["Some","List"]', false, IsJsonString::ERROR_TYPE_NOT_ALLOWED],
+ 'Object Notation' => [IsJsonString::ALLOW_OBJECT, '{"Some":"Object"}', true, null],
+ 'Object Notation, Not Allowed' => [IsJsonString::ALLOW_ALL ^ IsJsonString::ALLOW_OBJECT, '{"Some":"Object"}', false, IsJsonString::ERROR_TYPE_NOT_ALLOWED],
+ ];
+ // phpcs:enable Generic.Files.LineLength
+ }
+
+ /**
+ * @param int-mask-of<IsJsonString::ALLOW_*> $allowed
+ * @param IsJsonString::ERROR_*|null $expectedErrorKey
+ * @dataProvider allowProvider
+ */
+ public function testBasicBehaviour(int $allowed, string $input, bool $expect, string|null $expectedErrorKey): void
+ {
+ $validator = new IsJsonString();
+ $validator->setAllow($allowed);
+ $result = $validator->isValid($input);
+ self::assertSame($expect, $result);
+ if ($expectedErrorKey !== null) {
+ self::assertArrayHasKey($expectedErrorKey, $validator->getMessages());
+ }
+ }
+
+ /** @return list<array{0: mixed}> */
+ public function provideThingsThatAreNotStrings(): array
+ {
+ return [
+ [true],
+ [false],
+ [
+ new class () {
+ },
+ ],
+ [[]],
+ [1],
+ [1.23],
+ [null],
+ ];
+ }
+
+ /** @dataProvider provideThingsThatAreNotStrings */
+ public function testThatNonStringsAreInvalid(mixed $input): void
+ {
+ $validator = new IsJsonString();
+ self::assertFalse($validator->isValid($input));
+ self::assertArrayHasKey(IsJsonString::ERROR_NOT_STRING, $validator->getMessages());
+ }
+
+ public function testThatMaxDepthCanBeExceeded(): void
+ {
+ $validator = new IsJsonString();
+ $input = json_encode([
+ 'foo' => [
+ 'bar' => [
+ 'baz' => 'goats',
+ ],
+ ],
+ ]);
+
+ $validator->setMaxDepth(1);
+ self::assertFalse($validator->isValid($input));
+ self::assertArrayHasKey(IsJsonString::ERROR_MAX_DEPTH_EXCEEDED, $validator->getMessages());
+ }
+
+ /** @return array<array-key, array{0: string}> */
+ public function pluginManagerNameProvider(): array
+ {
+ return [
+ [IsJsonString::class],
+ ];
+ }
+
+ /** @dataProvider pluginManagerNameProvider */
+ public function testThatTheValidatorCanBeRetrievedFromThePluginManagerWithDefaultConfiguration(string $name): void
+ {
+ $pluginManager = new ValidatorPluginManager(new ServiceManager());
+ $validator = $pluginManager->get($name);
+ self::assertInstanceOf(IsJsonString::class, $validator);
+ }
+
+ /** @return array<string, array{0: string}> */
+ public function invalidStringProvider(): array
+ {
+ return [
+ 'Empty String' => [''],
+ 'Invalid Object' => ['{nuts}'],
+ 'Invalid Array' => ['["Foo"'],
+ 'Arbitrary String' => ['goats are friends'],
+ ];
+ }
+
+ /** @dataProvider invalidStringProvider */
+ public function testInvalidStrings(string $input): void
+ {
+ $validator = new IsJsonString();
+ self::assertFalse($validator->isValid($input));
+ self::assertArrayHasKey(IsJsonString::ERROR_INVALID_JSON, $validator->getMessages());
+ }
+}

original_message:
Introduce `IsJsonString` validator

Signed-off-by: George Steel <george@net-glue.co.uk>


message: Baseline update

diff:
<?xml version="1.0" encoding="UTF-8"?>
-<files psalm-version="5.3.0@b6faa3e96b8eb50ec71384c53799b8a107236bb6">
+<files psalm-version="5.4.0@62db5d4f6a7ae0a20f7cc5a4952d730272fc0863">
<file src="bin/update_hostname_validator.php">
<MissingClosureParamType occurrences="2">
$domain
</MixedArgument>
</file>
<file src="src/AbstractValidator.php">
- <LessSpecificReturnStatement occurrences="4">
+ <LessSpecificReturnStatement occurrences="5">
+ $this
$this
$this
$this
<PropertyTypeCoercion occurrences="1">
$this->abstractOptions
</PropertyTypeCoercion>
- <UndefinedThisPropertyAssignment occurrences="30">
+ <UndefinedThisPropertyAssignment occurrences="31">
+ $this->options
$this->options
$this->options
$this->options
is_array($options)
is_string($value)
</DocblockTypeContradiction>
- <MixedArgument occurrences="12">
+ <MixedArgument occurrences="2">
$regexChar
$regexKey
- $value
- 128 + ($value & 63)
- 128 + ($value & 63)
- 128 + ($value & 63)
- 128 + (($value >> 12) & 63)
- 128 + (($value >> 6) & 63)
- 128 + (($value >> 6) & 63)
- 192 + ($value >> 6)
- 224 + ($value >> 12)
- 240 + ($value >> 18)
- </MixedArgument>
- <MixedAssignment occurrences="10">
- $decoded[$i]
+ </MixedArgument>
+ <MixedAssignment occurrences="8">
$partRegexChars
$regexChar
$regexChars
$temp['ipValidator']
$temp['useIdnCheck']
$temp['useTldCheck']
- $value
</MixedAssignment>
<MixedInferredReturnType occurrences="4">
Ip
bool
int
</MixedInferredReturnType>
- <MixedOperand occurrences="19">
+ <MixedOperand occurrences="1">
$regexChars
- $value
- $value
- $value
- $value
- $value
- $value
- $value
- $value
- $value
- $value & 63
- $value & 63
- $value & 63
- $value >> 12
- $value >> 18
- $value >> 6
- ($value >> 12) & 63
- ($value >> 6) & 63
- ($value >> 6) & 63
</MixedOperand>
<MixedReturnStatement occurrences="4">
$this->options['allow']

original_message:
Baseline update

Signed-off-by: George Steel <george@net-glue.co.uk>


diff:
+# IsJsonString Validator
+
+`Laminas\Validator\IsJsonString` allows you to validate whether a given value is a string that will be successfully decoded by `json_decode`.
+
+## Basic Usage
+
+```php
+$validator = new Laminas\Validator\IsJsonString();
+$input = '{"some":"json"}';
+
+if ($validator->isValid($input)) {
+ // $input can be successfully decoded
+} else {
+ // $input is not a valid JSON string
+}
+```
+
+## Restricting Acceptable JSON types
+
+`json_decode` accepts numeric strings representing integers and floating point numbers, booleans, arrays and objects.
+You can restrict what is considered valid input using the `allow` option of the validator.
+
+```php
+use Laminas\Validator\IsJsonString;
+
+$validator = new IsJsonString([
+ 'allow' => IsJsonString::ALLOW_ALL ^ IsJsonString::ALLOW_BOOL,
+]);
+
+$validator->isValid('true'); // false
+```
+
+The `allow` option is a bit mask of the `ALLOW_*` constants in `IsJsonString`:
+
+- `IsJsonString::ALLOW_INT` - Accept numeric such as "1"
+- `IsJsonString::ALLOW_FLOAT` - Accept numeric strings such as "1.234"
+- `IsJsonString::ALLOW_BOOL` - Accept "true" and "false"
+- `IsJsonString::ALLOW_ARRAY` - Accept JSON arrays such as `["One", "Two"]`
+- `IsJsonString::ALLOW_OBJECT` - Accept JSON objects such as `{"Some":"Object"}`
+- `IsJsonString::ALLOW_ALL` - A convenience constant allowing all of the above _(Also the default)_.
+
+The `allow` option also has a companion setter method `setAllow`. For example, to only accept arrays and objects:
+
+```php
+use Laminas\Validator\IsJsonString;
+
+$validator = new IsJsonString();
+$validator->setAllow(IsJsonString::ALLOW_ARRAY | IsJsonString::ALLOW_OBJECT);
+$validator->isValid("1.234"); // false
+```
+
+## Restricting Max Object or Array Nesting Level
+
+If you wish to restrict the nesting level of arrays and objects that are considered valid, the validator accepts a `maxDepth` option. The default value of this option is `512` - the same default value as `json_decode`.
+
+```php
+$validator = new Laminas\Validator\IsJsonString(['maxDepth' => 2]);
+$validator->isValid('{"nested": {"object: "here"}}'); // false
+```
+
+Again, the max nesting level allowed has a companion setter method:
+
+```php
+$validator = new Laminas\Validator\IsJsonString();
+$validator->setMaxDepth(10);
+```

diff:
@@ -31,6 +31,7 @@ nav:
- Isbn: validators/isbn.md
- IsCountable: validators/is-countable.md
- IsInstanceOf: validators/isinstanceof.md
+ - IsJsonString: validators/is-json-string.md
- LessThan: validators/less-than.md
- NotEmpty: validators/not-empty.md
- Regex: validators/regex.md

original_message:
Add documentation for the `IsJsonString` validator

Signed-off-by: George Steel <george@net-glue.co.uk>


diff:
@@ -29,7 +29,7 @@ class Explode extends AbstractValidator implements ValidatorPluginManagerAwareIn
/** @var array */
protected $messageVariables = [];
- /** @var string */
+ /** @var non-empty-string */
protected $valueDelimiter = ',';
/** @var ValidatorInterface|null */
@@ -41,7 +41,7 @@ class Explode extends AbstractValidator implements ValidatorPluginManagerAwareIn
/**
* Sets the delimiter string that the values will be split upon
*
- * @param string $delimiter
+ * @param non-empty-string $delimiter
* @return $this
*/
public function setValueDelimiter($delimiter)
@@ -53,7 +53,7 @@ class Explode extends AbstractValidator implements ValidatorPluginManagerAwareIn
/**
* Returns the delimiter string that the values will be split upon
*
- * @return string
+ * @return non-empty-string
*/
public function getValueDelimiter()
{

original_message:
Refine explode delimiter to non-empty-string to match the expected type by explode

Signed-off-by: George Steel <george@net-glue.co.uk>


message:
AutoCasperNBI
Updated CodeSignatureVerifier for new cert

diff:
<key>input_path</key>
<string>%RECIPE_CACHE_DIR%/%NAME%/AutoCasperNBI.app</string>
<key>requirement</key>
- <string>identifier "com.macmule.AutoCasperNBI" and anchor apple generic and certificate leaf[subject.CN] = "Mac Developer: Ben Toms (L7N6BNQQEU)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */</string>
+ <string>anchor apple generic and identifier "com.macmule.AutoCasperNBI" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "7WHVYYF52U")</string>
</dict>
<key>Processor</key>
<string>CodeSignatureVerifier</string>

license: Apache License 2.0

repo: autopkg/homebysix-recipes


message:
GoToMeeting.download
CodeSignatureVerifier requirement update

diff:
<key>input_path</key>
<string>%RECIPE_CACHE_DIR%/%NAME%/Applications/%NAME% (%build%).app</string>
<key>requirement</key>
- <string>identifier "com.logmein.GoToMeeting" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "3Y9EC8WH48"</string>
+ <string>identifier "com.logmein.GoToMeeting" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = GFNFVT632V</string>
</dict>
<key>Processor</key>
<string>CodeSignatureVerifier</string>


message:
ParallelsDesktop.munki.recipe
Changed recipe to use  the .pkg as the parent, as the postinstall
correctly installs Parallels Desktop 13

diff:
<string>Virtualization tool for running Windows apps without rebooting.</string>
<key>developer</key>
<string>Parallels, Inc.</string>
+ <key>minimum_os_version</key>
+ <string>10.10</string>
<key>display_name</key>
<string>Parallels Desktop</string>
<key>name</key>
<string>%NAME%</string>
- <key>postinstall_script</key>
- <string>#!/bin/bash
-chflags nohidden "/Applications/Parallels Desktop.app"
- </string>
<key>unattended_install</key>
<true/>
+ <key>uninstall_method</key>
+ <string>uninstall_script</string>
+ <key>uninstall_script</key>
+ <string>#!/bin/bash
+
+# App bundle to remove
+app_Bundle="/Applications/Parallels Desktop.app/"
+
+if [ -d "${app_Bundle}" ]
+then
+ /bin/echo "Found ${app_Bundle}, removing..."
+ /bin/rm -rf "${app_Bundle}"
+fi
+
+# Remove the pkg receipt
+pkgReceipt="com.parallels.desktop.installer"
+
+/bin/echo "Removing ${pkgReceipt}, removing..."
+pkgutil --forget "${pkgReceipt}"</string>
</dict>
</dict>
<key>MinimumVersion</key>
<string>0.6.1</string>
<key>ParentRecipe</key>
- <string>com.github.homebysix.download.ParallelsDesktop</string>
+ <string>com.github.homebysix.pkg.ParallelsDesktop</string>
<key>Process</key>
<array>
<dict>
+ <key>Processor</key>
+ <string>Copier</string>
<key>Arguments</key>
<dict>
<key>destination_path</key>
- <string>%RECIPE_CACHE_DIR%/%NAME%/Parallels Desktop.app</string>
+ <string>%RECIPE_CACHE_DIR%/%NAME%/Applications/Parallels Desktop.app</string>
<key>overwrite</key>
<true/>
<key>source_path</key>
<string>%pathname%/Parallels Desktop.app</string>
</dict>
- <key>Processor</key>
- <string>Copier</string>
</dict>
<dict>
+ <key>Processor</key>
+ <string>MunkiInstallsItemsCreator</string>
<key>Arguments</key>
<dict>
- <key>dmg_path</key>
- <string>%RECIPE_CACHE_DIR%/%NAME%-%version%.dmg</string>
- <key>dmg_root</key>
- <string>%RECIPE_CACHE_DIR%/%NAME%</string>
+ <key>faux_root</key>
+ <string>%RECIPE_CACHE_DIR%/%NAME%/</string>
+ <key>installs_item_paths</key>
+ <array>
+ <string>/Applications/Parallels Desktop.app</string>
+ </array>
+ </dict>
</dict>
+ <dict>
<key>Processor</key>
- <string>DmgCreator</string>
+ <string>MunkiPkginfoMerger</string>
</dict>
<dict>
+ <key>Processor</key>
+ <string>MunkiImporter</string>
<key>Arguments</key>
<dict>
<key>pkg_path</key>
- <string>%dmg_path%</string>
+ <string>%pkg_path%</string>
<key>repo_subdirectory</key>
<string>%MUNKI_REPO_SUBDIR%</string>
- <key>version_comparison_key</key>
- <string>CFBundleShortVersionString</string>
</dict>
+ </dict>
+ <dict>
<key>Processor</key>
- <string>MunkiImporter</string>
+ <string>PathDeleter</string>
+ <key>Arguments</key>
+ <dict>
+ <key>path_list</key>
+ <array>
+ <string>%RECIPE_CACHE_DIR%/%NAME%</string>
+ </array>
+ </dict>
</dict>
</array>
</dict>


message:
Update Waltr.download.recipe
Updated Code Signature Requirement.
Seems like the dev cert has been updated as "Waltr 2" has rhe same Subject:
anchor apple generic and identifier "com.softorino.waltr2" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "5JVYAEUZ9N")

diff:
<key>input_path</key>
<string>%pathname%/Waltr.app</string>
<key>requirement</key>
- <string>anchor apple generic and identifier "com.softorino.Waltr" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = M23NK4PKWX)</string>
+ <string>anchor apple generic and identifier "com.softorino.Waltr" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "5JVYAEUZ9N")</string>
</dict>
<key>Processor</key>
<string>CodeSignatureVerifier</string>


message:
Update ADPassMon.download.recipe
Added DeprecationWarning

diff:
@@ -23,6 +23,24 @@ https://macmule.com/2017/04/01/adpassmon-is-dead-long-live-nomad/
<string>1.0.0</string>
<key>Process</key>
<array>
+ <dict>
+ <key>Processor</key>
+ <string>DeprecationWarning</string>
+ <key>Arguments</key>
+ <dict>
+ <key>warning_message</key>
+ <string>ADPassMon is no longer maintained. Please consider using NoMAD instead. https://macmule.com/2017/04/01/adpassmon-is-dead-long-live-nomad/</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Processor</key>
+ <string>StopProcessingIf</string>
+ <key>Arguments</key>
+ <dict>
+ <key>predicate</key>
+ <string>TRUEPREDICATE</string>
+ </dict>
+ </dict>
<dict>
<key>Arguments</key>
<dict>


message:
Update duet.download.recipe
Updated CodeSignatureVerifier requirement

diff:
<key>input_path</key>
<string>%RECIPE_CACHE_DIR%/%NAME%/Applications/duet.app</string>
<key>requirement</key>
- <string>anchor apple generic and identifier "com.kairos.duet" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = J6L96W8A86)</string>
+ <string>anchor apple generic and identifier "com.kairos.duetMac" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = J6L96W8A86)</string>
</dict>
<key>Processor</key>
<string>CodeSignatureVerifier</string>


message: Update ADPassMon.download.recipe

diff:
@@ -24,22 +24,22 @@ https://macmule.com/2017/04/01/adpassmon-is-dead-long-live-nomad/
<key>Process</key>
<array>
<dict>
- <key>Processor</key>
- <string>DeprecationWarning</string>
<key>Arguments</key>
<dict>
<key>warning_message</key>
<string>ADPassMon is no longer maintained. Please consider using NoMAD instead. https://macmule.com/2017/04/01/adpassmon-is-dead-long-live-nomad/</string>
</dict>
+ <key>Processor</key>
+ <string>DeprecationWarning</string>
</dict>
<dict>
- <key>Processor</key>
- <string>StopProcessingIf</string>
<key>Arguments</key>
<dict>
<key>predicate</key>
<string>TRUEPREDICATE</string>
</dict>
+ <key>Processor</key>
+ <string>StopProcessingIf</string>
</dict>
<dict>
<key>Arguments</key>

original_message: Update ADPassMon.download.recipe


message:
RingCentral Meetings
Recipe was erroring and taking a heap of time to download.
Grabbed static download url from:
Refactored .munki recipe as the Internet plugins stuff doesn't seem to be needed and can now have the .download as parent
The .pkg and .intstall recipes will need looking at.

diff:
<dict>
<key>NAME</key>
<string>RingCentral Meetings</string>
+ <key>DOWNLOAD_URL</key>
+ <string>https://downloads.ringcentral.com/RCM/RC/meetings/mac/RCMeetingsClientSetup.pkg</string>
</dict>
<key>MinimumVersion</key>
<string>1.0.1</string>
<key>Process</key>
<array>
<dict>
- <key>Arguments</key>
- <dict>
- <key>re_pattern</key>
- <string>(dn\.ringcentral\.com\/data\/web\/download\/RCMeetings\/[\d]+\/RCMeetingsClientSetup\.pkg)</string>
- <key>result_output_var_name</key>
- <string>download_path</string>
- <key>url</key>
- <string>https://www.ringcentral.com/apps/rc-meetings</string>
- </dict>
<key>Processor</key>
- <string>URLTextSearcher</string>
- </dict>
- <dict>
+ <string>URLDownloader</string>
<key>Arguments</key>
<dict>
<key>url</key>
- <string>http://%download_path%</string>
+ <string>%DOWNLOAD_URL%</string>
+ <key>filename</key>
+ <string>%NAME%.pkg</string>
</dict>
- <key>Processor</key>
- <string>URLDownloader</string>
</dict>
<dict>
<key>Processor</key>
<string>EndOfCheckPhase</string>
</dict>
<dict>
+ <key>Processor</key>
+ <string>CodeSignatureVerifier</string>
<key>Arguments</key>
<dict>
+ <key>input_path</key>
+ <string>%pathname%</string>
<key>expected_authority_names</key>
<array>
<string>Developer ID Installer: RingCentral, Inc. (M932RC5J66)</string>
<string>Developer ID Certification Authority</string>
<string>Apple Root CA</string>
</array>
- <key>input_path</key>
- <string>%pathname%</string>
</dict>
- <key>Processor</key>
- <string>CodeSignatureVerifier</string>
</dict>
</array>
</dict>

diff:
<key>MinimumVersion</key>
<string>1.0.1</string>
<key>ParentRecipe</key>
- <string>com.github.homebysix.pkg.RingCentralMeetings</string>
+ <string>com.github.homebysix.download.RingCentralMeetings</string>
<key>Process</key>
<array>
<dict>
+ <key>Processor</key>
+ <string>FlatPkgUnpacker</string>
+ <key>Arguments</key>
+ <dict>
+ <key>destination_path</key>
+ <string>%RECIPE_CACHE_DIR%/unpack</string>
+ <key>flat_pkg_path</key>
+ <string>%pathname%</string>
+ <key>purge_destination</key>
+ <true/>
+ </dict>
+ </dict>
+ <dict>
+ <key>Processor</key>
+ <string>PkgPayloadUnpacker</string>
+ <key>Arguments</key>
+ <dict>
+ <key>destination_path</key>
+ <string>%RECIPE_CACHE_DIR%/Applications/</string>
+ <key>pkg_payload_path</key>
+ <string>%RECIPE_CACHE_DIR%/unpack/install.pkg/Payload</string>
+ <key>purge_destination</key>
+ <true/>
+ </dict>
+ </dict>
+ <dict>
+ <key>Processor</key>
+ <string>MunkiInstallsItemsCreator</string>
<key>Arguments</key>
<dict>
<key>faux_root</key>
- <string>%RECIPE_CACHE_DIR%/%NAME%</string>
+ <string>%RECIPE_CACHE_DIR%</string>
<key>installs_item_paths</key>
<array>
<string>/Applications/RingCentral Meetings.app</string>
- <string>/Library/Internet Plug-Ins/RingCentralMeetings.plugin</string>
</array>
</dict>
+ </dict>
+ <dict>
<key>Processor</key>
- <string>MunkiInstallsItemsCreator</string>
+ <string>MunkiPkginfoMerger</string>
+ </dict>
+ <dict>
+ <key>Processor</key>
+ <string>Versioner</string>
+ <key>Arguments</key>
+ <dict>
+ <key>input_plist_path</key>
+ <string>%destination_path%/RingCentral Meetings.app/Contents/Info.plist</string>
+ <key>plist_version_key</key>
+ <string>CFBundleShortVersionString</string>
+ </dict>
</dict>
<dict>
<key>Processor</key>
<string>MunkiPkginfoMerger</string>
+ <key>Arguments</key>
+ <dict>
+ <key>additional_pkginfo</key>
+ <dict>
+ <key>version</key>
+ <string>%version%</string>
+ </dict>
+ </dict>
</dict>
<dict>
+ <key>Processor</key>
+ <string>MunkiImporter</string>
<key>Arguments</key>
<dict>
+ <key>pkg_path</key>
+ <string>%pathname%</string>
<key>repo_subdirectory</key>
<string>%MUNKI_REPO_SUBDIR%</string>
</dict>
+ </dict>
+ <dict>
<key>Processor</key>
- <string>MunkiImporter</string>
+ <string>PathDeleter</string>
+ <key>Arguments</key>
+ <dict>
+ <key>path_list</key>
+ <array>
+ <string>%RECIPE_CACHE_DIR%/Applications</string>
+ <string>%RECIPE_CACHE_DIR%/unpack</string>
+ </array>
+ </dict>
</dict>
</array>
</dict>

original_message:
RingCentral Meetings

Recipe was erroring and taking a heap of time to download.

Grabbed static download url from: https://support.ringcentral.com/download.html

Refactored .munki recipe as the Internet plugins stuff doesn't seem to be needed and can now have the .download as parent

The .pkg and .intstall recipes will need looking at.


message:
Update ApacheDirectoryStudio.munki.recipe
Amended so that the munki version  is reported the same as the version we're looking for.. as in, '2.0.0.v20200411-M15' vs '2.0.0'
Detabbed too

diff:
<key>Process</key>
<array>
<dict>
+ <key>Processor</key>
+ <string>Versioner</string>
<key>Arguments</key>
<dict>
- <key>dmg_path</key>
- <string>%pathname%</string>
+ <key>input_plist_path</key>
+ <string>%pathname%/ApacheDirectoryStudio.app/Contents/Info.plist</string>
+ <key>plist_version_key</key>
+ <string>CFBundleVersion</string>
+ </dict>
</dict>
+ <dict>
+ <key>Processor</key>
+ <string>MunkiPkginfoMerger</string>
+ <key>Arguments</key>
+ <dict>
+ <key>additional_pkginfo</key>
+ <dict>
+ <key>version</key>
+ <string>%version%</string>
+ </dict>
+ </dict>
+ </dict>
+ <dict>
<key>Processor</key>
<string>AppDmgVersioner</string>
+ <key>Arguments</key>
+ <dict>
+ <key>dmg_path</key>
+ <string>%pathname%</string>
+ </dict>
</dict>
<dict>
+ <key>Processor</key>
+ <string>MunkiImporter</string>
<key>Arguments</key>
<dict>
<key>pkg_path</key>
<key>version_comparison_key</key>
<string>CFBundleVersion</string>
</dict>
- <key>Processor</key>
- <string>MunkiImporter</string>
</dict>
</array>
</dict>


message:
Update ApacheDirectoryStudio.download.recipe
Amended download, as before was pulling 2.0.0.v20200411-M15, with 2.0.0.v20210213-M16 being released 2021-02-20

diff:
<key>Process</key>
<array>
<dict>
+ <key>Processor</key>
+ <string>URLTextSearcher</string>
<key>Arguments</key>
<dict>
<key>re_pattern</key>
- <string></string>
+ <string>(.*)/</a</string>
<key>result_output_var_name</key>
<string>version</string>
<key>url</key>
- <string>https://dist.apache.org/repos/dist/release/directory/studio/</string>
+ <string>https://archive.apache.org/dist/directory/studio/?C=M;O=D</string>
</dict>
- <key>Processor</key>
- <string>URLTextSearcher</string>
</dict>
<dict>
+ <key>Processor</key>
+ <string>URLDownloader</string>
<key>Arguments</key>
<dict>
<key>url</key>
- <string>https://dist.apache.org/repos/dist/release/directory/studio/%version%/ApacheDirectoryStudio-%version%-macosx.cocoa.x86_64.dmg</string>
+ <string>https://archive.apache.org/dist/directory/studio/%version%/ApacheDirectoryStudio-%version%-macosx.cocoa.x86_64.dmg</string>
</dict>
- <key>Processor</key>
- <string>URLDownloader</string>
</dict>
<dict>
<key>Processor</key>
<string>EndOfCheckPhase</string>
</dict>
+ <dict>
+ <key>Processor</key>
+ <string>CodeSignatureVerifier</string>
+ <key>Arguments</key>
+ <dict>
+ <key>input_path</key>
+ <string>%pathname%/ApacheDirectoryStudio.app</string>
+ <key>requirement</key>
+ <string>identifier "org.apache.directory.studio.product" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "2GLGAFWEQD"</string>
+ </dict>
+ </dict>
</array>
</dict>
</plist>
\ No newline at end of file


message: Update Grammarly.munki.recipe

diff:
<string>MunkiPkginfoMerger</string>
</dict>
<dict>
- <key>Processor</key>
- <string>Copier</string>
<key>Arguments</key>
<dict>
- <key>source_path</key>
- <string>%pathname%/Grammarly Installer.app</string>
<key>destination_path</key>
<string>%RECIPE_CACHE_DIR%/%NAME%/Grammarly Desktop.app</string>
<key>overwrite</key>
<true/>
+ <key>source_path</key>
+ <string>%pathname%/Grammarly Installer.app</string>
</dict>
+ <key>Processor</key>
+ <string>Copier</string>
</dict>
<dict>
- <key>Processor</key>
- <string>PlistReader</string>
<key>Arguments</key>
<dict>
<key>info_path</key>
<string>version</string>
</dict>
</dict>
+ <key>Processor</key>
+ <string>PlistReader</string>
</dict>
<dict>
- <key>Processor</key>
- <string>MunkiPkginfoMerger</string>
<key>Arguments</key>
<dict>
<key>additional_pkginfo</key>
<string>%version%</string>
</dict>
</dict>
+ <key>Processor</key>
+ <string>MunkiPkginfoMerger</string>
</dict>
<dict>
- <key>Processor</key>
- <string>DmgCreator</string>
<key>Arguments</key>
<dict>
- <key>dmg_root</key>
- <string>%RECIPE_CACHE_DIR%/%NAME%</string>
<key>dmg_path</key>
<string>%RECIPE_CACHE_DIR%/%NAME%.dmg</string>
+ <key>dmg_root</key>
+ <string>%RECIPE_CACHE_DIR%/%NAME%</string>
</dict>
+ <key>Processor</key>
+ <string>DmgCreator</string>
</dict>
<dict>
- <key>Processor</key>
- <string>MunkiImporter</string>
<key>Arguments</key>
<dict>
<key>pkg_path</key>
<key>version_comparison_key</key>
<string>CFBundleVersion</string>
</dict>
+ <key>Processor</key>
+ <string>MunkiImporter</string>
</dict>
<dict>
- <key>Processor</key>
- <string>PathDeleter</string>
<key>Arguments</key>
<dict>
<key>path_list</key>
<string>%RECIPE_CACHE_DIR%/%NAME%/</string>
</array>
</dict>
+ <key>Processor</key>
+ <string>PathDeleter</string>
</dict>
</array>
</dict>

original_message: Update Grammarly.munki.recipe


message: Bump gradle to 6.9.1

diff: Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ

old_path: gradlew

new_path: gradlew

diff:
@@ -82,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -129,6 +130,7 @@ fi
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath

repo: square/leakcanary

original_message: Bump gradle to 6.9.1


message: Bump setup-java actions to v2

diff:
@@ -14,9 +14,10 @@ jobs:
uses: actions/checkout@v2
- name: Setup JDK 1.8
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v2
with:
- java-version: 1.8
+ java-version: 8
+ distribution: 'zulu'
- name: Build project
run: ./gradlew build --stacktrace
@@ -40,9 +41,10 @@ jobs:
uses: actions/checkout@v2
- name: Setup JDK 1.8
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v2
with:
- java-version: 1.8
+ java-version: 8
+ distribution: 'zulu'
- name: Deploy snapshot
run: ./gradlew uploadArchives

original_message: Bump setup-java actions to v2


message: Bump JavaVersion to 8

diff:
@@ -2,8 +2,8 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
implementation deps.kotlin.stdlib

diff:
@@ -2,8 +2,8 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
api project(':shark')

diff:
@@ -2,8 +2,8 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
api project(':shark-hprof')

diff:
@@ -2,8 +2,8 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
api project(':shark-log')

diff:
@@ -2,8 +2,8 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}
-sourceCompatibility = JavaVersion.VERSION_1_7
-targetCompatibility = JavaVersion.VERSION_1_7
+sourceCompatibility = JavaVersion.VERSION_1_8
+targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
api project(':shark-graph')

original_message: Bump JavaVersion to 8


message: Remove versions plugin

diff:
-apply plugin: 'com.github.ben-manes.versions'
-
buildscript {
ext.versions = [
'minSdk': 14,
@@ -56,7 +54,6 @@ buildscript {
dependencies {
classpath deps.kotlin.gradlePlugin
classpath deps.androidGradlePlugin
- classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.18"
classpath deps.detekt
classpath deps.kotlin.binaryCompatibilityValidatorPlugin

original_message: Remove versions plugin


message: Adopt AGP 4.2.2

diff:
@@ -13,7 +13,7 @@ buildscript {
// relies on), we're sticking a bit older because most apps will be using a more recent version
// and they'll automatically resolve to higher version without having to necessarily resort to a
// resolution strategy.
- androidGradlePlugin: "com.android.tools.build:gradle:4.0.0",
+ androidGradlePlugin: "com.android.tools.build:gradle:4.2.2",
androidx: [
annotation: 'androidx.annotation:annotation:1.0.2',
core: 'androidx.core:core:1.0.1',

diff:
@@ -20,4 +20,5 @@ android.useAndroidX=true
#Gradle properties: https://docs.gradle.org/current/userguide/build_environment.html
org.gradle.caching=true
org.gradle.configureondemand=true
+org.gradle.jvmargs=-XX:+UseParallelGC -Dfile.encoding=UTF-8
org.gradle.parallel=true

diff:
@@ -29,9 +29,9 @@ def gitSha() {
android {
resourcePrefix 'leak_canary_'
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
- minSdkVersion versions.minSdk
+ minSdk versions.minSdk
buildConfigField "String", "LIBRARY_VERSION", "\"${rootProject.ext.VERSION_NAME}\""
buildConfigField "String", "GIT_SHA", "\"${gitSha()}\""
consumerProguardFiles 'consumer-proguard-rules.pro'
@@ -40,7 +40,7 @@ android {
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
}

diff:
@@ -16,16 +16,16 @@ dependencies {
}
android {
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
- minSdkVersion versions.minSdk
+ minSdk versions.minSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'GoogleAppIndexingWarning'
// junit references java.lang.management
ignore 'InvalidPackage'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
}

diff:
@@ -10,15 +10,15 @@ dependencies {
}
android {
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
- minSdkVersion versions.minSdk
+ minSdk versions.minSdk
}
lintOptions {
disable 'GoogleAppIndexingWarning'
// junit references java.lang.management
ignore 'InvalidPackage'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
}

diff:
@@ -17,7 +17,7 @@ def gitSha() {
android {
resourcePrefix 'leak_canary_'
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
minSdkVersion 16
buildConfigField "String", "LIBRARY_VERSION", "\"${rootProject.ext.VERSION_NAME}\""
@@ -27,7 +27,7 @@ android {
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
}

diff:
@@ -31,7 +31,7 @@ dependencies {
}
android {
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -41,7 +41,7 @@ android {
defaultConfig {
applicationId "com.example.leakcanary"
minSdkVersion 16
- targetSdkVersion versions.compileSdk
+ targetSdk versions.compileSdk
versionCode 1
versionName "1.0"

diff:
@@ -10,14 +10,14 @@ dependencies {
}
android {
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
- minSdkVersion versions.minSdk
+ minSdk versions.minSdk
}
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
}

diff:
@@ -12,15 +12,15 @@ dependencies {
}
android {
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
- minSdkVersion versions.minSdk
+ minSdk versions.minSdk
consumerProguardFiles 'consumer-proguard-rules.pro'
}
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
}

diff:
@@ -17,16 +17,16 @@ dependencies {
android {
resourcePrefix 'leak_canary_watcher_'
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
- minSdkVersion versions.minSdk
+ minSdk versions.minSdk
consumerProguardFiles 'consumer-proguard-rules.pro'
}
lintOptions {
disable 'GoogleAppIndexingWarning'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
buildFeatures {

diff:
@@ -14,15 +14,15 @@ dependencies {
android {
resourcePrefix 'leak_canary_plumber'
- compileSdkVersion versions.compileSdk
+ compileSdk versions.compileSdk
defaultConfig {
- minSdkVersion versions.minSdk
+ minSdk versions.minSdk
consumerProguardFiles 'consumer-proguard-rules.pro'
}
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'
- check 'Interoperability'
+ checkOnly 'Interoperability'
}
}

original_message: Adopt AGP 4.2.2


message: Disable BuildConfig generating by default

diff:
@@ -21,6 +21,7 @@ android {
minSdk versions.minSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+ buildFeatures.buildConfig = false
lintOptions {
disable 'GoogleAppIndexingWarning'
// junit references java.lang.management

diff:
@@ -14,6 +14,7 @@ android {
defaultConfig {
minSdk versions.minSdk
}
+ buildFeatures.buildConfig = false
lintOptions {
disable 'GoogleAppIndexingWarning'
// junit references java.lang.management

diff:
@@ -14,6 +14,7 @@ android {
defaultConfig {
minSdk versions.minSdk
}
+ buildFeatures.buildConfig = false
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'

diff:
@@ -17,6 +17,7 @@ android {
minSdk versions.minSdk
consumerProguardFiles 'consumer-proguard-rules.pro'
}
+ buildFeatures.buildConfig = false
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'

diff:
@@ -19,6 +19,7 @@ android {
minSdk versions.minSdk
consumerProguardFiles 'consumer-proguard-rules.pro'
}
+ buildFeatures.buildConfig = false
lintOptions {
disable 'GoogleAppIndexingWarning'
error 'ObsoleteSdkInt'


message: Specify languages in kdoc code fragments

diff:
@@ -158,7 +158,7 @@ object LeakCanary {
*
* The list of filters can be built from [AndroidObjectInspectors]:
*
- * ```
+ * ```kotlin
* LeakCanary.config = LeakCanary.config.copy(
* leakingObjectFinder = FilteringLeakingObjectFinder(
* AndroidObjectInspectors.appLeakingObjectFilters
@@ -188,7 +188,7 @@ object LeakCanary {
* Builder for [LeakCanary.Config] intended to be used only from Java code.
*
* Usage:
- * ```
+ * ```kotlin
* LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
* .retainedVisibleThreshold(3)
* .build();
@@ -196,7 +196,7 @@ object LeakCanary {
* ```
*
* For idiomatic Kotlin use `copy()` method instead:
- * ```
+ * ```kotlin
* LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
* ```
*/
@@ -278,12 +278,12 @@ object LeakCanary {
* The current LeakCanary configuration. Can be updated at any time, usually by replacing it with
* a mutated copy, e.g.:
*
- * ```
+ * ```kotlin
* LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 3)
* ```
*
* In Java, use [LeakCanary.Config.Builder] instead:
- * ```
+ * ```java
* LeakCanary.Config config = LeakCanary.getConfig().newBuilder()
* .retainedVisibleThreshold(3)
* .build();
@@ -330,7 +330,7 @@ object LeakCanary {
* Note: you can change the default value by overriding the `leak_canary_add_launcher_icon`
* boolean resource:
*
- * ```
+ * ```xml
* <?xml version="1.0" encoding="utf-8"?>
* <resources>
* <bool name="leak_canary_add_launcher_icon">false</bool>

diff:
@@ -55,7 +55,7 @@ import java.io.File
* your command line for running the UI tests:
* `-e listener leakcanary.FailTestOnLeakRunListener`. The full command line
* should look something like this:
- * ```
+ * ```shell
* adb shell am instrument \\
* -w com.android.foo/android.support.test.runner.AndroidJUnitRunner \\
* -e listener leakcanary.FailTestOnLeakRunListener

diff:
@@ -25,7 +25,7 @@ open class LeakCanaryDeobfuscationExtension {
/**
* Variant filtering function. It should be overriden to tell LeakCanary for which obfuscated
* variants it should copy the mapping file into apk. For example this:
- * ```
+ * ```kotlin
* filterObfuscatedVariants { variant ->
* variant.name == "debug"
* }

diff:
@@ -53,7 +53,7 @@ object AppWatcher {
* first disable the automatic call by overriding the `leak_canary_watcher_auto_install` boolean
* resource:
*
- * ```
+ * ```xml
* <?xml version="1.0" encoding="utf-8"?>
* <resources>
* <bool name="leak_canary_watcher_auto_install">false</bool>
@@ -62,7 +62,7 @@ object AppWatcher {
*
* [watchersToInstall] can be customized to a subset of the default app watchers:
*
- * ```
+ * ```kotlin
* val watchersToInstall = AppWatcher.appDefaultWatchers(application)
* .filter { it !is RootViewWatcher }
* AppWatcher.manualInstall(
@@ -74,7 +74,7 @@ object AppWatcher {
* [watchersToInstall] can also be customized to ignore specific instances (e.g. here ignoring
* leaks of BadSdkLeakingFragment):
*
- * ```
+ * ```kotlin
* val watchersToInstall = AppWatcher.appDefaultWatchers(application, ReachabilityWatcher { watchedObject, description ->
* if (watchedObject !is BadSdkLeakingFragment) {
* AppWatcher.objectWatcher.expectWeaklyReachable(watchedObject, description)


message: Bump plugins

diff:
@@ -16,6 +16,8 @@ buildscript {
// and they'll automatically resolve to higher version without having to necessarily resort to a
// resolution strategy.
androidGradlePlugin: "com.android.tools.build:gradle:4.2.2",
+ dokkaPlugin: "org.jetbrains.dokka:dokka-gradle-plugin:1.6.0",
+ mavenPublishPlugin: "com.vanniktech:gradle-maven-publish-plugin:0.18.0",
androidx: [
annotation: 'androidx.annotation:annotation:1.0.2',
core: 'androidx.core:core:1.0.1',
@@ -35,7 +37,7 @@ buildscript {
detekt: 'io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.6.0',
junit: 'junit:junit:4.12',
kotlin: [
- binaryCompatibilityValidatorPlugin: "org.jetbrains.kotlinx:binary-compatibility-validator:0.6.0",
+ binaryCompatibilityValidatorPlugin: "org.jetbrains.kotlinx:binary-compatibility-validator:0.8.0",
gradlePlugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlinCompiler}",
stdlib: "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlinLib}",
reflect: "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlinLib}"
@@ -56,8 +58,8 @@ buildscript {
dependencies {
classpath deps.kotlin.gradlePlugin
classpath deps.androidGradlePlugin
- classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.5.0"
- classpath "com.vanniktech:gradle-maven-publish-plugin:0.17.0"
+ classpath deps.dokkaPlugin
+ classpath deps.mavenPublishPlugin
classpath deps.detekt
classpath deps.kotlin.binaryCompatibilityValidatorPlugin
}

original_message: Bump plugins


message: Bump gradle to 7.3.3

diff: Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ

original_message: Bump gradle to 7.3.3


message: Put deps into version catalog

diff:
kotlinCompiler = "1.4.21"
kotlinLib = "1.3.72"
androidXTest = "1.1.0"
+androidXJunit = "1.1.3"
workManager = "2.7.0"
[libraries]
@@ -37,11 +38,14 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref =
# and they'll automatically resolve to higher version without having to necessarily resort to a
# resolution strategy.
androidX-fragment = { module = "androidx.fragment:fragment", version = "1.0.0" }
-androidX-test-core = { module = "androidx.test:core", version = "1.0.0" }
+androidX-test-core = { module = "androidx.test:core", version = "1.4.0" }
androidX-test-rules = { module = "androidx.test:rules", version.ref = "androidXTest" }
-androidX-test-runner = { module = "androidx.test:runner", version.ref = "androidXTest" }
-androidX-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidXTest" }
-androidX-test-espresso = { module = "androidx.test.espresso:espresso-core", version = "3.1.0" }
+# Exposed transitively, avoid increasing
+androidX-test-runner = { module = "androidx.test:runner", version = "1.1.0" }
+androidX-test-orchestrator = { module = "androidx.test:orchestrator", version = "1.4.1" }
+androidX-test-espresso = { module = "androidx.test.espresso:espresso-core", version = "3.4.0" }
+androidX-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidXJunit" }
+androidX-test-junitKtx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidXJunit" }
androidX-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workManager" }
androidX-work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workManager" }

diff:
@@ -32,7 +32,7 @@ dependencies {
androidTestImplementation libs.androidX.test.rules
androidTestImplementation libs.androidX.test.runner
androidTestImplementation libs.androidX.test.junit
- androidTestImplementation libs.androidX.test.junit_ktx
+ androidTestImplementation libs.androidX.test.junitKtx
androidTestUtil libs.androidX.test.orchestrator
}

original_message: Put deps into version catalog


message: Markdown cleanups

diff:
@@ -312,7 +312,7 @@ class BugsnagHeapAnalysisUploader(applicationContext: Application) {
We added 3 new automatic fixes for known AOSP leaks in `plumber-android` (details: [#1993](https://github.com/square/leakcanary/issues/1993)). As a reminder, `plumber-android` is automatically included when you add `leakcanary-android`, and you can add it manually for build types that don't include LeakCanary:
-```xml
+```gradle
dependencies {
// leakcanary-android adds plumber-android to debug builds
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'

diff:
@@ -226,7 +226,7 @@ public class LeakUploadService extends DisplayLeakService {
```java
RefWatcher refWatcher = LeakCanary.refWatcher(this)
- .listenerServiceClass(LeakUploadService.class);
+ .listenerServiceClass(LeakUploadService.class)
.buildAndInstall();
```

original_message: Markdown cleanups


message: Add `isMainThread` extension to check thread

diff:
@@ -5,14 +5,16 @@ import android.os.Looper
internal val mainHandler by lazy { Handler(Looper.getMainLooper()) }
+internal val isMainThread: Boolean get() = Looper.getMainLooper().thread === Thread.currentThread()
+
internal fun checkMainThread() {
- check(Looper.getMainLooper().thread === Thread.currentThread()) {
+ check(isMainThread) {
"Should be called from the main thread, not ${Thread.currentThread()}"
}
}
internal fun checkNotMainThread() {
- check(Looper.getMainLooper().thread !== Thread.currentThread()) {
+ check(!isMainThread) {
"Should not be called from the main thread"
}
}


message: Replace thread usages

diff:
@@ -478,11 +478,9 @@ class ReleaseExampleApplication : ExampleApplication() {
private val analysisExecutor by lazy {
Executors.newSingleThreadExecutor {
- Thread {
+ thread(start = false, name = "Heap analysis executor") {
android.os.Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)
it.run()
- }.apply {
- name = "Heap analysis executor"
}
}
}

diff:
@@ -30,6 +30,7 @@ import android.os.SystemClock
import android.view.View
import android.widget.Button
import java.util.concurrent.atomic.AtomicReference
+import kotlin.concurrent.thread
import kotlin.random.Random
import leakcanary.AppWatcher
@@ -53,7 +54,7 @@ class MainActivity : Activity() {
2 -> {
// Leak from local variable on thread
val ref = AtomicReference(this)
- val thread = Thread {
+ thread(name = "Leaking local variables") {
val activity = ref.get()
ref.set(null)
while (true) {
@@ -61,8 +62,6 @@ class MainActivity : Activity() {
SystemClock.sleep(1000)
}
}
- thread.name = "Leaking local variables"
- thread.start()
}
// Leak from thread fields
else -> LeakingThread.thread.leakedViews.add(leakedView)

diff:
@@ -12,6 +12,7 @@ import leakcanary.ScreenOffTrigger
import shark.SharkLog
import shark.SharkLog.Logger
import java.util.concurrent.Executors
+import kotlin.concurrent.thread
class ReleaseExampleApplication : ExampleApplication() {
@@ -27,11 +28,9 @@ class ReleaseExampleApplication : ExampleApplication() {
)
val analysisExecutor = Executors.newSingleThreadExecutor {
- Thread {
+ thread(start = false, name = "Heap analysis executor") {
android.os.Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)
it.run()
- }.apply {
- name = "Heap analysis executor"
}
}
analysisExecutor.execute {

diff:
@@ -4,6 +4,7 @@ import java.io.File
import java.util.EnumSet
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit.SECONDS
+import kotlin.concurrent.thread
import kotlin.math.absoluteValue
import kotlin.reflect.KClass
import org.assertj.core.api.AbstractIntegerAssert
@@ -154,12 +155,9 @@ class HprofRetainedHeapPerfTest {
): T {
lateinit var result: T
val latch = CountDownLatch(1)
- Thread {
+ thread(name = threadName) {
result = work()
latch.countDown()
- }.apply {
- name = threadName
- start()
}
check(latch.await(30, SECONDS))
return result

original_message: Replace thread usages


message: Replace io usages

diff:
@@ -10,7 +10,6 @@ import shark.HeapAnalysis
import shark.HeapAnalysisSuccess
import shark.LeakTrace.GcRootType.JAVA_FRAME
import shark.LeakTrace.GcRootType.STICKY_CLASS
-import java.io.FileOutputStream
class DatabaseMigrationTest {
@@ -94,7 +93,7 @@ class DatabaseMigrationTest {
.use { input ->
val databaseFile = context.getDatabasePath(LeaksDbHelper.DATABASE_NAME)
databaseFile.parentFile!!.mkdirs()
- FileOutputStream(databaseFile).use { output ->
+ databaseFile.outputStream().use { output ->
input.copyTo(output)
}
}

diff:
@@ -12,6 +12,9 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView
import android.widget.Toast
import com.squareup.leakcanary.core.R
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
import leakcanary.internal.InternalLeakCanary
import leakcanary.internal.LeakCanaryFileProvider
import leakcanary.internal.activity.db.executeOnIo
@@ -21,9 +24,6 @@ import leakcanary.internal.navigation.inflate
import leakcanary.internal.navigation.onCreateOptionsMenu
import leakcanary.internal.utils.humanReadableByteCount
import shark.SharkLog
-import java.io.File
-import java.io.FileOutputStream
-import java.io.IOException
internal class RenderHeapDumpScreen(
private val heapDumpFile: File
@@ -136,7 +136,7 @@ internal class RenderHeapDumpScreen(
): Boolean {
var outStream: FileOutputStream? = null
return try {
- outStream = FileOutputStream(imageFile)
+ outStream = imageFile.outputStream()
source.compress(Bitmap.CompressFormat.PNG, 100, outStream)
true
} catch (e: IOException) {

diff:
@@ -2,6 +2,7 @@ package leakcanary
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
import leakcanary.Profiler.runWithProfilerSampling
import org.junit.Ignore
import org.junit.Test
@@ -10,8 +11,6 @@ import shark.AndroidReferenceMatchers
import shark.HeapAnalyzer
import shark.KeyedWeakReferenceFinder
import shark.SharkLog
-import java.io.File
-import java.io.FileOutputStream
class ProfiledTest {
@@ -26,7 +25,7 @@ class ProfiledTest {
val heapDumpFile = File(context.filesDir, "ProfiledTest.hprof")
context.assets.open(fileName)
- .copyTo(FileOutputStream(heapDumpFile))
+ .copyTo(heapDumpFile.outputStream())
runWithProfilerSampling {
val analyzer = HeapAnalyzer { step -> Log.d("LeakCanary", step.name) }

diff:
@@ -10,12 +10,11 @@ import android.os.Process
import android.os.SystemClock
import android.system.Os
import android.system.OsConstants
+import java.io.File
+import java.io.FileReader
import leakcanary.ProcessInfo.AvailableRam.BelowThreshold
import leakcanary.ProcessInfo.AvailableRam.LowRamDevice
import leakcanary.ProcessInfo.AvailableRam.Memory
-import java.io.BufferedReader
-import java.io.File
-import java.io.FileReader
interface ProcessInfo {
@@ -110,7 +109,7 @@ interface ProcessInfo {
// Android 10. Median time: 0.13ms
private fun readProcessStartTicks(pid: Int): Long {
val path = "/proc/$pid/stat"
- val stat = BufferedReader(FileReader(path)).use { reader ->
+ val stat = FileReader(path).buffered().use { reader ->
reader.readLine()
}
val fields = stat.substringAfter(") ")

original_message: Replace io usages


message: Rename plugins in libs.versions.toml

diff:
@@ -11,13 +11,13 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath libs.kotlinPlugin
- classpath libs.androidPlugin
- classpath libs.dokkaPlugin
- classpath libs.mavenPublishPlugin
- classpath libs.detektPlugin
- classpath libs.binaryCompatibilityValidatorPlugin
- classpath libs.keeperPlugin
+ classpath libs.gradlePlugin.android
+ classpath libs.gradlePlugin.kotlin
+ classpath libs.gradlePlugin.dokka
+ classpath libs.gradlePlugin.mavenPublish
+ classpath libs.gradlePlugin.detekt
+ classpath libs.gradlePlugin.binaryCompatibility
+ classpath libs.gradlePlugin.keeper
}
}

diff:
@@ -22,13 +22,13 @@ androidXJunit = "1.1.3"
workManager = "2.7.0"
[libraries]
-androidPlugin = { module = "com.android.tools.build:gradle", version = "4.2.2" }
-kotlinPlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinCompiler" }
-dokkaPlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.6.0" }
-binaryCompatibilityValidatorPlugin = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version = "0.8.0" }
-mavenPublishPlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.18.0" }
-detektPlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.6.0" }
-keeperPlugin = { module = "com.slack.keeper:keeper", version = "0.7.0" }
+gradlePlugin-android = { module = "com.android.tools.build:gradle", version = "4.2.2" }
+gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinCompiler" }
+gradlePlugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version = "1.6.0" }
+gradlePlugin-binaryCompatibility = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version = "0.8.0" }
+gradlePlugin-mavenPublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.18.0" }
+gradlePlugin-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.6.0" }
+gradlePlugin-keeper = { module = "com.slack.keeper:keeper", version = "0.7.0" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlinLib" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinLib" }

diff:
@@ -20,8 +20,8 @@ gradlePlugin {
dependencies {
implementation libs.kotlin.stdlib
- implementation libs.kotlinPlugin
- implementation libs.androidPlugin
+ implementation libs.gradlePlugin.kotlin
+ implementation libs.gradlePlugin.android
compileOnly gradleApi()
testImplementation libs.assertjCore

original_message: Rename plugins in libs.versions.toml


message: Optimize CI a bit

diff:
name: Main
+
on:
pull_request:
push:
@@ -7,30 +8,24 @@ on:
jobs:
validation:
- name: "Validation"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
checks:
- runs-on: macos-latest
+ runs-on: ubuntu-latest
steps:
-
- - name: Project Checkout
- uses: actions/checkout@v2
-
- - name: Setup JDK 1.8
- uses: actions/setup-java@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v3
with:
java-version: 8
distribution: 'zulu'
-
+ - uses: gradle/gradle-build-action@v2
- name: Build project
run: ./gradlew build --stacktrace
instrumentation-tests:
- name: Instrumentation tests
runs-on: macos-latest
timeout-minutes: 30
strategy:
@@ -59,33 +54,46 @@ jobs:
# api-level: 30
# target: aosp_atd
# channel: canary
-
steps:
- - name: Project Checkout
- uses: actions/checkout@v2
-
- - name: Setup JDK 1.8
- uses: actions/setup-java@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v3
with:
java-version: 8
distribution: 'zulu'
-
+ - uses: gradle/gradle-build-action@v2
+ - name: AVD cache
+ uses: actions/cache@v3
+ id: avd-cache
+ with:
+ path: |
+ ~/.android/avd/*
+ ~/.android/adb*
+ key: avd-${{ matrix.api-level }}-${{ matrix.os }}-${{ matrix.target }}
+ - name: Create AVD and generate snapshot for caching
+ if: steps.avd-cache.outputs.cache-hit != 'true'
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: ${{ matrix.api-level }}
+ target: ${{ matrix.target }}
+ arch: ${{ matrix.arch }}
+ force-avd-creation: false
+ emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ disable-animations: false
+ script: echo "Generated AVD snapshot for caching."
- name: Instrumentation Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
- channel: ${{ matrix.channel }}
arch: ${{ matrix.arch }}
script: |
touch emulator.log # create log file
chmod 777 emulator.log # allow writing to log file
adb logcat >> emulator.log & # pipe all logcat messages into log file as a background process
./gradlew leakcanary-android-core:connectedCheck leakcanary-android:connectedCheck leakcanary-android-instrumentation:connectedCheck --no-build-cache --no-daemon --stacktrace
-
- name: Upload results
if: ${{ always() }}
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: ${{ matrix.api-level }}-${{ matrix.arch }}-instrumentation-test-results
path: |
@@ -97,22 +105,17 @@ jobs:
needs: [ checks, instrumentation-tests ]
runs-on: ubuntu-latest
steps:
-
- - name: Project Checkout
- uses: actions/checkout@v2
-
- - name: Setup JDK 1.8
- uses: actions/setup-java@v2
+ - uses: actions/checkout@v3
+ - uses: actions/setup-java@v3
with:
java-version: 8
distribution: 'zulu'
-
+ - uses: gradle/gradle-build-action@v2
- name: Deploy snapshot
run: ./gradlew publish
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
-
- name: Cleanup secrets
if: always()
run: rm -rf ~/.gradle/gradle.properties

diff:
@@ -9,7 +9,7 @@ jobs:
permissions:
issues: write
steps:
- - uses: actions/stale@v4
+ - uses: actions/stale@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30

original_message: Optimize CI a bit


message: example: Added with-tailwindcss example

diff:
+import React from 'react';
+
+export function rootContainer(container: any, opts: any) {
+ return React.createElement(Foo, opts, container);
+}
+
+export function innerProvider(container: any) {
+ return React.createElement(Foo, { title: 'innerProvider' }, container);
+}
+
+export function i18nProvider(container: any) {
+ return React.createElement(Foo, { title: 'i18nProvider' }, container);
+}
+
+export function dataflowProvider(container: any) {
+ return React.createElement(Foo, { title: 'dataflowProvider' }, container);
+}
+
+export function outerProvider(container: any) {
+ return React.createElement(Foo, { title: 'outerProvider' }, container);
+}
+
+function Foo(props: any) {
+ return <div>{props.children}</div>;
+}

diff: Binary files /dev/null and b/examples/with-tailwindcss/favicon.png differ

diff: Binary files /dev/null and b/examples/with-tailwindcss/images/tailwindcss.png differ

diff: Binary files /dev/null and b/examples/with-tailwindcss/images/umi.png differ

diff:
+{
+ "scripts": {
+ "build": "umi build",
+ "dev": "umi dev",
+ "start": "npm run dev"
+ },
+ "dependencies": {
+ "postcss-loader": "6.2.1",
+ "style-loader": "^3.3.1",
+ "tailwindcss": "^3.0.15",
+ "umi": "4.0.0-beta.16"
+ }
+}

diff:
+import React from 'react';
+import '../tailwind.css';
+
+export default function HomePage() {
+ return (
+

+

+

+

+
+
x

+
+

+

+

+

+ UmiJS
+ x
+ TailwindCss
+

+

+ This is an example using
+
+ UmiJS
+
+ with
+
+ TailwindCss
+
+ .
+

+

+

+ Examples
+

+
+

+
Text Size

+

+
text-xs

+
text-sm

+
text-base

+
text-lg

+
text-xl

+
text-2xl

+
text-3xl

+

+

+
+

+

+ Text Color
+

+
+

+
text-red-300

+
text-blue-600

+
text-green-300

+
text-pink-400

+

+

+
+

+
Sizes

+

+

+ w-24
+

+

+ w-36
+

+

+ w-64
+

+

+ w-96
+

+

+

+
+

+

+ Animations
+

+
+

+

+

+

+

+

+

+
+

+

+

+

+

+

+
+

+

+

+

+

+

+
+

+

+

+

+

+

+

+

+
+

+

+ Hover Animations
+

+
+

+

+

+

+

+

+

+
+

+

+

+

+

+

+
+

+

+

+

+

+

+
+

+

+

+

+

+

+
+

+

+

+

+

+

+
+

+

+

+

+

+

+
+

+

+
+

+

+

+

+
+

+

+

+

+

+

+

+

+

+

+

+ );
+}

diff:
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ }
+}

diff:
+# with-tailwindcss
+
+An example of using [UmiJS](https://umijs.org/zh-CN) with [Tailwindcss](https://tailwindcss.com/).

diff:
+module.exports = {
+ content: [
+ './pages/**/*.tsx',
+ ],
+}

diff:
+@tailwind base;
+@tailwind components;
+@tailwind utilities;

license: MIT License

repo: umijs/umi-next

original_message: example: Added with-tailwindcss example (#274)


diff:
+import React from 'react';
+import './style.css';
+
+export function rootContainer(container: any, opts: any) {
+ return React.createElement(Foo, opts, container);
+}
+
+export function innerProvider(container: any) {
+ return React.createElement(Foo, {title: 'innerProvider'}, container);
+}
+
+export function i18nProvider(container: any) {
+ return React.createElement(Foo, {title: 'i18nProvider'}, container);
+}
+
+export function dataflowProvider(container: any) {
+ return React.createElement(Foo, {title: 'dataflowProvider'}, container);
+}
+
+export function outerProvider(container: any) {
+ return React.createElement(Foo, {title: 'outerProvider'}, container);
+}
+
+function Foo(props: any) {
+ return <div>{props.children}</div>;
+}

diff:
+import React from "react";
+import state from "../state";
+
+function ControlCount() {
+ return <div>
+ <button onClick={() => state.number++}>+</button>
+ <button onClick={() => state.number--}>-</button>
+ </div>
+}
+
+export default ControlCount;

diff:
+import React from "react";
+import {useSnapshot} from "valtio";
+import state from "../state";
+
+function DisplayCount() {
+ const snap = useSnapshot(state);
+ return
{snap.number}

+}
+
+export default DisplayCount;

diff: Binary files /dev/null and b/examples/with-valtio/favicon.png differ

diff:
+{
+ "scripts": {
+ "build": "umi build",
+ "dev": "umi dev",
+ "start": "npm run dev"
+ },
+ "dependencies": {
+ "umi": "4.0.0-beta.16",
+ "valtio": "^1.2.9"
+ },
+ "devDependencies": {
+ "@types/three": "^0.136.1"
+ }
+}

diff:
+import React from 'react';
+import DisplayCount from "../components/DisplayCount";
+import ControlCount from "../components/ControlCount";
+
+export default function HomePage() {
+ return (
+

+
UmiJS x Valtio

+ <DisplayCount/>
+ <ControlCount/>
+

+
The buttons and the display number is not in one component.

+
They share the state by a proxy using valtio.

+

+

+ );
+}

diff:
+# with-valtio
+
+An example of using [UmiJS](https://umijs.org/zh-CN) with [valtio](https://github.com/pmndrs/valtio).

diff:
+import {proxy} from 'valtio'
+
+const state = proxy({number: 0})
+
+export default state;

diff:
+body {
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: 'Lucida Sans', sans-serif;
+}
+
+.title {
+ font-size: 64px;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+button {
+ background-color: #2eabff;
+ outline: none;
+ border: none;
+ color: white;
+ font-size: 24px;
+ padding: 16px;
+ width: 96px;
+ margin: 0 16px;
+ cursor: pointer;
+ transition: background-color .15s;
+ border-radius: 12px;
+}
+
+button:hover {
+ background-color: #79c4ff;
+}
+
+.display-count {
+ font-size: 36px;
+}
+
+.description {
+ margin-top: 32px;
+}
+
+.description > p {
+ text-align: center;
+ opacity: 0.3;
+}

original_message: example: Added example of using UmiJS with valtio (#283)


message: example: Added react-query example

diff:
+import React from 'react';
+import { QueryClient, QueryClientProvider } from 'react-query';
+
+const queryClient = new QueryClient();
+
+export function rootContainer(container: any, opts: any) {
+ return React.createElement(Foo, opts, container);
+}
+
+export function innerProvider(container: any) {
+ return React.createElement(Foo, { title: 'innerProvider' }, container);
+}
+
+export function i18nProvider(container: any) {
+ return React.createElement(Foo, { title: 'i18nProvider' }, container);
+}
+
+export function dataflowProvider(container: any) {
+ return React.createElement(
+ QueryClientProvider,
+ { client: queryClient },
+ container,
+ );
+}
+
+export function outerProvider(container: any) {
+ return React.createElement(Foo, { title: 'outerProvider' }, container);
+}
+
+function Foo(props: any) {
+ return <div>{props.children}</div>;
+}

diff: Binary files /dev/null and b/examples/react-query/favicon.png differ

diff:
+{
+ "scripts": {
+ "build": "umi build",
+ "dev": "umi dev",
+ "start": "npm run dev"
+ },
+ "dependencies": {
+ "react-query": "^3.34.8",
+ "umi": "4.0.0-beta.16"
+ }
+}

diff:
+import React from 'react';
+import { useQuery } from 'react-query';
+import '../style.less';
+
+export default function HomePage() {
+ const { isLoading, data } = useQuery('repoData', () =>
+ fetch('https://api.github.com/repos/umijs/umi').then((res) => res.json()),
+ );
+
+ return (
+

+

+
UmiJS x react-query

+ {isLoading &&
Loading ...
}
+ {data &&
UmiJS has {data.stargazers_count} stars now!
}
+

+

+ );
+}

diff:
+# react-query
+
+An example of using [UmiJS](https://umijs.org/zh-CN) with [react-query](https://github.com/tannerlinsley/react-query).

diff:
+body {
+ font-family: 'Lucida Sans', sans-serif;
+}
+
+.container {
+ width: 100%;
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ div {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .title {
+ font-size: 64px;
+ }
+
+}

original_message: example: Added react-query example (#289)


diff:
+import cx from 'classnames';
import React, { useState } from 'react';
import MoonIcon from './icons/moon.png';
import SunIcon from './icons/sun.png';
@@ -5,22 +6,21 @@ import SunIcon from './icons/sun.png';
export default () => {
const [toggle, setToggle] = useState(true);
- const toggleClass = ' transform translate-x-6';
-
return (
<div
- className={
- 'md:w-12 md:h-6 w-12 h-4 flex items-center bg-gray-300 ' +
- 'rounded-full p-1 cursor-pointer' +
- (toggle ? ' bg-blue-300' : ' bg-gray-700')
- }
+ className={cx(
+ 'md:w-12 md:h-6 w-12 h-4 flex items-center bg-gray-300 rounded-full ',
+ 'py-1 px-1.5 cursor-pointer',
+ toggle ? 'bg-blue-300' : 'bg-gray-700',
+ )}
onClick={() => setToggle(!toggle)}
>
<div
- className={
- 'md:w-4 md:h-4 h-3 w-3 rounded-full shadow-md transition transform' +
- (toggle ? null : toggleClass)
- }
+ className={cx(
+ 'md:w-4 md:h-4 h-3 w-3 rounded-full shadow-md ',
+ 'transition transform',
+ toggle && 'translate-x-5',
+ )}
>
<img
src={toggle ? SunIcon : MoonIcon}

diff:
"dev": "pnpm build -- --watch"
},
"dependencies": {
- "@mdx-js/mdx": "1.6.22"
+ "@mdx-js/mdx": "1.6.22",
+ "classnames": "^2.3.1"
},
"devDependencies": {
"tailwindcss": "^3.0.15",

diff:
@@ -643,10 +643,12 @@ importers:
packages/plugin-docs:
specifiers:
'@mdx-js/mdx': 1.6.22
+ classnames: ^2.3.1
tailwindcss: ^3.0.15
umi: 4.0.0-beta.16
dependencies:
'@mdx-js/mdx': 1.6.22
+ classnames: 2.3.1
devDependencies:
tailwindcss: 3.0.15_ts-node@10.4.0
umi: link:../umi

original_message: feat(plugin-docs): using classnames package to organize tailwind classnames (#296)


diff:
@@ -26,9 +26,6 @@ export default (props: any) => {

-

- <Toc />
-

{props.children}

diff:
@@ -16,7 +16,7 @@ export default () => {
border-gray-100 p-8 rounded-xl z-20"
>

{route.titles[0].title}

-

+

{titles.map((item: any) => {
return (
<li

original_message: feat(plugin-docs-toc): set max height of TOC and hide TOC in mobile layout (#297)


diff:
+import cx from 'classnames';
import React from 'react';
import Github from './Github';
import LangSwitch from './LangSwitch';
@@ -6,7 +7,12 @@ import NavBar from './NavBar';
import Search from './Search';
import ThemeSwitch from './ThemeSwitch';
-export default () => {
+interface HeadProps {
+ isMenuOpened: boolean;
+ setMenuOpened: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+export default (props: HeadProps) => {
return (

 {
<Logo />

<Search />
+ <HamburgerButton {...props} />

<NavBar />

@@ -31,3 +38,35 @@ export default () => {

);
};
+
+interface HamburgerButtonProps {
+ isMenuOpened: boolean;
+ setMenuOpened: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+function HamburgerButton(props: HamburgerButtonProps) {
+ const barClass =
+ 'block absolute h-0.5 w-5 bg-current transform' +
+ ' transition duration-500 ease-in-out';
+
+ return (
+
 props.setMenuOpened((o) => !o)}
+ >
+
+
+
+

+ );
+}

diff:
-import React from 'react';
+import cx from 'classnames';
+import React, { useState } from 'react';
import { ThemeContext } from './context';
import Head from './Head';
import Sidebar from './Sidebar';
import Toc from './Toc';
export default (props: any) => {
+ const [isMenuOpened, setIsMenuOpened] = useState(false);
+
return (
<ThemeContext.Provider
value={{
@@ -20,7 +23,7 @@ export default (props: any) => {
before:backdrop-blur-md before:absolute before:block
before:w-full before:h-full before:z-[-1]"
>
- <Head />
+ <Head setMenuOpened={setIsMenuOpened} isMenuOpened={isMenuOpened} />
</div>
<div className="w-full flex flex-row justify-center overflow-x-hidden">
@@ -53,6 +56,16 @@ export default (props: any) => {
</div>
</div>
</div>
+
+ <div
+ className={cx(
+ 'fixed top-12 w-screen bg-white z-20',
+ 'overflow-hidden transition-all duration-500',
+ isMenuOpened ? 'max-h-screen' : 'max-h-0',
+ )}
+ >
+ <Sidebar setMenuOpened={setIsMenuOpened} />
+ </div>
</ThemeContext.Provider>
);
};

diff:
@@ -29,7 +29,7 @@ export default () => {
return (
<Fragment>
<div
- className="rounded-lg w-64 flex items-center pr-2 flex-row hover:bg-gray-50
+ className="rounded-lg w-40 lg:w-64 flex items-center pr-2 flex-row hover:bg-gray-50
transition duration-300 bg-gray-100 border border-white focus-within:border-gray-100
focus-within:bg-white"
>

diff:
+import cx from 'classnames';
import React from 'react';
import { useThemeContext } from './context';
-export default () => {
+interface SidebarProps {
+ setMenuOpened?: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+export default (props: SidebarProps) => {
const { appData, components, themeConfig, location } = useThemeContext()!;
const matchedNav = themeConfig.navs.filter((nav) =>
location.pathname.startsWith(nav.path),
@@ -25,7 +30,12 @@ export default () => {
});
return (
-

+

{(matchedNav.children || []).map((item) => {
return (

@@ -50,7 +60,12 @@ export default () => {
}
return (
- <components.Link to={`${matchedNav.path}/${child}`}>
+ <components.Link
+ to={`${matchedNav.path}/${child}`}
+ onClick={() =>
+ props.setMenuOpened && props.setMenuOpened((o) => !o)
+ }
+ >
<div
key={child}
className="text-gray-700 my-2 hover:text-blue-400 transition-all px-4 py-1"

original_message: feat(plugin-docs-menu): Added hamburger menu for mobile screen (#299)


diff:
+import React, { PropsWithChildren } from "react"
+
+enum MessageType {
+ Info = "info",
+ Success = "success",
+ Warning = "warning",
+ Error = "error",
+}
+
+interface MessageProps {
+ type?: MessageType
+ emoji?: string
+}
+
+function Message(props: PropsWithChildren<MessageProps>) {
+ let bgColor = 'bg-blue-50';
+ let textColor = 'text-blue-900';
+
+ switch (props.type) {
+ case MessageType.Success:
+ bgColor = 'bg-green-50';
+ textColor = 'text-green-900';
+ break;
+ case MessageType.Warning:
+ bgColor = 'bg-orange-50';
+ textColor = 'text-orange-900';
+ break;
+ case MessageType.Error:
+ bgColor = 'bg-red-50';
+ textColor = 'text-red-900';
+ break;
+ }
+
+ return <>
+ <div
+ className={`w-full py-3 px-4 ${bgColor} ${textColor} rounded-lg my-2`}>
+ {props.emoji && <span role="img" className="mr-3">
+ {props.emoji}
+ </span>}
+ {props.children}
+ </div>
+ </>
+}
+
+export default Message

diff:
import './tailwind.out.css';
export { default as Layout } from './Layout';
+
+// Components
+export { default as Message } from './components/Message';

diff:
module.exports = {
- content: ['./client/theme-doc/*.tsx'],
+ content: ['./client/theme-doc/**/*.tsx'],
darkMode: 'class'
};

original_message: feat(plugin-docs): Added mdx Message component for docs (#310)


original_message: fix(plugin-docs): Fixed GitHub icon color in dark mode (#313)


diff:
@@ -47,9 +47,14 @@ export default () => {
(document.activeElement as HTMLElement).blur();
});
+ key('up', handleKeyUp);
+ key('down', handleKeyDown);
+
return () => {
key.unbind(searchHotKey);
key.unbind('escape');
+ key.unbind('up');
+ key.unbind('down');
};
}, []);
@@ -89,7 +94,7 @@ export default () => {
<a
href={r.href}
key={i}
- className="group outline-none"
+ className="group outline-none search-result"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
>
@@ -143,3 +148,31 @@ function search(routes: any, keyword: string): SearchResultItem[] {
return result;
}
+
+function handleKeyDown(e: KeyboardEvent) {
+ if (!document.activeElement) return;
+
+ if (document.activeElement.id === 'search-input') {
+ e.preventDefault();
+ (
+ document.getElementsByClassName('search-result')[0] as
+ | HTMLDivElement
+ | undefined
+ )?.focus();
+ return;
+ }
+
+ if (document.activeElement.className.indexOf('search-result') === -1) return;
+
+ e.preventDefault();
+ (document.activeElement?.nextSibling as HTMLDivElement | undefined)?.focus();
+}
+
+function handleKeyUp(e: KeyboardEvent) {
+ if (!document.activeElement) return;
+ if (document.activeElement.className.indexOf('search-result') === -1) return;
+ e.preventDefault();
+ (
+ document.activeElement?.previousSibling as HTMLDivElement | undefined
+ )?.focus();
+}

original_message: feat(plugin-docs): Search result can select with Up and Down key (#317)


diff:
@@ -34,7 +34,7 @@ export default () => {

{route.titles[0].title}

-

+

{titles.map((item: any) => {
return (
<li

original_message: fix(plugin-docs): Removed fadeout effect on TOC (#318)


diff:
@@ -21,10 +21,10 @@ function useLanguage(): useLanguageResult {
isFromPath = !!(s && s.match(/^[a-z]{2}-[A-Z]{2}$/));
if (isFromPath)
- currentLanguage = languages.find(
+ currentLanguage = languages?.find(
(item) => item.locale === location.pathname.split('/')[1],
);
- else currentLanguage = languages[0];
+ else currentLanguage = languages && languages[0] ? languages[0] : undefined;
function switchLanguage(locale: string) {
if (!languages || languages.length === 0) return;
@@ -60,7 +60,7 @@ function useLanguage(): useLanguageResult {
return {
isFromPath,
currentLanguage,
- languages,
+ languages: languages || [],
switchLanguage,
render,
};

original_message: fix(plugin-docs-lang): Fixed that i18n may be undefined bug (#319)


message: feat(plugin-docs): Added announcement

diff:
import cx from 'classnames';
import React, { Fragment, useState } from 'react';
+import Announcement from './components/Announcement';
import { ThemeContext } from './context';
import Head from './Head';
import Sidebar from './Sidebar';
@@ -23,6 +24,7 @@ export default (props: any) => {
before:backdrop-blur-md before:absolute before:block dark:before:bg-opacity-[.85]
before:w-full before:h-full before:z-[-1]"
>
+ <Announcement />
<Head setMenuOpened={setIsMenuOpened} isMenuOpened={isMenuOpened} />
</div>

diff:
+import cx from 'classnames';
+import React, { useEffect, useState } from 'react';
+import { useThemeContext } from '../context';
+
+function Announcement() {
+ const { themeConfig } = useThemeContext()!;
+
+ if (!themeConfig.announcement) {
+ return null;
+ }
+
+ const [closed, setClosed] = useState(false);
+
+ useEffect(() => {
+ if (!themeConfig.announcement) return;
+ const item = localStorage.getItem('closed_announcements');
+ const closed: string[] = item ? JSON.parse(item) : [];
+ if (closed.includes(themeConfig.announcement.title)) {
+ setClosed(true);
+ }
+ }, []);
+
+ function close(e: React.MouseEvent) {
+ e.preventDefault();
+ if (!themeConfig.announcement) return;
+ const item = localStorage.getItem('closed_announcements');
+ const closed: string[] = item ? JSON.parse(item) : [];
+ closed.push(themeConfig.announcement.title);
+ localStorage.setItem('closed_announcements', JSON.stringify(closed));
+ setClosed(true);
+ }
+
+ if (closed) return null;
+
+ return (
+ <div className="w-full py-1 bg-blue-200 dark:bg-blue-900 animate-pulse">
+ <p
+ className={cx(
+ 'text-center dark:text-white text-sm font-bold ',
+ themeConfig.announcement.link && 'cursor-pointer',
+ )}
+ onClick={() => {
+ window.open(themeConfig.announcement?.link, '_blank');
+ }}
+ >
+ {themeConfig.announcement.title}
+ </p>
+ <button className="absolute right-2 top-0 px-8" onClick={close}>
+ x
+ </button>
+ </div>
+ );
+}
+
+export default Announcement;

diff:
@@ -19,6 +19,10 @@ interface IContext {
title: string;
children: any[];
}[];
+ announcement?: {
+ title: string;
+ link?: string;
+ };
};
location: {
pathname: string;

original_message: feat(plugin-docs): Added announcement (#326)


diff:
import cx from 'classnames';
-import React, { Fragment, useState } from 'react';
+import React, { Fragment, useEffect, useState } from 'react';
import Announcement from './components/Announcement';
import { ThemeContext } from './context';
import Head from './Head';
@@ -9,6 +9,25 @@ import Toc from './Toc';
export default (props: any) => {
const [isMenuOpened, setIsMenuOpened] = useState(false);
+ /**
+ FireFox CSS backdrop-filter polyfill
+ https://www.cnblogs.com/coco1s/p/14953143.html
+ */
+ useEffect(() => {
+ let blur = document.getElementById('firefox-head-bg')?.style;
+ let offset = document.getElementById('head-container');
+
+ function updateBlur() {
+ if (!offset || !blur) return;
+ blur.backgroundPosition = `0px ` + `${-window.scrollY + 100}px`;
+ }
+
+ document.addEventListener('scroll', updateBlur, false), updateBlur();
+ return () => {
+ document.removeEventListener('scroll', updateBlur, false);
+ };
+ }, []);
+
return (
<ThemeContext.Provider
value={{
@@ -20,6 +39,7 @@ export default (props: any) => {
>

 {

+

+

+
{window.location.pathname === '/' ? (
-
{props.children}

+
{props.children}

) : (
<Fragment>
-

+

{props.children}

diff:
+/**
+ FireFox CSS backdrop-filter polyfill
+ https://www.cnblogs.com/coco1s/p/14953143.html
+*/
+
+.g-glossy-firefox, .g-glossy-firefox-cover {
+ display: none;
+}
+
+@supports (background: -moz-element(#article-body)) {
+ .g-glossy-firefox-cover {
+ display: block;
+ position: fixed;
+ width: 100%;
+ height: 100px;
+ z-index: 22;
+ background-color: white;
+ }
+
+ .g-glossy-firefox {
+ display: block;
+ position: fixed;
+ width: 100%;
+ height: 100px;
+ z-index: 24;
+ background: -moz-element(#article-body) no-repeat top;
+ filter: blur(10px);
+ }
+}

diff:
+import './firefox-polyfill.css';
import './tailwind.out.css';
// Components
export { default as Hero } from './components/Hero';

original_message: feat(plugin-docs): Added firefox backdrop-filter polyfill (#339)


diff:
@@ -3,4 +3,6 @@ import './tailwind.out.css';
// Components
export { default as Hero } from './components/Hero';
export { default as Message } from './components/Message';
+export { default as Features } from './components/Features';
+export { default as FeatureItem } from './components/FeatureItem';
export { default as Layout } from './Layout';

diff:
@@ -87,6 +87,11 @@ article a {
background-image: linear-gradient(transparent 60%, rgba(130, 199, 255, 0.28) 55%);
}
+.link-with-underline {
+ @apply text-blue-600 mx-1 hover:text-blue-300 transition dark:text-blue-400;
+ background-image: linear-gradient(transparent 60%, rgba(130, 199, 255, 0.28) 55%);
+}
+
/*article pre {*/
/* @apply pt-12;*/
/*}*/
@@ -113,3 +118,28 @@ html {
h1, h2, h3, h4, h5, h6 {
scroll-margin-top: 3em;
}
+
+@layer components {
+
+ .features-dark {
+ @apply bg-gray-900;
+ background-image: radial-gradient(#2a2a2a 20%, transparent 20%);
+ background-size: 6px 6px;
+ width: 100%;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+
+ .features {
+ background-image: radial-gradient(#f8f8f5 20%, transparent 20%);
+ background-color: white;
+ background-size: 6px 6px;
+ width: 100%;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
+
+ .features::-webkit-scrollbar {
+ display: none;
+ }
+}

diff:
@@ -68,7 +68,7 @@ export default (api: IApi) => {
api.writeTmpFile({
path: 'index.ts',
content: `
-export { Message, Hero } from '${require.resolve('../client/theme-doc')}';
+export { Message, Hero, Features, FeatureItem } from '${require.resolve('../client/theme-doc')}';
`,
});

original_message: feat(plugin-docs): Added <Features /> component for landing page (#341)


diff:
@@ -31,17 +31,24 @@ function Message(props: PropsWithChildren<MessageProps>) {
break;
}
+ const messageText =
+ typeof props.children === 'string'
+ ? props.children
+ : (props.children as React.ReactElement).props.children;
+
return (
<>
<div
- className={`w-full py-3 px-4 ${bgColor} ${textColor} rounded-lg my-2`}
+ className={`w-full py-3 px-4 ${bgColor} ${textColor} rounded-lg my-4 mdx-message`}
>
+

{props.emoji && (
-
+
{props.emoji}

)}
- {props.children}
+ {messageText}
+

</>
);

diff:
@@ -28,7 +28,7 @@ article h6 {
}
article p {
- @apply text-base font-light leading-8 mt-4 text-gray-700 dark:text-white;
+ @apply text-base leading-8 mt-4 text-gray-700 dark:text-white;
}
article ul {
@@ -146,4 +146,8 @@ h1, h2, h3, h4, h5, h6 {
.features::-webkit-scrollbar {
display: none;
}
+
+ .mdx-message > p {
+ @apply mt-0;
+ }
}

original_message: feat(plugin-docs): Improved <Message /> component styles (#388)


diff:
@@ -46,7 +46,7 @@ export default async ({
{
type: 'select',
name: 'registry',
- message: 'Pick Bpm Registry',
+ message: 'Pick Npm Registry',
choices: [
{
title: 'npm',

original_message: fix(create-umi-app): Fix typo "Bpm" to "Npm" (#423)


diff:
@@ -12,6 +12,7 @@ export default async function (api: IApi, apiRoutes: IRoute[]) {
await esbuild.build({
format: 'cjs',
+ platform: 'node',
bundle: true,
entryPoints: [
...apiRoutePaths,

diff:
@@ -18,8 +18,8 @@ export default async function (api: IApi, apiRoutes: IRoute[]) {
);
await esbuild.build({
- format: 'esm',
- outExtension: { '.js': '.mjs' },
+ format: 'cjs',
+ platform: 'node',
bundle: true,
entryPoints: [
...apiRoutePaths,

original_message: fix(api-route): Fix bug of build platform and format (#465)


diff:
+export type { UmiApiRequest, UmiApiResponse } from './features/apiRoute';
export type { IApi, IConfig, webpack } from './types';
export default () => {
return {

diff:
// @ts-ignore
export * from '@@/exports';
-export type { IApi, webpack } from '@umijs/preset-umi';
export type {
+ IApi,
+ webpack,
IRoute,
UmiApiRequest,
UmiApiResponse,
-} from '@umijs/preset-umi/src/types';
+} from '@umijs/preset-umi';
export * from './dist';

original_message: fix(api-route-types): Fix cannot import type of api route from preset-umi/dist (#527)


message: fix(plugin-docs): Fix TOC link makes page reload loop

diff:
@@ -38,8 +38,7 @@ function MDXContent(props = {}) {
useEffect(() => {
if (window.location.hash.length !== 0) {
const hash = window.location.hash;
- window.location.hash = '';
- window.location.hash = hash;
+ document.getElementById(hash.slice(1))?.scrollIntoView();
}
}, []);

original_message: fix(plugin-docs): Fix #644 TOC link makes page reload loop (#653)


diff:
-import React from 'react';
+import React, { useState } from 'react';
import { useThemeContext } from './context';
import useLanguage from './useLanguage';
export default () => {
- const { components, themeConfig } = useThemeContext()!;
- const lang = useLanguage();
+ const { themeConfig } = useThemeContext()!;
return (

- {themeConfig.navs.map((nav: any) => {
+ {themeConfig.navs.map((nav: any) => (
+ <NavItem nav={nav} key={nav.path} />
+ ))}
+

+ );
+};
+
+interface NavItemProps {
+ nav: {
+ path: string;
+ title: string;
+ dropdown?: {
+ title: string;
+ path: string;
+ }[];
+ };
+}
+
+function NavItem(props: NavItemProps) {
+ const { components } = useThemeContext()!;
+ const { nav } = props;
+ const lang = useLanguage();
+ const [isExpanded, setExpanded] = useState(false);
return (
-

+
 nav.dropdown && setExpanded(true)}
+ onMouseLeave={() => nav.dropdown && setExpanded(false)}
+ >
<components.Link
to={
- lang.isFromPath
- ? lang.currentLanguage?.locale + nav.path
- : nav.path
+ lang.isFromPath ? lang.currentLanguage?.locale + nav.path : nav.path
}
>
{lang.render(nav.title)}
</components.Link>
+ {nav.dropdown && (
+

+ {nav.dropdown.map((n) => (
+ <components.Link
+ key={n.path}
+ to={
+ lang.isFromPath ? lang.currentLanguage?.locale + n.path : n.path
+ }
+ >
+

+ {n.title}
+

+ </components.Link>
+ ))}
+

+ )}

);
- })}
-

- );
-};
+}

diff:
@@ -22,6 +22,7 @@ interface IContext {
navs: {
path: string;
title: string;
+ dropdown?: { title: string; path: string }[];
children: any[];
}[];
announcement?: {

original_message: feat(plugin-docs): Add dropdown for nav items (#657)


diff:
@@ -46,6 +46,8 @@ function MDXContent(props = {}) {
if (window.location.hash.length !== 0) {
const hash = window.location.hash;
document.getElementById(hash.slice(1))?.scrollIntoView();
+ } else {
+ window.scrollTo(0, 0);
}
}, []);

original_message: fix(plugin-docs): not scroll to top when route change (#658)


diff:
@@ -63,6 +63,7 @@ export default () => {
return (
<Fragment>
<div
+ id="search-input-wrapper"
className="rounded-lg w-40 lg:w-64 flex items-center pr-2 flex-row hover:bg-gray-50
transition duration-300 bg-gray-100 border border-white focus-within:border-gray-100
focus-within:bg-white dark:bg-gray-700 dark:border-gray-700 relative
@@ -84,6 +85,7 @@ export default () => {
{isMac ? macSearchKey : windowsSearchKey}
</div>
<div
+ id="search-results-wrapper"
className={cx(
'absolute transition-all duration-500 top-12 w-96 rounded-lg',
'cursor-pointer shadow overflow-hidden',

original_message: feat(plugin-docs): Add id attribute of search input component for style override (#674)