diff --git a/MIGRATION.md b/MIGRATION.md index 56abc82..94abfac 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -50,28 +50,52 @@ with this: ``` -### 3. [New feature] __Multiple *script* tags are now supported within the same *.html* file and can live together with standard *HTML* code.__ +### 3. [Breaking change] __*Twig*'s extensions are now wrapped in the *extensions* property within the configuration.__ -```html - - - - +Replace this: +```js +/* vite.config.js */ +import { defineConfig } from 'vite' +import twig from 'vite-plugin-twig' - - - + }) + ] +}) +``` - +with this: +```js +/* vite.config.js */ +import { defineConfig } from 'vite' +import twig from 'vite-plugin-twig' - - - - +export default defineConfig({ + // ... + plugins: [ + twig({ + // ... + extensions: { + filters: { + // ... + }, + functions: { + // ... + } + } + }) + ] +}) ``` @@ -93,11 +117,74 @@ export default defineConfig({ }) ``` -or + + +### 5. [New option] __Via the *fileFilter* option, now the plugin provides a custom way to determine if the current transforming .html file should be processed/ignored or not__ + ```js -/* twig.config.js */ -module.exports = { +/* vite.config.js */ +import { defineConfig } from 'vite' +import twig from 'vite-plugin-twig' + +export default defineConfig({ // ... - cache: true -} + plugins: [ + twig({ + // ... + fileFilter: filename => { + // your custom logic + return true // or false + } + }) + ] +}) + ``` + + +### 6. [New option] __Via the *fragmentFilter* option, now the plugin provides a custom way to determine if a matched fragment should be processed/ignored or not__ + +```js +/* vite.config.js */ +import { defineConfig } from 'vite' +import twig from 'vite-plugin-twig' + +export default defineConfig({ + // ... + plugins: [ + twig({ + // ... + fragmentFilter: (fragment, template, data) => { + // your custom logic + return true // or false + } + }) + ] +}) + +``` + + +### 7. [New feature] __Multiple *script* tags are now supported within the same *.html* file and can live together with standard *HTML* code.__ + +```html + + + + + + + + + + + + + + + +``` \ No newline at end of file diff --git a/README.md b/README.md index a05b25a..9bfe51c 100644 --- a/README.md +++ b/README.md @@ -24,50 +24,94 @@ import twig from 'vite-plugin-twig' export default defineConfig({ // ... plugins: [ - twig() + twig({ /* ...options */ }) ] }) ``` ### Options -The plugin can be configured both via the *twig.config.js* file from the project root or by passing a configuration object directly as argument to the function above (in this last case, the configuration file will be ignored). +The plugin can be configured both directly with the options parameter shown above or via the dedicated *twig.config.(js|ts)* file, like following: + +```js +/* twig.config.js */ +import { defineConfig } from 'vite-plugin-twig' + +export default defineConfig({ + // ... +}) +``` + +> ℹ️ *defineConfig* is a bypass function with type hints, which means you can also omit it if you don't need the autocompletion/typecheck. Here below the list of the supported options. #### `cache` -__type__ `Boolean` +__type:__ `boolean` -__default__ `false` +__default:__ `false` If *true*, it enables internal *Twig*'s template caching. -#### `filters` -__type__ `{ [key: String]: (...args: Any[]) => Any }` +#### `extensions` +__type:__ `{ filters: TwigExtensions, functions: TwigExtensions }` + +__default:__ `undefined` + +A collection of custom filters and functions to extend *Twig*. Look at [*twig.js* documentation](https://github.com/twigjs/twig.js/wiki/Extending-twig.js) to learn more. + +#### `fileFilter` +__type:__ `(filename: string) => boolean` + +__default:__ `undefined` + +A custom filter to determine if the current transforming *.html* file should be processed/ignored or not (useful for improving compatibility with other plugins). -__default__ `{}` +Example: +```js +/* twig.config.js */ +import { defineConfig } from 'vite-plugin-twig' -A collection of custom filters to extend *Twig*. Look at [*twig.js* documentation](https://github.com/twigjs/twig.js/wiki/Extending-twig.js) to learn more. +export default defineConfig({ + // ... + fileFilter: filename => filename.endsWith('.twig.html') +}) +``` + +#### `fragmentFilter` +__type:__ `TwigFragmentFilter` -#### `functions` -__type__ `{ [key: String]: (...args: Any[]) => Any }` +__default:__ `undefined` -__default__ `{}` +A custom filter to determine if the current matched fragment should be processed/ignored or not (useful for improving compatibility with other plugins). -A collection of custom functions to extend *Twig*. Look at [*twig.js* documentation](https://github.com/twigjs/twig.js/wiki/Extending-twig.js) to learn more. +Example: +```js +/* twig.config.js */ +import { defineConfig } from 'vite-plugin-twig' + +export default defineConfig({ + // ... + fragmentFilter: (fragment, template, data) => { + return fragment.indexOf('data-engine="twig"') > -1 + // or template.endsWith('.twig') + // or data.engine === 'twig' + } +}) +``` #### `globals` -__type__ `{ [key: String]: Any }` +__type:__ `{ [key: string]: any }` -__default__ `{}` +__default:__ `undefined` The global variables to be injected in each template. #### `settings` -__type__ `{ [key: String]: Any }` +__type:__ `{ views: any, 'twig options': any }` -__default__ `{}` +__default:__ `undefined` -The *Twig* settings. Please refer to [*twig.js* documentation](https://github.com/twigjs/twig.js/wiki/) to learn more. +The *Twig* settings. Please refer to *twig.js* [documentation](https://github.com/twigjs/twig.js/wiki/) and [types](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/twig/index.d.ts) to learn more. ### Templates diff --git a/package.json b/package.json index 0a6120a..083138c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vite-plugin-twig", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "keywords": [ "vite-plugin", diff --git a/playground/vite.config.js b/playground/vite.config.js index a66dc96..e910d05 100644 --- a/playground/vite.config.js +++ b/playground/vite.config.js @@ -1,5 +1,5 @@ import { defineConfig } from 'vite' -import twig from '../src' +import twig from 'vite-plugin-twig' export default defineConfig({ plugins: [ diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..db0baf1 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,5 @@ +import type { PluginOptions } from '../types' + +export function defineConfig(options: PluginOptions) { + return options +} diff --git a/src/index.ts b/src/index.ts index 607628f..f8eab91 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -import { viteTwigPlugin } from './plugin' +export { defineConfig } from './config' +export { viteTwigPlugin as default } from './plugin' -export default viteTwigPlugin diff --git a/src/plugin.ts b/src/plugin.ts index 4368a6f..8a62761 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,18 +1,21 @@ -import path from 'path' +import path from 'node:path' import type { Plugin } from 'vite' -import type { TwigOptions } from '../types' -import { retrieveOptionsFromConfigFile, configureTwig, parseHTML } from './tasks' +import type { PluginOptions } from '../types' +import { configureTwig, parseHTML, retrieveConfigFromFile } from './tasks' -export function viteTwigPlugin(options: TwigOptions): Plugin { - const { cache, filters, functions, globals, settings } = options || retrieveOptionsFromConfigFile() +export async function viteTwigPlugin(options: PluginOptions): Promise { - configureTwig({ cache, filters, functions }) + const config = options || await retrieveConfigFromFile() + + configureTwig(config) return { name: 'vite-plugin-twig', transformIndexHtml: { enforce: 'pre', - transform: html => parseHTML(html, { globals, settings }) + transform: async (html, ctx) => { + return await parseHTML(html, ctx, config) + } }, handleHotUpdate({ file, server }) { if (path.extname(file) === '.twig') { diff --git a/src/tasks.ts b/src/tasks.ts index 4aee29e..55541c1 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -1,7 +1,8 @@ -import process from 'process' -import path from 'path' -import Twig, { RenderOptions } from 'twig' -import type { TwigOptions, TwigFragment } from '../types' +import path from 'node:path' +import process from 'node:process' +import Twig from 'twig' +import type { IndexHtmlTransformContext } from 'vite' +import type { PluginOptions, TwigFragment, TwigFragmentFilter } from '../types' const cwd = process.cwd() @@ -9,73 +10,80 @@ function warn(message: string) { console.log('\x1b[31m%s\x1b[0m', message) } -export function retrieveOptionsFromConfigFile(filename: string = 'twig.config.js'): TwigOptions { +export async function retrieveConfigFromFile(): Promise { try { - const config = path.resolve(cwd, filename) - return require(config) + const { default: config } = await import(`${cwd}/twig.config`) + return config } catch { - return {} + return } } -export function configureTwig(options: TwigOptions = {}) { - Twig.cache(options.cache || false) +export function configureTwig({ cache, extensions }: PluginOptions = {}) { + Twig.cache(cache || false) - if (options.filters) { + if (extensions?.filters) { Object - .entries(options.filters) + .entries(extensions.filters) .forEach(([key, fn]) => Twig.extendFilter(key, fn)) } - if (options.functions) { + if (extensions?.functions) { Object - .entries(options.functions) + .entries(extensions.functions) .forEach(([key, fn]) => Twig.extendFunction(key, fn)) } } -export async function parseHTML(html: string, options: TwigOptions) { - let output = html - const placeholders = retrieveTemplatePlaceholders(html) - - if (placeholders.length) { - const contents = await Promise.allSettled( - placeholders.map(({ template, data }) => { - const context = { ...options.globals, ...data, ...options.settings } - return renderTwigTemplate(template, context) - }) - ) - - contents.forEach((res, i) => { - if (res.status === 'fulfilled') { - output = output.replace(placeholders[i].placeholder, res.value) - } else { - warn(res.reason.message) - } - }) - } +export async function parseHTML(html: string, ctx: IndexHtmlTransformContext, { fileFilter, fragmentFilter, globals, settings }: PluginOptions = {}) { + const filename = ctx.path.replace(/^\/?/, '') + + if (typeof fileFilter === 'function' && !fileFilter(filename)) return html + + const placeholders = retrieveTemplatePlaceholders(html, fragmentFilter) + + if (!placeholders.length) return html - return output + const contents = await Promise.allSettled(placeholders.map(({ template, data }) => { + const filepath = path.join(cwd, settings?.views || '', template) + const context = { ...data, ...globals, settings: settings } + return renderTwigTemplate(filepath, context) + })) + + return contents.reduce((output, res, i) => { + if (res.status === 'fulfilled') { + output = output.replace(placeholders[i].placeholder, res.value) + } else { + warn(res.reason.message) + } + return output + }, html) } -export function retrieveTemplatePlaceholders(html: string): TwigFragment[] { +export function retrieveTemplatePlaceholders(html: string, fragmentFilter?: TwigFragmentFilter): TwigFragment[] { const matches = html.matchAll(/]*data-template="(?