diff --git a/packages/knip/fixtures/plugins/eleventy/eleventy.config.cjs b/packages/knip/fixtures/plugins/eleventy/eleventy.config.cjs index b8824232e..24b2ea532 100644 --- a/packages/knip/fixtures/plugins/eleventy/eleventy.config.cjs +++ b/packages/knip/fixtures/plugins/eleventy/eleventy.config.cjs @@ -1,7 +1,4 @@ -module.exports = (_config) => { - return { - htmlTemplateEngine: 'njk', - markdownTemplateEngine: 'njk', - templateFormats: ['njk'], - }; +module.exports = { + htmlTemplateEngine: 'njk', + markdownTemplateEngine: 'njk', }; diff --git a/packages/knip/fixtures/plugins/eleventy/eleventy.config.dynamic.cjs b/packages/knip/fixtures/plugins/eleventy/eleventy.config.dynamic.cjs new file mode 100644 index 000000000..5c9ef3266 --- /dev/null +++ b/packages/knip/fixtures/plugins/eleventy/eleventy.config.dynamic.cjs @@ -0,0 +1,57 @@ +// https://github.com/11ty/eleventy-base-blog/blob/main/eleventy.config.js +module.exports = function (eleventyConfig) { + eleventyConfig.addPassthroughCopy({ + './public/': '/', + './node_modules/prismjs/themes/prism-okaidia.css': '/css/prism-okaidia.css', + }); + eleventyConfig.addWatchTarget('content/**/*.{svg,webp,png,jpeg}'); + eleventyConfig.addPlugin(); + eleventyConfig.addPlugin(); + eleventyConfig.addPlugin(); + eleventyConfig.addPlugin(undefined, { + preAttributes: { tabindex: 0 }, + }); + eleventyConfig.addPlugin(); + eleventyConfig.addPlugin(); + eleventyConfig.addPlugin(); + eleventyConfig.addFilter('readableDate', (dateObj, format, zone) => {}); + eleventyConfig.addFilter('htmlDateString', dateObj => {}); + eleventyConfig.addFilter('head', (array, n) => { + if (!Array.isArray(array) || array.length === 0) { + return []; + } + if (n < 0) { + return array.slice(n); + } + + return array.slice(0, n); + }); + eleventyConfig.addFilter('min', (...numbers) => { + return Math.min.apply(null, numbers); + }); + eleventyConfig.addFilter('getAllTags', collection => { + let tagSet = new Set(); + for (let item of collection) { + (item.data.tags || []).forEach(tag => tagSet.add(tag)); + } + return Array.from(tagSet); + }); + eleventyConfig.addFilter('filterTagList', function filterTagList(tags) { + return (tags || []).filter(tag => ['all', 'nav', 'post', 'posts'].indexOf(tag) === -1); + }); + eleventyConfig.amendLibrary('md', mdLib => { + eleventyConfig.getFilter('slugify'); + }); + return { + templateFormats: ['md', 'njk', 'html', 'liquid'], + markdownTemplateEngine: 'njk', + htmlTemplateEngine: 'njk', + dir: { + input: 'content', + includes: '../_includes', + data: '../_data', + output: '_site', + }, + pathPrefix: '/', + }; +}; diff --git a/packages/knip/src/plugins/eleventy/helpers.ts b/packages/knip/src/plugins/eleventy/helpers.ts new file mode 100644 index 000000000..7425652c5 --- /dev/null +++ b/packages/knip/src/plugins/eleventy/helpers.ts @@ -0,0 +1,153 @@ +import type { EleventyConfig } from './types.js'; + +// https://github.com/11ty/eleventy/blob/main/src/UserConfig.js +export class DummyEleventyConfig { + constructor() {} + _getUniqueId() {} + reset() {} + versionCheck() {} + on() {} + emit() {} + _enablePluginExecution() {} + addMarkdownHighlighter() {} + addLiquidTag() {} + addLiquidFilter() {} + addNunjucksAsyncFilter() {} + addNunjucksFilter() {} + addHandlebarsHelper() {} + addFilter() {} + addAsyncFilter() {} + getFilter() {} + addNunjucksTag() {} + addGlobalData() {} + addNunjucksGlobal() {} + addTransform() {} + addLinter() {} + addLayoutAlias() {} + setLayoutResolution() {} + enableLayoutResolution() {} + getCollections() {} + addCollection() {} + addPlugin() {} + _getPluginName() {} + _executePlugin() {} + getNamespacedName() {} + namespace() {} + addPassthroughCopy() {} + _normalizeTemplateFormats() {} + setTemplateFormats() {} + addTemplateFormats() {} + setLibrary() {} + amendLibrary() {} + setPugOptions() {} + setLiquidOptions() {} + setNunjucksEnvironmentOptions() {} + setNunjucksPrecompiledTemplates() {} + setEjsOptions() {} + setDynamicPermalinks() {} + setUseGitIgnore() {} + addShortcode() {} + addAsyncShortcode() {} + addNunjucksAsyncShortcode() {} + addNunjucksShortcode() {} + addLiquidShortcode() {} + addHandlebarsShortcode() {} + addPairedShortcode() {} + addPairedAsyncShortcode() {} + addPairedNunjucksAsyncShortcode() {} + addPairedNunjucksShortcode() {} + addPairedLiquidShortcode() {} + addPairedHandlebarsShortcode() {} + addJavaScriptFunction() {} + setDataDeepMerge() {} + isDataDeepMergeModified() {} + addWatchTarget() {} + setWatchJavaScriptDependencies() {} + setServerOptions() {} + setBrowserSyncConfig() {} + setChokidarConfig() {} + setWatchThrottleWaitTime() {} + setFrontMatterParsingOptions() {} + setQuietMode() {} + addExtension() {} + addDataExtension() {} + setUseTemplateCache() {} + setPrecompiledCollections() {} + setServerPassthroughCopyBehavior() {} + addUrlTransform() {} + setDataFileSuffixes() {} + setDataFileBaseName() {} + getMergingConfigObject() {} + + _uniqueId = {}; + events = {}; + benchmarkManager = {}; + benchmarks = {}; + collections = {}; + precompiledCollections = {}; + templateFormats = {}; + liquidOptions = {}; + liquidTags = {}; + liquidFilters = {}; + liquidShortcodes = {}; + liquidPairedShortcodes = {}; + nunjucksEnvironmentOptions = {}; + nunjucksPrecompiledTemplates = {}; + nunjucksFilters = {}; + nunjucksAsyncFilters = {}; + nunjucksTags = {}; + nunjucksGlobals = {}; + nunjucksShortcodes = {}; + nunjucksAsyncShortcodes = {}; + nunjucksPairedShortcodes = {}; + nunjucksAsyncPairedShortcodes = {}; + javascriptFunctions = {}; + markdownHighlighter = null; + libraryOverrides = {}; + passthroughCopies = {}; + layoutAliases = {}; + layoutResolution = true; + linters = {}; + transforms = {}; + activeNamespace = ''; + DateTime = {}; + dynamicPermalinks = true; + useGitIgnore = true; + ignores = new Set(); + watchIgnores = new Set(); + dataDeepMerge = true; + extensionMap = new Set(); + watchJavaScriptDependencies = true; + additionalWatchTargets = []; + serverOptions = {}; + globalData = {}; + chokidarConfig = {}; + watchThrottleWaitTime = 0; + dataExtensions = new Map(); + quietMode = false; + plugins = []; + _pluginExecution = false; + useTemplateCache = true; + dataFilterSelectors = new Set(); + libraryAmendments = {}; + serverPassthroughCopyBehavior = ''; + urlTransforms = []; + dataFileSuffixesOverride = false; + dataFileDirBaseNameOverride = false; + frontMatterParsingOptions = { + engines: {}, + }; + templateFormatsAdded = {}; +} + +// https://www.11ty.dev/docs/config/#configuration-options +export const defaultEleventyConfig: EleventyConfig = { + dir: { + input: '.', + output: '_site', + includes: '_includes', + layouts: '_includes', + data: '_data', + }, + templateFormats: '11ty.js', +}; diff --git a/packages/knip/src/plugins/eleventy/index.ts b/packages/knip/src/plugins/eleventy/index.ts index 7f5bb9576..09c16acba 100644 --- a/packages/knip/src/plugins/eleventy/index.ts +++ b/packages/knip/src/plugins/eleventy/index.ts @@ -1,6 +1,9 @@ +import { join } from '../../util/path.js'; import { timerify } from '../../util/Performance.js'; -import { hasDependency } from '../../util/plugin.js'; +import { hasDependency, load } from '../../util/plugin.js'; import { toEntryPattern, toProductionEntryPattern } from '../../util/protocols.js'; +import { DummyEleventyConfig, defaultEleventyConfig } from './helpers.js'; +import type { EleventyConfig } from './types.js'; import type { IsPluginEnabledCallback, GenericPluginCallback } from '../../types/plugins.js'; // https://www.11ty.dev/docs/ @@ -11,16 +14,37 @@ const ENABLERS = ['@11ty/eleventy']; const isEnabled: IsPluginEnabledCallback = ({ dependencies }) => hasDependency(dependencies, ENABLERS); -const ENTRY_FILE_PATTERNS = ['.eleventy.js', 'eleventy.config.{js,cjs}']; +const CONFIG_FILE_PATTERNS: string[] = ['.eleventy.js', 'eleventy.config.{js,cjs}']; -const PRODUCTION_ENTRY_FILE_PATTERNS = ['posts/**/*.11tydata.js', '_data/**/*.{js,cjs,mjs}']; +const ENTRY_FILE_PATTERNS: string[] = []; + +const PRODUCTION_ENTRY_FILE_PATTERNS: string[] = ['posts/**/*.11tydata.js', '_data/**/*.{js,cjs,mjs}']; + +const PROJECT_FILE_PATTERNS: string[] = []; const findEleventyDependencies: GenericPluginCallback = async (configFilePath, options) => { const { config } = options; - return config.entry - ? config.entry.map(toProductionEntryPattern) - : [...ENTRY_FILE_PATTERNS.map(toEntryPattern), ...PRODUCTION_ENTRY_FILE_PATTERNS.map(toProductionEntryPattern)]; + let localConfig = (await load(configFilePath)) as + | Partial + | ((arg: DummyEleventyConfig) => Promise>); + if (!localConfig) + return config.entry + ? config.entry.map(toProductionEntryPattern) + : PRODUCTION_ENTRY_FILE_PATTERNS.map(toProductionEntryPattern); + if (typeof localConfig === 'function') localConfig = await localConfig(new DummyEleventyConfig()); + + const inputDir = localConfig?.dir?.input || defaultEleventyConfig.dir.input; + const dataDir = localConfig?.dir?.data || defaultEleventyConfig.dir.data; + const templateFormats = localConfig.templateFormats || defaultEleventyConfig.templateFormats; + + return ( + config?.entry ?? [ + join(inputDir, dataDir, '**/*.js'), + join(inputDir, `**/*.{${typeof templateFormats === 'string' ? templateFormats : templateFormats.join(',')}}`), + join(inputDir, '**/*.11tydata.js'), + ] + ).map(toEntryPattern); }; const findDependencies = timerify(findEleventyDependencies); @@ -29,7 +53,9 @@ export default { NAME, ENABLERS, isEnabled, + CONFIG_FILE_PATTERNS, ENTRY_FILE_PATTERNS, PRODUCTION_ENTRY_FILE_PATTERNS, + PROJECT_FILE_PATTERNS, findDependencies, }; diff --git a/packages/knip/src/plugins/eleventy/types.ts b/packages/knip/src/plugins/eleventy/types.ts new file mode 100644 index 000000000..6d109f3b6 --- /dev/null +++ b/packages/knip/src/plugins/eleventy/types.ts @@ -0,0 +1,10 @@ +export type EleventyConfig = { + dir: { + input: string; + output: string; + includes: string; + layouts: string; + data: string; + }; + templateFormats: string | string[]; +}; diff --git a/packages/knip/test/plugins/eleventy.test.ts b/packages/knip/test/plugins/eleventy.test.ts index fe6302b6e..ae76d3ca7 100644 --- a/packages/knip/test/plugins/eleventy.test.ts +++ b/packages/knip/test/plugins/eleventy.test.ts @@ -1,21 +1,24 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { main } from '../../src/index.js'; -import { resolve } from '../../src/util/path.js'; -import baseArguments from '../helpers/baseArguments.js'; -import baseCounters from '../helpers/baseCounters.js'; +import { default as eleventy } from '../../src/plugins/eleventy/index.js'; +import { resolve, join } from '../../src/util/path.js'; +import { getManifest, pluginConfig as config } from '../helpers/index.js'; const cwd = resolve('fixtures/plugins/eleventy'); +const manifest = getManifest(cwd); -test('Find dependencies in Eleventy configuration', async () => { - const { counters } = await main({ - ...baseArguments, - cwd, - }); +test('Find dependencies in Eleventy configuration (static)', async () => { + const configFilePath = join(cwd, 'eleventy.config.cjs'); + const dependencies = await eleventy.findDependencies(configFilePath, { manifest, config }); + assert.deepEqual(dependencies, ['entry:_data/**/*.js', 'entry:**/*.{11ty.js}', 'entry:**/*.11tydata.js']); +}); - assert.deepEqual(counters, { - ...baseCounters, - processed: 2, - total: 2, - }); +test('Find dependencies in Eleventy configuration (dynamic)', async () => { + const configFilePath = join(cwd, 'eleventy.config.dynamic.cjs'); + const dependencies = await eleventy.findDependencies(configFilePath, { manifest, config }); + assert.deepEqual(dependencies, [ + 'entry:_data/**/*.js', + 'entry:content/**/*.{md,njk,html,liquid}', + 'entry:content/**/*.11tydata.js', + ]); });