Skip to content

Commit

Permalink
THE BIG TYPESCRIPT REFACTOR
Browse files Browse the repository at this point in the history
  • Loading branch information
noelforte committed Oct 31, 2024
1 parent 78e9748 commit 2b5f337
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 129 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-stingrays-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eleventy-plugin-vento': minor
---

Refactor project as Typescript. (resolves #22)
35 changes: 13 additions & 22 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import js from '@eslint/js';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';

// Configs
import gitignore from 'eslint-config-flat-gitignore';
import eslintConfigPrettier from 'eslint-config-prettier';

// Plugins
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
import globals from 'globals';

export default [
// Set ignores from gitignore
gitignore(),

export default tseslint.config(
// Import configuration presets
js.configs.recommended,
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintPluginUnicorn.configs['flat/recommended'],
eslintConfigPrettier,

// Configure ignores
{
ignores: ['**/node_modules/**'],
ignores: ['**/node_modules/**', 'dist'],
},

// Configure defaults
{
languageOptions: {
ecmaVersion: 'latest',
globals: {
...globals.node,
},
},
rules: {
'no-unused-vars': [
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
Expand Down Expand Up @@ -56,15 +57,5 @@ export default [
eqeqeq: ['error', 'smart'],
'unicorn/prevent-abbreviations': 'off',
},
},

// Match all node-files
{
files: ['**/*.js'],
languageOptions: {
globals: {
...globals.node,
},
},
},
];
}
);
7 changes: 0 additions & 7 deletions jsconfig.json

This file was deleted.

56 changes: 17 additions & 39 deletions src/engine.js → src/engine.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
/**
* @file Function that handles creating the Vento environment. Exposes
* a simple API for Eleventy to interface with.
*
* @typedef {{eleventy?: Record<string, unknown>, page?: Record<string, unknown>}} EleventyContext
* @typedef {EleventyContext & Record<string, unknown>} EleventyData
* @typedef {(...args: unknown[]) => unknown} EleventyFunction
* @typedef {Record<string, EleventyFunction>} EleventyFunctionSet
* @typedef {import('ventojs/src/environment.js').Environment} VentoEnvironment
* @typedef {VentoEnvironment & {
* utils: {
* _11tyFns: { shortcodes: EleventyFunctionSet, pairedShortcodes: EleventyFunctionSet }
* _11tyCtx: EleventyContext
* }
* }} EleventyVentoEnvironment
*/

// External library
import ventojs from 'ventojs';
import { default as ventojs, type Options } from 'ventojs';
import type { Plugin, Environment, Template } from 'ventojs/src/environment.js';
import type { EleventyContext, EleventyFunctionSet, EleventyData } from '@11ty/eleventy';

// Internal modules
import { createVentoTag } from './modules/create-vento-tag.js';
import { CONTEXT_DATA_KEYS, DEBUG } from './modules/utils.js';

/** @param {import('ventojs').Options} options */
export function createVentoEngine(options) {
/** @type {EleventyVentoEnvironment} */
const env = ventojs(options);
interface EleventyUtils {
_11tyFns: { shortcodes: EleventyFunctionSet; pairedShortcodes: EleventyFunctionSet };
_11tyCtx: EleventyContext;
}

export function createVentoEngine(options: Options) {
const env = ventojs(options) as Environment & { utils: EleventyUtils };
env.utils._11tyFns = { shortcodes: {}, pairedShortcodes: {} };
env.utils._11tyCtx = {};

/** @param {EleventyData} newContext */
function setContext(newContext) {
function setContext(newContext: EleventyData) {
if (env.utils._11tyCtx?.page?.inputPath === newContext?.page?.inputPath) {
return;
}
Expand All @@ -42,42 +34,33 @@ export function createVentoEngine(options) {
DEBUG.setup('Reload context, new context is: %o', env.utils._11tyCtx);
}

/** @param {import('ventojs/src/environment.js').Plugin[]} plugins */
function loadPlugins(plugins) {
function loadPlugins(plugins: Plugin[]) {
for (const plugin of plugins) {
env.use(plugin);
}
}

/** @param {Record<string, EleventyFunction>} filters */
function loadFilters(filters) {
function loadFilters(filters: EleventyFunctionSet) {
for (const [name, fn] of Object.entries(filters)) {
env.filters[name] = fn.bind(env.utils._11tyCtx);
}
}

/** @param {Record<string, EleventyFunction>} shortcodes */
function loadShortcodes(shortcodes) {
function loadShortcodes(shortcodes: EleventyFunctionSet) {
for (const [name, fn] of Object.entries(shortcodes)) {
env.utils._11tyFns.shortcodes[name] = fn;
env.tags.push(createVentoTag({ name, group: 'shortcodes' }));
}
}

/** @param {Record<string, EleventyFunction>} pairedShortcodes */
function loadPairedShortcodes(pairedShortcodes) {
function loadPairedShortcodes(pairedShortcodes: EleventyFunctionSet) {
for (const [name, fn] of Object.entries(pairedShortcodes)) {
env.utils._11tyFns.pairedShortcodes[name] = fn;
env.tags.push(createVentoTag({ name, group: 'pairedShortcodes' }));
}
}

/**
* @param {string} source
* @param {string} file
* @param {boolean} [useVentoCache=true]
*/
function getTemplateFunction(source, file, useVentoCache = true) {
function getTemplateFunction(source: string, file: string, useVentoCache: boolean = true) {
// Attempt to retrieve template function from cache
let template = env.cache.get(file);

Expand All @@ -99,12 +82,7 @@ export function createVentoEngine(options) {
return template;
}

/**
* @param {import('ventojs/src/environment.js').Template} template
* @param {EleventyData} data
* @param {string} from
*/
async function render(template, data, from) {
async function render(template: Template, data: EleventyData, from: string) {
// Load new context
setContext(data);

Expand Down
54 changes: 22 additions & 32 deletions src/index.js → src/index.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,37 @@
/**
* @file Main plugin declaration
*
* @typedef VentoPluginOptions
* @prop {import('ventojs/src/environment.js').Plugin[]} plugins
* Array of vento plugins to use when compiling templates
* @prop {boolean|string[]} autotrim
* Enable Vento's [`autoTrim`](https://vento.js.org/plugins/auto-trim/)
* plugin to remove whitespace from tags in output
* @prop {boolean} [shortcodes=true]
* Create vento tags for Eleventy [Shortcodes](https://www.11ty.dev/docs/shortcodes/)
* @prop {boolean} [pairedShortcodes=true]
* Create vento tags for Eleventy [Paired Shortcodes](https://www.11ty.dev/docs/shortcodes/#paired-shortcodes)
* @prop {boolean} [filters=true]
* Create vento filters for Eleventy [Filters](https://www.11ty.dev/docs/filters/)
* @prop {boolean} [ignoreTag=false]
* Enables/disables tag ignore (`{{! ... }}`) syntax in templates
* @prop {import('ventojs').Options} ventoOptions
* Options to pass on to the `ventojs` engine.
* (See [Vento Docs](https://vento.js.org/configuration/#options))
*/

// Built-ins
import path from 'node:path';

// External modules
import type { UserConfig, EleventyData } from '@11ty/eleventy';
import type { Options } from 'ventojs';
import autotrimPlugin, { defaultTags as autotrimDefaultTags } from 'ventojs/plugins/auto_trim.js';
import type { Plugin } from 'ventojs/src/environment.js';

// Local modules
import { createVentoEngine } from './engine.js';
import { ignoreTagPlugin } from './modules/ignore-tag.js';
import { DEBUG, runCompatibilityCheck } from './modules/utils.js';

/**
* @param {import('@11ty/eleventy').UserConfig} eleventyConfig
* @param {Partial<VentoPluginOptions>} userOptions
*/
export function VentoPlugin(eleventyConfig, userOptions) {
export interface VentoPluginOptions {
plugins: Plugin[];
autotrim: boolean | string[];
filters: boolean;
shortcodes: boolean;
pairedShortcodes: boolean;
/** @deprecated */
ignoreTag: boolean;
ventoOptions: Options;
}

export function VentoPlugin(eleventyConfig: UserConfig, userOptions: Partial<VentoPluginOptions>) {
DEBUG.setup('Initializing eleventy-plugin-vento');
runCompatibilityCheck(eleventyConfig);

/** @type {VentoPluginOptions} */
const options = {
const options: VentoPluginOptions = {
// Define defaults
autotrim: false,
plugins: [],
Expand Down Expand Up @@ -80,8 +71,7 @@ export function VentoPlugin(eleventyConfig, userOptions) {
if (options.autotrim) {
const defaults = ['@vento', '@11ty'];

/** @type {Set<string>} */
const tagSet = new Set(options.autotrim === true ? defaults : options.autotrim);
const tagSet: Set<string> = new Set(options.autotrim === true ? defaults : options.autotrim);

if (tagSet.has('@vento')) {
tagSet.delete('@vento');
Expand Down Expand Up @@ -132,7 +122,7 @@ export function VentoPlugin(eleventyConfig, userOptions) {

// Handle emptying the cache when files are updated
DEBUG.setup('Registering Vento cache handler on eleventy.beforeWatch event');
eleventyConfig.on('eleventy.beforeWatch', async (updatedFiles) => {
eleventyConfig.on('eleventy.beforeWatch', async (updatedFiles: string[]) => {
for (let file of updatedFiles) {
file = path.normalize(file);
DEBUG.cache('Delete cache entry for %s', file);
Expand All @@ -150,20 +140,20 @@ export function VentoPlugin(eleventyConfig, userOptions) {
outputFileExtension: 'html',
read: true,

compile(inputContent, inputPath) {
compile(inputContent: string, inputPath: string) {
// Normalize input path
inputPath = path.normalize(inputPath);

// Retrieve the template function
const template = engine.getTemplateFunction(inputContent, inputPath, false);

// Return a render function
return async (data) => await engine.render(template, data, inputPath);
return async (data: EleventyData) => await engine.render(template, data, inputPath);
},

compileOptions: {
// Custom permalink compilation
permalink(permalinkContent, inputPath) {
permalink(permalinkContent: string, inputPath: string) {
// Short circuit if input isn't a string and doesn't look like a vento template
if (typeof permalinkContent === 'string' && /\{\{\s+.+\s+\}\}/.test(permalinkContent)) {
// Normalize input path
Expand All @@ -173,7 +163,7 @@ export function VentoPlugin(eleventyConfig, userOptions) {
const template = engine.getTemplateFunction(permalinkContent, inputPath);

// Return a render function
return async (data) => await engine.render(template, data, inputPath);
return async (data: EleventyData) => await engine.render(template, data, inputPath);
}

return permalinkContent;
Expand Down
30 changes: 17 additions & 13 deletions src/modules/create-vento-tag.js → src/modules/create-vento-tag.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
/**
* @file Helper function that creates vento tags from eleventy functions
*
* @param {{name: string, group: 'shortcodes' | 'pairedShortcodes' }} options
*/

export function createVentoTag(options) {
const IS_PAIRED = options.group === 'pairedShortcodes';
import type { Tag } from 'ventojs/src/environment.js';

/** @type {import("ventojs/src/environment.js").Tag} */
const tag = (env, code, output, tokens) => {
if (!code.startsWith(options.name)) return;
interface TagSpec {
name: string;
group: 'shortcodes' | 'pairedShortcodes';
}

export function createVentoTag(spec: TagSpec) {
const IS_PAIRED = spec.group === 'pairedShortcodes';

const tag: Tag = (env, code, output, tokens) => {
if (!code.startsWith(spec.name)) return;

// Declare helper variables for repeated strings in template
const fn = `__env.utils._11tyFns.${options.group}.${options.name}`;
const fn = `__env.utils._11tyFns.${spec.group}.${spec.name}`;
const ctx = '__env.utils._11tyCtx';
const args = [code.replace(options.name, '').trim()];
const args = [code.replace(spec.name, '').trim()];

const varname = output.startsWith('__shortcode_content')
? `${output}_precomp`
Expand All @@ -28,10 +32,10 @@ export function createVentoTag(options) {
compiled.push(
'{',
`let ${varname} = "";`,
...env.compileTokens(tokens, varname, [`/${options.name}`])
...env.compileTokens(tokens, varname, [`/${spec.name}`])
);
if (tokens.length > 0 && (tokens[0][0] !== 'tag' || tokens[0][1] !== `/${options.name}`)) {
throw new Error(`Vento: Missing closing tag for ${options.name} tag: ${code}`);
if (tokens.length > 0 && (tokens[0][0] !== 'tag' || tokens[0][1] !== `/${spec.name}`)) {
throw new Error(`Vento: Missing closing tag for ${spec.name} tag: ${code}`);
}
tokens.shift();
}
Expand All @@ -55,6 +59,6 @@ export function createVentoTag(options) {
};

return Object.defineProperty(tag, 'name', {
value: options.name + IS_PAIRED ? `PairedTag` : `Tag`,
value: spec.name + IS_PAIRED ? `PairedTag` : `Tag`,
});
}
15 changes: 0 additions & 15 deletions src/modules/ignore-tag.js

This file was deleted.

15 changes: 15 additions & 0 deletions src/modules/ignore-tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* @file Definition for tag that preserves vento syntax in output
*/

import type { Environment, Tag } from 'ventojs/src/environment.js';

const tag: Tag = (_env, code, output, _tokens) => {
if (!code.startsWith('!')) return;
const compiled = `{{${code.replace(/!(>?)\s+/, '$1 ')}}}`;
return `${output} += "${compiled}";`;
};

export function ignoreTagPlugin(env: Environment) {
env.tags.push(tag);
}
Loading

0 comments on commit 2b5f337

Please sign in to comment.