Skip to main content

PUG

PUG

image.png

PUG (ранее: Jade) - высокопроизводительный шаблонизатор HTML разметки.

Этот проект ранее был известен как "Jade". Однако разработчикам стало известно, что "Jade" является зарегистрированной торговой маркой; в результате потребовалось переименование. После некоторого обсуждения среди сопровождающих в качестве нового имени для этого проекта было выбрано "Pug" . Начиная с версии 2, "pug" является официальным именем пакета.


Основные ресурсы

Ссылки на библиотеки и фреймворки используемые в сборке. 

Vite: https://vite.dev/

RollUpJs: https://rollupjs.org/

TypeScript: https://www.typescriptlang.org/

HTMX: https://htmx.org/

_hyperScript: https://hyperscript.org/

PUG JavaScript:

Основная документация JavaScript (может потребоваться VPN) : https://pugjs.org/api/getting-started.html
Основной репозиторий JavaScript кода: https://github.com/pugjs/pug

PUG PHP:

Основная документация для транспайлера PHP: https://www.phug-lang.com/
Основной репозиторий для PHP: https://github.com/pug-php/pug


Инсталляция

Разворачиваем каркас приложения

Для установки необходимо наличие служб Node.JS и NPM. 
В примерах производится установка на версиях:
node - 21.7.3
npm - 10.5.0

Инициализируем установку шаблона приложение, с помощью движка Vite.

npm create vite@latest . -- --template vanilla-ts

Устанавливаем зависимости.
После установки зависимостей (пакеты библиотек) в проекте появится папка /node_modules 

npm install

Делаем тестовый запуск.

npm run dev

image.png

Следующий шагом необходимо привести файл package.json к следующему виду:

Добавляются следующие библиотеки и решения: веб-сервер express, htmx, pino логгер, типы для typescript, плагин pug, sass-препроцессор css, плагин для инъекций в приложение.

{
  "name": "any-project-name",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "description": "",
  "author": "",
  "license": "ISC",
  "main": "index.js",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  },
  "dependencies": {
    "express": "^4.18.2",
    "htmx.org": "^2.0.1",
    "pino": "^8.19.0",
    "pino-colada": "^2.2.2",
    "pino-pretty": "^10.3.1"
  },
  "devDependencies": {
    "@antfu/eslint-config": "^2.6.4",
    "@rollup/plugin-inject": "^5.0.5",
    "@rushstack/eslint-patch": "^1.7.2",
    "@types/glob": "^8.1.0",
    "@types/node": "^20.11.13",
    "@types/pino": "^7.0.5",
    "@types/pug": "^2.0.10",
    "@typescript-eslint/eslint-plugin": "^6.20.0",
    "@typescript-eslint/parser": "^6.20.0",
    "eslint": "^8.56.0",
    "glob": "^10.4.2",
    "lint-staged": "^15.2.1",
    "pug": "^3.0.2",
    "sass": "^1.70.0",
    "sass-loader": "^14.1.0",
    "typescript": "~5.6.2",
    "vite": "^5.4.10",
    "vite-plugin-pug": "^0.3.2"
  }
}

Повторно запустить процесс добавление зависимостей.

npm install

Удаляем все файлы из папки /src

Модифицируем файл tsconfig.json следующими правками:

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "baseUrl": ".",

    /* Bundler mode */
    "moduleResolution": "Bundler",
    "paths": {
      "@/*": ["src/*"],
      "@/assets": ["src/assets/*"],
      "@/components": ["src/components/*"],
      "@/models": ["src/models/*"],
      "@/pages": ["src/pages/*"],
      "@/plugins": ["src/plugins/*"],
      "@/scripts": ["src/scripts/*"],
      "@/libs": ["src/libs/*"]
  },
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",

    /* Linting */
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmit": true,

  },
  "references": [{ "path": "./tsconfig.node.json" }],
  "include": ["src", "src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules"],
  "types": ["vite/client"],
}

Добавляем файл tsconfig.node.json в корень проекта, со следующим содержанием:

