From 2b5f3379f4614283129475f590c735e44cf4f770 Mon Sep 17 00:00:00 2001 From: Noel Forte Date: Wed, 30 Oct 2024 18:58:07 -0400 Subject: [PATCH] THE BIG TYPESCRIPT REFACTOR --- .changeset/red-stingrays-cross.md | 5 ++ eslint.config.js | 35 +++++------- jsconfig.json | 7 --- src/{engine.js => engine.ts} | 56 ++++++------------- src/{index.js => index.ts} | 54 ++++++++---------- ...reate-vento-tag.js => create-vento-tag.ts} | 30 +++++----- src/modules/ignore-tag.js | 15 ----- src/modules/ignore-tag.ts | 15 +++++ src/modules/{utils.js => utils.ts} | 5 +- tsconfig.json | 16 ++++++ tsup.config.ts | 9 +++ types/eleventy.d.ts | 33 +++++++++++ 12 files changed, 151 insertions(+), 129 deletions(-) create mode 100644 .changeset/red-stingrays-cross.md delete mode 100644 jsconfig.json rename src/{engine.js => engine.ts} (55%) rename src/{index.js => index.ts} (72%) rename src/modules/{create-vento-tag.js => create-vento-tag.ts} (60%) delete mode 100644 src/modules/ignore-tag.js create mode 100644 src/modules/ignore-tag.ts rename src/modules/{utils.js => utils.ts} (88%) create mode 100644 tsconfig.json create mode 100644 tsup.config.ts create mode 100644 types/eleventy.d.ts diff --git a/.changeset/red-stingrays-cross.md b/.changeset/red-stingrays-cross.md new file mode 100644 index 0000000..fa86e19 --- /dev/null +++ b/.changeset/red-stingrays-cross.md @@ -0,0 +1,5 @@ +--- +'eleventy-plugin-vento': minor +--- + +Refactor project as Typescript. (resolves #22) diff --git a/eslint.config.js b/eslint.config.js index e9498b0..c07d787 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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: '^_', @@ -56,15 +57,5 @@ export default [ eqeqeq: ['error', 'smart'], 'unicorn/prevent-abbreviations': 'off', }, - }, - - // Match all node-files - { - files: ['**/*.js'], - languageOptions: { - globals: { - ...globals.node, - }, - }, - }, -]; + } +); diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index fe65a14..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "baseUrl": "./", - } -} diff --git a/src/engine.js b/src/engine.ts similarity index 55% rename from src/engine.js rename to src/engine.ts index ea0a506..10b0a30 100644 --- a/src/engine.js +++ b/src/engine.ts @@ -1,36 +1,28 @@ /** * @file Function that handles creating the Vento environment. Exposes * a simple API for Eleventy to interface with. - * - * @typedef {{eleventy?: Record, page?: Record}} EleventyContext - * @typedef {EleventyContext & Record} EleventyData - * @typedef {(...args: unknown[]) => unknown} EleventyFunction - * @typedef {Record} 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; } @@ -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} 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} 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} 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); @@ -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); diff --git a/src/index.js b/src/index.ts similarity index 72% rename from src/index.js rename to src/index.ts index 790b8a7..60e76d3 100644 --- a/src/index.js +++ b/src/index.ts @@ -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} 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) { DEBUG.setup('Initializing eleventy-plugin-vento'); runCompatibilityCheck(eleventyConfig); - /** @type {VentoPluginOptions} */ - const options = { + const options: VentoPluginOptions = { // Define defaults autotrim: false, plugins: [], @@ -80,8 +71,7 @@ export function VentoPlugin(eleventyConfig, userOptions) { if (options.autotrim) { const defaults = ['@vento', '@11ty']; - /** @type {Set} */ - const tagSet = new Set(options.autotrim === true ? defaults : options.autotrim); + const tagSet: Set = new Set(options.autotrim === true ? defaults : options.autotrim); if (tagSet.has('@vento')) { tagSet.delete('@vento'); @@ -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); @@ -150,7 +140,7 @@ export function VentoPlugin(eleventyConfig, userOptions) { outputFileExtension: 'html', read: true, - compile(inputContent, inputPath) { + compile(inputContent: string, inputPath: string) { // Normalize input path inputPath = path.normalize(inputPath); @@ -158,12 +148,12 @@ export function VentoPlugin(eleventyConfig, userOptions) { 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 @@ -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; diff --git a/src/modules/create-vento-tag.js b/src/modules/create-vento-tag.ts similarity index 60% rename from src/modules/create-vento-tag.js rename to src/modules/create-vento-tag.ts index b93896d..d1e5069 100644 --- a/src/modules/create-vento-tag.js +++ b/src/modules/create-vento-tag.ts @@ -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` @@ -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(); } @@ -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`, }); } diff --git a/src/modules/ignore-tag.js b/src/modules/ignore-tag.js deleted file mode 100644 index c69d7c4..0000000 --- a/src/modules/ignore-tag.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @file Definition for tag that preserves vento syntax in output - */ - -/** @type {import('ventojs/src/environment.js').Tag} */ -function ignoreTag(_env, code, output, _tokens) { - if (!code.startsWith('!')) return; - const compiled = `{{${code.replace(/!(>?)\s+/, '$1 ')}}}`; - return `${output} += "${compiled}";`; -} - -/** @type {import('ventojs/src/environment.js').Plugin} */ -export function ignoreTagPlugin(env) { - env.tags.push(ignoreTag); -} diff --git a/src/modules/ignore-tag.ts b/src/modules/ignore-tag.ts new file mode 100644 index 0000000..2d7d8d1 --- /dev/null +++ b/src/modules/ignore-tag.ts @@ -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); +} diff --git a/src/modules/utils.js b/src/modules/utils.ts similarity index 88% rename from src/modules/utils.js rename to src/modules/utils.ts index f79e4ee..38bcafb 100644 --- a/src/modules/utils.js +++ b/src/modules/utils.ts @@ -1,6 +1,9 @@ // Set up debugger global +import type { UserConfig } from '@11ty/eleventy'; import createDebugger from 'debug'; + const debugBaseNamespace = 'Eleventy:Vento'; + export const DEBUG = { setup: createDebugger(`${debugBaseNamespace}:Setup`), cache: createDebugger(`${debugBaseNamespace}:Cache`), @@ -18,7 +21,7 @@ export const REQUIRED_API_METHODS = [ export const CONTEXT_DATA_KEYS = ['page', 'eleventy']; // Helper functions -export function runCompatibilityCheck(config) { +export function runCompatibilityCheck(config: UserConfig) { DEBUG.setup('Run compatibility check'); for (const [method, version] of REQUIRED_API_METHODS) { if (!config?.[method]) { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..344d2c2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["**/*.ts", "eslint.config.js"], + "exclude": ["**/node_modules/**", "dist"], + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "checkJs": true, + "target": "ES2021", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true + } +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..ab5d3aa --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/**'], + format: 'esm', + clean: true, + dts: true, + bundle: false, +}); diff --git a/types/eleventy.d.ts b/types/eleventy.d.ts new file mode 100644 index 0000000..f29d3f9 --- /dev/null +++ b/types/eleventy.d.ts @@ -0,0 +1,33 @@ +/** + * @file Minimum viable types for Eleventy used by this plugin's source code + */ + +declare module '@11ty/eleventy' { + type EleventyContext = { eleventy?: Record; page?: Record } & { + [K: string]: Record; + }; + + type EleventyData = EleventyContext & { [K: string]: Record }; + + type EleventyEventListener = (data: T) => void | Promise; + + type EleventyFunction = (...args: T[]) => unknown; + type EleventyFunctionSet = Record>; + + interface UserConfig { + // Metadata retrieval + directories: { includes: string }; + getFilters(): EleventyFunctionSet; + getShortcodes(): EleventyFunctionSet; + getPairedShortcodes(): EleventyFunctionSet; + + // Event listeners + on(event: string, listener: EleventyEventListener); + + // Extension registering + addTemplateFormats(extension: string): void; + addExtension(extension: string, extensionConfig: ExtensionConfig); + + [K: string]: unknown; + } +}