Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e020f0ea6 | ||
|
|
a37d4f89de | ||
|
|
c82e095b75 | ||
|
|
8168865f3a | ||
|
|
4b80b885e0 | ||
|
|
e2b9fc0392 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "commands",
|
"name": "commands",
|
||||||
"version": "0.2.6",
|
"version": "0.2.8",
|
||||||
"description": "CLI y migraciones para Duckbrain",
|
"description": "CLI y migraciones para Duckbrain",
|
||||||
"components": {
|
"components": {
|
||||||
"cli": {
|
"cli": {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ final class AdoptCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$coreAdopted = $this->adoptCore($parsed['options'], $root, $lock, $dryRun);
|
[$coreAdopted, $coreHasLocal] = $this->adoptCore($parsed['options'], $root, $lock, $dryRun);
|
||||||
|
|
||||||
if (!$dryRun) {
|
if (!$dryRun) {
|
||||||
$lock->save($root);
|
$lock->save($root);
|
||||||
@@ -100,7 +100,8 @@ final class AdoptCommand extends Command
|
|||||||
$this->console->ok('adoptado ' . $id . $mark);
|
$this->console->ok('adoptado ' . $id . $mark);
|
||||||
}
|
}
|
||||||
if ($coreAdopted > 0) {
|
if ($coreAdopted > 0) {
|
||||||
$this->console->ok("adoptado el core ({$coreAdopted} archivos)");
|
$mark = $coreHasLocal ? ' (con ediciones locales)' : '';
|
||||||
|
$this->console->ok("adoptado el core ({$coreAdopted} archivos){$mark}");
|
||||||
}
|
}
|
||||||
if ($adopted === [] && $coreAdopted === 0) {
|
if ($adopted === [] && $coreAdopted === 0) {
|
||||||
$this->console->out('No se encontró nada que adoptar.');
|
$this->console->out('No se encontró nada que adoptar.');
|
||||||
@@ -111,33 +112,42 @@ final class AdoptCommand extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Registra el core existente: sólo los archivos que también existen en el
|
* Registra el core existente: sólo los archivos que también existen en el
|
||||||
* core de referencia, con su hash actual. No toca archivos.
|
* core de referencia, con su hash CANÓNICO. Si la copia local difiere,
|
||||||
|
* el archivo queda marcado como edición local y `update core` preguntará
|
||||||
|
* antes de pisarla (mismo contrato que `adoptComponent`).
|
||||||
*
|
*
|
||||||
* @param array<string, string|bool> $options
|
* @param array<string, string|bool> $options
|
||||||
|
* @return array{0:int,1:bool}
|
||||||
*/
|
*/
|
||||||
private function adoptCore(array $options, string $root, $lock, bool $dryRun): int
|
private function adoptCore(array $options, string $root, $lock, bool $dryRun): array
|
||||||
{
|
{
|
||||||
if ($lock->core() !== null) {
|
if ($lock->core() !== null) {
|
||||||
return 0;
|
return [0, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$provider = $this->coreProvider($options);
|
$provider = $this->coreProvider($options);
|
||||||
} catch (ToolboxException $e) {
|
} catch (ToolboxException $e) {
|
||||||
$this->console->warn('no se pudo resolver el core para adoptar: ' . $e->getMessage());
|
$this->console->warn('no se pudo resolver el core para adoptar: ' . $e->getMessage());
|
||||||
return 0;
|
return [0, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
$files = [];
|
$files = [];
|
||||||
|
$hasLocal = false;
|
||||||
foreach ($provider->files() as $relative) {
|
foreach ($provider->files() as $relative) {
|
||||||
$absolute = $root . '/' . $relative;
|
$absolute = $root . '/' . $relative;
|
||||||
if (is_file($absolute)) {
|
if (!is_file($absolute)) {
|
||||||
$files[$relative] = Drift::hash($absolute);
|
continue;
|
||||||
|
}
|
||||||
|
$canonical = $provider->read($relative);
|
||||||
|
$files[$relative] = 'sha256:' . hash('sha256', $canonical);
|
||||||
|
if (file_get_contents($absolute) !== $canonical) {
|
||||||
|
$hasLocal = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($files === []) {
|
if ($files === []) {
|
||||||
return 0;
|
return [0, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$dryRun) {
|
if (!$dryRun) {
|
||||||
@@ -145,9 +155,10 @@ final class AdoptCommand extends Command
|
|||||||
'version' => $provider->version(),
|
'version' => $provider->version(),
|
||||||
'commit' => $provider->commit(),
|
'commit' => $provider->commit(),
|
||||||
'files' => $files,
|
'files' => $files,
|
||||||
|
'origin' => $hasLocal ? 'local' : 'adopted',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return count($files);
|
return [count($files), $hasLocal];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Toolbox;
|
|||||||
*/
|
*/
|
||||||
final class Config
|
final class Config
|
||||||
{
|
{
|
||||||
public const VERSION = '0.2.6';
|
public const VERSION = '0.2.8';
|
||||||
|
|
||||||
public static function toolboxUrl(): string
|
public static function toolboxUrl(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -284,6 +284,13 @@ final class Installer
|
|||||||
}
|
}
|
||||||
|
|
||||||
$absolute = $this->root . '/' . $relative;
|
$absolute = $this->root . '/' . $relative;
|
||||||
|
|
||||||
|
if ($options['dryRun'] ?? false) {
|
||||||
|
$actions[$relative] = 'planned';
|
||||||
|
$installed[$relative] = 'sha256:' . hash('sha256', $provider->read($relative));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$action = $this->sync->write($absolute, $provider->read($relative), $expected[$relative] ?? null, $options);
|
$action = $this->sync->write($absolute, $provider->read($relative), $expected[$relative] ?? null, $options);
|
||||||
$actions[$relative] = $action;
|
$actions[$relative] = $action;
|
||||||
|
|
||||||
@@ -300,6 +307,10 @@ final class Installer
|
|||||||
if (isset($installed[$relative]) || isset($protected[$relative]) || in_array($relative, $files, true)) {
|
if (isset($installed[$relative]) || isset($protected[$relative]) || in_array($relative, $files, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if ($options['dryRun'] ?? false) {
|
||||||
|
$actions[$relative] = 'remove-planned';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$action = $this->sync->remove($this->root . '/' . $relative, $hash, $options);
|
$action = $this->sync->remove($this->root . '/' . $relative, $hash, $options);
|
||||||
if ($action === FileSync::ABORTED) {
|
if ($action === FileSync::ABORTED) {
|
||||||
$aborted[] = $relative;
|
$aborted[] = $relative;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "htmx",
|
"name": "htmx",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"description": "Motor de vistas basado en componentes y HTMX",
|
"description": "Motor de vistas basado en componentes y HTMX",
|
||||||
"core": {
|
"core": {
|
||||||
"requires": ">=0.1",
|
"requires": ">=0.1",
|
||||||
|
|||||||
@@ -13,29 +13,6 @@ namespace Libs;
|
|||||||
*/
|
*/
|
||||||
class HTMX extends HTMLComponent
|
class HTMX extends HTMLComponent
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Valid HTMX swap styles.
|
|
||||||
*/
|
|
||||||
public const SWAP_STYLES = [
|
|
||||||
'innerHTML', 'outerHTML', 'outerSync',
|
|
||||||
'beforebegin', 'before',
|
|
||||||
'afterbegin', 'prepend',
|
|
||||||
'beforeend', 'append',
|
|
||||||
'afterend', 'after',
|
|
||||||
'delete', 'none',
|
|
||||||
'innerMorph', 'outerMorph',
|
|
||||||
'textContent',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Valid HTMX swap option keys (used as key:value after the style).
|
|
||||||
*/
|
|
||||||
public const SWAP_OPTIONS = [
|
|
||||||
'swap', 'settle', 'transition', 'ignoreTitle', 'strip',
|
|
||||||
'focusScroll', 'swapEmpty', 'scroll', 'show', 'showTarget',
|
|
||||||
'scrollTarget', 'target',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a component
|
* Renders a component
|
||||||
*
|
*
|
||||||
@@ -266,8 +243,6 @@ class HTMX extends HTMLComponent
|
|||||||
* @param bool $excludeBoosted Exclude if the request is boosted (default: false)
|
* @param bool $excludeBoosted Exclude if the request is boosted (default: false)
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*
|
|
||||||
* @throws \InvalidArgumentException On an unknown style or option.
|
|
||||||
*/
|
*/
|
||||||
public static function swapOob(string $value = 'true', bool $excludeBoosted = false): string
|
public static function swapOob(string $value = 'true', bool $excludeBoosted = false): string
|
||||||
{
|
{
|
||||||
@@ -275,10 +250,6 @@ class HTMX extends HTMLComponent
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value !== 'true') {
|
|
||||||
$value = static::validateSwap($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return static::isHtmx() ? 'hx-swap-oob="' . $value . '"' : '';
|
return static::isHtmx() ? 'hx-swap-oob="' . $value . '"' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,8 +289,6 @@ class HTMX extends HTMLComponent
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$swap = static::validateSwap($swap);
|
|
||||||
|
|
||||||
return '<hx-partial hx-target="' . htmlspecialchars($target)
|
return '<hx-partial hx-target="' . htmlspecialchars($target)
|
||||||
. '" hx-swap="' . htmlspecialchars($swap) . '">'
|
. '" hx-swap="' . htmlspecialchars($swap) . '">'
|
||||||
. $content
|
. $content
|
||||||
@@ -381,52 +350,10 @@ class HTMX extends HTMLComponent
|
|||||||
* @param string $rule
|
* @param string $rule
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*
|
|
||||||
* @throws \InvalidArgumentException On an unknown style or option.
|
|
||||||
*/
|
*/
|
||||||
public static function reswap(string $rule = 'innerHTML'): void
|
public static function reswap(string $rule = 'innerHTML'): void
|
||||||
{
|
{
|
||||||
header('HX-Reswap: ' . static::validateSwap($rule));
|
header('HX-Reswap: ' . $rule);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates a HTMX swap value ("style [key:value]...").
|
|
||||||
*
|
|
||||||
* @param string $value
|
|
||||||
*
|
|
||||||
* @return string The normalized value.
|
|
||||||
*
|
|
||||||
* @throws \InvalidArgumentException On an unknown style or option.
|
|
||||||
*/
|
|
||||||
public static function validateSwap(string $value): string
|
|
||||||
{
|
|
||||||
$parts = preg_split(
|
|
||||||
'/\s+/',
|
|
||||||
trim($value),
|
|
||||||
-1,
|
|
||||||
PREG_SPLIT_NO_EMPTY
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($parts)) {
|
|
||||||
throw new \InvalidArgumentException('Empty swap value.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!in_array($parts[0], static::SWAP_STYLES, true)) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
'Invalid swap style: ' . $parts[0]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (array_slice($parts, 1) as $option) {
|
|
||||||
$key = explode(':', $option)[0];
|
|
||||||
if (!in_array($key, static::SWAP_OPTIONS, true)) {
|
|
||||||
throw new \InvalidArgumentException(
|
|
||||||
'Invalid swap option: ' . $option
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return implode(' ', $parts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -194,6 +194,48 @@ Harness::it('--dry-run no escribe archivos ni lock', function () {
|
|||||||
rrmdir($tool);
|
rrmdir($tool);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Harness::it('update core --dry-run no escribe archivos', function () {
|
||||||
|
$proj = tmpdir('proj');
|
||||||
|
$tool = g7_toolbox();
|
||||||
|
$core = g7_core('1.0.0', 'router-v1');
|
||||||
|
$core2 = g7_core('2.0.0', 'router-v2');
|
||||||
|
|
||||||
|
(new InstallCommand(new Console()))->run([$proj, '--toolbox=' . $tool, '--core-dir=' . $core, '--yes']);
|
||||||
|
|
||||||
|
$routerBefore = (string) file_get_contents($proj . '/src/Libs/Router.php');
|
||||||
|
(new UpdateCommand(new Console()))->run(['core', '--root=' . $proj, '--toolbox=' . $tool, '--core-dir=' . $core2, '--yes', '--dry-run']);
|
||||||
|
|
||||||
|
ok(str_contains($routerBefore, 'router-v1'), 'estado inicial correcto');
|
||||||
|
ok(str_contains((string) file_get_contents($proj . '/src/Libs/Router.php'), 'router-v1'), 'dry-run no escribe el fichero');
|
||||||
|
eq('1.0.0', Lock::load($proj)->core()['version'], 'dry-run no actualiza la version en el lock');
|
||||||
|
|
||||||
|
rrmdir($proj);
|
||||||
|
rrmdir($tool);
|
||||||
|
rrmdir($core);
|
||||||
|
rrmdir($core2);
|
||||||
|
});
|
||||||
|
|
||||||
|
Harness::it('update core --dry-run aborta por drift sin escribir', function () {
|
||||||
|
$proj = tmpdir('proj');
|
||||||
|
$tool = g7_toolbox();
|
||||||
|
$core = g7_core('1.0.0', 'router-v1');
|
||||||
|
$core2 = g7_core('2.0.0', 'router-v2');
|
||||||
|
|
||||||
|
(new InstallCommand(new Console()))->run([$proj, '--toolbox=' . $tool, '--core-dir=' . $core, '--yes']);
|
||||||
|
write_file($proj . '/config.php', "<?php\n// customizado\n");
|
||||||
|
|
||||||
|
(new UpdateCommand(new Console()))->run(['core', '--root=' . $proj, '--toolbox=' . $tool, '--core-dir=' . $core2, '--yes', '--dry-run']);
|
||||||
|
|
||||||
|
ok(!is_file($proj . '/config.php.bak'), 'dry-run no crea backups');
|
||||||
|
ok(str_contains((string) file_get_contents($proj . '/config.php'), 'customizado'), 'dry-run conserva la edicion local');
|
||||||
|
ok(str_contains((string) file_get_contents($proj . '/src/Libs/Router.php'), 'router-v1'), 'dry-run no toca ficheros sin drift');
|
||||||
|
|
||||||
|
rrmdir($proj);
|
||||||
|
rrmdir($tool);
|
||||||
|
rrmdir($core);
|
||||||
|
rrmdir($core2);
|
||||||
|
});
|
||||||
|
|
||||||
Harness::it('core sin tags: versión desconocida y commit pineado', function () {
|
Harness::it('core sin tags: versión desconocida y commit pineado', function () {
|
||||||
$proj = tmpdir('proj');
|
$proj = tmpdir('proj');
|
||||||
$tool = g7_toolbox();
|
$tool = g7_toolbox();
|
||||||
|
|||||||
@@ -121,3 +121,31 @@ Harness::it('adopt registra el core existente con sus hashes', function () {
|
|||||||
rrmdir($proj);
|
rrmdir($proj);
|
||||||
rrmdir($core);
|
rrmdir($core);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Harness::it('adopt del core registra hash canónico y marca ediciones locales', function () {
|
||||||
|
$repo = $GLOBALS['g9_repo'];
|
||||||
|
$core = g7_core('0.1.0');
|
||||||
|
$proj = tmpdir('proj');
|
||||||
|
|
||||||
|
// Copia idéntica de Router.php + config.php customizado (proyecto heredado).
|
||||||
|
write_file($proj . '/src/Libs/Router.php', (string) file_get_contents($core . '/src/Libs/Router.php'));
|
||||||
|
write_file($proj . '/config.php', "<?php\n// credenciales del proyecto\n");
|
||||||
|
|
||||||
|
(new AdoptCommand(new Console()))->run(['--root=' . $proj, '--toolbox=' . $repo, '--core-dir=' . $core]);
|
||||||
|
|
||||||
|
$lock = Lock::load($proj);
|
||||||
|
$coreHashRouter = $lock->core()['files']['src/Libs/Router.php'];
|
||||||
|
$coreHashConfig = $lock->core()['files']['config.php'];
|
||||||
|
|
||||||
|
$canonicalRouter = (string) file_get_contents($core . '/src/Libs/Router.php');
|
||||||
|
$canonicalConfig = (string) file_get_contents($core . '/config.php');
|
||||||
|
|
||||||
|
eq('sha256:' . hash('sha256', $canonicalRouter), $coreHashRouter, 'hash de Router.php = canónico');
|
||||||
|
eq('sha256:' . hash('sha256', $canonicalConfig), $coreHashConfig, 'hash de config.php = canónico (aunque el local difiera)');
|
||||||
|
eq('local', $lock->core()['origin'] ?? null, 'origin = local porque hay ediciones locales');
|
||||||
|
|
||||||
|
ok(file_get_contents($proj . '/config.php') !== $canonicalConfig, 'la copia local customizada no se tocó');
|
||||||
|
|
||||||
|
rrmdir($proj);
|
||||||
|
rrmdir($core);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user