{
    "compilerOptions": {
        "composite": true,
        "module": "ESNext",
        "moduleResolution": "bundler",
        "allowSyntheticDefaultImports": true,
        "skipLibCheck": true
    },
    "include": ["vite.config.ts"]
}

Добавляем файл .prettierrc в корень проекта, со следующим содержанием:

{
    "semi": true,
    "singleQuote": true,
    "bracketSpacing": true,
    "trailingComma": "all",
    "printWidth": 80,
    "tabWidth": 4,
    "arrowParens": "always",
    "endOfLine": "lf"
}

Добавляем файл eslint.config.js в корень проекта, со следующим содержанием:

import antfu from '@antfu/eslint-config';

export default antfu({
    rules: {
        curly: 'off',
        'antfu/consistent-list-newline': 'off',
        'antfu/if-newline': 'off',
        'import/newline-after-import': 0,
        'no-console': 'off',
        'style/arrow-parens': 'off',
        'style/brace-style': 'off',
        'style/indent': 'off',
        'style/operator-linebreak': 'off',
        'style/spaced-comment': 'off',
    },
    typescript: true,
    yaml: false,
    stylistic: {
        semi: true,
        indent: 4,
        quotes: 'single',
    },
    ignores: ['public/', 'node_modules/', 'dist/', 'build/', 'bruno/'],
});

Добавляем файл vite.config.ts в корень проекта, со следующим содержанием:

import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { globSync } from 'glob';
import { defineConfig } from 'vite';
import pugPlugin from 'vite-plugin-pug';

/**
 * ! не удалять и неизменять
 * Добавляет на каждую страницу инициализацию для скриптов из ./src/common.ts, в head нужно для htmx и _hyperscript
 *
 * todo: приветствуюется лучшее решение для инициализации, или ждем пока разработчики запакуют исходники в модуль ES6
 *
 * @see https://vitejs.dev/guide/api-plugin#transformindexhtml
 */
function commonPlugin() {
    return {
        name: 'html-add-common',
        transformIndexHtml() {
            return [
                {
                    tag: 'script',
                    attrs: {
                        src: 'https://unpkg.com/hyperscript.org@0.9.13',
                    },
                    injectTo: 'head',
                },
                {
                    tag: 'script',
                    attrs: {
                        src: 'https://unpkg.com/htmx.org@2.0.3',
                        integrity:
                            'sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq',
                        crossorigin: 'anonymous',
                    },
                    injectTo: 'head',
                },
                {
                    tag: 'title',
                    children: 'Умный Сервис',
                    injectTo: 'head',
                },
                {
                    tag: 'script',
                    attrs: {
                        type: 'module',
                        src: '/src/main.ts',
                    },
                    injectTo: 'body',
                },
                //{
                //    tag: 'script',
                //    attrs: { type: 'module', src: './src/q-core.ts' },
                //},
                //{
                //    tag: 'script',
                //    attrs: { type: 'module', src: './src/common.ts' },
                //},
            ];
        },
    };
}

