/**
* Hyva Theme Coding Standards Preset
* Technology: Alpine.js + TailwindCSS (no jQuery, no RequireJS, no KnockoutJS, no LESS)
* Reference: https://docs.hyva.io/hyva-themes/compatibility-modules/development-guidelines.html
*/
import { ThemeStandard } from '../types.js';
export const HYVA_THEME: ThemeStandard = {
id: 'hyva',
name: 'Hyva Theme',
version: '1.0.0',
description: 'Standards for Hyva-based Magento themes using Alpine.js and TailwindCSS. Hyva removes jQuery, KnockoutJS, RequireJS, and LESS entirely.',
technologies: {
use: [
'Alpine.js (x-data, x-show, x-on, x-model, x-for, x-if)',
'TailwindCSS utility classes',
'Vanilla JavaScript',
'ViewModelRegistry ($viewModels->require())',
'Inline JS in phtml templates',
'ES modules (where needed)',
'Magento\\Framework\\Escaper ($escaper)',
],
avoid: [
'jQuery and $ selectors',
'KnockoutJS (data-bind, ko.observable)',
'RequireJS (define(), require())',
'LESS preprocessor',
'data-mage-init attribute',
'x-magento-init scripts with RequireJS paths',
'Magento UI Components (uiComponent)',
'$.widget() factory',
],
},
validationRules: [
{
pattern: '\\$\\s*\\(|jQuery\\s*\\(',
fileTypes: ['js', 'phtml'],
severity: 9,
type: 'error',
message: 'jQuery usage detected. Hyva themes do NOT include jQuery. Use Alpine.js or vanilla JavaScript.',
rule: 'Hyva.JS.NoJQuery',
suggestion: 'Replace $() with document.querySelector() or Alpine.js directives (x-data, x-on, x-show)',
mode: 'discourage',
},
{
pattern: '\\bdefine\\s*\\(\\s*\\[|\\brequire\\s*\\(\\s*\\[',
fileTypes: ['js'],
severity: 9,
type: 'error',
message: 'RequireJS (define/require) detected. Hyva does NOT use RequireJS. Use inline scripts or ES modules.',
rule: 'Hyva.JS.NoRequireJS',
suggestion: 'Use Alpine.js components or plain <script> tags. For reusable logic, use Alpine.data() or ES modules.',
mode: 'discourage',
},
{
pattern: 'data-mage-init',
fileTypes: ['phtml'],
severity: 8,
type: 'warning',
message: 'data-mage-init detected. Hyva uses Alpine.js directives instead of Magento JS initialization.',
rule: 'Hyva.Template.NoDataMageInit',
suggestion: 'Use x-data with Alpine.data() for component initialization. See: docs.hyva.io/hyva-themes/writing-code/',
mode: 'discourage',
},
{
pattern: 'data-bind\\s*=',
fileTypes: ['phtml'],
severity: 9,
type: 'error',
message: 'KnockoutJS data-bind detected. Hyva does NOT use KnockoutJS. Use Alpine.js directives.',
rule: 'Hyva.Template.NoKnockout',
suggestion: 'Replace data-bind="text: value" with x-text="value", data-bind="visible: show" with x-show="show"',
mode: 'discourage',
},
{
pattern: 'ko\\.observable|ko\\.computed|ko\\.applyBindings',
fileTypes: ['js'],
severity: 9,
type: 'error',
message: 'KnockoutJS API detected. Hyva uses Alpine.js reactivity instead.',
rule: 'Hyva.JS.NoKnockout',
suggestion: 'Use Alpine.js reactive properties in x-data: { count: 0, get double() { return this.count * 2 } }',
mode: 'discourage',
},
{
pattern: '\\.widget\\s*\\(|\\$\\.widget\\s*\\(',
fileTypes: ['js'],
severity: 8,
type: 'warning',
message: 'jQuery widget factory detected. Hyva uses Alpine.js components instead.',
rule: 'Hyva.JS.NoWidgetFactory',
suggestion: 'Use Alpine.data("widgetName", () => ({ ... })) for reusable components',
mode: 'discourage',
},
{
pattern: 'x-magento-init',
fileTypes: ['phtml'],
severity: 8,
type: 'warning',
message: 'x-magento-init script detected. In Hyva, use Alpine.js initialization instead.',
rule: 'Hyva.Template.NoMagentoInit',
suggestion: 'Use <script> with Alpine.data() or x-data directly on elements',
mode: 'discourage',
},
{
pattern: '@import\\s+["\']|@import\\s+\\(',
fileTypes: ['less', 'css'],
severity: 8,
type: 'warning',
message: 'LESS/CSS imports detected. Hyva uses TailwindCSS. Custom styles go in tailwind.config.js.',
rule: 'Hyva.CSS.NoLessImports',
suggestion: 'Use TailwindCSS utility classes in templates. Extend styles in tailwind.config.js.',
mode: 'discourage',
},
],
patternOverrides: [
{
taskKeyword: 'requirejs module',
correctPattern: 'Use Alpine.js components or inline vanilla JavaScript',
example: `<!-- Hyva: Alpine.js component in phtml template -->
<div x-data="initMyComponent()">
<button x-on:click="handleClick()" class="btn btn-primary">
<span x-text="buttonLabel"></span>
</button>
<div x-show="isOpen" x-transition class="mt-4">
Content here
</div>
</div>
<script>
'use strict';
function initMyComponent() {
return {
isOpen: false,
buttonLabel: 'Toggle',
handleClick() {
this.isOpen = !this.isOpen;
this.buttonLabel = this.isOpen ? 'Close' : 'Toggle';
}
}
}
</script>`,
avoidPatterns: [
{ pattern: 'define() / require()', reason: 'Hyva does not use RequireJS' },
{ pattern: 'jQuery / $', reason: 'jQuery is not available in Hyva' },
{ pattern: 'data-mage-init', reason: 'Use Alpine.js directives in Hyva' },
],
explanation: 'Hyva replaces RequireJS with Alpine.js and vanilla JavaScript. Components are defined as functions returning Alpine.js state objects.',
},
{
taskKeyword: 'jquery widget',
correctPattern: 'Use Alpine.data() for reusable components (CSP-compatible)',
example: `<?php
declare(strict_types=1);
/** @var \\Magento\\Framework\\View\\Element\\Template $block */
/** @var \\Magento\\Framework\\Escaper $escaper */
/** @var \\Hyva\\Theme\\Model\\ViewModelRegistry $viewModels */
?>
<!-- Hyva: Reusable Alpine.js component (CSP-safe) -->
<div x-data="myWidget" class="p-4 bg-white rounded-lg shadow">
<span x-text="count" class="text-2xl font-bold"></span>
<button @click="increment" class="ml-2 px-4 py-2 bg-blue-500 text-white rounded">+</button>
<button @click="decrement" class="ml-2 px-4 py-2 bg-gray-500 text-white rounded">-</button>
</div>
<script>
'use strict';
Alpine.data('myWidget', () => ({
count: 0,
increment() { this.count++; },
decrement() { if (this.count > 0) this.count--; }
}));
</script>`,
avoidPatterns: [
{ pattern: '$.widget()', reason: 'jQuery widget factory not available in Hyva' },
{ pattern: 'uiComponent', reason: 'Magento UI Components not used in Hyva' },
],
explanation: 'Use Alpine.data() for reusable components. For CSP compliance, register components with Alpine.data() instead of inline x-data expressions.',
},
{
taskKeyword: 'template structure',
correctPattern: 'Use $block, $escaper, ViewModelRegistry, Alpine.js, TailwindCSS',
example: `<?php
declare(strict_types=1);
use Hyva\\Theme\\Model\\ViewModelRegistry;
use Magento\\Framework\\Escaper;
use Magento\\Framework\\View\\Element\\Template;
/** @var Template $block */
/** @var Escaper $escaper */
/** @var ViewModelRegistry $viewModels */
$myViewModel = $viewModels->require(\\Vendor\\Module\\ViewModel\\MyViewModel::class);
?>
<div x-data="{ showDetails: false }" class="container mx-auto p-4">
<h2 class="text-2xl font-bold mb-4">
<?= $escaper->escapeHtml($myViewModel->getTitle()) ?>
</h2>
<?php if ($myViewModel->hasItems()): ?>
<ul class="space-y-2">
<?php foreach ($myViewModel->getItems() as $item): ?>
<li class="flex items-center gap-2">
<a href="<?= $escaper->escapeUrl($item->getUrl()) ?>"
class="text-blue-600 hover:underline">
<?= $escaper->escapeHtml($item->getName()) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p class="text-gray-500"><?= $escaper->escapeHtml(__('No items found.')) ?></p>
<?php endif; ?>
<button @click="showDetails = !showDetails"
class="mt-4 px-4 py-2 bg-primary text-white rounded hover:bg-primary-darker">
<span x-text="showDetails ? '<?= $escaper->escapeJs(__('Hide')) ?>' : '<?= $escaper->escapeJs(__('Show Details')) ?>'"></span>
</button>
<template x-if="showDetails">
<div class="mt-4 p-4 bg-gray-50 rounded">
<?= /** @noEscape */ $block->getChildHtml('details') ?>
</div>
</template>
</div>`,
avoidPatterns: [
{ pattern: '$this->helper()', reason: 'Use ViewModelRegistry $viewModels->require() in Hyva' },
{ pattern: '$block->getData("viewModel")', reason: 'Hyva uses $viewModels->require(ClassName::class) instead' },
],
explanation: 'Hyva templates use ViewModelRegistry for data access, Alpine.js for interactivity, and TailwindCSS for styling. Always use declare(strict_types=1) and proper PHPDoc annotations.',
},
{
taskKeyword: 'escape html',
correctPattern: '$escaper->escapeHtml() (same as base Magento, but always with TailwindCSS context)',
example: `<?php
declare(strict_types=1);
/** @var \\Magento\\Framework\\Escaper $escaper */
?>
<!-- Hyva: Escaping with TailwindCSS classes -->
<div class="p-4">
<h1 class="text-xl font-bold"><?= $escaper->escapeHtml($block->getTitle()) ?></h1>
<p class="text-gray-600"><?= $escaper->escapeHtml($block->getDescription()) ?></p>
<a href="<?= $escaper->escapeUrl($block->getUrl()) ?>" class="text-blue-500 hover:underline">
<?= $escaper->escapeHtml(__('Learn More')) ?>
</a>
</div>`,
avoidPatterns: [],
explanation: 'Escaping works the same in Hyva as base Magento. Always escape output. Style with TailwindCSS utility classes.',
},
{
taskKeyword: 'ui component',
correctPattern: 'Use Alpine.js component (Hyva does not use Magento UI Components)',
example: `<!-- Hyva: Alpine.js replaces uiComponent/KnockoutJS -->
<div x-data="{
items: [],
loading: true,
async init() {
const response = await fetch('/rest/V1/my/endpoint');
this.items = await response.json();
this.loading = false;
}
}">
<template x-if="loading">
<div class="animate-pulse">Loading...</div>
</template>
<template x-if="!loading">
<ul>
<template x-for="item in items" :key="item.id">
<li x-text="item.name" class="py-1"></li>
</template>
</ul>
</template>
</div>`,
avoidPatterns: [
{ pattern: 'uiComponent / Component.extend()', reason: 'Not used in Hyva' },
{ pattern: 'ko.observable()', reason: 'KnockoutJS not available in Hyva' },
],
explanation: 'Hyva replaces Magento UI Components with Alpine.js. Use x-data for state, x-for for iteration, template x-if for conditionals, and fetch() for API calls.',
},
],
bestPractices: [
'Always use declare(strict_types=1) at the top of phtml files',
'Annotate templates with: /** @var \\Hyva\\Theme\\Model\\ViewModelRegistry $viewModels */',
'Use $viewModels->require(ClassName::class) instead of $block->getData("viewModel")',
'Use Alpine.data("name", () => ({...})) for reusable, CSP-compatible components',
'x-if REQUIRES a <template> tag wrapper: <template x-if="condition"><div>...</div></template>',
'Use TailwindCSS utility classes for all styling; configure custom values in tailwind.config.js',
'No custom .css or .less files needed in most cases - use Tailwind @apply in source CSS if needed',
'Inline JavaScript in templates rather than external JS files',
'Use vanilla JS fetch() for AJAX instead of jQuery.ajax()',
'Use document.querySelector/querySelectorAll instead of jQuery selectors',
'For event delegation, use Alpine.js @click or vanilla addEventListener',
'Hyva provides helper ViewModels: use $viewModels->require() for cart, customer, etc.',
],
};