diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..c27096e
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,21 @@
+* text=auto eol=lf
+
+/.github export-ignore
+/tests export-ignore
+/docs export-ignore
+/AGENTS.md export-ignore
+/CONTRIBUTING.md export-ignore
+/qa-adoption.json export-ignore
+/phpcs.xml export-ignore
+/phpcs.xml.dist export-ignore
+/phpstan.neon export-ignore
+/phpstan.neon.dist export-ignore
+/phpunit*.xml* export-ignore
+/.php-cs-fixer* export-ignore
+/.gitignore export-ignore
+/.gitattributes export-ignore
+/.env* export-ignore
+/auth.json export-ignore
+/.npmrc export-ignore
+/composer.review*.json export-ignore
+/composer.review*.lock export-ignore
diff --git a/.github/workflows/dependency-canary.yml b/.github/workflows/dependency-canary.yml
new file mode 100644
index 0000000..c716908
--- /dev/null
+++ b/.github/workflows/dependency-canary.yml
@@ -0,0 +1,18 @@
+name: Dependency compatibility
+
+on:
+ schedule:
+ - cron: '26 4 * * 3'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ compatibility:
+ permissions:
+ contents: read
+ issues: write
+ uses: sympress/workflows/.github/workflows/dependency-canary.yml@177fa0d727b278d2103052ec77c102b4a4c492a0
+ with:
+ php_version: '8.5'
diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml
index 98a7111..f43b763 100644
--- a/.github/workflows/qa.yml
+++ b/.github/workflows/qa.yml
@@ -12,10 +12,10 @@ permissions:
jobs:
workflows:
- uses: sympress/workflows/.github/workflows/lint-workflows.yml@v1
+ uses: sympress/workflows/.github/workflows/lint-workflows.yml@177fa0d727b278d2103052ec77c102b4a4c492a0
qa:
- uses: sympress/workflows/.github/workflows/sympress-qa.yml@v1
+ uses: sympress/workflows/.github/workflows/sympress-qa.yml@177fa0d727b278d2103052ec77c102b4a4c492a0
with:
php_version: '8.5'
secrets:
diff --git a/README.md b/README.md
index afff22b..1fc2c66 100644
--- a/README.md
+++ b/README.md
@@ -71,3 +71,19 @@ repository.
## License
This package is licensed under `GPL-2.0-or-later`.
+
+## Generated literals, output paths, and Process
+
+PHP skeleton values use exported literals; JavaScript/TSX values use JSON-based
+quoting with escaped HTML delimiters and Unicode separators. Identifiers are
+validated separately, and generated frontend code compares data-block attribute
+values without interpolating user text into a CSS selector. Root project packages
+and nested packages both generate relative output paths. MakePackage --path must
+be relative and remain inside the canonical project, including nonexistent children
+of symlinks; traversal and absolute paths are rejected and checked again before
+writing. Project/cache parents must remain trusted while generation runs.
+
+This bundle declares the real symfony/process ^8.1 dependency and integrates with
+symfony/maker-bundle ^1.68. It no longer provides a fictitious Symfony Process
+version through wp-cli/process. Generated PHP, webpack JavaScript and TSX/TS must
+be syntax checked alongside existing fixture contracts before release.
diff --git a/Resources/skeleton/asset_entry/AssetConfigLoader.tpl.php b/Resources/skeleton/asset_entry/AssetConfigLoader.tpl.php
index 06fd508..6fcc4bf 100644
--- a/Resources/skeleton/asset_entry/AssetConfigLoader.tpl.php
+++ b/Resources/skeleton/asset_entry/AssetConfigLoader.tpl.php
@@ -9,7 +9,7 @@
final class = $class_name; ?> implements ConfigLoaderInterface
{
- public const HANDLE = '= $asset_handle; ?>';
+ public const HANDLE = = var_export($asset_handle, true); ?>;
public function accepts(Asset $asset): bool
{
diff --git a/Resources/skeleton/block/Block.tpl.php b/Resources/skeleton/block/Block.tpl.php
index 8b2cb64..949a9a6 100644
--- a/Resources/skeleton/block/Block.tpl.php
+++ b/Resources/skeleton/block/Block.tpl.php
@@ -14,12 +14,12 @@ final class = $class_name; ?> implements = $localizable ? 'LocalizableBlockI
{
- public const JS_CONFIG_VAR = '= $js_config_var; ?>';
+ public const JS_CONFIG_VAR = = var_export($js_config_var, true); ?>;
public function name(): string
{
- return '= $block_name; ?>';
+ return = var_export($block_name, true); ?>;
}
/**
@@ -29,17 +29,17 @@ public function args(): array
{
return [
'api_version' => 2,
- 'title' => __('= $title; ?>', '= $text_domain; ?>'),
- 'description' => __('= $description; ?>', '= $text_domain; ?>'),
- 'category' => '= $category; ?>',
- 'icon' => '= $icon; ?>',
+ 'title' => __(= var_export($title, true); ?>, = var_export($text_domain, true); ?>),
+ 'description' => __(= var_export($description, true); ?>, = var_export($text_domain, true); ?>),
+ 'category' => = var_export($category, true); ?>,
+ 'icon' => = var_export($icon, true); ?>,
'supports' => [
'html' => false,
],
'attributes' => [],
- 'editor_script' => '= $editor_handle; ?>',
+ 'editor_script' => = var_export($editor_handle, true); ?>,
- 'script' => '= $frontend_handle; ?>',
+ 'script' => = var_export($frontend_handle, true); ?>,
'render_callback' => [$this, 'render'],
];
@@ -61,7 +61,7 @@ public function localize(): array
public function render(array $attributes): string
{
- $template = __DIR__ . '/= $view_path; ?>';
+ $template = __DIR__ . = var_export('/' . $view_path, true); ?>;
if (!is_file($template)) {
return '';
@@ -73,7 +73,7 @@ public function render(array $attributes): string
return (string) ob_get_clean();
- return '
';
+ return = var_export('', true); ?>;
}
}
diff --git a/Resources/skeleton/config_loader/FrontendConfigLoader.tpl.php b/Resources/skeleton/config_loader/FrontendConfigLoader.tpl.php
index 36ea562..94f7cd4 100644
--- a/Resources/skeleton/config_loader/FrontendConfigLoader.tpl.php
+++ b/Resources/skeleton/config_loader/FrontendConfigLoader.tpl.php
@@ -9,7 +9,7 @@
final class = $class_name; ?> implements ConfigLoaderInterface
{
- public const FRONTEND = '= $frontend_handle; ?>';
+ public const FRONTEND = = var_export($frontend_handle, true); ?>;
public function accepts(Asset $asset): bool
{
diff --git a/Resources/skeleton/config_loader/GutenbergConfigLoader.tpl.php b/Resources/skeleton/config_loader/GutenbergConfigLoader.tpl.php
index a07dfb5..2ad2884 100644
--- a/Resources/skeleton/config_loader/GutenbergConfigLoader.tpl.php
+++ b/Resources/skeleton/config_loader/GutenbergConfigLoader.tpl.php
@@ -11,7 +11,7 @@
final class = $class_name; ?> implements ConfigLoaderInterface
{
- public const EDITOR = '= $editor_handle; ?>';
+ public const EDITOR = = var_export($editor_handle, true); ?>;
/** @var list */
private array $blocks = [];
@@ -45,7 +45,7 @@ public function process(Asset $asset): Asset
if ($asset instanceof Script) {
$asset
- ->withLocalize('= $localize_var; ?>', $localization)
+ ->withLocalize(= var_export($localize_var, true); ?>, $localization)
->withDependencies('wp-blocks');
}
diff --git a/composer.json b/composer.json
index 7fb4dd6..e11c215 100644
--- a/composer.json
+++ b/composer.json
@@ -13,17 +13,14 @@
"require": {
"php": "^8.5",
"composer-runtime-api": "^2.1",
- "symfony/maker-bundle": "^1.0",
- "sympress/kernel": "dev-main",
- "wp-cli/process": "^5.9.99"
- },
- "provide": {
- "symfony/process": "8.1.0"
+ "symfony/maker-bundle": "^1.68",
+ "sympress/kernel": "^1.1.1",
+ "symfony/process": "^8.1"
},
"require-dev": {
"symfony/security-core": "^8.1",
"symfony/validator": "^8.1",
- "sympress/qa": "dev-main"
+ "sympress/qa": "^0.1.2"
},
"autoload": {
"psr-4": {
@@ -75,15 +72,6 @@
"phpstan/extension-installer": true
}
},
- "repositories": [
- {
- "type": "path",
- "url": "./../*",
- "options": {
- "symlink": true
- }
- }
- ],
- "minimum-stability": "dev",
+ "minimum-stability": "stable",
"prefer-stable": true
}
diff --git a/docs/releases/1.0.2.md b/docs/releases/1.0.2.md
new file mode 100644
index 0000000..7636db3
--- /dev/null
+++ b/docs/releases/1.0.2.md
@@ -0,0 +1,9 @@
+# 1.0.2 release candidate
+
+This candidate contains the 2026-10-01 review repairs and requires the stable
+SymPress dependency train. It has not been published yet. CI consumers use the
+reviewed workflows commit 177fa0d727b278d2103052ec77c102b4a4c492a0.
+
+Local exact-commit Composer archives verify dependency resolution and distribution
+contents before publication. They do not imply that prospective tags exist on
+GitHub or Packagist. Historical tags remain unchanged.
diff --git a/src/Maker/MakeBlock.php b/src/Maker/MakeBlock.php
index c37aac9..d0451a2 100644
--- a/src/Maker/MakeBlock.php
+++ b/src/Maker/MakeBlock.php
@@ -8,6 +8,7 @@
use SymPress\MakerBundle\Util\PackageContext;
use SymPress\MakerBundle\Util\PackageContextResolver;
use SymPress\MakerBundle\Util\PackageServiceConfigurator;
+use SymPress\MakerBundle\Util\SourceLiteral;
use SymPress\MakerBundle\Util\WebpackEncoreEntryConfigurator;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
@@ -137,7 +138,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
if ($withView) {
$generator->dumpFile(
$context->packageRelativePath(sprintf('Resources/views/block/%s.php', $slug)),
- "\n",
+ '' . "\n",
);
}
@@ -248,14 +249,14 @@ private function editorSource(string $blockName, string $title, string $textDoma
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
-registerBlockType('%s', {
- title: __('%s', '%s'),
+registerBlockType(%s, {
+ title: __(%s, %s),
edit: () => {
const blockProps = useBlockProps();
return (
- {__('%s', '%s')}
+ {__(%s, %s)}
);
},
@@ -263,16 +264,17 @@ private function editorSource(string $blockName, string $title, string $textDoma
});
TS
,
- $blockName,
- $title,
- $textDomain,
- $title,
- $textDomain,
+ SourceLiteral::javascript($blockName),
+ SourceLiteral::javascript($title),
+ SourceLiteral::javascript($textDomain),
+ SourceLiteral::javascript($title),
+ SourceLiteral::javascript($textDomain),
) . "\n";
}
private function frontendSource(string $blockName): string
{
- return sprintf("document.querySelectorAll('[data-block=\"%s\"]');\n", $blockName);
+ return "Array.from(document.querySelectorAll('[data-block]')).filter(element => element.getAttribute('data-block') === "
+ . SourceLiteral::javascript($blockName) . ");\n";
}
}
diff --git a/src/Maker/MakeHook.php b/src/Maker/MakeHook.php
index aca3b8f..62549bc 100644
--- a/src/Maker/MakeHook.php
+++ b/src/Maker/MakeHook.php
@@ -6,6 +6,7 @@
use SymPress\MakerBundle\Util\PackageContextResolver;
use SymPress\MakerBundle\Util\PackageServiceConfigurator;
+use SymPress\MakerBundle\Util\SourceLiteral;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
@@ -55,6 +56,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
}
$method = (string) $input->getOption('method');
+ SourceLiteral::identifier($method);
$classDetails = $generator->createClassNameDetails((string) $input->getArgument('class'), 'Hook\\');
$generator->generateClass(
$classDetails->getFullName(),
diff --git a/src/Maker/MakeSympressPackage.php b/src/Maker/MakeSympressPackage.php
index 26a9f69..92b7657 100644
--- a/src/Maker/MakeSympressPackage.php
+++ b/src/Maker/MakeSympressPackage.php
@@ -5,6 +5,8 @@
namespace SymPress\MakerBundle\Maker;
use SymPress\MakerBundle\Util\JsonFile;
+use SymPress\MakerBundle\Util\ProjectPath;
+use SymPress\MakerBundle\Util\SourceLiteral;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Generator;
@@ -73,6 +75,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$packageType = $this->normalizePackageType((string) $input->getOption('type'));
$packageDirectory = $this->packageDirectory($packageName);
$packagePath = (string) ($input->getOption('path') ?: sprintf('packages/%s', $packageDirectory));
+ $packagePath = ProjectPath::relative($this->projectDir, $packagePath);
$absolutePackagePath = sprintf('%s/%s', rtrim($this->projectDir, '/'), trim($packagePath, '/'));
if (is_dir($absolutePackagePath)) {
@@ -80,6 +83,9 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
}
$namespace = trim((string) ($input->getOption('namespace') ?: $this->namespace($packageName)), '\\');
+ foreach (explode('\\', $namespace) as $segment) {
+ SourceLiteral::identifier($segment);
+ }
$bundleClass = $this->isKernelPackage($packageType)
? sprintf('%s\\%s', $namespace, $this->bundleClassName($packageName))
: null;
@@ -117,6 +123,7 @@ public function generate(InputInterface $input, ConsoleStyle $io, Generator $gen
$generator->dumpFile('composer.json', $this->rootComposerWithPackage($packageName, (bool) $input->getOption('dev')));
}
+ ProjectPath::relative($this->projectDir, $packagePath);
$generator->writeChanges();
$this->writeSuccessMessage($io);
$io->text(sprintf('Package "%s" (%s) was created at %s.', $packageName, $packageType, $packagePath));
@@ -373,7 +380,7 @@ private function pluginEntry(string $packageName, string $description, string $n
PHP
,
Str::asHumanWords($packageName),
- $description,
+ str_replace(['*/', "\r", "\n"], ['* /', ' ', ' '], $description),
$namespace,
Str::getShortClassName($bundleClass),
) . "\n";
@@ -421,7 +428,7 @@ private function themeStylesheet(string $packageName, string $description): stri
CSS
,
Str::asHumanWords($name),
- $description,
+ str_replace(['*/', "\r", "\n"], ['* /', ' ', ' '], $description),
$name,
) . "\n";
}
diff --git a/src/SymPressMakerBundle.php b/src/SymPressMakerBundle.php
index f11ac78..224b322 100644
--- a/src/SymPressMakerBundle.php
+++ b/src/SymPressMakerBundle.php
@@ -8,6 +8,7 @@
use SymPress\MakerBundle\Composer\InstalledComposerPackageLocator;
use SymPress\MakerBundle\Composer\PackageAutoloadResolver;
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass;
+use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeDecoratorPass;
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\RemoveMissingParametersPass;
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\SetDoctrineAnnotatedPrefixesPass;
use Symfony\Bundle\MakerBundle\MakerBundle;
@@ -17,6 +18,7 @@
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
+use Symfony\Component\DependencyInjection\Reference;
final class SymPressMakerBundle extends AbstractBundle
{
@@ -51,6 +53,9 @@ public function loadExtension(
$configurator->import(sprintf('%s/config/makers.php', $makerBundlePath));
$configurator->import('../Resources/config/services.php');
+ // Keep the complete constructor positional when Symfony rebuilds debug/lint definitions.
+ $container->getDefinition('maker.maker.make_command')->setArguments([null, new Reference('maker.file_manager')]);
+
$rootNamespace = $this->rootNamespace($config, $container);
$entityNamespace = $this->entityNamespace($config, $rootNamespace);
@@ -79,6 +84,7 @@ public function build(ContainerBuilder $container): void
->addTag(MakeCommandRegistrationPass::MAKER_TAG);
$container->addCompilerPass(new MakeCommandRegistrationPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
+ $container->addCompilerPass(new MakeDecoratorPass());
$container->addCompilerPass(new RemoveMissingParametersPass());
$container->addCompilerPass(new SetDoctrineAnnotatedPrefixesPass());
}
diff --git a/src/Util/PackageContext.php b/src/Util/PackageContext.php
index b3effca..4f5fcc9 100644
--- a/src/Util/PackageContext.php
+++ b/src/Util/PackageContext.php
@@ -47,20 +47,23 @@ public function relativePath(string $path): string
$path = str_replace('\\', '/', $path);
$projectDir = rtrim(str_replace('\\', '/', $this->projectDir), '/');
+ if ($path === $projectDir) {
+ return '';
+ }
if (str_starts_with($path, $projectDir . '/')) {
return substr($path, strlen($projectDir) + 1);
}
- return ltrim($path, '/');
+ return ProjectPath::relative($this->projectDir, $path);
}
public function packageRelativePath(string $path): string
{
- return sprintf(
+ return ltrim(sprintf(
'%s/%s',
$this->relativePath($this->packagePath),
ltrim(str_replace('\\', '/', $path), '/'),
- );
+ ), '/');
}
public function tagPrefix(): string
diff --git a/src/Util/PackageServiceConfigurator.php b/src/Util/PackageServiceConfigurator.php
index c3f9438..1dc81e7 100644
--- a/src/Util/PackageServiceConfigurator.php
+++ b/src/Util/PackageServiceConfigurator.php
@@ -4,6 +4,8 @@
namespace SymPress\MakerBundle\Util;
+use Symfony\Component\Yaml\Yaml;
+
final class PackageServiceConfigurator
{
/** @param list> $tags */
@@ -245,7 +247,7 @@ private function appendPhpDefinition(string $contents, string $className, array
private function yamlSimpleTags(array $tags): array
{
if (count($tags) === 1) {
- return [sprintf(' tags: [\'%s\']', $this->yamlQuote($tags[0]))];
+ return [sprintf(' tags: [%s]', $this->yamlQuote($tags[0]))];
}
return [
@@ -275,7 +277,7 @@ private function yamlTags(array $tags): array
*/
private function phpSimpleTags(array $tags): array
{
- return array_map(static fn (string $tag): string => sprintf(' ->tag(\'%s\')', $tag), $tags);
+ return array_map(static fn (string $tag): string => sprintf(' ->tag(%s)', var_export($tag, true)), $tags);
}
/**
@@ -285,7 +287,7 @@ private function phpSimpleTags(array $tags): array
private function phpTags(array $tags): array
{
return array_map(
- fn (array $tag): string => sprintf(' ->tag(\'%s\', %s)', $tag['name'], $this->phpArray($tag)),
+ fn (array $tag): string => sprintf(' ->tag(%s, %s)', var_export($tag['name'], true), $this->phpArray($tag)),
$tags,
);
}
@@ -312,7 +314,7 @@ private function phpArray(array $values): string
$parts[] = sprintf(
'\'%s\' => %s',
$key,
- is_int($value) ? (string) $value : sprintf('\'%s\'', str_replace('\'', '\\\'', (string) $value)),
+ is_int($value) ? (string) $value : var_export((string) $value, true),
);
}
@@ -321,6 +323,6 @@ private function phpArray(array $values): string
private function yamlQuote(string $value): string
{
- return '\'' . str_replace('\'', '\'\'', $value) . '\'';
+ return Yaml::dump($value);
}
}
diff --git a/src/Util/ProjectPath.php b/src/Util/ProjectPath.php
new file mode 100644
index 0000000..acd1c9e
--- /dev/null
+++ b/src/Util/ProjectPath.php
@@ -0,0 +1,35 @@
+insertEntry($contents, $line);
return new FileUpdate($context->relativePath($file), $updated);
diff --git a/tests/Maker/MakerContainerTest.php b/tests/Maker/MakerContainerTest.php
new file mode 100644
index 0000000..e090623
--- /dev/null
+++ b/tests/Maker/MakerContainerTest.php
@@ -0,0 +1,95 @@
+setParameter('kernel.project_dir', sys_get_temp_dir());
+ $container->setParameter('kernel.package_prefixes', []);
+ $container->setParameter('twig.default_path', null);
+ $container->register('filesystem', Filesystem::class);
+ $container->register('event_dispatcher', EventDispatcher::class);
+ $bundle = new SymPressMakerBundle();
+ $bundle->build($container);
+ $container->getExtension('sympress_maker')->load([['root_namespace' => 'App']], $container);
+ foreach ($container->getDefinitions() as $definition) {
+ if ($definition->isAbstract()) {
+ continue;
+ }
+ $definition->setPublic(true);
+ }
+ $container->addCompilerPass(new class implements CompilerPassInterface {
+ public function process(ContainerBuilder $container): void
+ {
+ foreach ($container->findTaggedServiceIds('console.command') as $id => $tags) {
+ $container->getDefinition($id)->setPublic(true);
+ }
+ }
+ }, PassConfig::TYPE_BEFORE_REMOVING, 1000);
+ $container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
+ $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
+ $container->compile();
+ $dump = new PhpDumper($container)->dump(['class' => 'MakerContainerRegression']);
+ self::assertIsString($dump);
+ self::assertArrayNotHasKey('fileManager', $container->getDefinition('maker.maker.make_command')->getArguments());
+
+ // Debug/lint reconstructs definitions in a fresh builder; normalized bare named keys fail there.
+ $lint = new ContainerBuilder();
+ $lint->getParameterBag()->add($container->getParameterBag()->all());
+ $lint->setDefinitions($container->getDefinitions());
+ $lint->setAliases($container->getAliases());
+ $lint->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100);
+ $lint->compile();
+ self::assertInstanceOf(MakeCommand::class, $lint->get('maker.maker.make_command'));
+ $helper = $lint->get('maker.decorator_helper');
+ self::assertInstanceOf(DecoratorHelper::class, $helper);
+ self::assertContains('event_dispatcher', $helper->suggestIds());
+ self::assertSame('event_dispatcher', $helper->getRealId('EventDispatcher'));
+ self::assertSame(EventDispatcher::class, $helper->getClass('event_dispatcher'));
+ foreach ($lint->getDefinitions() as $id => $definition) {
+ if (!str_starts_with($id, 'maker.') || str_starts_with($id, 'maker.auto_command.')) {
+ continue;
+ }
+ $class = $definition->getClass();
+ if (!is_string($class)) {
+ continue;
+ }
+ self::assertInstanceOf($class, $lint->get($id), $id);
+ }
+ $application = new Application();
+ $application->setAutoExit(false);
+ foreach ($lint->getDefinitions() as $id => $definition) {
+ if (!str_starts_with($id, 'maker.auto_command.')) {
+ continue;
+ }
+ $command = $lint->get($id);
+ self::assertInstanceOf(Command::class, $command, $id);
+ $application->addCommand($command);
+ }
+ $tester = new ApplicationTester($application);
+ self::assertSame(Command::SUCCESS, $tester->run(['command' => 'list', 'namespace' => 'make', '--format' => 'json']));
+ $commands = json_decode($tester->getDisplay(), true, flags: JSON_THROW_ON_ERROR);
+ self::assertContains('make:decorator', array_column($commands['commands'], 'name'));
+ }
+}
diff --git a/tests/Util/GeneratedSourceSafetyTest.php b/tests/Util/GeneratedSourceSafetyTest.php
new file mode 100644
index 0000000..407c0f5
--- /dev/null
+++ b/tests/Util/GeneratedSourceSafetyTest.php
@@ -0,0 +1,76 @@
+";
+ $variables = [
+ 'namespace' => 'Example',
+ 'class_name' => 'DangerBlock',
+ 'localizable' => true,
+ 'block_name' => $value,
+ 'title' => $value,
+ 'description' => $value,
+ 'category' => $value,
+ 'icon' => $value,
+ 'text_domain' => $value,
+ 'editor_handle' => $value,
+ 'frontend_handle' => $value,
+ 'js_config_var' => $value,
+ 'with_frontend' => true,
+ 'with_view' => false,
+ 'view_path' => $value,
+ ];
+ $file = tempnam(sys_get_temp_dir(), 'maker-literal-');
+ self::assertIsString($file);
+ file_put_contents($file, (new \ReflectionClass(FileManager::class))->newInstanceWithoutConstructor()->parseTemplate(dirname(__DIR__, 2) . '/Resources/skeleton/block/Block.tpl.php', $variables));
+ try {
+ $process = new Process([PHP_BINARY, '-l', $file]);
+ self::assertSame(0, $process->run(), $process->getOutput() . $process->getErrorOutput());
+ self::assertStringNotContainsString('