export default defineConfig({
    build: {
        cssMinify: false,
        minify: false,
        rollupOptions: {
            /**
             * @see https://vite-docs-ru.vercel.app/config/#build-sourcemap
             * @see https://rollupjs.org/configuration-options/#core-functionality
             */
            input: Object.fromEntries(
                globSync(['./*.html', './src/html/**/*.html'])
                    .filter((file) => {
                        // Exclude debug.html in build stage
                        if (
                            process.env.NODE_ENV === 'production' &&
                            file === 'debug.html'
                        )
                            return false;
                        return true;
                    })
                    .map((file) => [
                        path.relative(
                            '',
                            file.slice(
                                0,
                                file.length - path.extname(file).length,
                            ),
                        ),
                        fileURLToPath(new URL(file, import.meta.url)),
                    ]),
            ),
            output: {
                // inlineDynamicImports: true,
                chunkFileNames: `assets/js/[name].[hash].js`,
                entryFileNames: `assets/js/[name].[hash].js`,
                assetFileNames: (assetInfo) => {
                    let extType = assetInfo.name.split('.').pop();
                    if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType))
                        extType = 'img';

                    if (/eot|ttf|woff2?/i.test(extType)) extType = 'font';

                    return `assets/${extType}/[name].[hash][extname]`;
                },
            },
        },
    },
    css: {
        preprocessorOptions: {
            sass: {
                //? https://sass-lang.com/documentation/breaking-changes/import/
                silenceDeprecations: ['import', 'call-string'],
                //additionalData: `@import './src/assets/sass/main.sass'`,
            },
        },
    },
    plugins: [
        pugPlugin({ doctype: 'html' }),
        /** @ts-expect-error some description generic problems*/
        commonPlugin(),
    ],
    resolve: {
        alias: {
            '@': path.resolve(__dirname, './src'),
            '@assets': path.resolve(__dirname, './src/assets'),
            '@components': path.resolve(__dirname, './src/components'),
            '@models': path.resolve(__dirname, './src/models'),
            '@pages': path.resolve(__dirname, './src/pages'),
            '@plugins': path.resolve(__dirname, './src/plugins'),
            '@scripts': path.resolve(__dirname, './src/scripts'),
            '@libs': path.resolve(__dirname, './src/libs'),
        },
    },
    server: {
        host: '0.0.0.0',
        port: 8084,
    },
});

В паке /src создаем следующие папки:

/assets - в этой папке создаем папки: /font - исходники шрифтов, /sass - файлы со стилями проекта для препроцессора sass
/libs - для библиотек js/ts
/pages - содержит страницы сущности в формате PUG
/plugins - кастомные плагины
/scripts - кастомные скрипты
 /blocks - файлы с блоками PUG (их можно воспринимать как классы в программировании)
/components - файлы с компонентами PUG (в основном это миксины)
/partials - файлы с частицами PUG (логически завершенные куски кода/разметки)



Файловая структура проекта

В этой секции изложены основные требования к содержанию и ведению файловой структуры любого проекта в котором используется этот шаблонизатор.


Блоки (Blocks)

Блок



Частицы (Partials)

Частица



Компоненты (Components)

Компонент


Примеси (Mixins)

Примеси (Миксины) - скомпилированные функции позволяющие создавать повторнопереиспользуемые блоки с разметкой. 

Миксины содержут неявную директиву &attributes(attributes) , позволяющую прокинуть извне атрибуты и их значения к привязонному к ней селектору.

Аргументы функции миксина позволяют принимать значения по умолчанию. mixin article(title='Default Title')

Миксин может получить неограниченное число аргументов. Используется структура аргумента spread Пример: mixin list(id, ...items)

Подробнее: https://pugjs.org/language/mixins.html


Стандартизация

В этом блоке описываются общие требования к синтаксическому оформлению pug файлов проекта.

  1. Название вызываемой функции миксина должно соответствовать названию файла с миксином.
  2. Функция миксина должна использовать не более трех аргументов, если аргументов больше, необходимо воспользоваться директивой &attributes(attributes).
  3. При подключении файла, первым идет указатель на папку: ./ - текущая директория, ../ - родительская директория.
  4. При подключении файла всегда указываем его расширение.
  5. Файл pug не должен содержать "магических" строк и чисел, все значения привязываем к переменным.
  6. Если набор данных возможно проитерировать, его необходимо вынести в json файл.
  7. Обязательно оставляем комментарии если файл разметки содержит опциональные варианты контента.
  8. Запрещено использовать литералы шаблона JS `${ переменная }`, не использовать .join('') и иные функции объектов в буферизированном потоке. Исполняемый код необходимо размещать в переменных.
  9. Не использовать фиксированные наименования у маршрутов к ресурсам проектов. Необходимо передавать полный uri путь к файлу.
  10. Использовать интерполяцию для закрывающихся и самозакрывающихся тегов.

 


 

 

FAQ

Основные ошибки и способы их устранения.